Skip to content

Commit 7ec5b9b

Browse files
committed
fix(stuff): wip
1 parent 95bd475 commit 7ec5b9b

84 files changed

Lines changed: 7241 additions & 14 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ thiserror = "1.0"
6161
tempfile = "3"
6262
regex = "1"
6363

64+
# Tree-sitter for AST parsing
65+
tree-sitter = "0.26"
66+
tree-sitter-c = "0.23"
67+
tree-sitter-rust = "0.23"
68+
tree-sitter-python = "0.23"
69+
tree-sitter-javascript = "0.23"
70+
6471
[dev-dependencies]
6572
mockall = "0.13"
6673
serial_test = "3"

src/agent/executor.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub fn create_empty_finding(
4747
poc_format: None,
4848
llm_model: model_name.filter(|m| !m.is_empty()),
4949
agent_mode: true,
50+
statement_range: None,
5051
},
5152
compile_path: None,
5253
test_source_path: None,
@@ -94,6 +95,7 @@ pub fn create_audit_finding(
9495
poc_format: None,
9596
llm_model: model_name.filter(|m| !m.is_empty()),
9697
agent_mode: true,
98+
statement_range: None,
9799
},
98100
compile_path: None,
99101
test_source_path: None,

src/agent/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ mod tests {
107107
poc_format: None,
108108
llm_model: None,
109109
agent_mode: true,
110+
statement_range: None,
110111
},
111112
compile_path: Some(PathBuf::from("/path/to/compile")),
112113
test_source_path: Some(PathBuf::from("/path/to/test")),
@@ -155,6 +156,7 @@ mod tests {
155156
poc_format: None,
156157
llm_model: None,
157158
agent_mode: true,
159+
statement_range: None,
158160
},
159161
compile_path: Some(PathBuf::from("/path/to/compile")),
160162
test_source_path: None,
@@ -203,6 +205,7 @@ mod tests {
203205
poc_format: None,
204206
llm_model: None,
205207
agent_mode: true,
208+
statement_range: None,
206209
},
207210
compile_path: None,
208211
test_source_path: None,
@@ -251,6 +254,7 @@ mod tests {
251254
poc_format: None,
252255
llm_model: None,
253256
agent_mode: true,
257+
statement_range: None,
254258
},
255259
compile_path: None,
256260
test_source_path: None,
@@ -299,6 +303,7 @@ mod tests {
299303
poc_format: None,
300304
llm_model: None,
301305
agent_mode: true,
306+
statement_range: None,
302307
},
303308
compile_path: None,
304309
test_source_path: None,

src/agent/sandbox.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ use std::path::{Path, PathBuf};
55
use std::process::{Command, Output, Stdio};
66
use std::time::{Duration, Instant};
77

8+
/// Error type for sandbox operations
9+
#[derive(Debug, thiserror::Error)]
10+
pub enum SandboxError {
11+
#[error("Docker unavailable: {0}")]
12+
DockerUnavailable(String),
13+
#[error("Runtime error: {0}")]
14+
RuntimeError(String),
15+
#[error("Timeout after {0}s")]
16+
Timeout(u64),
17+
}
18+
819
pub struct ToolSandbox {
920
temp_dir: PathBuf,
1021
timeout_secs: u64,

src/agent/session.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ impl AgentSession {
235235
}
236236
},
237237
agent_mode: true,
238+
statement_range: None,
238239
},
239240
compile_path,
240241
test_source_path,
@@ -635,6 +636,7 @@ mod tests {
635636
poc_format: None,
636637
llm_model: None,
637638
agent_mode: true,
639+
statement_range: None,
638640
};
639641

640642
let result = session.verify_finding("test.rs", &finding).await;
@@ -697,6 +699,7 @@ mod tests {
697699
poc_format: None,
698700
llm_model: None,
699701
agent_mode: true,
702+
statement_range: None,
700703
};
701704

702705
let result = session.verify_finding("test.rs", &finding).await;
@@ -917,6 +920,7 @@ mod tests {
917920
poc_format: None,
918921
llm_model: None,
919922
agent_mode: true,
923+
statement_range: None,
920924
};
921925

922926
let result = session.verify_finding("test.rs", &finding).await;
@@ -979,6 +983,7 @@ mod tests {
979983
poc_format: None,
980984
llm_model: None,
981985
agent_mode: true,
986+
statement_range: None,
982987
};
983988

984989
let result = session.verify_finding("test.rs", &finding).await;

