shagghiesuperstar/veilbreaker
v0.3.0MIT
Deterministic, zero-dependency coverage of the latest documented (2025–2026) prompt-injection concealment and exfiltration TTPs. Built to pair with an ML classifier.
Changelog
0.3.0 (2026-10-18)
- New: metadata-only event log. Every MCP tool call appends one JSON line to
VEILBREAKER_LOG(default~/.veilbreaker/events.jsonl;VEILBREAKER_LOG=offdisables it). Fields: ts, tool, verdict, decision, mode, max_severity, ttps, sink, host (registrable domain only), input_sha256, input_chars, duration_ms, version, plus egressreasons,mcp_changesanderror. Never stores raw input, page text, arguments, URLs with paths or queries, emails or secrets. Egress reason codes are mapped to catalog TTP ids (e.g. PRIVATE_DATA_IN_ARGS → VB-E05). Rotates at 10 MB and keeps one old file. A log write failure never breaks the tool call; it is returned as an extra note.reportcalls are not logged. - New MCP tool
report{since?: ISO|"24h"|"7d", group_by?: "ttp"|"tool"|"host"|"day"}: calls; suspicious/review/clean/unsupported; egress deny vs allow and shadow-only denies; top TTPs with catalog names and citations; top hosts; mcp_verify changes; log_path, first_event, last_event; per-day counts. An empty log returns explicit zeros and "0 events". 8 tools in total. - New dashboard.
--cli dashboard [--since 7d] [--out veilbreaker-dashboard.html]writes one self-contained static HTML file (inline CSS and SVG, no scripts, no external resources, no network requests, no raw content, CSPdefault-src 'none').--cli serve-dashboard [--port 8790]binds 127.0.0.1 only, regenerates on each request, refuses a non-loopback bind unless--bindis given explicitly (and then warns), and refuses non-loopback Host headers. Also--cli report. - Honest wording. README "Honest scope" and the egress
handlingtext now say: shadow = a deny is recorded to the event log and reported to the user; VeilBreaker still cannot block host tools itself. If the log is off,handlingsays nothing is recorded.step-0-untrusted-contentandwhy-deterministicskills updated to match. - New template
grokbot-template/veilbreaker-watch.template.json("VeilBreaker Watch"), same key layout as the guard template: 4 memories, 4 verbatim skills (getting-started, the newwatch-and-report, step-0-untrusted-content, mcp-tool-poisoning), routines for a daily 08:00 digest, a weekly dashboard file and a daily MCP pin check. README section added. getting-startedskill: 8 tools,VEILBREAKER_MODEandVEILBREAKER_LOG, runreport, open the dashboard. The guard template carries the updated skills verbatim.mcp.jsonpassesVEILBREAKER_LOGthrough.- Tests: new
tests/watch.test.mjs(log write/rotate/off, write failure as a note, no planted secret or email in the log,reportmath on a fixture log, dashboard has no scripts/src/href/URLs and contains the totals, escaping of hostile log values, serve-dashboard loopback-only) andtests/watch-template.test.mjs. Every test file importstests/helpers/isolate-log.mjs, so tests never touch~/.veilbreaker. - Folded in from the unreleased 0.2.4 entry:
- Fix:
safe_fetchnow fails loudly on encoded responses. A response with aContent-Encodingother thanidentity(gzip, deflate, br, or a list) returns verdictunsupported, a reason that says the body is encoded and NOT scanned, and 0 findings. v0.2.3 regressed against v0.2.2 here: a gzip response came back as verdictunsupportedwith no concealment findings and no reason saying the page was encoded, so a hidden payload that v0.2.2 caught went unreported. Nothing is decompressed (zip-bomb risk); the README "Honest scope" now says so. - Fix: a never-ending encoded body is aborted early. The response is dropped as soon as the headers show a non-identity encoding, so
safe_fetchdoes not wait for the body. - Docs: the 198.18.0.0/15 carve-out (RFC 2544 range, allowed for fake-IP DNS setups) is documented in the README "Honest scope".
- Docs: a personal first name is removed from the docs.
- Repo:
tests/test-output.txtis untracked and gitignored; the local file is kept.
- Fix:
- Fix (found in the in-app run): an egress deny was logged with
max_severity: "none". Every egress deny is now logged with at leasthigh(criticalfor SECRET_IN_ARGS / PRIVATE_DATA_IN_ARGS), for any reason code; an allow staysnone. New test intests/watch.test.mjs. - Docs (R1, R2): upgrade note in the README, the getting-started and watch-and-report skills, and the Watch template README and persona, limited to what was verified: a fresh install lists 8 tools; when upgrading, Grok Bot may keep the old tool list (removing and re-adding under the same name, or restarting, did not refresh it in our test); if
reportis missing, add the connector under a new name, or run--cli reporton the same log. - Fix (R1): egress
handlingis surface-aware. MCP calls say a deny is recorded to the event log;--cli egresssays "CLI runs are not recorded to the event log"; direct library calls say they are not recorded. - Fix (R1): IP-literal hosts are no longer logged verbatim; they become
<ipv4-private>,<ipv4-public>,<ipv6-private>or<ipv6-public>(private = refused by the SSRF guard'sipBlocked). - Fix (R1):
--cli dashboard --outcreates missing parent directories instead of failing with ENOENT. - Tests (R1): new stdio transport test (spawn the server; initialize, tools/list = 8 tools, tools/call scan).
- In-app test (2026-10-18, Node v20.19.2, code e6f3002): 3 scans and 1 egress deny PASS; the event log is metadata only. Grok Bot kept the 7-tool list from install time after a restart, so
reportwas not callable in-app and was run with the CLI on the same log. After the connector was added under a new name, the in-appreportran with the same numbers. Details: docs/IN-APP-TEST.md. - Version 0.3.0 in all manifests and the server.
0.2.3 (2026-10-17)
- New Grok Bot template
grokbot-template/veilbreaker-guard.template.jsonand its README, using the same key layout as the hermes-fleet-ops template (visibility, profile, memory, skills, routines, plugins, gettingStarted). It has a guard persona, 4 memory rules, and 11 plugin skills copied verbatim. Routines: a daily MCP pin check (mcp_verify) and a weekly self-test. Plugin:veilbreaker. - New
getting-startedskill: install, connect, first scan. - New
--cli selftest: 18 built-in scan and egress fixtures. Prints pass/fail counts; exits 1 on any failure. - New
tests/template.test.mjs: the template parses and matches the reference layout, every skill exists in the plugin and is copied verbatim, every tool it names exists intools/list, and the self-test passes. - Version 0.2.3 in all manifests and the server.
- Security: SSRF guard for
safe_fetch.- Covered: http(s) only; no credentials in URLs. Metadata and local hostnames are refused (metadata.google.internal, metadata, instance-data, localhost,
*.internal,*.local, …). - All resolved addresses are checked, and the fetch is refused if any one is loopback (127/8, ::1), RFC1918, CGNAT 100.64/10, link-local or metadata (169.254/16, fe80::/10), ULA fc00::/7, 0.0.0.0/8, multicast, reserved, TEST-NETs, or the unspecified address. The same check applies to v4 addresses embedded in IPv6: IPv4-mapped (::ffff:a.b.c.d), IPv4-compatible, NAT64 64:ff9b::/96 and 6to4. Odd IPv4 literals (hex, octal, integer, shortened) are normalised by the URL parser first.
- Redirects are followed manually and every hop is re-checked, up to 5 hops.
- Each request connects to the exact IP that passed the check (a custom
lookupon node:http/https), so a DNS answer that changes after the check cannot redirect the request. allowHostsis an exact host:port opt-in for tests only. It isn't exposed through MCP, the CLI or environment variables.- 198.18.0.0/15 is deliberately NOT blocked: some sandboxes, including this build box, answer DNS with fake IPs in that range.
- Residual risks: when traffic goes through an outbound proxy or a fake-IP DNS layer, the guard validates the address the proxy hands out, not the real upstream one. Plain-http requests send Host/path to the validated IP only, but HTTPS certificate checks still use the hostname. IPv6 zone IDs are stripped.
- Covered: http(s) only; no credentials in URLs. Metadata and local hostnames are refused (metadata.google.internal, metadata, instance-data, localhost,
- Tests: new
tests/ssrf.test.mjswith 10 tests. The duplicate headline test is merged into one, and egress fixtures use user@example.com. - README: "Measured results" now gives counts (1,634 of 2,242 and 797 of 2,242 within the 5,120-sample set) and names the 8 legitimate-use categories (8 × 40). One "Honest scope" line describes the SSRF guard.
- In-app test (2026-10-17, Grok Bot connector, Node v20.19.2): scan, safe_fetch, egress_check, mcp_pin and mcp_verify (unchanged and tampered) all PASS. egress_check ran on fake data after one approved Auto-review block; the metadata-URL safe_fetch was blocked by Auto-review and is covered by
tests/ssrf.test.mjs. Details: docs/IN-APP-TEST.md.
0.2.2 (2026-10-17)
- Headline is now the same text in the README, the
ttp_catalog()scope string and both plugin.json descriptions: "Deterministic, zero-dependency coverage of the latest documented (2025–2026) prompt-injection concealment and exfiltration TTPs. Built to pair with an ML classifier." The previous false-alarm claim is removed everywhere, and a test checks it stays out. - README: new "Measured results" section (held-out benchmark, legitimate-use samples, and what
reviewmeans on real pages). - Version 0.2.2 in all manifests and the server.
0.2.1 (2026-10-17)
- README now says that every TTP finding carries a citation, and that operational notes (VB-I01 size cap, VB-I02 unsupported input) do not. VB-I01 findings now have
citation: null. - CATALOG gains VB-I01 and VB-I02 with
kind: "operational". Every entry now has akind(ttp,operationalordesign). The README TTP table is generated from the catalog and a test checks they match exactly (id, kind, name, default severity, order). - In
tests/ttp.test.mjs, every benign look-alike also checks that there is no finding of that TTP at all, or that its maximum severity is none/low. The medium-and-abovehas()helper is unchanged. - The README headline and the
ttp_catalog()scope string are replaced with the exact new positioning line. Out-of-scope items move to a newout_of_scopefield inttp_catalog(). The word that previously led the positioning line is removed everywhere. - Version 0.2.1 in
plugin.json,.cursor-plugin/plugin.json,package.jsonand the server.
0.2.0 (2026-10-17)
- CLI: works through a symlinked entry point (
realpathSyncon both sides). It never exits 0 with empty output: errors print{"error"}JSON with exit 2 (usage/input) or 1 (internal). A missing file gives exit 2 with no stack trace. Input over the size cap returnstruncated: trueplus a VB-I01 finding. Binary or non-UTF-8 input returns verdictunsupported(VB-I02). - VB-T08 (key signal) reworked: substantive text present only in a hidden or decoded view is
review. It escalates tosuspiciouswhen agent-directed, judged by general grammar rules: an imperative addressed to an AI, about "the user", or carrying a URL/email target, or a second-person directive in CSS-hidden text. The phrase list is now only an extra signal. Descriptive alt/title/aria/meta text, machine data in attributes, and developer comments stay clean.<noscript>andaria-hiddencontent counts as visible. - New VB-T09, decode-and-follow wrappers: high, verdict
suspicious. New skilldecode-and-follow. - VB-T02: zero-width characters inside words above a density threshold are high. Stray ones are low. Emoji ZWJ and Arabic/Persian/Indic joiners stay exempt.
- VB-T01: TAG characters are high unless they form a valid emoji tag sequence (any emoji base + 1–32 tag digits/lowercase letters + CANCEL TAG).
- VB-T03: overrides and embeddings (LRO/RLO/LRE/RLE/PDF) are high. RLM/LRM and isolates are low in genuine RTL text and medium otherwise.
- VB-T04: any word mixing scripts is high (CJK scripts grouped; Common/Inherited ignored). Whole words in one non-Latin script are clean.
- Decoding: URL and entity decoding now targets encoded spans only, and HTML is decoded from text content rather than markup. This removes whole-page false positives found on real sites.
- Verdicts: low findings no longer raise the verdict to
review. egress_check: theenforcemode is renamedstrict(old name accepted), and the result fieldenforcementis renamedhandling. The README no longer uses "full protection" or "enforced" wording.- README: correct test command,
npm test, a default-severity column in the TTP table, and split/multi-message payloads stated as out of scope. - New
package.json: private, zero dependencies,npm test. - Tests: a new
tests/v02.test.mjswith an attack and a benign look-alike per change, including a symlink test. Thehas()helper intests/ttp.test.mjsnow counts only medium-and-above findings as "flagged", because low notes are informational.
0.1.0 (2026-10-16)
- First release. Tools:
safe_fetch,reveal,scan,egress_check,mcp_pin,mcp_verify,ttp_catalog. - 12 skills:
step-0-untrusted-contentplus one per TTP family and a design-basis skill. - 21 catalog entries; every citation verified by fetch on 2026-10-16.
- Tests: per-TTP attack/benign pairs, 12 blocked + 12 allowed egress flows, MCP pin/verify, safe_fetch fencing, CLI.
- Default egress mode: shadow (log-only).