1#![warn(missing_docs)]
2
3use nargo_linter::Severity;
4use nargo_types::{NargoContext, Result};
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7use std::{path::PathBuf, sync::Arc};
8use walkdir::WalkDir;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AuditIssue {
12 pub file: PathBuf,
13 pub line: usize,
14 pub column: usize,
15 pub code: String,
16 pub message: String,
17 pub severity: Severity,
18 pub category: AuditCategory,
19}
20
21impl AuditIssue {
22 pub fn category_name(&self) -> &'static str {
23 self.category.as_str()
24 }
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub enum AuditCategory {
29 Secret,
30 Dependency,
31 DangerousPattern,
32}
33
34impl AuditCategory {
35 pub fn as_str(&self) -> &'static str {
36 match self {
37 AuditCategory::Secret => "Secret",
38 AuditCategory::Dependency => "Dependency",
39 AuditCategory::DangerousPattern => "DangerousPattern",
40 }
41 }
42}
43
44pub struct NargoAudit {
45 ctx: Arc<NargoContext>,
46 secret_regexes: Vec<(String, Regex)>,
47}
48
49impl NargoAudit {
50 pub fn new(ctx: Arc<NargoContext>) -> Self {
51 let mut secret_regexes = Vec::new();
52
53 let patterns = vec![("AWS_KEY", r"AKIA[0-9A-Z]{16}"), ("GITHUB_TOKEN", r"ghp_[a-zA-Z0-9]{36}"), ("PRIVATE_KEY", r"-----BEGIN [A-Z ]+ PRIVATE KEY-----"), ("GENERIC_SECRET", r"(?i)secret|password|token|api_key|apikey")];
55
56 for (name, pattern) in patterns {
57 if let Ok(re) = Regex::new(pattern) {
58 secret_regexes.push((name.to_string(), re));
59 }
60 }
61
62 Self { ctx, secret_regexes }
63 }
64
65 pub async fn run_all(&self) -> Result<Vec<AuditIssue>> {
66 let mut issues = Vec::new();
67
68 issues.extend(self.audit_secrets().await?);
69 issues.extend(self.audit_dependencies().await?);
70 issues.extend(self.audit_dangerous_patterns().await?);
71
72 Ok(issues)
73 }
74
75 pub async fn audit_secrets(&self) -> Result<Vec<AuditIssue>> {
77 let mut issues = Vec::new();
78 let root = std::env::current_dir()?;
79
80 for entry in WalkDir::new(&root).into_iter().filter_entry(|e| !self.is_ignored(e)).filter_map(|e| e.ok()) {
81 if entry.file_type().is_file() {
82 let path = entry.path();
83 if let Ok(content) = std::fs::read_to_string(path) {
84 for (name, re) in &self.secret_regexes {
85 for mat in re.find_iter(&content) {
86 let (line, col) = self.get_line_col(&content, mat.start());
87 issues.push(AuditIssue { file: path.to_path_buf(), line, column: col, code: name.clone(), message: format!("Potential secret found: {}", name), severity: Severity::Error, category: AuditCategory::Secret });
88 }
89 }
90 }
91 }
92 }
93
94 Ok(issues)
95 }
96
97 pub async fn audit_dependencies(&self) -> Result<Vec<AuditIssue>> {
99 let mut issues = Vec::new();
100 let root = std::env::current_dir()?;
101 let package_json_path = root.join("package.json");
102
103 if package_json_path.exists() {
104 let content = std::fs::read_to_string(&package_json_path)?;
105 let json: serde_json::Value = serde_json::from_str(&content)?;
106
107 if let Some(deps) = json.get("dependencies").and_then(|d| d.as_object()) {
108 for (name, _version) in deps {
109 if name == "request" {
111 issues.push(AuditIssue { file: package_json_path.clone(), line: 0, column: 0, code: "DEPRECATED_DEP".to_string(), message: format!("Package '{}' is deprecated and may have security issues.", name), severity: Severity::Warning, category: AuditCategory::Dependency });
112 }
113 }
114 }
115 }
116
117 Ok(issues)
118 }
119
120 pub async fn audit_dangerous_patterns(&self) -> Result<Vec<AuditIssue>> {
122 let mut issues = Vec::new();
123 let root = std::env::current_dir()?;
124
125 let patterns = vec![("EVAL_USAGE", Regex::new(r"eval\s*\(").unwrap()), ("INNER_HTML", Regex::new(r"dangerouslySetInnerHTML").unwrap()), ("FUNCTION_CTOR", Regex::new(r"new\s+Function\s*\(").unwrap())];
126
127 for entry in WalkDir::new(&root).into_iter().filter_entry(|e| !self.is_ignored(e)).filter_map(|e| e.ok()) {
128 if entry.file_type().is_file() {
129 let path = entry.path();
130 let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
131 if matches!(ext, "ts" | "js" | "nargo" | "tsx" | "jsx") {
132 if let Ok(content) = std::fs::read_to_string(path) {
133 for (code, re) in &patterns {
134 for mat in re.find_iter(&content) {
135 let (line, col) = self.get_line_col(&content, mat.start());
136 issues.push(AuditIssue { file: path.to_path_buf(), line, column: col, code: code.to_string(), message: format!("Dangerous pattern detected: {}", code), severity: Severity::Warning, category: AuditCategory::DangerousPattern });
137 }
138 }
139 }
140 }
141 }
142 }
143
144 Ok(issues)
145 }
146
147 fn is_ignored(&self, entry: &walkdir::DirEntry) -> bool {
148 let name = entry.file_name().to_str().unwrap_or("");
149 name == "node_modules" || name == ".git" || name == "target" || name == "dist" || name == "dist-check" || name.ends_with(".html")
150 }
151
152 fn get_line_col(&self, content: &str, offset: usize) -> (usize, usize) {
153 let mut line = 1;
154 let mut col = 1;
155 for (i, c) in content.char_indices() {
156 if i == offset {
157 break;
158 }
159 if c == '\n' {
160 line += 1;
161 col = 1;
162 }
163 else {
164 col += 1;
165 }
166 }
167 (line, col)
168 }
169}