0.2.0 release — taint soundness + hardening scrub, version bump + changelog#12
Merged
Conversation
…rub) A 7-agent review of the one-shot SP0–SP9 system surfaced 9 findings; all fixed here with test-first coverage. Suite 843→913, ruff/mypy clean, self-hosting green. Acceptance oracle: a 30-case FN-fire/FP-clean taint battery (kept out-of-tree). P0 — L2 taint soundness hole (false negatives): `_resolve_expr` fell through to `return function_taint` for unmodelled AST shapes; in a @trusted producer that resets untrusted data to the trusted tier (fail-open) and yields a clean report. Now modelled: Subscript/Attribute/Await/BoolOp/JoinedStr(f-string)/comprehensions; container-write taint (Assign/AugAssign/AnnAssign subscript+attr targets); curated taint-propagating ops in _resolve_call (str/repr/ascii/bytes/ bytearray/format/next; .format/.join; .get/.pop/.setdefault); self/cls method-call L2 keys (build_call_taint_map); serialization sink-alias closure (call_taint_map). String-building combines via least_trusted (weakest-link) so a benign literal + validated data stays clean — no new FP on validated code. The unknown-call fallback is unchanged (no FP explosion); pre-existing combiners (BinOp/List/Dict/IfExp) untouched. P0 — security: symlink confine_to_root bypass (THREAT-001 residual). discovery.py now resolves each discovered file under root when confine_to_root (MCP path); a symlinked .py can no longer escape to read out-of-root content. CLI default behavior unchanged. Hardening: - Scan observability: parse-error / unreadable / recursion-skipped / missing-source-root files are now COUNTED (ScanSummary.unanalyzed) and surfaced (CLI summary + MCP result), with opt-in `--fail-on-unanalyzed`; a no-module file (top-level __init__.py) emits an observable WLN-ENGINE-NO-MODULE FACT (deliberately NOT counted as unanalyzed). - An explicit --config path that does not exist now raises ConfigError instead of silently running with default policy. - Line-less engine-diagnostic DEFECTs (WLN-L3-* / WLN-ENGINE-DIAGNOSTIC) no longer crash the run via the suppression line-invariant assert; the <engine> sentinel is exempted (ENGINE_PATH hoisted to core/finding). - MCP: an unexpected exception in a tool handler now returns an isError result (which clients reliably surface) instead of a -32603 error; McpError still propagates as a JSON-RPC protocol fault. - clarion/client docstring corrected: routes on the HTTP status band, not the envelope code. Known residuals left for triage (pre-existing / cross-rule, out of scope): BinOp `"" + validated` over-taints; .get/.pop tainted-default over-taints (IfExp-consistent); star-import of markers is a FN but observable via WLN-ENGINE-UNKNOWN-IMPORT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The L2 variable-level resolver used taint_join (provenance-clash; any cross-family pair → MIXED_RAW) for value-building / either-or / container-summary combiners, where the rank-meet least_trusted (weakest-link) is correct — the operator already used by the f-string/.format/.join/builtins paths. taint_join(INTEGRAL, ASSURED) = MIXED_RAW manufactured a spurious provenance clash on benign literal+validated combinations, firing PY-WL-101 on validated data. Switch 9 expression/value-building sites in variable_level.py to least_trusted: BinOp, List/Tuple/Set, Dict, IfExp, BoolOp, DictComp, .get/.pop/.setdefault default, augmented-assign, and container writes (_taint_container_base). Control-flow MERGES (if/else, loops, match arms) deliberately keep taint_join — the documented join lattice; their full-sweep conversion is tracked separately (wardline-4d9f840c24). Soundness: both firing rules gate on a clean declared level, so any raw result (rank 6 or 7) still fires — no false negatives. Verified: 923 tests (+10 clean-direction, 15 buggy-MIXED_RAW assertions corrected to the precise least_trusted value), soundness battery 30/30, end-to-end FP repro 3→0, self-host 0 PY-WL defects, ruff + mypy --strict clean. Closes wardline-4d94577013. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump _version.py 0.1.0 → 0.2.0 and finalize the CHANGELOG [0.2.0] entry: MCP server (SP8), explain_taint provenance + N-hop chain, Clarion-backed taint store (SP9), the Material for MkDocs docs site (SP7), the L2 taint soundness fix and expression-combiner FP fix, scan observability + the other hardening fixes, the symlink path-confinement fix (Security), and the loom-extra removal. Live-docs version strings bumped (getting-started, cli reference); the __version__ guard test updated to 0.2.0. Build verified: 923 tests pass, mkdocs --strict clean, sdist+wheel build as wardline-0.2.0, wheel metadata valid (MIT, py>=3.12, extras clarion/dev/docs/scanner). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A full-codebase scrub of the one-shot SP0–SP9 system, driven by a 7-agent review panel → 9 findings, all fixed with test-first coverage. Each fix was implemented by a fresh subagent, independently reviewed, and empirically re-verified against a known-vulnerable taint battery. A later pass (commit
58cca6d) closes two of the three deferred residuals.--strictclean · self-hosting greenP0 — L2 taint soundness hole (silent false negatives)
_resolve_exprfell through toreturn function_taintfor unmodelled AST shapes. In a@trustedproducer that resets untrusted data to the trusted tier (fail-open) and emits a clean report the user wrongly trusts — the worst outcome for a static analyzer. Verified-and-fixed shapes: f-strings,str()/.format()/.join(),dict.get()/subscript, BoolOp, attribute reads,await, comprehensions, container-writes,self/clsmethod calls, aliased serialization sinks.least_trusted(weakest-link), so a benign literal + validated data stays clean — no new false positives on validated code (caught and corrected mid-review via base-commit diffing).taint_join; they are now migrated toleast_trustedin the follow-up below.P0 — security: symlink
confine_to_rootbypass (THREAT-001 residual)The per-file discovery loop used a lexical path check, so a symlinked
.pyinside a legit source-root could escape the root and the analyzer would read out-of-tree content (reproduced:→ /etc/passwd) via the MCPscantool. Now each discovered file is resolved under root whenconfine_to_rootis set (MCP path). CLI default behavior unchanged.Follow-up — expression-combiner over-tainting (false positives) ·
58cca6dCloses residuals #1 and #2 below. The L2 resolver used
taint_join(provenance-clash; any cross-family pair →MIXED_RAW) for value-building / either-or / container-summary combiners, where the rank-meetleast_trusted(weakest-link) is correct — the operator already used by the f-string/.format/.joinpaths.taint_join(INTEGRAL, ASSURED) = MIXED_RAWmanufactured a spurious clash on benign literal+validated combinations, firing PY-WL-101 on validated data.variable_level.pyswitched toleast_trusted: BinOp, List/Tuple/Set, Dict, IfExp, BoolOp, DictComp,.get/.pop/.setdefaultdefault, augmented-assign, and container writes (_taint_container_base).taint_join— the documented join lattice; the full-sweep conversion is tracked separately (wardline-4d9f840c24).MIXED_RAW→UNKNOWN_RAWrelabel never straddles a threshold.MIXED_RAWassertions corrected to the preciseleast_trustedvalue, soundness battery 30/30, end-to-end FP repro 3 → 0, self-host 0 PY-WL defects.Hardening
ScanSummary.unanalyzed) and surfaced (CLI summary + MCP result), with opt-in--fail-on-unanalyzed. A no-module file (top-level__init__.py) emits an observableWLN-ENGINE-NO-MODULEFACT (deliberately not counted as unanalyzed, to avoid signal dilution).--configpath that does not exist now raisesConfigErrorinstead of silently running with default policy.WLN-L3-*/WLN-ENGINE-DIAGNOSTIC) no longer crash the run via the suppression line-invariant assert.isErrorresult (which clients reliably surface) instead of a-32603error;McpErrorstill propagates as a JSON-RPC protocol fault.clarion/clientdocstring corrected (routes on HTTP status band, not envelope code).Known residuals (left for triage)
BinOp— fixed in"" + validatedover-taints58cca6d(wardline-4d94577013).— fixed in.get/.poptainted-default over-taints58cca6d(wardline-4d94577013).WLN-ENGINE-UNKNOWN-IMPORT(wardline-2b427a9579).wardline-4d9f840c24).Test plan
ruff check src tests·mypy·pytest -q(923 passed) ·pytest tests/test_self_hosting.py🤖 Generated with Claude Code