clawft_kernel/assessment/analyzers/
dependency.rs1use std::path::{Path, PathBuf};
4
5use crate::assessment::analyzer::Analyzer;
6use crate::assessment::{Finding, analyzer::AnalysisContext};
7
8pub struct DependencyAnalyzer;
10
11impl Analyzer for DependencyAnalyzer {
12 fn id(&self) -> &str {
13 "dependency"
14 }
15
16 fn name(&self) -> &str {
17 "Dependency Analyzer"
18 }
19
20 fn categories(&self) -> &[&str] {
21 &["dependency"]
22 }
23
24 fn analyze(
25 &self,
26 project: &Path,
27 files: &[PathBuf],
28 _context: &AnalysisContext,
29 ) -> Vec<Finding> {
30 let mut findings = Vec::new();
31
32 for path in files {
33 let name = match path.file_name().and_then(|n| n.to_str()) {
34 Some(n) => n,
35 None => continue,
36 };
37 let rel = path.strip_prefix(project).unwrap_or(path);
38 let rel_str = rel.display().to_string();
39
40 match name {
41 "Cargo.toml" => {
42 findings.extend(analyze_cargo_toml(path, &rel_str));
43 }
44 "package.json" => {
45 findings.extend(analyze_package_json(path, &rel_str));
46 }
47 _ => {}
48 }
49 }
50
51 findings
52 }
53}
54
55fn analyze_cargo_toml(path: &Path, rel_str: &str) -> Vec<Finding> {
56 let mut findings = Vec::new();
57 let content = match std::fs::read_to_string(path) {
58 Ok(c) => c,
59 Err(_) => return findings,
60 };
61
62 let mut in_deps = false;
63 let mut dep_count: usize = 0;
64 let mut missing_version = Vec::new();
65
66 for line in content.lines() {
67 let trimmed = line.trim();
68
69 if trimmed.starts_with('[') {
70 in_deps = trimmed == "[dependencies]"
71 || trimmed == "[dev-dependencies]"
72 || trimmed == "[build-dependencies]"
73 || trimmed.starts_with("[dependencies.")
74 || trimmed.starts_with("[dev-dependencies.")
75 || trimmed.starts_with("[build-dependencies.");
76 continue;
77 }
78
79 if in_deps
80 && !trimmed.is_empty()
81 && !trimmed.starts_with('#')
82 && let Some(dep_name) = trimmed.split('=').next().map(|s| s.trim())
83 && !dep_name.is_empty()
84 {
85 dep_count += 1;
86 let value = trimmed
88 .split_once('=')
89 .map(|x| x.1)
90 .map(|s| s.trim())
91 .unwrap_or("");
92 if value == "\"*\"" || value.is_empty() {
93 missing_version.push(dep_name.to_string());
94 }
95 }
96 }
97
98 findings.push(Finding {
99 severity: "info".into(),
100 category: "dependency".into(),
101 file: rel_str.to_string(),
102 line: None,
103 message: format!("Cargo.toml has {dep_count} dependencies"),
104 });
105
106 for dep in missing_version {
107 findings.push(Finding {
108 severity: "warning".into(),
109 category: "dependency".into(),
110 file: rel_str.to_string(),
111 line: None,
112 message: format!("Dependency '{dep}' has no pinned version"),
113 });
114 }
115
116 findings
117}
118
119fn analyze_package_json(path: &Path, rel_str: &str) -> Vec<Finding> {
120 let mut findings = Vec::new();
121 let content = match std::fs::read_to_string(path) {
122 Ok(c) => c,
123 Err(_) => return findings,
124 };
125
126 let mut dep_count: usize = 0;
129 let mut dev_dep_count: usize = 0;
130
131 if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) {
133 if let Some(deps) = val.get("dependencies").and_then(|v| v.as_object()) {
134 dep_count = deps.len();
135 for (name, ver) in deps {
136 if ver.as_str().map(|s| s == "*").unwrap_or(true) {
137 findings.push(Finding {
138 severity: "warning".into(),
139 category: "dependency".into(),
140 file: rel_str.to_string(),
141 line: None,
142 message: format!("Dependency '{name}' has no pinned version"),
143 });
144 }
145 }
146 }
147 if let Some(deps) = val.get("devDependencies").and_then(|v| v.as_object()) {
148 dev_dep_count = deps.len();
149 }
150 }
151
152 findings.push(Finding {
153 severity: "info".into(),
154 category: "dependency".into(),
155 file: rel_str.to_string(),
156 line: None,
157 message: format!(
158 "package.json has {dep_count} dependencies, {dev_dep_count} devDependencies"
159 ),
160 });
161
162 findings
163}