tamperward 2.23.20 → 2.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/cli/index.js +290 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -626,11 +626,11 @@ option can never be reinterpreted as the agent command.
|
|
|
626
626
|
|
|
627
627
|
### The rules
|
|
628
628
|
|
|
629
|
-
|
|
629
|
+
Nineteen rules are specified and eighteen ship (see the table in
|
|
630
630
|
[SPEC.md](./SPEC.md)). The families: test protection (`test-deletion`,
|
|
631
631
|
`test-skip`, `test-content-removal`, plus the warning-only JS/TS
|
|
632
632
|
`assertion-weakening` heuristic), verification-signal protection
|
|
633
|
-
(`coverage-lowering`, `snapshot-rewrite`, `snapshot-only-rewrite`), suppression
|
|
633
|
+
(`coverage-lowering`, `coverage-exclusion`, `snapshot-rewrite`, `snapshot-only-rewrite`), suppression
|
|
634
634
|
(`ts-any-cast`, `ts-any-launder`, `ts-cast-growth`, `lint-suppression`), pipeline protection
|
|
635
635
|
(`ci-tampering`, `hook-tampering`, `no-verify`), and the effect/outcome layers
|
|
636
636
|
(`transient-protected-mutation`, `pristine-verification`, plus the `run` envelope).
|
|
@@ -653,6 +653,13 @@ change removed, reported as a warning that never blocks by default. It fires on
|
|
|
653
653
|
of legitimate mainline pairs across four real TypeScript libraries
|
|
654
654
|
(`harness/fp-study/CAST-GROWTH-CORPUS.md`), so block is closed by corpus; the same
|
|
655
655
|
pass removed every assertion from TamperWard's own `src/` (`docs/CAST-INVENTORY.md`).
|
|
656
|
+
`coverage-exclusion` (2.24.0) is the per-function form of `coverage-lowering`: an
|
|
657
|
+
inline `istanbul ignore` / `c8 ignore` / `v8 ignore` / `node:coverage ignore` /
|
|
658
|
+
`# pragma: no cover` / `#[coverage(off)]` added to non-test source, or a `//go:build`
|
|
659
|
+
constraint added to an existing Go file. It fired on 0 of the same 460 mainline
|
|
660
|
+
pairs, but every real add in the deeper histories is an honest unreachable-branch
|
|
661
|
+
marker (`harness/fp-study/COVERAGE-EXCLUSION-CORPUS.md`), so it warns and never
|
|
662
|
+
blocks by default.
|
|
656
663
|
|
|
657
664
|
## What Tamperward does not do
|
|
658
665
|
|
package/dist/cli/index.js
CHANGED
|
@@ -571,6 +571,12 @@ function defaultPolicy(version = 1) {
|
|
|
571
571
|
"ts-cast-growth": { severity: "warn" },
|
|
572
572
|
"lint-suppression": { severity: "block" },
|
|
573
573
|
"coverage-lowering": { severity: "block" },
|
|
574
|
+
// the per-function form of coverage-lowering: an inline `istanbul ignore` /
|
|
575
|
+
// `c8 ignore` / `v8 ignore` / `node:coverage ignore` / `# pragma: no cover` /
|
|
576
|
+
// `#[coverage(off)]` added to non-test source (#438). Also the ordinary way to
|
|
577
|
+
// mark a genuinely unreachable branch, so it WARNs until the corpus in
|
|
578
|
+
// harness/fp-study/coverage-exclusion-corpus.json earns block (SPEC §7).
|
|
579
|
+
"coverage-exclusion": { severity: "warn" },
|
|
574
580
|
"ci-tampering": { severity: "block" },
|
|
575
581
|
"hook-tampering": { severity: "block" },
|
|
576
582
|
"no-verify": { severity: "block" },
|
|
@@ -4590,7 +4596,7 @@ function literals(e) {
|
|
|
4590
4596
|
return { items, opaque };
|
|
4591
4597
|
}
|
|
4592
4598
|
function parseLists(src) {
|
|
4593
|
-
const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], opaqueCollect: false, opaqueIgnore: false };
|
|
4599
|
+
const res = { collectFrom: null, collectKey: "collectCoverageFrom", ignore: [], ignoreRe: [], opaqueCollect: false, opaqueIgnore: false };
|
|
4594
4600
|
try {
|
|
4595
4601
|
const sf = sourceOf(src);
|
|
4596
4602
|
if (!sf) return res;
|
|
@@ -4606,7 +4612,7 @@ function parseLists(src) {
|
|
|
4606
4612
|
if (opaque) res.opaqueCollect = true;
|
|
4607
4613
|
} else if (k === "coveragePathIgnorePatterns" || k === "exclude" && vitestCoverage) {
|
|
4608
4614
|
const { items, opaque } = literals(resolve16(node.initializer));
|
|
4609
|
-
res.ignore.push(...items);
|
|
4615
|
+
(k === "exclude" ? res.ignore : res.ignoreRe).push(...items);
|
|
4610
4616
|
if (opaque) res.opaqueIgnore = true;
|
|
4611
4617
|
}
|
|
4612
4618
|
}
|
|
@@ -4628,6 +4634,8 @@ function parseSwitches(src) {
|
|
|
4628
4634
|
const k = keyName(node.name);
|
|
4629
4635
|
const owner = ownerKey(node);
|
|
4630
4636
|
if (k === "enabled" && owner === "coverage") res.enabled = bool2(node.initializer);
|
|
4637
|
+
else if (k === "all" && owner === "coverage") res.all = bool2(node.initializer);
|
|
4638
|
+
else if (k === "collectCoverage" && (owner === null || owner === "jest")) res.collectCoverage = bool2(node.initializer);
|
|
4631
4639
|
else if (k === "perFile" && owner === "thresholds" && underKey(node, "coverage")) res.perFile = bool2(node.initializer);
|
|
4632
4640
|
}
|
|
4633
4641
|
ts.forEachChild(node, visit);
|
|
@@ -4736,13 +4744,19 @@ function narrowings(before, after, sources) {
|
|
|
4736
4744
|
}
|
|
4737
4745
|
const had = new Set(before.ignore);
|
|
4738
4746
|
for (const g of after.ignore) {
|
|
4739
|
-
if (!had.has(g) && !BENIGN_EXEMPTION.test(g)) out3.push(`coverage now exempts ${JSON.stringify(g)} (
|
|
4747
|
+
if (!had.has(g) && !BENIGN_EXEMPTION.test(g)) out3.push(`coverage now exempts ${JSON.stringify(g)} (coverage.exclude)`);
|
|
4748
|
+
}
|
|
4749
|
+
const hadRe = new Set(before.ignoreRe);
|
|
4750
|
+
for (const g of after.ignoreRe) {
|
|
4751
|
+
if (!hadRe.has(g) && !BENIGN_EXEMPTION_RE.test(g)) out3.push(`coverage now exempts ${JSON.stringify(g)} (coveragePathIgnorePatterns)`);
|
|
4740
4752
|
}
|
|
4741
4753
|
return out3;
|
|
4742
4754
|
}
|
|
4743
4755
|
function switchWeakenings(before, after) {
|
|
4744
4756
|
const out3 = [];
|
|
4745
4757
|
if (before.enabled === true && after.enabled === false) out3.push("coverage.enabled switched off \u2014 the thresholds are never checked");
|
|
4758
|
+
if (before.collectCoverage === true && after.collectCoverage === false) out3.push("collectCoverage switched off \u2014 coverage is no longer collected, so the thresholds are never checked");
|
|
4759
|
+
if (before.all === true && after.all === false) out3.push("coverage.all switched off \u2014 a file no test imports is no longer measured");
|
|
4746
4760
|
if (before.perFile === true && after.perFile !== true) out3.push("thresholds.perFile removed \u2014 a single uncovered file now hides behind the aggregate");
|
|
4747
4761
|
return out3;
|
|
4748
4762
|
}
|
|
@@ -4897,7 +4911,49 @@ function gateMovedElsewhere(c, changes, policy) {
|
|
|
4897
4911
|
});
|
|
4898
4912
|
});
|
|
4899
4913
|
}
|
|
4900
|
-
|
|
4914
|
+
function flagWeakenings(c, added, removed, policy) {
|
|
4915
|
+
const out3 = [];
|
|
4916
|
+
const removedCheck = removed.find((l) => CHECK_COVERAGE.test(l.content));
|
|
4917
|
+
if (removedCheck && !added.some((l) => CHECK_COVERAGE.test(l.content))) {
|
|
4918
|
+
out3.push(
|
|
4919
|
+
makeFinding(RULE5, policy, {
|
|
4920
|
+
file: c.path,
|
|
4921
|
+
message: "nyc --check-coverage was removed \u2014 the coverage thresholds are no longer enforced.",
|
|
4922
|
+
evidence: removedCheck.content.trim(),
|
|
4923
|
+
remediation: "Keep --check-coverage on; raise real coverage instead of switching the gate off."
|
|
4924
|
+
})
|
|
4925
|
+
);
|
|
4926
|
+
}
|
|
4927
|
+
const was = floorsOf(removed);
|
|
4928
|
+
const now = floorsOf(added);
|
|
4929
|
+
if (was.length === 0) return out3;
|
|
4930
|
+
const high = was.reduce((a, b) => b.v > a.v ? b : a);
|
|
4931
|
+
if (now.length === 0) {
|
|
4932
|
+
out3.push(
|
|
4933
|
+
makeFinding(RULE5, policy, {
|
|
4934
|
+
file: c.path,
|
|
4935
|
+
message: `--cov-fail-under removed (was ${high.v}) \u2014 the coverage floor is no longer enforced.`,
|
|
4936
|
+
evidence: high.l.content.trim(),
|
|
4937
|
+
remediation: "Keep the --cov-fail-under floor; raise real coverage instead of removing the gate."
|
|
4938
|
+
})
|
|
4939
|
+
);
|
|
4940
|
+
return out3;
|
|
4941
|
+
}
|
|
4942
|
+
const low = now.reduce((a, b) => b.v < a.v ? b : a);
|
|
4943
|
+
if (low.v < high.v) {
|
|
4944
|
+
out3.push(
|
|
4945
|
+
makeFinding(RULE5, policy, {
|
|
4946
|
+
file: c.path,
|
|
4947
|
+
line: low.l.newLine ?? void 0,
|
|
4948
|
+
message: `--cov-fail-under lowered ${high.v} \u2192 ${low.v}.`,
|
|
4949
|
+
evidence: low.l.content.trim(),
|
|
4950
|
+
remediation: "Restore the coverage floor and raise real coverage; do not lower the gate to pass."
|
|
4951
|
+
})
|
|
4952
|
+
);
|
|
4953
|
+
}
|
|
4954
|
+
return out3;
|
|
4955
|
+
}
|
|
4956
|
+
var RULE5, METRICS, isMetric, norm, BENIGN_DIRS, BENIGN_EXEMPTION, BENIGN_EXEMPTION_RE, globs, globList, CANONICAL_SOURCES, NON_SOURCE, NYC_KEYS, COVERAGE_PY, CODECOV, SIMPLE_FILES, lessThan, numOf, SWITCHES, isJsConfig, COV_FAIL_UNDER, CHECK_COVERAGE, floorsOf, coverageLowering;
|
|
4901
4957
|
var init_coverage_lowering = __esm({
|
|
4902
4958
|
"src/detectors/coverage-lowering.ts"() {
|
|
4903
4959
|
"use strict";
|
|
@@ -4912,7 +4968,9 @@ var init_coverage_lowering = __esm({
|
|
|
4912
4968
|
METRICS = ["branches", "functions", "lines", "statements"];
|
|
4913
4969
|
isMetric = (k) => METRICS.some((m) => m === k);
|
|
4914
4970
|
norm = (p) => p.replace(/^\.\//, "");
|
|
4915
|
-
|
|
4971
|
+
BENIGN_DIRS = /node_modules|(?:^|[/!])(?:dist|build|out|coverage|tmp|temp|vendor|test|tests|__tests__|__mocks__|__fixtures__|fixtures?|mocks?|spec|specs|e2e|cypress|playwright|storybook|scripts?|examples?|docs?|bin|types?|typings|generated|__generated__|codegen|migrations?)(?:\/|$|\*)|\.(?:config|setup)\.|(?:^|[/!*])(?:manage|wsgi|asgi|__init__)\.py$|(?:^|[/!*])main\.[jt]sx?$|\.git\b|\.cache|\.next|\.nuxt|\.output|\.idea/i;
|
|
4972
|
+
BENIGN_EXEMPTION = new RegExp(`${BENIGN_DIRS.source}|\\.(?:test|spec|stories|d|gen|generated|pb|e2e-spec)\\.|\\.d\\.ts|\\.json$|\\.md$|\\.css$|\\.s[ac]ss$`, "i");
|
|
4973
|
+
BENIGN_EXEMPTION_RE = new RegExp(`${BENIGN_DIRS.source}|\\\\\\.(?:test|spec|stories|gen|generated|pb|e2e-spec)\\\\\\.|\\\\\\.(?:d\\\\\\.ts|json|md|css|s[ac]ss)\\$?$`, "i");
|
|
4916
4974
|
globs = (patterns) => {
|
|
4917
4975
|
const ms = patterns.map((g) => {
|
|
4918
4976
|
try {
|
|
@@ -4935,12 +4993,12 @@ var init_coverage_lowering = __esm({
|
|
|
4935
4993
|
label: m,
|
|
4936
4994
|
weakens: "lower"
|
|
4937
4995
|
}));
|
|
4938
|
-
COVERAGE_PY = /(?:^|\/)(?:\.coveragerc|pyproject\.toml)$/;
|
|
4996
|
+
COVERAGE_PY = /(?:^|\/)(?:\.coveragerc|pyproject\.toml|setup\.cfg|tox\.ini)$/;
|
|
4939
4997
|
CODECOV = /(?:^|\/)\.?codecov\.yml$/;
|
|
4940
4998
|
SIMPLE_FILES = [
|
|
4941
4999
|
{
|
|
4942
|
-
// coverage.py: `fail_under = 90` under [report] (.coveragerc,
|
|
4943
|
-
// [tool.coverage.report] (pyproject.toml)
|
|
5000
|
+
// coverage.py: `fail_under = 90` under [report] (.coveragerc), [coverage:report]
|
|
5001
|
+
// (setup.cfg, tox.ini) or [tool.coverage.report] (pyproject.toml)
|
|
4944
5002
|
file: COVERAGE_PY,
|
|
4945
5003
|
keys: [{ key: /^\s*fail[_-]under\s*=\s*"?(\d+(?:\.\d+)?)/, label: "fail_under", weakens: "lower" }],
|
|
4946
5004
|
removable: true
|
|
@@ -4999,6 +5057,12 @@ var init_coverage_lowering = __esm({
|
|
|
4999
5057
|
}
|
|
5000
5058
|
];
|
|
5001
5059
|
isJsConfig = (path) => /\.(?:[cm]?[jt]sx?|json)$/.test(path);
|
|
5060
|
+
COV_FAIL_UNDER = /--cov-fail-under(?:=|\s+)(\d+(?:\.\d+)?)/;
|
|
5061
|
+
CHECK_COVERAGE = /--check-coverage\b/;
|
|
5062
|
+
floorsOf = (lines) => lines.flatMap((l) => {
|
|
5063
|
+
const m = l.content.match(COV_FAIL_UNDER);
|
|
5064
|
+
return m ? [{ v: Number(m[1]), l }] : [];
|
|
5065
|
+
});
|
|
5002
5066
|
coverageLowering = {
|
|
5003
5067
|
id: RULE5,
|
|
5004
5068
|
surface: ["file"],
|
|
@@ -5007,7 +5071,10 @@ var init_coverage_lowering = __esm({
|
|
|
5007
5071
|
const out3 = [];
|
|
5008
5072
|
for (const c of changes) {
|
|
5009
5073
|
if (c.kind !== "file") continue;
|
|
5010
|
-
if (!isProtected(c.path, policy, "config"))
|
|
5074
|
+
if (!isProtected(c.path, policy, "config")) {
|
|
5075
|
+
if (isProtected(c.path, policy, "ci")) out3.push(...flagWeakenings(c, addedLines(c), removedLines(c), policy));
|
|
5076
|
+
continue;
|
|
5077
|
+
}
|
|
5011
5078
|
if (c.after != null && c.op !== "delete") {
|
|
5012
5079
|
const reasons = [];
|
|
5013
5080
|
if (isJsConfig(c.path)) {
|
|
@@ -5057,6 +5124,7 @@ var init_coverage_lowering = __esm({
|
|
|
5057
5124
|
})
|
|
5058
5125
|
);
|
|
5059
5126
|
}
|
|
5127
|
+
out3.push(...flagWeakenings(c, added, removed, policy));
|
|
5060
5128
|
for (const l of added) {
|
|
5061
5129
|
for (const s of SWITCHES) {
|
|
5062
5130
|
if (!s.re.test(l.content)) continue;
|
|
@@ -5110,6 +5178,93 @@ var init_coverage_lowering = __esm({
|
|
|
5110
5178
|
}
|
|
5111
5179
|
});
|
|
5112
5180
|
|
|
5181
|
+
// src/detectors/coverage-exclusion.ts
|
|
5182
|
+
function isText(line, idx, lang) {
|
|
5183
|
+
const opener = LINE_COMMENT[lang];
|
|
5184
|
+
let quote = null;
|
|
5185
|
+
for (let j = 0; j < idx; j++) {
|
|
5186
|
+
const ch = line[j];
|
|
5187
|
+
if (quote) {
|
|
5188
|
+
if (ch === "\\") j++;
|
|
5189
|
+
else if (ch === quote) quote = null;
|
|
5190
|
+
continue;
|
|
5191
|
+
}
|
|
5192
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
5193
|
+
else if (opener && opener.test(line.slice(j, j + 2))) return true;
|
|
5194
|
+
}
|
|
5195
|
+
return quote !== null;
|
|
5196
|
+
}
|
|
5197
|
+
var RULE6, PATTERNS3, OUT_OF_SCOPE_PATH, LINE_COMMENT, coverageExclusion;
|
|
5198
|
+
var init_coverage_exclusion = __esm({
|
|
5199
|
+
"src/detectors/coverage-exclusion.ts"() {
|
|
5200
|
+
"use strict";
|
|
5201
|
+
init_select();
|
|
5202
|
+
init_files();
|
|
5203
|
+
init_policy();
|
|
5204
|
+
init_finding();
|
|
5205
|
+
RULE6 = "coverage-exclusion";
|
|
5206
|
+
PATTERNS3 = {
|
|
5207
|
+
js: [
|
|
5208
|
+
{ re: /\/\*\s*istanbul\s+ignore\b/, why: "`istanbul ignore` takes the next statement, branch or the whole file out of the coverage measurement" },
|
|
5209
|
+
{ re: /\/\*\s*c8\s+ignore\b(?!\s*stop\b)/, why: "`c8 ignore` takes the next statement or the marked range out of the coverage measurement" },
|
|
5210
|
+
{ re: /\/\*\s*v8\s+ignore\b(?!\s*stop\b)/, why: "`v8 ignore` takes the next statement or the marked range out of the coverage measurement" },
|
|
5211
|
+
{ re: /\/\*\s*node:coverage\s+(?:ignore|disable)\b/, why: "`node:coverage ignore/disable` takes the following code out of the coverage measurement" }
|
|
5212
|
+
],
|
|
5213
|
+
py: [{ re: /#\s*pragma[:\s]\s*no\s*cover\b/i, why: "`# pragma: no cover` takes the line or block out of the coverage measurement" }],
|
|
5214
|
+
rs: [{ re: /#\s*!?\[[^\]]*\bcoverage\s*\(\s*off\s*\)/, why: "`#[coverage(off)]` takes the item out of the coverage measurement" }],
|
|
5215
|
+
go: [
|
|
5216
|
+
{
|
|
5217
|
+
re: /^\s*\/\/\s*(?:go:build\b|\+build\b)/,
|
|
5218
|
+
why: "a build constraint added to an existing file takes it out of the default build, and out of `go test -cover`",
|
|
5219
|
+
existingOnly: true
|
|
5220
|
+
}
|
|
5221
|
+
]
|
|
5222
|
+
};
|
|
5223
|
+
OUT_OF_SCOPE_PATH = /(?:^|\/)(?:node_modules|vendor|third_party|dist|build|out|coverage|generated|__generated__|codegen|examples?|docs?|scripts?|fixtures?|__fixtures__|__mocks__)\/|\.d\.[cm]?ts$|\.(?:generated|gen|pb)\.[^/]*$/;
|
|
5224
|
+
LINE_COMMENT = { js: /^\/\//, go: /^\/\//, rs: /^\/\//, py: /^#/ };
|
|
5225
|
+
coverageExclusion = {
|
|
5226
|
+
id: RULE6,
|
|
5227
|
+
surface: ["file"],
|
|
5228
|
+
certainty: "mechanical",
|
|
5229
|
+
run(changes, policy) {
|
|
5230
|
+
const out3 = [];
|
|
5231
|
+
for (const c of changes) {
|
|
5232
|
+
if (c.kind !== "file" || c.op === "delete") continue;
|
|
5233
|
+
const lang = langOf(c.path);
|
|
5234
|
+
if (!lang) continue;
|
|
5235
|
+
const patterns = PATTERNS3[lang];
|
|
5236
|
+
if (!patterns) continue;
|
|
5237
|
+
const category = protectedCategory(c.path, policy);
|
|
5238
|
+
if (OUT_OF_SCOPE_PATH.test(c.path) || category === "tests" || category === "config") continue;
|
|
5239
|
+
const removed = new Set(removedLines(c).map((l) => l.content.trim()));
|
|
5240
|
+
const before = c.before ?? "";
|
|
5241
|
+
for (const l of addedLines(c)) {
|
|
5242
|
+
const trimmed = l.content.trim();
|
|
5243
|
+
if (removed.has(trimmed)) continue;
|
|
5244
|
+
for (const p of patterns) {
|
|
5245
|
+
const m = p.re.exec(l.content);
|
|
5246
|
+
if (!m || isText(l.content, m.index, lang)) continue;
|
|
5247
|
+
if (p.existingOnly && (c.op === "add" || c.before == null || before.split("\n").some((b) => p.re.test(b)))) continue;
|
|
5248
|
+
out3.push(
|
|
5249
|
+
makeFinding(RULE6, policy, {
|
|
5250
|
+
file: c.path,
|
|
5251
|
+
line: l.newLine ?? void 0,
|
|
5252
|
+
message: `Coverage exclusion added: ${p.why}.`,
|
|
5253
|
+
evidence: trimmed,
|
|
5254
|
+
remediation: "Cover the code with a test instead of excluding it from the measurement; if it is genuinely unreachable, say so in review.",
|
|
5255
|
+
defaultSeverity: "warn"
|
|
5256
|
+
})
|
|
5257
|
+
);
|
|
5258
|
+
break;
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
return out3;
|
|
5263
|
+
}
|
|
5264
|
+
};
|
|
5265
|
+
}
|
|
5266
|
+
});
|
|
5267
|
+
|
|
5113
5268
|
// src/detectors/gh-expression.ts
|
|
5114
5269
|
function foldExpressions(s) {
|
|
5115
5270
|
return s.replace(/\$\{\{([^}]*)\}\}/g, (_m, e) => {
|
|
@@ -5878,7 +6033,7 @@ function ancestorOf(lines, i) {
|
|
|
5878
6033
|
while (h >= 0 && (lines[h].trim() === "" || /^\s*#/.test(lines[h]) || indentOf(lines[h]) >= ind)) h--;
|
|
5879
6034
|
return h >= 0 ? h : null;
|
|
5880
6035
|
}
|
|
5881
|
-
var
|
|
6036
|
+
var RULE7, USES, CHECK, YAML_KEY, invokesCheck2, uncommented, SHELL_ERREXIT, indentOf, STEP_START, REPORTER_ACTION, usesCheck, stepHasCheck, jobHasCheck, NEUTRALISERS, LIST_KEYS, DEFAULT_BRANCH, ALL, CODE_EVENTS, PR_CODE_TYPES, pathsMatchSource, ignoresEverySource, CODE_PROBES, REUSED_WORKFLOW, MANIFEST, WORKDIR_LINE, REF_LINE, CHECKOUT, ciTampering;
|
|
5882
6037
|
var init_ci_tampering = __esm({
|
|
5883
6038
|
"src/detectors/ci-tampering.ts"() {
|
|
5884
6039
|
"use strict";
|
|
@@ -5891,7 +6046,7 @@ var init_ci_tampering = __esm({
|
|
|
5891
6046
|
init_gh_expression();
|
|
5892
6047
|
init_invocation();
|
|
5893
6048
|
init_invocation();
|
|
5894
|
-
|
|
6049
|
+
RULE7 = "ci-tampering";
|
|
5895
6050
|
USES = /^\s*-\s*uses:/;
|
|
5896
6051
|
CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
|
|
5897
6052
|
YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
|
|
@@ -5931,7 +6086,7 @@ var init_ci_tampering = __esm({
|
|
|
5931
6086
|
REF_LINE = /^\s*ref:\s*(.+?)\s*$/;
|
|
5932
6087
|
CHECKOUT = /^\s*-?\s*uses:\s*actions\/checkout(?:@|\s|$)/;
|
|
5933
6088
|
ciTampering = {
|
|
5934
|
-
id:
|
|
6089
|
+
id: RULE7,
|
|
5935
6090
|
surface: ["file"],
|
|
5936
6091
|
certainty: "mechanical",
|
|
5937
6092
|
run(changes, policy, _view, ctx) {
|
|
@@ -5943,7 +6098,7 @@ var init_ci_tampering = __esm({
|
|
|
5943
6098
|
if (c.kind !== "file") continue;
|
|
5944
6099
|
if (c.op === "rename" && c.oldPath && isActiveWorkflow(c.oldPath) && !isActiveWorkflow(c.path)) {
|
|
5945
6100
|
out3.push(
|
|
5946
|
-
makeFinding(
|
|
6101
|
+
makeFinding(RULE7, policy, {
|
|
5947
6102
|
file: c.path,
|
|
5948
6103
|
message: `A CI workflow was renamed so it no longer runs (${c.oldPath} \u2192 ${c.path}).`,
|
|
5949
6104
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -5967,7 +6122,7 @@ var init_ci_tampering = __esm({
|
|
|
5967
6122
|
}
|
|
5968
6123
|
if (coe && isTruthy(coe[1])) {
|
|
5969
6124
|
out3.push(
|
|
5970
|
-
makeFinding(
|
|
6125
|
+
makeFinding(RULE7, policy, {
|
|
5971
6126
|
file: c.path,
|
|
5972
6127
|
line: l.newLine ?? void 0,
|
|
5973
6128
|
message: "continue-on-error: true added \u2014 failures will no longer fail the job.",
|
|
@@ -5977,7 +6132,7 @@ var init_ci_tampering = __esm({
|
|
|
5977
6132
|
);
|
|
5978
6133
|
} else if (cond && isAlwaysFalse(cond[1])) {
|
|
5979
6134
|
out3.push(
|
|
5980
|
-
makeFinding(
|
|
6135
|
+
makeFinding(RULE7, policy, {
|
|
5981
6136
|
file: c.path,
|
|
5982
6137
|
line: l.newLine ?? void 0,
|
|
5983
6138
|
message: `if: ${uncommented(cond[1])} added \u2014 this step can never run.`,
|
|
@@ -5997,7 +6152,7 @@ var init_ci_tampering = __esm({
|
|
|
5997
6152
|
const n = NEUTRALISERS.find((x) => x.re.test(b.body[k]));
|
|
5998
6153
|
if (!n || propagates && /set\s\+e/.test(b.body[k])) continue;
|
|
5999
6154
|
out3.push(
|
|
6000
|
-
makeFinding(
|
|
6155
|
+
makeFinding(RULE7, policy, {
|
|
6001
6156
|
file: c.path,
|
|
6002
6157
|
line: idx + 1,
|
|
6003
6158
|
message: `A check's run block was neutralised: ${n.what}.`,
|
|
@@ -6013,7 +6168,7 @@ var init_ci_tampering = __esm({
|
|
|
6013
6168
|
const step = stepOf(afterLines, l.newLine - 1);
|
|
6014
6169
|
if (step && !stepHasCheck(afterLines, step)) continue;
|
|
6015
6170
|
out3.push(
|
|
6016
|
-
makeFinding(
|
|
6171
|
+
makeFinding(RULE7, policy, {
|
|
6017
6172
|
file: c.path,
|
|
6018
6173
|
line: l.newLine,
|
|
6019
6174
|
message: "shell: \u2026 {0} added on a check step \u2014 the shell no longer stops on the first failure.",
|
|
@@ -6039,7 +6194,7 @@ var init_ci_tampering = __esm({
|
|
|
6039
6194
|
if (job ? !jobHasCheck(afterLines, job) : !afterLines.some((x) => invokesCheck2(x) || usesCheck(x))) continue;
|
|
6040
6195
|
}
|
|
6041
6196
|
out3.push(
|
|
6042
|
-
makeFinding(
|
|
6197
|
+
makeFinding(RULE7, policy, {
|
|
6043
6198
|
file: c.path,
|
|
6044
6199
|
line: l.newLine,
|
|
6045
6200
|
message: `A check was pointed at ${dir} by working-directory \u2014 a directory holding no code or package the check could run.`,
|
|
@@ -6056,7 +6211,7 @@ var init_ci_tampering = __esm({
|
|
|
6056
6211
|
const target = literalValue(ref[1]);
|
|
6057
6212
|
if (target === null) continue;
|
|
6058
6213
|
out3.push(
|
|
6059
|
-
makeFinding(
|
|
6214
|
+
makeFinding(RULE7, policy, {
|
|
6060
6215
|
file: c.path,
|
|
6061
6216
|
line: l.newLine,
|
|
6062
6217
|
message: `actions/checkout was pinned to ref: ${target} \u2014 the checks run against that ref, not against the change under review.`,
|
|
@@ -6069,7 +6224,7 @@ var init_ci_tampering = __esm({
|
|
|
6069
6224
|
triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
|
|
6070
6225
|
for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
|
|
6071
6226
|
out3.push(
|
|
6072
|
-
makeFinding(
|
|
6227
|
+
makeFinding(RULE7, policy, {
|
|
6073
6228
|
file: c.path,
|
|
6074
6229
|
message: `The workflow's triggers were narrowed: ${reason}.`,
|
|
6075
6230
|
evidence: reason,
|
|
@@ -6099,7 +6254,7 @@ var init_ci_tampering = __esm({
|
|
|
6099
6254
|
if (s.state === "kept") continue;
|
|
6100
6255
|
if (s.state === "neutralised") {
|
|
6101
6256
|
out3.push(
|
|
6102
|
-
makeFinding(
|
|
6257
|
+
makeFinding(RULE7, policy, {
|
|
6103
6258
|
file: c.path,
|
|
6104
6259
|
message: "A CI check command was neutralised in place: it still runs, but its result no longer decides (or its suite was narrowed).",
|
|
6105
6260
|
evidence: `${l.content.trim()} \u2192 ${s.by}`,
|
|
@@ -6109,7 +6264,7 @@ var init_ci_tampering = __esm({
|
|
|
6109
6264
|
continue;
|
|
6110
6265
|
}
|
|
6111
6266
|
out3.push(
|
|
6112
|
-
makeFinding(
|
|
6267
|
+
makeFinding(RULE7, policy, {
|
|
6113
6268
|
file: c.path,
|
|
6114
6269
|
message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
|
|
6115
6270
|
evidence: l.content.trim(),
|
|
@@ -7139,7 +7294,7 @@ function claudeSettingsFindings(c, policy) {
|
|
|
7139
7294
|
if (c.after == null) return null;
|
|
7140
7295
|
const after = parseSettings(c.after);
|
|
7141
7296
|
const before = c.before != null ? parseSettings(c.before) : null;
|
|
7142
|
-
const finding = (reason) => makeFinding(
|
|
7297
|
+
const finding = (reason) => makeFinding(RULE8, policy, {
|
|
7143
7298
|
file: c.path,
|
|
7144
7299
|
message: `The Claude Code hook wiring was weakened: ${reason}.`,
|
|
7145
7300
|
evidence: reason,
|
|
@@ -7164,7 +7319,7 @@ function policyFindings(c, policy, ctx) {
|
|
|
7164
7319
|
const reasons = c.before == null ? policyAddWeakening(c.after, probe) : policyWeakening(c.before, c.after, probe);
|
|
7165
7320
|
if (reasons === null) return null;
|
|
7166
7321
|
return reasons.map(
|
|
7167
|
-
(reason) => makeFinding(
|
|
7322
|
+
(reason) => makeFinding(RULE8, policy, {
|
|
7168
7323
|
file: c.path,
|
|
7169
7324
|
message: added ? `A policy file was added that weakens the baseline: ${reason}.` : `The policy was weakened: ${reason}.`,
|
|
7170
7325
|
evidence: reason,
|
|
@@ -7210,6 +7365,50 @@ function syntheticBase(over) {
|
|
|
7210
7365
|
}
|
|
7211
7366
|
return base;
|
|
7212
7367
|
}
|
|
7368
|
+
function freshLefthookBase(after) {
|
|
7369
|
+
const base = {};
|
|
7370
|
+
for (const [section, v] of Object.entries(after)) {
|
|
7371
|
+
if (!isObj(v)) continue;
|
|
7372
|
+
for (const group of ["commands", "scripts"]) {
|
|
7373
|
+
const g = v[group];
|
|
7374
|
+
if (!isObj(g)) continue;
|
|
7375
|
+
for (const [name, cfg] of Object.entries(g)) {
|
|
7376
|
+
if (!isObj(cfg)) continue;
|
|
7377
|
+
const run = cfg.run ?? cfg.runner;
|
|
7378
|
+
if (!namesGate(name) && !namesGate(run)) continue;
|
|
7379
|
+
const sec = base[section] ??= {};
|
|
7380
|
+
const grp = sec[group] ??= {};
|
|
7381
|
+
grp[name] = { run: typeof run === "string" ? liveOrCanonical(run) : CANONICAL_GATE };
|
|
7382
|
+
}
|
|
7383
|
+
}
|
|
7384
|
+
}
|
|
7385
|
+
return base;
|
|
7386
|
+
}
|
|
7387
|
+
function freshPreCommitBase(after) {
|
|
7388
|
+
const stagesOf = (v) => Array.isArray(v) && v.map(String).some((s) => COMMIT_STAGES.has(s)) ? v.map(String) : null;
|
|
7389
|
+
const defaults = stagesOf(after.default_stages);
|
|
7390
|
+
const repos = [];
|
|
7391
|
+
for (const repo of Array.isArray(after.repos) ? after.repos : []) {
|
|
7392
|
+
if (!isObj(repo)) continue;
|
|
7393
|
+
const hooks = [];
|
|
7394
|
+
for (const h of Array.isArray(repo.hooks) ? repo.hooks : []) {
|
|
7395
|
+
if (!isObj(h)) continue;
|
|
7396
|
+
if (![repo.repo, h.id, h.entry, h.name].some(namesGate)) continue;
|
|
7397
|
+
const fresh = { id: h.id, name: h.name };
|
|
7398
|
+
if (h.entry !== void 0) {
|
|
7399
|
+
const args = Array.isArray(h.args) ? h.args.map(String).join(" ") : "";
|
|
7400
|
+
fresh.entry = liveOrCanonical(`${typeof h.entry === "string" ? h.entry : ""} ${args}`.trim());
|
|
7401
|
+
}
|
|
7402
|
+
const stages = stagesOf(h.stages);
|
|
7403
|
+
if (stages) fresh.stages = stages;
|
|
7404
|
+
hooks.push(fresh);
|
|
7405
|
+
}
|
|
7406
|
+
repos.push({ repo: repo.repo, hooks });
|
|
7407
|
+
}
|
|
7408
|
+
const base = { repos };
|
|
7409
|
+
if (defaults) base.default_stages = defaults;
|
|
7410
|
+
return base;
|
|
7411
|
+
}
|
|
7213
7412
|
function configFindings(c, changes, policy, ctx) {
|
|
7214
7413
|
if (c.after == null) return null;
|
|
7215
7414
|
let reasons;
|
|
@@ -7219,15 +7418,18 @@ function configFindings(c, changes, policy, ctx) {
|
|
|
7219
7418
|
if (overBefore === null) return null;
|
|
7220
7419
|
const base = lefthookBase(c, changes, ctx) ?? syntheticBase(overAfter ?? {});
|
|
7221
7420
|
reasons = overAfter === null ? ["the file no longer parses as YAML \u2014 the tool that reads it runs nothing"] : lefthookWeakening(mergeDocs(base, overBefore), mergeDocs(base, overAfter));
|
|
7421
|
+
} else if (c.before == null) {
|
|
7422
|
+
const after = parseDoc(c.after);
|
|
7423
|
+
if (after === null) return [];
|
|
7424
|
+
reasons = isLefthook(c.path) ? lefthookWeakening(freshLefthookBase(after), after) : preCommitWeakening(freshPreCommitBase(after), after);
|
|
7222
7425
|
} else {
|
|
7223
|
-
if (c.before == null) return null;
|
|
7224
7426
|
const before = parseDoc(c.before);
|
|
7225
7427
|
const after = parseDoc(c.after);
|
|
7226
7428
|
if (before === null) return null;
|
|
7227
7429
|
reasons = after === null ? ["the file no longer parses as YAML \u2014 the tool that reads it runs nothing"] : isLefthook(c.path) ? lefthookWeakening(before, after) : preCommitWeakening(before, after);
|
|
7228
7430
|
}
|
|
7229
7431
|
return reasons.map(
|
|
7230
|
-
(reason) => makeFinding(
|
|
7432
|
+
(reason) => makeFinding(RULE8, policy, {
|
|
7231
7433
|
file: c.path,
|
|
7232
7434
|
message: `The hook configuration was weakened: ${reason}.`,
|
|
7233
7435
|
evidence: reason,
|
|
@@ -7278,7 +7480,7 @@ function huskyRuntimeWrite(path, content, ctx) {
|
|
|
7278
7480
|
(i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
|
|
7279
7481
|
);
|
|
7280
7482
|
}
|
|
7281
|
-
var
|
|
7483
|
+
var RULE8, ownerExec2, EVENTS, isObj, declares, GATE_KEYS, HOOK_TYPES, SIBLING_KEYS, MENTIONS_GATE, FORGERY, ENV_MECHANISM, envMechanism, mentionsGate, carriesGate, CANONICAL_GATE, COMMIT_STAGES, namesGate, liveOrCanonical, HUSKY_SHIM, HUSKY_SHIM_NAMES, HUSKY_H_KNOWN, HUSKY_DEPRECATION, stripTrailingNl, isHuskyScript, isHookNote, hookTampering;
|
|
7282
7484
|
var init_hook_tampering = __esm({
|
|
7283
7485
|
"src/detectors/hook-tampering.ts"() {
|
|
7284
7486
|
"use strict";
|
|
@@ -7291,7 +7493,7 @@ var init_hook_tampering = __esm({
|
|
|
7291
7493
|
init_command();
|
|
7292
7494
|
init_repo();
|
|
7293
7495
|
init_hook_wiring();
|
|
7294
|
-
|
|
7496
|
+
RULE8 = "hook-tampering";
|
|
7295
7497
|
ownerExec2 = (mode) => (parseInt(mode.slice(-3), 8) & 64) !== 0;
|
|
7296
7498
|
EVENTS = ["PreToolUse", "Stop"];
|
|
7297
7499
|
isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
@@ -7314,6 +7516,10 @@ var init_hook_tampering = __esm({
|
|
|
7314
7516
|
envMechanism = (k) => ENV_MECHANISM.find(([re]) => re.test(k))?.[1] ?? null;
|
|
7315
7517
|
mentionsGate = (v) => /\btamperward\b/.test(JSON.stringify(v) ?? "");
|
|
7316
7518
|
carriesGate = (s) => EVENTS.some((e) => gateCandidates(s, e).length > 0);
|
|
7519
|
+
CANONICAL_GATE = "npx tamperward check --staged";
|
|
7520
|
+
COMMIT_STAGES = /* @__PURE__ */ new Set(["pre-commit", "pre-push", "commit", "push"]);
|
|
7521
|
+
namesGate = (v) => typeof v === "string" && /\btamperward\b/.test(v);
|
|
7522
|
+
liveOrCanonical = (cmd) => gateOf(cmd) !== null ? cmd : CANONICAL_GATE;
|
|
7317
7523
|
HUSKY_SHIM = '#!/usr/bin/env sh\n. "$(dirname "$0")/h"';
|
|
7318
7524
|
HUSKY_SHIM_NAMES = /* @__PURE__ */ new Set(["applypatch-msg", "commit-msg", "post-applypatch", "post-checkout", "post-commit", "post-merge", "post-rewrite", "pre-applypatch", "pre-auto-gc", "pre-commit", "pre-merge-commit", "pre-push", "pre-rebase", "prepare-commit-msg"]);
|
|
7319
7525
|
HUSKY_H_KNOWN = [
|
|
@@ -7353,9 +7559,9 @@ They WILL FAIL in v10.0.0
|
|
|
7353
7559
|
"`;
|
|
7354
7560
|
stripTrailingNl = (s) => s.endsWith("\n") ? s.slice(0, -1) : s;
|
|
7355
7561
|
isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
|
|
7356
|
-
isHookNote = (path) => /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
|
|
7562
|
+
isHookNote = (path) => !/(?:^|\/)\.husky\/_\//.test(path) && /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
|
|
7357
7563
|
hookTampering = {
|
|
7358
|
-
id:
|
|
7564
|
+
id: RULE8,
|
|
7359
7565
|
surface: ["file", "command"],
|
|
7360
7566
|
certainty: "mechanical",
|
|
7361
7567
|
run(changes, policy, _view, ctx) {
|
|
@@ -7368,7 +7574,7 @@ They WILL FAIL in v10.0.0
|
|
|
7368
7574
|
if (isPackageJson(c.path) && c.before != null && c.after != null) {
|
|
7369
7575
|
for (const reason of packageJsonWeakening(c.before, c.after)) {
|
|
7370
7576
|
out3.push(
|
|
7371
|
-
makeFinding(
|
|
7577
|
+
makeFinding(RULE8, policy, {
|
|
7372
7578
|
file: c.path,
|
|
7373
7579
|
message: `The hook installation was weakened: ${reason}.`,
|
|
7374
7580
|
evidence: reason,
|
|
@@ -7381,7 +7587,7 @@ They WILL FAIL in v10.0.0
|
|
|
7381
7587
|
}
|
|
7382
7588
|
if (c.op !== "delete" && c.oldMode && c.newMode && ownerExec2(c.oldMode) && !ownerExec2(c.newMode)) {
|
|
7383
7589
|
out3.push(
|
|
7384
|
-
makeFinding(
|
|
7590
|
+
makeFinding(RULE8, policy, {
|
|
7385
7591
|
file: c.path,
|
|
7386
7592
|
message: `Execute permission was removed from a protected hook (${c.oldMode} \u2192 ${c.newMode}) \u2014 git will skip it.`,
|
|
7387
7593
|
evidence: `${c.path}: mode ${c.oldMode} \u2192 ${c.newMode}`,
|
|
@@ -7391,7 +7597,7 @@ They WILL FAIL in v10.0.0
|
|
|
7391
7597
|
}
|
|
7392
7598
|
if (c.op === "delete") {
|
|
7393
7599
|
out3.push(
|
|
7394
|
-
makeFinding(
|
|
7600
|
+
makeFinding(RULE8, policy, {
|
|
7395
7601
|
file: c.path,
|
|
7396
7602
|
message: "A protected hook/policy file was deleted.",
|
|
7397
7603
|
evidence: c.path,
|
|
@@ -7400,7 +7606,7 @@ They WILL FAIL in v10.0.0
|
|
|
7400
7606
|
);
|
|
7401
7607
|
} else if (c.op === "rename" && !renameKept(c)) {
|
|
7402
7608
|
out3.push(
|
|
7403
|
-
makeFinding(
|
|
7609
|
+
makeFinding(RULE8, policy, {
|
|
7404
7610
|
file: c.path,
|
|
7405
7611
|
message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
|
|
7406
7612
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -7414,7 +7620,7 @@ They WILL FAIL in v10.0.0
|
|
|
7414
7620
|
if (c.before != null && c.after != null) {
|
|
7415
7621
|
for (const reason of codeownersWeakening(c.before, c.after, [...repoGateFiles(ctx), c.path])) {
|
|
7416
7622
|
out3.push(
|
|
7417
|
-
makeFinding(
|
|
7623
|
+
makeFinding(RULE8, policy, {
|
|
7418
7624
|
file: c.path,
|
|
7419
7625
|
message: `CODEOWNERS was weakened: ${reason}.`,
|
|
7420
7626
|
evidence: reason,
|
|
@@ -7427,6 +7633,7 @@ They WILL FAIL in v10.0.0
|
|
|
7427
7633
|
out3.push(...semantic);
|
|
7428
7634
|
} else if (!c.path.endsWith(POLICY_FILE)) {
|
|
7429
7635
|
if ((c.op === "add" || c.op === "modify") && huskyRuntimeWrite(c.path, c.after, ctx)) continue;
|
|
7636
|
+
if (isHookNote(c.path)) continue;
|
|
7430
7637
|
const husky = isHuskyScript(c.path);
|
|
7431
7638
|
const opts = husky ? { errexit: true } : {};
|
|
7432
7639
|
const beforePin = c.before != null && c.after != null ? initScriptPin(c.before) : null;
|
|
@@ -7435,7 +7642,7 @@ They WILL FAIL in v10.0.0
|
|
|
7435
7642
|
const kept = afterPin !== null && (beforePin === "" ? afterPin === "" || PLAIN_SEMVER.test(afterPin) : pinNotBelow(afterPin, beforePin));
|
|
7436
7643
|
if (!kept) {
|
|
7437
7644
|
out3.push(
|
|
7438
|
-
makeFinding(
|
|
7645
|
+
makeFinding(RULE8, policy, {
|
|
7439
7646
|
file: c.path,
|
|
7440
7647
|
message: "A protected hook script no longer matches the shape `tamperward init` writes.",
|
|
7441
7648
|
evidence: "the gate script no longer matches the shape init wrote; re-run init or sign off",
|
|
@@ -7458,7 +7665,7 @@ They WILL FAIL in v10.0.0
|
|
|
7458
7665
|
if (shebang || !live) {
|
|
7459
7666
|
const why = shebang ?? (gate.length ? `its \`${gate[0].identity}\` is ${gate[0].state}${gate[0].why ? ` (${gate[0].why})` : ""}` : "it runs no `tamperward check`");
|
|
7460
7667
|
out3.push(
|
|
7461
|
-
makeFinding(
|
|
7668
|
+
makeFinding(RULE8, policy, {
|
|
7462
7669
|
file: c.path,
|
|
7463
7670
|
message: `A protected hook script was added that does not run the gate live: ${why}. A new gate script needs a sign-off unless the gate is live in it.`,
|
|
7464
7671
|
evidence: `a hook script was added; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or make the gate live in it`,
|
|
@@ -7467,7 +7674,7 @@ They WILL FAIL in v10.0.0
|
|
|
7467
7674
|
);
|
|
7468
7675
|
} else if (earlyExit) {
|
|
7469
7676
|
out3.push(
|
|
7470
|
-
makeFinding(
|
|
7677
|
+
makeFinding(RULE8, policy, {
|
|
7471
7678
|
file: c.path,
|
|
7472
7679
|
line: earlyExit.newLine ?? void 0,
|
|
7473
7680
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -7489,7 +7696,7 @@ They WILL FAIL in v10.0.0
|
|
|
7489
7696
|
deadGuard2 ? `a guard that can never be true was inserted (\`${deadGuard2.content.trim()}\`)` : null
|
|
7490
7697
|
].filter((d) => d !== null);
|
|
7491
7698
|
out3.push(
|
|
7492
|
-
makeFinding(
|
|
7699
|
+
makeFinding(RULE8, policy, {
|
|
7493
7700
|
file: c.path,
|
|
7494
7701
|
message: `A hand-written protected hook script was changed: ${detail.length ? detail.join("; ") : "an edit other than a pin raise"}. Every edit to a gate script other than raising its pin needs a sign-off.`,
|
|
7495
7702
|
evidence: `the gate script changed; sign off with \`tamperward allow hook-tampering --file ${c.path} --reason "..."\` locally or the \`tamperward:allow:hook-tampering@<head-sha>\` label in CI, or restore it`,
|
|
@@ -7501,7 +7708,7 @@ They WILL FAIL in v10.0.0
|
|
|
7501
7708
|
}
|
|
7502
7709
|
if (shebangChanged && c.after != null) {
|
|
7503
7710
|
out3.push(
|
|
7504
|
-
makeFinding(
|
|
7711
|
+
makeFinding(RULE8, policy, {
|
|
7505
7712
|
file: c.path,
|
|
7506
7713
|
line: 1,
|
|
7507
7714
|
message: `A protected hook's interpreter was changed: ${shebang}.`,
|
|
@@ -7512,7 +7719,7 @@ They WILL FAIL in v10.0.0
|
|
|
7512
7719
|
}
|
|
7513
7720
|
if (earlyExit) {
|
|
7514
7721
|
out3.push(
|
|
7515
|
-
makeFinding(
|
|
7722
|
+
makeFinding(RULE8, policy, {
|
|
7516
7723
|
file: c.path,
|
|
7517
7724
|
line: earlyExit.newLine ?? void 0,
|
|
7518
7725
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -7524,7 +7731,7 @@ They WILL FAIL in v10.0.0
|
|
|
7524
7731
|
const deadGuard = c.before == null || c.after == null ? added.find((l) => insertsDeadGuard(l.content)) : void 0;
|
|
7525
7732
|
if (deadGuard) {
|
|
7526
7733
|
out3.push(
|
|
7527
|
-
makeFinding(
|
|
7734
|
+
makeFinding(RULE8, policy, {
|
|
7528
7735
|
file: c.path,
|
|
7529
7736
|
line: deadGuard.newLine ?? void 0,
|
|
7530
7737
|
message: "A guard that can never be true was inserted into a protected hook \u2014 what it wraps no longer runs.",
|
|
@@ -7536,7 +7743,7 @@ They WILL FAIL in v10.0.0
|
|
|
7536
7743
|
const lost = scriptWeakening(beforeLines, afterLines, opts);
|
|
7537
7744
|
if (lost.length) {
|
|
7538
7745
|
out3.push(
|
|
7539
|
-
makeFinding(
|
|
7746
|
+
makeFinding(RULE8, policy, {
|
|
7540
7747
|
file: c.path,
|
|
7541
7748
|
message: `A check invocation was removed from a protected hook: ${lost[0].reason}.`,
|
|
7542
7749
|
evidence: lost[0].evidence,
|
|
@@ -7555,7 +7762,7 @@ They WILL FAIL in v10.0.0
|
|
|
7555
7762
|
const m = l.content.match(INLINE);
|
|
7556
7763
|
if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
|
|
7557
7764
|
out3.push(
|
|
7558
|
-
makeFinding(
|
|
7765
|
+
makeFinding(RULE8, policy, {
|
|
7559
7766
|
file: c.path,
|
|
7560
7767
|
line: l.newLine ?? void 0,
|
|
7561
7768
|
message: `Rule "${m[1]}" was lowered from block to warn in the policy.`,
|
|
@@ -7565,7 +7772,7 @@ They WILL FAIL in v10.0.0
|
|
|
7565
7772
|
);
|
|
7566
7773
|
} else if (/enabled:\s*false\b/.test(l.content)) {
|
|
7567
7774
|
out3.push(
|
|
7568
|
-
makeFinding(
|
|
7775
|
+
makeFinding(RULE8, policy, {
|
|
7569
7776
|
file: c.path,
|
|
7570
7777
|
line: l.newLine ?? void 0,
|
|
7571
7778
|
message: "A rule was disabled (enabled: false) in the policy.",
|
|
@@ -7583,7 +7790,7 @@ They WILL FAIL in v10.0.0
|
|
|
7583
7790
|
const hiding = updateIndexHiding(toks, policy);
|
|
7584
7791
|
if (hiding) {
|
|
7585
7792
|
out3.push(
|
|
7586
|
-
makeFinding(
|
|
7793
|
+
makeFinding(RULE8, policy, {
|
|
7587
7794
|
file: repoRelative(hiding.file, ctx),
|
|
7588
7795
|
message: `Hook tampering via shell: ${hiding.reason}.`,
|
|
7589
7796
|
evidence: seg,
|
|
@@ -7599,7 +7806,7 @@ They WILL FAIL in v10.0.0
|
|
|
7599
7806
|
const file = target ? repoRelative(target, ctx) : void 0;
|
|
7600
7807
|
const outside = file !== void 0 && (isAbsolute6(file) || /^[A-Za-z]:\//.test(file)) && isClaudeSettings(file) ? ` \u2014 ${file} is a Claude Code settings file the runtime reads hooks from` : "";
|
|
7601
7808
|
out3.push(
|
|
7602
|
-
makeFinding(
|
|
7809
|
+
makeFinding(RULE8, policy, {
|
|
7603
7810
|
...file !== void 0 ? { file } : {},
|
|
7604
7811
|
message: `Hook tampering via shell: ${why}${outside}.`,
|
|
7605
7812
|
evidence: seg,
|
|
@@ -7835,7 +8042,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
7835
8042
|
if (!was || !now || was.path === now.path) return [];
|
|
7836
8043
|
const samples = runnerSamples(now.path, policy, ctx, "pytest");
|
|
7837
8044
|
return suiteNarrowings(was.content, now.content, now.path, samples).map(
|
|
7838
|
-
(n) => makeFinding(
|
|
8045
|
+
(n) => makeFinding(RULE9, policy, {
|
|
7839
8046
|
file: now.path,
|
|
7840
8047
|
message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
|
|
7841
8048
|
evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
|
|
@@ -7843,7 +8050,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
7843
8050
|
})
|
|
7844
8051
|
);
|
|
7845
8052
|
}
|
|
7846
|
-
var
|
|
8053
|
+
var RULE9, JVM_TEST, TEST_DEFS, TABLE_METHOD, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
|
|
7847
8054
|
var init_test_deletion = __esm({
|
|
7848
8055
|
"src/detectors/test-deletion.ts"() {
|
|
7849
8056
|
"use strict";
|
|
@@ -7856,7 +8063,7 @@ var init_test_deletion = __esm({
|
|
|
7856
8063
|
init_suite_config();
|
|
7857
8064
|
init_invocation();
|
|
7858
8065
|
init_narrow();
|
|
7859
|
-
|
|
8066
|
+
RULE9 = "test-deletion";
|
|
7860
8067
|
JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
|
|
7861
8068
|
TEST_DEFS = {
|
|
7862
8069
|
py: /^\s*(?:async\s+)?def\s+test\w*\s*\(/,
|
|
@@ -7876,7 +8083,7 @@ var init_test_deletion = __esm({
|
|
|
7876
8083
|
SLICE_SCRIPT = /^test:(?:unit|integration)$/;
|
|
7877
8084
|
SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
|
|
7878
8085
|
testDeletion = {
|
|
7879
|
-
id:
|
|
8086
|
+
id: RULE9,
|
|
7880
8087
|
surface: ["file", "command"],
|
|
7881
8088
|
certainty: "mechanical",
|
|
7882
8089
|
run(changes, policy, _view, ctx) {
|
|
@@ -7908,7 +8115,7 @@ var init_test_deletion = __esm({
|
|
|
7908
8115
|
if (c.op === "delete" && isTest) {
|
|
7909
8116
|
if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
|
|
7910
8117
|
out3.push(
|
|
7911
|
-
makeFinding(
|
|
8118
|
+
makeFinding(RULE9, policy, {
|
|
7912
8119
|
file: c.path,
|
|
7913
8120
|
message: "A test file was deleted.",
|
|
7914
8121
|
evidence: c.path,
|
|
@@ -7917,7 +8124,7 @@ var init_test_deletion = __esm({
|
|
|
7917
8124
|
);
|
|
7918
8125
|
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && !isTest) {
|
|
7919
8126
|
out3.push(
|
|
7920
|
-
makeFinding(
|
|
8127
|
+
makeFinding(RULE9, policy, {
|
|
7921
8128
|
file: c.path,
|
|
7922
8129
|
message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
|
|
7923
8130
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -7926,7 +8133,7 @@ var init_test_deletion = __esm({
|
|
|
7926
8133
|
);
|
|
7927
8134
|
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && isTest && leavesWalk(c.oldPath, c.path)) {
|
|
7928
8135
|
out3.push(
|
|
7929
|
-
makeFinding(
|
|
8136
|
+
makeFinding(RULE9, policy, {
|
|
7930
8137
|
file: c.path,
|
|
7931
8138
|
message: `A test file was renamed into a path the runner does not walk (${c.oldPath} \u2192 ${c.path}): ${leavesWalk(c.oldPath, c.path)}.`,
|
|
7932
8139
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -7941,7 +8148,7 @@ var init_test_deletion = __esm({
|
|
|
7941
8148
|
const removed = new Set([...significantLines(c.before, c.path)].filter((l) => !afterLines.has(l)));
|
|
7942
8149
|
if (isRelocation(removed, before.min - after.min)) continue;
|
|
7943
8150
|
out3.push(
|
|
7944
|
-
makeFinding(
|
|
8151
|
+
makeFinding(RULE9, policy, {
|
|
7945
8152
|
file: c.path,
|
|
7946
8153
|
message: `Test blocks removed: ${before.min} \u2192 ${after.min} it()/test() in this spec (each-table rows count as tests).`,
|
|
7947
8154
|
evidence: `${before.min - after.min} test block(s) removed from ${c.path}`,
|
|
@@ -7954,7 +8161,7 @@ var init_test_deletion = __esm({
|
|
|
7954
8161
|
const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
|
|
7955
8162
|
for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
|
|
7956
8163
|
out3.push(
|
|
7957
|
-
makeFinding(
|
|
8164
|
+
makeFinding(RULE9, policy, {
|
|
7958
8165
|
file: c.path,
|
|
7959
8166
|
message: `The runner no longer selects ${n.path}: ${n.reason}.`,
|
|
7960
8167
|
evidence: `${n.path} dropped from the suite by ${c.path}`,
|
|
@@ -7966,12 +8173,12 @@ var init_test_deletion = __esm({
|
|
|
7966
8173
|
const w = n.weakening;
|
|
7967
8174
|
const evidence = `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${n.after === "" ? "deleted" : JSON.stringify(n.after)}`;
|
|
7968
8175
|
out3.push(
|
|
7969
|
-
w.state === "removed" ? makeFinding(
|
|
8176
|
+
w.state === "removed" ? makeFinding(RULE9, policy, {
|
|
7970
8177
|
file: c.path,
|
|
7971
8178
|
message: `The test script no longer runs the ${w.kind} check: scripts.${n.script} ${w.what}.`,
|
|
7972
8179
|
evidence,
|
|
7973
8180
|
remediation: "Keep the script running its check. Replacing the runner in package.json removes every test from every `npm test` while the workflow line stays untouched."
|
|
7974
|
-
}) : makeFinding(
|
|
8181
|
+
}) : makeFinding(RULE9, policy, {
|
|
7975
8182
|
file: c.path,
|
|
7976
8183
|
message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${n.script}.`,
|
|
7977
8184
|
evidence,
|
|
@@ -8062,7 +8269,7 @@ var init_test_deletion = __esm({
|
|
|
8062
8269
|
}
|
|
8063
8270
|
if (why) {
|
|
8064
8271
|
out3.push(
|
|
8065
|
-
makeFinding(
|
|
8272
|
+
makeFinding(RULE9, policy, {
|
|
8066
8273
|
message: `Test removed via shell: ${why}.`,
|
|
8067
8274
|
evidence,
|
|
8068
8275
|
remediation: "Fix the code under test. Mutating a spec from the shell is still deleting the test \u2014 and is blocked the same way."
|
|
@@ -8118,7 +8325,7 @@ function significantLinesOrdered(src, path) {
|
|
|
8118
8325
|
}
|
|
8119
8326
|
return out3;
|
|
8120
8327
|
}
|
|
8121
|
-
var
|
|
8328
|
+
var RULE10, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, testContentRemoval;
|
|
8122
8329
|
var init_test_content_removal = __esm({
|
|
8123
8330
|
"src/detectors/test-content-removal.ts"() {
|
|
8124
8331
|
"use strict";
|
|
@@ -8127,13 +8334,13 @@ var init_test_content_removal = __esm({
|
|
|
8127
8334
|
init_finding();
|
|
8128
8335
|
init_test_deletion();
|
|
8129
8336
|
init_files();
|
|
8130
|
-
|
|
8337
|
+
RULE10 = "test-content-removal";
|
|
8131
8338
|
MIN_REMOVED_LINES = 3;
|
|
8132
8339
|
MIN_REMOVED_ROWS = 2;
|
|
8133
8340
|
ws = (s) => s.replace(/\s+/g, "");
|
|
8134
8341
|
TEST_DIR_FILE = /(?:^|\/)(?:test|tests|__tests__|spec|specs)\//;
|
|
8135
8342
|
testContentRemoval = {
|
|
8136
|
-
id:
|
|
8343
|
+
id: RULE10,
|
|
8137
8344
|
surface: ["file"],
|
|
8138
8345
|
certainty: "mechanical",
|
|
8139
8346
|
run(changes, policy) {
|
|
@@ -8174,7 +8381,7 @@ var init_test_content_removal = __esm({
|
|
|
8174
8381
|
const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
|
|
8175
8382
|
if (goneRows.length >= MIN_REMOVED_ROWS) {
|
|
8176
8383
|
out3.push(
|
|
8177
|
-
makeFinding(
|
|
8384
|
+
makeFinding(RULE10, policy, {
|
|
8178
8385
|
file: c.path,
|
|
8179
8386
|
message: `Rows removed from a test table: ${goneRows.length} case(s) gone from an each-table that spreads from elsewhere, and they reappear nowhere in the change.`,
|
|
8180
8387
|
evidence: goneRows.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -8185,7 +8392,7 @@ var init_test_content_removal = __esm({
|
|
|
8185
8392
|
}
|
|
8186
8393
|
if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
|
|
8187
8394
|
out3.push(
|
|
8188
|
-
makeFinding(
|
|
8395
|
+
makeFinding(RULE10, policy, {
|
|
8189
8396
|
file: c.path,
|
|
8190
8397
|
message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
|
|
8191
8398
|
evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -8208,14 +8415,14 @@ function namesProtectedSnapshot(seg, policy) {
|
|
|
8208
8415
|
}
|
|
8209
8416
|
return null;
|
|
8210
8417
|
}
|
|
8211
|
-
var
|
|
8418
|
+
var RULE11, RUNNER3, PM_TEST_SCRIPT, UPDATE_ANY, UPDATE_WITH_RUNNER, INSTA_ACCEPT, UPDATE_EXPECT, REGEN_SCRIPT, MUTATE, snapshotRewrite;
|
|
8212
8419
|
var init_snapshot_rewrite = __esm({
|
|
8213
8420
|
"src/detectors/snapshot-rewrite.ts"() {
|
|
8214
8421
|
"use strict";
|
|
8215
8422
|
init_policy();
|
|
8216
8423
|
init_finding();
|
|
8217
8424
|
init_command();
|
|
8218
|
-
|
|
8425
|
+
RULE11 = "snapshot-rewrite";
|
|
8219
8426
|
RUNNER3 = /\b(?:jest|vitest|playwright|ava)\b/;
|
|
8220
8427
|
PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
|
|
8221
8428
|
UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
|
|
@@ -8225,12 +8432,12 @@ var init_snapshot_rewrite = __esm({
|
|
|
8225
8432
|
REGEN_SCRIPT = /\b(?:update|regen(?:erate)?|re?bless)[-_.]?(?:golden|snapshots?|baselines?|expected)\b/i;
|
|
8226
8433
|
MUTATE = /(?:^|\s)(?:rm|cp|mv|tee|truncate|dd)(?:\s|$)|sed\s+-i|>{1,2}|\bfind\b.*\s-(?:delete\b|exec\s+rm\b)|\bgit\s+(?:checkout|restore)\b/;
|
|
8227
8434
|
snapshotRewrite = {
|
|
8228
|
-
id:
|
|
8435
|
+
id: RULE11,
|
|
8229
8436
|
surface: ["command", "file"],
|
|
8230
8437
|
certainty: "mechanical",
|
|
8231
8438
|
run(changes, policy) {
|
|
8232
8439
|
const out3 = [];
|
|
8233
|
-
const warn = (input) => out3.push(makeFinding(
|
|
8440
|
+
const warn = (input) => out3.push(makeFinding(RULE11, policy, { ...input, defaultSeverity: "warn" }));
|
|
8234
8441
|
for (const c of changes) {
|
|
8235
8442
|
if (c.kind === "command") {
|
|
8236
8443
|
for (const seg of segments(c.raw)) {
|
|
@@ -8278,16 +8485,16 @@ function isSnapshotChange(c, policy) {
|
|
|
8278
8485
|
if (isProtected(c.path, policy, "snapshots")) return true;
|
|
8279
8486
|
return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
|
|
8280
8487
|
}
|
|
8281
|
-
var
|
|
8488
|
+
var RULE12, COMMIT_VIEWS, snapshotOnlyRewrite;
|
|
8282
8489
|
var init_snapshot_only = __esm({
|
|
8283
8490
|
"src/detectors/snapshot-only.ts"() {
|
|
8284
8491
|
"use strict";
|
|
8285
8492
|
init_policy();
|
|
8286
8493
|
init_finding();
|
|
8287
|
-
|
|
8494
|
+
RULE12 = "snapshot-only-rewrite";
|
|
8288
8495
|
COMMIT_VIEWS = ["staged", "range"];
|
|
8289
8496
|
snapshotOnlyRewrite = {
|
|
8290
|
-
id:
|
|
8497
|
+
id: RULE12,
|
|
8291
8498
|
surface: ["file"],
|
|
8292
8499
|
certainty: "mechanical",
|
|
8293
8500
|
run(changes, policy, view) {
|
|
@@ -8300,7 +8507,7 @@ var init_snapshot_only = __esm({
|
|
|
8300
8507
|
if (!first) return [];
|
|
8301
8508
|
const names = snaps.map((c) => c.path).slice(0, 3).join(", ");
|
|
8302
8509
|
return [
|
|
8303
|
-
makeFinding(
|
|
8510
|
+
makeFinding(RULE12, policy, {
|
|
8304
8511
|
file: first.path,
|
|
8305
8512
|
message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
|
|
8306
8513
|
evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
|
|
@@ -8474,7 +8681,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
8474
8681
|
const weakened2 = directionalWeakening(before.assertions, after.assertions);
|
|
8475
8682
|
if (weakened2) {
|
|
8476
8683
|
const message = weakened2.kind === "throw-specificity" ? `Assertion weakened in test "${before.identity}": exception-message specificity was removed from toThrow().` : `Assertion weakened in test "${before.identity}": ${weakened2.before.matcher} was replaced by weaker ${weakened2.after.matcher} on the same subject.`;
|
|
8477
|
-
return makeFinding(
|
|
8684
|
+
return makeFinding(RULE13, policy, {
|
|
8478
8685
|
file: path,
|
|
8479
8686
|
message,
|
|
8480
8687
|
evidence: `${weakened2.before.text} -> ${weakened2.after.text}`,
|
|
@@ -8483,7 +8690,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
8483
8690
|
}
|
|
8484
8691
|
const removed = pureRemoval(before.assertions, after.assertions);
|
|
8485
8692
|
if (removed) {
|
|
8486
|
-
return makeFinding(
|
|
8693
|
+
return makeFinding(RULE13, policy, {
|
|
8487
8694
|
file: path,
|
|
8488
8695
|
message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
|
|
8489
8696
|
evidence: removed.text,
|
|
@@ -8492,7 +8699,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
8492
8699
|
}
|
|
8493
8700
|
return null;
|
|
8494
8701
|
}
|
|
8495
|
-
var
|
|
8702
|
+
var RULE13, TEST_CALLS, SUITE_CALLS, EXACT_MATCHERS, WEAK_MATCHERS, compact, assertionWeakening;
|
|
8496
8703
|
var init_assertion_weakening = __esm({
|
|
8497
8704
|
"src/detectors/assertion-weakening.ts"() {
|
|
8498
8705
|
"use strict";
|
|
@@ -8500,14 +8707,14 @@ var init_assertion_weakening = __esm({
|
|
|
8500
8707
|
init_policy();
|
|
8501
8708
|
init_finding();
|
|
8502
8709
|
init_files();
|
|
8503
|
-
|
|
8710
|
+
RULE13 = "assertion-weakening";
|
|
8504
8711
|
TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
8505
8712
|
SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
|
|
8506
8713
|
EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
|
|
8507
8714
|
WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
|
|
8508
8715
|
compact = (s) => s.replace(/\s+/g, "");
|
|
8509
8716
|
assertionWeakening = {
|
|
8510
|
-
id:
|
|
8717
|
+
id: RULE13,
|
|
8511
8718
|
surface: ["file"],
|
|
8512
8719
|
certainty: "heuristic",
|
|
8513
8720
|
run(changes, policy) {
|
|
@@ -8542,6 +8749,7 @@ var init_detectors = __esm({
|
|
|
8542
8749
|
init_lint_suppression();
|
|
8543
8750
|
init_test_skip();
|
|
8544
8751
|
init_coverage_lowering();
|
|
8752
|
+
init_coverage_exclusion();
|
|
8545
8753
|
init_ci_tampering();
|
|
8546
8754
|
init_hook_tampering();
|
|
8547
8755
|
init_test_deletion();
|
|
@@ -8556,6 +8764,7 @@ var init_detectors = __esm({
|
|
|
8556
8764
|
lintSuppression,
|
|
8557
8765
|
testSkip,
|
|
8558
8766
|
coverageLowering,
|
|
8767
|
+
coverageExclusion,
|
|
8559
8768
|
ciTampering,
|
|
8560
8769
|
hookTampering,
|
|
8561
8770
|
testDeletion,
|
|
@@ -10027,7 +10236,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
|
|
|
10027
10236
|
};
|
|
10028
10237
|
}
|
|
10029
10238
|
function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
10030
|
-
if (!isEnabled(
|
|
10239
|
+
if (!isEnabled(RULE14, policy)) return [];
|
|
10031
10240
|
const byPath = /* @__PURE__ */ new Map();
|
|
10032
10241
|
for (const e of events) {
|
|
10033
10242
|
if (!isProtected(e.path, policy)) continue;
|
|
@@ -10047,7 +10256,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
10047
10256
|
const modeChurn = modes.size > 1;
|
|
10048
10257
|
if (!contentChurn && !modeChurn) continue;
|
|
10049
10258
|
const what = contentChurn ? `content was modified and restored within the turn (${hashes.size} distinct states observed)` : `file mode changed during the turn (${[...modes].map((m) => (m & 4095).toString(8)).join(" \u2192 ")})`;
|
|
10050
|
-
const f = makeFinding(
|
|
10259
|
+
const f = makeFinding(RULE14, policy, {
|
|
10051
10260
|
file: path,
|
|
10052
10261
|
message: `Transient protected mutation: ${what}, leaving no net diff.`,
|
|
10053
10262
|
evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
|
|
@@ -10062,14 +10271,14 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
10062
10271
|
}
|
|
10063
10272
|
return out3;
|
|
10064
10273
|
}
|
|
10065
|
-
var
|
|
10274
|
+
var RULE14, MAX_EVENT_READ_BYTES, MAX_EVENT_SWEEP_BYTES, DEFAULT_EVENT_READ_IO;
|
|
10066
10275
|
var init_fs_events = __esm({
|
|
10067
10276
|
"src/detectors/fs-events.ts"() {
|
|
10068
10277
|
"use strict";
|
|
10069
10278
|
init_policy();
|
|
10070
10279
|
init_finding();
|
|
10071
10280
|
init_narrow();
|
|
10072
|
-
|
|
10281
|
+
RULE14 = "transient-protected-mutation";
|
|
10073
10282
|
MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
|
|
10074
10283
|
MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
|
|
10075
10284
|
DEFAULT_EVENT_READ_IO = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.24.1",
|
|
4
4
|
"description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "hexrift",
|