forge_guard/ai/mod.rs
1//! AI-powered auditing — integrates OpenAI, Anthropic Claude, and Ollama
2//! for intelligent vulnerability analysis of Solidity smart contracts.
3//!
4//! # Architecture
5//!
6//! ```text
7//! CLI ──→ ConsensusEngine ──→ AuditorAgent(s)
8//! │ │
9//! │ └── LlmProvider (HTTP)
10//! │ ├── OpenAiProvider
11//! │ ├── ClaudeProvider
12//! │ └── OllamaProvider
13//! │
14//! └── ConsensusReport (dedup + confidence scoring)
15//! ```
16//!
17//! # Usage
18//!
19//! ```ignore
20//! forge-guard audit --ai # use default provider (OpenAI)
21//! forge-guard audit --ai --ai-provider claude # use Claude
22//! forge-guard audit --ai --ai-provider ollama # use local Ollama
23//! ```
24
25pub mod auditors;
26pub mod consensus;
27pub mod providers;
28
29use crate::core::Severity;
30use std::collections::HashMap;
31
32// ─────────────────────────────────────────────────────────────
33// Shared types
34// ─────────────────────────────────────────────────────────────
35
36/// Configuration for AI auditors.
37#[derive(Debug, Clone)]
38pub struct AiAuditorConfig {
39 /// Provider type: "openai", "claude", "ollama".
40 pub provider: String,
41 /// Model identifier (e.g. "gpt-5", "claude-5-sonnet-20260701").
42 pub model: String,
43 /// Sampling temperature (0.0–1.0).
44 pub temperature: f64,
45 /// Maximum tokens per response.
46 pub max_tokens: u32,
47 /// Minimum confidence (0.0–1.0) for a finding to be included.
48 pub min_confidence: f64,
49 /// Optional API key override. Reads from env var when `None`.
50 pub api_key: Option<String>,
51 /// Optional Ollama endpoint override.
52 pub ollama_endpoint: Option<String>,
53 /// Whether to include gas and logic auditors in addition to security.
54 pub full_audit: bool,
55}
56
57impl Default for AiAuditorConfig {
58 fn default() -> Self {
59 Self {
60 provider: "openai".into(),
61 model: "gpt-5".into(),
62 temperature: 0.1,
63 max_tokens: 4000,
64 min_confidence: 0.5,
65 api_key: None,
66 ollama_endpoint: None,
67 full_audit: false,
68 }
69 }
70}
71
72/// Context passed to each auditor agent for analysis.
73#[derive(Debug, Clone)]
74pub struct AuditContext {
75 /// Full Solidity source code to analyze.
76 pub source_code: String,
77 /// File name (for context in prompts).
78 pub file_name: String,
79 /// Compiler version string (e.g. "^0.8.20").
80 pub compiler_version: String,
81 /// Additional context key-value pairs.
82 pub additional: HashMap<String, String>,
83}
84
85impl AuditContext {
86 /// Create a new audit context.
87 pub fn new(source_code: &str, file_name: &str, compiler_version: &str) -> Self {
88 Self {
89 source_code: source_code.to_owned(),
90 file_name: file_name.to_owned(),
91 compiler_version: compiler_version.to_owned(),
92 additional: HashMap::new(),
93 }
94 }
95}
96
97/// A single finding produced by an AI auditor.
98#[derive(Debug, Clone)]
99pub struct AuditorFinding {
100 /// Human-readable title.
101 pub title: String,
102 /// Detailed description of the vulnerability.
103 pub description: String,
104 /// Model confidence (0.0 – 1.0).
105 pub confidence: f64,
106 /// Assigned severity.
107 pub severity: Severity,
108 /// Suggested remediation.
109 pub suggestion: String,
110 /// Line numbers where the issue appears.
111 pub line_numbers: Vec<usize>,
112 /// Vulnerability category (e.g. "Reentrancy", "AccessControl").
113 pub category: String,
114}
115
116/// A finding with consensus metadata.
117#[derive(Debug, Clone)]
118pub struct ConsensusFinding {
119 /// Name of the auditor that produced this finding.
120 pub auditor: String,
121 /// Domain of the auditor (e.g. "Security", "Gas").
122 pub domain: String,
123 /// The finding itself.
124 pub finding: AuditorFinding,
125 /// Whether this finding was cross-validated by multiple auditors.
126 pub cross_validated: bool,
127}
128
129/// Convert a consensus finding into a core Finding for the audit pipeline.
130pub fn consensus_to_core_finding(cf: &ConsensusFinding, file_name: &str) -> crate::core::Finding {
131 use crate::core::FindingBuilder;
132
133 let line = cf.finding.line_numbers.first().copied();
134
135 FindingBuilder::default()
136 .id(&format!("AI-{:04}", rand_id()))
137 .title(&cf.finding.title)
138 .description(&format!(
139 "[AI {}] {} (confidence: {:.0}%){}",
140 cf.auditor,
141 cf.finding.description,
142 cf.finding.confidence * 100.0,
143 if cf.cross_validated {
144 " [cross-validated]"
145 } else {
146 ""
147 }
148 ))
149 .severity(cf.finding.severity)
150 .file(file_name)
151 .location(line.unwrap_or(0), 0)
152 .code(&format!(
153 "AI-auditor '{}' ({}) — {}",
154 cf.auditor, cf.domain, cf.finding.title
155 ))
156 .recommendation(&cf.finding.suggestion)
157 .category(&cf.finding.category)
158 .build()
159}
160
161fn rand_id() -> u16 {
162 use std::time::{SystemTime, UNIX_EPOCH};
163 let nanos = SystemTime::now()
164 .duration_since(UNIX_EPOCH)
165 .unwrap_or_default()
166 .subsec_nanos();
167 (nanos % 9999) as u16
168}
169
170// ── Re-exports ───────────────────────────────────────────────
171pub use auditors::*;
172pub use consensus::*;
173pub use providers::*;