1use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7use std::process::Command;
8use std::str::from_utf8;
9use tracing::{debug, warn};
10
11#[derive(Debug, serde::Serialize, serde::Deserialize)]
16#[serde(rename_all = "camelCase", untagged)]
17pub enum NpmAuditData {
18 Version1(NpmAuditDataV1),
20 Version2(NpmAuditDataV2),
22}
23
24#[derive(Debug, serde::Serialize, serde::Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct NpmAuditDataV1 {
28 pub run_id: Option<String>,
32 pub actions: Vec<Action>,
34 pub advisories: BTreeMap<String, Advisory>,
36 pub muted: Option<Vec<String>>,
40 pub metadata: MetadataV1,
42}
43
44pub fn deserialize_module_path<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
46where
47 D: serde::Deserializer<'de>,
48{
49 let s = String::deserialize(deserializer)?;
50
51 Ok(s.split('>').map(|s| s.to_string()).collect())
52}
53
54pub fn serialize_module_path<S>(xs: &[String], serializer: S) -> Result<S::Ok, S::Error>
56where
57 S: serde::Serializer,
58{
59 let s = xs.join(">");
60
61 s.serialize(serializer)
62}
63
64pub fn deserialize_module_path_vec<'de, D>(deserializer: D) -> Result<Vec<Vec<String>>, D::Error>
66where
67 D: serde::Deserializer<'de>,
68{
69 let xs = <Vec<String>>::deserialize(deserializer)?;
70
71 Ok(xs
72 .into_iter()
73 .map(|x| x.split('>').map(|s| s.to_string()).collect())
74 .collect())
75}
76
77pub fn serialize_module_path_vec<S>(xxs: &[Vec<String>], serializer: S) -> Result<S::Ok, S::Error>
79where
80 S: serde::Serializer,
81{
82 let v: Vec<String> = xxs.iter().map(|xs| xs.join(">")).collect();
83
84 v.serialize(serializer)
85}
86
87pub fn deserialize_rfc3339<'de, D>(deserializer: D) -> Result<time::OffsetDateTime, D::Error>
90where
91 D: serde::Deserializer<'de>,
92{
93 let s = String::deserialize(deserializer)?;
94
95 time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
96 .map_err(serde::de::Error::custom)
97}
98
99pub fn serialize_rfc3339<S>(t: &time::OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
102where
103 S: serde::Serializer,
104{
105 let s = t
106 .format(&time::format_description::well_known::Rfc3339)
107 .map_err(serde::ser::Error::custom)?;
108
109 s.serialize(serializer)
110}
111
112pub fn deserialize_optional_rfc3339<'de, D>(
115 deserializer: D,
116) -> Result<Option<time::OffsetDateTime>, D::Error>
117where
118 D: serde::Deserializer<'de>,
119{
120 let s = <Option<String> as Deserialize<'de>>::deserialize(deserializer)?;
121
122 if let Some(s) = s {
123 Ok(Some(
124 time::OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339)
125 .map_err(serde::de::Error::custom)?,
126 ))
127 } else {
128 Ok(None)
129 }
130}
131
132pub fn serialize_optional_rfc3339<S>(
135 t: &Option<time::OffsetDateTime>,
136 serializer: S,
137) -> Result<S::Ok, S::Error>
138where
139 S: serde::Serializer,
140{
141 if let Some(t) = t {
142 let s = t
143 .format(&time::format_description::well_known::Rfc3339)
144 .map_err(serde::ser::Error::custom)?;
145
146 s.serialize(serializer)
147 } else {
148 let n: Option<String> = None;
149 n.serialize(serializer)
150 }
151}
152
153#[derive(Debug, serde::Serialize, serde::Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct Advisory {
160 pub id: u64,
162 pub title: String,
164 pub findings: Vec<Finding>,
167 pub vulnerable_versions: Option<String>,
169 pub module_name: Option<String>,
171 pub severity: Severity,
173 pub github_advisory_id: Option<String>,
175 pub cves: Option<Vec<String>>,
177 pub access: String,
179 pub patched_versions: Option<String>,
181 pub recommendation: String,
183 pub cwe: Option<Vec<String>>,
185 pub found_by: Option<String>,
187 pub reported_by: Option<String>,
189 #[serde(
191 serialize_with = "serialize_rfc3339",
192 deserialize_with = "deserialize_rfc3339"
193 )]
194 pub created: time::OffsetDateTime,
195 #[serde(
197 serialize_with = "serialize_optional_rfc3339",
198 deserialize_with = "deserialize_optional_rfc3339"
199 )]
200 pub updated: Option<time::OffsetDateTime>,
201 #[serde(
203 serialize_with = "serialize_optional_rfc3339",
204 deserialize_with = "deserialize_optional_rfc3339"
205 )]
206 pub deleted: Option<time::OffsetDateTime>,
207 pub references: Option<String>,
209 pub npm_advisory_id: Option<String>,
211 pub overview: String,
213 pub url: String,
215}
216
217#[derive(Debug, serde::Serialize, serde::Deserialize)]
219#[serde(rename_all = "camelCase")]
220pub struct Finding {
221 version: String,
223 #[serde(
225 serialize_with = "serialize_module_path_vec",
226 deserialize_with = "deserialize_module_path_vec"
227 )]
228 paths: Vec<Vec<String>>,
229}
230
231#[derive(Debug, serde::Serialize, serde::Deserialize)]
233#[serde(rename_all = "camelCase")]
234pub struct NpmAuditDataV2 {
235 pub audit_report_version: Option<u32>,
239 pub vulnerabilities: BTreeMap<String, VulnerablePackage>,
241 pub metadata: MetadataV2,
243}
244
245#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize)]
247#[serde(rename_all = "camelCase", tag = "action")]
248pub enum Action {
249 #[serde(rename_all = "camelCase")]
251 Install {
252 resolves: Vec<Resolves>,
254 module: String,
256 depth: Option<u32>,
258 target: String,
260 is_major: bool,
262 },
263 #[serde(rename_all = "camelCase")]
265 Update {
266 resolves: Vec<Resolves>,
268 module: String,
270 depth: Option<u32>,
272 target: String,
274 },
275 #[serde(rename_all = "camelCase")]
277 Review {
278 resolves: Vec<Resolves>,
280 module: String,
282 depth: Option<u32>,
284 },
285}
286
287#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize)]
289#[serde(rename_all = "camelCase")]
290pub struct Resolves {
291 pub id: u64,
293 #[serde(
295 serialize_with = "serialize_module_path",
296 deserialize_with = "deserialize_module_path"
297 )]
298 pub path: Vec<String>,
299 pub dev: bool,
301 pub optional: bool,
303 pub bundled: bool,
305}
306
307#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize)]
309#[serde(rename_all = "camelCase")]
310pub enum Severity {
311 None,
313 Info,
315 Low,
317 Moderate,
319 High,
321 Critical,
323}
324
325#[derive(Debug, serde::Serialize, serde::Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct VulnerablePackage {
329 pub name: String,
331 pub severity: Severity,
333 pub is_direct: bool,
335 pub via: Vec<Vulnerability>,
337 pub effects: Vec<String>,
339 pub range: String,
341 pub nodes: Vec<String>,
343 pub fix_available: Fix,
345}
346
347#[derive(Debug, serde::Serialize, serde::Deserialize)]
349#[serde(rename_all = "camelCase", untagged)]
350pub enum Vulnerability {
351 NameOnly(String),
353 Full {
355 source: u64,
357 name: String,
359 dependency: String,
361 title: String,
363 url: String,
365 severity: Severity,
367 range: String,
369 },
370}
371
372#[derive(Debug, serde::Serialize, serde::Deserialize)]
374#[serde(untagged)]
375pub enum Fix {
376 BoolOnly(bool),
378 #[serde(rename_all = "camelCase")]
380 Full {
381 name: String,
383 version: String,
385 is_sem_ver_major: bool,
387 },
388}
389
390#[derive(Debug, serde::Serialize, serde::Deserialize)]
393#[serde(rename_all = "camelCase")]
394pub struct MetadataV1 {
395 pub vulnerabilities: VulnerabilityCountsV1,
397 pub dependencies: u32,
399 pub dev_dependencies: u32,
401 pub optional_dependencies: u32,
403 pub total_dependencies: u32,
405}
406
407#[derive(Debug, serde::Serialize, serde::Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct MetadataV2 {
412 pub vulnerabilities: VulnerabilityCountsV2,
414 pub dependencies: DependencyCounts,
416}
417
418#[derive(Debug, serde::Serialize, serde::Deserialize)]
421pub struct VulnerabilityCountsV1 {
422 pub info: u32,
424 pub low: u32,
426 pub moderate: u32,
428 pub high: u32,
430 pub critical: u32,
432}
433
434#[derive(Debug, serde::Serialize, serde::Deserialize)]
437pub struct VulnerabilityCountsV2 {
438 pub total: u32,
440 pub info: u32,
442 pub low: u32,
444 pub moderate: u32,
446 pub high: u32,
448 pub critical: u32,
450}
451
452#[derive(Debug, serde::Serialize, serde::Deserialize)]
454#[serde(rename_all = "camelCase")]
455pub struct DependencyCounts {
456 pub total: u32,
458 pub prod: u32,
460 pub dev: u32,
462 pub optional: u32,
464 pub peer: u32,
468 pub peer_optional: u32,
470}
471
472#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
474pub enum IndicatedUpdateRequirement {
475 UpToDate,
477 UpdateRequired,
479}
480
481impl std::fmt::Display for IndicatedUpdateRequirement {
482 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 match self {
484 IndicatedUpdateRequirement::UpToDate => {
485 write!(f, "up-to-date")
486 }
487 IndicatedUpdateRequirement::UpdateRequired => {
488 write!(f, "update-required")
489 }
490 }
491 }
492}
493
494pub fn audit() -> Result<(IndicatedUpdateRequirement, NpmAuditData), crate::Error> {
496 let mut version_cmd = Command::new("npm");
497
498 version_cmd.args(["--version"]);
499
500 let version_output = version_cmd.output()?;
501
502 let version = from_utf8(&version_output.stdout)?.trim();
503
504 debug!("Got version string {} from npm --version", version);
505
506 let report_format = match versions::Versioning::new(version) {
507 Some(version) => {
508 debug!("Got version {} from npm --version", version);
509 let audit_report_change = versions::Versioning::new("7.0.0").unwrap();
510 if version < audit_report_change {
511 debug!(
512 "Dealing with npm before version {}, using report format 1",
513 audit_report_change
514 );
515 1
516 } else {
517 debug!(
518 "Dealing with npm version {} or above, using report format 2",
519 audit_report_change
520 );
521 2
522 }
523 }
524 None => {
525 debug!("Could not parse npm version, defaulting to report format 2");
529 2
530 }
531 };
532 debug!("Using report format {}", report_format);
533
534 let mut cmd = Command::new("npm");
535
536 cmd.args(["audit", "--json"]);
537
538 let output = cmd.output()?;
539
540 if !output.status.success() {
541 warn!(
542 "npm audit did not return with a successful exit code: {}",
543 output.status
544 );
545 debug!("stdout:\n{}", from_utf8(&output.stdout)?);
546 if !output.stderr.is_empty() {
547 warn!("stderr:\n{}", from_utf8(&output.stderr)?);
548 }
549 }
550
551 let update_requirement = if output.status.success() {
552 IndicatedUpdateRequirement::UpToDate
553 } else {
554 IndicatedUpdateRequirement::UpdateRequired
555 };
556
557 let json_str = from_utf8(&output.stdout)?;
558 let jd = &mut serde_json::Deserializer::from_str(json_str);
559 let data: NpmAuditData = match report_format {
560 1 => NpmAuditData::Version1(serde_path_to_error::deserialize::<_, NpmAuditDataV1>(jd)?),
561 2 => NpmAuditData::Version2(serde_path_to_error::deserialize::<_, NpmAuditDataV2>(jd)?),
562 _ => {
563 panic!("Unknown report version")
564 }
565 };
566 Ok((update_requirement, data))
567}
568
569#[cfg(test)]
570mod test {
571 use super::*;
572 use crate::Error;
573 use tracing_test::traced_test;
574
575 #[traced_test]
578 #[test]
579 fn test_run_npm_audit() -> Result<(), Error> {
580 audit()?;
581 Ok(())
582 }
583}