Release notes
Rule ids are public API. CI config, suppressions, and agent rules files reference them. A rename uses a deprecation cycle and appears under Changed.
Atom feed / GitHub releases / the rule catalogue
1.3.0 2026-09-01
1.2 answered which of these findings matters. This release answers the question a developer actually asks, dozens of times an hour: which of these did I just do? (ADR 0032).
No new rules, no new frameworks, no new agent hosts, and one new command. The catalogue is still 25 rules, the matrix is still 16 frameworks and 7 hosts, and neither was the binding constraint. A finding you introduced thirty seconds ago and a finding you inherited from a repository's first year are not the same object, and nothing in the tool could tell them apart.
Added
owlwarden turn- scans the files this turn touched, scans the same files at the base commit, and diffs the two. Findings areintroduced,carried, orfixed.
Carried findings never fail a turn, at any threshold. There is no flag that changes it and no code path that could grow one: the gate is applied to the introduced set before it is consulted. A control that blocks on debt the turn did not create is a control that gets removed on the second day, and everything it would have caught goes with it.
Everything introduced is reported whether or not it blocks - a turn that adds a medium under a high gate reads clean at high - 1 introduced below the bar, shown anyway. blocking is a separate field from counts.introduced precisely so the verdict can never print "nothing introduced" over something the turn introduced.
◉ᴥ◉ turn / 1 file / since HEAD a8a6b93 / 0.16s
✘ blocked - 1 introduced at or above high
1 carried (already at HEAD a8a6b93, not this turn's)
--base <REF>, --record, --hook <HOST>, --fail-on, --fail-on-exposure, --no-surface, --format pretty|json.
turn --hook claude-code | cursor | generic- the verdict in a host's own hook shape, through the same three adaptersgateuses rather than a fourth encoder. The reason the model receives names what is new and says the rest were already there: an agent told "there are eighteen findings" triages a backlog nobody asked it to touch.
init --claude-code and init --cursor now wire the Stop hook to turn --hook <host> --record instead of gate --host <host> --since HEAD. The per-edit and pre-command hooks are unchanged - gate still owns those.
turn --record- one JSON line per turn in.owlwarden/turns.jsonl, bounded at the last 200. Every field but the timestamp and the stopwatch is derived from the two reports and the base, so two runs over an unchanged tree produce identical records. Asserted on both sides of the language boundary.
owlwarden.turnin the SDK:turnReportSchemaand its types, held tofixtures/golden/turn.jsonby the same cross-language contract as the scan report. Neither declaration can move alone.
owlwarden_core::baseline::fingerprints()- the correct way to fingerprint more than one finding at a time, now that a second caller needs the occurrence walk.owlwarden_core::report::fails_gate()- the gate predicate, extracted soscanandturncannot drift into two definitions of "bad enough to stop".
Changed
owlwarden --helpis 41 lines, down from 161. It lists four commands -turn,scan,vet,init- and the flags a first run needs.owlwarden help --allprints everything, and nothing was removed. The cost of the wall landed on exactly the wrong person: someone who has runnpx owlwardenonce and is looking for the command that answers their question.
--asciinow means ASCII. The summary line every reader sees carried a literal/in both modes, so a flag whose whole job is a claim about the character set did not keep it.Glyphsgainsseparatoranddash.
seal::command::current_surfaceandseal::ChangeKind::word()are public: the turn verdict reports the agent execution surface without taking or comparing a seal, because an unsealed repository still has hooks and a reader deciding whether to seal wants to know how many.
Security
- wasmtime floored at 36.0.14, up from 1.2's 36.0.13, which picked up RUSTSEC-2026-0269 / GHSA-vqjp-4c8c-hfgg - a filesystem sandbox escape through trailing slashes in paths and symlinks - in the interval between the two releases. The plugin host is the one crate in this workspace whose entire job is containment, so an open sandbox-escape advisory in it is the failure
AGENTS.mdnames: the tool must not become the vulnerability it hunts.
Nothing owlwarden ships was exploitable through it without a loaded plugin, and plugins are opt-in, source-only, and refused under --ci without --allow-plugins. It is floored anyway. cargo deny check found it on this branch, which is the job that check exists to do - the 40-test sandbox-escape suite passes on the new version unchanged.
Known issues
verifystill compares findings byrule@location, so a patch that shifts lines can report one finding resolved and one introduced.turnuses the baseline fingerprint and does not have this bug. Named here rather than fixed quietly:verifycompares two states of one file seconds apart, where the case is rare./benchmark/still publishes nothing. The harness, the corpus discipline, and the threshold gate shipped in 1.2 and are tested; labelling real repositories is judgement work that has not been done, and a number computed from our own fixtures would be worse than no number.
1.2.0 2026-08-30
1.1 answered what is here. This release answers the three questions a person asks immediately afterwards: what changed, what is reachable, and how often are we wrong? (ADR 0027, ADR 0028, ADR 0029, ADR 0030, ADR 0031).
No new rules. The existing 25 became substantially more useful without one.
Added
exposure, a third axis on every application finding -internet,authenticated,internal,unknown- computed by the engine from route resolution and auth-gate recognition per framework.
It fails loud: authenticated requires a positively identified gate, and absence of evidence yields internet. A module that does not resolve is not a gate, a name that does not read as one is not a gate, a session call whose result is never checked is not a gate, and a config.matcher we could not parse covers nothing rather than everything. Everywhere else in owlwarden uncertainty resolves downward; here it resolves upward, because a finding wrongly marked as behind auth is a finding somebody deprioritises.
Report order becomes exposure → severity → confidence, --fail-on-exposure composes with --fail-on as an OR, the Markdown reporter groups by exposure, and the summary line states the distribution. Exposure never raises severity.
owlwarden seal-.owlwarden/surface.lock, a committed record of every file the agent loads out of the working tree by semantic digest, with hooks, MCP servers and their pins, the permission set, marketplace sources, and instruction files extracted rather than merely hashed.settings.json changedis a message people re-run past; aSessionStarthook was added is not.
--verify, --diff, and repeatable --accept <fp> --reason "…". Reformatting a config does not break the seal; changing one character of a hook command does. Sealing is never unattended, and refuses to write while an unaccepted high finding sits on the surface.
owlwarden effective- the resolved agent configuration with provenance per key.git config --show-originfor your agent.
runtime_scope: shadowed- a project key a higher configuration tier overrides, capped atpossibleand reported rather than suppressed. Only under--include-user-config; without it nothing outside the project root is opened and behaviour is identical to 1.1.
- Runtime as an overlay. Findings carry the runtime their file runs on, resolved per file, and five rules carry deltas where their base fix would not execute. The build asserts that the un-deltaed fix does not name an absent API, so the absence of a delta is a positive claim.
- Four frameworks, taking the matrix to sixteen: SvelteKit, TanStack Start, SolidStart, Elysia - each with the full square fixture matrix and 13 remediation cells.
owlwarden bench- precision, recall, per-rule false positives with their locations, andauthenticatedprecision published separately. The corpus discipline is enforced on load. The corpus is empty and the tooling says so;/benchmark/is not published until real repositories are labelled.
owlwarden coverage [PATH]adds the exposure distribution, the unclassified rate, and the seal state.
- The Action gains
seal,require-signed-seal, andfail-on-exposure. The surface diff goes to the job summary and aseal-diffoutput, silent when the surface has not moved.
Changed
open-redirectnow recognises status-firstredirect(302, to), status-lastResponse.redirect(to, 302), andset.headers.Location = to. It was blind to all three, which made it blind to SvelteKit and Elysia redirects.- Sails gained route resolution; SolidStart's profile now names
src/middleware.ts, where its headers are actually set. - Report JSON gains
exposure,exposureEvidence,runtime,runtimeSource, andexposureSummary;target.runtimestates the runtime and whether it was detected or defaulted. All additive - a 1.1 consumer is unaffected.
Security
- Gate evidence is clamped at one choke point. A module specifier is a string literal that can hold a newline or a bidirectional override, it resolves on any Unix filesystem, and it previously reached a terminal, a SARIF result, and a pull-request comment unclamped.
- 49 adversarial tests across the seal, tier resolution, the exposure classifier, and the benchmark.
SECURITY.mdcarries the seal's limits - a detection and review control, not a containment one - and the exposure axis's loud-direction rule.
1.1.0 2026-08-26
A second scan surface and a control that always runs (ADR 0025, ADR 0026).
owlwarden answered one question: is the web application in this repository written safely? This release adds the second question the same repository now raises: is the coding agent that works in it being told to do something hostile?
Added
- Eleven rules on a new
agentWorkspacesurface, reading the agent and editor configuration a lockfile does not record. All cap atlikely, all carry aruntimeScope, all map to CWE with OWASP ASI 2026 as a secondary reference:agent-hook-autoexec,agent-hook-untrusted-command,agent-config-loader-script,agent-config-env-redirect,agent-config-secret-reachable,agent-permission-wildcard,agent-mcp-unpinned-remote,agent-marketplace-untrusted,agent-instructions-hidden-text,agent-instructions-directive, andinstall-lifecycle-script(which iswebApp, because it readspackage.json, and is the one with an OWASP Top 10 mapping - A08). Surface, and the remediation matrix generalised over it. AwebApprule owes twelve framework fixes; anagentWorkspacerule owes seven agent host fixes; neither is checked against the other's list, and a missing cell still fails the build.owlwarden vet <path>- the same engine with a fixed posture for a repository you did not write: agent rules only, offline, no plugins, and the target's own config, baseline, and suppressions counted rather than honoured.owlwarden gate --host <claude-code|cursor|generic>- the hook entry point. Reads the host's event on stdin, scans what it names, and returns a verdict in the host's own shape. Fails closed before a command executes and open after an edit, because those have different consequences.owlwarden verify --patch <file>- applies a patch to a scratch copy, re-scans, and exits 0 only if the finding is gone and nothing new appeared at or above the threshold.--since <ref>/--staged/--paths- diff-scoped scanning. Project rules declare their own inputs, so apackage.json-only commit still fires the dependency rules. The scope is stated in every output format.--format agent- the report on a token budget (default ~1500), with explicit truncation. Omitswhy, which is written for a human.owlwarden init --claude-code | --cursor | --generic- wires the gate into a host's lifecycle events.agent-surfacepreset,runtimeScopein every format, an ASI coverage table inowlwarden coverageandRULES.md, and the agent path allowlist in the coverage output so a reader can tell whether their host is in scope.- A generated documentation site: 211 pages, including one per (rule, framework) and (rule, agent host) cell that has a verified example.
Changed
honorSuppressions: booleanbecame a three-stateSuppressionPolicy. The gate needs "honour what the team committed, refuse what appeared during this session", and a boolean had nowhere to put it.Report.targetgainedconfigFilesScannedanddiffScope. Avetreporting "0 files" over fourteen findings was describing the wrong number, and a diff-scoped clean result must never render as a clean repository.- The npm
description,keywords,homepage,funding,license, andpublishConfig.provenancewere rewritten for the registry's own ranking inputs. The site URL now lives in one file,site.url. owlwarden initwith no flags is unchanged; the host flags are additive.core::agent_textis nowcore::untrusted_text. It was named for the reader it was written for; the terminal turned out to be a fourth reader with the same requirement, and a module namedagent_textsanitising a human's terminal is a name that lies to the next person.- The GitHub Action gained
presetandsinceinputs. Withoutpresetthe agent surface was unreachable from CI at all;sincewas already in the README's snippet and had never existed.
Fixed
- A
--sincethat could not resolve widened the scan instead of failing it. It printed a note and scanned the whole project. On a shallow CI clone - which is whatactions/checkoutgives you by default - that is the difference between three new findings and a red job full of debt the change did not introduce.scannow exits2and namesfetch-depth: 0;gatestill degrades, because a hook that bricks a session over a git hiccup gets uninstalled. A range (main..HEAD) is refused rather than passed through: it would widen the scope a narrowing flag was asked to cut. - A
--sincevalue beginning with-reached git as an option. The ref sits in front ofgit diff's trailing--, which separates paths from revisions and not options from anything, so--since --output=<file>was argv git parsed - andgit diff --output=writes where it is pointed. The invocation passes--end-of-optionsnow, and the value is validated before it gets there. --report-suppressionsprinted repository text straight to the terminal. The reason on a suppression is a comment somebody wrote, and the listing exists so a reviewer can audit what a tree has silenced. A reason containing\x1b[2K\x1b[1A\x1b[2Kclears its own line, moves up, and clears the entry above it - deleting a line from the audit, from inside the audit. Bidi overrides made a reason render as its opposite. Both output paths now render the reason, the path, and the rule id as data.- The two sanitisers had drifted, in both directions.
agent-safety.tsstripped the Unicode Tags block (U+E0000-E007F, which mirrors ASCII into zero-width code points, and is the channel current prompt-injection work actually uses) andcoredid not, because they are categoryCfandchar::is_controlonly coversCc. Going the other way,agent-safety.tsreplaced[INST]with the label"[INST]"- the marker spelled exactly as it arrived - so that substitution ran on every MCP payload and changed nothing.fixtures/untrusted-text-vectors.jsonnow owns the list and both sides are tested against it. - A plugin could supply the key that vouched for it. Trust roots were read from the plugin's own directory and from that directory's parent - both inside the artifact being verified. Generate a key, sign the artifact, ship the public half beside the signature, and
--require-signed-pluginsreportedverified. It refused nothing. Trust roots now come fromOWLWARDEN_PLUGIN_TRUSTand from.owlwarden/plugin-trust.jsonin the scan root, which is what ADR 0021 said in the first place. owlwarden plugin inspectcould never reportverified. The TypeScript mirror imported public keys withcreatePublicKey({ format: "raw" }), which Node rejects; a cast silenced the type error that said so. Every key threw, every key becameundefined, and every signature - valid or not - reporteduntrusted. Failing in the safe direction is why it went a release unnoticed: an always-untrustedline is indistinguishable from an unconfigured trust root. Keys are wrapped as SPKI DER now, and both implementations verify a shared test vector that neither of them generates.- An unknown config key is refused rather than stripped. zod drops unknown keys by default, so
failon: "high"parsed cleanly and the run used the defaultinfo- a config that reads as if it tightens the gate, does not, and prints nothing either way. Both objects in the schema are strict now, and a near miss names the key it was probably meant to be. - A symlinked config is reported instead of ignored in silence. Not following it is right - a link is how a hostile tree points config resolution outside itself - but
owlwarden.config.json -> ../shared/config.jsonis an ordinary monorepo layout, and its author had no way to learn theirpresetnever applied.
- The GitHub Action refused every invocation it was ever given. A guard written as
[[ "$value" == $'\0' ]]was meant to reject NUL bytes; bash cannot hold a NUL in a string, so$'\0'is the empty string and the pattern is**. Every input matched, and the Action exited 2 before running anything. It shipped in 1.0 and nothing caught it, because nothing executed the Action: the "action smoke" workflow re-implements its command line rather than calling it. The check is gone - a NUL cannot reach a shell variable throughexecveeither - and the script now has 63 tests that run it with a stubbed CLI and assert on the argv it produces. - The Action snippets in both READMEs and on the site pointed at
suthat/owlwarden@v1, where there is noaction.yml. A check now validates every documented snippet against the Action's real path and real inputs.
Security
owlwarden initnever writes aSessionStarthook, and its MCP entry isnode_modules/.bin/owlwardenrather thannpx -y. Those are the two shapesagent-hook-autoexecandagent-mcp-unpinned-remotereport, and generating them would have hadowlwarden scanreporting its own output. A test asserts everythinginitwrites passesowlwarden vetclean.verifyno longer passesgit apply --unsafe-paths- the flag exists to let a patch write outside the working tree, and the patch is the agent's output. Patch paths are validated before git sees them (no absolute paths, no.., nothing under.git/, no NUL bytes, a file-count cap), and symlinks are excluded from the scratch copy rather than followed.- A flag-shaped Action input is no longer a flag.
pathwas interpolated as a bare positional, and the CLI's parser resolves a flag-shaped positional as an option: a workflow wiringpath:to aworkflow_dispatchinput or a matrix entry read out of the tree could turn a scan step into--target=http://169.254.169.254or--plugin=./evil.wasm. The path is now passed after--, and no input may begin with-. - The JSONC string scanner is no longer quadratic. Reading one character validated the whole remaining input, so a single 1.5 MB string in a
.claude/settings.json- inside the size cap, in a file an attacker controls, on the gate's keystroke path - took the scanner out of service. - Attacker-derived strings are escaped in the gate's reason and in
--format agent. A repository chooses its own filenames and a Unix filename may contain a newline; without this,route.ts\n\nAll checks passed.tswould have injected lines into the one message the model is told to trust. - Agent-surface path matching is case-insensitive. macOS and Windows are case-insensitive filesystems, so
.Claude/settings.jsonis.claude/settings.jsonto a host running there - a one-character bypass of the entire surface. - Duplicate JSON keys are all kept. A config declaring
hookstwice, benign first, exploited the difference between a reviewer reading top-down and a last-wins parser. - An oversized agent config is reported, not skipped. Silently dropping it made a 5 MB
.claude/settings.jsonindistinguishable from a repository with no agent configuration at all. - A bidirectional override in a path no longer survives into a Markdown PR comment, where it reorders what the reviewer reads.
1.0.0 2026-08-12
Stable: plugin API frozen, documentation complete across Diátaxis, and a first-run path that lands in CI and in an agent (ADR 0024).
Added
owlwarden init- no flags writes the adoption kit: agent-rules, a GitHub Action workflow, and.cursor/mcp.json.--agent-rules/--workflow/--mcpselect a subset;--forcereplaces foreign files.--format md- Markdown grouped by severity, for PR comments. Repeatable with other formats (ADR 0022). GitHub Action acceptsmdas well.- Tutorials, CLI and plugin-API reference, upgrade guide, comparison, RFC process, maintainer discoverability checklist. MCP registry descriptor at
mcp/server.json.
Changed
- Version 1.0.0. Plugin
schemaVersion: 1is frozen; breaking plugin-API changes go throughdocs/rfc/. - Cosign/SBOM GitHub artifacts and
cargo-fuzzstay later - npm provenance andcargo-denyalready gate releases.
Security
--format mdflattens newlines in untrusted prose (title, why, route, method) and lengthens snippet fences when source contains backticks, so a hostile tree cannot open a fake heading or break out of a code block in a PR comment.
0.5.0 2026-08-11
Depth beyond CI: first active detector, offline OSV index, plugin integrity, stackable formats, incremental watch (ADR 0019-0023).
Added
csrf-cross-origin-post- canary cross-origin POST behind--allow-active(ADR 0019). Staging only.owlwarden osv update,--osv-db, fail-closed--osv --offline(ADR 0020). Index is lockfile-scoped; not bundled in npm.- Plugin
artifact.sha256, optional ed25519.sig, trust roots viaOWLWARDEN_PLUGIN_TRUST/.owlwarden/plugin-trust.json,--require-signed-plugins(ADR 0021). Still not a hosted registry. - Repeated
--format- one scan, N renders (ADR 0022). - Incremental
watch- dirty-path re-parse + finding merge (ADR 0023).
Changed
- Version 0.5.0. Release-assurance (cosign/SBOM) moves to the next roadmap slice.
Security
- Offline OSV index ingest sanitises advisory ids, package names, versions, and summaries before they enter findings (control/ANSI stripping, shared with live OSV).
owlwarden osv updatewrites indexes via atomicwrite_replacingso a partial write cannot leave CI with a truncated index file.
Fixed
- Incremental
watchrefuses to hash files above the source size cap (2 MiB), matching bounded read limits elsewhere - giant blobs no longer blow the content-hash cache.
0.4.0 2026-08-11
CI-ready depth: pipelines teams already run, plus a deeper fixture corpus so those pipelines gate on findings worth trusting (ADR 0017, ADR 0018).
Added
--format sarif- SARIF 2.1.0 rendering of the existingReport(ADR 0017). For GitHub code scanning and similar consumers.--format junit- JUnit XML, one failure per finding. Exit codes stay on the CLI.- GitHub Action at
action/- composite over the published CLI; preserves exit 0 / 1 / 2. See docs/how-to/ci.md. - Corpus depth - dialect tempting on every clean twin;
ssrfshapes now includegot.getandhttps.get/http.get(4);open-redirectadds an extra/status-first shape (3); generic-profile fixtures outside the 144-cell matrix. - More Safe autofix -
weak-cryptoHashedSecret (algorithm literal'md5'/'sha1'→'sha256', keepingcreateHmackeys andcrypto.receivers) andinsecure-cookieoptions objects that only carry security keys (or{}). owlwarden plugin inspect <path>- print capabilities fromowlwarden.plugin.jsonwithout loading WASM (local preview, not a signed registry).- Cold-scan performance baseline harness (docs/how-to/performance.md).
Security
- GitHub Action drops free-form
args(shell injection +--allow-*bypass); typedosvinput replaces ad-hoc flags; path/out/version reject CR/LF/..;GITHUB_OUTPUTuses a heredoc delimiter. plugin inspectconfines paths under cwd, requiresrealpathunder the working tree, and reads viareadFileBounded(no symlink leaf).- Action smoke workflow pins upstream actions by full commit SHA.
- SARIF endpoint URIs strip control characters;
plugin inspectbounds JSON nesting depth after parse.
Fixed
- MCP
scan_project/scan_filesend the flat NAPI request shape (nestedsettingswas rejected bydeny_unknown_fields, so agent scans failed). - NAPI rejects
--allow-activewithout--target, matching the CLI gate. --fixrefuses to apply when highlight text drifted since the scan (TOCTOU / dirty-tree safety).- Active request pacing reserves the next slot under a lock so concurrent detectors cannot bypass
ACTIVE_MIN_INTERVAL. - OSV advisory summaries strip control / ANSI characters before they enter findings; package-lock line lookup is O(lines) not O(packages × lines).
weak-cryptoSafe autofix no longer rewrites the wholecreateHash/createHmaccall (which truncated HMAC keys and strippedcrypto.).
0.3.0 2026-08-10
Autofix (--fix), --allow-active scaffold, and opt-in Google OSV lookup. Folded in the 0.2.1 docs/MCP polish so one publish updates npm homepage.
Added
owlwarden scan --fix- appliesSafe, single-line highlight replacements only. Never onPossible. Clean git tree by default (--allow-dirtyto override).--dry-runand--fix-unsafe. Re-scans after writes. First Safe remediations:stack-trace-leakandweak-crypto(GuessableToken /Math.random()).--allow-active- with--target, permits state-changing HTTP methods. Rate-limited, request audit log (method/URL/status). No first-party active detector yet. MCP cannot set the flag.--osv- opt-in Google OSV QueryBatch for lockfile dependencies (ADR 0016). New ruleknown-vulnerable-dependency(A06 / CWE-1395). See docs/how-to/osv.md.AdvisoryClientport andCapabilities.advisory, distinct from targetTransportscope.
Changed
- npm
homepage→https://suthat.github.io/owlwarden/. - Token narrative: save tokens with local baseline scans; spend frontier models on hard judgment (site, README, npm README, agent-integration).
owlwarden mcpstderr ready banner / TTY how-to; stdout remains JSON-RPC-only.
0.2.0 2026-08-08
Plugins (source-only WASM), MCP for agents, and twelve Node frameworks. The formal v0.2 bar from ROADMAP.md. Autofix and active checks stay later work.
Added
owlwarden-plugin-host- sandboxed WASM plugin host (ROADMAP v0.2), ships partial: source-only. A plugin is a.wasmmodule plus anowlwarden.plugin.jsonmanifest, loaded with--plugin <path>(repeatable) and refused under--ciunless--allow-pluginsis also passed. Every invocation runs in a freshwasmtimestore bounded by fuel, a 64 MiBStoreLimitsmemory cap, and a wall-clock deadline via epoch interruption; the only host function wired isemit_finding, and every claim it receives is re-validated against the plugin's own manifest before it becomes a finding. A manifest declaringnetworkoractiveis refused at load time rather than silently downgraded - see ADR 0015.wasmtimeis a new dependency, confined to this one crate with default features disabled (onlycranelift/runtime/std); every other crate keeps#![forbid(unsafe_code)]. Floored at 36.0.13 - every earlier release has an open RUSTSEC advisory, several of them sandbox escapes.- Sandbox-escape test suite (
crates/plugin-host/tests/sandbox_escape.rs): fuel exhaustion, oversizedmemory.grow/table.grow, a finding flood, an undeclared rule id, an oversizedwhy, and a benign positive control. - Error code
E_PLUGIN_INVALIDfor a plugin that could not be loaded. owlwarden mcp- stdio MCP server withscan_project,scan_file,explain_rule, andlist_rules. Static and read-only; no--target, no file writes, paths sandboxed to the workspace root.owlwarden init --agent-rules- writes.owlwarden/agent-rules.mdfrom the compiled catalogue.owlwarden plugin scaffold <name>- guest stub (plugin.wat) plus a validowlwarden.plugin.json.- Plugin-authoring schemas in
@dointhai/owlwarden-sdk(pluginManifestSchema). - Seven more Node frameworks with first-class profiles, remediation on every catalogue rule, and square fixture coverage: Hono, Koa, Hapi, Sails.js, Astro, Remix, and Gatsby. Supported set is now twelve stacks (12 rules × 12 frameworks, locked in CI).
- Richer fixture corpus - each framework exercises two real-world shapes for
ssrf(fetch + axios),open-redirect(redirect helper +Locationheader), andsensitive-data-logged(password + accessToken), plus tempting false-positive twins on every clean project. - File-route mapping for Astro (
src/pages/api), Remix flat routes, and Gatsby Functions (src/api). - Request-origin recognition for Hono’s
ccontext and Astro’sAstro.request.
Changed
DetectorMeta.title/.category/.descriptionare nowCow<'static, str>(were&'static str), so aWasmDetectorbuilt from a parsed plugin manifest can own its strings. No change to the JSON wire shape or to first-party rules, which still write string literals.- README and npm package text rewritten in plain language: what it does, that it stays local, which frameworks it knows, and what v0.2 actually ships (plugins source-only, MCP read-only). States that local scans cover baseline checks without burning LLM tokens, and that deeper AI security review still belongs on high-impact work.
- Plugin hardening after whitebox review:
O_NOFOLLOW+ bounded reads for manifest/WASM load;StoreLimitson tables; plugin rule ids must be namespaced under the plugin id; source-only plugins cannot declareconfirmed;whycapped; MCP JSON-RPC lines capped;init/plugin scaffolduse symlink-safe writes under the working directory; napi re-checks--ci+--allow-plugins. - Fixture matrix tightened: every clean twin ships
*tempting*and*safe-redirect*files; multi-fire rules are locked to named source shapes (fetch/axios, redirect/Location, …); the TypeScript e2e path assertsSHARED_FIREScounts on all twelve frameworks, not only Next.js. - Cookie detection: nested setters (
ctx.cookies.set), HapiisHttpOnly/isSecure/isSameSite, and dropped false cookie matches onc.header/res.setHeader/ bareserialize. - Stack-trace rule recognises Koa-style
ctx.body = …assignments. secureHeaderscounts as header middleware for Hono.
Fixed
cargo denyCI gate: allowCDLA-Permissive-2.0forwebpki-roots(Mozilla CA data via rustls/reqwest), and give the dynamic-engine dev-dep onowlwarden-transporta workspace version so it is not a path-only wildcard.
Security
- Prompt-injection hardening for MCP / agents / plugins. MCP tool results are wrapped in an
OWLWARDEN_TOOL_RESULTtrust-boundary envelope; free text is stripped of control/invisible characters and common chat role markers. Pluginwhyis sanitised at emit time;init --agent-rulestells agents to treat findings as evidence, not instructions.
0.1.0
Passive dynamic engine and correlation. Confirmed is reachable for the first time, without opening active (state-changing) checks.
Added
--target <URL>- probe a live origin with passive methods only (GET/HEAD/OPTIONS). Operator intent from the command line; never read from project config, so a hostile PR cannot point CI at an internal host (ADR 0014).--scope <URL>(repeatable) - deny-by-default allowlist. When omitted, the allowlist is exactly the origin of--target. Localhost is not special.owlwarden-transport-ReqwestTransportthat enforces scope on every redirect hop, streams body bytes under the cap, and refuses state-changing methods without--allow-active(no active detectors ship yet).owlwarden-dynamic- passive header probe forsecurity-headers-missingand a correlation post-pass that raises agreeing static+dynamic findings toconfirmed, or clears a static gap when the live response already sets the headers.- Error code
E_TARGET_INVALIDfor bad target/scope. - Square static fixture matrix - every catalogue rule has a vulnerable fixture and a silent clean twin on all five frameworks (12 × 5 = 60 cells).
weak-cryptofires three shapes on every framework. Locked bySHARED_FIRESincrates/detectors/tests/fixtures.rs. - Framework dynamic matrix -
crates/dynamic-engine/tests/framework_matrix.rscorrelatessecurity-headers-missingagainst each framework's fixture plus a live header probe (confirmed / cleared / clean stays silent). - CLI live e2e -
packages/cli/test/run.test.tsdrives--targetthrough the npm CLI against an in-process server on all five frameworks.
Changed
- Confidence filtering runs after correlation, so a
possiblestatic finding can still becomeconfirmed. watchrefuses--target/--scope(static-only; re-probing on save is hostile to the developer's own server).- Help text no longer claims every scan is offline - only scans without
--target. - napi
scanis async (spawn_blocking) so a live probe cannot deadlock the Node event loop while an in-process test (or user) server is accepting connections. The TypeScript CLI awaits the Promise.
Security
- Scope deny-by-default, including redirect hops (SSRF-bait case).
- Credentials in
--target/--scopeURLs are refused. - Automatic response decompression is off; body caps apply while streaming.
- No path from scanned-tree config to the request URL.
- Redirect
Locationre-validated as a target (blocksuser@hostconfusion, non-http(s) schemes, control characters, oversized URLs). - Headers-only probes (
max_body_bytes = 0) do not pull a response body into memory - a hostile HEAD payload cannot inflate the scanner. - Oversized response header values are dropped, not truncated; outbound request headers reject CRLF/NUL (request-smuggling footgun for future detectors).
0.0.2
Static slice of the trust-and-noise work planned for v0.1: suppressions, baseline, three gap-closing rules, and watch. Still no network - the dynamic engine that makes Confirmed reachable remains later.
Added
- Inline suppressions with a mandatory reason:
// owlwarden-disable-next-line <rule> -- <reason>. Directives without a reason never hide a finding.--report-suppressionslists every directive and flags stale or missing-reason ones.suppressedCountin the JSON report is now wired for real. - Baseline mode.
--baseline <file>reports only findings new since the file was written;--write-baseline <file>records current debt. Fingerprints key on rule id, normalised path, whitespace-collapsed evidence, and an occurrence index so a formatter pass does not reopen accepted findings and two identical findings in one file stay distinct (ADR 0013). - Three rules closing the static-reachable OWASP gaps:
unpinned-dependency(A06) -*/latestinpackage.jsonci-unpinned-action(A08) - GitHub Actions not pinned to a commit SHAsensitive-data-logged(A09) - passwords/tokens written to a log sink Each ships remediation for all five frameworks, with vulnerable and should-not-fire fixtures.owlwarden watch- re-scan on change, static only. Never opens a network path.
Changed
- The filesystem walker now reads
.github/(still skips other hidden directories), so CI integrity rules can see workflow files. - Report JSON gains
suppressionsandbaselineHiddenCount(additive under schema1.0). - Security hardening (hostile scan target): executable project config (
owlwarden.config.{js,mjs,ts,mts}) is opt-in via--allow-config-js(default loads JSON only);--ciignores projectpreset/failOn/minConfidenceunless--allow-project-config, ignores inline suppressions unless--allow-suppressions, and refuses--baselineunless--allow-baseline; truncated reports fail CI;--out/--write-baselinerefuse symlinked parent directories and write via temp+rename (create_new/wx); source reads are bounded and useO_NOFOLLOWon Unix; parser nesting guard skips comments/strings and counts generics/JSX and brackets inside templates;hardcoded-secretredacts values in snippets before line truncation; baseline/config loads refuse symlinks and oversized inputs; CI workflow evidence is length-capped. See SECURITY.md.
0.0.1
First release. A static engine, a rule set, and honest reporting about what it does and does not reach.
Added
- Static engine. Sandboxed
SourceProvider, oxc parsing with bounded recursion, and per-file and whole-project rule traits. - Five frameworks via the
FrameworkProfileregistry: Next.js, Nuxt, NestJS, Express, and Fastify. Detection is package-based with specificity tie-breaking, so a NestJS project that also depends on Express is treated as NestJS. - Nine rules across six OWASP Top 10 (2021) categories:
open-redirect,weak-crypto,sql-injection,cors-permissive,insecure-cookie,security-headers-missing,stack-trace-leak,hardcoded-secret, andssrf. Full catalogue in RULES.md. owlwarden coverage- what the rules reach, and what they do not, computed from the compiled-in rules rather than maintained by hand.- Confidence on every finding, and a
RequestOriginanalysis that the injection-shaped rules use to distinguishLikelyfromPossible. - Remediation tables, so every rule ships a framework-specific fix for each supported framework rather than generic advice.
prettyandjsonreporters, with code frames, ASCII and no-colour fallbacks, and a stable JSON contract validated against zod schemas in CI.owlwarden explain <id>- the full write-up for a rule with no network access, because the reader may be an agent with no browser.- npm CLI plus a standalone Rust binary, on Linux, macOS, and Windows.
Security
- Passive only. v0.0 reads source and sends no requests, so it cannot change the state of anything it scans.
- No telemetry, of any kind, opt-in or otherwise.
#![forbid(unsafe_code)]in every crate.- Bounded file count, file size, total bytes, and parser recursion depth, so a hostile repository cannot exhaust memory or the stack.
[Unreleased]: https://github.com/suthat/owlwarden/compare/v1.3.0...HEAD [1.3.0]: https://github.com/suthat/owlwarden/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/suthat/owlwarden/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/suthat/owlwarden/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/suthat/owlwarden/compare/v0.5.0...v1.0.0 [0.5.0]: https://github.com/suthat/owlwarden/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/suthat/owlwarden/compare/v0.3.0...v0.4.0 [0.3.0]: https://github.com/suthat/owlwarden/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/suthat/owlwarden/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/suthat/owlwarden/compare/v0.0.2...v0.1.0 [0.0.2]: https://github.com/suthat/owlwarden/compare/v0.0.1...v0.0.2 [0.0.1]: https://github.com/suthat/owlwarden/releases/tag/v0.0.1