1use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::PathBuf;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum HuntMode {
18 Falsify,
20 Hunt,
22 Analyze,
24 Fuzz,
26 DeepHunt,
28 Quick,
30}
31
32impl std::fmt::Display for HuntMode {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 HuntMode::Falsify => write!(f, "Falsify"),
36 HuntMode::Hunt => write!(f, "Hunt"),
37 HuntMode::Analyze => write!(f, "Analyze"),
38 HuntMode::Fuzz => write!(f, "Fuzz"),
39 HuntMode::DeepHunt => write!(f, "DeepHunt"),
40 HuntMode::Quick => write!(f, "Quick"),
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
47pub enum FindingSeverity {
48 Info,
50 Low,
52 Medium,
54 High,
56 Critical,
58}
59
60impl std::fmt::Display for FindingSeverity {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 FindingSeverity::Info => write!(f, "INFO"),
64 FindingSeverity::Low => write!(f, "LOW"),
65 FindingSeverity::Medium => write!(f, "MEDIUM"),
66 FindingSeverity::High => write!(f, "HIGH"),
67 FindingSeverity::Critical => write!(f, "CRITICAL"),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
74pub enum DefectCategory {
75 TraitBounds,
77 AstTransform,
79 OwnershipBorrow,
81 ConfigurationErrors,
83 ConcurrencyBugs,
85 SecurityVulnerabilities,
87 TypeErrors,
89 MemorySafety,
91 LogicErrors,
93 PerformanceIssues,
95 GpuKernelBugs,
97 SilentDegradation,
99 TestDebt,
101 HiddenDebt,
103 ContractGap,
105 ModelParityGap,
107 Unknown,
109}
110
111impl std::fmt::Display for DefectCategory {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 match self {
114 DefectCategory::TraitBounds => write!(f, "TraitBounds"),
115 DefectCategory::AstTransform => write!(f, "ASTTransform"),
116 DefectCategory::OwnershipBorrow => write!(f, "OwnershipBorrow"),
117 DefectCategory::ConfigurationErrors => write!(f, "ConfigurationErrors"),
118 DefectCategory::ConcurrencyBugs => write!(f, "ConcurrencyBugs"),
119 DefectCategory::SecurityVulnerabilities => write!(f, "SecurityVulnerabilities"),
120 DefectCategory::TypeErrors => write!(f, "TypeErrors"),
121 DefectCategory::MemorySafety => write!(f, "MemorySafety"),
122 DefectCategory::LogicErrors => write!(f, "LogicErrors"),
123 DefectCategory::PerformanceIssues => write!(f, "PerformanceIssues"),
124 DefectCategory::GpuKernelBugs => write!(f, "GpuKernelBugs"),
125 DefectCategory::SilentDegradation => write!(f, "SilentDegradation"),
126 DefectCategory::TestDebt => write!(f, "TestDebt"),
127 DefectCategory::HiddenDebt => write!(f, "HiddenDebt"),
128 DefectCategory::ContractGap => write!(f, "ContractGap"),
129 DefectCategory::ModelParityGap => write!(f, "ModelParityGap"),
130 DefectCategory::Unknown => write!(f, "Unknown"),
131 }
132 }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct Finding {
138 pub id: String,
140
141 pub file: PathBuf,
143
144 pub line: usize,
146
147 pub column: Option<usize>,
149
150 pub title: String,
152
153 pub description: String,
155
156 pub severity: FindingSeverity,
158
159 pub category: DefectCategory,
161
162 pub suspiciousness: f64,
164
165 pub discovered_by: HuntMode,
167
168 pub evidence: Vec<FindingEvidence>,
170
171 pub suggested_fix: Option<String>,
173
174 pub related: Vec<String>,
176
177 #[serde(default)]
179 pub regression_risk: Option<f64>,
180
181 #[serde(default)]
183 pub blame_author: Option<String>,
184
185 #[serde(default)]
187 pub blame_commit: Option<String>,
188
189 #[serde(default)]
191 pub blame_date: Option<String>,
192}
193
194impl Finding {
195 pub fn new(
197 id: impl Into<String>,
198 file: impl Into<PathBuf>,
199 line: usize,
200 title: impl Into<String>,
201 ) -> Self {
202 Self {
203 id: id.into(),
204 file: file.into(),
205 line,
206 column: None,
207 title: title.into(),
208 description: String::new(),
209 severity: FindingSeverity::Medium,
210 category: DefectCategory::Unknown,
211 suspiciousness: 0.5,
212 discovered_by: HuntMode::Analyze,
213 evidence: Vec::new(),
214 suggested_fix: None,
215 related: Vec::new(),
216 regression_risk: None,
217 blame_author: None,
218 blame_commit: None,
219 blame_date: None,
220 }
221 }
222
223 pub fn with_column(mut self, column: usize) -> Self {
225 self.column = Some(column);
226 self
227 }
228
229 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
231 self.description = desc.into();
232 self
233 }
234
235 pub fn with_severity(mut self, severity: FindingSeverity) -> Self {
237 self.severity = severity;
238 self
239 }
240
241 pub fn with_category(mut self, category: DefectCategory) -> Self {
243 self.category = category;
244 self
245 }
246
247 pub fn with_suspiciousness(mut self, score: f64) -> Self {
249 self.suspiciousness = score.clamp(0.0, 1.0);
250 self
251 }
252
253 pub fn with_discovered_by(mut self, mode: HuntMode) -> Self {
255 self.discovered_by = mode;
256 self
257 }
258
259 pub fn with_evidence(mut self, evidence: FindingEvidence) -> Self {
261 self.evidence.push(evidence);
262 self
263 }
264
265 pub fn with_fix(mut self, fix: impl Into<String>) -> Self {
267 self.suggested_fix = Some(fix.into());
268 self
269 }
270
271 pub fn with_regression_risk(mut self, risk: f64) -> Self {
273 self.regression_risk = Some(risk.clamp(0.0, 1.0));
274 self
275 }
276
277 pub fn with_blame(
279 mut self,
280 author: impl Into<String>,
281 commit: impl Into<String>,
282 date: impl Into<String>,
283 ) -> Self {
284 self.blame_author = Some(author.into());
285 self.blame_commit = Some(commit.into());
286 self.blame_date = Some(date.into());
287 self
288 }
289
290 pub fn location(&self) -> String {
292 match self.column {
293 Some(col) => format!("{}:{}:{}", self.file.display(), self.line, col),
294 None => format!("{}:{}", self.file.display(), self.line),
295 }
296 }
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct FindingEvidence {
302 pub evidence_type: EvidenceKind,
304
305 pub description: String,
307
308 pub data: Option<String>,
310}
311
312impl FindingEvidence {
313 pub fn mutation(mutant_id: impl Into<String>, survived: bool) -> Self {
315 Self {
316 evidence_type: EvidenceKind::MutationSurvival,
317 description: format!(
318 "Mutant {} {}",
319 mutant_id.into(),
320 if survived { "SURVIVED" } else { "KILLED" }
321 ),
322 data: Some(if survived { "SURVIVED".into() } else { "KILLED".into() }),
323 }
324 }
325
326 pub fn sbfl(formula: impl Into<String>, score: f64) -> Self {
328 Self {
329 evidence_type: EvidenceKind::SbflScore,
330 description: format!("{} suspiciousness: {:.3}", formula.into(), score),
331 data: Some(format!("{:.6}", score)),
332 }
333 }
334
335 pub fn static_analysis(tool: impl Into<String>, message: impl Into<String>) -> Self {
337 Self {
338 evidence_type: EvidenceKind::StaticAnalysis,
339 description: format!("[{}] {}", tool.into(), message.into()),
340 data: None,
341 }
342 }
343
344 pub fn fuzzing(input: impl Into<String>, crash_type: impl Into<String>) -> Self {
346 Self {
347 evidence_type: EvidenceKind::FuzzingCrash,
348 description: crash_type.into(),
349 data: Some(input.into()),
350 }
351 }
352
353 pub fn quality_metrics(grade: impl Into<String>, tdg_score: f64, complexity: u32) -> Self {
355 Self {
356 evidence_type: EvidenceKind::QualityMetrics,
357 description: format!(
358 "PMAT grade {} (TDG: {:.1}, complexity: {})",
359 grade.into(),
360 tdg_score,
361 complexity
362 ),
363 data: Some(format!("{:.1}", tdg_score)),
364 }
365 }
366
367 pub fn contract_binding(
369 contract: impl Into<String>,
370 equation: impl Into<String>,
371 status: impl Into<String>,
372 ) -> Self {
373 Self {
374 evidence_type: EvidenceKind::ContractBinding,
375 description: format!(
376 "Contract {} eq {} — {}",
377 contract.into(),
378 equation.into(),
379 status.into()
380 ),
381 data: None,
382 }
383 }
384
385 pub fn model_parity(
387 model: impl Into<String>,
388 check: impl Into<String>,
389 result: impl Into<String>,
390 ) -> Self {
391 Self {
392 evidence_type: EvidenceKind::ModelParity,
393 description: format!("Model {} — {} — {}", model.into(), check.into(), result.into()),
394 data: None,
395 }
396 }
397
398 pub fn concolic(path_constraint: impl Into<String>) -> Self {
400 Self {
401 evidence_type: EvidenceKind::ConcolicPath,
402 description: "Path constraint solved".into(),
403 data: Some(path_constraint.into()),
404 }
405 }
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
410pub enum EvidenceKind {
411 MutationSurvival,
413 SbflScore,
415 StaticAnalysis,
417 FuzzingCrash,
419 ConcolicPath,
421 LlmClassification,
423 GitHistory,
425 QualityMetrics,
427 ContractBinding,
429 ModelParity,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
435pub struct HuntConfig {
436 pub mode: HuntMode,
438
439 pub targets: Vec<PathBuf>,
441
442 pub min_suspiciousness: f64,
444
445 pub max_findings: usize,
447
448 pub sbfl_formula: SbflFormula,
450
451 pub llm_filter: bool,
453
454 pub fuzz_duration_secs: u64,
456
457 pub mutation_timeout_secs: u64,
459
460 pub include_categories: Vec<DefectCategory>,
462
463 pub exclude_categories: Vec<DefectCategory>,
465
466 pub spec_path: Option<PathBuf>,
471
472 pub spec_section: Option<String>,
474
475 pub ticket_ref: Option<String>,
477
478 pub update_spec: bool,
480
481 pub lib_only: bool,
483
484 pub bin_target: Option<String>,
486
487 pub exclude_tests: bool,
489
490 pub suppress_false_positives: bool,
492
493 pub coverage_path: Option<PathBuf>,
495
496 pub coverage_weight: f64,
498
499 pub localization_strategy: LocalizationStrategy,
504
505 pub channel_weights: ChannelWeights,
507
508 pub predictive_mutation: bool,
510
511 pub crash_bucketing: CrashBucketingMode,
513
514 pub use_pmat_quality: bool,
519
520 pub quality_weight: f64,
522
523 pub pmat_scope: bool,
525
526 pub pmat_satd: bool,
528
529 pub pmat_query: Option<String>,
531
532 pub contracts_path: Option<PathBuf>,
537
538 pub contracts_auto: bool,
540
541 pub model_parity_path: Option<PathBuf>,
543
544 pub model_parity_auto: bool,
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
550pub enum CrashBucketingMode {
551 #[default]
553 None,
554 StackTrace,
556 Semantic,
558}
559
560impl Default for HuntConfig {
561 fn default() -> Self {
562 Self {
563 mode: HuntMode::Analyze,
564 targets: vec![PathBuf::from("src")],
565 min_suspiciousness: 0.5,
566 max_findings: 50,
567 sbfl_formula: SbflFormula::Ochiai,
568 llm_filter: false,
569 fuzz_duration_secs: 60,
570 mutation_timeout_secs: 30,
571 include_categories: Vec::new(),
572 exclude_categories: Vec::new(),
573 spec_path: None,
575 spec_section: None,
576 ticket_ref: None,
577 update_spec: false,
578 lib_only: false,
579 bin_target: None,
580 exclude_tests: true, suppress_false_positives: true, coverage_path: None,
583 coverage_weight: 0.5, localization_strategy: LocalizationStrategy::default(),
586 channel_weights: ChannelWeights::default(),
587 predictive_mutation: false,
588 crash_bucketing: CrashBucketingMode::default(),
589 use_pmat_quality: false,
591 quality_weight: 0.5,
592 pmat_scope: false,
593 pmat_satd: true,
594 pmat_query: None,
595 contracts_path: None,
597 contracts_auto: false,
598 model_parity_path: None,
599 model_parity_auto: false,
600 }
601 }
602}
603
604#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
606pub enum SbflFormula {
607 Tarantula,
609 #[default]
611 Ochiai,
612 DStar2,
614 DStar3,
616}
617
618#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
620pub enum LocalizationStrategy {
621 #[default]
623 Sbfl,
624 Mbfl,
626 Causal,
628 MultiChannel,
630 Hybrid,
632}
633
634impl std::fmt::Display for LocalizationStrategy {
635 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636 match self {
637 Self::Sbfl => write!(f, "SBFL"),
638 Self::Mbfl => write!(f, "MBFL"),
639 Self::Causal => write!(f, "Causal"),
640 Self::MultiChannel => write!(f, "MultiChannel"),
641 Self::Hybrid => write!(f, "Hybrid"),
642 }
643 }
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct ChannelWeights {
649 pub spectrum: f64,
651 pub mutation: f64,
653 pub static_analysis: f64,
655 pub semantic: f64,
657 #[serde(default)]
659 pub quality: f64,
660}
661
662impl Default for ChannelWeights {
663 fn default() -> Self {
664 Self {
665 spectrum: 0.30,
666 mutation: 0.25,
667 static_analysis: 0.20,
668 semantic: 0.15,
669 quality: 0.10,
670 }
671 }
672}
673
674impl ChannelWeights {
675 pub fn normalize(&mut self) {
677 let sum =
678 self.spectrum + self.mutation + self.static_analysis + self.semantic + self.quality;
679 if sum > 0.0 {
680 self.spectrum /= sum;
681 self.mutation /= sum;
682 self.static_analysis /= sum;
683 self.semantic /= sum;
684 self.quality /= sum;
685 }
686 }
687
688 pub fn combine(
690 &self,
691 spectrum: f64,
692 mutation: f64,
693 static_score: f64,
694 semantic: f64,
695 quality: f64,
696 ) -> f64 {
697 self.spectrum * spectrum
698 + self.mutation * mutation
699 + self.static_analysis * static_score
700 + self.semantic * semantic
701 + self.quality * quality
702 }
703}
704
705impl std::fmt::Display for SbflFormula {
706 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
707 match self {
708 SbflFormula::Tarantula => write!(f, "Tarantula"),
709 SbflFormula::Ochiai => write!(f, "Ochiai"),
710 SbflFormula::DStar2 => write!(f, "DStar2"),
711 SbflFormula::DStar3 => write!(f, "DStar3"),
712 }
713 }
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct HuntResult {
719 pub project_path: PathBuf,
721
722 pub mode: HuntMode,
724
725 pub config: HuntConfig,
727
728 pub findings: Vec<Finding>,
730
731 pub stats: HuntStats,
733
734 pub timestamp: String,
736
737 pub duration_ms: u64,
739
740 #[serde(default)]
742 pub phase_timings: PhaseTimings,
743}
744
745impl HuntResult {
746 pub fn new(project_path: impl Into<PathBuf>, mode: HuntMode, config: HuntConfig) -> Self {
748 Self {
749 project_path: project_path.into(),
750 mode,
751 config,
752 findings: Vec::new(),
753 stats: HuntStats::default(),
754 timestamp: chrono::Utc::now().to_rfc3339(),
755 duration_ms: 0,
756 phase_timings: PhaseTimings::default(),
757 }
758 }
759
760 pub fn add_finding(&mut self, finding: Finding) {
762 self.findings.push(finding);
763 }
764
765 pub fn with_duration(mut self, ms: u64) -> Self {
767 self.duration_ms = ms;
768 self
769 }
770
771 pub fn finalize(&mut self) {
773 self.stats = HuntStats::from_findings(&self.findings);
774 }
775
776 pub fn top_findings(&self, n: usize) -> Vec<&Finding> {
778 let mut sorted: Vec<_> = self.findings.iter().collect();
779 sorted.sort_by(|a, b| {
780 b.suspiciousness.partial_cmp(&a.suspiciousness).unwrap_or(std::cmp::Ordering::Equal)
781 });
782 sorted.into_iter().take(n).collect()
783 }
784
785 pub fn summary(&self) -> String {
787 let c = self.stats.by_severity.get(&FindingSeverity::Critical).unwrap_or(&0);
788 let h = self.stats.by_severity.get(&FindingSeverity::High).unwrap_or(&0);
789 format!(
790 "{} mode: {} findings in {} files ({}C {}H) -- {}ms",
791 self.mode,
792 self.findings.len(),
793 self.stats.files_analyzed,
794 c,
795 h,
796 self.duration_ms
797 )
798 }
799}
800
801impl Default for HuntResult {
802 fn default() -> Self {
803 Self {
804 project_path: PathBuf::new(),
805 mode: HuntMode::Quick,
806 config: HuntConfig::default(),
807 findings: Vec::new(),
808 stats: HuntStats::default(),
809 timestamp: String::new(),
810 duration_ms: 0,
811 phase_timings: PhaseTimings::default(),
812 }
813 }
814}
815
816#[derive(Debug, Clone, Default, Serialize, Deserialize)]
818pub struct HuntStats {
819 pub total_findings: usize,
821
822 pub by_severity: HashMap<FindingSeverity, usize>,
824
825 pub by_category: HashMap<DefectCategory, usize>,
827
828 pub files_analyzed: usize,
830
831 pub lines_analyzed: usize,
833
834 pub avg_suspiciousness: f64,
836
837 pub max_suspiciousness: f64,
839
840 pub mode_stats: ModeStats,
842}
843
844impl HuntStats {
845 pub fn from_findings(findings: &[Finding]) -> Self {
847 let mut by_severity: HashMap<FindingSeverity, usize> = HashMap::new();
848 let mut by_category: HashMap<DefectCategory, usize> = HashMap::new();
849 let mut total_suspiciousness = 0.0;
850 let mut max_suspiciousness = 0.0;
851 let mut unique_files: std::collections::HashSet<&std::path::Path> =
852 std::collections::HashSet::new();
853
854 for finding in findings {
855 *by_severity.entry(finding.severity).or_default() += 1;
856 *by_category.entry(finding.category).or_default() += 1;
857 total_suspiciousness += finding.suspiciousness;
858 if finding.suspiciousness > max_suspiciousness {
859 max_suspiciousness = finding.suspiciousness;
860 }
861 unique_files.insert(&finding.file);
862 }
863
864 Self {
865 total_findings: findings.len(),
866 by_severity,
867 by_category,
868 files_analyzed: unique_files.len(),
869 avg_suspiciousness: if findings.is_empty() {
870 0.0
871 } else {
872 total_suspiciousness / findings.len() as f64
873 },
874 max_suspiciousness,
875 ..Default::default()
876 }
877 }
878}
879
880#[derive(Debug, Clone, Default, Serialize, Deserialize)]
882pub struct PhaseTimings {
883 pub mode_dispatch_ms: u64,
885 pub pmat_index_ms: u64,
887 pub pmat_weights_ms: u64,
889 pub finalize_ms: u64,
891 pub contract_gap_ms: u64,
893 pub model_parity_ms: u64,
895}
896
897#[derive(Debug, Clone, Default, Serialize, Deserialize)]
899pub struct ModeStats {
900 pub mutants_total: usize,
902 pub mutants_killed: usize,
904 pub mutants_survived: usize,
906
907 pub sbfl_passing_tests: usize,
909 pub sbfl_failing_tests: usize,
911
912 pub fuzz_executions: usize,
914 pub fuzz_crashes: usize,
916 pub fuzz_coverage: f64,
918
919 pub concolic_paths: usize,
921 pub concolic_constraints_solved: usize,
923 pub concolic_timeouts: usize,
925
926 pub llm_filtered: usize,
928 pub llm_retained: usize,
930}
931
932#[cfg(test)]
933#[path = "types_tests.rs"]
934mod tests;