Commit Graph
7 Commits
Author SHA1 Message Date
Marius CiepluchandClaude Opus 5 fe45081b43 fix(ci): run ruff format, and record run 5 at 90% pair-correct
CI was red on `ruff format --check`: 23 files. I ran `ruff check` after every
change this session and never once ran `ruff format --check`, though CI runs
both. Lint passing is not formatting passing, and I treated one as evidence for
the other for the whole session.

Formatting applied. The full CI sequence now reproduces locally: format, lint,
mypy over 94 files, 735 tests, three gates byte-identical, compileall, and the
semantic job's 16 marked tests.

Also records run 5 of the pipeline measurement, which the vampi fix lifted as
predicted:

                       hunter (K=5)   pipeline (K=1)
  pair-correct              76.7%          90.0%
  leak                      12.3%           6.7%
  detection                    --          96.7%
  cost                     ~$0.69        $0.0068

27 of 30 pairs. Five runs, each step from a measurement contradicting an
assumption: 73.3 (no pipeline path) -> 73.3 (routing fixed, IDENTICAL, which
exposed the discarded verdict) -> 80.0 (residual question matched to its
arbiter) -> 86.7 (aiohttp settings) -> 90.0 (dominance question no longer
contradicting its own evidence).

One miss left: dsvpwa-dsvpwa-main, whose setting is an argparse --ssl default
rather than a security-setting call. Out of reach before the settings work and
still is; contorting the arbiter around one pair would be worse than the miss.

The caveat is recorded beside the number: 30 pairs of one slice, and the slice
iterated against all session. The +16% overfitting gap against vfc-js is why
contributor-scan Req 8.1/8.2 exist.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-08 19:46:58 +02:00
Marius CiepluchandClaude Opus 5 db3db6056d feat(model-grounded-detection): widen the settings table, wire the weak-algorithm arm
Two things, and a finding that says neither fixes the pairs that prompted them.

The settings table was Flask/FastAPI-shaped (CORS, set_cookie, app.run) while
the corpus contains aiohttp and Django applications, so three config_secrets
pairs contained no modelled security setting at all. Added aiohttp's
session_setup and cookie storages and Django's SECURE_* / DEBUG settings: 8
calls to 24.

And config_secrets spans three abstractions while only one was wired -- a
permissive setting VALUE. Weak algorithm CHOICE (CWE-326/327) is a different
thing: the danger is the algorithm NAMED, not a flag left open. Its data has
been sitting on the sink facts as `weak_literals` since before this feature and
nothing read it. Both abstractions read the same literal, so the arm is small:
hashlib.new("md5") is now entailed and hashlib.new("sha256") is not.

The finding: the three silent config pairs are CORPUS DEFECTS, not model gaps.
Measured similarity 23%, 45% and 53% -- dsvpwa-dsvpwa-main pairs `def main()`
against `def run(self, handler)`, a different function entirely. No settings
table can make those split, and it is worth saying so rather than letting the
work imply otherwise. The table gap was real and worth closing on its own
merits; it just is not why those pairs are silent.

734 passing, mypy/ruff clean, gates byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-08 18:43:33 +02:00
Marius CiepluchandClaude Opus 5 eebca3f663 fix(model-grounded-detection): the absence arbiter was silencing its own family
Two bugs, together making 4.1 report "no bug" on a textbook IDOR. Both found by
running it against a real corpus pair rather than a fixture.

1. constraint_params were treated as dischargers. A field named `user_id` is
   what an IDOR is MADE OF -- `filter_by(id=user_id)` with user_id off the
   request is the bug, not the fix. The deleted checker only counted an identity
   constraint whose value's PROVENANCE was the authenticated context, so the
   parameter name alone evidences nothing. This is the mirror of group 2's
   sanitizer bug: a thing that must never count as a discharge, counted.

2. Guard tokens matched as substrings. With `user` in the token list, the string
   "SELECT * FROM users WHERE id = ?" contains `user`, so the query reported
   every operation in the handler as guarded. Matching is word-boundary now.

On threatbyte-api-v1-delete the arbiter previously found three operations, all
"guarded" by evidence like '"SELECT * FROM users WHERE id = ?"' and '(user_id,)',
and returned no verdict at all. After the fix, on the real pairs:

  threatbyte-api-v1-delete     vuln=model_corroborated  fixed=none
  vampi-users-update-password  vuln=model_corroborated  fixed=none

