1use crate::error::Result;
10use chrono::Utc;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14
15pub mod context;
16pub mod dclint;
17pub mod dependency_parser;
18pub mod display;
19pub mod docker_analyzer;
20pub mod framework_detector;
21pub mod frameworks;
22pub mod hadolint;
23pub mod helmlint;
24pub mod k8s_optimize;
25pub mod kubelint;
26pub mod language_detector;
27pub mod monorepo;
28pub mod runtime;
29pub mod security;
30pub mod security_analyzer;
31pub mod tool_management;
32pub mod vulnerability;
33
34pub use dependency_parser::{DependencyAnalysis, DependencyInfo, DetailedDependencyMap};
36
37pub use security_analyzer::{
39 ComplianceStatus, SecurityAnalysisConfig, SecurityAnalyzer, SecurityCategory, SecurityFinding,
40 SecurityReport, SecuritySeverity,
41};
42
43pub use security::SecretPatternManager;
45pub use security::config::SecurityConfigPreset;
46
47pub use tool_management::{InstallationSource, ToolDetector, ToolInstaller, ToolStatus};
49
50pub use runtime::{
52 DetectionConfidence, JavaScriptRuntime, PackageManager, RuntimeDetectionResult, RuntimeDetector,
53};
54
55pub use vulnerability::types::VulnerabilitySeverity as VulnSeverity;
57pub use vulnerability::{
58 VulnerabilityChecker, VulnerabilityInfo, VulnerabilityReport, VulnerableDependency,
59};
60
61pub use monorepo::{MonorepoDetectionConfig, analyze_monorepo, analyze_monorepo_with_config};
63
64pub use docker_analyzer::{
66 ComposeFileInfo, DiscoveredDockerfile, DockerAnalysis, DockerEnvironment, DockerService,
67 DockerfileInfo, NetworkingConfig, OrchestrationPattern, analyze_docker_infrastructure,
68 discover_dockerfiles_for_deployment,
69};
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73pub struct DetectedLanguage {
74 pub name: String,
75 pub version: Option<String>,
76 pub confidence: f32,
77 pub files: Vec<PathBuf>,
78 pub main_dependencies: Vec<String>,
79 pub dev_dependencies: Vec<String>,
80 pub package_manager: Option<String>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
85pub enum TechnologyCategory {
86 MetaFramework,
88 FrontendFramework,
90 BackendFramework,
92 Library(LibraryType),
94 BuildTool,
96 Database,
98 Testing,
100 Runtime,
102 PackageManager,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
108pub enum LibraryType {
109 UI,
111 StateManagement,
113 DataFetching,
115 Routing,
117 Styling,
119 Utility,
121 HttpClient,
123 Authentication,
125 CLI,
127 Other(String),
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
133pub struct DetectedTechnology {
134 pub name: String,
135 pub version: Option<String>,
136 pub category: TechnologyCategory,
137 pub confidence: f32,
138 pub requires: Vec<String>,
140 pub conflicts_with: Vec<String>,
142 pub is_primary: bool,
144 pub file_indicators: Vec<String>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
150pub struct ServiceAnalysis {
151 pub name: String,
152 pub path: PathBuf,
153 pub languages: Vec<DetectedLanguage>,
154 pub technologies: Vec<DetectedTechnology>,
155 pub entry_points: Vec<EntryPoint>,
156 pub ports: Vec<Port>,
157 pub environment_variables: Vec<EnvVar>,
158 pub build_scripts: Vec<BuildScript>,
159 pub service_type: ProjectType,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
164pub struct EntryPoint {
165 pub file: PathBuf,
166 pub function: Option<String>,
167 pub command: Option<String>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
172pub enum PortSource {
173 Dockerfile,
175 DockerCompose,
177 PackageJson,
179 FrameworkDefault,
181 EnvVar,
183 SourceCode,
185 ConfigFile,
187}
188
189impl PortSource {
190 pub fn description(&self) -> &'static str {
192 match self {
193 PortSource::Dockerfile => "Dockerfile EXPOSE",
194 PortSource::DockerCompose => "docker-compose.yml",
195 PortSource::PackageJson => "package.json scripts",
196 PortSource::FrameworkDefault => "framework default",
197 PortSource::EnvVar => "environment variable",
198 PortSource::SourceCode => "source code",
199 PortSource::ConfigFile => "configuration file",
200 }
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
206pub struct Port {
207 pub number: u16,
208 pub protocol: Protocol,
209 pub description: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
212 pub source: Option<PortSource>,
213}
214
215impl Port {
216 pub fn with_source(number: u16, protocol: Protocol, source: PortSource) -> Self {
218 Self {
219 number,
220 protocol,
221 description: None,
222 source: Some(source),
223 }
224 }
225
226 pub fn with_source_and_description(
228 number: u16,
229 protocol: Protocol,
230 source: PortSource,
231 description: impl Into<String>,
232 ) -> Self {
233 Self {
234 number,
235 protocol,
236 description: Some(description.into()),
237 source: Some(source),
238 }
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
243pub enum Protocol {
244 Tcp,
245 Udp,
246 Http,
247 Https,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
252pub enum HealthEndpointSource {
253 CodePattern,
255 FrameworkDefault,
257 ConfigFile,
259}
260
261impl HealthEndpointSource {
262 pub fn description(&self) -> &'static str {
264 match self {
265 HealthEndpointSource::CodePattern => "source code analysis",
266 HealthEndpointSource::FrameworkDefault => "framework convention",
267 HealthEndpointSource::ConfigFile => "configuration file",
268 }
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
274pub struct HealthEndpoint {
275 pub path: String,
277 pub confidence: f32,
279 pub source: HealthEndpointSource,
281 pub description: Option<String>,
283}
284
285impl HealthEndpoint {
286 pub fn from_code(path: impl Into<String>, confidence: f32) -> Self {
288 Self {
289 path: path.into(),
290 confidence,
291 source: HealthEndpointSource::CodePattern,
292 description: None,
293 }
294 }
295
296 pub fn from_framework(path: impl Into<String>, framework: &str) -> Self {
298 Self {
299 path: path.into(),
300 confidence: 0.7, source: HealthEndpointSource::FrameworkDefault,
302 description: Some(format!("{} default health endpoint", framework)),
303 }
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
309pub struct EnvVar {
310 pub name: String,
311 pub default_value: Option<String>,
312 pub required: bool,
313 pub description: Option<String>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
318pub enum ProjectType {
319 WebApplication,
320 ApiService,
321 CliTool,
322 Library,
323 MobileApp,
324 DesktopApp,
325 Microservice,
326 StaticSite,
327 Hybrid, Unknown,
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
333pub struct BuildScript {
334 pub name: String,
335 pub command: String,
336 pub description: Option<String>,
337 pub is_default: bool,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
342pub struct InfrastructurePresence {
343 pub has_kubernetes: bool,
345 pub kubernetes_paths: Vec<PathBuf>,
347 pub has_helm: bool,
349 pub helm_chart_paths: Vec<PathBuf>,
351 pub has_docker_compose: bool,
353 pub has_terraform: bool,
355 pub terraform_paths: Vec<PathBuf>,
357 pub has_deployment_config: bool,
359 pub summary: Option<String>,
361}
362
363impl InfrastructurePresence {
364 pub fn has_any(&self) -> bool {
366 self.has_kubernetes
367 || self.has_helm
368 || self.has_docker_compose
369 || self.has_terraform
370 || self.has_deployment_config
371 }
372
373 pub fn detected_types(&self) -> Vec<&'static str> {
375 let mut types = Vec::new();
376 if self.has_kubernetes {
377 types.push("Kubernetes");
378 }
379 if self.has_helm {
380 types.push("Helm");
381 }
382 if self.has_docker_compose {
383 types.push("Docker Compose");
384 }
385 if self.has_terraform {
386 types.push("Terraform");
387 }
388 if self.has_deployment_config {
389 types.push("Syncable Config");
390 }
391 types
392 }
393}
394
395pub type DependencyMap = HashMap<String, String>;
397
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
400pub enum ArchitectureType {
401 Monolithic,
403 Microservices,
405 Hybrid,
407}
408
409pub type DetectedFramework = DetectedTechnology;
411
412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
414pub struct ProjectAnalysis {
415 pub project_root: PathBuf,
416 pub languages: Vec<DetectedLanguage>,
417 pub technologies: Vec<DetectedTechnology>,
419 #[deprecated(note = "Use technologies field instead")]
421 pub frameworks: Vec<DetectedFramework>,
422 pub dependencies: DependencyMap,
423 pub entry_points: Vec<EntryPoint>,
424 pub ports: Vec<Port>,
425 #[serde(default)]
427 pub health_endpoints: Vec<HealthEndpoint>,
428 pub environment_variables: Vec<EnvVar>,
429 pub project_type: ProjectType,
430 pub build_scripts: Vec<BuildScript>,
431 pub services: Vec<ServiceAnalysis>,
433 pub architecture_type: ArchitectureType,
435 pub docker_analysis: Option<DockerAnalysis>,
437 #[serde(default)]
439 pub infrastructure: Option<InfrastructurePresence>,
440 pub analysis_metadata: AnalysisMetadata,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
445pub struct AnalysisMetadata {
446 pub timestamp: String,
447 pub analyzer_version: String,
448 pub analysis_duration_ms: u64,
449 pub files_analyzed: usize,
450 pub confidence_score: f32,
451}
452
453#[derive(Debug, Clone)]
455pub struct AnalysisConfig {
456 pub include_dev_dependencies: bool,
457 pub deep_analysis: bool,
458 pub ignore_patterns: Vec<String>,
459 pub max_file_size: usize,
460}
461
462impl Default for AnalysisConfig {
463 fn default() -> Self {
464 Self {
465 include_dev_dependencies: false,
466 deep_analysis: true,
467 ignore_patterns: vec![
468 "node_modules".to_string(),
469 ".git".to_string(),
470 "target".to_string(),
471 "build".to_string(),
472 ".next".to_string(),
473 "dist".to_string(),
474 ],
475 max_file_size: 1024 * 1024, }
477 }
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
482pub struct ProjectInfo {
483 pub path: PathBuf,
485 pub name: String,
487 pub project_category: ProjectCategory,
489 pub analysis: ProjectAnalysis,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
495pub enum ProjectCategory {
496 Frontend,
497 Backend,
498 Api,
499 Service,
500 Library,
501 Tool,
502 Documentation,
503 Infrastructure,
504 Unknown,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
509pub struct MonorepoAnalysis {
510 pub root_path: PathBuf,
512 pub is_monorepo: bool,
514 pub projects: Vec<ProjectInfo>,
516 pub metadata: AnalysisMetadata,
518 pub technology_summary: TechnologySummary,
520}
521
522#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
524pub struct TechnologySummary {
525 pub languages: Vec<String>,
526 pub frameworks: Vec<String>,
527 pub databases: Vec<String>,
528 pub total_projects: usize,
529 pub architecture_pattern: ArchitecturePattern,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
534pub enum ArchitecturePattern {
535 Monolithic,
537 Fullstack,
539 Microservices,
541 ApiFirst,
543 EventDriven,
545 Mixed,
547}
548
549pub fn analyze_project(path: &Path) -> Result<ProjectAnalysis> {
569 analyze_project_with_config(path, &AnalysisConfig::default())
570}
571
572pub fn analyze_project_with_config(
574 path: &Path,
575 config: &AnalysisConfig,
576) -> Result<ProjectAnalysis> {
577 let start_time = std::time::Instant::now();
578
579 let project_root = crate::common::file_utils::validate_project_path(path)?;
581
582 log::info!("Starting analysis of project: {}", project_root.display());
583
584 let files = crate::common::file_utils::collect_project_files(&project_root, config)?;
586 log::debug!("Found {} files to analyze", files.len());
587
588 let languages = language_detector::detect_languages(&files, config)?;
590 let frameworks = framework_detector::detect_frameworks(&project_root, &languages, config)?;
591 let dependencies = dependency_parser::parse_dependencies(&project_root, &languages, config)?;
592 let context = context::analyze_context(&project_root, &languages, &frameworks, config)?;
593
594 let health_endpoints =
596 context::detect_health_endpoints(&project_root, &frameworks, config.max_file_size);
597
598 let infrastructure = context::detect_infrastructure(&project_root);
600
601 let docker_analysis = analyze_docker_infrastructure(&project_root).ok();
603
604 let duration = start_time.elapsed();
605 let confidence = calculate_confidence_score(&languages, &frameworks);
606
607 #[allow(deprecated)]
608 let analysis = ProjectAnalysis {
609 project_root,
610 languages,
611 technologies: frameworks.clone(), frameworks, dependencies,
614 entry_points: context.entry_points,
615 ports: context.ports,
616 health_endpoints,
617 environment_variables: context.environment_variables,
618 project_type: context.project_type,
619 build_scripts: context.build_scripts,
620 services: vec![], architecture_type: ArchitectureType::Monolithic, docker_analysis,
623 infrastructure: Some(infrastructure),
624 analysis_metadata: AnalysisMetadata {
625 timestamp: Utc::now().to_rfc3339(),
626 analyzer_version: env!("CARGO_PKG_VERSION").to_string(),
627 analysis_duration_ms: duration.as_millis() as u64,
628 files_analyzed: files.len(),
629 confidence_score: confidence,
630 },
631 };
632
633 log::info!("Analysis completed in {}ms", duration.as_millis());
634 Ok(analysis)
635}
636
637fn calculate_confidence_score(
639 languages: &[DetectedLanguage],
640 frameworks: &[DetectedFramework],
641) -> f32 {
642 if languages.is_empty() {
643 return 0.0;
644 }
645
646 let lang_confidence: f32 =
647 languages.iter().map(|l| l.confidence).sum::<f32>() / languages.len() as f32;
648 let framework_confidence: f32 = if frameworks.is_empty() {
649 0.5 } else {
651 frameworks.iter().map(|f| f.confidence).sum::<f32>() / frameworks.len() as f32
652 };
653
654 (lang_confidence * 0.7 + framework_confidence * 0.3).min(1.0)
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660
661 #[test]
662 fn test_confidence_calculation() {
663 let languages = vec![DetectedLanguage {
664 name: "Rust".to_string(),
665 version: Some("1.70.0".to_string()),
666 confidence: 0.9,
667 files: vec![],
668 main_dependencies: vec!["serde".to_string(), "tokio".to_string()],
669 dev_dependencies: vec!["assert_cmd".to_string()],
670 package_manager: Some("cargo".to_string()),
671 }];
672
673 let technologies = vec![DetectedTechnology {
674 name: "Actix Web".to_string(),
675 version: Some("4.0".to_string()),
676 category: TechnologyCategory::BackendFramework,
677 confidence: 0.8,
678 requires: vec!["serde".to_string(), "tokio".to_string()],
679 conflicts_with: vec![],
680 is_primary: true,
681 file_indicators: vec![],
682 }];
683
684 let frameworks = technologies.clone(); let score = calculate_confidence_score(&languages, &frameworks);
687 assert!(score > 0.8);
688 assert!(score <= 1.0);
689 }
690
691 #[test]
692 fn test_empty_analysis() {
693 let languages = vec![];
694 let frameworks = vec![];
695 let score = calculate_confidence_score(&languages, &frameworks);
696 assert_eq!(score, 0.0);
697 }
698}