Changelog¶
All notable changes to Bulwark are documented here. The format is based on Keep a Changelog, and the project follows Semantic Versioning.
Unreleased¶
Changed¶
- PyPI distributions moved into the
bulwark-namespace. Every unnamespaced name the suite wanted is already taken on PyPI by an unrelated project —airlockis a 2015 Google App Engine OAuth wrapper,wardena 2012 monitoring shim,manifestan LLM code-execution tool, andbulwarka pandas testing library. The documented install commands therefore installed someone else's package.
| Was (someone else's) | Now |
|---|---|
pip install airlock |
pip install bulwark-airlock |
pip install warden |
pip install bulwark-warden |
pip install manifest |
pip install bulwark-manifest |
pip install bulwark |
pip install bulwark-suite |
Nothing about usage changes. CLI commands are still airlock, warden, manifest,
and bulwark; import names are still import airlock, from warden.scanner import ….
Only the distribution name moves. A packaging invariant test now enforces the namespace,
so the suite cannot be republished under a squatted name by accident.
- CITATION.cff added, so GitHub renders a "Cite this repository" button and a Zenodo
release archive carries real authorship instead of a repository slug.
Added¶
- Validation harnesses for Warden and Manifest (
packages/<tool>/scripts/study.py), closing a gap where every published Bulwark measurement came from Airlock. Warden gets four studies — cross-framework invariance, lexicon robustness under obfuscation, false positives on benign assemblies, and--recommendefficacy. Manifest gets four — discovery recall against hand-written ground truth, CycloneDX/SPDX conformance, governance control coverage, and risk-bridge fidelity. Both are deterministic and offline; results are written todocs/VALIDATION.mdper tool and folded intodocs/EMPIRICAL_VALIDATION.md, which is now organized by layer (part / assembly / system) rather than by tool. - 15 tests pinning every published validation figure, so a rule or lexicon change that moves a number fails CI instead of quietly making the docs wrong.
Fixed¶
camelCasetool names were entirely unclassifiable, so any assembly using them — most of the TypeScript MCP ecosystem — silently lost A2, Warden's flagship attacker-triggerable exfiltration finding, while still reporting a clean-looking MEDIUM verdict._tool_text()appended an underscore/hyphen-normalized copy so\bbrowse\bmatchedbrowse_web, but never split case transitions, sobrowseWebmatched nothing. It now appends a camel-split copy as well;snake_caseandcamelCaseclassify identically. Found by the new lexicon-robustness study.transfer/wireno longer implyFINANCIALwithout a money noun. "Transfer the meaning of a phrase into another language" was classified as a financial operation, and becauseFINANCIALis high-impact it also raised a spurious A3 missing-gate finding on a plain translation tool. ("Transfer learning" tripped it too.)requestno longer impliesNET_OUTwithout network context. A bare\brequest\bmatched "the user's request" — ordinary English long before it is an HTTP verb, and the single noisiest source of spurious egress capability on benign agents.
Together the two false-positive fixes halve the benign agents carrying a HIGH+ finding (5/7 → 3/7) with no loss of true positives, and the camelCase fix raises A2 recovery under obfuscation from 3/7 to 4/7.
[0.2.0] — 2026-08-07¶
Productionisation release: the suite becomes installable software rather than a
repository. Five packages move to a src/ layout, gain structured logging, a
documentation site, runnable examples, and supply-chain hardening of the project
itself — alongside a large correctness pass.
Upgrading from 0.1.0: no API breaks. airlock.toml now actually takes effect, which
is a behaviour change if you had a config file that was previously being ignored — check
that its fail_on and suppress_rules are what you intend before upgrading a gate.
Added¶
src/layout for all five packages (packages/<name>/src/<name>/). Imports now resolve to the installed wheel rather than the working directory, which is what surfaces a data file missing from a distribution. Verified: all 18 rule packs and the HTML template ship inside the wheels, andpip install airlockworks standalone from a wheel with no workspace present.- Structured logging (
bulwark_core.logging), with-v/-vvon every CLI. Diagnostics go to stderr only, so--format json > out.jsonstays valid at any verbosity; as a library, Bulwark emits nothing until an application callsconfigure(). Artifact-derived text is single-lined and truncated before it can reach a log record, so a hostile description cannot forge log lines. examples/— five runnable scripts covering the Python API for all three tools, custom rule packs, and the least-privilege recommender. Exercised by CI, so they cannot rot.- Documentation site (MkDocs Material) with installation, quick start, guides, CLI
and Python API reference, taxonomy, configuration, architecture, and threat model.
Builds under
--strict; published to GitHub Pages. load_rules(extra_roots=...)in all three tools — the documented "layer your own rule packs" story was previously not expressible from Python, only by replacing the built-in roots entirely.airlock rules debug <kind> <target>— dumps the signal bundle for a target. "Is the evidence there?" is the first question when a rule stops firing, and it previously required a fifteen-line script.- CodeQL analysis and Dependabot grouped update policy.
- Suite-wide invariant tests: version agreement between
pyproject.tomland__version__,src/layout, and wheel-content declarations.
Fixed¶
- Snake_case tool names were not classified.
_is a regex word character, so\bbrowse\bnever matchedbrowse_weband\bshell\bnever matchedrun_shell— meaning Warden's flagship CRITICAL finding (an attacker-triggerable exfiltration flow) silently did not fire on the naming convention the ecosystem actually uses. Tool text is now matched against a de-snaked copy as well as the raw form. - The CRITICAL injectable-exfiltration flow ignored gates on the sink, while the sibling injectable-action check credited them. A declared approval gate breaks the automated chain, so the two escalations now agree — and the recommender's advice measurably clears the finding.
\bopen\bclassified "open a support ticket" as filesystem read. Third instance of the verb-without-a-domain-noun pattern, after\bformat\band\bsandbox\b; now requires a filesystem noun.
Changed¶
- All five packages are at 0.2.0 and release in lockstep from one tag.
check.py,noxfile.py, and CI type-checksrc/<module>paths.
[0.1.1] — 2026-08-02¶
Fixed¶
- Configuration files were silently ignored.
airlock.tomlwas read, parsed, and then discarded: the merge helper recursed into nested tables but never assigned a scalar, sofail_on,output_format,strict_allowlist,suppress_rules/suppress_paths, and[ai].enabledall fell back to defaults with no error. Settings now layer through pydantic-settings'settings_customise_sourceswith a TOML source ordered below the environment, so files work and env still wins. Warden and Manifest gain the same layering (warden.toml,manifest.toml), which they previously lacked entirely. - Manifest's Warden bridge skipped agent assemblies.
--scan-riskconsidered onlymcp-servercomponents, so an assembly discovered from a CrewAI crew, an OpenAI Assistants config, or an agent manifest was inventoried and then never audited.agentcomponents are now bridged too. - Post-processing dropped
scoreandmeta. Waivers and baselines rebuilt the result from an explicit field list, losing Warden's agency score and Manifest's entire AIBOM — which would have broken--format cyclonedxafter any suppression. Now amodel_copy, so future fields survive. --strictdid not apply to pickles embedded in numpy object arrays, making allowlist coverage depend on the container rather than the payload.serialized.pynow sharespickle_scan's emitter.- MCP
timeoutwas accepted and never used, so a server that connected and never answeredinitializehung the scan indefinitely. Enumeration is now bounded byLimits.connect_timeout_s(AIRLOCK_LIMIT_CONNECT_TIMEOUT), and a timeout is reported as a connect error like any other. - Directory walks followed symlinks and had no file cap in both Airlock's model resolver and
Manifest's discovery context — a target containing a link to
/made a scan traverse the whole filesystem. Both now use a sharedwalk_fileshelper with resolved-path containment andLimits.max_files(AIRLOCK_LIMIT_MAX_FILES). - Warden lexicon false positives.
format_responseclassified asDESTRUCTIVE(and therefore high-impact, producing a spurious missing-gate finding and +10 agency score); "runs in a sandbox" classified asCODE_EXEC. Both patterns now require corroborating context. - A5 egress allow-listing recognised only the literal string
allowlist, so a genuine scope such ashttps://api.example.com/**was reported as unrestricted — a false positive on exactly the configuration the finding asks for. Concrete hosts, URL prefixes, and CIDRs now count. - BOM drift ignored
provenance.source/author, so a model switching publisher under the same name was reported as unchanged. AIBOM.addmerged more shallowly than documented — provenance, licence, and findings from a later discoverer were discarded. The merge is now field-wise.noxfile.pylintnever changed directory, running ruff from the repo root five times instead of once per package, contradicting its own docstring.
Added¶
airlock rules debug <kind> <target>— dump the signal bundle a scan produces without applying rules. The first question when a rule stops firing is whether the evidence exists; this answers it.- Signal-name validation in
rules lint(all three tools). A mistypedmatch.signalwas the one rule error that failed silently — no rule matched, nothing errored, the detection was simply gone. Each tool now declaresKNOWN_SIGNALSand lint rejects a rule referencing anything outside it. hf:org/name@revisionpinning — pin a scan to an immutable Hub commit, so a result is reproducible and attributable to specific bytes rather than to a mutable branch.- Hugging Face purls — models and datasets emit
pkg:huggingface/org/name@revisionin CycloneDX, so they are identifiable across tools and advisory feeds, not just libraries. - Typed SPDX relationships — the AIBOM's own verbs (
trained-on,contains,variant-of, …) now map to real SPDX relationship types instead of flattening toDEPENDS_ON. - Rug-pull detection reports added and removed tools, not only changed ones. A tool appearing after approval is the classic rug-pull shape.
- Remote MCP auth detection —
auth.missingnow checks for credentials actually supplied (URL userinfo, an auth query parameter, or anMCP_*token env var) instead of being a synonym for "is remote". - Three-state governance status —
ok/advisory/gap, driven by the worst severity mapped to a control. A single LOW advisory no longer marks a NIST function or an EU AI Act article as a gap. - Risk register
ownerandstatuscolumns (emitted as a template), so the output is trackable rather than a list of complaints. - AGPL is classified separately from GPL — network copyleft is the highest-consequence licence
term for a hosted product and is now surfaced as
restricted. - Source→sink pairings are capped and rolled up in both Airlock's P5 and Warden's A2, so a large tool-set produces a readable report instead of hundreds of near-duplicates.
bulwark-coretest suite — 31 tests covering severity ordering, finding identity, post-processing field preservation, the zip-slip guard, and the bounded walk, plus architectural invariant tests asserting that core imports nothing from the suite and never executes its input.- Regression suites for the configuration layering, the Warden bridge, lexicon classification, BOM
merging and drift, and a property test that
--recommendactually lowers the agency score. bulwarkmeta-CLI — one front door over all three tools plusbulwark scan(full pipeline).- Airlock — format/extension-confusion detector (M6) — sniffs magic bytes and flags a pickle disguised under a safe extension (the picklescan CVE-2025-10155 bypass class), then scans it anyway.
- Airlock — allowlist mode (
--strict, M3) — Fickling-style: flags pickle imports from modules outside the ML allowlist, catching novel callables a denylist misses. - Warden — policy profiles (
--profile strict|balanced|permissive). - Warden — attacker-triggerable toxic flows — escalates A2 to CRITICAL when untrusted input (browse / inbound) can drive a read-secret-then-exfiltrate kill chain (indirect prompt injection), plus an injectable-high-impact-action variant.
- Manifest — SPDX 2.3 output, EU AI Act mapping, notebook discoverer,
manifest diff(BOM drift). - Manifest — VEX output (
--format vex) — CycloneDX VEX seeded from detected vulnerable deps. - Manifest — agent discovery + CycloneDX Agent BOM —
agentcomponents (autonomy + wired tools) surfaced asbulwark:agent:*properties, aligned with the emerging CycloneDX Agent BOM. - Empirical validation — a real-model corpus study, a 14-payload adversarial suite, and a head-to-head benchmark against picklescan / modelscan / fickling.
- Project docs —
USAGE.md,LANDSCAPE.md,EMPIRICAL_VALIDATION.md,DATASETS_AND_TESTING.md. - OSS hygiene — root
LICENSE,SECURITY.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md, issue/PR templates, and a matrixed CI across all five packages.
Security (self-hardening — the scanner ingests hostile input)¶
- HTML report XSS fixed. The report template (
report.html.j2) fell throughselect_autoescape(["html"])(its.j2suffix), so attacker-controlled strings from a scanned artifact (finding evidence, file paths, MCP tool descriptions) rendered unescaped — opening a report on a hostile artifact could execute injected<script>/onerror. Autoescape is now forced on. - Rule-feed zip extraction hardened.
rule_feed._extract_zipreplaced a weak"/../"substring check with a resolved-path containment guard (defeats../and absolute/drive zip-slip), and added per-member, total-uncompressed, and member-count caps (decompression-bomb / member-flood guards); members are streamed, neverzf.extract-ed. - Bounded reads. Whole-file
read_bytes()on artifacts (numpy.npy, GGUF magic check, compressed pickles, Keras zip members) replaced with capped reads viabulwark_core.limits.read_bounded, so a crafted multi-GB file can't OOM the scanner. - ReDoS blast-radius bound. Rule-engine regexes now run against a length-capped input
(
MAX_MATCH_INPUT) and use a compiled-pattern cache, limiting catastrophic backtracking from a hostile field or an untrusted community rule pack. - Symlink containment and a file cap on directory walks. Both Airlock's local model resolver and
Manifest's discovery context used
rglob, which follows symbolic links, with no file cap — so a hostile artifact directory containing a link to/turned a scan into a filesystem traversal. Both now sharebulwark_core.limits.walk_files, which resolves each entry and requires it to remain under the scan root (the same containment check the rule feed uses against zip-slip) and stops atLimits.max_files. - A time bound on MCP enumeration. Time was the one resource dimension left unbounded: a server
that accepted a connection and never responded hung the scan forever. Enumeration now runs under
anyio.fail_after(Limits.connect_timeout_s). - Prompt-injection spotlighting in the AI layer. Content sent to a provider comes from the artifact
being scanned and is therefore attacker-controlled. It is now fenced in
<untrusted_content>markers, forged markers are stripped, and the system prompt states that the fence marks data — text inside it demanding a particular verdict is treated as evidence of manipulation. The layer's blast radius was already bounded (AI can add findings, never remove or downgrade one); this removes the cheapest version of the attack. - Honest AI budget accounting. The executive-summary and model-card calls previously ran outside
the
max_findings_to_enrichcounter, so the real ceiling was the documented cap plus two. All provider calls now share one counter. - A stdio-scanning warning.
airlock scan mcpwith a command target now states that enumerating a stdio server starts it — Airlock never invokes a tool, but the server's own startup code runs with your privileges — and points at the fully staticairlock scan toolspecpath for CI. - Versioned OWASP citations. Reference strings were a mix of 2023 and 2025 LLM Top 10 numbering
(
LLM05for supply chain is 2023;LLM02/LLM06were 2025). All 76 sites now carry an explicit edition —OWASP:LLM03:2025for supply chain, and so on — so a future renumbering cannot silently invalidate a citation. Placeholder references (best-practice,license-compliance, …) were replaced with real, citable sources.
Changed (CI/CD — applying the project's own advice to itself)¶
- Pinned the CI toolchain.
ruff,mypy,pytest, andpytest-covwere installed unpinned, so a third-party release could break the build with no change on our side — precisely the reproducibility gap Manifest's own B1 finding reports on other people's projects. Extras are installed without a silent|| fallback, since a degraded install turns real failures into skipped tests. - Enforced release ordering.
bulwark-corenow publishes in its own job with the tools gated onneeds: core. The matrix previously ran in parallel with a comment claiming core went first, which could leave PyPI briefly holding anairlockdepending on abulwark-corethat did not exist. - Signed release artifacts with Sigstore, using the same keyless OIDC identity Trusted Publishing already establishes. A project arguing for model signing should not ship unsigned wheels.
- Bulwark now publishes its own AI-BOM (CycloneDX, SPDX, and a governance report) as a release artifact.
Fixed¶
- Benchmark harness no longer copies scan artifacts into the corpus directory (an earlier version silently inflated repeated runs); it now scans each file in place.
0.1.0 — Airlock · Warden · Manifest · bulwark-core¶
Added¶
bulwark-core— shared spine:Finding/Severity/ScanResult, YAML rule engine, signal IR, report renderers (terminal / JSON / HTML / SARIF), optional AI provider layer, resource limits.- Airlock — static scanner for models (M1–M7), MCP servers (P1–P9), and tool-specs; formats: pickle, safetensors, GGUF, ONNX, Keras, numpy, TensorFlow SavedModel, Flax, PMML; hardened parsers; SARIF/CI; optional AI enrichment; corpus study harness; GitHub Action.
- Warden — least-privilege auditor: AgentSpec IR, capability graph, toxic-combination detection
(A1–A10), agency score, framework importers (manifest / MCP config / OpenAI Assistants / LangChain /
CrewAI), least-privilege recommendation, and
--scan-parts(runs Airlock on wired MCP servers). - Manifest — AI-BOM generator: discoverers, provenance/license resolution, OSV vulns, CycloneDX 1.5 output, Airlock/Warden risk bridges (B1–B9), NIST AI RMF governance mapping.