Both distinguished, deterministically, with no model call. Corroborated rather
than entailed is right for these: they are single-handler excerpts with no
guarded sibling to be inconsistent with, which is the asymmetry the rung
distinction exists to express.

identity_sources still discharge; request_sources and permissive_values remain
excluded because they describe the vulnerable shape rather than the fix.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-08 15:10:51 +02:00
Marius CiepluchandClaude Opus 5 d9ceeb6a09 feat(model-grounded-detection): 4.2 constant abstraction, the third arbiter form
A configuration bug has no flow to follow and no guard to dominate.
CORS(app, origins="*") is dangerous because of the value it is SET TO, so the
arbiter evaluates the setting's arguments against the closed permissive set in
the obligation facts -- the third form beside taint reachability and guard
dominance, and the one Req 6.3 names.

  ENTAILED      a security setting is passed a literal in the permissive set
  CORROBORATED  the argument is computed, so constant abstraction cannot decide
                it -- that residual is the judge's, never silently "safe"
  (none)        every literal is outside the set, or the call is not a setting

Two Joern node-shape bugs, both found by running it and both of the same class
as group 2's receiver bug -- the graph's indexing is not what the source text
suggests:

  * a keyword argument (`origins="*"`) is an assignment-shaped node, so
    isLiteral is false on the argument itself and the literal sits in its
    subtree; literals are collected from the argument's AST now.
  * Joern indexes the receiver at 0, positional arguments from 1, and NAMED
    arguments at -1. Settings are configured overwhelmingly by keyword, so
    argumentIndexGt(0) -- correct when counting arity for the taint shape test
    -- dropped exactly the arguments that matter and every keyword-configured
    setting read as "computed".

Live, deterministic, zero model calls:
  create_app -> model_entailed  CORS(app, origins = "*"): permissive literal "*"
  safe_app   -> no finding      (origins = "https://app.example")

All three arbiter forms now work against a real CPG: taint reachability for the
flow families, guard dominance for absence, constant abstraction for config.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-08 15:00:41 +02:00
Marius CiepluchandClaude Opus 5 fec67253f3 feat(model-grounded-detection): 4.1 guard dominance, 4.2 the missing sink facts
4.1 is the arbiter for the class no crash oracle reaches. An access-control bug
is the ABSENCE of a guard: no flow to follow, no crash to reproduce. What the
model can establish is a consistency violation -- an obligated operation no
discharging guard governs, in a file where its siblings are governed by one.

"Governed" turned out not to be one relation, and asking pure CFG dominance of
the operation gets two of three shapes wrong. Measured against the real engine:

  identity constraint  the guard is INSIDE the operation's arguments
                       filter_by(owner_id = current_user.id)
  identity branch      abort(403).controlledBy == [note.owner_id != current_user.id]
                       -- the check runs AFTER the fetch, so it never dominates
                       the operation, but it controls whether the value escapes
  dominating guard     a discharger-naming call that dominates the operation

That is why the query uses controlledBy rather than dominatedBy for shape 2.

Two bugs found by running it, both the same class as the sanitizer bug in group
2 -- part of a fact flattened, the part carrying the signal dropped:

  * dominance_specs used only `calls` and `decorators`, omitting
    identity_sources (current_user, request.user) and constraint_params
    (owner_id, user_id) -- which is how an identity constraint is actually
    written. The arbiter reported "no guard" on handlers that plainly had one.
    request_sources and permissive_values stay excluded: they describe the
    vulnerable shape, and including them would let the arbiter read a bug as
    its own fix.
  * matching operations by code containment yielded four or five desugared rows
    per operation (the bare `tmp1.filter_by` receiver among them, carrying none
    of the guards the real call has), so a guarded operation could be flagged.
    Matching is by call name now.

Live, deterministic, zero model calls:
  leaky      -> model_entailed  no discharging guard, while 2 sibling handlers are guarded
  edit_note  -> no finding      (ownership check controls the denial)
  list_notes -> no finding      (identity constraint in the query)

4.2 authors the sink facts the go/no-go measured as its largest miss class (11
of 23): Django ORM raw SQL, and the four families that previously had no fact
routing to them by CWE at all -- path, output_encoding, untrusted_destination,
prototype. Each entry is a documented dangerous API of the named library with
its provenance recorded inline, and rule_ids left empty because a sink fact is
not a detection rule. Flagged for maintainer review as data, given the standing
"no LLM-authored patterns" constraint. The exact-set assertion written in task
1.1 to hold this gap open is updated rather than deleted, so it still fails if a
family ever loses its seed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-08 14:45:12 +02:00
Marius CiepluchandClaude Opus 5 f59e0f5b5a fix(model-grounded-detection): two modelling bugs the first live Joern run exposed
Both were found by running the real engine, and neither was visible offline.