src/confidence.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ mod tests {
9595
poc_format: None,
9696
llm_model: None,
9797
agent_mode: false,
98+
statement_range: None,
9899
};
99100

100101
let score = ConfidenceCalculator::calculate_composite(&mut finding);
@@ -133,6 +134,7 @@ mod tests {
133134
poc_format: None,
134135
llm_model: None,
135136
agent_mode: false,
137+
statement_range: None,
136138
};
137139

138140
let score = ConfidenceCalculator::calculate_composite(&mut finding);
@@ -170,6 +172,7 @@ mod tests {
170172
poc_format: None,
171173
llm_model: None,
172174
agent_mode: false,
175+
statement_range: None,
173176
};
174177

175178
ConfidenceCalculator::recalculate_priority(&mut finding);

src/confidence_refinement.rs

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
//! - Integrates with AnalysisContext (T5)
1010
1111
use crate::analysis_context::AnalysisContext;
12+
use crate::config::{NormalizationConfig, NormalizationTier};
1213
use crate::findings::{VerificationStatus, VulnerabilityFinding};
1314
use std::collections::HashMap;
15+
use std::fs;
16+
use std::path::PathBuf;
1417

1518
#[cfg(test)]
1619
use crate::findings::Severity;
@@ -59,6 +62,8 @@ pub enum ConfidenceFactor {
5962
TriageTruePositive,
6063
/// Triage identified false positive
6164
TriageFalsePositive,
65+
/// Rationale validated by LLM-as-judge
66+
RationaleValidated,
6267
}
6368

6469
/// Historical data for confidence refinement.
@@ -361,7 +366,7 @@ impl ConfidenceRefinementPhase {
361366
}
362367
}
363368

