fix(config): report every permissive setting in a file, not the first one

NodeGoat's server.js configures a permissive session at line 78 and disables template
auto-escaping at line 135, both inside the same function. The arbiter returned one verdict per
question, so the escaping defect that task 14.3 added was detected on every run and reported on
none: which of two real defects a contributor saw depended on where in the file it sat.

Configuration now returns one verdict per setting whose value the model could read. The pipeline
already emitted one finding per entailed answer, so this arbiter was the only place still folding
them to one.

Three boundaries are drawn rather than assumed.

- A setting whose value is COMPUTED stays one verdict per region. That band is a question for the
  judge rather than a finding, and returning every undecided setting would multiply model calls
  for claims the graph did not establish.
- A call carrying two permissive literals is one finding. cors({origin: true, credentials: true})
  is one misconfiguration, not two.
- Config verdicts now carry a path:line:function location. Without one they shared the region's
  identity and the deduplicator kept a single finding, so the plural answer collapsed straight
  back to one. That is the same gap dominance carried until its findings stopped arriving with no
  line at all.

Dominance is left answering once per region, and that is settled rather than pending: its claim is
that a region contradicts itself, so the region is the unit and a second verdict would restate the
same inconsistency. The pipeline now says so where it used to call this a known gap.

Measured: NodeGoat's config_secrets goes from one finding to two, server.js:78 for the session and
server.js:135 for the disabled escaping, with the applied marked.setOptions fix between them
staying silent. VAmPI is unchanged and no other family moved.

1,417 tests pass, seventeen skipped; thirteen Joern-gated query tests pass in the image.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-20 16:30:04 +02:00
co-authored by Claude Opus 5
parent e6bdac46e2
commit 3bd844055c
6 changed files with 167 additions and 20 deletions
+41
View File
@@ -214,3 +214,44 @@ def test_the_first_setting_in_a_file_is_the_one_reported() -> None:
]
answer = verdict(_cpg(rows), _javascript_spec(), function="m")
assert answer is not None and "line 78" in answer.witness, answer.witness
def test_a_file_reports_every_permissive_setting_not_the_first() -> None:
"""Two defects in one file are two findings, with their own sites.
Measured on NodeGoat: `server.js` configures a permissive session at line 78 and disables template
auto-escaping at line 135, both inside the same function. One verdict per region reported the session and
the escaping defect was detected on every run and reported on none, so which of two real defects a
contributor saw depended on where in the file it sat.
"""
from openultrasast.model.config_value import verdicts
from openultrasast.model.ladder import Rung
rows = [
{"setting": "session({ resave: true })", "line": "78", "method": "<lambda>0", "literalArgs": ["true"], "args": []},
{"setting": "marked.setOptions({ sanitize: true })", "line": "126", "method": "<lambda>0", "literalArgs": ["true"], "args": []},
{"setting": "swig.setDefaults({ autoescape: false })", "line": "135", "method": "<lambda>0", "literalArgs": ["false"], "args": []},
]
answers = verdicts(_cpg(rows), _javascript_spec(), function="<lambda>0", file="server.js")
assert len(answers) == 2, [a.witness for a in answers]
assert all(a.rung is Rung.ENTAILED for a in answers)
assert [a.location for a in answers] == ["server.js:78:<lambda>0", "server.js:135:<lambda>0"]
# The applied fix in between stays silent, which is what the per-setting polarity is for.
assert all("marked" not in a.witness for a in answers)
def test_one_setting_with_two_permissive_literals_is_one_finding() -> None:
"""A call is a defect once, however many of its values are open. No family reports a defect twice."""
from openultrasast.model.config_value import verdicts
rows = [
{
"setting": "cors({ origin: true, credentials: true })",
"line": "9",
"method": "app",
"literalArgs": ["true", "true"],
"args": [],
}
]
answers = verdicts(_cpg(rows), _javascript_spec(), function="app", file="server.js")
assert len(answers) == 1, [a.witness for a in answers]