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