364-
// Factor 9: Tiage-based adjustments
369+
// Factor 9: Triage-based adjustments
365370
if let Some(ref notes) = finding.verification_notes {
366371
if notes.contains("triage") || notes.contains("Triage") {
367372
if notes.contains("false_positive") || notes.contains("False positive") {
@@ -376,6 +381,25 @@ impl ConfidenceRefinementPhase {
376381
}
377382
}
378383

384+
// Factor 10: Rationale validation via LLM-as-judge
385+
// This applies when a finding has been through the rationale_check step
386+
// The verification_notes may contain rationale validation results
387+
if let Some(ref notes) = finding.verification_notes {
388+
if notes.contains("rationale") || notes.contains("Rationale") {
389+
if notes.contains("sound") || notes.contains("validated") {
390+
// Sound rationale - boost confidence
391+
refined_score = (refined_score + 0.10).min(1.0);
392+
factors.push(ConfidenceFactor::RationaleValidated);
393+
explanations.push("Rationale validated as sound by LLM judge".to_string());
394+
} else if notes.contains("flawed") || notes.contains("invalid") {
395+
// Flawed rationale - penalize confidence
396+
refined_score = (refined_score - 0.20).max(0.0);
397+
factors.push(ConfidenceFactor::RationaleValidated);
398+
explanations.push("Rationale identified as flawed by LLM judge".to_string());
399+
}
400+
}
401+
}
402+
379403
// Clamp final score
380404
refined_score = refined_score.clamp(0.0, 1.0);
381405

@@ -512,6 +536,160 @@ pub struct ContextAnalysis {
512536
explanation: String,
513537
}
514538

539+
/// Project baseline for confidence normalization.
540+
///
541+
/// Stores historical triage outcomes to enable per-project confidence calibration.
542+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
543+
pub struct ProjectBaseline {
544+
/// Total number of findings analyzed.
545+
pub total_findings: usize,
546+
/// Number of true positives confirmed.
547+
pub true_positives: usize,
548+
/// Number of false positives identified.
549+
pub false_positives: usize,
550+
/// Mean confidence score of all findings.
551+
pub mean_confidence: f32,
552+
/// Sum of squared deviations for std dev calculation.
553+
#[serde(default)]
554+
pub sum_sq_dev: f32,
555+
}
556+
557+
impl ProjectBaseline {
558+
/// Create an empty baseline.
559+
pub fn empty() -> Self {
560+
Self {
561+
total_findings: 0,
562+
true_positives: 0,
563+
false_positives: 0,
564+
mean_confidence: 0.0,
565+
sum_sq_dev: 0.0,
566+
}
567+
}
568+
569+
/// Load baseline from a file path.
570+
///
571+
/// Returns empty baseline if file doesn't exist or is invalid.
572+
pub fn load(path: &PathBuf) -> Self {
573+
if !path.exists() {
574+
return Self::empty();
575+
}
576+
577+
match fs::read_to_string(path) {
578+
Ok(content) => match serde_json::from_str(&content) {
579+
Ok(baseline) => baseline,
580+
Err(e) => {
581+
tracing::warn!("Failed to parse baseline at {:?}: {}", path, e);
582+
Self::empty()
583+
}
584+
},
585+
Err(e) => {
586+
tracing::warn!("Failed to read baseline at {:?}: {}", path, e);
587+
Self::empty()
588+
}
589+
}
590+
}
591+
592+
/// Save baseline to a file path.
593+
pub fn save(&self, path: &PathBuf) -> std::io::Result<()> {
594+
let json = serde_json::to_string_pretty(self).map_err(std::io::Error::other)?;
595+
596+
// Ensure parent directory exists
597+
if let Some(parent) = path.parent() {
598+
fs::create_dir_all(parent)?;
599+
}
600+
601+
fs::write(path, json)
602+
}
603+
604+
/// Get false positive rate.
605+
pub fn false_positive_rate(&self) -> f32 {
606+
if self.total_findings == 0 {
607+
return 0.0;
608+
}
609+
self.false_positives as f32 / self.total_findings as f32
610+
}
611+
612+
/// Get standard deviation of confidence scores.
613+
pub fn std_dev(&self) -> f32 {
614+
if self.total_findings <= 1 {
615+
return 0.0;
616+
}
617+
(self.sum_sq_dev / self.total_findings as f32).sqrt()
618+
}
619+
620+
/// Update baseline with a new finding's confidence score.
621+
pub fn update(&mut self, confidence: f32, is_true_positive: bool) {
622+
let old_mean = self.mean_confidence;
623+
self.total_findings += 1;
624+
625+
// Update mean using Welford's online algorithm
626+
self.mean_confidence = old_mean + (confidence - old_mean) / self.total_findings as f32;
627+
628+
// Update sum of squared deviations
629+
self.sum_sq_dev += (confidence - old_mean) * (confidence - self.mean_confidence);
630+
631+
// Update TP/FP counts
632+
if is_true_positive {
633+
self.true_positives += 1;
634+
} else {
635+
self.false_positives += 1;
636+
}
637+
}
638+
}
639+
640+
/// Normalize confidence score based on project baseline.
641+
///
642+
/// # Arguments
643+
/// * `raw_confidence` - Original confidence score
644+
/// * `config` - Normalization configuration
645+
/// * `baseline` - Project baseline with historical data
646+
///
647+
/// # Returns
648+
/// Normalized confidence score
649+
pub fn normalize_confidence(
650+
raw_confidence: f32,
651+
config: &NormalizationConfig,
652+
baseline: &ProjectBaseline,
653+
) -> f32 {
654+
if !config.enabled {
655+
return raw_confidence;
656+
}
657+
658+
match config.normalization_tier {
659+
NormalizationTier::None => raw_confidence,
660+
661+
NormalizationTier::ProjectRelative => {
662+
let fp_rate = baseline.false_positive_rate();
663+
664+
if fp_rate > 0.30 {
665+
// High FP rate: scale down
666+
let scale = 1.0 - fp_rate * 0.5;
667+
raw_confidence * scale
668+
} else if fp_rate < 0.10 {
669+
// Low FP rate: scale up (capped at 1.0)
670+
let scale = 1.0 + (0.10 - fp_rate) * 2.0;
671+
(raw_confidence * scale).min(1.0)
672+
} else {
673+
// Medium FP rate: no adjustment
674+
raw_confidence
675+
}
676+
}
677+
678+
NormalizationTier::Isotonic => {
679+
// Apply simple linear calibration
680+
let std_dev = baseline.std_dev();
681+
682+
// Fallback to raw if std_dev is 0 or baseline has <10 findings
683+
if std_dev == 0.0 || baseline.total_findings < 10 {
684+
return raw_confidence;
685+
}
686+
687+
let calibrated = (raw_confidence - baseline.mean_confidence) / std_dev * 0.5 + 0.5;
688+
calibrated.clamp(0.0, 1.0)
689+
}
690+
}
691+
}
692+
515693
#[cfg(test)]
516694
mod tests {
517695
use super::*;

0 commit comments

Comments
 (0)