1. A sink was its own sanitizer. The shipped facts mark some sanitizers with a
   *shape* qualifier -- parameterized ("execute(sql, params) binds rather than
   interpolates") and literal_format_arg ("printf with a constant format string
   is safe"). These describe a safe FORM OF THE SINK CALL, not a call that
   cleans a value on the path. Flattening them into the sanitizer list made
   `execute` both the injection sink and its own sanitizer, so every SQL flow
   would have reported as already-clean and nothing could ever be entailed.
   Fixed as a class, not an instance: any fact carrying a shape qualifier is
   excluded from `sanitizers` and kept in `safe_shape_sinks` for the shape test
   task 4.2 will model properly. Fixing only the Python `parameterized` case
   would have left C's entire printf family -- which the generalised test then
   caught immediately.

2. labeled_family lost its deterministic derivation in group 1. The deleted
   classifier resolved a pair's family by declared label, else obligation, else
   mechanism, else CWE, and only asked an LLM when every link failed -- which
   for a labeled corpus was never. Group 1 reduced this to the declared label
   alone, which silently emptied every family whose pairs are labeled by CWE:
   the injection slice went to zero pairs. The chain is restored, still with no
   model call, and the family census again matches the committed entailment
   ceiling exactly (injection 50, memory 176, access_control 8, ...).

Also: the Joern seam now runs in the CPG's own scratch directory, because
`joern --script` writes a `workspace/` beside the working directory and would
otherwise leave one inside the repository being analysed.

The taint query itself was corrected against the live engine: the sink is the
call's ARGUMENTS (data flows into arguments, not into the call node) and
reachableByFlows is called on the sink with the source. With that, the
three-hop chain our flat IR could not resolve is found:
  request.args["cmd"] -> cmd -> "echo " + cmd -> full -> os.system(full)

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-08 12:45:42 +02:00
Marius CiepluchandClaude Opus 5 9ed0e45c5f feat(model-grounded-detection): group 1, subtract the noise architecture
99 source modules -> 81. 832 tests -> 646. The whole learning/ package (16
modules, ~4,100 lines) and six semantic/ modules removed, with the three gates
byte-identical to 2026-09-06-gate-baseline.txt at every step.

The sequence is what made it safe: vocabularies ported first (1.1), importers
detached second (1.2-1.3), deletion third (1.4). Every import break during the
deletion was therefore a detachment miss in the importer and was fixed there --
no module was restored to satisfy one. Both misses were the same shape and were
fixed as a class: operations.py and shapes.py each imported trailing_name from
the deleted variants.py, so it moved to semantic/ir.py, its natural home, and
both importers changed in one edit.

Ported before deleting: the closed guard vocabulary (verbatim, nothing else
holds it) into model/specs.py, which derives TaintSpec and DominanceSpec from
the retained ruleset facts rather than copying them. Re-homed: families ->
model/taxonomy, candidates -> model/candidates, endpoint -> model/endpoint,
plus Region, labeled_family and labeled_spans. labeled_family no longer falls
back to the LLM classifier -- an unlabeled pair is honestly unknown.

Four deviations, each recorded in the spec notes with its reason. improve/
evolve.py and validator.py were stripped, not deleted: each mixes the mechanism
lever (gone) with the deterministic rule-status loop `ousast improve` still
runs, which has no LLM in it; a test now asserts the lever's absence by symbol
so it cannot creep back. The learning CLI group became `model candidates`,
because deleting it outright would have orphaned the retained enumerator by
removing its only entry point. thinking moved from [learning] to [models].

A gap named rather than papered over: no retained sink fact routes by CWE to
path, output_encoding, untrusted_destination or prototype -- the same hole the
entailment ceiling diagnosed. The test asserts the seeded set and the gap set
exactly, so task 4.2 must update it when it authors those sinks.

The audit found exactly one genuine orphan: vectorstore.py, a bakeoff harness
with no production caller and no CLI, alive only through its own test. The five
named subsystems each carry an explicit decision. Manifest committed, with a
test asserting the tree still matches it.

Group 2 (the Joern go/no-go) has not started.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
2026-09-08 12:15:54 +02:00