Skip to content

Commit 5cc75ae

Browse files
committed
feat(update): working on next release
♬ Fury Weekend - Stuck In the Sunlight
1 parent 015aa3c commit 5cc75ae

25 files changed

Lines changed: 1499 additions & 84 deletions

‎docs/architecture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ The pipeline includes several verification gates and calibration layers that aug
100100

101101
Before rendering the final report, deterministic checks verify that all citations (file paths + line ranges) match the scanned source tree. Findings failing this check have their confidence score halved and a note added explaining the discrepancy.
102102

103-
Configured via `[citation_verification] enabled = false` (disabled by default).
103+
Configured via `[citation_verification] enabled = true` (enabled by default).
104104

105105
### Prior-Runs Store (Discovery Phase)
106106

‎docs/configuration.md‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,26 @@ cp presets/wordpress-core.toml ~/.config/baco/presets/my-project.toml
814814
baco scan --config my.toml --preset my-project
815815
```
816816

817+
Name the preset in the config instead of on the command line:
818+
819+
```toml
820+
preset = "my-project"
821+
822+
[project]
823+
name = "my-project"
824+
path = "."
825+
```
826+
827+
`--preset` wins when both are present, since passing it is a deliberate
828+
override. A name that does not resolve fails the load and names the preset,
829+
rather than quietly scanning with no preset applied.
830+
831+
Presets matter more than the flag suggests: they carry
832+
`required_security_primitives` and the hook registry, which is what the
833+
entry-point primitive check reads. A scan run without the WordPress preset
834+
finds no missing-nonce findings, because it has no list of nonce functions to
835+
check against.
836+
817837
### Fields a Preset Can Override
818838

819839
| Section | Key fields |
@@ -861,15 +881,22 @@ enabled = false
861881

862882
## Citation verification
863883

864-
The `[citation_verification]` section controls verification that finding citations (file + line) actually resolve in the target source, downgrading unverifiable findings.
884+
The `[citation_verification]` section controls verification that finding citations (file + line) actually resolve in the target source.
885+
886+
A citation that does not resolve gets `verification_status = failed` and a note
887+
in `verification_notes`. The finding is kept and its confidence is left alone:
888+
confidence is the model's own estimate, and multiplying it by a constant
889+
corrupts that estimate without saying anything about how wrong the citation is.
890+
Whether failed findings reach the report is decided by
891+
`[output] evidence_gate`; `include_rejected` keeps them visible when it is on.
865892

866893
| Key | Type | Default | Description |
867894
| --------- | ---- | ------- | ---------------------------------------------------- |
868-
| `enabled` | bool | `false` | Verify finding citations against source files |
895+
| `enabled` | bool | `true` | Verify finding citations against source files |
869896

870897
```toml
871898
[citation_verification]
872-
enabled = false
899+
enabled = true
873900
```
874901

875902
## Prior runs

‎src/citation_verification.rs‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,18 @@ pub struct CitationReport {
2626
/// - If `line_number` is Some(n), reads the file and requires n <= total line count
2727
///
2828
/// On failure:
29-
/// - `confidence_score *= 0.5`
29+
/// - `verification_status = Failed`
3030
/// - Appends to `verification_notes`: "citation verification failed: reason" (where reason is the failure cause)
3131
///
32+
/// The finding is kept, not dropped. Two reasons: `include_rejected` exists so a
33+
/// user can ask to see everything, and deleting here would be a second filter
34+
/// that does not respect it. The status is the signal, and whether it is
35+
/// filtered is `apply_evidence_gate`'s decision.
36+
///
37+
/// Confidence is deliberately left alone. `classify_finding` tiers on it, so
38+
/// scaling it by a constant corrupts the model's own estimate with a number
39+
/// that says nothing about how wrong the finding is.
40+
///
3241
/// Returns a summary report with counts.
3342
pub fn verify_citations(
3443
findings: &mut [VulnerabilityFinding],
@@ -49,7 +58,7 @@ pub fn verify_citations(
4958

5059
// Reject absolute paths and path traversal attempts
5160
if finding.file_path.starts_with('/') || finding.file_path.contains("..") {
52-
finding.confidence_score *= 0.5;
61+
finding.verification_status = Some(crate::findings::VerificationStatus::Failed);
5362
let note = format!(
5463
"citation verification failed: path traversal rejected: {}",
5564
finding.file_path
@@ -63,7 +72,7 @@ pub fn verify_citations(
6372
let file_content = match fs::read_to_string(&file_path) {
6473
Ok(content) => content,
6574
Err(_) => {
66-
finding.confidence_score *= 0.5;
75+
finding.verification_status = Some(crate::findings::VerificationStatus::Failed);
6776
let note = format!(
6877
"citation verification failed: file not found or unreadable: {}",
6978
finding.file_path
@@ -78,7 +87,7 @@ pub fn verify_citations(
7887
if let Some(line_num) = finding.line_number {
7988
// Line 0 is invalid (lines are 1-indexed)
8089
if line_num == 0 {
81-
finding.confidence_score *= 0.5;
90+
finding.verification_status = Some(crate::findings::VerificationStatus::Failed);
8291
let note = format!(
8392
"citation verification failed: line 0 is invalid (1-indexed): {}",
8493
finding.file_path
@@ -91,7 +100,7 @@ pub fn verify_citations(
91100
let line_count = file_content.lines().count();
92101

93102
if line_num as usize > line_count {
94-
finding.confidence_score *= 0.5;
103+
finding.verification_status = Some(crate::findings::VerificationStatus::Failed);
95104
let note = format!(
96105
"citation verification failed: line {} out of range (file has {} lines): {}",
97106
line_num, line_count, finding.file_path

‎src/config/knowledge.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::collections::HashMap;
33

44
/// Per-language hook registry configuration
55
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
6+
#[serde(deny_unknown_fields)]
67
pub struct HookRegistryLanguageConfig {
78
/// Label used to synthesize hook names when the registration pattern lacks a `hook` named capture
89
#[serde(default = "default_hook_label")]
@@ -21,6 +22,7 @@ fn default_hook_label() -> String {
2122

2223
/// Knowledge configuration: per-CWE false-positive indicator patterns
2324
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
25+
#[serde(deny_unknown_fields)]
2426
pub struct KnowledgeConfig {
2527
/// CWE id ("CWE-79") -> literal code substrings indicating a likely false positive
2628
#[serde(default)]

‎src/config/llm.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
22
use std::collections::HashMap;
33

44
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
5+
#[serde(deny_unknown_fields)]
56
pub struct LlmConfig {
67
/// Global endpoint used by any phase without its own `base_url`.
78
#[serde(default)]
@@ -36,6 +37,7 @@ fn default_enable_llm_cache() -> bool {
3637

3738
/// Pricing for a specific LLM model (per 1K tokens)
3839
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
40+
#[serde(deny_unknown_fields)]
3941
pub struct ModelPricing {
4042
/// Cost per 1K prompt tokens (in USD or currency unit of choice)
4143
#[serde(default)]
@@ -54,6 +56,7 @@ impl ModelPricing {
5456
}
5557

5658
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
59+
#[serde(deny_unknown_fields)]
5760
pub struct LlmPhasesConfig {
5861
#[serde(default)]
5962
pub discovery: LlmPhaseConfig,
@@ -74,6 +77,7 @@ pub struct LlmPhasesConfig {
7477
impl LlmPhasesConfig {}
7578

7679
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
80+
#[serde(deny_unknown_fields)]
7781
pub struct LlmPhaseConfig {
7882
#[serde(default)]
7983
pub base_url: String,
@@ -107,6 +111,7 @@ impl LlmPhaseConfig {
107111

108112
/// AgentFlow phase configuration
109113
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
114+
#[serde(deny_unknown_fields)]
110115
pub struct AgentFlowPhaseConfig {
111116
/// Gate: run AgentFlow harness synthesis
112117
#[serde(default)]
@@ -121,12 +126,14 @@ fn default_agent_flow_max_iterations() -> u32 {
121126
}
122127

123128
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
129+
#[serde(deny_unknown_fields)]
124130
pub struct PromptOverrides {
125131
#[serde(default, rename = "phases")]
126132
pub phase_overrides: HashMap<String, String>,
127133
}
128134

129135
#[derive(Debug, Clone, Serialize, Deserialize)]
136+
#[serde(deny_unknown_fields)]
130137
pub struct AgentConfig {
131138
#[serde(default)]
132139
pub enabled: bool,

‎src/config/mod.rs‎

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use std::path::PathBuf;
2323

2424
/// Eval-suite settings (`[eval]` section).
2525
#[derive(Debug, Clone, Serialize, Deserialize)]
26+
#[serde(deny_unknown_fields)]
2627
pub struct EvalConfig {
2728
/// Minimum aggregate pass-rate for `baco eval` (0.0..=1.0).
2829
#[serde(default = "default_eval_floor")]
@@ -42,6 +43,7 @@ impl Default for EvalConfig {
4243
}
4344

4445
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
46+
#[serde(deny_unknown_fields)]
4547
pub struct ScannerConfig {
4648
#[serde(default)]
4749
pub project: ProjectConfig,
@@ -215,17 +217,45 @@ impl ScannerConfig {
215217
/// Load with a preset applied UNDER the user file: defaults → preset →
216218
/// user-explicit-keys (TOML deep merge, arrays replaced wholesale).
217219
/// User config wins over preset; explicit `temperature = 0.0` applies.
220+
///
221+
/// The preset can be named two ways: with the `preset` argument, which is
222+
/// what the `--preset` flag supplies, or with a `preset = "name"` key in the
223+
/// config file. The flag wins when both are present, because passing it is a
224+
/// deliberate override.
218225
pub fn from_file_with_preset(
219226
path: &str,
220227
preset: Option<crate::preset::PresetOverlay>,
221228
) -> Result<Self, ConfigError> {
229+
let content = fs::read_to_string(path)?;
230+
let expanded = expand_env_vars(&content);
231+
let mut user_val: toml::Value = toml::from_str(&expanded)?;
232+
233+
// `preset` is a directive, not a configuration field. It is taken out
234+
// here because deserialisation denies unknown keys, and it has to be
235+
// read before the user file is merged so the layering below still sees
236+
// only real settings.
237+
let preset_from_config: Option<String> = user_val
238+
.as_table_mut()
239+
.and_then(|t| t.remove("preset"))
240+
.and_then(|v| v.as_str().map(str::to_string));
241+
242+
let preset = match preset {
243+
Some(explicit) => Some(explicit),
244+
None => match preset_from_config {
245+
Some(name) => Some(crate::preset::load_preset(&name).map_err(|e| {
246+
ConfigError::Validation {
247+
field: "preset".to_string(),
248+
message: format!("preset `{name}` could not be loaded: {e}"),
249+
}
250+
})?),
251+
None => None,
252+
},
253+
};
254+
222255
let mut base = ScannerConfig::default();
223256
if let Some(overlay) = preset {
224257
overlay.merge_into(&mut base);
225258
}
226-
let content = fs::read_to_string(path)?;
227-
let expanded = expand_env_vars(&content);
228-
let user_val: toml::Value = toml::from_str(&expanded)?;
229259
let mut base_val = toml::Value::try_from(&base).map_err(|e| ConfigError::Parse {
230260
message: e.to_string(),
231261
line: None,
@@ -349,6 +379,7 @@ impl ScannerConfig {
349379
}
350380

351381
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
382+
#[serde(deny_unknown_fields)]
352383
pub struct ProjectConfig {
353384
#[serde(default)]
354385
pub name: String,
@@ -359,6 +390,7 @@ pub struct ProjectConfig {
359390
}
360391

361392
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
393+
#[serde(deny_unknown_fields)]
362394
pub struct OutputConfig {
363395
#[serde(default)]
364396
pub dir: String,
@@ -370,12 +402,22 @@ pub struct OutputConfig {
370402

371403
/// Citation verification gate: deterministic checks that report citations
372404
/// (file existence, line ranges) match the scanned tree before rendering.
373-
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
374-
#[serde(default)]
405+
///
406+
/// On by default. It is local file I/O and no LLM call, and its absence is not
407+
/// visible: a finding citing line 3894 of a 326-line file is simply wrong, and
408+
/// nothing downstream catches it unless this runs.
409+
#[derive(Debug, Clone, Serialize, Deserialize)]
410+
#[serde(deny_unknown_fields, default)]
375411
pub struct CitationVerificationConfig {
376412
pub enabled: bool,
377413
}
378414

415+
impl Default for CitationVerificationConfig {
416+
fn default() -> Self {
417+
Self { enabled: true }
418+
}
419+
}
420+
379421
/// Prior-runs store: cross-run findings history used for skip directives
380422
/// and coverage gap targeting on subsequent scans.
381423
#[derive(Debug, Clone, Serialize, Deserialize)]

‎src/config/phases.rs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ use std::path::PathBuf;
33

44
/// Aggregation configuration including false positive store settings
55
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
6+
#[serde(deny_unknown_fields)]
67
pub struct AggregationConfig {}
78

89
/// Rule synthesis configuration (MoCQ: LLM→semgrep rule generation)
910
#[derive(Debug, Clone, Serialize, Deserialize)]
11+
#[serde(deny_unknown_fields)]
1012
pub struct RuleSynthConfig {
1113
/// Whether rules synthesis is enabled
1214
#[serde(default)]
@@ -94,6 +96,7 @@ pub enum NormalizationTier {
9496

9597
/// Configuration for confidence normalization.
9698
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
99+
#[serde(deny_unknown_fields)]
97100
pub struct NormalizationConfig {
98101
/// Whether normalization is enabled.
99102
pub enabled: bool,
@@ -172,6 +175,7 @@ pub struct ValidateConfig {
172175
/// Augments LLM input with control path (AST/CFG/DFG), knowledge path
173176
/// (CWE pattern RAG), and semantic path (function summary) before judgement.
174177
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
178+
#[serde(deny_unknown_fields)]
175179
pub struct VultriageConfig {
176180
/// Whether triple-path context augmentation is enabled
177181
#[serde(default)]
@@ -185,6 +189,7 @@ pub struct VultriageConfig {
185189
/// First pass: cheap model triage per file to filter out non-suspicious files.
186190
/// Second pass: deep analysis only on files that pass the suspicion threshold.
187191
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
192+
#[serde(deny_unknown_fields)]
188193
pub struct TriageConfig {
189194
/// Whether triage cascade is enabled (default: false)
190195
#[serde(default)]
@@ -214,6 +219,7 @@ impl Default for TriageConfig {
214219
/// Configuration for file prioritization (T18).
215220
/// Scores files based on recency, entry-point status, and size to prioritize LLM budget.
216221
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
222+
#[serde(deny_unknown_fields)]
217223
pub struct PriorityConfig {
218224
/// Whether prioritization is enabled (default: false)
219225
#[serde(default)]
@@ -251,6 +257,7 @@ impl Default for PriorityConfig {
251257
/// Configuration for LLM budget enforcement (T18).
252258
/// Limits total LLM calls and reserves budget for high-risk files.
253259
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
260+
#[serde(deny_unknown_fields)]
254261
pub struct BudgetConfig {
255262
/// Whether budget enforcement is enabled (default: false)
256263
#[serde(default)]
@@ -277,6 +284,7 @@ impl Default for BudgetConfig {
277284
/// Queries the LLM N times to get a CWE candidate set ("policy"),
278285
/// then a final call with the policy as context to pick one label.
279286
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
287+
#[serde(deny_unknown_fields)]
280288
pub struct PolicySamplingConfig {
281289
/// Whether policy-based generation is enabled
282290
#[serde(default)]
@@ -298,6 +306,7 @@ impl Default for PolicySamplingConfig {
298306
/// Configuration for the VulnLLM-R agent scaffold (P2.5).
299307
/// Builds 3-path call-graph context + function-lookup tool per target.
300308
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
309+
#[serde(deny_unknown_fields)]
301310
pub struct AgentScaffoldConfig {
302311
/// Whether the agent scaffold is enabled
303312
#[serde(default)]
@@ -325,6 +334,7 @@ impl Default for AgentScaffoldConfig {
325334
/// LLM prompt. With `auto_level = true`, the level is chosen based on
326335
/// the configured model name.
327336
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
337+
#[serde(deny_unknown_fields)]
328338
pub struct PacvdConfig {
329339
/// Whether PacVD abstraction is enabled
330340
#[serde(default)]

0 commit comments

Comments
 (0)