Skip to content

Commit 165f2f6

Browse files
committed
feat(update): working on next release
1 parent 5d1e71c commit 165f2f6

39 files changed

Lines changed: 2678 additions & 175 deletions

‎docs/ci-integration.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ After the workflow runs:
6464
- **Code Scanning alerts**: Navigate to **GitHub → Security → Code Scanning alerts**
6565
- **PR annotations**: Findings automatically appear as inline annotations on changed lines in pull requests
6666

67+
## PR-Scoped Scans with `--diff`
68+
69+
For pull request reviews, limit the scan to only files changed in the PR using `--diff`:
70+
71+
```bash
72+
baco scan --config baco.toml --diff origin/main...HEAD
73+
```
74+
75+
The `--diff <revspec>` flag filters findings to include only those in files modified in the specified git revision range. Use this for:
76+
- PR reviews: scan only changed files (`origin/main...HEAD`)
77+
- Incremental checks: compare against a specific commit (`main..feature-branch`)
78+
- Reducing noise: focus on recent changes rather than the entire codebase
79+
80+
The revspec follows standard git syntax (e.g., `A...B` for changes in B not in A, `A..B` for changes reachable from B but not A).
81+
6782
## Other CI Systems
6883

6984
### Azure DevOps

‎docs/configuration.md‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,30 @@ timeout_secs = 120 # Per-phase timeout override
182182
temperature = 0.3 # Per-phase temperature override
183183
```
184184

185+
### LLM Timeout Semantics
186+
187+
Timeout configuration operates at two levels:
188+
189+
| Level | Config Path | Type | Default | Behavior |
190+
|-------|-------------|------|---------|----------|
191+
| Global | `[llm] timeout_secs` | `u64` | `60` (in `config.example.toml`) | Applied to all phases without a per-phase override |
192+
| Per-phase | `[llm.phases.<phase>] timeout_secs` | `Option<u64>` | `None` | When set, overrides the global timeout for that phase |
193+
194+
**Runtime behavior:**
195+
- HTTP request timeout: `Duration::from_secs(timeout_secs)` (source: `src/llm/mod.rs:236`)
196+
- Connection timeout: Fixed at 10 seconds (source: `src/llm/mod.rs:192`)
197+
- Default runtime timeout (if no config provided): 30 seconds (source: `src/llm/mod.rs:170`)
198+
199+
**What value `0` means:** Setting `timeout_secs = 0` results in `Duration::from_secs(0)`, which causes an immediate timeout on every request. Use only for testing.
200+
201+
**Retry behavior:** Failed requests (429, 5xx, network errors) are retried with exponential backoff:
202+
- Base delay: Configured via `[llm] retry_backoff_ms` (default: 2000ms)
203+
- Formula: `base_ms * 2^attempt`, capped at 30 seconds
204+
- Max retries: Configured via `[llm] max_retries` (default: 3)
205+
- Source: `src/llm/mod.rs:254-269`
206+
207+
Retries are bounded; after exhausting retries on one model, the client fails over to the next configured model (if multiple models are specified).
208+
185209
**Single model:**
186210
```toml
187211
[llm.phases.discovery]
@@ -353,6 +377,8 @@ include_rejected = false
353377
| `LLM_STATIC_ANALYSIS_KEY` | Overrides `llm.phases.static_analysis.api_key` |
354378
| `LLM_SECURITY_AGENT_VERIFICATION_KEY` | Overrides `llm.phases.security_agent_verification.api_key` |
355379
| `LLM_THREAT_MODELING_KEY` | Overrides `llm.phases.threat_modeling.api_key` |
380+
| `LLM_CONFIG_PATH` | Overrides config path for `baco verify` (source: `src/cli/verify.rs:24`) |
381+
| `NO_COLOR` | Disables colored output (source: `src/ui.rs:46`) |
356382
| `TICKET_GITHUB_KEY` | Overrides `[[tickets.systems]]` api_key for GitHub |
357383
| `TICKET_GITLAB_KEY` | Overrides `[[tickets.systems]]` api_key for GitLab |
358384

@@ -362,6 +388,57 @@ include_rejected = false
362388
- **report.html**: Visual report with severity colors, code snippets, AI summary
363389
- **report.sarif**: SARIF format for CI/CD integration
364390

