mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
Closing a case is the oldest governed act here and the one an obligation rests on directly (AMLR Art. 69(2)): the retained assessment is what a supervisor reads. Its decider was bound to the credential with the rest of them, and this adds what was missing — the tests that say so, and the mutation that says the tests can fail. Two properties, both already true and neither proven: the retained assessment is signed by the token's subject and not by the `by` a request body carried, and a credential that names nobody cannot close a case at all. The second refuses at the assessment, which is the record that matters, and it refuses as a REFUSAL (400) rather than as an outage — a 503 reads as "retry", and this one never clears. The console still renders a "by" field on its resolve form. It is now ignored, which is the point, but a form asking for something the API discards is a lie to the analyst filling it in: ui/ owes a one-field deletion. That repo is another track's. Co-authored-by: Hanzo Dev <[email protected]>
280 lines
13 KiB
Python
280 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Mutation harness: break one guard, prove a test goes red for the right reason.
|
|
|
|
Reports per mutation: ANCHOR MISS (not applied -> not a kill), COMPILE FAIL
|
|
(not a kill), SURVIVED (no test noticed -> the property is unproven), or KILLED.
|
|
"""
|
|
import subprocess, sys, os, shutil
|
|
|
|
# The module root, one level above this script, so the harness runs from anywhere.
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
ENV = dict(os.environ, TMPDIR="/tmp", GOWORK="off",
|
|
PATH="/usr/local/go/bin:" + os.environ["PATH"])
|
|
|
|
# (name, file, old, new, package, test regex, property)
|
|
M = [
|
|
("usable-gate", "pkg/anomaly/anomaly.go",
|
|
"\tif !p.usable() {\n\t\tm.refused[ReasonUnusable]++", "\tif false {\n\t\tm.refused[ReasonUnusable]++",
|
|
"./pkg/anomaly/", "TestUnusablePointsAreRefused",
|
|
"a coordinate outside the unit cube is refused, not learned"),
|
|
|
|
("warm-gate", "pkg/anomaly/anomaly.go",
|
|
"\twarm := m.learned >= int64(s.cfg.Appetite.Warm)", "\twarm := true",
|
|
"./pkg/anomaly/", "TestColdModelRefusesToScore",
|
|
"a model with no reference says nothing"),
|
|
|
|
("cut-none", "pkg/anomaly/anomaly.go",
|
|
"\t\tif acc > budget {\n\t\t\treturn float64(i+1) / float64(len(hist))",
|
|
"\t\tif acc > budget {\n\t\t\treturn 0",
|
|
"./pkg/anomaly/", "TestAppetiteGovernsTheAlertRate",
|
|
"the appetite bounds the alert rate"),
|
|
|
|
("cut-lower-edge", "pkg/anomaly/anomaly.go",
|
|
"return float64(i+1) / float64(len(hist))", "return float64(i) / float64(len(hist))",
|
|
"./pkg/anomaly/", "TestThresholdCannotAdmitMoreThanTheAppetite",
|
|
"the realised rate stays at or below the stated one"),
|
|
|
|
("shadow-acts", "pkg/anomaly/anomaly.go",
|
|
"\t\ta.Alert = !s.cfg.Shadow", "\t\ta.Alert = true",
|
|
"./pkg/anomaly/", "TestShadowScoresWithoutActing",
|
|
"shadow scores without contributing"),
|
|
|
|
("shared-geometry", "pkg/anomaly/anomaly.go",
|
|
"\tz := seed ^ h", "\tz := seed",
|
|
"./pkg/anomaly/", "TestTenantsAreIsolated",
|
|
"no two tenants share a tree geometry"),
|
|
|
|
("shared-model", "pkg/anomaly/anomaly.go",
|
|
"func (s *Store) model(orgID string) *model {",
|
|
"func (s *Store) model(orgID string) *model {\n\torgID = \"\"",
|
|
"./pkg/anomaly/", "TestTenantsAreIsolated",
|
|
"one tenant's traffic cannot move another's score"),
|
|
|
|
("mass-invariant", "pkg/anomaly/forest.go",
|
|
"func (t *tree) sound(depth int) bool {", "func (t *tree) sound(depth int) bool {\n\treturn true",
|
|
"./pkg/anomaly/", "TestSnapshotRoundTripsAndRestoreRejectsBentState",
|
|
"a bent mass array is rejected on restore"),
|
|
|
|
("digest-check", "pkg/anomaly/anomaly.go",
|
|
"\tcase snap.Digest != s.Digest():", "\tcase false:",
|
|
"./pkg/anomaly/", "TestSnapshotRoundTripsAndRestoreRejectsBentState",
|
|
"state from another model shape is rejected"),
|
|
|
|
("counterfactual", "pkg/anomaly/anomaly.go",
|
|
"\t\twithout[i] = m.score(x[:], cfg)", "\t\twithout[i] = score",
|
|
"./pkg/anomaly/", "TestAttributionIsACounterfactualOnTheModel",
|
|
"attribution is a counterfactual on the model, measured not asserted"),
|
|
|
|
("per-tree-clamp", "pkg/anomaly/anomaly.go",
|
|
"\t\tif mass := t.mass(x, cfg.Depth, 0.1*scale); mass < scale {\n\t\t\tvote += 1 - mass/scale\n\t\t}",
|
|
"\t\tvote += t.mass(x, cfg.Depth, 0.1*scale) / scale",
|
|
"./pkg/anomaly/", "TestStructuringAlertsAndOrdinaryTrafficDoesNot",
|
|
"no single tree can swamp the forest's vote"),
|
|
|
|
("ratio-guard", "pkg/anomaly/feature.go",
|
|
"\tcase math.IsNaN(r), r <= 0:\n\t\treturn 0\n\tcase math.IsInf(r, 1):\n\t\treturn 1\n\t}",
|
|
"\tcase false:\n\t\treturn 0\n\t}",
|
|
"./pkg/anomaly/", "TestProjectNeverEmitsAnUnusablePoint",
|
|
"no arithmetic path emits a coordinate the trees cannot take"),
|
|
|
|
("window-check", "pkg/anomaly/anomaly.go",
|
|
"\t\tif f.Window != \"\" && !kept[f.Window] {", "\t\tif false {",
|
|
"./pkg/anomaly/", "TestMissingWindowsFailAtConstruction",
|
|
"a store missing the windows the inventory reads fails at construction"),
|
|
|
|
("ceiling-check", "pkg/anomaly/anomaly.go",
|
|
"\tif types.ActionRank(cfg.Action) > types.ActionRank(types.ActionCeiling) {", "\tif false {",
|
|
"./pkg/anomaly/", "TestActionAboveTheCeilingIsRefused",
|
|
"the model cannot be configured to act"),
|
|
|
|
("subject-check", "pkg/anomaly/anomaly.go",
|
|
"\tif account(tx) == \"\" {", "\tif false {",
|
|
"./pkg/anomaly/", "TestUnidentifiedSubjectIsRefused",
|
|
"a transaction naming no subject is refused"),
|
|
|
|
("inspect-learns", "pkg/anomaly/anomaly.go",
|
|
"\treturn s.judge(tx, false)", "\treturn s.judge(tx, true)",
|
|
"./pkg/anomaly/", "TestInspectDoesNotMutate",
|
|
"testing a candidate mutates nothing"),
|
|
|
|
("active-days", "pkg/anomaly/feature.go",
|
|
"\trate := over(float64(month.Count-1), month.Days)", "\trate := over(float64(month.Count-1), 30)",
|
|
"./pkg/anomaly/", "TestOccasionalCustomerIsNotFlaggedForTransactingAtAll",
|
|
"a rate is per active day, not per calendar day"),
|
|
|
|
("self-baseline", "pkg/anomaly/feature.go",
|
|
"\tperTx := over(month.Sum-usd, month.Count-1)", "\tperTx := over(month.Sum, month.Count)",
|
|
"./pkg/anomaly/", "TestNoBaselineIncludesTheTransactionBeingScored",
|
|
"nothing is measured against a baseline containing itself"),
|
|
|
|
("blind-count", "pkg/anomaly/anomaly.go",
|
|
"\t\t\tm.blind[i]++", "\t\t\t_ = i",
|
|
"./pkg/anomaly/", "TestBlindFeaturesAreCounted",
|
|
"a feature with no data is reported blind"),
|
|
|
|
("sample-rate", "pkg/anomaly/anomaly.go",
|
|
"\tif rate <= 0 || txID == \"\" {", "\tif false {",
|
|
"./pkg/anomaly/", "TestBelowTheLineSampleIsRetainedAndReproducible",
|
|
"below-the-line selection honours its rate"),
|
|
|
|
("roll-keeps-cur", "pkg/anomaly/forest.go",
|
|
"\t\tt.cur[i] = 0", "\t\t_ = i",
|
|
"./pkg/anomaly/", "TestModelForgetsWhatTheTenantStoppedDoing",
|
|
"a closed window starts empty, so the model forgets"),
|
|
|
|
("learn-leaf-only", "pkg/anomaly/forest.go",
|
|
"\t\tt.cur[i]++\n\t\tif at == depth {", "\t\tif at == depth {\n\t\t\tt.cur[i]++",
|
|
"./pkg/anomaly/", "TestConcurrentAssessKeepsTheMassesSound",
|
|
"every region on the path records the point"),
|
|
|
|
("seed-disclosure", "pkg/anomaly/anomaly.go",
|
|
"\tc := s.cfg\n\tc.Seed = 0\n\treturn c", "\treturn s.cfg",
|
|
"./pkg/anomaly/", "TestStateReportsWhatGovernanceReads",
|
|
"the tree seed does not travel in a governance payload"),
|
|
|
|
# engine-side confinement
|
|
("scorer-recover", "pkg/engine/engine.go",
|
|
"\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\te.faults.Add(1)\n\t\t\thit, ok = types.RuleHit{}, false\n\t\t}\n\t}()",
|
|
"",
|
|
"./pkg/engine/", "TestPanickingScorerDoesNotLoseTheRulesVerdict",
|
|
"a fault in the model plane does not take the rule plane down"),
|
|
|
|
("scorer-negative", "pkg/engine/engine.go",
|
|
"\tif hit.Rule.Weight < 0 || math.IsNaN(hit.Rule.Weight) || math.IsInf(hit.Rule.Weight, 0) {",
|
|
"\tif math.IsNaN(hit.Rule.Weight) || math.IsInf(hit.Rule.Weight, 0) {",
|
|
"./pkg/engine/", "TestScorerCannotWeakenAVerdict",
|
|
"a negative weight cannot subtract from what the rules found"),
|
|
|
|
("scorer-infinite", "pkg/engine/engine.go",
|
|
"\tif hit.Rule.Weight < 0 || math.IsNaN(hit.Rule.Weight) || math.IsInf(hit.Rule.Weight, 0) {",
|
|
"\tif hit.Rule.Weight < 0 || math.IsNaN(hit.Rule.Weight) {",
|
|
"./pkg/engine/", "TestScorerCannotWeakenAVerdict",
|
|
"an infinite weight cannot saturate the score"),
|
|
|
|
("scorer-cap", "pkg/engine/engine.go",
|
|
"\tif types.ActionRank(hit.Rule.Action) > types.ActionRank(types.ActionCeiling) {",
|
|
"\tif false {",
|
|
"./pkg/engine/", "TestScorerActionIsCappedAtTheCeiling",
|
|
"the model's action is capped at a review"),
|
|
|
|
("clamp-nan", "pkg/engine/scoring.go",
|
|
"\tif math.IsNaN(v) {\n\t\treturn 1\n\t}", "\tif math.IsNaN(v) {\n\t\treturn v\n\t}",
|
|
"./pkg/engine/", "TestScoreClampHandlesNotANumber",
|
|
"a score that is not a number never reaches a caller"),
|
|
|
|
("causes-dropped", "pkg/engine/engine.go",
|
|
"\t\t\tCauses: h.Causes,", "",
|
|
"./pkg/engine/", "TestCausesReachTheAlert",
|
|
"attribution survives into the alert an investigator opens"),
|
|
|
|
("scorer-ignored", "pkg/engine/engine.go",
|
|
"\tif hit, ok := e.assess(tx, ent); ok {\n\t\thits = append(hits, hit)\n\t}", "",
|
|
"./pkg/engine/", "TestScorerOnlyAdds",
|
|
"the model's evidence reaches the score at all"),
|
|
|
|
# ---- The risk-planes hardening. One mutation per finding, so a fix that lost
|
|
# its test says so here rather than in the next review.
|
|
|
|
("study-ungated", "pkg/api/gate.go",
|
|
"\tif g.in[tenant] {\n\t\treturn false\n\t}", "\tif false {\n\t\treturn false\n\t}",
|
|
"./pkg/api/", "TestOneRefusesTheSecondStudyAndFreesTheSlot|TestOnePerTenant",
|
|
"a tenant gets one model study at a time"),
|
|
|
|
("workers-unclamped", "pkg/topology/search.go",
|
|
"\tif asked > MaxWorkers {\n\t\tasked = MaxWorkers\n\t}", "",
|
|
"./pkg/topology/", "TestWorkersAreClamped",
|
|
"a caller cannot name the width of its own study"),
|
|
|
|
("decider-from-the-body", "pkg/api/typed.go",
|
|
"\t\t// And the decider is written after both, from the credential.\n\t\tdecide(&in, who.Subject)", "",
|
|
"./pkg/api/", "TestTheDeciderIsTheCredentialAndNotTheBody",
|
|
"a governed record names the verified subject, not the request body"),
|
|
|
|
("case-decider-from-the-body", "pkg/api/routes.go",
|
|
"\t\t// The decider, from the credential and after the body, exactly as the typed\n\t\t// adapters do it for the record planes.\n\t\tdecide(&in, who.Subject)", "\t\t_ = who",
|
|
"./pkg/api/", "TestClosingACaseNamesTheCredential",
|
|
"the retained assessment is signed by the credential, not the request body"),
|
|
|
|
("crowding-unbounded", "pkg/suppress/shelf.go",
|
|
"\tif inForce >= s.maxInForce() {", "\tif false {",
|
|
"./pkg/suppress/", "TestCrowdingIsRefusedWhereItCostsARequest",
|
|
"crowding is refused where a refusal costs a request, not a payment"),
|
|
|
|
("cover-silently-partial", "pkg/suppress/shelf.go",
|
|
"\t\trows, out.Partial = rows[:s.maxCandidates()], true", "\t\trows = rows[:s.maxCandidates()]",
|
|
"./pkg/suppress/", "TestACrowdedCoverDegradesAndSaysSo",
|
|
"an incomplete cover check is marked rather than absorbed"),
|
|
|
|
("firing-not-identified", "pkg/watch/shelf.go",
|
|
"\tif a.Tx != \"\" {\n\t\ta.ID = firing(org, a.Tx, rule, subj)", "\tif false {\n\t\ta.ID = firing(org, a.Tx, rule, subj)",
|
|
"./pkg/watch/", "TestARetriedFiringIsOneActivation|TestARetryDoesNotFoldAgainstItself",
|
|
"one firing is one activation however often it is offered"),
|
|
|
|
("record-unnameable", "pkg/retention/record.go",
|
|
"\tif r.Fingerprint != \"\" {", "\tif false {",
|
|
"./pkg/api/", "TestARetriedTransactionIsCountedOnce",
|
|
"a retried transaction is the same retained fact, not a conflict"),
|
|
|
|
("rung-unbounded", "pkg/watch/shelf.go",
|
|
"\tcase in.Count > MaxCount:", "\tcase false:",
|
|
"./pkg/watch/", "TestARungIsBounded",
|
|
"a rung may not declare a count that becomes an unbounded read"),
|
|
|
|
("streak-unclamped", "pkg/watch/shelf.go",
|
|
"\tif deepest > MaxCount {\n\t\tdeepest = MaxCount\n\t}", "",
|
|
"./pkg/watch/", "TestTheStreakReadIsBoundedWhateverTheRowsSay",
|
|
"the ingest-path read is bounded whatever a stored rung says"),
|
|
|
|
("custom-number-stored", "pkg/dictionary/shelf.go",
|
|
"\t\tif s.origin == Declared {", "\t\tif true {",
|
|
"./pkg/dictionary/", "TestACustomNumberIsNeverStored",
|
|
"no payload value is ever stored, numbers included"),
|
|
|
|
("vocabulary-unbounded", "pkg/dictionary/shelf.go",
|
|
"\t\tif c.names >= room {", "\t\tif false {",
|
|
"./pkg/dictionary/", "TestAVocabularyIsBoundedPerTenant",
|
|
"a tenant's payload vocabulary is bounded per tenant"),
|
|
|
|
("store-vocabulary-unbounded", "pkg/dictionary/shelf.go",
|
|
"\t\t\t\t\tif room <= 0 {", "\t\t\t\t\tif false {",
|
|
"./pkg/dictionary/", "TestAVocabularyIsBoundedPerTenant",
|
|
"the STORED vocabulary is bounded too, not only the accumulator"),
|
|
|
|
("adoption-not-reloaded", "pkg/anomaly/anomaly.go",
|
|
"\tif adopted != nil {", "\tif false {",
|
|
"./pkg/models/", "TestAnAdoptedControlDoesNotGoQuietOnItsOwn",
|
|
"an adopted control comes back after a rollout or an eviction"),
|
|
]
|
|
|
|
def run(cmd, timeout=420):
|
|
return subprocess.run(cmd, cwd=ROOT, env=ENV, capture_output=True, text=True, timeout=timeout)
|
|
|
|
results = []
|
|
for name, path, old, new, pkg, tests, prop in M:
|
|
full = os.path.join(ROOT, path)
|
|
orig = open(full).read()
|
|
if old not in orig:
|
|
results.append((name, "ANCHOR MISS", prop)); continue
|
|
if orig.count(old) > 1 and name != "cut-lower-edge":
|
|
results.append((name, "ANCHOR AMBIGUOUS x%d" % orig.count(old), prop)); continue
|
|
shutil.copy(full, full + ".bak")
|
|
try:
|
|
open(full, "w").write(orig.replace(old, new, 1))
|
|
b = run(["go", "build", pkg])
|
|
if b.returncode != 0:
|
|
results.append((name, "COMPILE FAIL", prop)); continue
|
|
t = run(["go", "test", "-count=1", "-run", tests, pkg])
|
|
killed = t.returncode != 0
|
|
results.append((name, "KILLED" if killed else "SURVIVED", prop))
|
|
finally:
|
|
shutil.move(full + ".bak", full)
|
|
|
|
w = max(len(n) for n, _, _ in results)
|
|
for name, verdict, prop in results:
|
|
print(f"{name.ljust(w)} {verdict.ljust(20)} {prop}")
|
|
bad = [r for r in results if r[1] != "KILLED"]
|
|
print(f"\n{len(results)-len(bad)}/{len(results)} killed")
|
|
for name, verdict, prop in bad:
|
|
print(f" NOT KILLED: {name} ({verdict})")
|
|
sys.exit(1 if bad else 0)
|