1use crate::analyzer::{AnalysisConfig, DetectedLanguage, DependencyMap};
2use crate::analyzer::vulnerability_checker::{VulnerabilityChecker, VulnerabilityInfo};
3use crate::error::{Result, AnalysisError};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::Path;
7use std::fs;
8use log::{debug, info, warn};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct DependencyInfo {
13 pub name: String,
14 pub version: String,
15 pub dep_type: DependencyType,
16 pub license: String,
17 pub source: Option<String>,
18 pub language: Language,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22pub enum DependencyType {
23 Production,
24 Dev,
25 Optional,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
29pub enum Language {
30 Rust,
31 JavaScript,
32 TypeScript,
33 Python,
34 Go,
35 Java,
36 Kotlin,
37 Unknown,
38}
39
40impl Language {
41 pub fn as_str(&self) -> &str {
42 match self {
43 Language::Rust => "Rust",
44 Language::JavaScript => "JavaScript",
45 Language::TypeScript => "TypeScript",
46 Language::Python => "Python",
47 Language::Go => "Go",
48 Language::Java => "Java",
49 Language::Kotlin => "Kotlin",
50 Language::Unknown => "Unknown",
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct Vulnerability {
58 pub id: String,
59 pub severity: VulnerabilitySeverity,
60 pub description: String,
61 pub fixed_in: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65pub enum VulnerabilitySeverity {
66 Critical,
67 High,
68 Medium,
69 Low,
70 Info,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
75pub struct LegacyDependencyInfo {
76 pub version: String,
77 pub is_dev: bool,
78 pub license: Option<String>,
79 pub vulnerabilities: Vec<Vulnerability>,
80 pub source: String, }
82
83pub type DetailedDependencyMap = HashMap<String, LegacyDependencyInfo>;
85
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
88pub struct DependencyAnalysis {
89 pub dependencies: DetailedDependencyMap,
90 pub total_count: usize,
91 pub production_count: usize,
92 pub dev_count: usize,
93 pub vulnerable_count: usize,
94 pub license_summary: HashMap<String, usize>,
95}
96
97pub struct DependencyParser;
99
100impl DependencyParser {
101 pub fn new() -> Self {
102 Self
103 }
104
105 async fn check_vulnerabilities_for_dependencies(
107 &self,
108 dependencies: &HashMap<Language, Vec<DependencyInfo>>,
109 project_path: &Path,
110 ) -> HashMap<String, Vec<VulnerabilityInfo>> {
111 let mut vulnerability_map = HashMap::new();
112
113 let checker = VulnerabilityChecker::new();
114
115 match checker.check_all_dependencies(dependencies, project_path).await {
116 Ok(report) => {
117 info!("Found {} total vulnerabilities across all dependencies", report.total_vulnerabilities);
118
119 for vuln_dep in report.vulnerable_dependencies {
121 vulnerability_map.insert(vuln_dep.name, vuln_dep.vulnerabilities);
122 }
123 }
124 Err(e) => {
125 warn!("Failed to check vulnerabilities: {}", e);
126 }
127 }
128
129 vulnerability_map
130 }
131
132 fn convert_vulnerability_info(vuln_info: &VulnerabilityInfo) -> Vulnerability {
134 Vulnerability {
135 id: vuln_info.id.clone(),
136 severity: match vuln_info.severity {
137 crate::analyzer::vulnerability_checker::VulnerabilitySeverity::Critical => VulnerabilitySeverity::Critical,
138 crate::analyzer::vulnerability_checker::VulnerabilitySeverity::High => VulnerabilitySeverity::High,
139 crate::analyzer::vulnerability_checker::VulnerabilitySeverity::Medium => VulnerabilitySeverity::Medium,
140 crate::analyzer::vulnerability_checker::VulnerabilitySeverity::Low => VulnerabilitySeverity::Low,
141 crate::analyzer::vulnerability_checker::VulnerabilitySeverity::Info => VulnerabilitySeverity::Info,
142 },
143 description: vuln_info.description.clone(),
144 fixed_in: vuln_info.patched_versions.clone(),
145 }
146 }
147
148 pub fn parse_all_dependencies(&self, project_root: &Path) -> Result<HashMap<Language, Vec<DependencyInfo>>> {
149 let mut dependencies = HashMap::new();
150
151 if project_root.join("Cargo.toml").exists() {
153 let rust_deps = self.parse_rust_deps(project_root)?;
154 if !rust_deps.is_empty() {
155 dependencies.insert(Language::Rust, rust_deps);
156 }
157 }
158
159 if project_root.join("package.json").exists() {
161 let js_deps = self.parse_js_deps(project_root)?;
162 if !js_deps.is_empty() {
163 dependencies.insert(Language::JavaScript, js_deps);
164 }
165 }
166
167 if project_root.join("requirements.txt").exists() ||
169 project_root.join("pyproject.toml").exists() ||
170 project_root.join("Pipfile").exists() {
171 let py_deps = self.parse_python_deps(project_root)?;
172 if !py_deps.is_empty() {
173 dependencies.insert(Language::Python, py_deps);
174 }
175 }
176
177 if project_root.join("go.mod").exists() {
179 let go_deps = self.parse_go_deps(project_root)?;
180 if !go_deps.is_empty() {
181 dependencies.insert(Language::Go, go_deps);
182 }
183 }
184
185 if project_root.join("pom.xml").exists() || project_root.join("build.gradle").exists() {
187 let java_deps = self.parse_java_deps(project_root)?;
188 if !java_deps.is_empty() {
189 dependencies.insert(Language::Java, java_deps);
190 }
191 }
192
193 Ok(dependencies)
194 }
195
196 fn parse_rust_deps(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
197 let cargo_lock = project_root.join("Cargo.lock");
198 let cargo_toml = project_root.join("Cargo.toml");
199
200 let mut deps = Vec::new();
201
202 if cargo_lock.exists() {
204 let content = fs::read_to_string(&cargo_lock)?;
205 let parsed: toml::Value = toml::from_str(&content)
206 .map_err(|e| AnalysisError::DependencyParsing {
207 file: "Cargo.lock".to_string(),
208 reason: e.to_string(),
209 })?;
210
211 if let Some(packages) = parsed.get("package").and_then(|p| p.as_array()) {
213 for package in packages {
214 if let Some(package_table) = package.as_table() {
215 if let (Some(name), Some(version)) = (
216 package_table.get("name").and_then(|n| n.as_str()),
217 package_table.get("version").and_then(|v| v.as_str())
218 ) {
219 let dep_type = self.get_rust_dependency_type(name, &cargo_toml);
221
222 deps.push(DependencyInfo {
223 name: name.to_string(),
224 version: version.to_string(),
225 dep_type,
226 license: detect_rust_license(name).unwrap_or_else(|| "Unknown".to_string()),
227 source: Some("crates.io".to_string()),
228 language: Language::Rust,
229 });
230 }
231 }
232 }
233 }
234 } else if cargo_toml.exists() {
235 let content = fs::read_to_string(&cargo_toml)?;
237 let parsed: toml::Value = toml::from_str(&content)
238 .map_err(|e| AnalysisError::DependencyParsing {
239 file: "Cargo.toml".to_string(),
240 reason: e.to_string(),
241 })?;
242
243 if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_table()) {
245 for (name, value) in dependencies {
246 let version = extract_version_from_toml_value(value);
247 deps.push(DependencyInfo {
248 name: name.clone(),
249 version,
250 dep_type: DependencyType::Production,
251 license: detect_rust_license(name).unwrap_or_else(|| "Unknown".to_string()),
252 source: Some("crates.io".to_string()),
253 language: Language::Rust,
254 });
255 }
256 }
257
258 if let Some(dev_deps) = parsed.get("dev-dependencies").and_then(|d| d.as_table()) {
260 for (name, value) in dev_deps {
261 let version = extract_version_from_toml_value(value);
262 deps.push(DependencyInfo {
263 name: name.clone(),
264 version,
265 dep_type: DependencyType::Dev,
266 license: detect_rust_license(name).unwrap_or_else(|| "Unknown".to_string()),
267 source: Some("crates.io".to_string()),
268 language: Language::Rust,
269 });
270 }
271 }
272 }
273
274 Ok(deps)
275 }
276
277 fn get_rust_dependency_type(&self, dep_name: &str, cargo_toml_path: &Path) -> DependencyType {
278 if !cargo_toml_path.exists() {
279 return DependencyType::Production;
280 }
281
282 if let Ok(content) = fs::read_to_string(cargo_toml_path) {
283 if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
284 if let Some(dev_deps) = parsed.get("dev-dependencies").and_then(|d| d.as_table()) {
286 if dev_deps.contains_key(dep_name) {
287 return DependencyType::Dev;
288 }
289 }
290
291 if let Some(deps) = parsed.get("dependencies").and_then(|d| d.as_table()) {
293 if deps.contains_key(dep_name) {
294 return DependencyType::Production;
295 }
296 }
297 }
298 }
299
300 DependencyType::Production
302 }
303
304 fn parse_js_deps(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
305 let package_json = project_root.join("package.json");
306 let content = fs::read_to_string(&package_json)?;
307 let parsed: serde_json::Value = serde_json::from_str(&content)
308 .map_err(|e| AnalysisError::DependencyParsing {
309 file: "package.json".to_string(),
310 reason: e.to_string(),
311 })?;
312
313 let mut deps = Vec::new();
314
315 if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_object()) {
317 for (name, version) in dependencies {
318 if let Some(ver_str) = version.as_str() {
319 deps.push(DependencyInfo {
320 name: name.clone(),
321 version: ver_str.to_string(),
322 dep_type: DependencyType::Production,
323 license: detect_npm_license(name).unwrap_or_else(|| "Unknown".to_string()),
324 source: Some("npm".to_string()),
325 language: Language::JavaScript,
326 });
327 }
328 }
329 }
330
331 if let Some(dev_deps) = parsed.get("devDependencies").and_then(|d| d.as_object()) {
333 for (name, version) in dev_deps {
334 if let Some(ver_str) = version.as_str() {
335 deps.push(DependencyInfo {
336 name: name.clone(),
337 version: ver_str.to_string(),
338 dep_type: DependencyType::Dev,
339 license: detect_npm_license(name).unwrap_or_else(|| "Unknown".to_string()),
340 source: Some("npm".to_string()),
341 language: Language::JavaScript,
342 });
343 }
344 }
345 }
346
347 Ok(deps)
348 }
349
350 fn parse_python_deps(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
351 let mut deps = Vec::new();
352
353 let pyproject = project_root.join("pyproject.toml");
355 if pyproject.exists() {
356 debug!("Found pyproject.toml, parsing Python dependencies");
357 let content = fs::read_to_string(&pyproject)?;
358 if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
359 if let Some(poetry_deps) = parsed
361 .get("tool")
362 .and_then(|t| t.get("poetry"))
363 .and_then(|p| p.get("dependencies"))
364 .and_then(|d| d.as_table())
365 {
366 debug!("Found Poetry dependencies in pyproject.toml");
367 for (name, value) in poetry_deps {
368 if name != "python" {
369 let version = extract_version_from_toml_value(value);
370 deps.push(DependencyInfo {
371 name: name.clone(),
372 version,
373 dep_type: DependencyType::Production,
374 license: detect_pypi_license(name).unwrap_or_else(|| "Unknown".to_string()),
375 source: Some("pypi".to_string()),
376 language: Language::Python,
377 });
378 }
379 }
380 }
381
382 if let Some(poetry_dev_deps) = parsed
384 .get("tool")
385 .and_then(|t| t.get("poetry"))
386 .and_then(|p| p.get("group"))
387 .and_then(|g| g.get("dev"))
388 .and_then(|d| d.get("dependencies"))
389 .and_then(|d| d.as_table())
390 .or_else(|| {
391 parsed
393 .get("tool")
394 .and_then(|t| t.get("poetry"))
395 .and_then(|p| p.get("dev-dependencies"))
396 .and_then(|d| d.as_table())
397 })
398 {
399 debug!("Found Poetry dev dependencies in pyproject.toml");
400 for (name, value) in poetry_dev_deps {
401 let version = extract_version_from_toml_value(value);
402 deps.push(DependencyInfo {
403 name: name.clone(),
404 version,
405 dep_type: DependencyType::Dev,
406 license: detect_pypi_license(name).unwrap_or_else(|| "Unknown".to_string()),
407 source: Some("pypi".to_string()),
408 language: Language::Python,
409 });
410 }
411 }
412
413 if let Some(project_deps) = parsed
415 .get("project")
416 .and_then(|p| p.get("dependencies"))
417 .and_then(|d| d.as_array())
418 {
419 debug!("Found PEP 621 dependencies in pyproject.toml");
420 for dep in project_deps {
421 if let Some(dep_str) = dep.as_str() {
422 let (name, version) = self.parse_python_requirement_spec(dep_str);
423 deps.push(DependencyInfo {
424 name: name.clone(),
425 version,
426 dep_type: DependencyType::Production,
427 license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
428 source: Some("pypi".to_string()),
429 language: Language::Python,
430 });
431 }
432 }
433 }
434
435 if let Some(optional_deps) = parsed
437 .get("project")
438 .and_then(|p| p.get("optional-dependencies"))
439 .and_then(|d| d.as_table())
440 {
441 debug!("Found PEP 621 optional dependencies in pyproject.toml");
442 for (group_name, group_deps) in optional_deps {
443 if let Some(deps_array) = group_deps.as_array() {
444 let is_dev = group_name.contains("dev") || group_name.contains("test");
445 for dep in deps_array {
446 if let Some(dep_str) = dep.as_str() {
447 let (name, version) = self.parse_python_requirement_spec(dep_str);
448 deps.push(DependencyInfo {
449 name: name.clone(),
450 version,
451 dep_type: if is_dev { DependencyType::Dev } else { DependencyType::Optional },
452 license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
453 source: Some("pypi".to_string()),
454 language: Language::Python,
455 });
456 }
457 }
458 }
459 }
460 }
461
462 if let Some(pdm_deps) = parsed
464 .get("tool")
465 .and_then(|t| t.get("pdm"))
466 .and_then(|p| p.get("dev-dependencies"))
467 .and_then(|d| d.as_table())
468 {
469 debug!("Found PDM dev dependencies in pyproject.toml");
470 for (group_name, group_deps) in pdm_deps {
471 if let Some(deps_array) = group_deps.as_array() {
472 for dep in deps_array {
473 if let Some(dep_str) = dep.as_str() {
474 let (name, version) = self.parse_python_requirement_spec(dep_str);
475 deps.push(DependencyInfo {
476 name: name.clone(),
477 version,
478 dep_type: DependencyType::Dev,
479 license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
480 source: Some("pypi".to_string()),
481 language: Language::Python,
482 });
483 }
484 }
485 }
486 }
487 }
488
489 if let Some(setuptools_deps) = parsed
491 .get("tool")
492 .and_then(|t| t.get("setuptools"))
493 .and_then(|s| s.get("dynamic"))
494 .and_then(|d| d.get("dependencies"))
495 .and_then(|d| d.as_array())
496 {
497 debug!("Found setuptools dependencies in pyproject.toml");
498 for dep in setuptools_deps {
499 if let Some(dep_str) = dep.as_str() {
500 let (name, version) = self.parse_python_requirement_spec(dep_str);
501 deps.push(DependencyInfo {
502 name: name.clone(),
503 version,
504 dep_type: DependencyType::Production,
505 license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
506 source: Some("pypi".to_string()),
507 language: Language::Python,
508 });
509 }
510 }
511 }
512 }
513 }
514
515 let pipfile = project_root.join("Pipfile");
517 if pipfile.exists() && deps.is_empty() {
518 debug!("Found Pipfile, parsing pipenv dependencies");
519 let content = fs::read_to_string(&pipfile)?;
520 if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
521 if let Some(packages) = parsed.get("packages").and_then(|p| p.as_table()) {
523 for (name, value) in packages {
524 let version = extract_version_from_toml_value(value);
525 deps.push(DependencyInfo {
526 name: name.clone(),
527 version,
528 dep_type: DependencyType::Production,
529 license: detect_pypi_license(name).unwrap_or_else(|| "Unknown".to_string()),
530 source: Some("pypi".to_string()),
531 language: Language::Python,
532 });
533 }
534 }
535
536 if let Some(dev_packages) = parsed.get("dev-packages").and_then(|p| p.as_table()) {
538 for (name, value) in dev_packages {
539 let version = extract_version_from_toml_value(value);
540 deps.push(DependencyInfo {
541 name: name.clone(),
542 version,
543 dep_type: DependencyType::Dev,
544 license: detect_pypi_license(name).unwrap_or_else(|| "Unknown".to_string()),
545 source: Some("pypi".to_string()),
546 language: Language::Python,
547 });
548 }
549 }
550 }
551 }
552
553 let requirements_txt = project_root.join("requirements.txt");
555 if requirements_txt.exists() && deps.is_empty() {
556 debug!("Found requirements.txt, parsing legacy Python dependencies");
557 let content = fs::read_to_string(&requirements_txt)?;
558 for line in content.lines() {
559 let line = line.trim();
560 if !line.is_empty() && !line.starts_with('#') && !line.starts_with('-') {
561 let (name, version) = self.parse_python_requirement_spec(line);
562 deps.push(DependencyInfo {
563 name: name.clone(),
564 version,
565 dep_type: DependencyType::Production,
566 license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
567 source: Some("pypi".to_string()),
568 language: Language::Python,
569 });
570 }
571 }
572 }
573
574 let requirements_dev = project_root.join("requirements-dev.txt");
576 if requirements_dev.exists() {
577 debug!("Found requirements-dev.txt, parsing dev dependencies");
578 let content = fs::read_to_string(&requirements_dev)?;
579 for line in content.lines() {
580 let line = line.trim();
581 if !line.is_empty() && !line.starts_with('#') && !line.starts_with('-') {
582 let (name, version) = self.parse_python_requirement_spec(line);
583 deps.push(DependencyInfo {
584 name: name.clone(),
585 version,
586 dep_type: DependencyType::Dev,
587 license: detect_pypi_license(&name).unwrap_or_else(|| "Unknown".to_string()),
588 source: Some("pypi".to_string()),
589 language: Language::Python,
590 });
591 }
592 }
593 }
594
595 debug!("Parsed {} Python dependencies", deps.len());
596 if !deps.is_empty() {
597 debug!("Sample Python dependencies:");
598 for dep in deps.iter().take(5) {
599 debug!(" - {} v{} ({:?})", dep.name, dep.version, dep.dep_type);
600 }
601 }
602
603 Ok(deps)
604 }
605
606 fn parse_go_deps(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
607 let go_mod = project_root.join("go.mod");
608 let content = fs::read_to_string(&go_mod)?;
609 let mut deps = Vec::new();
610 let mut in_require_block = false;
611
612 for line in content.lines() {
613 let trimmed = line.trim();
614
615 if trimmed.starts_with("require (") {
616 in_require_block = true;
617 continue;
618 }
619
620 if in_require_block && trimmed == ")" {
621 in_require_block = false;
622 continue;
623 }
624
625 if in_require_block || trimmed.starts_with("require ") {
626 let parts: Vec<&str> = trimmed
627 .trim_start_matches("require ")
628 .split_whitespace()
629 .collect();
630
631 if parts.len() >= 2 {
632 let name = parts[0];
633 let version = parts[1];
634
635 deps.push(DependencyInfo {
636 name: name.to_string(),
637 version: version.to_string(),
638 dep_type: DependencyType::Production,
639 license: detect_go_license(name).unwrap_or("Unknown".to_string()),
640 source: Some("go modules".to_string()),
641 language: Language::Go,
642 });
643 }
644 }
645 }
646
647 Ok(deps)
648 }
649
650 fn parse_python_requirement_spec(&self, spec: &str) -> (String, String) {
652 let spec = spec.trim();
660
661 let spec = if let Some(index) = spec.find("--") {
663 &spec[..index]
664 } else {
665 spec
666 }.trim();
667
668 let version_operators = ['=', '>', '<', '~', '!'];
670 let version_start = spec.find(&version_operators[..]);
671
672 if let Some(pos) = version_start {
673 let package_part = spec[..pos].trim();
675 let version_part = spec[pos..].trim();
676
677 let package_name = if package_part.contains('[') && package_part.contains(']') {
679 if let Some(bracket_start) = package_part.find('[') {
681 package_part[..bracket_start].trim().to_string()
682 } else {
683 package_part.to_string()
684 }
685 } else {
686 package_part.to_string()
687 };
688
689 (package_name, version_part.to_string())
690 } else {
691 let package_name = if spec.contains('[') && spec.contains(']') {
693 if let Some(bracket_start) = spec.find('[') {
694 spec[..bracket_start].trim().to_string()
695 } else {
696 spec.to_string()
697 }
698 } else {
699 spec.to_string()
700 };
701
702 (package_name, "*".to_string())
703 }
704 }
705
706 fn parse_java_deps(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
707 let mut deps = Vec::new();
708
709 debug!("Parsing Java dependencies in: {}", project_root.display());
710
711 let pom_xml = project_root.join("pom.xml");
713 if pom_xml.exists() {
714 debug!("Found pom.xml, parsing Maven dependencies");
715 let content = fs::read_to_string(&pom_xml)?;
716
717 if let Ok(maven_deps) = self.parse_maven_dependencies_with_command(project_root) {
719 if !maven_deps.is_empty() {
720 debug!("Successfully parsed {} Maven dependencies using mvn command", maven_deps.len());
721 deps.extend(maven_deps);
722 }
723 }
724
725 if deps.is_empty() {
727 debug!("Falling back to XML parsing for Maven dependencies");
728 let xml_deps = self.parse_pom_xml(&content)?;
729 debug!("Parsed {} dependencies from pom.xml", xml_deps.len());
730 deps.extend(xml_deps);
731 }
732 }
733
734 let build_gradle = project_root.join("build.gradle");
736 let build_gradle_kts = project_root.join("build.gradle.kts");
737
738 if (build_gradle.exists() || build_gradle_kts.exists()) && deps.is_empty() {
739 debug!("Found Gradle build file, parsing Gradle dependencies");
740
741 if let Ok(gradle_deps) = self.parse_gradle_dependencies_with_command(project_root) {
743 if !gradle_deps.is_empty() {
744 debug!("Successfully parsed {} Gradle dependencies using gradle command", gradle_deps.len());
745 deps.extend(gradle_deps);
746 }
747 }
748
749 if deps.is_empty() {
751 if build_gradle.exists() {
752 debug!("Falling back to build.gradle parsing");
753 let content = fs::read_to_string(&build_gradle)?;
754 let gradle_deps = self.parse_gradle_build(&content)?;
755 debug!("Parsed {} dependencies from build.gradle", gradle_deps.len());
756 deps.extend(gradle_deps);
757 }
758
759 if build_gradle_kts.exists() && deps.is_empty() {
760 debug!("Falling back to build.gradle.kts parsing");
761 let content = fs::read_to_string(&build_gradle_kts)?;
762 let gradle_deps = self.parse_gradle_build(&content)?; debug!("Parsed {} dependencies from build.gradle.kts", gradle_deps.len());
764 deps.extend(gradle_deps);
765 }
766 }
767 }
768
769 debug!("Total Java dependencies found: {}", deps.len());
770 if !deps.is_empty() {
771 debug!("Sample dependencies:");
772 for dep in deps.iter().take(5) {
773 debug!(" - {} v{}", dep.name, dep.version);
774 }
775 }
776
777 Ok(deps)
778 }
779
780 fn parse_maven_dependencies_with_command(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
782 use std::process::Command;
783
784 let output = Command::new("mvn")
785 .args(&["dependency:list", "-DoutputFile=deps.txt", "-DappendOutput=false", "-DincludeScope=compile"])
786 .current_dir(project_root)
787 .output();
788
789 match output {
790 Ok(result) if result.status.success() => {
791 let deps_file = project_root.join("deps.txt");
793 if deps_file.exists() {
794 let content = fs::read_to_string(&deps_file)?;
795 let deps = self.parse_maven_dependency_list(&content)?;
796
797 let _ = fs::remove_file(&deps_file);
799
800 return Ok(deps);
801 }
802 }
803 _ => {
804 debug!("Maven command failed or not available, falling back to XML parsing");
805 }
806 }
807
808 Ok(vec![])
809 }
810
811 fn parse_gradle_dependencies_with_command(&self, project_root: &Path) -> Result<Vec<DependencyInfo>> {
813 use std::process::Command;
814
815 let gradle_cmds = vec!["gradle", "./gradlew"];
817
818 for gradle_cmd in gradle_cmds {
819 let output = Command::new(gradle_cmd)
820 .args(&["dependencies", "--configuration=runtimeClasspath", "--console=plain"])
821 .current_dir(project_root)
822 .output();
823
824 match output {
825 Ok(result) if result.status.success() => {
826 let output_str = String::from_utf8_lossy(&result.stdout);
827 let deps = self.parse_gradle_dependency_tree(&output_str)?;
828 if !deps.is_empty() {
829 return Ok(deps);
830 }
831 }
832 _ => {
833 debug!("Gradle command '{}' failed, trying next", gradle_cmd);
834 continue;
835 }
836 }
837 }
838
839 debug!("All Gradle commands failed, falling back to build file parsing");
840 Ok(vec![])
841 }
842
843 fn parse_maven_dependency_list(&self, content: &str) -> Result<Vec<DependencyInfo>> {
845 let mut deps = Vec::new();
846
847 for line in content.lines() {
848 let trimmed = line.trim();
849 if trimmed.is_empty() || trimmed.starts_with("The following") || trimmed.starts_with("---") {
850 continue;
851 }
852
853 let parts: Vec<&str> = trimmed.split(':').collect();
855 if parts.len() >= 4 {
856 let group_id = parts[0];
857 let artifact_id = parts[1];
858 let version = parts[3];
859 let scope = if parts.len() > 4 { parts[4] } else { "compile" };
860
861 let name = format!("{}:{}", group_id, artifact_id);
862 let dep_type = match scope {
863 "test" | "provided" => DependencyType::Dev,
864 _ => DependencyType::Production,
865 };
866
867 deps.push(DependencyInfo {
868 name,
869 version: version.to_string(),
870 dep_type,
871 license: "Unknown".to_string(),
872 source: Some("maven".to_string()),
873 language: Language::Java,
874 });
875 }
876 }
877
878 Ok(deps)
879 }
880
881 fn parse_gradle_dependency_tree(&self, content: &str) -> Result<Vec<DependencyInfo>> {
883 let mut deps = Vec::new();
884
885 for line in content.lines() {
886 let trimmed = line.trim();
887
888 if (trimmed.starts_with("+---") || trimmed.starts_with("\\---") || trimmed.starts_with("|"))
890 && trimmed.contains(':') {
891
892 let dep_part = if let Some(pos) = trimmed.find(' ') {
894 &trimmed[pos + 1..]
895 } else {
896 trimmed
897 };
898
899 let clean_dep = dep_part
901 .replace(" (*)", "")
902 .replace(" (c)", "")
903 .replace(" (n)", "")
904 .replace("(*)", "")
905 .trim()
906 .to_string();
907
908 let parts: Vec<&str> = clean_dep.split(':').collect();
909 if parts.len() >= 3 {
910 let group_id = parts[0];
911 let artifact_id = parts[1];
912 let version = parts[2];
913
914 let name = format!("{}:{}", group_id, artifact_id);
915
916 deps.push(DependencyInfo {
917 name,
918 version: version.to_string(),
919 dep_type: DependencyType::Production,
920 license: "Unknown".to_string(),
921 source: Some("gradle".to_string()),
922 language: Language::Java,
923 });
924 }
925 }
926 }
927
928 Ok(deps)
929 }
930
931 fn parse_pom_xml(&self, content: &str) -> Result<Vec<DependencyInfo>> {
933 let mut deps = Vec::new();
934 let mut in_dependencies = false;
935 let mut in_dependency = false;
936 let mut current_group_id = String::new();
937 let mut current_artifact_id = String::new();
938 let mut current_version = String::new();
939 let mut current_scope = String::new();
940
941 for line in content.lines() {
942 let trimmed = line.trim();
943
944 if trimmed.contains("<dependencies>") {
945 in_dependencies = true;
946 continue;
947 }
948
949 if trimmed.contains("</dependencies>") {
950 in_dependencies = false;
951 continue;
952 }
953
954 if in_dependencies {
955 if trimmed.contains("<dependency>") {
956 in_dependency = true;
957 current_group_id.clear();
958 current_artifact_id.clear();
959 current_version.clear();
960 current_scope.clear();
961 continue;
962 }
963
964 if trimmed.contains("</dependency>") && in_dependency {
965 in_dependency = false;
966
967 if !current_group_id.is_empty() && !current_artifact_id.is_empty() {
968 let name = format!("{}:{}", current_group_id, current_artifact_id);
969 let version = if current_version.is_empty() {
970 "unknown".to_string()
971 } else {
972 current_version.clone()
973 };
974
975 let dep_type = match current_scope.as_str() {
976 "test" | "provided" => DependencyType::Dev,
977 _ => DependencyType::Production,
978 };
979
980 deps.push(DependencyInfo {
981 name,
982 version,
983 dep_type,
984 license: "Unknown".to_string(),
985 source: Some("maven".to_string()),
986 language: Language::Java,
987 });
988 }
989 continue;
990 }
991
992 if in_dependency {
993 if trimmed.contains("<groupId>") {
994 current_group_id = extract_xml_value(trimmed, "groupId").to_string();
995 } else if trimmed.contains("<artifactId>") {
996 current_artifact_id = extract_xml_value(trimmed, "artifactId").to_string();
997 } else if trimmed.contains("<version>") {
998 current_version = extract_xml_value(trimmed, "version").to_string();
999 } else if trimmed.contains("<scope>") {
1000 current_scope = extract_xml_value(trimmed, "scope").to_string();
1001 }
1002 }
1003 }
1004 }
1005
1006 Ok(deps)
1007 }
1008
1009 fn parse_gradle_build(&self, content: &str) -> Result<Vec<DependencyInfo>> {
1011 let mut deps = Vec::new();
1012
1013 for line in content.lines() {
1014 let trimmed = line.trim();
1015
1016 if (trimmed.starts_with("implementation ") ||
1018 trimmed.starts_with("compile ") ||
1019 trimmed.starts_with("api ") ||
1020 trimmed.starts_with("runtimeOnly ") ||
1021 trimmed.starts_with("testImplementation ") ||
1022 trimmed.starts_with("testCompile ")) {
1023
1024 if let Some(dep_str) = extract_gradle_dependency(trimmed) {
1025 let parts: Vec<&str> = dep_str.split(':').collect();
1026 if parts.len() >= 3 {
1027 let group_id = parts[0];
1028 let artifact_id = parts[1];
1029 let version = parts[2].trim_matches('"').trim_matches('\'');
1030
1031 let name = format!("{}:{}", group_id, artifact_id);
1032 let dep_type = if trimmed.starts_with("test") {
1033 DependencyType::Dev
1034 } else {
1035 DependencyType::Production
1036 };
1037
1038 deps.push(DependencyInfo {
1039 name,
1040 version: version.to_string(),
1041 dep_type,
1042 license: "Unknown".to_string(),
1043 source: Some("gradle".to_string()),
1044 language: Language::Java,
1045 });
1046 }
1047 }
1048 }
1049 }
1050
1051 Ok(deps)
1052 }
1053}
1054
1055pub fn parse_dependencies(
1057 project_root: &Path,
1058 languages: &[DetectedLanguage],
1059 _config: &AnalysisConfig,
1060) -> Result<DependencyMap> {
1061 let mut all_dependencies = DependencyMap::new();
1062
1063 for language in languages {
1064 let deps = match language.name.as_str() {
1065 "Rust" => parse_rust_dependencies(project_root)?,
1066 "JavaScript" | "TypeScript" | "JavaScript/TypeScript" => parse_js_dependencies(project_root)?,
1067 "Python" => parse_python_dependencies(project_root)?,
1068 "Go" => parse_go_dependencies(project_root)?,
1069 "Java" | "Kotlin" | "Java/Kotlin" => parse_jvm_dependencies(project_root)?,
1070 _ => DependencyMap::new(),
1071 };
1072 all_dependencies.extend(deps);
1073 }
1074
1075 Ok(all_dependencies)
1076}
1077
1078pub async fn parse_detailed_dependencies(
1080 project_root: &Path,
1081 languages: &[DetectedLanguage],
1082 _config: &AnalysisConfig,
1083) -> Result<DependencyAnalysis> {
1084 let mut detailed_deps = DetailedDependencyMap::new();
1085 let mut license_summary = HashMap::new();
1086
1087 for language in languages {
1089 let deps = match language.name.as_str() {
1090 "Rust" => parse_rust_dependencies_detailed(project_root)?,
1091 "JavaScript" | "TypeScript" | "JavaScript/TypeScript" => parse_js_dependencies_detailed(project_root)?,
1092 "Python" => parse_python_dependencies_detailed(project_root)?,
1093 "Go" => parse_go_dependencies_detailed(project_root)?,
1094 "Java" | "Kotlin" | "Java/Kotlin" => parse_jvm_dependencies_detailed(project_root)?,
1095 _ => DetailedDependencyMap::new(),
1096 };
1097
1098 for (_, dep_info) in &deps {
1100 if let Some(license) = &dep_info.license {
1101 *license_summary.entry(license.clone()).or_insert(0) += 1;
1102 }
1103 }
1104
1105 detailed_deps.extend(deps);
1106 }
1107
1108 let parser = DependencyParser::new();
1110 let all_deps = parser.parse_all_dependencies(project_root)?;
1111 let vulnerability_map = parser.check_vulnerabilities_for_dependencies(&all_deps, project_root).await;
1112
1113 for (dep_name, dep_info) in detailed_deps.iter_mut() {
1115 if let Some(vulns) = vulnerability_map.get(dep_name) {
1116 dep_info.vulnerabilities = vulns.iter()
1117 .map(|v| DependencyParser::convert_vulnerability_info(v))
1118 .collect();
1119 }
1120 }
1121
1122 let total_count = detailed_deps.len();
1123 let production_count = detailed_deps.values().filter(|d| !d.is_dev).count();
1124 let dev_count = detailed_deps.values().filter(|d| d.is_dev).count();
1125 let vulnerable_count = detailed_deps.values().filter(|d| !d.vulnerabilities.is_empty()).count();
1126
1127 Ok(DependencyAnalysis {
1128 dependencies: detailed_deps,
1129 total_count,
1130 production_count,
1131 dev_count,
1132 vulnerable_count,
1133 license_summary,
1134 })
1135}
1136
1137fn parse_rust_dependencies(project_root: &Path) -> Result<DependencyMap> {
1139 let cargo_toml = project_root.join("Cargo.toml");
1140 if !cargo_toml.exists() {
1141 return Ok(DependencyMap::new());
1142 }
1143
1144 let content = fs::read_to_string(&cargo_toml)?;
1145 let parsed: toml::Value = toml::from_str(&content)
1146 .map_err(|e| AnalysisError::DependencyParsing {
1147 file: "Cargo.toml".to_string(),
1148 reason: e.to_string(),
1149 })?;
1150
1151 let mut deps = DependencyMap::new();
1152
1153 if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_table()) {
1155 for (name, value) in dependencies {
1156 let version = extract_version_from_toml_value(value);
1157 deps.insert(name.clone(), version);
1158 }
1159 }
1160
1161 if let Some(dev_deps) = parsed.get("dev-dependencies").and_then(|d| d.as_table()) {
1163 for (name, value) in dev_deps {
1164 let version = extract_version_from_toml_value(value);
1165 deps.insert(format!("{} (dev)", name), version);
1166 }
1167 }
1168
1169 Ok(deps)
1170}
1171
1172fn parse_rust_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1174 let cargo_toml = project_root.join("Cargo.toml");
1175 if !cargo_toml.exists() {
1176 return Ok(DetailedDependencyMap::new());
1177 }
1178
1179 let content = fs::read_to_string(&cargo_toml)?;
1180 let parsed: toml::Value = toml::from_str(&content)
1181 .map_err(|e| AnalysisError::DependencyParsing {
1182 file: "Cargo.toml".to_string(),
1183 reason: e.to_string(),
1184 })?;
1185
1186 let mut deps = DetailedDependencyMap::new();
1187
1188 if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_table()) {
1190 for (name, value) in dependencies {
1191 let version = extract_version_from_toml_value(value);
1192 deps.insert(name.clone(), LegacyDependencyInfo {
1193 version,
1194 is_dev: false,
1195 license: detect_rust_license(name),
1196 vulnerabilities: vec![], source: "crates.io".to_string(),
1198 });
1199 }
1200 }
1201
1202 if let Some(dev_deps) = parsed.get("dev-dependencies").and_then(|d| d.as_table()) {
1204 for (name, value) in dev_deps {
1205 let version = extract_version_from_toml_value(value);
1206 deps.insert(name.clone(), LegacyDependencyInfo {
1207 version,
1208 is_dev: true,
1209 license: detect_rust_license(name),
1210 vulnerabilities: vec![], source: "crates.io".to_string(),
1212 });
1213 }
1214 }
1215
1216 Ok(deps)
1217}
1218
1219fn parse_js_dependencies(project_root: &Path) -> Result<DependencyMap> {
1221 let package_json = project_root.join("package.json");
1222 if !package_json.exists() {
1223 return Ok(DependencyMap::new());
1224 }
1225
1226 let content = fs::read_to_string(&package_json)?;
1227 let parsed: serde_json::Value = serde_json::from_str(&content)
1228 .map_err(|e| AnalysisError::DependencyParsing {
1229 file: "package.json".to_string(),
1230 reason: e.to_string(),
1231 })?;
1232
1233 let mut deps = DependencyMap::new();
1234
1235 if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_object()) {
1237 for (name, version) in dependencies {
1238 if let Some(ver_str) = version.as_str() {
1239 deps.insert(name.clone(), ver_str.to_string());
1240 }
1241 }
1242 }
1243
1244 if let Some(dev_deps) = parsed.get("devDependencies").and_then(|d| d.as_object()) {
1246 for (name, version) in dev_deps {
1247 if let Some(ver_str) = version.as_str() {
1248 deps.insert(format!("{} (dev)", name), ver_str.to_string());
1249 }
1250 }
1251 }
1252
1253 Ok(deps)
1254}
1255
1256fn parse_js_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1258 let package_json = project_root.join("package.json");
1259 if !package_json.exists() {
1260 return Ok(DetailedDependencyMap::new());
1261 }
1262
1263 let content = fs::read_to_string(&package_json)?;
1264 let parsed: serde_json::Value = serde_json::from_str(&content)
1265 .map_err(|e| AnalysisError::DependencyParsing {
1266 file: "package.json".to_string(),
1267 reason: e.to_string(),
1268 })?;
1269
1270 let mut deps = DetailedDependencyMap::new();
1271
1272 if let Some(dependencies) = parsed.get("dependencies").and_then(|d| d.as_object()) {
1274 for (name, version) in dependencies {
1275 if let Some(ver_str) = version.as_str() {
1276 deps.insert(name.clone(), LegacyDependencyInfo {
1277 version: ver_str.to_string(),
1278 is_dev: false,
1279 license: detect_npm_license(name),
1280 vulnerabilities: vec![], source: "npm".to_string(),
1282 });
1283 }
1284 }
1285 }
1286
1287 if let Some(dev_deps) = parsed.get("devDependencies").and_then(|d| d.as_object()) {
1289 for (name, version) in dev_deps {
1290 if let Some(ver_str) = version.as_str() {
1291 deps.insert(name.clone(), LegacyDependencyInfo {
1292 version: ver_str.to_string(),
1293 is_dev: true,
1294 license: detect_npm_license(name),
1295 vulnerabilities: vec![], source: "npm".to_string(),
1297 });
1298 }
1299 }
1300 }
1301
1302 Ok(deps)
1303}
1304
1305fn parse_python_dependencies(project_root: &Path) -> Result<DependencyMap> {
1307 let mut deps = DependencyMap::new();
1308
1309 let requirements_txt = project_root.join("requirements.txt");
1311 if requirements_txt.exists() {
1312 let content = fs::read_to_string(&requirements_txt)?;
1313 for line in content.lines() {
1314 if !line.trim().is_empty() && !line.starts_with('#') {
1315 let parts: Vec<&str> = line.split(&['=', '>', '<', '~', '!'][..]).collect();
1316 if !parts.is_empty() {
1317 let name = parts[0].trim();
1318 let version = if parts.len() > 1 {
1319 line[name.len()..].trim().to_string()
1320 } else {
1321 "*".to_string()
1322 };
1323 deps.insert(name.to_string(), version);
1324 }
1325 }
1326 }
1327 }
1328
1329 let pyproject = project_root.join("pyproject.toml");
1331 if pyproject.exists() {
1332 let content = fs::read_to_string(&pyproject)?;
1333 if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
1334 if let Some(poetry_deps) = parsed
1336 .get("tool")
1337 .and_then(|t| t.get("poetry"))
1338 .and_then(|p| p.get("dependencies"))
1339 .and_then(|d| d.as_table())
1340 {
1341 for (name, value) in poetry_deps {
1342 if name != "python" {
1343 let version = extract_version_from_toml_value(value);
1344 deps.insert(name.clone(), version);
1345 }
1346 }
1347 }
1348
1349 if let Some(poetry_dev_deps) = parsed
1351 .get("tool")
1352 .and_then(|t| t.get("poetry"))
1353 .and_then(|p| p.get("dev-dependencies"))
1354 .and_then(|d| d.as_table())
1355 {
1356 for (name, value) in poetry_dev_deps {
1357 let version = extract_version_from_toml_value(value);
1358 deps.insert(format!("{} (dev)", name), version);
1359 }
1360 }
1361
1362 if let Some(project_deps) = parsed
1364 .get("project")
1365 .and_then(|p| p.get("dependencies"))
1366 .and_then(|d| d.as_array())
1367 {
1368 for dep in project_deps {
1369 if let Some(dep_str) = dep.as_str() {
1370 let parts: Vec<&str> = dep_str.split(&['=', '>', '<', '~', '!'][..]).collect();
1371 if !parts.is_empty() {
1372 let name = parts[0].trim();
1373 let version = if parts.len() > 1 {
1374 dep_str[name.len()..].trim().to_string()
1375 } else {
1376 "*".to_string()
1377 };
1378 deps.insert(name.to_string(), version);
1379 }
1380 }
1381 }
1382 }
1383 }
1384 }
1385
1386 Ok(deps)
1387}
1388
1389fn parse_python_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1391 let mut deps = DetailedDependencyMap::new();
1392
1393 let requirements_txt = project_root.join("requirements.txt");
1395 if requirements_txt.exists() {
1396 let content = fs::read_to_string(&requirements_txt)?;
1397 for line in content.lines() {
1398 if !line.trim().is_empty() && !line.starts_with('#') {
1399 let parts: Vec<&str> = line.split(&['=', '>', '<', '~', '!'][..]).collect();
1400 if !parts.is_empty() {
1401 let name = parts[0].trim();
1402 let version = if parts.len() > 1 {
1403 line[name.len()..].trim().to_string()
1404 } else {
1405 "*".to_string()
1406 };
1407 deps.insert(name.to_string(), LegacyDependencyInfo {
1408 version,
1409 is_dev: false,
1410 license: detect_pypi_license(name),
1411 vulnerabilities: vec![], source: "pypi".to_string(),
1413 });
1414 }
1415 }
1416 }
1417 }
1418
1419 let pyproject = project_root.join("pyproject.toml");
1421 if pyproject.exists() {
1422 let content = fs::read_to_string(&pyproject)?;
1423 if let Ok(parsed) = toml::from_str::<toml::Value>(&content) {
1424 if let Some(poetry_deps) = parsed
1426 .get("tool")
1427 .and_then(|t| t.get("poetry"))
1428 .and_then(|p| p.get("dependencies"))
1429 .and_then(|d| d.as_table())
1430 {
1431 for (name, value) in poetry_deps {
1432 if name != "python" {
1433 let version = extract_version_from_toml_value(value);
1434 deps.insert(name.clone(), LegacyDependencyInfo {
1435 version,
1436 is_dev: false,
1437 license: detect_pypi_license(name),
1438 vulnerabilities: vec![],
1439 source: "pypi".to_string(),
1440 });
1441 }
1442 }
1443 }
1444
1445 if let Some(poetry_dev_deps) = parsed
1447 .get("tool")
1448 .and_then(|t| t.get("poetry"))
1449 .and_then(|p| p.get("dev-dependencies"))
1450 .and_then(|d| d.as_table())
1451 {
1452 for (name, value) in poetry_dev_deps {
1453 let version = extract_version_from_toml_value(value);
1454 deps.insert(name.clone(), LegacyDependencyInfo {
1455 version,
1456 is_dev: true,
1457 license: detect_pypi_license(name),
1458 vulnerabilities: vec![],
1459 source: "pypi".to_string(),
1460 });
1461 }
1462 }
1463 }
1464 }
1465
1466 Ok(deps)
1467}
1468
1469fn parse_go_dependencies(project_root: &Path) -> Result<DependencyMap> {
1471 let go_mod = project_root.join("go.mod");
1472 if !go_mod.exists() {
1473 return Ok(DependencyMap::new());
1474 }
1475
1476 let content = fs::read_to_string(&go_mod)?;
1477 let mut deps = DependencyMap::new();
1478 let mut in_require_block = false;
1479
1480 for line in content.lines() {
1481 let trimmed = line.trim();
1482
1483 if trimmed.starts_with("require (") {
1484 in_require_block = true;
1485 continue;
1486 }
1487
1488 if in_require_block && trimmed == ")" {
1489 in_require_block = false;
1490 continue;
1491 }
1492
1493 if in_require_block || trimmed.starts_with("require ") {
1494 let parts: Vec<&str> = trimmed
1495 .trim_start_matches("require ")
1496 .split_whitespace()
1497 .collect();
1498
1499 if parts.len() >= 2 {
1500 let name = parts[0];
1501 let version = parts[1];
1502 deps.insert(name.to_string(), version.to_string());
1503 }
1504 }
1505 }
1506
1507 Ok(deps)
1508}
1509
1510fn parse_go_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1512 let go_mod = project_root.join("go.mod");
1513 if !go_mod.exists() {
1514 return Ok(DetailedDependencyMap::new());
1515 }
1516
1517 let content = fs::read_to_string(&go_mod)?;
1518 let mut deps = DetailedDependencyMap::new();
1519 let mut in_require_block = false;
1520
1521 for line in content.lines() {
1522 let trimmed = line.trim();
1523
1524 if trimmed.starts_with("require (") {
1525 in_require_block = true;
1526 continue;
1527 }
1528
1529 if in_require_block && trimmed == ")" {
1530 in_require_block = false;
1531 continue;
1532 }
1533
1534 if in_require_block || trimmed.starts_with("require ") {
1535 let parts: Vec<&str> = trimmed
1536 .trim_start_matches("require ")
1537 .split_whitespace()
1538 .collect();
1539
1540 if parts.len() >= 2 {
1541 let name = parts[0];
1542 let version = parts[1];
1543 let is_indirect = parts.len() > 2 && parts.contains(&"//") && parts.contains(&"indirect");
1544
1545 deps.insert(name.to_string(), LegacyDependencyInfo {
1546 version: version.to_string(),
1547 is_dev: is_indirect,
1548 license: detect_go_license(name),
1549 vulnerabilities: vec![], source: "go modules".to_string(),
1551 });
1552 }
1553 }
1554 }
1555
1556 Ok(deps)
1557}
1558
1559fn parse_jvm_dependencies(project_root: &Path) -> Result<DependencyMap> {
1561 let mut deps = DependencyMap::new();
1562
1563 let pom_xml = project_root.join("pom.xml");
1565 if pom_xml.exists() {
1566 let content = fs::read_to_string(&pom_xml)?;
1569 let lines: Vec<&str> = content.lines().collect();
1570
1571 for i in 0..lines.len() {
1572 if lines[i].contains("<dependency>") {
1573 let mut group_id = "";
1574 let mut artifact_id = "";
1575 let mut version = "";
1576
1577 for j in i..lines.len() {
1578 if lines[j].contains("</dependency>") {
1579 break;
1580 }
1581 if lines[j].contains("<groupId>") {
1582 group_id = extract_xml_value(lines[j], "groupId");
1583 }
1584 if lines[j].contains("<artifactId>") {
1585 artifact_id = extract_xml_value(lines[j], "artifactId");
1586 }
1587 if lines[j].contains("<version>") {
1588 version = extract_xml_value(lines[j], "version");
1589 }
1590 }
1591
1592 if !group_id.is_empty() && !artifact_id.is_empty() {
1593 let name = format!("{}:{}", group_id, artifact_id);
1594 deps.insert(name, version.to_string());
1595 }
1596 }
1597 }
1598 }
1599
1600 let build_gradle = project_root.join("build.gradle");
1602 if build_gradle.exists() {
1603 let content = fs::read_to_string(&build_gradle)?;
1604
1605 for line in content.lines() {
1607 let trimmed = line.trim();
1608 if trimmed.starts_with("implementation") ||
1609 trimmed.starts_with("compile") ||
1610 trimmed.starts_with("testImplementation") ||
1611 trimmed.starts_with("testCompile") {
1612
1613 if let Some(dep_str) = extract_gradle_dependency(trimmed) {
1614 let parts: Vec<&str> = dep_str.split(':').collect();
1615 if parts.len() >= 3 {
1616 let name = format!("{}:{}", parts[0], parts[1]);
1617 let version = parts[2];
1618 let is_test = trimmed.starts_with("test");
1619 let key = if is_test { format!("{} (test)", name) } else { name };
1620 deps.insert(key, version.to_string());
1621 }
1622 }
1623 }
1624 }
1625 }
1626
1627 Ok(deps)
1628}
1629
1630fn parse_jvm_dependencies_detailed(project_root: &Path) -> Result<DetailedDependencyMap> {
1632 let mut deps = DetailedDependencyMap::new();
1633
1634 let pom_xml = project_root.join("pom.xml");
1636 if pom_xml.exists() {
1637 let content = fs::read_to_string(&pom_xml)?;
1638 let lines: Vec<&str> = content.lines().collect();
1639
1640 for i in 0..lines.len() {
1641 if lines[i].contains("<dependency>") {
1642 let mut group_id = "";
1643 let mut artifact_id = "";
1644 let mut version = "";
1645 let mut scope = "compile";
1646
1647 for j in i..lines.len() {
1648 if lines[j].contains("</dependency>") {
1649 break;
1650 }
1651 if lines[j].contains("<groupId>") {
1652 group_id = extract_xml_value(lines[j], "groupId");
1653 }
1654 if lines[j].contains("<artifactId>") {
1655 artifact_id = extract_xml_value(lines[j], "artifactId");
1656 }
1657 if lines[j].contains("<version>") {
1658 version = extract_xml_value(lines[j], "version");
1659 }
1660 if lines[j].contains("<scope>") {
1661 scope = extract_xml_value(lines[j], "scope");
1662 }
1663 }
1664
1665 if !group_id.is_empty() && !artifact_id.is_empty() {
1666 let name = format!("{}:{}", group_id, artifact_id);
1667 deps.insert(name.clone(), LegacyDependencyInfo {
1668 version: version.to_string(),
1669 is_dev: scope == "test" || scope == "provided",
1670 license: detect_maven_license(&name),
1671 vulnerabilities: vec![], source: "maven".to_string(),
1673 });
1674 }
1675 }
1676 }
1677 }
1678
1679 let build_gradle = project_root.join("build.gradle");
1681 if build_gradle.exists() {
1682 let content = fs::read_to_string(&build_gradle)?;
1683
1684 for line in content.lines() {
1685 let trimmed = line.trim();
1686 if trimmed.starts_with("implementation") ||
1687 trimmed.starts_with("compile") ||
1688 trimmed.starts_with("testImplementation") ||
1689 trimmed.starts_with("testCompile") {
1690
1691 if let Some(dep_str) = extract_gradle_dependency(trimmed) {
1692 let parts: Vec<&str> = dep_str.split(':').collect();
1693 if parts.len() >= 3 {
1694 let name = format!("{}:{}", parts[0], parts[1]);
1695 let version = parts[2];
1696 let is_test = trimmed.starts_with("test");
1697
1698 deps.insert(name.clone(), LegacyDependencyInfo {
1699 version: version.to_string(),
1700 is_dev: is_test,
1701 license: detect_maven_license(&name),
1702 vulnerabilities: vec![],
1703 source: "gradle".to_string(),
1704 });
1705 }
1706 }
1707 }
1708 }
1709 }
1710
1711 Ok(deps)
1712}
1713
1714fn extract_version_from_toml_value(value: &toml::Value) -> String {
1717 match value {
1718 toml::Value::String(s) => s.clone(),
1719 toml::Value::Table(t) => {
1720 t.get("version")
1721 .and_then(|v| v.as_str())
1722 .unwrap_or("*")
1723 .to_string()
1724 }
1725 _ => "*".to_string(),
1726 }
1727}
1728
1729fn extract_xml_value<'a>(line: &'a str, tag: &str) -> &'a str {
1730 let start_tag = format!("<{}>", tag);
1731 let end_tag = format!("</{}>", tag);
1732
1733 if let Some(start) = line.find(&start_tag) {
1734 if let Some(end) = line.find(&end_tag) {
1735 return &line[start + start_tag.len()..end];
1736 }
1737 }
1738 ""
1739}
1740
1741fn extract_gradle_dependency(line: &str) -> Option<&str> {
1742 if let Some(start) = line.find('\'') {
1744 if let Some(end) = line.rfind('\'') {
1745 if start < end {
1746 return Some(&line[start + 1..end]);
1747 }
1748 }
1749 }
1750 if let Some(start) = line.find('"') {
1751 if let Some(end) = line.rfind('"') {
1752 if start < end {
1753 return Some(&line[start + 1..end]);
1754 }
1755 }
1756 }
1757 None
1758}
1759
1760fn detect_rust_license(crate_name: &str) -> Option<String> {
1763 match crate_name {
1765 "serde" | "serde_json" | "tokio" | "clap" => Some("MIT OR Apache-2.0".to_string()),
1766 "actix-web" => Some("MIT OR Apache-2.0".to_string()),
1767 _ => Some("Unknown".to_string()),
1768 }
1769}
1770
1771fn detect_npm_license(package_name: &str) -> Option<String> {
1772 match package_name {
1774 "react" | "vue" | "angular" => Some("MIT".to_string()),
1775 "express" => Some("MIT".to_string()),
1776 "webpack" => Some("MIT".to_string()),
1777 _ => Some("Unknown".to_string()),
1778 }
1779}
1780
1781fn detect_pypi_license(package_name: &str) -> Option<String> {
1782 match package_name {
1784 "django" => Some("BSD-3-Clause".to_string()),
1785 "flask" => Some("BSD-3-Clause".to_string()),
1786 "requests" => Some("Apache-2.0".to_string()),
1787 "numpy" | "pandas" => Some("BSD-3-Clause".to_string()),
1788 _ => Some("Unknown".to_string()),
1789 }
1790}
1791
1792fn detect_go_license(module_name: &str) -> Option<String> {
1793 if module_name.starts_with("github.com/gin-gonic/") {
1795 Some("MIT".to_string())
1796 } else if module_name.starts_with("github.com/gorilla/") {
1797 Some("BSD-3-Clause".to_string())
1798 } else {
1799 Some("Unknown".to_string())
1800 }
1801}
1802
1803fn detect_maven_license(artifact: &str) -> Option<String> {
1804 if artifact.starts_with("org.springframework") {
1806 Some("Apache-2.0".to_string())
1807 } else if artifact.starts_with("junit:junit") {
1808 Some("EPL-1.0".to_string())
1809 } else {
1810 Some("Unknown".to_string())
1811 }
1812}
1813
1814#[cfg(test)]
1815mod tests {
1816 use super::*;
1817 use tempfile::TempDir;
1818 use std::fs;
1819
1820 #[test]
1821 fn test_parse_rust_dependencies() {
1822 let temp_dir = TempDir::new().unwrap();
1823 let cargo_toml = r#"
1824[package]
1825name = "test"
1826version = "0.1.0"
1827
1828[dependencies]
1829serde = "1.0"
1830tokio = { version = "1.0", features = ["full"] }
1831
1832[dev-dependencies]
1833assert_cmd = "2.0"
1834"#;
1835
1836 fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml).unwrap();
1837
1838 let deps = parse_rust_dependencies(temp_dir.path()).unwrap();
1839 assert_eq!(deps.get("serde"), Some(&"1.0".to_string()));
1840 assert_eq!(deps.get("tokio"), Some(&"1.0".to_string()));
1841 assert_eq!(deps.get("assert_cmd (dev)"), Some(&"2.0".to_string()));
1842 }
1843
1844 #[test]
1845 fn test_parse_js_dependencies() {
1846 let temp_dir = TempDir::new().unwrap();
1847 let package_json = r#"{
1848 "name": "test",
1849 "version": "1.0.0",
1850 "dependencies": {
1851 "express": "^4.18.0",
1852 "react": "^18.0.0"
1853 },
1854 "devDependencies": {
1855 "jest": "^29.0.0"
1856 }
1857}"#;
1858
1859 fs::write(temp_dir.path().join("package.json"), package_json).unwrap();
1860
1861 let deps = parse_js_dependencies(temp_dir.path()).unwrap();
1862 assert_eq!(deps.get("express"), Some(&"^4.18.0".to_string()));
1863 assert_eq!(deps.get("react"), Some(&"^18.0.0".to_string()));
1864 assert_eq!(deps.get("jest (dev)"), Some(&"^29.0.0".to_string()));
1865 }
1866
1867 #[test]
1868 fn test_vulnerability_severity() {
1869 let vuln = Vulnerability {
1870 id: "CVE-2023-1234".to_string(),
1871 severity: VulnerabilitySeverity::High,
1872 description: "Test vulnerability".to_string(),
1873 fixed_in: Some("1.0.1".to_string()),
1874 };
1875
1876 assert!(matches!(vuln.severity, VulnerabilitySeverity::High));
1877 }
1878
1879 #[test]
1880 fn test_parse_python_requirement_spec() {
1881 let parser = DependencyParser::new();
1882
1883 let (name, version) = parser.parse_python_requirement_spec("requests");
1885 assert_eq!(name, "requests");
1886 assert_eq!(version, "*");
1887
1888 let (name, version) = parser.parse_python_requirement_spec("requests==2.28.0");
1890 assert_eq!(name, "requests");
1891 assert_eq!(version, "==2.28.0");
1892
1893 let (name, version) = parser.parse_python_requirement_spec("requests>=2.25.0,<3.0.0");
1895 assert_eq!(name, "requests");
1896 assert_eq!(version, ">=2.25.0,<3.0.0");
1897
1898 let (name, version) = parser.parse_python_requirement_spec("fastapi[all]>=0.95.0");
1900 assert_eq!(name, "fastapi");
1901 assert_eq!(version, ">=0.95.0");
1902
1903 let (name, version) = parser.parse_python_requirement_spec("django~=4.1.0");
1905 assert_eq!(name, "django");
1906 assert_eq!(version, "~=4.1.0");
1907 }
1908
1909 #[test]
1910 fn test_parse_pyproject_toml_poetry() {
1911 use std::fs;
1912 use tempfile::tempdir;
1913
1914 let dir = tempdir().unwrap();
1915 let pyproject_path = dir.path().join("pyproject.toml");
1916
1917 let pyproject_content = r#"
1918[tool.poetry]
1919name = "test-project"
1920version = "0.1.0"
1921
1922[tool.poetry.dependencies]
1923python = "^3.9"
1924fastapi = "^0.95.0"
1925uvicorn = {extras = ["standard"], version = "^0.21.0"}
1926
1927[tool.poetry.group.dev.dependencies]
1928pytest = "^7.0.0"
1929black = "^23.0.0"
1930"#;
1931
1932 fs::write(&pyproject_path, pyproject_content).unwrap();
1933
1934 let parser = DependencyParser::new();
1935 let deps = parser.parse_python_deps(dir.path()).unwrap();
1936
1937 assert!(!deps.is_empty());
1938
1939 let fastapi = deps.iter().find(|d| d.name == "fastapi");
1941 assert!(fastapi.is_some());
1942 assert!(matches!(fastapi.unwrap().dep_type, DependencyType::Production));
1943
1944 let uvicorn = deps.iter().find(|d| d.name == "uvicorn");
1945 assert!(uvicorn.is_some());
1946 assert!(matches!(uvicorn.unwrap().dep_type, DependencyType::Production));
1947
1948 let pytest = deps.iter().find(|d| d.name == "pytest");
1950 assert!(pytest.is_some());
1951 assert!(matches!(pytest.unwrap().dep_type, DependencyType::Dev));
1952
1953 let black = deps.iter().find(|d| d.name == "black");
1954 assert!(black.is_some());
1955 assert!(matches!(black.unwrap().dep_type, DependencyType::Dev));
1956
1957 assert!(deps.iter().find(|d| d.name == "python").is_none());
1959 }
1960
1961 #[test]
1962 fn test_parse_pyproject_toml_pep621() {
1963 use std::fs;
1964 use tempfile::tempdir;
1965
1966 let dir = tempdir().unwrap();
1967 let pyproject_path = dir.path().join("pyproject.toml");
1968
1969 let pyproject_content = r#"
1970[project]
1971name = "test-project"
1972version = "0.1.0"
1973dependencies = [
1974 "fastapi>=0.95.0",
1975 "uvicorn[standard]>=0.21.0",
1976 "pydantic>=1.10.0"
1977]
1978
1979[project.optional-dependencies]
1980test = [
1981 "pytest>=7.0.0",
1982 "pytest-cov>=4.0.0"
1983]
1984dev = [
1985 "black>=23.0.0",
1986 "mypy>=1.0.0"
1987]
1988"#;
1989
1990 fs::write(&pyproject_path, pyproject_content).unwrap();
1991
1992 let parser = DependencyParser::new();
1993 let deps = parser.parse_python_deps(dir.path()).unwrap();
1994
1995 assert!(!deps.is_empty());
1996
1997 let prod_deps: Vec<_> = deps.iter().filter(|d| matches!(d.dep_type, DependencyType::Production)).collect();
1999 assert_eq!(prod_deps.len(), 3);
2000 assert!(prod_deps.iter().any(|d| d.name == "fastapi"));
2001 assert!(prod_deps.iter().any(|d| d.name == "uvicorn"));
2002 assert!(prod_deps.iter().any(|d| d.name == "pydantic"));
2003
2004 let dev_deps: Vec<_> = deps.iter().filter(|d| matches!(d.dep_type, DependencyType::Dev)).collect();
2006 assert!(dev_deps.iter().any(|d| d.name == "pytest"));
2007 assert!(dev_deps.iter().any(|d| d.name == "black"));
2008 assert!(dev_deps.iter().any(|d| d.name == "mypy"));
2009
2010 let test_deps: Vec<_> = deps.iter().filter(|d| d.name == "pytest-cov").collect();
2012 assert_eq!(test_deps.len(), 1);
2013 assert!(matches!(test_deps[0].dep_type, DependencyType::Dev));
2014 }
2015
2016 #[test]
2017 fn test_parse_pipfile() {
2018 use std::fs;
2019 use tempfile::tempdir;
2020
2021 let dir = tempdir().unwrap();
2022 let pipfile_path = dir.path().join("Pipfile");
2023
2024 let pipfile_content = r#"
2025[[source]]
2026url = "https://pypi.org/simple"
2027verify_ssl = true
2028name = "pypi"
2029
2030[packages]
2031django = "~=4.1.0"
2032django-rest-framework = "*"
2033psycopg2 = ">=2.9.0"
2034
2035[dev-packages]
2036pytest = "*"
2037flake8 = "*"
2038black = ">=22.0.0"
2039"#;
2040
2041 fs::write(&pipfile_path, pipfile_content).unwrap();
2042
2043 let parser = DependencyParser::new();
2044 let deps = parser.parse_python_deps(dir.path()).unwrap();
2045
2046 assert!(!deps.is_empty());
2047
2048 let prod_deps: Vec<_> = deps.iter().filter(|d| matches!(d.dep_type, DependencyType::Production)).collect();
2050 assert_eq!(prod_deps.len(), 3);
2051 assert!(prod_deps.iter().any(|d| d.name == "django"));
2052 assert!(prod_deps.iter().any(|d| d.name == "django-rest-framework"));
2053 assert!(prod_deps.iter().any(|d| d.name == "psycopg2"));
2054
2055 let dev_deps: Vec<_> = deps.iter().filter(|d| matches!(d.dep_type, DependencyType::Dev)).collect();
2057 assert_eq!(dev_deps.len(), 3);
2058 assert!(dev_deps.iter().any(|d| d.name == "pytest"));
2059 assert!(dev_deps.iter().any(|d| d.name == "flake8"));
2060 assert!(dev_deps.iter().any(|d| d.name == "black"));
2061 }
2062
2063 #[test]
2064 fn test_dependency_analysis_summary() {
2065 let mut deps = DetailedDependencyMap::new();
2066 deps.insert("prod-dep".to_string(), LegacyDependencyInfo {
2067 version: "1.0.0".to_string(),
2068 is_dev: false,
2069 license: Some("MIT".to_string()),
2070 vulnerabilities: vec![],
2071 source: "npm".to_string(),
2072 });
2073 deps.insert("dev-dep".to_string(), LegacyDependencyInfo {
2074 version: "2.0.0".to_string(),
2075 is_dev: true,
2076 license: Some("MIT".to_string()),
2077 vulnerabilities: vec![],
2078 source: "npm".to_string(),
2079 });
2080
2081 let analysis = DependencyAnalysis {
2082 dependencies: deps,
2083 total_count: 2,
2084 production_count: 1,
2085 dev_count: 1,
2086 vulnerable_count: 0,
2087 license_summary: {
2088 let mut map = HashMap::new();
2089 map.insert("MIT".to_string(), 2);
2090 map
2091 },
2092 };
2093
2094 assert_eq!(analysis.total_count, 2);
2095 assert_eq!(analysis.production_count, 1);
2096 assert_eq!(analysis.dev_count, 1);
2097 assert_eq!(analysis.license_summary.get("MIT"), Some(&2));
2098 }
2099}