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