mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
fix(pre-push-safety-net): take the operation symbol from the call (M1b 12.1)
Admission extracted everything before the first parenthesis, so the operation for `const preTax = eval(req.body.preTax)` was `const preTax = eval`. No real declaration can enumerate every assignment a codebase might write, so admission would have rejected genuine findings on operation_semantics_mismatch. The M1b demonstration only passed because its declaration listed the assignment spellings. The symbol is now the trailing qualified callee. Receiver spelling stays part of it, so `$wpdb->prepare` and `prepare` remain different operations and a declaration cannot widen from one to the other. A spelling that is not a qualified name, such as a dynamic `handlers[name](x)`, yields the statement text, matches no declaration and stays ineligible exactly as before. Controls cover the assigned call, the receiver distinction, the dynamic spelling, and that a declaration for a different operation still does not cover it. The recorded-veto report reads the same helper instead of repeating the extraction. 1,403 tests passed, nine skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5
parent
6653b6aca8
commit
4ee31142ab
@@ -397,12 +397,13 @@ Preserve all completed task evidence below.
|
|||||||
|
|
||||||
- [ ] 12. Security capability work found during M1a and M1b
|
- [ ] 12. Security capability work found during M1a and M1b
|
||||||
- Group 11 established that M2 needs graph reuse rather than more runtime tuning, and that decision is recorded there. This group returns to what the tool can actually establish about a codebase, which is where the remaining release risk sits: two of the three shipped families cannot complete their change context at all, and admission rejects genuine findings on a mis-extracted operation symbol.
|
- Group 11 established that M2 needs graph reuse rather than more runtime tuning, and that decision is recorded there. This group returns to what the tool can actually establish about a codebase, which is where the remaining release risk sits: two of the three shipped families cannot complete their change context at all, and admission rejects genuine findings on a mis-extracted operation symbol.
|
||||||
- [ ] 12.1 Extract the operation symbol from the operation, not from the statement
|
- [x] 12.1 Extract the operation symbol from the operation, not from the statement
|
||||||
- **Found 2026-09-16 in the M1b explanation run.** Admission takes the text before the first parenthesis as the operation symbol, so `const preTax = eval(req.body.preTax)` yields `const preTax = eval`. The demonstration declaration had to enumerate each assignment spelling for its coverage check to pass.
|
- **Found 2026-09-16 in the M1b explanation run.** Admission takes the text before the first parenthesis as the operation symbol, so `const preTax = eval(req.body.preTax)` yields `const preTax = eval`. The demonstration declaration had to enumerate each assignment spelling for its coverage check to pass.
|
||||||
- A qualified declaration cannot enumerate every assignment a codebase might write, so admission would reject genuine findings on `operation_semantics_mismatch`. Take the symbol from the called operation the engine reported, preserving receiver spelling, and leave an unresolved or dynamic spelling ineligible as it is today.
|
- A qualified declaration cannot enumerate every assignment a codebase might write, so admission would reject genuine findings on `operation_semantics_mismatch`. Take the symbol from the called operation the engine reported, preserving receiver spelling, and leave an unresolved or dynamic spelling ineligible as it is today.
|
||||||
- Done when the symbol for an assigned call is the call's own name, a receiver-qualified call keeps its receiver, a dynamic spelling stays ineligible, and a declaration naming the plain symbol covers the assigned form.
|
- Done when the symbol for an assigned call is the call's own name, a receiver-qualified call keeps its receiver, a dynamic spelling stays ineligible, and a declaration naming the plain symbol covers the assigned form.
|
||||||
- _Boundary: Delta and Actionability Policy_
|
- _Boundary: Delta and Actionability Policy_
|
||||||
- _Requirements: 3.1, 3.2, 3.4_
|
- _Requirements: 3.1, 3.2, 3.4_
|
||||||
|
- _Verified 2026-09-17:_ 1,403 tests passed, nine skipped; Ruff/format/mypy passed. The symbol is now the trailing qualified callee rather than everything before the first parenthesis, so `const preTax = eval(...)` yields `eval` and a declaration naming the call covers the assigned form. Receiver spelling is preserved, so `$wpdb->prepare` and `prepare` remain different operations. A spelling that is not a qualified name, such as a dynamic `handlers[name](x)`, yields the statement text, matches no declaration and stays ineligible exactly as before. The recorded-veto report reads the same helper rather than repeating the extraction.
|
||||||
- [ ] 12.2 Supply per-family context producers for dominance and configuration
|
- [ ] 12.2 Supply per-family context producers for dominance and configuration
|
||||||
- The M1b exit run left 231 of 436 head questions reporting `change_context_incomplete` from `context_projection_unavailable` and `context_scope_empty`, because those families' queries export no context. They did not block the Node injection case, so M1b correctly left them alone, but their families stay experimental until they can answer.
|
- The M1b exit run left 231 of 436 head questions reporting `change_context_incomplete` from `context_projection_unavailable` and `context_scope_empty`, because those families' queries export no context. They did not block the Node injection case, so M1b correctly left them alone, but their families stay experimental until they can answer.
|
||||||
- Give dominance and configuration their own context producers describing the actual scope of the query they accompany. Do not copy taint context into them: dominance evaluates sibling operations across a file and configuration has different scope and obligations.
|
- Give dominance and configuration their own context producers describing the actual scope of the query they accompany. Do not copy taint context into them: dominance evaluates sibling operations across a file and configuration has different scope and obligations.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from dataclasses import asdict, dataclass, replace
|
from dataclasses import asdict, dataclass, replace
|
||||||
@@ -363,6 +364,30 @@ def _blocking_degradations(scan: ModelScanResult) -> tuple[str, ...]:
|
|||||||
return tuple(str(d.get("reason")) for d in scan.degradations if str(d.get("reason")) not in DECLARED_EXCLUSION_GAPS)
|
return tuple(str(d.get("reason")) for d in scan.degradations if str(d.get("reason")) not in DECLARED_EXCLUSION_GAPS)
|
||||||
|
|
||||||
|
|
||||||
|
# A qualified callee: an identifier, optionally reached through `.`, `->` or `::`. Receiver
|
||||||
|
# spelling is part of the symbol, so `$wpdb->prepare` and `prepare` stay different operations.
|
||||||
|
_CALLEE = re.compile(r"[A-Za-z_$\\][\w$\\]*(?:\s*(?:\.|->|::)\s*[A-Za-z_$\\][\w$\\]*)*\s*$")
|
||||||
|
|
||||||
|
|
||||||
|
def operation_symbol(operation: str) -> str:
|
||||||
|
"""The operation a declaration can name, taken from the call rather than the statement.
|
||||||
|
|
||||||
|
Everything before the first parenthesis is the statement, not the operation: for
|
||||||
|
`const preTax = eval(req.body.preTax)` that is `const preTax = eval`, and a declaration
|
||||||
|
naming `eval` would not match it. No real declaration can enumerate every assignment a
|
||||||
|
codebase might write, so admission rejected genuine findings on `operation_semantics_mismatch`.
|
||||||
|
|
||||||
|
Only the trailing qualified callee is the symbol. A spelling that is not a qualified name,
|
||||||
|
such as a dynamic `handlers[name](x)`, yields the statement text unchanged, which matches no
|
||||||
|
declaration and so stays ineligible exactly as it did before.
|
||||||
|
"""
|
||||||
|
prefix = operation.partition("(")[0].strip()
|
||||||
|
found = _CALLEE.search(prefix)
|
||||||
|
if found is None:
|
||||||
|
return prefix
|
||||||
|
return re.sub(r"\s*(\.|->|::)\s*", r"\1", found.group(0).strip())
|
||||||
|
|
||||||
|
|
||||||
def _owning_question(gap: str) -> str | None:
|
def _owning_question(gap: str) -> str | None:
|
||||||
"""The question id a gap is attributed to, or None when nothing attributed it."""
|
"""The question id a gap is attributed to, or None when nothing attributed it."""
|
||||||
tail = gap.rsplit(":", 1)[-1]
|
tail = gap.rsplit(":", 1)[-1]
|
||||||
@@ -902,7 +927,7 @@ def admit_candidates(candidates: Sequence[AdmissionCandidate], *, capabilities:
|
|||||||
if cap is not None:
|
if cap is not None:
|
||||||
# These are existing graph operation spellings, not source scanning.
|
# These are existing graph operation spellings, not source scanning.
|
||||||
# A declaration may narrow an entailed claim; it cannot create one.
|
# A declaration may narrow an entailed claim; it cannot create one.
|
||||||
symbol = op.operation.partition("(")[0].strip()
|
symbol = operation_symbol(op.operation)
|
||||||
if symbol not in cap.operation_symbols:
|
if symbol not in cap.operation_symbols:
|
||||||
reasons.append("operation_semantics_mismatch")
|
reasons.append("operation_semantics_mismatch")
|
||||||
consequence = _grounded_template(cap.consequence_template, op, key.context)
|
consequence = _grounded_template(cap.consequence_template, op, key.context)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from openultrasast.push.policy import (
|
|||||||
_grounded_template,
|
_grounded_template,
|
||||||
admit_candidates,
|
admit_candidates,
|
||||||
operation_provenance,
|
operation_provenance,
|
||||||
|
operation_symbol,
|
||||||
)
|
)
|
||||||
|
|
||||||
FLAG = "--experimental-record-vetoes"
|
FLAG = "--experimental-record-vetoes"
|
||||||
@@ -369,7 +370,7 @@ def _evaluation(delta: CandidateDelta, declarations: Sequence[CapabilityAdmissio
|
|||||||
return None
|
return None
|
||||||
key = CapabilityKey(op.question.language, "unspecified", "unspecified", delta.family, op.mechanism, "unreviewed", semantics)
|
key = CapabilityKey(op.question.language, "unspecified", "unspecified", delta.family, op.mechanism, "unreviewed", semantics)
|
||||||
matching = [declaration for declaration in declarations if declaration.key == key]
|
matching = [declaration for declaration in declarations if declaration.key == key]
|
||||||
symbol = op.operation.partition("(")[0].strip()
|
symbol = operation_symbol(op.operation)
|
||||||
unavailable: list[str] = []
|
unavailable: list[str] = []
|
||||||
consequence = repair = None
|
consequence = repair = None
|
||||||
declaration = matching[0] if len(matching) == 1 else None
|
declaration = matching[0] if len(matching) == 1 else None
|
||||||
|
|||||||
@@ -231,3 +231,35 @@ def test_an_operation_the_change_introduced_is_a_supported_change_basis():
|
|||||||
invented = replace(candidate, delta=replace(candidate.delta, reason="operation_moved_within_change"))
|
invented = replace(candidate, delta=replace(candidate.delta, reason="operation_moved_within_change"))
|
||||||
disposition = admit_candidates((invented,), capabilities=(capability,)).dispositions[0]
|
disposition = admit_candidates((invented,), capabilities=(capability,)).dispositions[0]
|
||||||
assert not disposition.admitted and "change_unsupported" in disposition.reasons
|
assert not disposition.admitted and "change_unsupported" in disposition.reasons
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_operation_symbol_comes_from_the_call_not_the_statement():
|
||||||
|
"""M1b 12.1: a declaration cannot enumerate every assignment a codebase might write."""
|
||||||
|
from openultrasast.push.policy import operation_symbol
|
||||||
|
|
||||||
|
assert operation_symbol("eval(req.body.preTax)") == "eval"
|
||||||
|
assert operation_symbol("const preTax = eval(req.body.preTax)") == "eval"
|
||||||
|
assert operation_symbol("return os.system(full)") == "os.system"
|
||||||
|
# Receiver spelling is part of the symbol: these are different operations.
|
||||||
|
assert operation_symbol("$result = $wpdb->query($sql)") == "$wpdb->query"
|
||||||
|
assert operation_symbol("$wpdb->prepare($sql)") != operation_symbol("prepare($sql)")
|
||||||
|
# A spelling that is not a qualified name matches no declaration and stays ineligible.
|
||||||
|
assert operation_symbol("handlers[name](x)") == "handlers[name]"
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_declaration_naming_the_call_now_covers_an_assigned_call():
|
||||||
|
candidate, capability, _ = control()
|
||||||
|
assigned = replace(
|
||||||
|
candidate,
|
||||||
|
delta=replace(
|
||||||
|
candidate.delta,
|
||||||
|
head_operation=replace(candidate.delta.head_operation, operation="const out = exec(command)"),
|
||||||
|
site="handler.src:4:handler",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
covered = replace(capability, operation_symbols=("exec",))
|
||||||
|
disposition = admit_candidates((assigned,), capabilities=(covered,)).dispositions[0]
|
||||||
|
assert "operation_semantics_mismatch" not in disposition.reasons
|
||||||
|
# A declaration for a different operation still does not cover it.
|
||||||
|
other = replace(capability, operation_symbols=("query",))
|
||||||
|
assert "operation_semantics_mismatch" in admit_candidates((assigned,), capabilities=(other,)).dispositions[0].reasons
|
||||||
|
|||||||
Reference in New Issue
Block a user