Skip to main content

nargo_audit/
lib.rs

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        // Common secret patterns
54        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    /// Scan for secrets in the codebase
76    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    /// Scan for dangerous dependencies in package.json
98    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                    // Example check: deprecated or known dangerous packages
110                    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    /// Scan for dangerous code patterns
121    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}