391+
## Scan Options
392+
393+
### `--dry-run`
394+
395+
The `--dry-run` flag performs indexing and prioritization without executing any LLM or semgrep phases. It prints an estimate of the resources a full scan would consume, then exits.
396+
397+
**What it does:**
398+
1. Indexes the project (counts files, calculates total size, estimates tokens)
399+
2. Computes priority scores for each file
400+
3. Estimates LLM calls based on budget and triage configuration
401+
4. Prints the summary and exits
402+
403+
**What it does NOT do:**
404+
- No LLM API calls
405+
- No semgrep scanning
406+
- No findings produced
407+
- No report generation
408+
409+
**Example usage:**
410+
```bash
411+
baco scan --config baco.toml --dry-run
412+
```
413+
414+
**Output example:**
415+
```
416+
[Dry Run] Project Estimate
417+
═══════════════════════════════════════
418+
Target: ./my-project
419+
420+
Files by language:
421+
python: 42 files
422+
javascript: 18 files
423+
424+
Total files: 60
425+
Total size: 1234567 bytes
426+
Estimated tokens (~4 chars/token): 308641
427+
428+
Planned LLM calls: 45
429+
(budget max: 100, normal cap: 75, high-risk: 25)
430+
431+
Average priority score: 0.67
432+
```
433+
434+
Use `--dry-run` before a full scan to:
435+
- Estimate costs (LLM call count, token usage)
436+
- Verify your configuration is correct
437+
- Check which files will be analyzed
438+
- Decide if you need to adjust budget/triage settings
439+
440+
Source: `src/cli/scan.rs:346-478` (`run_dry_run` function).
441+
365442
## Aggregation Configuration
366443

367444
The `[aggregation]` section configures the AI Aggregation phase settings.

