Skip to content

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=off disables it). Fields: ts, tool, verdict, decision, mode, max_severity, ttps, sink, host (registrable domain only), input_sha256, input_chars, duration_ms, version, plus egress reasons, mcp_changes and error. 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. report calls 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, CSP default-src 'none'). --cli serve-dashboard [--port 8790] binds 127.0.0.1 only, regenerates on each request, refuses a non-loopback bind unless --bind is given explicitly (and then warns), and refuses non-loopback Host headers. Also --cli report.
  • Honest wording. README "Honest scope" and the egress handling text 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, handling says nothing is recorded. step-0-untrusted-content and why-deterministic skills 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 new watch-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-started skill: 8 tools, VEILBREAKER_MODE and VEILBREAKER_LOG, run report, open the dashboard. The guard template carries the updated skills verbatim.
  • mcp.json passes VEILBREAKER_LOG through.
  • Tests: new tests/watch.test.mjs (log write/rotate/off, write failure as a note, no planted secret or email in the log, report math on a fixture log, dashboard has no scripts/src/href/URLs and contains the totals, escaping of hostile log values, serve-dashboard loopback-only) and tests/watch-template.test.mjs. Every test file imports tests/helpers/isolate-log.mjs, so tests never touch ~/.veilbreaker.
  • Folded in from the unreleased 0.2.4 entry:
    • Fix: safe_fetch now fails loudly on encoded responses. A response with a Content-Encoding other than identity (gzip, deflate, br, or a list) returns verdict unsupported, 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 verdict unsupported with 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_fetch does 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.txt is untracked and gitignored; the local file is kept.
  • Fix (found in the in-app run): an egress deny was logged with max_severity: "none". Every egress deny is now logged with at least high (critical for SECRET_IN_ARGS / PRIVATE_DATA_IN_ARGS), for any reason code; an allow stays none. New test in tests/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 report is missing, add the connector under a new name, or run --cli report on the same log.
  • Fix (R1): egress handling is surface-aware. MCP calls say a deny is recorded to the event log; --cli egress says "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's ipBlocked).
  • Fix (R1): --cli dashboard --out creates 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 report was 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-app report ran 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.json and 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-started skill: 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 in tools/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 lookup on node:http/https), so a DNS answer that changes after the check cannot redirect the request.
    • allowHosts is 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.
  • Tests: new tests/ssrf.test.mjs with 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 review means 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 a kind (ttp, operational or design). 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-above has() 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 new out_of_scope field in ttp_catalog(). The word that previously led the positioning line is removed everywhere.
  • Version 0.2.1 in plugin.json, .cursor-plugin/plugin.json, package.json and the server.

0.2.0 (2026-10-17)

  • CLI: works through a symlinked entry point (realpathSync on 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 returns truncated: true plus a VB-I01 finding. Binary or non-UTF-8 input returns verdict unsupported (VB-I02).
  • VB-T08 (key signal) reworked: substantive text present only in a hidden or decoded view is review. It escalates to suspicious when 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> and aria-hidden content counts as visible.
  • New VB-T09, decode-and-follow wrappers: high, verdict suspicious. New skill decode-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: the enforce mode is renamed strict (old name accepted), and the result field enforcement is renamed handling. 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.mjs with an attack and a benign look-alike per change, including a symlink test. The has() helper in tests/ttp.test.mjs now 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-content plus 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).