1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
//! This parses the output of npm-audit
//!
//! [npm-audit](https://docs.npmjs.com/cli/v7/commands/npm-audit)

use std::collections::BTreeMap;
use std::process::Command;
use std::str::from_utf8;
use tracing::{debug, warn};

/// Outer structure for parsing npm-audit output
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NpmAuditData {
    /// version of the audit report
    audit_report_version: u32,
    /// Vulnerabilities found in dependencies
    vulnerabilities: BTreeMap<String, VulnerablePackage>,
    /// vulnerability and dependency counts
    metadata: Metadata,
}

/// Severity of vulnerabilities
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Severity {
    /// no need to take action
    None,
    /// just informational
    Info,
    /// low severity
    Low,
    /// moderate severity
    Moderate,
    /// high severity
    High,
    /// critical severity
    Critical,
}

/// The details for a single vulnerable package
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VulnerablePackage {
    /// Package name
    name: String,
    /// The severity of the vulnerabilities
    severity: Severity,
    /// is this a direct dependency
    is_direct: bool,
    /// the vulnerabilities that make this a vulnerable package
    via: Vec<Vulnerability>,
    /// not sure what htis means
    effects: Vec<String>,
    /// affected version range
    range: String,
    /// not sure what this means
    nodes: Vec<String>,
    /// is there a fix available
    fix_available: Fix,
}

/// a single vulnerability
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase", untagged)]
pub enum Vulnerability {
    /// some vulnerabilities in the via list are only a name
    NameOnly(String),
    /// and some contain full details
    Full {
        /// numeric id, not sure what it means
        source: u64,
        /// the name of the vulnerability, or if none exists the vulnerable package
        name: String,
        /// the name of the dependency which is vulnerable
        dependency: String,
        /// the human readable title of the vulnerability
        title: String,
        /// an URL explaining the vulnerability
        url: String,
        /// the severity of this vulnerability
        severity: Severity,
        /// the affected version range
        range: String,
    },
}

/// a single fix
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum Fix {
    /// some packages only indicate whether a fix is available or not
    BoolOnly(bool),
    /// others provide more details
    #[serde(rename_all = "camelCase")]
    Full {
        /// the fixed package name
        name: String,
        /// the fixed package version
        version: String,
        /// is this a semver major update
        is_sem_ver_major: bool,
    },
}

/// The vulnerability and dependency counts returned by npm-audit
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Metadata {
    /// Vulnerability counts
    vulnerabilities: VulnerabilityCounts,
    /// Dependency counts
    dependencies: DependencyCounts,
}

/// The vulnerability and dependency counts returned by npm-audit
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct VulnerabilityCounts {
    /// Number of total vulnerabilities
    total: u32,
    /// Number of info level vulnerabilities
    info: u32,
    /// Number of low level vulnerabilities
    low: u32,
    /// Number of moderate level vulnerabilities
    moderate: u32,
    /// Number of high level vulnerabilities
    high: u32,
    /// Number of critical level vulnerabilities
    critical: u32,
}

/// The vulnerability and dependency counts returned by npm-audit
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DependencyCounts {
    /// Total number of dependencies
    total: u32,
    /// Number of production dependencies
    prod: u32,
    /// Number of development dependencies
    dev: u32,
    /// Number of optional dependencies
    optional: u32,
    /// Number of peer dependencies
    ///
    /// see <https://nodejs.org/es/blog/npm/peer-dependencies/>
    peer: u32,
    /// Number of optional peer dependencies
    peer_optional: u32,
}

/// What the exit code indicated about required updates
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum IndicatedUpdateRequirement {
    /// No update is required
    UpToDate,
    /// An update is required
    UpdateRequired,
}

impl std::fmt::Display for IndicatedUpdateRequirement {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            IndicatedUpdateRequirement::UpToDate => {
                write!(f, "up-to-date")
            }
            IndicatedUpdateRequirement::UpdateRequired => {
                write!(f, "update-required")
            }
        }
    }
}

/// main entry point for the npm-audit call
pub fn audit() -> Result<(IndicatedUpdateRequirement, NpmAuditData), crate::Error> {
    let mut cmd = Command::new("npm");

    cmd.args(["audit", "--json"]);

    let output = cmd.output()?;

    if !output.status.success() {
        warn!(
            "npm audit did not return with a successful exit code: {}",
            output.status
        );
        debug!("stdout:\n{}", from_utf8(&output.stdout)?);
        if !output.stderr.is_empty() {
            warn!("stderr:\n{}", from_utf8(&output.stderr)?);
        }
    }

    let update_requirement = if output.status.success() {
        IndicatedUpdateRequirement::UpdateRequired
    } else {
        IndicatedUpdateRequirement::UpToDate
    };

    let json_str = from_utf8(&output.stdout)?;
    let jd = &mut serde_json::Deserializer::from_str(json_str);
    let data: NpmAuditData = serde_path_to_error::deserialize(jd)?;
    Ok((update_requirement, data))
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::Error;
    use tracing_test::traced_test;

    /// this test requires a package.json and package-lock.json in the main crate
    /// directory (working dir of the tests)
    #[traced_test]
    #[test]
    fn test_run_npm_audit() -> Result<(), Error> {
        audit()?;
        Ok(())
    }
}