‎presets/cpp.toml‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,6 @@ rules:
7171
- pattern-not: printf('...')
7272
''']
7373

74-
[scanner.performance]
75-
enable_caching = true
76-
max_workers = 4
77-
parallel_threshold_kb = 100
78-
skip_large_files_mb = 5
79-
8074
[llm]
8175
temperature = 0.2
8276
max_concurrent = 4

‎src/agent/sandbox.rs‎

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,72 @@ pub enum SandboxError {
1616
Timeout(u64),
1717
}
1818

19+
/// Validate that a resolved path is contained within the sandbox root.
20+
///
21+
/// Checks:
22+
/// - Path is not absolute
23+
/// - No `..` components in the path
24+
/// - Final resolved path starts with canonicalized sandbox root
25+
/// - Parent directory exists (does not create directories)
26+
fn validate_path_containment(sandbox_root: &Path, user_path: &str) -> Result<PathBuf, String> {
27+
let path = Path::new(user_path);
28+
29+
// Reject absolute paths
30+
if path.is_absolute() {
31+
return Err(format!("Absolute path not allowed: {}", user_path));
32+
}
33+
34+
// Reject dot sequences before component analysis: encoded, spaced, and
35+
// separator-mixed variants are never legitimate sandbox paths.
36+
if user_path.contains("..") {
37+
return Err(format!("Path traversal: {}", user_path));
38+
}
39+
40+
// Reject any path with `..` components
41+
if path
42+
.components()
43+
.any(|c| c == std::path::Component::ParentDir)
44+
{
45+
return Err(format!("Path traversal not allowed: {}", user_path));
46+
}
47+
48+
// Join with sandbox root
49+
let full = sandbox_root.join(path);
50+
51+
// Canonicalize the sandbox root for comparison
52+
let canonical_root = sandbox_root
53+
.canonicalize()
54+
.map_err(|e| format!("Cannot canonicalize sandbox root {:?}: {}", sandbox_root, e))?;
55+
56+
// Check containment by canonicalizing the target if it exists,
57+
// or verifying the parent path structure is within bounds
58+
let final_check = if full.exists() {
59+
full.canonicalize()
60+
.map_err(|e| format!("Cannot canonicalize target {:?}: {}", full, e))?
61+
} else {
62+
// If the file doesn't exist, verify parent is within bounds
63+
if let Some(parent) = full.parent() {
64+
if parent.exists() {
65+
parent
66+
.canonicalize()
67+
.map_err(|e| format!("Cannot canonicalize parent {:?}: {}", parent, e))?
68+
} else {
69+
// Parent doesn't exist - check path components are safe
70+
// We already rejected .. and absolute, so join is safe
71+
canonical_root.join(parent.strip_prefix(&canonical_root).unwrap_or(parent))
72+
}
73+
} else {
74+
canonical_root.clone()
75+
}
76+
};
77+
78+
if !final_check.starts_with(&canonical_root) {
79+
return Err(format!("Path escapes sandbox: {}", user_path));
80+
}
81+
82+
Ok(full)
83+
}
84+
1985
pub struct ToolSandbox {
2086
pub(super) temp_dir: PathBuf,
2187
pub(super) timeout_secs: u64,
@@ -26,11 +92,7 @@ impl SandboxLike for ToolSandbox {
2692
&self.temp_dir
2793
}
2894
fn resolve_safe_path(&self, path: &str) -> Result<PathBuf, String> {
29-
// Check for path traversal before joining
30-
if path.contains("..") {
31-
return Err(format!("Path traversal: {}", path));
32-
}
33-
let full = self.temp_dir.join(path);
95+
let full = validate_path_containment(&self.temp_dir, path)?;
3496
if !full.exists() {
3597
return Err(format!("Path does not exist: {}", path));
3698
}
@@ -87,14 +149,29 @@ impl SandboxLike for ToolSandbox {
87149
fn create_temp_file(&self, path: &str, content: &str) -> Result<PathBuf, String> {
88150
self.validate_test_source(content)
89151
.map_err(|e| format!("Validation failed: {}", e))?;
90-
// Check path traversal by looking for ".." in the input path
91-
if path.contains("..") {
92-
return Err(format!("Path traversal: {}", path));
93-
}
94-
let full = self.temp_dir.join(path);
152+
153+
// Pre-condition: validate path containment before write
154+
let full = validate_path_containment(&self.temp_dir, path)?;
155+
156+
// Write the file
95157
std::fs::File::create(&full)
96158
.and_then(|mut f| f.write_all(content.as_bytes()))
97159
.map_err(|e| format!("Write failed: {}", e))?;
160+
161+
// Post-condition: verify containment after write (catches symlink swaps)
162+
let canonical_after = full
163+
.canonicalize()
164+
.map_err(|e| format!("Cannot canonicalize after write {:?}: {}", full, e))?;
165+
let canonical_root = self
166+
.temp_dir
167+
.canonicalize()
168+
.map_err(|e| format!("Cannot canonicalize sandbox root: {}", e))?;
169+
if !canonical_after.starts_with(&canonical_root) {
170+
// File was created via symlink outside sandbox - delete and error
171+
let _ = std::fs::remove_file(&full);
172+
return Err(format!("Path escapes sandbox after write: {}", path));
173+
}
174+
98175
Ok(full)
99176
}
100177
fn is_path_allowed(&self, path: &Path) -> bool {
@@ -151,11 +228,7 @@ impl ToolSandbox {
151228
}
152229

153230
pub fn resolve_safe_path(&self, path: &str) -> Result<PathBuf, String> {
154-
// Check for path traversal before joining
155-
if path.contains("..") {
156-
return Err(format!("Path traversal: {}", path));
157-
}
158-
let full = self.temp_dir.join(path);
231+
let full = validate_path_containment(&self.temp_dir, path)?;
159232
if !full.exists() {
160233
return Err(format!("Path does not exist: {}", path));
161234
}
@@ -215,14 +288,29 @@ impl ToolSandbox {
215288
pub fn create_temp_file(&self, path: &str, content: &str) -> Result<PathBuf, String> {
216289
self.validate_test_source(content)
217290
.map_err(|e| format!("Validation failed: {}", e))?;
218-
// Check path traversal by looking for ".." in the input path
219-
if path.contains("..") {
220-
return Err(format!("Path traversal: {}", path));
221-
}
222-
let full = self.temp_dir.join(path);
291+
292+
// Pre-condition: validate path containment before write
293+
let full = validate_path_containment(&self.temp_dir, path)?;
294+
295+
// Write the file
223296
std::fs::File::create(&full)
224297
.and_then(|mut f| f.write_all(content.as_bytes()))
225298
.map_err(|e| format!("Write failed: {}", e))?;
299+
300+
// Post-condition: verify containment after write (catches symlink swaps)
301+
let canonical_after = full
302+
.canonicalize()
303+
.map_err(|e| format!("Cannot canonicalize after write {:?}: {}", full, e))?;
304+
let canonical_root = self
305+
.temp_dir
306+
.canonicalize()
307+
.map_err(|e| format!("Cannot canonicalize sandbox root: {}", e))?;
308+
if !canonical_after.starts_with(&canonical_root) {
309+
// File was created via symlink outside sandbox - delete and error
310+
let _ = std::fs::remove_file(&full);
311+
return Err(format!("Path escapes sandbox after write: {}", path));
312+
}
313+
226314
Ok(full)
227315
}
228316

‎src/citation_verification.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub struct CitationReport {
2727
///
2828
/// On failure:
2929
/// - `confidence_score *= 0.5`
30-
/// - Appends to `verification_notes`: "citation verification failed: <reason>"
30+
/// - Appends to `verification_notes`: "citation verification failed: reason" (where reason is the failure cause)
3131
///
3232
/// Returns a summary report with counts.
3333
pub fn verify_citations(

‎src/cli/report.rs‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ impl std::fmt::Display for ReportFormat {
2626
pub fn run_report(
2727
input: &Path,
2828
format: ReportFormat,
29+
config_path: Option<&Path>,
2930
quiet: bool,
3031
) -> Result<(), Box<dyn std::error::Error>> {
3132
let findings = validation::validate_findings(input)?;
@@ -42,15 +43,29 @@ pub fn run_report(
4243
ReportFormat::Markdown => output_dir.join("report.md"),
4344
};
4445

46+
// Load config if provided
47+
let config = if let Some(cfg_path) = config_path {
48+
Some(crate::config::ScannerConfig::from_file(
49+
cfg_path.to_string_lossy().as_ref(),
50+
)?)
51+
} else {
52+
None
53+
};
54+
4555
if !quiet {
4656
info!("Generating {} report to {:?}", format, output_path);
4757
}
4858

4959
match format {
5060
ReportFormat::Html => {
5161
use crate::report::html::generate_html_report;
52-
generate_html_report(&findings, &output_path.to_string_lossy(), None, None)
53-
.map_err(|e| format!("Failed to generate HTML report: {}", e))?;
62+
generate_html_report(
63+
&findings,
64+
&output_path.to_string_lossy(),
65+
config.as_ref(),
66+
None,
67+
)
68+
.map_err(|e| format!("Failed to generate HTML report: {}", e))?;
5469
}
5570
ReportFormat::Json => {
5671
use crate::report::json::write_findings_json;
@@ -59,7 +74,7 @@ pub fn run_report(
5974
&[],
6075
&output_path.to_string_lossy(),
6176
None,
62-
None,
77+
config.as_ref(),
6378
None,
6479
None, // scan_health
6580
)
@@ -68,7 +83,7 @@ pub fn run_report(
6883
ReportFormat::Sarif => {
6984
use crate::report::sarif::generate_sarif_report;
7085
let sarif_path = output_path.clone();
71-
let sarif_json = generate_sarif_report(&findings, None)?;
86+
let sarif_json = generate_sarif_report(&findings, config.as_ref())?;
7287
std::fs::write(&sarif_path, sarif_json)
7388
.map_err(|e| format!("Failed to write SARIF report: {}", e))?;
7489
if !quiet {
@@ -84,7 +99,13 @@ pub fn run_report(
8499
.and_then(|p| p.file_name())
85100
.map(|n| n.to_string_lossy().to_string())
86101
.unwrap_or_else(|| "unknown".to_string());
87-
let md_content = generate_markdown_report(&findings, &project_name);
102+
// Markdown does not take config, so use pre-filtered slice
103+
let gated_findings = if config.as_ref().is_some_and(|c| c.output.evidence_gate) {
104+
crate::report::apply_evidence_gate(&findings, config.as_ref())
105+
} else {
106+
findings.clone()
107+
};
108+
let md_content = generate_markdown_report(&gated_findings, &project_name);
88109
std::fs::write(&md_path, md_content)
89110
.map_err(|e| format!("Failed to write markdown report: {}", e))?;
90111
if !quiet {

0 commit comments

Comments
 (0)