Mutate guards by what they DO, not just what they are about

The generator selected lines by an auth vocabulary — token, scope, expire,
password. src/configFile.ts, whose validators are the API's only defence
against a nonsense value being written permanently into an encrypted file,
generated ZERO mutants: `if (n <= 0) throw new ConfigValueError(...)` is a
refusal in every meaningful sense and contains not one word that list knows.
The NOT MEASURED reporting added earlier is what surfaced it, on its first
run against a new target.

A guard whose body refuses is now selected on that basis, vocabulary or not.
That takes the corpus from 132 mutants to 234 and it is the right trade: a
refusal is the security surface, definitionally.

It immediately found three gaps in the config validators written in the
previous commit:

- The `||` chain rejecting non-numbers was only tested with a string.
  Number(true) is 1 and Number(null) is 0, so `inactivity_timeout: true`
  became a one-second timeout and `clipboard_clear_delay: null` became zero —
  a setting silently changed to something nobody asked for.
- `n < 0` versus `n <= 0` on the non-negative fields: zero was never tested,
  and it is a legitimate value that must not be conflated with negative.
- The `kdf_iterations` floor had no test at all. Zero iterations is not a
  slow KDF, it is no KDF, in the config of a vault whose master password
  protects everything else.

configFile.ts is registered as a mutation target so it stays measured.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
This commit is contained in:
thePR0M3TH3ANandClaude Opus 5 committed 2026-08-19 20:10:31 -04:00
1 parent 33aeeead9c
commit 9c9544b725
2 files changed
+52 -3

No files matched your search

+23 -3
View File
@@ -94,6 +94,10 @@ TARGETS: dict[str, tuple[str, list[str]]] = {
"highrisk": ("src/highRisk.ts", ["test/highRisk.test.ts"]),
"sinks": ("src/sinks.ts", ["test/sinks.test.ts", "test/tokens.test.ts"]),
"approvals": ("src/approvals.ts", ["test/highRisk.test.ts"]),
# The config normalizers are the API's only defence against a well-typed
# nonsense value being stored permanently in an encrypted file nobody
# reads back by hand.
"config": ("src/configFile.ts", ["test/api.test.ts"]),
}
# Operator swaps that change a decision rather than a value's shape.
@@ -124,6 +128,13 @@ SECURITY_HINTS = re.compile(
re.IGNORECASE,
)
# A guard whose body REFUSES is security-relevant whether or not it happens to
# use the vocabulary above. src/configFile.ts generated zero mutants because
# its validators say `if (n <= 0) throw new ConfigValueError(...)` -- refusals
# in every meaningful sense, containing not one word the hint list knows. This
# catches them by what the line DOES rather than what it is about.
REFUSAL = re.compile(r"\bthrow new \w*(Error|Exception)\b|\breturn \{ ?ok: false")
SKIP_LINE = re.compile(r"^\s*(//|\*|/\*)")
# A line preceded by this marker has been triaged as an EQUIVALENT mutant:
@@ -157,7 +168,16 @@ def generate(source: str) -> list[Mutant]:
mutants: list[Mutant] = []
lines = source.splitlines()
for i, line in enumerate(lines, start=1):
if SKIP_LINE.match(line) or not SECURITY_HINTS.search(line):
if SKIP_LINE.match(line):
continue
# A block-form guard puts its refusal on a following line, so look
# ahead a little rather than only at the condition itself.
block_refuses = any(REFUSAL.search(l) for l in lines[i : i + 3])
if not (
SECURITY_HINTS.search(line)
or REFUSAL.search(line)
or (line.rstrip().endswith("{") and block_refuses)
):
continue
# Skip lines a human has already judged unkillable, with a reason.
# Scans the whole contiguous comment block above, not just the line
@@ -180,7 +200,7 @@ def generate(source: str) -> list[Mutant]:
# entirely, that check is unverified.
# Block form: `if (cond) {`
guard = re.match(r"^(\s*)if \((.+)\) \{\s*$", line)
if guard and SECURITY_HINTS.search(guard.group(2)):
if guard and (SECURITY_HINTS.search(guard.group(2)) or block_refuses):
mutants.append(
Mutant(
i,
@@ -201,7 +221,7 @@ def generate(source: str) -> list[Mutant]:
if (
inline
and not line.rstrip().endswith("{")
and SECURITY_HINTS.search(inline.group(2))
and (SECURITY_HINTS.search(inline.group(2)) or REFUSAL.search(inline.group(3)))
):
mutants.append(
Mutant(