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