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