Skip to main content

entrenar/quality/supply_chain/
dependency_audit.rs

1//! Dependency audit results and cargo-deny parsing.
2
3use serde::{Deserialize, Serialize};
4
5use super::{Advisory, AuditStatus, Result, Severity, SupplyChainError};
6
7// Field name constants for cargo-deny JSON parsing (CB-525)
8const FIELD_TYPE: &str = "type";
9const FIELD_FIELDS: &str = "fields";
10const FIELD_SEVERITY: &str = "severity";
11const FIELD_CODE: &str = "code";
12const FIELD_LABELS: &str = "labels";
13const FIELD_SPAN: &str = "span";
14const FIELD_CRATE: &str = "crate";
15const FIELD_NAME: &str = "name";
16const FIELD_VERSION: &str = "version";
17const FIELD_MESSAGE: &str = "message";
18
19/// Dependency audit result
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct DependencyAudit {
22    /// Crate name
23    pub crate_name: String,
24
25    /// Version string
26    pub version: String,
27
28    /// Security advisories affecting this crate
29    pub advisories: Vec<Advisory>,
30
31    /// License identifier (e.g., "MIT", "Apache-2.0")
32    pub license: String,
33
34    /// Overall audit status
35    pub audit_status: AuditStatus,
36}
37
38impl DependencyAudit {
39    /// Create a new clean dependency audit
40    pub fn clean(
41        crate_name: impl Into<String>,
42        version: impl Into<String>,
43        license: impl Into<String>,
44    ) -> Self {
45        Self {
46            crate_name: crate_name.into(),
47            version: version.into(),
48            advisories: Vec::new(),
49            license: license.into(),
50            audit_status: AuditStatus::Clean,
51        }
52    }
53
54    /// Create a vulnerable dependency audit
55    pub fn vulnerable(
56        crate_name: impl Into<String>,
57        version: impl Into<String>,
58        license: impl Into<String>,
59        advisories: Vec<Advisory>,
60    ) -> Self {
61        Self {
62            crate_name: crate_name.into(),
63            version: version.into(),
64            advisories,
65            license: license.into(),
66            audit_status: AuditStatus::Vulnerable,
67        }
68    }
69
70    /// Parse dependency audits from cargo-deny JSON output
71    ///
72    /// # Arguments
73    ///
74    /// * `json` - JSON output from `cargo deny check --format json`
75    ///
76    /// # Example JSON format
77    ///
78    /// ```json
79    /// {
80    ///   "type": "diagnostic",
81    ///   "fields": {
82    ///     "graphs": [...],
83    ///     "severity": "error",
84    ///     "code": "A001",
85    ///     "message": "Detected security vulnerability",
86    ///     "labels": [{"span": {"crate": {"name": "foo", "version": "1.0.0"}}}]
87    ///   }
88    /// }
89    /// ```
90    pub fn from_cargo_deny_output(json: &str) -> Result<Vec<Self>> {
91        let mut audits = Vec::new();
92
93        // cargo deny outputs newline-delimited JSON
94        for line in json.lines() {
95            let line = line.trim();
96            if line.is_empty() {
97                continue;
98            }
99
100            let value: serde_json::Value = serde_json::from_str(line)
101                .map_err(|e| SupplyChainError::ParseError(e.to_string()))?;
102
103            // Skip non-diagnostic messages
104            if value.get(FIELD_TYPE).and_then(|t| t.as_str()) != Some("diagnostic") {
105                continue;
106            }
107
108            let fields = match value.get(FIELD_FIELDS) {
109                Some(f) => f,
110                None => continue,
111            };
112
113            // Extract severity
114            let severity_str =
115                fields.get(FIELD_SEVERITY).and_then(|s| s.as_str()).unwrap_or("none");
116
117            let is_vulnerability = severity_str == "error"
118                && fields
119                    .get(FIELD_CODE)
120                    .and_then(|c| c.as_str())
121                    .is_some_and(|c| c.starts_with('A'));
122
123            if !is_vulnerability {
124                continue;
125            }
126
127            // Extract crate info from labels
128            if let Some(labels) = fields.get(FIELD_LABELS).and_then(|l| l.as_array()) {
129                for label in labels {
130                    if let Some(span) = label.get(FIELD_SPAN) {
131                        if let Some(krate) = span.get(FIELD_CRATE) {
132                            let crate_name = krate
133                                .get(FIELD_NAME)
134                                .and_then(|n| n.as_str())
135                                .unwrap_or("unknown")
136                                .to_string();
137
138                            let version = krate
139                                .get(FIELD_VERSION)
140                                .and_then(|v| v.as_str())
141                                .unwrap_or("unknown")
142                                .to_string();
143
144                            let message = fields
145                                .get(FIELD_MESSAGE)
146                                .and_then(|m| m.as_str())
147                                .unwrap_or("Unknown vulnerability")
148                                .to_string();
149
150                            let code = fields
151                                .get(FIELD_CODE)
152                                .and_then(|c| c.as_str())
153                                .unwrap_or("UNKNOWN")
154                                .to_string();
155
156                            let advisory = Advisory::new(code, Severity::High, message);
157
158                            audits.push(Self::vulnerable(
159                                crate_name,
160                                version,
161                                "unknown",
162                                vec![advisory],
163                            ));
164                        }
165                    }
166                }
167            }
168        }
169
170        Ok(audits)
171    }
172
173    /// Returns true if this dependency has vulnerabilities
174    pub fn is_vulnerable(&self) -> bool {
175        self.audit_status == AuditStatus::Vulnerable
176    }
177
178    /// Returns the highest severity advisory
179    pub fn max_severity(&self) -> Severity {
180        self.advisories.iter().map(|a| a.severity).max().unwrap_or(Severity::None)
181    }
182}