Skip to main content

forge_guard/security/
engine.rs

1//! Security engine — coordinates all vulnerability checks.
2
3use crate::chains::ChainRegistry;
4use crate::core::{Finding, ProjectConfig, SecurityScores, Severity};
5use crate::plugins::{PluginContext, PluginRegistry};
6use crate::security::checks;
7use std::path::Path;
8use std::sync::Arc;
9
10/// The security engine drives all vulnerability analysis.
11#[derive(Clone)]
12pub struct SecurityEngine {
13    config: Arc<crate::core::config::SecurityConfig>,
14    project_config: Arc<ProjectConfig>,
15    plugin_registry: Arc<PluginRegistry>,
16    check_registry: Vec<&'static checks::SecurityCheckMeta>,
17}
18
19impl SecurityEngine {
20    /// Create a new security engine with the given configuration.
21    pub fn new(
22        config: &ProjectConfig,
23        plugin_registry: &PluginRegistry,
24    ) -> Result<Self, crate::core::ForgeGuardError> {
25        let mut engine = Self {
26            config: Arc::new(config.security.clone()),
27            project_config: Arc::new(config.clone()),
28            plugin_registry: Arc::new(plugin_registry.clone()),
29            check_registry: Vec::new(),
30        };
31
32        // Register built-in checks based on config
33        engine.register_builtin_checks();
34        // Register plugin checks
35        engine.register_plugin_checks()?;
36
37        Ok(engine)
38    }
39
40    fn register_builtin_checks(&mut self) {
41        for check in checks::ALL_CHECKS {
42            let should_register = match check.severity {
43                "critical" | "high" => self.config.enable_high,
44                "medium" => self.config.enable_medium,
45                "low" => self.config.enable_low,
46                "informational" => self.config.enable_info,
47                _ => true,
48            };
49
50            // Check severity overrides
51            let effective_severity = self
52                .config
53                .severity_overrides
54                .get(check.name)
55                .map(|s| s.as_str())
56                .unwrap_or(check.severity);
57
58            if should_register && self.filter_by_severity(effective_severity) {
59                self.check_registry.push(check);
60            }
61        }
62    }
63
64    fn filter_by_severity(&self, severity: &str) -> bool {
65        match severity {
66            "critical" | "high" => self.config.enable_high,
67            "medium" => self.config.enable_medium,
68            "low" => self.config.enable_low,
69            "informational" => self.config.enable_info,
70            _ => true,
71        }
72    }
73
74    fn register_plugin_checks(&mut self) -> Result<(), crate::core::ForgeGuardError> {
75        // Plugin checks are loaded at runtime via `execute_all()` in `analyze_files`.
76        // This method exists as an extension point for future dynamic check registration.
77        Ok(())
78    }
79
80    /// Analyze a set of Solidity source files and return findings.
81    /// Includes findings from registered plugins (both built-in and external).
82    pub fn analyze_files(
83        &self,
84        files: &[std::path::PathBuf],
85        _chain_registry: &ChainRegistry,
86    ) -> Result<Vec<Finding>, crate::core::ForgeGuardError> {
87        use rayon::prelude::*;
88        let max_findings = self.config.max_findings_per_check;
89
90        // Process files in parallel using Rayon
91        let mut all_findings: Vec<Finding> = files
92            .par_iter()
93            .flat_map(|file| {
94                let content = match std::fs::read_to_string(file) {
95                    Ok(c) => c,
96                    Err(_) => return Vec::new(),
97                };
98
99                self.analyze_file(file, &content, max_findings)
100            })
101            .collect();
102
103        // ── Collect plugin findings ──
104        // Run enabled plugins against all source files at once
105        let plugin_ctx = PluginContext::new(&self.project_config, files.to_vec());
106        let plugin_results = self.plugin_registry.execute_all(&plugin_ctx);
107
108        for result in plugin_results {
109            if result.success {
110                all_findings.extend(result.findings);
111            } else if let Some(err) = &result.error {
112                eprintln!("  ⚠️  Plugin '{}' failed: {}", result.plugin_name, err);
113            }
114        }
115
116        Ok(all_findings)
117    }
118
119    /// Analyze a set of Solidity source files in quick mode.
120    /// Skips parser-heavy checks (reentrancy, access control) and only runs
121    /// pattern-based HIGH/CRITICAL checks for lightning-fast results.
122    /// Plugins are also skipped in quick mode.
123    pub fn analyze_files_quick(
124        &self,
125        files: &[std::path::PathBuf],
126        _chain_registry: &ChainRegistry,
127    ) -> Result<Vec<Finding>, crate::core::ForgeGuardError> {
128        use rayon::prelude::*;
129        // In quick mode: cap findings lower and only run high-severity checks
130        let max_findings = self.config.max_findings_per_check.min(15);
131
132        // Only register high/critical severity checks that use line-based patterns
133        let quick_checks: Vec<&&'static checks::SecurityCheckMeta> = self
134            .check_registry
135            .iter()
136            .filter(|check| {
137                // Only HIGH/CRITICAL checks
138                let sev = check.severity;
139                (sev == "high" || sev == "critical")
140                    // Skip parser-heavy checks (reentrancy, access control)
141                    && check.id != "FA-H-001"
142                    && check.id != "FA-H-002"
143            })
144            .collect();
145
146        let all_findings: Vec<Finding> = files
147            .par_iter()
148            .flat_map(|file| {
149                let content = match std::fs::read_to_string(file) {
150                    Ok(c) => c,
151                    Err(_) => return Vec::new(),
152                };
153
154                let file_name = file.to_string_lossy().to_string();
155                let lines: Vec<&str> = content.lines().collect();
156                let mut findings = Vec::new();
157
158                for check in &quick_checks {
159                    if findings.len() >= max_findings {
160                        break;
161                    }
162                    let check_findings = self.execute_check(check, &file_name, &lines, &content);
163                    findings.extend(check_findings);
164                }
165
166                findings
167            })
168            .collect();
169
170        // Skip plugins in quick mode
171        Ok(all_findings)
172    }
173
174    /// Analyze a single Solidity file for vulnerabilities.
175    fn analyze_file(&self, file: &Path, content: &str, max_findings: usize) -> Vec<Finding> {
176        let file_name = file.to_string_lossy().to_string();
177        let lines: Vec<&str> = content.lines().collect();
178        let mut findings = Vec::new();
179
180        // Run each registered check against this file
181        for check in &self.check_registry {
182            if findings.len() >= max_findings {
183                break;
184            }
185
186            let check_findings = self.execute_check(check, &file_name, &lines, content);
187            findings.extend(check_findings);
188        }
189
190        findings
191    }
192
193    /// Execute a single security check against source content.
194    fn execute_check(
195        &self,
196        check: &&'static checks::SecurityCheckMeta,
197        file_name: &str,
198        lines: &[&str],
199        content: &str,
200    ) -> Vec<Finding> {
201        match check.id {
202            // ── HIGH severity checks ───────────────────────
203            "FA-H-001" => self.check_reentrancy(file_name, lines, content),
204            "FA-H-002" => self.check_access_control(file_name, lines, content),
205            "FA-H-003" => self.check_delegatecall(file_name, lines, content),
206            "FA-H-004" => self.check_tx_origin(file_name, lines, content),
207            "FA-H-005" => self.check_create2(file_name, lines, content),
208            "FA-H-006" => self.check_dos(file_name, lines, content),
209            "FA-H-007" => self.check_storage_collision(file_name, lines, content),
210            "FA-H-008" => self.check_unsafe_assembly(file_name, lines, content),
211            "FA-H-009" => self.check_selfdestruct(file_name, lines, content),
212            "FA-H-010" => self.check_proxy_vulnerabilities(file_name, lines, content),
213            "FA-H-011" => self.check_oracle_manipulation(file_name, lines, content),
214            "FA-H-012" => self.check_signature_vulnerabilities(file_name, lines, content),
215            "FA-H-013" => self.check_replay_attacks(file_name, lines, content),
216            "FA-H-014" => self.check_erc20_issues(file_name, lines, content),
217            "FA-H-015" => self.check_bridge_vulnerabilities(file_name, lines, content),
218            "FA-H-016" => self.check_flash_loan_issues(file_name, lines, content),
219            "FA-H-017" => self.check_mev_issues(file_name, lines, content),
220            "FA-H-018" => self.check_cross_chain_issues(file_name, lines, content),
221            "FA-H-019" => self.check_dependency_vulnerabilities(file_name, lines, content),
222            "FA-H-020" => self.check_unsafe_imports(file_name, lines, content),
223            "FA-H-021" => self.check_unsafe_initializers(file_name, lines, content),
224            "FA-H-022" => self.check_unsafe_upgrade_paths(file_name, lines, content),
225            "FA-H-023" => self.check_clone_vulnerabilities(file_name, lines, content),
226
227            // ── MEDIUM severity checks ─────────────────────
228            "FA-M-001" => self.check_gas_problems(file_name, lines, content),
229            "FA-M-002" => self.check_unsafe_casting(file_name, lines, content),
230            "FA-M-003" => self.check_timestamp_manipulation(file_name, lines, content),
231            "FA-M-004" => self.check_storage_inefficiencies(file_name, lines, content),
232            "FA-M-005" => self.check_unsafe_events(file_name, lines, content),
233            "FA-M-006" => self.check_poor_visibility(file_name, lines, content),
234            "FA-M-007" => self.check_bad_modifiers(file_name, lines, content),
235            "FA-M-008" => self.check_unsafe_math(file_name, lines, content),
236            "FA-M-009" => self.check_poor_access_patterns(file_name, lines, content),
237
238            // ── LOW severity checks ────────────────────────
239            "FA-L-001" => self.check_naming_issues(file_name, lines, content),
240            "FA-L-002" => self.check_code_duplication(file_name, lines, content),
241            "FA-L-003" => self.check_optimization(file_name, lines, content),
242
243            // ── INFO checks ───────────────────────────────
244            "FA-I-001" => self.check_style_issues(file_name, lines, content),
245            "FA-I-002" => self.check_documentation(file_name, lines, content),
246
247            _ => Vec::new(),
248        }
249    }
250
251    /// Calculate security scores based on findings.
252    pub fn calculate_scores(&self, findings: &[Finding]) -> SecurityScores {
253        let mut scores = SecurityScores::perfect();
254
255        for finding in findings {
256            let deduction = match finding.severity {
257                Severity::Critical => 30,
258                Severity::High => 15,
259                Severity::Medium => 8,
260                Severity::Low => 3,
261                Severity::Informational => 1,
262            };
263
264            match finding.category.as_str() {
265                "Access Control" => {
266                    scores.access_control = scores.access_control.saturating_sub(deduction)
267                }
268                "DeFi" => {
269                    scores.security = scores.security.saturating_sub(deduction);
270                    scores.exploit_resistance = scores.exploit_resistance.saturating_sub(deduction);
271                }
272                "Upgradeability" => {
273                    scores.upgradeability = scores.upgradeability.saturating_sub(deduction);
274                    scores.proxy_safety = scores.proxy_safety.saturating_sub(deduction);
275                }
276                "Gas" => scores.gas = scores.gas.saturating_sub(deduction),
277                "Deployment" => scores.deployment = scores.deployment.saturating_sub(deduction),
278                "Dependencies" => {
279                    scores.dependencies = scores.dependencies.saturating_sub(deduction)
280                }
281                "Cross-Chain" => {
282                    scores.chain_compatibility =
283                        scores.chain_compatibility.saturating_sub(deduction)
284                }
285                "Logic" | "Security" => scores.security = scores.security.saturating_sub(deduction),
286                "Cryptography" => scores.security = scores.security.saturating_sub(deduction),
287                _ => scores.security = scores.security.saturating_sub(deduction),
288            }
289
290            // General deductions
291            scores.production_readiness = scores.production_readiness.saturating_sub(deduction / 2);
292            scores.architecture = scores.architecture.saturating_sub(deduction / 3);
293        }
294
295        scores
296    }
297
298    // ─────────────────────────────────────────────────────────
299    // Pattern-based check implementations
300    // ─────────────────────────────────────────────────────────
301
302    fn find_lines_containing<'a>(
303        &self,
304        lines: &[&'a str],
305        patterns: &[&str],
306    ) -> Vec<(usize, &'a str)> {
307        let mut results = Vec::new();
308        for (i, line) in lines.iter().enumerate() {
309            for pattern in patterns {
310                if line.contains(pattern)
311                    && !line.trim_start().starts_with("//")
312                    && !line.trim_start().starts_with("/*")
313                {
314                    results.push((i + 1, line.trim()));
315                    break;
316                }
317            }
318        }
319        results
320    }
321
322    #[allow(dead_code)]
323    fn find_lines_containing_excluding<'a>(
324        &self,
325        lines: &[&'a str],
326        pattern: &str,
327        exclude: &[&str],
328    ) -> Vec<(usize, &'a str)> {
329        let mut results = Vec::new();
330        for (i, line) in lines.iter().enumerate() {
331            if line.contains(pattern) && !line.trim_start().starts_with("//") {
332                let excluded = exclude.iter().any(|e| line.contains(e));
333                if !excluded {
334                    results.push((i + 1, line.trim()));
335                }
336            }
337        }
338        results
339    }
340
341    fn make_finding(
342        &self,
343        check: &'static checks::SecurityCheckMeta,
344        file: &str,
345        line: usize,
346        snippet: &str,
347    ) -> Finding {
348        Finding::builder()
349            .id(&format!("{}-{}", check.id, line))
350            .title(check.name)
351            .description(check.description)
352            .severity(match check.severity {
353                "critical" => Severity::Critical,
354                "high" => Severity::High,
355                "medium" => Severity::Medium,
356                "low" => Severity::Low,
357                _ => Severity::Informational,
358            })
359            .file(file)
360            .location(line, 0)
361            .code(snippet)
362            .recommendation(check.remediation)
363            .category(check.category)
364            .blocks_deployment(check.blocks_deployment)
365            .build()
366    }
367
368    // ── Checks: classification helpers ────────────────────
369
370    /// Check if a function body contains ERC-777/ERC-1155 style callback patterns
371    /// that could introduce reentrancy via token transfers.
372    fn has_callback_reentrancy_indicators(
373        &self,
374        func: &crate::parser::FunctionDef,
375        contract: &crate::parser::Contract,
376    ) -> bool {
377        // ERC-777: _mint / _burn / _transfer call tokensReceived() on sender/recipient if they are contracts
378        // ERC-721: _mint / _safeMint
379        // ERC-1155: _mint / _safeTransferFrom
380        let has_mint = func.body.iter().any(|s| {
381            s.text.contains("_mint(")
382                || s.text.contains("_safeMint(")
383                || s.text.contains("_transfer(")
384        });
385        // Check if contract is ERC-1155 or ERC-777 compatible
386        let is_safe_mint_contract = contract.is_erc721()
387            || contract.is_erc1155()
388            || contract.inheritance.iter().any(|i| {
389                let l = i.to_lowercase();
390                l.contains("erc777")
391                    || l.contains("erc721")
392                    || l.contains("erc1155")
393                    || l.contains("erc721upgradeable")
394                    || l.contains("erc1155upgradeable")
395            });
396        is_safe_mint_contract && has_mint
397    }
398
399    /// Check if a function has read-only reentrancy patterns.
400    /// Read-only reentrancy occurs when an external call queries state that a reentering
401    /// call could modify before the original function completes.
402    fn has_read_only_reentrancy(&self, func: &crate::parser::FunctionDef) -> bool {
403        // Pattern: external call, then read state that the call could affect
404        let mut has_ext_call = false;
405        let mut state_read_after_call = false;
406        for stmt in &func.body {
407            match &stmt.kind {
408                crate::parser::StatementKind::ExternalCall => {
409                    has_ext_call = true;
410                }
411                crate::parser::StatementKind::StateRead if has_ext_call => {
412                    state_read_after_call = true;
413                }
414                _ => {}
415            }
416        }
417        // Common read-only reentrancy targets: balanceOf, ownerOf, totalSupply
418        if has_ext_call && state_read_after_call {
419            let reads_critical = func.body.iter().any(|s| {
420                matches!(s.kind, crate::parser::StatementKind::StateRead)
421                    && (s.text.contains("balanceOf")
422                        || s.text.contains("totalSupply")
423                        || s.text.contains("ownerOf")
424                        || s.text.contains("getBalance")
425                        || s.text.contains(".balance"))
426            });
427            return reads_critical;
428        }
429        false
430    }
431
432    /// Check if external call targets are to user-controlled addresses (higher risk).
433    fn is_external_call_to_user_address(&self, stmts: &[crate::parser::Statement]) -> bool {
434        stmts.iter().any(|s| {
435            if matches!(s.kind, crate::parser::StatementKind::ExternalCall) {
436                let target = crate::parser::extract_call_target(&s.text);
437                matches!(target, crate::parser::CallTarget::LocalVariable(_))
438                    || matches!(target, crate::parser::CallTarget::Address)
439            } else {
440                false
441            }
442        })
443    }
444
445    /// Check if a function's external calls target the contract itself (self-call).
446    fn has_self_call_reentrancy(&self, func: &crate::parser::FunctionDef) -> bool {
447        func.body.iter().any(|s| {
448            if matches!(s.kind, crate::parser::StatementKind::ExternalCall) {
449                let target = crate::parser::extract_call_target(&s.text);
450                matches!(target, crate::parser::CallTarget::SelfCall)
451            } else {
452                false
453            }
454        })
455    }
456
457    // ── Reentrancy ─────────────────────────────────────────
458    fn check_reentrancy(&self, file: &str, _lines: &[&str], content: &str) -> Vec<Finding> {
459        // Use the parser to perform proper CEI (Checks-Effects-Interactions) analysis
460        let source_file = crate::parser::parse_source(content);
461        let mut findings = Vec::new();
462
463        for contract in &source_file.contracts {
464            // Resolve inheritance for reentrancy guard
465            let has_global_guard = source_file.inheritance_includes_reentrancy_guard(contract)
466                || contract.has_reentrancy_guard_modifier();
467
468            for func in &contract.functions {
469                // Skip view/pure functions — they cannot modify state
470                if !func.modifies_state() {
471                    continue;
472                }
473
474                // Skip functions with explicit nonReentrant modifier
475                if func.has_reentrancy_guard() {
476                    continue;
477                }
478
479                // ── Check 1: ERC-777 / ERC-721 / ERC-1155 callback reentrancy ──
480                // _safeMint, _mint in ERC-721/1155 call onERC721Received/onERC1155Received
481                // which can re-enter the contract before state changes propagate
482                if self.has_callback_reentrancy_indicators(func, contract) {
483                    findings.push(
484                        Finding::builder()
485                            .id(&format!("FA-H-001-CB-{}", func.line))
486                            .title(checks::REENTRANCY.name)
487                            .description(&format!(
488                                "{} — Callback reentrancy: function '{}' uses safe mint/transfer patterns that invoke receiver callbacks, enabling reentrancy via ERC-777/721/1155 hooks",
489                                checks::REENTRANCY.description, func.name
490                            ))
491                            .severity(Severity::High)
492                            .file(file)
493                            .location(func.line, 0)
494                            .code(&format!("Function {} uses safe mint/transfer with callback hooks", func.name))
495                            .recommendation("Apply the Checks-Effects-Interactions pattern: update state before _safeMint/_mint. Consider using _mint instead of _safeMint when safe, or add a nonReentrant modifier. For ERC-1155, ensure balances are set before _safeTransferFrom.")
496                            .category("Logic")
497                            .blocks_deployment(true)
498                            .build()
499                    );
500                    continue;
501                }
502
503                // ── Check 2: Read-only reentrancy ──
504                // External call followed by reads of state that could be manipulated
505                if self.has_read_only_reentrancy(func) {
506                    findings.push(
507                        Finding::builder()
508                            .id(&format!("FA-H-001-RO-{}", func.line))
509                            .title(checks::REENTRANCY.name)
510                            .description(&format!(
511                                "{} — Read-only reentrancy: function '{}' makes external calls then reads state (balanceOf, totalSupply, etc.) that the re-entering call could have modified",
512                                checks::REENTRANCY.description, func.name
513                            ))
514                            .severity(Severity::High)
515                            .file(file)
516                            .location(func.line, 0)
517                            .code(&format!("Function {} reads manipulated state after external call", func.name))
518                            .recommendation("Use nonReentrant modifier for functions that read state after external calls. Consider using a snapshot pattern to isolate pre-call state from post-call reads.")
519                            .category("Logic")
520                            .blocks_deployment(true)
521                            .build()
522                    );
523                    continue;
524                }
525
526                // ── Check 3: Self-call reentrancy ──
527                // Functions that call this.function() which could re-enter
528                if self.has_self_call_reentrancy(func) {
529                    findings.push(
530                        Finding::builder()
531                            .id(&format!("FA-H-001-SC-{}", func.line))
532                            .title(checks::REENTRANCY.name)
533                            .description(&format!(
534                                "{} — Self-call reentrancy path: function '{}' calls this.function() which could re-enter",
535                                checks::REENTRANCY.description, func.name
536                            ))
537                            .severity(Severity::Medium)
538                            .file(file)
539                            .location(func.line, 0)
540                            .code(&format!("Function {} contains this.function() call — possible reentrancy path", func.name))
541                            .recommendation("Use nonReentrant modifier or restructure to avoid self-calls that could create reentrancy paths.")
542                            .category("Logic")
543                            .blocks_deployment(false)
544                            .build()
545                    );
546                    continue;
547                }
548
549                // ── Check 4: If the whole contract has ReentrancyGuard, check for missing nonReentrant ──
550                if has_global_guard
551                    && func
552                        .body
553                        .iter()
554                        .any(|s| matches!(s.kind, crate::parser::StatementKind::ExternalCall))
555                {
556                    if let Some(ext_call_stmt) = func
557                        .body
558                        .iter()
559                        .find(|s| matches!(s.kind, crate::parser::StatementKind::ExternalCall))
560                    {
561                        findings.push(
562                            Finding::builder()
563                                .id(&format!("FA-H-001-MG-{}", func.line))
564                                .title(checks::REENTRANCY.name)
565                                .description(&format!(
566                                    "Function '{}' makes external calls (line {}) but lacks nonReentrant modifier, despite contract having ReentrancyGuard",
567                                    func.name, ext_call_stmt.line
568                                ))
569                                .severity(Severity::Medium)
570                                .file(file)
571                                .location(func.line, 0)
572                                .code(&format!("Function {} makes external call at line {} without nonReentrant", func.name, ext_call_stmt.line))
573                                .recommendation(checks::REENTRANCY.remediation)
574                                .category("Logic")
575                                .blocks_deployment(false)
576                                .build()
577                        );
578                        continue;
579                    }
580                }
581
582                // ── Check 5: Proper CEI (Checks-Effects-Interactions) analysis ──
583                // VIOLATION: State write AFTER an external call
584                let mut findings_from_cei = self.analyze_cei_pattern(func, file);
585                findings.append(&mut findings_from_cei);
586
587                // ── Check 6: External calls to user-controlled addresses ──
588                if !findings.iter().any(|f| f.id.starts_with("FA-H-001")) {
589                    // No other reentrancy finding yet — check for high-risk call targets
590                    if self.is_external_call_to_user_address(&func.body) {
591                        findings.push(
592                            Finding::builder()
593                                .id(&format!("FA-H-001-UC-{}", func.line))
594                                .title(checks::REENTRANCY.name)
595                                .description(&format!(
596                                    "{} — External call to user-controlled address in function '{}'. User-supplied addresses are high risk for reentrancy attacks",
597                                    checks::REENTRANCY.description, func.name
598                                ))
599                                .severity(Severity::High)
600                                .file(file)
601                                .location(func.line, 0)
602                                .code(&format!("Function {} calls external address from user-supplied input", func.name))
603                                .recommendation("Use nonReentrant modifier for functions that make external calls to user-supplied addresses. Validate call targets against a whitelist when possible.")
604                                .category("Logic")
605                                .blocks_deployment(true)
606                                .build()
607                        );
608                    }
609                }
610            }
611        }
612
613        findings
614    }
615
616    /// Perform CEI (Checks-Effects-Interactions) analysis on a function body.
617    fn analyze_cei_pattern(&self, func: &crate::parser::FunctionDef, file: &str) -> Vec<Finding> {
618        let mut seen_external_call = false;
619        let mut first_ext_call_line = 0;
620        let mut cei_violations = Vec::new();
621
622        for stmt in &func.body {
623            match &stmt.kind {
624                crate::parser::StatementKind::ExternalCall => {
625                    seen_external_call = true;
626                    if first_ext_call_line == 0 {
627                        first_ext_call_line = stmt.line;
628                    }
629                }
630                crate::parser::StatementKind::StateWrite if seen_external_call => {
631                    // CEI violation: state write after external call
632                    // Filter out known guard patterns (reentrancy status toggle)
633                    let is_guard_assignment = stmt.text.contains("_status")
634                        || stmt.text.contains("_ENTERED")
635                        || stmt.text.contains("_NOT_ENTERED")
636                        || stmt.text.contains("locked");
637                    if !is_guard_assignment {
638                        cei_violations.push((stmt.line, stmt.text.clone()));
639                    }
640                }
641                _ => {}
642            }
643        }
644
645        if !cei_violations.is_empty() {
646            let snippet = cei_violations
647                .iter()
648                .map(|(line, text)| format!("line {}: {}", line, text))
649                .collect::<Vec<_>>()
650                .join("; ");
651            vec![
652                Finding::builder()
653                    .id(&format!("FA-H-001-{}", func.line))
654                    .title(checks::REENTRANCY.name)
655                    .description(&format!(
656                        "{} — CEI violation: state modification after external call in function '{}' (first external call at line {})",
657                        checks::REENTRANCY.description, func.name, first_ext_call_line
658                    ))
659                    .severity(Severity::High)
660                    .file(file)
661                    .location(func.line, 0)
662                    .code(&snippet)
663                    .recommendation(checks::REENTRANCY.remediation)
664                    .category("Logic")
665                    .blocks_deployment(true)
666                    .build()
667            ]
668        } else {
669            Vec::new()
670        }
671    }
672
673    // ── Access Control ─────────────────────────────────────
674    fn check_access_control(&self, file: &str, _lines: &[&str], content: &str) -> Vec<Finding> {
675        // Use the parser to analyze access control with inheritance and modifier tracking
676        let source_file = crate::parser::parse_source(content);
677        let mut findings = Vec::new();
678
679        // Sensitive function name patterns that typically need access control
680        let sensitive_prefixes = [
681            "withdraw",
682            "transfer",
683            "mint",
684            "burn",
685            "set",
686            "update",
687            "initialize",
688            "upgradeTo",
689            "upgrade",
690            "pause",
691            "unpause",
692            "freeze",
693            "unfreeze",
694            "destroy",
695            "kill",
696            "recover",
697            "drain",
698            "swap",
699            "add",
700            "remove",
701            "grant",
702            "revoke",
703            "change",
704            "configure",
705            "deposit",
706            "stake",
707            "unstake",
708            "claim",
709            "collect",
710            "distribute",
711            "allocate",
712            "delegate",
713            "execute",
714            "send",
715            "approve",
716            "reset",
717            "toggle",
718            "blacklist",
719            "whitelist",
720            "ban",
721            "suspend",
722            "close",
723            "open",
724            "lock",
725            "unlock",
726            "renounce",
727            "rescue",
728            "emergency",
729            "shutdown",
730            "finalize",
731            "override",
732        ];
733
734        for contract in &source_file.contracts {
735            // Use SourceFile to resolve inheritance chain for access control
736            let inherits_ac = source_file.inheritance_includes_access_control(contract)
737                || contract.inherits_access_control();
738
739            // Check for known access control patterns in the contract
740            let has_known_ac_modifiers = self.contract_has_ac_modifiers(contract);
741
742            for func in &contract.functions {
743                // Only check public/external functions
744                if func.visibility != crate::parser::Visibility::Public
745                    && func.visibility != crate::parser::Visibility::External
746                {
747                    continue;
748                }
749
750                // Skip constructors
751                if func.is_constructor || func.is_fallback || func.is_receive {
752                    continue;
753                }
754
755                // Skip view/pure functions as they can't modify state
756                if func.mutability == crate::parser::Mutability::Pure
757                    || func.mutability == crate::parser::Mutability::View
758                {
759                    continue;
760                }
761
762                // Check if the function name matches sensitive patterns
763                let is_sensitive = sensitive_prefixes
764                    .iter()
765                    .any(|prefix| func.name.to_lowercase().starts_with(prefix));
766
767                if !is_sensitive {
768                    // Also check for functions that write to state variables but seem unprotected
769                    // Functions that contain external calls to transfer value
770                    let has_value_transfer = func.body.iter().any(|s| {
771                        s.text.contains(".transfer(")
772                            || s.text.contains(".send(")
773                            || s.text.contains(".call{value")
774                    });
775                    // Also check for functions that access important state variables
776                    let writes_critical = func
777                        .body
778                        .iter()
779                        .filter(|s| matches!(s.kind, crate::parser::StatementKind::StateWrite))
780                        .count()
781                        > 1;
782                    if !has_value_transfer && !writes_critical {
783                        continue;
784                    }
785                }
786
787                // ── Check 1: Has explicit access control modifier ──
788                let has_ac_modifier = func.has_access_control(contract);
789
790                // ── Check 2: Has inline access control in function body ──
791                let has_inline_ac = self.function_has_inline_access_control(func);
792
793                // ── Check 3: Role-specific patterns (OpenZeppelin AccessControl v4/v5) ──
794                let has_role_ac = self.function_has_role_based_access(func);
795
796                // ── Check 4: Initialize function specific checks ──
797                if func.name.to_lowercase().starts_with("initialize") {
798                    self.check_initializer_access(
799                        func,
800                        contract,
801                        &source_file,
802                        file,
803                        &mut findings,
804                    );
805                    continue;
806                }
807
808                // Determine overall access control status
809                let has_any_ac = has_ac_modifier || has_inline_ac || has_role_ac;
810                let has_any_modifier = !func.modifiers.is_empty();
811
812                if !has_any_ac {
813                    if inherits_ac && !has_any_modifier {
814                        // Contract inherits Ownable/AccessControl but this function has no modifier
815                        findings.push(self.make_finding(
816                            &checks::ACCESS_CONTROL, file, func.line, &format!(
817                                "Function '{}' is public/external but lacks any access control modifier (contract inherits {})",
818                                func.name,
819                                contract.inheritance.join(", ")
820                            )
821                        ));
822                    } else if !inherits_ac && !has_any_modifier {
823                        // No access control at all
824                        findings.push(self.make_finding(
825                            &checks::ACCESS_CONTROL, file, func.line, &format!(
826                                "Function '{}' is public/external with no access control modifier — any caller can execute this sensitive function",
827                                func.name
828                            )
829                        ));
830                    } else if has_any_modifier && !has_ac_modifier && !has_inline_ac {
831                        // Function has modifiers but none are access control
832                        let mods = func.modifiers.join(", ");
833                        findings.push(self.make_finding(
834                            &checks::ACCESS_CONTROL, file, func.line, &format!(
835                                "Function '{}' has modifiers ({}) but none appear to be access control",
836                                func.name, mods
837                            )
838                        ));
839                    }
840                }
841            }
842
843            // ── Post-contract analysis: check for missing _disableInitializers ──
844            if has_known_ac_modifiers || inherits_ac {
845                self.check_missing_disable_initializers(contract, file, &mut findings);
846            }
847        }
848
849        findings
850    }
851
852    /// Check if a contract has known access control modifiers (Ownable, AccessControl, etc.).
853    fn contract_has_ac_modifiers(&self, contract: &crate::parser::Contract) -> bool {
854        // Check modifier definitions for access control patterns
855        contract.modifiers_defs.iter().any(|m| {
856            let lower = m.name.to_lowercase();
857            lower.contains("only") || lower.contains("auth") || lower == "whennotpaused"
858        }) || contract.functions.iter().any(|f| {
859            f.modifiers.iter().any(|mod_name| {
860                let lower = mod_name.to_lowercase();
861                lower.contains("only") || lower.contains("role") || lower == "whennotpaused"
862            })
863        })
864    }
865
866    /// Check if a function uses inline access control via require() checks in the function body.
867    fn function_has_inline_access_control(&self, func: &crate::parser::FunctionDef) -> bool {
868        func.body.iter().any(|s| {
869            if matches!(s.kind, crate::parser::StatementKind::Guard) {
870                let text_lower = s.text.to_lowercase();
871                // Check for access-control require patterns
872                text_lower.contains("msg.sender == owner")
873                    || text_lower.contains("msg.sender == ") && text_lower.contains("owner")
874                    || text_lower.contains("msg.sender == addresses")
875                    || text_lower.contains("hasrole(")
876                    || text_lower.contains("onlyowner")
877                    || text_lower.contains("_isowner")
878                    || text_lower.contains("_authorized")
879                    || text_lower.contains("isowner")
880                    || text_lower.contains("authorized")
881                    || text_lower.contains("isadmin")
882                    || text_lower.contains("_isadmin")
883                    || text_lower.contains("onlyrole")
884                    || text_lower.contains("hasrole")
885                    || text_lower.contains("allowed to")
886                    || text_lower.contains("not authorized")
887                    || text_lower.contains("unauthorized")
888                    || text_lower.contains("caller not")
889                    || text_lower.contains("_checkrole")
890            } else {
891                false
892            }
893        })
894    }
895
896    /// Check if a function uses OpenZeppelin's AccessControl role-based patterns.
897    fn function_has_role_based_access(&self, func: &crate::parser::FunctionDef) -> bool {
898        // Check for _checkRole(ROLE, address) calls
899        func.body.iter().any(|s| {
900            s.text.contains("_checkRole(") || s.text.contains("_grantRole(")
901                || s.text.contains("_revokeRole(") || s.text.contains("onlyRole")
902        })
903            // Also check modifiers that reference role constants
904            || func.modifiers.iter().any(|m| {
905                let lower = m.to_lowercase();
906                lower.contains("role") || lower == "onlyrole"
907            })
908    }
909
910    /// Perform specialized access control checks for initialize() functions.
911    fn check_initializer_access(
912        &self,
913        func: &crate::parser::FunctionDef,
914        _contract: &crate::parser::Contract,
915        _source_file: &crate::parser::SourceFile,
916        file: &str,
917        findings: &mut Vec<Finding>,
918    ) {
919        let body = &func.body;
920        let has_initializer_modifier = func.modifiers.iter().any(|m| {
921            let lower = m.to_lowercase();
922            lower == "initializer" || lower == "reinitializer" || lower.contains("initializer")
923        });
924
925        // Check for Ownable 2-step initialize pattern
926        let calls_ownable_init = body.iter().any(|s| {
927            s.text.contains("__Ownable_init(") || s.text.contains("__Ownable_init_unchain(")
928        });
929        let calls_accesscontrol_init = body.iter().any(|s| {
930            s.text.contains("__AccessControl_init(")
931                || s.text.contains("__AccessControl_init_unchain(")
932        });
933        let calls_reentrancyguard_init = body
934            .iter()
935            .any(|s| s.text.contains("__ReentrancyGuard_init("));
936
937        if !has_initializer_modifier {
938            let ac_methods = [
939                (calls_ownable_init, "__Ownable_init"),
940                (calls_accesscontrol_init, "__AccessControl_init"),
941                (calls_reentrancyguard_init, "__ReentrancyGuard_init"),
942            ];
943            let init_calls: Vec<&str> = ac_methods
944                .iter()
945                .filter(|(called, _)| *called)
946                .map(|(_, name)| *name)
947                .collect();
948            if !init_calls.is_empty() {
949                findings.push(self.make_finding(
950                    &checks::ACCESS_CONTROL,
951                    file,
952                    func.line,
953                    &format!(
954                        "Initialize function '{}' calls {} but lacks the 'initializer' modifier — can be front-run",
955                        func.name,
956                        init_calls.join(", ")
957                    )
958                ));
959            }
960        }
961
962        // Check for onlyOwner-like direct assignment in initialize
963        let assigns_owner = body.iter().any(|s| {
964            let lower = s.text.to_lowercase();
965            (lower.contains("owner") || lower.contains("admin"))
966                && (s.text.contains('=') || s.text.contains(" := "))
967                && !matches!(s.kind, crate::parser::StatementKind::Guard)
968        });
969
970        if assigns_owner && !has_initializer_modifier {
971            findings.push(self.make_finding(
972                &checks::ACCESS_CONTROL,
973                file,
974                func.line,
975                &format!(
976                    "Function '{}' assigns owner/admin directly but lacks 'initializer' modifier — unprotected",
977                    func.name
978                )
979            ));
980        }
981    }
982
983    /// Check if the contract should have _disableInitializers() in its constructor.
984    fn check_missing_disable_initializers(
985        &self,
986        contract: &crate::parser::Contract,
987        file: &str,
988        findings: &mut Vec<Finding>,
989    ) {
990        let has_constructor = contract.functions.iter().any(|f| f.is_constructor);
991        let calls_disable = contract.functions.iter().any(|f| {
992            f.body
993                .iter()
994                .any(|s| s.text.contains("disableInitializers"))
995        });
996        let is_upgradeable = contract.inheritance.iter().any(|i| {
997            let l = i.to_lowercase();
998            l.contains("initializable")
999                || l.contains("uups")
1000                || l.contains("transparentupgradeable")
1001        });
1002
1003        if is_upgradeable && has_constructor && !calls_disable {
1004            findings.push(self.make_finding(
1005                &checks::ACCESS_CONTROL,
1006                file,
1007                contract.line,
1008                &format!(
1009                    "Implementation contract '{}' is upgradeable but its constructor does not call _disableInitializers(), leaving it vulnerable to selfdestruct",
1010                    contract.name
1011                )
1012            ));
1013        }
1014    }
1015
1016    // ── Delegatecall ───────────────────────────────────────
1017    fn check_delegatecall(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1018        self.find_lines_containing(lines, &["delegatecall(", ".delegatecall("])
1019            .into_iter()
1020            .map(|(line, snippet)| self.make_finding(&checks::DELEGATECALL, file, line, snippet))
1021            .collect()
1022    }
1023
1024    // ── tx.origin ──────────────────────────────────────────
1025    fn check_tx_origin(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1026        self.find_lines_containing(lines, &["tx.origin"])
1027            .into_iter()
1028            .map(|(line, snippet)| self.make_finding(&checks::TX_ORIGIN, file, line, snippet))
1029            .collect()
1030    }
1031
1032    // ── CREATE2 ────────────────────────────────────────────
1033    fn check_create2(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1034        self.find_lines_containing(lines, &["CREATE2", "create2(", ".create2("])
1035            .into_iter()
1036            .map(|(line, snippet)| self.make_finding(&checks::CREATE2, file, line, snippet))
1037            .collect()
1038    }
1039
1040    // ── DoS ────────────────────────────────────────────────
1041    fn check_dos(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1042        self.find_lines_containing(lines, &["for (uint", "for (uint256", "while ("])
1043            .into_iter()
1044            .filter(|(_, snippet)| snippet.contains(".length"))
1045            .map(|(line, snippet)| self.make_finding(&checks::DOS, file, line, snippet))
1046            .collect()
1047    }
1048
1049    // ── Storage Collision ──────────────────────────────────
1050    fn check_storage_collision(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1051        // Look for storage gap patterns in upgradeable contracts
1052        let has_storage_gap = lines
1053            .iter()
1054            .any(|l| l.contains("__gap") || l.contains("_gap"));
1055        let is_contract = lines.iter().any(|l| {
1056            l.contains("contract ")
1057                && l.contains("is ")
1058                && (l.contains("UUPS") || l.contains("Transparent") || l.contains("Beacon"))
1059        });
1060
1061        if is_contract && !has_storage_gap {
1062            vec![self.make_finding(
1063                &checks::STORAGE_COLLISION,
1064                file,
1065                1,
1066                "Upgradeable contract without storage gap",
1067            )]
1068        } else {
1069            Vec::new()
1070        }
1071    }
1072
1073    // ── Unsafe Assembly ────────────────────────────────────
1074    fn check_unsafe_assembly(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1075        self.find_lines_containing(lines, &["assembly {"])
1076            .into_iter()
1077            .map(|(line, snippet)| self.make_finding(&checks::UNSAFE_ASSEMBLY, file, line, snippet))
1078            .collect()
1079    }
1080
1081    // ── Selfdestruct ───────────────────────────────────────
1082    fn check_selfdestruct(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1083        self.find_lines_containing(lines, &["selfdestruct(", "selfdestruct ("])
1084            .into_iter()
1085            .map(|(line, snippet)| self.make_finding(&checks::SELFDESTRUCT, file, line, snippet))
1086            .collect()
1087    }
1088
1089    // ── Proxy Vulnerabilities ──────────────────────────────
1090    fn check_proxy_vulnerabilities(
1091        &self,
1092        file: &str,
1093        lines: &[&str],
1094        _content: &str,
1095    ) -> Vec<Finding> {
1096        let mut findings = Vec::new();
1097        // Check for initialize function in implementation
1098        let has_initialize = lines.iter().any(|l| l.contains("function initialize"));
1099        let has_initializer_modifier = lines.iter().any(|l| l.contains("initializer"));
1100        let is_implementation = lines.iter().any(|l| {
1101            l.contains("is UUPSUpgradeable") || l.contains("is TransparentUpgradeableProxy")
1102        });
1103
1104        if is_implementation && has_initialize && !has_initializer_modifier {
1105            findings.push(self.make_finding(
1106                &checks::PROXY_VULNERABILITIES,
1107                file,
1108                1,
1109                "Implementation contract has initialize() without initializer modifier",
1110            ));
1111        }
1112
1113        // Check for disableInitializers call in constructor
1114        if is_implementation {
1115            let has_disable = lines.iter().any(|l| l.contains("disableInitializers"));
1116            let has_constructor = lines.iter().any(|l| l.contains("constructor("));
1117            if has_constructor && !has_disable {
1118                findings.push(self.make_finding(
1119                    &checks::PROXY_VULNERABILITIES,
1120                    file,
1121                    1,
1122                    "Implementation contract constructor does not call disableInitializers()",
1123                ));
1124            }
1125        }
1126
1127        findings
1128    }
1129
1130    // ── Oracle Manipulation ────────────────────────────────
1131    fn check_oracle_manipulation(
1132        &self,
1133        file: &str,
1134        lines: &[&str],
1135        _content: &str,
1136    ) -> Vec<Finding> {
1137        let has_chainlink = lines
1138            .iter()
1139            .any(|l| l.contains("Chainlink") || l.contains("AggregatorV3Interface"));
1140        let has_price_reference = lines.iter().any(|l| {
1141            l.contains("price") || l.contains("oracle") || l.contains("twap") || l.contains("TWAP")
1142        });
1143
1144        if has_price_reference
1145            && !has_chainlink
1146            && !lines
1147                .iter()
1148                .any(|l| l.contains("//") && l.contains("oracle"))
1149        {
1150            self.find_lines_containing(lines, &["price(", ".price(", "getPrice", "getLatestPrice"])
1151                .into_iter()
1152                .map(|(line, snippet)| {
1153                    self.make_finding(&checks::ORACLE_MANIPULATION, file, line, snippet)
1154                })
1155                .collect()
1156        } else {
1157            Vec::new()
1158        }
1159    }
1160
1161    // ── Signature Vulnerabilities ──────────────────────────
1162    fn check_signature_vulnerabilities(
1163        &self,
1164        file: &str,
1165        lines: &[&str],
1166        _content: &str,
1167    ) -> Vec<Finding> {
1168        self.find_lines_containing(lines, &["ecrecover(", "ECDSA", "SignatureChecker"])
1169            .into_iter()
1170            .map(|(line, snippet)| {
1171                self.make_finding(&checks::SIGNATURE_VULNERABILITIES, file, line, snippet)
1172            })
1173            .collect()
1174    }
1175
1176    // ── Replay Attacks ─────────────────────────────────────
1177    fn check_replay_attacks(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1178        let has_nonce = lines
1179            .iter()
1180            .any(|l| l.contains("nonce") || l.contains("Nonce"));
1181        let has_signing = lines
1182            .iter()
1183            .any(|l| l.contains("signature") || l.contains("ecrecover"));
1184
1185        if has_signing && !has_nonce {
1186            self.find_lines_containing(lines, &["ecrecover(", "ECDSA"])
1187                .into_iter()
1188                .map(|(line, snippet)| {
1189                    self.make_finding(&checks::REPLAY_ATTACKS, file, line, snippet)
1190                })
1191                .collect()
1192        } else {
1193            Vec::new()
1194        }
1195    }
1196
1197    // ── ERC20 Issues ───────────────────────────────────────
1198    fn check_erc20_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1199        let is_erc20 = lines
1200            .iter()
1201            .any(|l| l.contains("is IERC20") || l.contains("ERC20"));
1202        if !is_erc20 {
1203            return Vec::new();
1204        }
1205
1206        let mut findings = Vec::new();
1207
1208        // Check for approve without safe increase/decrease
1209        let has_approve = lines.iter().any(|l| l.contains("approve("));
1210        let has_safe_approve = lines
1211            .iter()
1212            .any(|l| l.contains("increaseAllowance") || l.contains("decreaseAllowance"));
1213        if has_approve && !has_safe_approve {
1214            findings.push(self.make_finding(
1215                &checks::ERC20_ISSUES,
1216                file,
1217                1,
1218                "ERC20 approve() used without safe increaseAllowance/decreaseAllowance pattern",
1219            ));
1220        }
1221
1222        // Check for _mint in constructor (common frontrunning issue)
1223        let has_init_mint = lines.iter().any(|l| l.contains("_mint("));
1224        if has_init_mint {
1225            findings.push(self.make_finding(
1226                &checks::ERC20_ISSUES,
1227                file,
1228                1,
1229                "ERC20 tokens minted — verify initialization and distribution",
1230            ));
1231        }
1232
1233        findings
1234    }
1235
1236    // ── Bridge Vulnerabilities ─────────────────────────────
1237    fn check_bridge_vulnerabilities(
1238        &self,
1239        file: &str,
1240        lines: &[&str],
1241        _content: &str,
1242    ) -> Vec<Finding> {
1243        let bridge_keywords = [
1244            "bridge",
1245            "relayer",
1246            "validator",
1247            "crossChain",
1248            "cross-chain",
1249            "message",
1250            "relay",
1251        ];
1252        let has_bridge = lines
1253            .iter()
1254            .any(|l| bridge_keywords.iter().any(|k| l.contains(k)));
1255
1256        if has_bridge {
1257            vec![self.make_finding(
1258                &checks::BRIDGE_VULNERABILITIES,
1259                file,
1260                1,
1261                "Bridge/relayer pattern detected — review validator set and message signing",
1262            )]
1263        } else {
1264            Vec::new()
1265        }
1266    }
1267
1268    // ── Flash Loan Issues ──────────────────────────────────
1269    fn check_flash_loan_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1270        let flash_keywords = [
1271            "flashLoan",
1272            "flashloan",
1273            "flash_loan",
1274            "onFlashLoan",
1275            "IFlashLoan",
1276        ];
1277        let has_flash = lines
1278            .iter()
1279            .any(|l| flash_keywords.iter().any(|k| l.contains(k)));
1280
1281        if has_flash {
1282            vec![self.make_finding(
1283                &checks::FLASH_LOAN_ISSUES,
1284                file,
1285                1,
1286                "Flash loan pattern detected — verify price and balance checks",
1287            )]
1288        } else {
1289            Vec::new()
1290        }
1291    }
1292
1293    // ── MEV Issues ─────────────────────────────────────────
1294    fn check_mev_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1295        let has_swap = lines
1296            .iter()
1297            .any(|l| l.contains("swap") || l.contains("Swap"));
1298        let has_slippage = lines.iter().any(|l| {
1299            l.contains("slippage")
1300                || l.contains("minAmount")
1301                || l.contains("minReturn")
1302                || l.contains("amountOutMin")
1303        });
1304
1305        if has_swap && !has_slippage {
1306            self.find_lines_containing(lines, &[".swap", "swap("])
1307                .into_iter()
1308                .map(|(line, snippet)| self.make_finding(&checks::MEV_ISSUES, file, line, snippet))
1309                .collect()
1310        } else {
1311            Vec::new()
1312        }
1313    }
1314
1315    // ── Cross Chain Issues ─────────────────────────────────
1316    fn check_cross_chain_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1317        let cross_chain_keywords = [
1318            "crossChain",
1319            "cross-chain",
1320            "LZ",
1321            "LayerZero",
1322            "CCIP",
1323            "Wormhole",
1324            "Hyperlane",
1325        ];
1326        let has_cross_chain = lines
1327            .iter()
1328            .any(|l| cross_chain_keywords.iter().any(|k| l.contains(k)));
1329
1330        if has_cross_chain {
1331            vec![self.make_finding(&checks::CROSS_CHAIN_ISSUES, file, 1,
1332                "Cross-chain interaction detected — verify chain ID handling and message verification")]
1333        } else {
1334            Vec::new()
1335        }
1336    }
1337
1338    // ── Dependency Vulnerabilities ─────────────────────────
1339    fn check_dependency_vulnerabilities(
1340        &self,
1341        file: &str,
1342        lines: &[&str],
1343        _content: &str,
1344    ) -> Vec<Finding> {
1345        // Check imports for well-known vulnerable libraries
1346        let mut findings = Vec::new();
1347        if let Some(_line) = lines
1348            .iter()
1349            .find(|l| l.contains("import") && l.contains("../"))
1350        {
1351            findings.push(self.make_finding(
1352                &checks::DEPENDENCY_VULNERABILITIES,
1353                file,
1354                1,
1355                "Relative import detected — verify package version",
1356            ));
1357        }
1358        findings
1359    }
1360
1361    // ── Unsafe Imports ─────────────────────────────────────
1362    fn check_unsafe_imports(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1363        self.find_lines_containing(lines, &["import "])
1364            .into_iter()
1365            .filter(|(_, snippet)| {
1366                snippet.contains("http")
1367                    || snippet.contains("github.com/")
1368                        && !snippet.contains("OpenZeppelin")
1369                        && !snippet.contains("solmate")
1370                        && !snippet.contains("forge-std")
1371            })
1372            .map(|(line, snippet)| self.make_finding(&checks::UNSAFE_IMPORTS, file, line, snippet))
1373            .collect()
1374    }
1375
1376    // ── Unsafe Initializers ────────────────────────────────
1377    fn check_unsafe_initializers(
1378        &self,
1379        file: &str,
1380        lines: &[&str],
1381        _content: &str,
1382    ) -> Vec<Finding> {
1383        let has_initializer = lines
1384            .iter()
1385            .any(|l| l.contains("initializer") || l.contains("reinitializer"));
1386        let has_constructor = lines.iter().any(|l| l.contains("constructor("));
1387
1388        if has_initializer && !has_constructor {
1389            // Has initializer but no constructor — could be upgradeable
1390            Vec::new()
1391        } else if has_initializer && has_constructor {
1392            let has_disable = lines.iter().any(|l| l.contains("disableInitializers"));
1393            if !has_disable {
1394                vec![self.make_finding(
1395                    &checks::UNSAFE_INITIALIZERS,
1396                    file,
1397                    1,
1398                    "Contract has initializer and constructor but no disableInitializers() call",
1399                )]
1400            } else {
1401                Vec::new()
1402            }
1403        } else {
1404            Vec::new()
1405        }
1406    }
1407
1408    // ── Unsafe Upgrade Paths ──────────────────────────────
1409    fn check_unsafe_upgrade_paths(
1410        &self,
1411        file: &str,
1412        lines: &[&str],
1413        _content: &str,
1414    ) -> Vec<Finding> {
1415        let is_upgradeable = lines.iter().any(|l| {
1416            l.contains("is UUPSUpgradeable")
1417                || l.contains("is TransparentUpgradeableProxy")
1418                || l.contains("upgradeTo")
1419        });
1420        if is_upgradeable {
1421            vec![self.make_finding(
1422                &checks::UNSAFE_UPGRADE_PATHS,
1423                file,
1424                1,
1425                "Upgradeable contract detected — verify upgrade path security",
1426            )]
1427        } else {
1428            Vec::new()
1429        }
1430    }
1431
1432    // ── Clone Vulnerabilities ──────────────────────────────
1433    fn check_clone_vulnerabilities(
1434        &self,
1435        file: &str,
1436        lines: &[&str],
1437        _content: &str,
1438    ) -> Vec<Finding> {
1439        self.find_lines_containing(lines, &["Clones.", "clone(", "ERC1167"])
1440            .into_iter()
1441            .map(|(line, snippet)| {
1442                self.make_finding(&checks::CLONE_VULNERABILITIES, file, line, snippet)
1443            })
1444            .collect()
1445    }
1446
1447    // ── Gas Problems ───────────────────────────────────────
1448    fn check_gas_problems(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1449        let mut findings = Vec::new();
1450        // Detect loops with length evaluated each iteration
1451        for (i, line) in lines.iter().enumerate() {
1452            let trimmed = line.trim();
1453            if trimmed.contains("for (uint")
1454                && trimmed.contains("i < ")
1455                && !trimmed.contains("memory ")
1456                && !trimmed.contains(".length")
1457            {
1458                // Check if the next lines verify the pattern
1459                findings.push(self.make_finding(&checks::GAS_PROBLEMS, file, i + 1, trimmed));
1460            }
1461        }
1462        findings
1463    }
1464
1465    // ── Unsafe Casting ─────────────────────────────────────
1466    fn check_unsafe_casting(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1467        self.find_lines_containing(lines, &["uint256(", "int256(", "address("])
1468            .into_iter()
1469            .filter(|(_, s)| {
1470                s.contains("uint8(")
1471                    || s.contains("uint16(")
1472                    || s.contains("uint32(")
1473                    || s.contains("uint64(")
1474                    || s.contains("uint128(")
1475                    || s.contains("int8(")
1476                    || s.contains("int16(")
1477            })
1478            .map(|(line, snippet)| self.make_finding(&checks::UNSAFE_CASTING, file, line, snippet))
1479            .collect()
1480    }
1481
1482    // ── Timestamp Manipulation ─────────────────────────────
1483    fn check_timestamp_manipulation(
1484        &self,
1485        file: &str,
1486        lines: &[&str],
1487        _content: &str,
1488    ) -> Vec<Finding> {
1489        let mut findings = Vec::new();
1490        for (i, line) in lines.iter().enumerate() {
1491            if !line.trim_start().starts_with("//")
1492                && (line.contains("block.timestamp") || line.contains("now "))
1493            {
1494                // Check if used in critical logic (not just logging)
1495                if line.contains("if ")
1496                    || line.contains("require")
1497                    || line.contains("==")
1498                    || line.contains(">=")
1499                    || line.contains("<=")
1500                {
1501                    findings.push(self.make_finding(
1502                        &checks::TIMESTAMP_MANIPULATION,
1503                        file,
1504                        i + 1,
1505                        line.trim(),
1506                    ));
1507                }
1508            }
1509        }
1510        findings
1511    }
1512
1513    // ── Storage Inefficiencies ─────────────────────────────
1514    fn check_storage_inefficiencies(
1515        &self,
1516        file: &str,
1517        lines: &[&str],
1518        _content: &str,
1519    ) -> Vec<Finding> {
1520        // Check for variables that could be packed (uint256 used when smaller type works)
1521        self.find_lines_containing(
1522            lines,
1523            &["uint256 public ", "uint256 internal ", "uint256 private "],
1524        )
1525        .into_iter()
1526        .filter(|(_, s)| !s.contains("address") && !s.contains("mapping") && s.len() < 100)
1527        .take(3)
1528        .map(|(line, snippet)| {
1529            self.make_finding(&checks::STORAGE_INEFFICIENCIES, file, line, snippet)
1530        })
1531        .collect()
1532    }
1533
1534    // ── Unsafe Events ──────────────────────────────────────
1535    fn check_unsafe_events(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1536        self.find_lines_containing(lines, &["event ", "emit "])
1537            .into_iter()
1538            .filter(|(_, s)| {
1539                s.contains("password")
1540                    || s.contains("secret")
1541                    || s.contains("key")
1542                    || s.contains("privateKey")
1543                    || s.contains("mnemonic")
1544            })
1545            .map(|(line, snippet)| self.make_finding(&checks::UNSAFE_EVENTS, file, line, snippet))
1546            .collect()
1547    }
1548
1549    // ── Poor Visibility ────────────────────────────────────
1550    fn check_poor_visibility(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1551        // Check for public state variables that could be private
1552        self.find_lines_containing(lines, &["public "])
1553            .into_iter()
1554            .filter(|(_, s)| s.contains("mapping(") && s.contains("public"))
1555            .map(|(line, snippet)| self.make_finding(&checks::POOR_VISIBILITY, file, line, snippet))
1556            .collect()
1557    }
1558
1559    // ── Bad Modifiers ──────────────────────────────────────
1560    fn check_bad_modifiers(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1561        // Check for modifiers that make external calls
1562        self.find_lines_containing(lines, &["modifier "])
1563            .into_iter()
1564            .filter(|(_, s)| {
1565                s.contains(".call")
1566                    || s.contains(".transfer")
1567                    || s.contains(".send")
1568                    || s.contains("delegatecall")
1569            })
1570            .map(|(line, snippet)| self.make_finding(&checks::BAD_MODIFIERS, file, line, snippet))
1571            .collect()
1572    }
1573
1574    // ── Unsafe Math ────────────────────────────────────────
1575    fn check_unsafe_math(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1576        // Check for unchecked block usage
1577        let mut in_unchecked = false;
1578        let mut findings = Vec::new();
1579
1580        for (i, line) in lines.iter().enumerate() {
1581            let trimmed = line.trim();
1582            if trimmed.contains("unchecked {") || trimmed.contains("unchecked{") {
1583                in_unchecked = true;
1584                continue;
1585            }
1586            if trimmed.starts_with('}') && in_unchecked {
1587                in_unchecked = false;
1588                continue;
1589            }
1590            if in_unchecked
1591                && (trimmed.contains("++")
1592                    || trimmed.contains("--")
1593                    || trimmed.contains("+=")
1594                    || trimmed.contains("-=")
1595                    || trimmed.contains("*="))
1596            {
1597                findings.push(self.make_finding(&checks::UNSAFE_MATH, file, i + 1, trimmed));
1598            }
1599        }
1600        findings
1601    }
1602
1603    // ── Poor Access Patterns ───────────────────────────────
1604    fn check_poor_access_patterns(
1605        &self,
1606        file: &str,
1607        lines: &[&str],
1608        _content: &str,
1609    ) -> Vec<Finding> {
1610        self.find_lines_containing(lines, &["storage ", "storage)"])
1611            .into_iter()
1612            .filter(|(_, s)| {
1613                !s.contains("memory") && (s.contains("memory") || s.contains("calldata"))
1614            })
1615            .take(3)
1616            .map(|(line, snippet)| {
1617                self.make_finding(&checks::POOR_ACCESS_PATTERNS, file, line, snippet)
1618            })
1619            .collect()
1620    }
1621
1622    // ── Naming Issues ──────────────────────────────────────
1623    fn check_naming_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1624        let mut findings = Vec::new();
1625        for (i, line) in lines.iter().enumerate() {
1626            let trimmed = line.trim();
1627            // Check for contract/functions that don't follow camelCase
1628            if trimmed.starts_with("function ") && !trimmed.contains("(") {
1629                let name = trimmed.split_whitespace().nth(1).unwrap_or("");
1630                if name.contains('_') {
1631                    findings.push(self.make_finding(&checks::NAMING_ISSUES, file, i + 1, trimmed));
1632                    if findings.len() >= 3 {
1633                        break;
1634                    }
1635                }
1636            }
1637        }
1638        findings
1639    }
1640
1641    // ── Code Duplication ──────────────────────────────────
1642    fn check_code_duplication(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1643        // Simple heuristic: check for repeated patterns across lines
1644        let mut findings = Vec::new();
1645        if lines.len() > 50 {
1646            let total_lines = lines.len();
1647            let unique_lines: std::collections::HashSet<&&str> = lines.iter().collect();
1648            let ratio = unique_lines.len() as f64 / total_lines as f64;
1649            if ratio < 0.5 {
1650                findings.push(self.make_finding(
1651                    &checks::CODE_DUPLICATION,
1652                    file,
1653                    1,
1654                    &format!(
1655                        "Low code diversity ({:.0}% unique lines) — possible duplication",
1656                        ratio * 100.0
1657                    ),
1658                ));
1659            }
1660        }
1661        findings
1662    }
1663
1664    // ── Optimization ──────────────────────────────────────
1665    fn check_optimization(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1666        let mut findings = Vec::new();
1667        for (i, line) in lines.iter().enumerate() {
1668            let trimmed = line.trim();
1669            // Suggest using calldata instead of memory
1670            if trimmed.contains("memory ")
1671                && !trimmed.contains("internal")
1672                && !trimmed.starts_with("//")
1673            {
1674                findings.push(self.make_finding(
1675                    &checks::OPTIMIZATION_ISSUES,
1676                    file,
1677                    i + 1,
1678                    trimmed,
1679                ));
1680                if findings.len() >= 2 {
1681                    break;
1682                }
1683            }
1684        }
1685        findings
1686    }
1687
1688    // ── Style Issues ──────────────────────────────────────
1689    fn check_style_issues(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1690        let mut findings = Vec::new();
1691        for (i, line) in lines.iter().enumerate() {
1692            if line.len() > 120 {
1693                findings.push(self.make_finding(
1694                    &checks::STYLE_ISSUES,
1695                    file,
1696                    i + 1,
1697                    "Line exceeds 120 characters",
1698                ));
1699                if findings.len() >= 2 {
1700                    break;
1701                }
1702            }
1703        }
1704        findings
1705    }
1706
1707    // ── Documentation ─────────────────────────────────────
1708    fn check_documentation(&self, file: &str, lines: &[&str], _content: &str) -> Vec<Finding> {
1709        let mut findings = Vec::new();
1710        let mut has_natspec = false;
1711
1712        for (i, line) in lines.iter().enumerate() {
1713            let trimmed = line.trim();
1714
1715            if trimmed.starts_with("/**") {
1716                has_natspec = true;
1717                continue;
1718            }
1719
1720            if trimmed.starts_with("function ") {
1721                if !has_natspec && i > 0 && !lines[i - 1].trim().starts_with("//") {
1722                    findings.push(self.make_finding(
1723                        &checks::DOCUMENTATION_ISSUES,
1724                        file,
1725                        i + 1,
1726                        trimmed,
1727                    ));
1728                    if findings.len() >= 3 {
1729                        break;
1730                    }
1731                }
1732                has_natspec = false;
1733            }
1734
1735            if trimmed.ends_with("*/") || trimmed.starts_with("///") || trimmed.starts_with("* ") {
1736                has_natspec = true;
1737            }
1738        }
1739        findings
1740    }
1741}