entrenar/quality/supply_chain/
dependency_audit.rs1use serde::{Deserialize, Serialize};
4
5use super::{Advisory, AuditStatus, Result, Severity, SupplyChainError};
6
7const 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct DependencyAudit {
22 pub crate_name: String,
24
25 pub version: String,
27
28 pub advisories: Vec<Advisory>,
30
31 pub license: String,
33
34 pub audit_status: AuditStatus,
36}
37
38impl DependencyAudit {
39 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 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 pub fn from_cargo_deny_output(json: &str) -> Result<Vec<Self>> {
91 let mut audits = Vec::new();
92
93 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 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 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 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 pub fn is_vulnerable(&self) -> bool {
175 self.audit_status == AuditStatus::Vulnerable
176 }
177
178 pub fn max_severity(&self) -> Severity {
180 self.advisories.iter().map(|a| a.severity).max().unwrap_or(Severity::None)
181 }
182}