Spaces:
Running
Running
| use pyo3::prelude::*; | |
| use pyo3::types::PyDict; | |
| use regex::Regex; | |
| use regex::RegexSet; | |
| // ββ Secret scanning ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const SECRET_PATTERNS: &[(&str, &str)] = &[ | |
| ( | |
| r"-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY", | |
| "Private key", | |
| ), | |
| (r"AKIA[0-9A-Z]{16}", "AWS Access Key ID"), | |
| ( | |
| r#"(?i)(?:password|passwd|pwd)\s*=\s*["\'][^"\']{6,}["\']"#, | |
| "Hardcoded password", | |
| ), | |
| ( | |
| r#"(?i)(?:api[_-]?key|apikey)\s*=\s*["\'][^"\']{8,}["\']"#, | |
| "Hardcoded API key", | |
| ), | |
| ( | |
| r#"(?i)(?:secret|token)\s*=\s*["\'][^"\']{8,}["\']"#, | |
| "Hardcoded secret/token", | |
| ), | |
| (r"ghp_[a-zA-Z0-9]{36}", "GitHub Personal Access Token"), | |
| (r"gsk_[a-zA-Z0-9_]{48}", "Groq API Key"), | |
| (r"sk-[a-zA-Z0-9]{48}", "OpenAI API Key"), | |
| ( | |
| r"(?i)mongodb(?:\+srv)?://[^@\s]+@", | |
| "MongoDB connection string with credentials", | |
| ), | |
| ( | |
| r"(?i)postgres(?:ql)?://[^@\s]+:[^@\s]+@", | |
| "PostgreSQL connection string with credentials", | |
| ), | |
| ]; | |
| const SKIP_PATH_RE: &str = r"(?i)(test|spec|mock|fixture|example|sample|\.example|placeholder)"; | |
| const CRITICAL_KEYWORDS: &[&str] = &["Private key", "AWS", "Token", "OpenAI", "Groq"]; | |
| // ββ Sensitive file scanning ββββββββββββββββββββββββββββββββββββββββββββββ | |
| const SENSITIVE_FILE_RE: &str = r"(?i)(^|/)(\.env(\.|$)|credentials?\.json|secrets?\.(yaml|yml|json|toml)|.*\.pem$|.*\.key$|.*\.p12$|.*\.pfx$|id_rsa|id_dsa|id_ecdsa|id_ed25519|\.netrc$|\.aws/credentials|service.?account.*\.json|.*\.keystore$)"; | |
| // ββ Route pattern detection ββββββββββββββββββββββββββββββββββββββββββββββ | |
| const ROUTE_PATTERNS: &[&str] = &[ | |
| r"@app\.(get|post|put|delete|patch)\s*\(", | |
| r"@router\.(get|post|put|delete|patch)\s*\(", | |
| r"router\.(get|post|put|delete|patch)\s*\(", | |
| r"app\.(get|post|put|delete|patch)\s*\(", | |
| r"@(Get|Post|Put|Delete|Patch|Options|Head)\s*\(", | |
| r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\s*\(", | |
| r"path\s*\(", | |
| r"url\s*\(", | |
| r"resources?\s+:[a-z_]+", | |
| r#"(get|post|put|delete|patch)\s+["\'][/:]"#, | |
| r"r\.(GET|POST|PUT|DELETE|PATCH)\s*\(", | |
| r"e\.(GET|POST|PUT|DELETE|PATCH)\s*\(", | |
| r"mux\.(HandleFunc|Handle)\s*\(", | |
| ]; | |
| // ββ Comment ratio ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const COMMENT_RE: &str = r#"^\s*(#|//|/\*|\*|""")"#; | |
| const CODE_EXTS: &[&str] = &[ | |
| ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".go", ".rb", ".php", | |
| ]; | |
| // ββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| pub fn scan_secrets( | |
| py: Python<'_>, | |
| file_contents: std::collections::HashMap<String, String>, | |
| ) -> Vec<PyObject> { | |
| let skip_re = Regex::new(SKIP_PATH_RE).unwrap(); | |
| let set = RegexSet::new(SECRET_PATTERNS.iter().map(|(r, _)| r)).unwrap(); | |
| let regexes: Vec<Regex> = set | |
| .patterns() | |
| .iter() | |
| .map(|p| Regex::new(p).unwrap()) | |
| .collect(); | |
| let mut results = Vec::new(); | |
| for (path, content) in &file_contents { | |
| if skip_re.is_match(path) { | |
| continue; | |
| } | |
| for (idx, re) in regexes.iter().enumerate() { | |
| if let Some(m) = re.find(content) { | |
| let line_no = content[..m.start()].matches('\n').count() as u32 + 1; | |
| let label = SECRET_PATTERNS[idx].1; | |
| let sev = if CRITICAL_KEYWORDS.iter().any(|k| label.contains(k)) { | |
| "critical" | |
| } else { | |
| "high" | |
| }; | |
| let d = PyDict::new(py); | |
| d.set_item("path", path).ok(); | |
| d.set_item("line", line_no).ok(); | |
| d.set_item("type", label).ok(); | |
| let snippet = &m.as_str()[..m.as_str().len().min(80)]; | |
| d.set_item("snippet", snippet).ok(); | |
| d.set_item("severity", sev).ok(); | |
| results.push(d.into()); | |
| } | |
| } | |
| } | |
| results | |
| } | |
| pub fn scan_sensitive_files(py: Python<'_>, file_tree: Vec<String>) -> Vec<PyObject> { | |
| let re = Regex::new(SENSITIVE_FILE_RE).unwrap(); | |
| let mut results = Vec::new(); | |
| for path in &file_tree { | |
| if re.is_match(path) { | |
| let d = PyDict::new(py); | |
| d.set_item("path", path).ok(); | |
| d.set_item("reason", "Sensitive filename tracked in git") | |
| .ok(); | |
| results.push(d.into()); | |
| } | |
| } | |
| results | |
| } | |
| pub fn scan_route_files(file_contents: std::collections::HashMap<String, String>) -> Vec<String> { | |
| let set = RegexSet::new(ROUTE_PATTERNS).unwrap(); | |
| let mut results = Vec::new(); | |
| for (path, content) in &file_contents { | |
| if set.is_match(content) { | |
| results.push(path.clone()); | |
| } | |
| } | |
| results | |
| } | |
| pub fn compute_docs_score(file_contents: std::collections::HashMap<String, String>) -> f64 { | |
| let comment_re = Regex::new(COMMENT_RE).unwrap(); | |
| let mut total_lines = 0usize; | |
| let mut comment_lines = 0usize; | |
| for (path, content) in &file_contents { | |
| let ext = path | |
| .rsplit('.') | |
| .next() | |
| .map(|e| format!(".{}", e)) | |
| .unwrap_or_default(); | |
| if !CODE_EXTS.contains(&ext.as_str()) { | |
| continue; | |
| } | |
| for line in content.lines() { | |
| total_lines += 1; | |
| if comment_re.is_match(line) { | |
| comment_lines += 1; | |
| } | |
| } | |
| } | |
| if total_lines == 0 { | |
| return 50.0; | |
| } | |
| let ratio = comment_lines as f64 / total_lines as f64; | |
| (ratio * 1000.0).min(100.0) | |
| } | |