tamperward 2.23.20 → 2.24.0
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 +236 -79
- 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,
|
|
@@ -7227,7 +7382,7 @@ function configFindings(c, changes, policy, ctx) {
|
|
|
7227
7382
|
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
7383
|
}
|
|
7229
7384
|
return reasons.map(
|
|
7230
|
-
(reason) => makeFinding(
|
|
7385
|
+
(reason) => makeFinding(RULE8, policy, {
|
|
7231
7386
|
file: c.path,
|
|
7232
7387
|
message: `The hook configuration was weakened: ${reason}.`,
|
|
7233
7388
|
evidence: reason,
|
|
@@ -7278,7 +7433,7 @@ function huskyRuntimeWrite(path, content, ctx) {
|
|
|
7278
7433
|
(i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
|
|
7279
7434
|
);
|
|
7280
7435
|
}
|
|
7281
|
-
var
|
|
7436
|
+
var RULE8, ownerExec2, EVENTS, isObj, declares, GATE_KEYS, HOOK_TYPES, SIBLING_KEYS, MENTIONS_GATE, FORGERY, ENV_MECHANISM, envMechanism, mentionsGate, carriesGate, HUSKY_SHIM, HUSKY_SHIM_NAMES, HUSKY_H_KNOWN, HUSKY_DEPRECATION, stripTrailingNl, isHuskyScript, isHookNote, hookTampering;
|
|
7282
7437
|
var init_hook_tampering = __esm({
|
|
7283
7438
|
"src/detectors/hook-tampering.ts"() {
|
|
7284
7439
|
"use strict";
|
|
@@ -7291,7 +7446,7 @@ var init_hook_tampering = __esm({
|
|
|
7291
7446
|
init_command();
|
|
7292
7447
|
init_repo();
|
|
7293
7448
|
init_hook_wiring();
|
|
7294
|
-
|
|
7449
|
+
RULE8 = "hook-tampering";
|
|
7295
7450
|
ownerExec2 = (mode) => (parseInt(mode.slice(-3), 8) & 64) !== 0;
|
|
7296
7451
|
EVENTS = ["PreToolUse", "Stop"];
|
|
7297
7452
|
isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
@@ -7355,7 +7510,7 @@ They WILL FAIL in v10.0.0
|
|
|
7355
7510
|
isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
|
|
7356
7511
|
isHookNote = (path) => /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
|
|
7357
7512
|
hookTampering = {
|
|
7358
|
-
id:
|
|
7513
|
+
id: RULE8,
|
|
7359
7514
|
surface: ["file", "command"],
|
|
7360
7515
|
certainty: "mechanical",
|
|
7361
7516
|
run(changes, policy, _view, ctx) {
|
|
@@ -7368,7 +7523,7 @@ They WILL FAIL in v10.0.0
|
|
|
7368
7523
|
if (isPackageJson(c.path) && c.before != null && c.after != null) {
|
|
7369
7524
|
for (const reason of packageJsonWeakening(c.before, c.after)) {
|
|
7370
7525
|
out3.push(
|
|
7371
|
-
makeFinding(
|
|
7526
|
+
makeFinding(RULE8, policy, {
|
|
7372
7527
|
file: c.path,
|
|
7373
7528
|
message: `The hook installation was weakened: ${reason}.`,
|
|
7374
7529
|
evidence: reason,
|
|
@@ -7381,7 +7536,7 @@ They WILL FAIL in v10.0.0
|
|
|
7381
7536
|
}
|
|
7382
7537
|
if (c.op !== "delete" && c.oldMode && c.newMode && ownerExec2(c.oldMode) && !ownerExec2(c.newMode)) {
|
|
7383
7538
|
out3.push(
|
|
7384
|
-
makeFinding(
|
|
7539
|
+
makeFinding(RULE8, policy, {
|
|
7385
7540
|
file: c.path,
|
|
7386
7541
|
message: `Execute permission was removed from a protected hook (${c.oldMode} \u2192 ${c.newMode}) \u2014 git will skip it.`,
|
|
7387
7542
|
evidence: `${c.path}: mode ${c.oldMode} \u2192 ${c.newMode}`,
|
|
@@ -7391,7 +7546,7 @@ They WILL FAIL in v10.0.0
|
|
|
7391
7546
|
}
|
|
7392
7547
|
if (c.op === "delete") {
|
|
7393
7548
|
out3.push(
|
|
7394
|
-
makeFinding(
|
|
7549
|
+
makeFinding(RULE8, policy, {
|
|
7395
7550
|
file: c.path,
|
|
7396
7551
|
message: "A protected hook/policy file was deleted.",
|
|
7397
7552
|
evidence: c.path,
|
|
@@ -7400,7 +7555,7 @@ They WILL FAIL in v10.0.0
|
|
|
7400
7555
|
);
|
|
7401
7556
|
} else if (c.op === "rename" && !renameKept(c)) {
|
|
7402
7557
|
out3.push(
|
|
7403
|
-
makeFinding(
|
|
7558
|
+
makeFinding(RULE8, policy, {
|
|
7404
7559
|
file: c.path,
|
|
7405
7560
|
message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
|
|
7406
7561
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -7414,7 +7569,7 @@ They WILL FAIL in v10.0.0
|
|
|
7414
7569
|
if (c.before != null && c.after != null) {
|
|
7415
7570
|
for (const reason of codeownersWeakening(c.before, c.after, [...repoGateFiles(ctx), c.path])) {
|
|
7416
7571
|
out3.push(
|
|
7417
|
-
makeFinding(
|
|
7572
|
+
makeFinding(RULE8, policy, {
|
|
7418
7573
|
file: c.path,
|
|
7419
7574
|
message: `CODEOWNERS was weakened: ${reason}.`,
|
|
7420
7575
|
evidence: reason,
|
|
@@ -7435,7 +7590,7 @@ They WILL FAIL in v10.0.0
|
|
|
7435
7590
|
const kept = afterPin !== null && (beforePin === "" ? afterPin === "" || PLAIN_SEMVER.test(afterPin) : pinNotBelow(afterPin, beforePin));
|
|
7436
7591
|
if (!kept) {
|
|
7437
7592
|
out3.push(
|
|
7438
|
-
makeFinding(
|
|
7593
|
+
makeFinding(RULE8, policy, {
|
|
7439
7594
|
file: c.path,
|
|
7440
7595
|
message: "A protected hook script no longer matches the shape `tamperward init` writes.",
|
|
7441
7596
|
evidence: "the gate script no longer matches the shape init wrote; re-run init or sign off",
|
|
@@ -7458,7 +7613,7 @@ They WILL FAIL in v10.0.0
|
|
|
7458
7613
|
if (shebang || !live) {
|
|
7459
7614
|
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
7615
|
out3.push(
|
|
7461
|
-
makeFinding(
|
|
7616
|
+
makeFinding(RULE8, policy, {
|
|
7462
7617
|
file: c.path,
|
|
7463
7618
|
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
7619
|
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 +7622,7 @@ They WILL FAIL in v10.0.0
|
|
|
7467
7622
|
);
|
|
7468
7623
|
} else if (earlyExit) {
|
|
7469
7624
|
out3.push(
|
|
7470
|
-
makeFinding(
|
|
7625
|
+
makeFinding(RULE8, policy, {
|
|
7471
7626
|
file: c.path,
|
|
7472
7627
|
line: earlyExit.newLine ?? void 0,
|
|
7473
7628
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -7489,7 +7644,7 @@ They WILL FAIL in v10.0.0
|
|
|
7489
7644
|
deadGuard2 ? `a guard that can never be true was inserted (\`${deadGuard2.content.trim()}\`)` : null
|
|
7490
7645
|
].filter((d) => d !== null);
|
|
7491
7646
|
out3.push(
|
|
7492
|
-
makeFinding(
|
|
7647
|
+
makeFinding(RULE8, policy, {
|
|
7493
7648
|
file: c.path,
|
|
7494
7649
|
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
7650
|
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 +7656,7 @@ They WILL FAIL in v10.0.0
|
|
|
7501
7656
|
}
|
|
7502
7657
|
if (shebangChanged && c.after != null) {
|
|
7503
7658
|
out3.push(
|
|
7504
|
-
makeFinding(
|
|
7659
|
+
makeFinding(RULE8, policy, {
|
|
7505
7660
|
file: c.path,
|
|
7506
7661
|
line: 1,
|
|
7507
7662
|
message: `A protected hook's interpreter was changed: ${shebang}.`,
|
|
@@ -7512,7 +7667,7 @@ They WILL FAIL in v10.0.0
|
|
|
7512
7667
|
}
|
|
7513
7668
|
if (earlyExit) {
|
|
7514
7669
|
out3.push(
|
|
7515
|
-
makeFinding(
|
|
7670
|
+
makeFinding(RULE8, policy, {
|
|
7516
7671
|
file: c.path,
|
|
7517
7672
|
line: earlyExit.newLine ?? void 0,
|
|
7518
7673
|
message: "An early `exit 0` was inserted into a protected hook \u2014 it now passes everything.",
|
|
@@ -7524,7 +7679,7 @@ They WILL FAIL in v10.0.0
|
|
|
7524
7679
|
const deadGuard = c.before == null || c.after == null ? added.find((l) => insertsDeadGuard(l.content)) : void 0;
|
|
7525
7680
|
if (deadGuard) {
|
|
7526
7681
|
out3.push(
|
|
7527
|
-
makeFinding(
|
|
7682
|
+
makeFinding(RULE8, policy, {
|
|
7528
7683
|
file: c.path,
|
|
7529
7684
|
line: deadGuard.newLine ?? void 0,
|
|
7530
7685
|
message: "A guard that can never be true was inserted into a protected hook \u2014 what it wraps no longer runs.",
|
|
@@ -7536,7 +7691,7 @@ They WILL FAIL in v10.0.0
|
|
|
7536
7691
|
const lost = scriptWeakening(beforeLines, afterLines, opts);
|
|
7537
7692
|
if (lost.length) {
|
|
7538
7693
|
out3.push(
|
|
7539
|
-
makeFinding(
|
|
7694
|
+
makeFinding(RULE8, policy, {
|
|
7540
7695
|
file: c.path,
|
|
7541
7696
|
message: `A check invocation was removed from a protected hook: ${lost[0].reason}.`,
|
|
7542
7697
|
evidence: lost[0].evidence,
|
|
@@ -7555,7 +7710,7 @@ They WILL FAIL in v10.0.0
|
|
|
7555
7710
|
const m = l.content.match(INLINE);
|
|
7556
7711
|
if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
|
|
7557
7712
|
out3.push(
|
|
7558
|
-
makeFinding(
|
|
7713
|
+
makeFinding(RULE8, policy, {
|
|
7559
7714
|
file: c.path,
|
|
7560
7715
|
line: l.newLine ?? void 0,
|
|
7561
7716
|
message: `Rule "${m[1]}" was lowered from block to warn in the policy.`,
|
|
@@ -7565,7 +7720,7 @@ They WILL FAIL in v10.0.0
|
|
|
7565
7720
|
);
|
|
7566
7721
|
} else if (/enabled:\s*false\b/.test(l.content)) {
|
|
7567
7722
|
out3.push(
|
|
7568
|
-
makeFinding(
|
|
7723
|
+
makeFinding(RULE8, policy, {
|
|
7569
7724
|
file: c.path,
|
|
7570
7725
|
line: l.newLine ?? void 0,
|
|
7571
7726
|
message: "A rule was disabled (enabled: false) in the policy.",
|
|
@@ -7583,7 +7738,7 @@ They WILL FAIL in v10.0.0
|
|
|
7583
7738
|
const hiding = updateIndexHiding(toks, policy);
|
|
7584
7739
|
if (hiding) {
|
|
7585
7740
|
out3.push(
|
|
7586
|
-
makeFinding(
|
|
7741
|
+
makeFinding(RULE8, policy, {
|
|
7587
7742
|
file: repoRelative(hiding.file, ctx),
|
|
7588
7743
|
message: `Hook tampering via shell: ${hiding.reason}.`,
|
|
7589
7744
|
evidence: seg,
|
|
@@ -7599,7 +7754,7 @@ They WILL FAIL in v10.0.0
|
|
|
7599
7754
|
const file = target ? repoRelative(target, ctx) : void 0;
|
|
7600
7755
|
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
7756
|
out3.push(
|
|
7602
|
-
makeFinding(
|
|
7757
|
+
makeFinding(RULE8, policy, {
|
|
7603
7758
|
...file !== void 0 ? { file } : {},
|
|
7604
7759
|
message: `Hook tampering via shell: ${why}${outside}.`,
|
|
7605
7760
|
evidence: seg,
|
|
@@ -7835,7 +7990,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
7835
7990
|
if (!was || !now || was.path === now.path) return [];
|
|
7836
7991
|
const samples = runnerSamples(now.path, policy, ctx, "pytest");
|
|
7837
7992
|
return suiteNarrowings(was.content, now.content, now.path, samples).map(
|
|
7838
|
-
(n) => makeFinding(
|
|
7993
|
+
(n) => makeFinding(RULE9, policy, {
|
|
7839
7994
|
file: now.path,
|
|
7840
7995
|
message: `The effective pytest config moved from ${was.path} to ${now.path}, and ${n.path} is no longer selected: ${n.reason}.`,
|
|
7841
7996
|
evidence: `inifile precedence ${was.path} \u2192 ${now.path} drops ${n.path} from the suite`,
|
|
@@ -7843,7 +7998,7 @@ function pytestPrecedenceFindings(changes, policy, ctx) {
|
|
|
7843
7998
|
})
|
|
7844
7999
|
);
|
|
7845
8000
|
}
|
|
7846
|
-
var
|
|
8001
|
+
var RULE9, JVM_TEST, TEST_DEFS, TABLE_METHOD, isEachOf, isDescribeEach, OTHER_RUNNER_DIR, WHOLE_SUITE_SCRIPT, SLICE_SCRIPT, SED_INPLACE, testDeletion;
|
|
7847
8002
|
var init_test_deletion = __esm({
|
|
7848
8003
|
"src/detectors/test-deletion.ts"() {
|
|
7849
8004
|
"use strict";
|
|
@@ -7856,7 +8011,7 @@ var init_test_deletion = __esm({
|
|
|
7856
8011
|
init_suite_config();
|
|
7857
8012
|
init_invocation();
|
|
7858
8013
|
init_narrow();
|
|
7859
|
-
|
|
8014
|
+
RULE9 = "test-deletion";
|
|
7860
8015
|
JVM_TEST = /^\s*(?:@\w+(?:\([^)]*\))?\s+)*@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b/;
|
|
7861
8016
|
TEST_DEFS = {
|
|
7862
8017
|
py: /^\s*(?:async\s+)?def\s+test\w*\s*\(/,
|
|
@@ -7876,7 +8031,7 @@ var init_test_deletion = __esm({
|
|
|
7876
8031
|
SLICE_SCRIPT = /^test:(?:unit|integration)$/;
|
|
7877
8032
|
SED_INPLACE = /(?:^|\s)(?:-i\b|--in-place\b|-[a-zA-Z]*i[a-zA-Z]*\b)/;
|
|
7878
8033
|
testDeletion = {
|
|
7879
|
-
id:
|
|
8034
|
+
id: RULE9,
|
|
7880
8035
|
surface: ["file", "command"],
|
|
7881
8036
|
certainty: "mechanical",
|
|
7882
8037
|
run(changes, policy, _view, ctx) {
|
|
@@ -7908,7 +8063,7 @@ var init_test_deletion = __esm({
|
|
|
7908
8063
|
if (c.op === "delete" && isTest) {
|
|
7909
8064
|
if (c.before != null && isRelocation(significantLines(c.before, c.path), countTestBlocks(c.before, c.path))) continue;
|
|
7910
8065
|
out3.push(
|
|
7911
|
-
makeFinding(
|
|
8066
|
+
makeFinding(RULE9, policy, {
|
|
7912
8067
|
file: c.path,
|
|
7913
8068
|
message: "A test file was deleted.",
|
|
7914
8069
|
evidence: c.path,
|
|
@@ -7917,7 +8072,7 @@ var init_test_deletion = __esm({
|
|
|
7917
8072
|
);
|
|
7918
8073
|
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && !isTest) {
|
|
7919
8074
|
out3.push(
|
|
7920
|
-
makeFinding(
|
|
8075
|
+
makeFinding(RULE9, policy, {
|
|
7921
8076
|
file: c.path,
|
|
7922
8077
|
message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
|
|
7923
8078
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -7926,7 +8081,7 @@ var init_test_deletion = __esm({
|
|
|
7926
8081
|
);
|
|
7927
8082
|
} else if (c.op === "rename" && c.oldPath && isSpec(c.oldPath) && isTest && leavesWalk(c.oldPath, c.path)) {
|
|
7928
8083
|
out3.push(
|
|
7929
|
-
makeFinding(
|
|
8084
|
+
makeFinding(RULE9, policy, {
|
|
7930
8085
|
file: c.path,
|
|
7931
8086
|
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
8087
|
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
@@ -7941,7 +8096,7 @@ var init_test_deletion = __esm({
|
|
|
7941
8096
|
const removed = new Set([...significantLines(c.before, c.path)].filter((l) => !afterLines.has(l)));
|
|
7942
8097
|
if (isRelocation(removed, before.min - after.min)) continue;
|
|
7943
8098
|
out3.push(
|
|
7944
|
-
makeFinding(
|
|
8099
|
+
makeFinding(RULE9, policy, {
|
|
7945
8100
|
file: c.path,
|
|
7946
8101
|
message: `Test blocks removed: ${before.min} \u2192 ${after.min} it()/test() in this spec (each-table rows count as tests).`,
|
|
7947
8102
|
evidence: `${before.min - after.min} test block(s) removed from ${c.path}`,
|
|
@@ -7954,7 +8109,7 @@ var init_test_deletion = __esm({
|
|
|
7954
8109
|
const readable = rn !== "pytest" || effectivePytestConfig(c.path, ctx);
|
|
7955
8110
|
for (const n of readable ? suiteNarrowings(c.before, c.after, c.path, runnerSamples(c.path, policy, ctx, rn)) : []) {
|
|
7956
8111
|
out3.push(
|
|
7957
|
-
makeFinding(
|
|
8112
|
+
makeFinding(RULE9, policy, {
|
|
7958
8113
|
file: c.path,
|
|
7959
8114
|
message: `The runner no longer selects ${n.path}: ${n.reason}.`,
|
|
7960
8115
|
evidence: `${n.path} dropped from the suite by ${c.path}`,
|
|
@@ -7966,12 +8121,12 @@ var init_test_deletion = __esm({
|
|
|
7966
8121
|
const w = n.weakening;
|
|
7967
8122
|
const evidence = `"${n.script}": ${JSON.stringify(n.before)} \u2192 ${n.after === "" ? "deleted" : JSON.stringify(n.after)}`;
|
|
7968
8123
|
out3.push(
|
|
7969
|
-
w.state === "removed" ? makeFinding(
|
|
8124
|
+
w.state === "removed" ? makeFinding(RULE9, policy, {
|
|
7970
8125
|
file: c.path,
|
|
7971
8126
|
message: `The test script no longer runs the ${w.kind} check: scripts.${n.script} ${w.what}.`,
|
|
7972
8127
|
evidence,
|
|
7973
8128
|
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(
|
|
8129
|
+
}) : makeFinding(RULE9, policy, {
|
|
7975
8130
|
file: c.path,
|
|
7976
8131
|
message: `The test script now narrows the suite: ${w.what} ${w.direction} ${w.direction === "added" ? "to" : "from"} scripts.${n.script}.`,
|
|
7977
8132
|
evidence,
|
|
@@ -8062,7 +8217,7 @@ var init_test_deletion = __esm({
|
|
|
8062
8217
|
}
|
|
8063
8218
|
if (why) {
|
|
8064
8219
|
out3.push(
|
|
8065
|
-
makeFinding(
|
|
8220
|
+
makeFinding(RULE9, policy, {
|
|
8066
8221
|
message: `Test removed via shell: ${why}.`,
|
|
8067
8222
|
evidence,
|
|
8068
8223
|
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 +8273,7 @@ function significantLinesOrdered(src, path) {
|
|
|
8118
8273
|
}
|
|
8119
8274
|
return out3;
|
|
8120
8275
|
}
|
|
8121
|
-
var
|
|
8276
|
+
var RULE10, MIN_REMOVED_LINES, MIN_REMOVED_ROWS, ws, TEST_DIR_FILE, testContentRemoval;
|
|
8122
8277
|
var init_test_content_removal = __esm({
|
|
8123
8278
|
"src/detectors/test-content-removal.ts"() {
|
|
8124
8279
|
"use strict";
|
|
@@ -8127,13 +8282,13 @@ var init_test_content_removal = __esm({
|
|
|
8127
8282
|
init_finding();
|
|
8128
8283
|
init_test_deletion();
|
|
8129
8284
|
init_files();
|
|
8130
|
-
|
|
8285
|
+
RULE10 = "test-content-removal";
|
|
8131
8286
|
MIN_REMOVED_LINES = 3;
|
|
8132
8287
|
MIN_REMOVED_ROWS = 2;
|
|
8133
8288
|
ws = (s) => s.replace(/\s+/g, "");
|
|
8134
8289
|
TEST_DIR_FILE = /(?:^|\/)(?:test|tests|__tests__|spec|specs)\//;
|
|
8135
8290
|
testContentRemoval = {
|
|
8136
|
-
id:
|
|
8291
|
+
id: RULE10,
|
|
8137
8292
|
surface: ["file"],
|
|
8138
8293
|
certainty: "mechanical",
|
|
8139
8294
|
run(changes, policy) {
|
|
@@ -8174,7 +8329,7 @@ var init_test_content_removal = __esm({
|
|
|
8174
8329
|
const goneRows = table.rows.size && tableAfter.open ? [...table.rows].filter(([k]) => !tableAfter.rows.has(k) && !addedPool.includes(k)).map(([, text]) => text) : [];
|
|
8175
8330
|
if (goneRows.length >= MIN_REMOVED_ROWS) {
|
|
8176
8331
|
out3.push(
|
|
8177
|
-
makeFinding(
|
|
8332
|
+
makeFinding(RULE10, policy, {
|
|
8178
8333
|
file: c.path,
|
|
8179
8334
|
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
8335
|
evidence: goneRows.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -8185,7 +8340,7 @@ var init_test_content_removal = __esm({
|
|
|
8185
8340
|
}
|
|
8186
8341
|
if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
|
|
8187
8342
|
out3.push(
|
|
8188
|
-
makeFinding(
|
|
8343
|
+
makeFinding(RULE10, policy, {
|
|
8189
8344
|
file: c.path,
|
|
8190
8345
|
message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
|
|
8191
8346
|
evidence: gone.slice(0, 3).map((l) => l.slice(0, 80)).join(" | "),
|
|
@@ -8208,14 +8363,14 @@ function namesProtectedSnapshot(seg, policy) {
|
|
|
8208
8363
|
}
|
|
8209
8364
|
return null;
|
|
8210
8365
|
}
|
|
8211
|
-
var
|
|
8366
|
+
var RULE11, RUNNER3, PM_TEST_SCRIPT, UPDATE_ANY, UPDATE_WITH_RUNNER, INSTA_ACCEPT, UPDATE_EXPECT, REGEN_SCRIPT, MUTATE, snapshotRewrite;
|
|
8212
8367
|
var init_snapshot_rewrite = __esm({
|
|
8213
8368
|
"src/detectors/snapshot-rewrite.ts"() {
|
|
8214
8369
|
"use strict";
|
|
8215
8370
|
init_policy();
|
|
8216
8371
|
init_finding();
|
|
8217
8372
|
init_command();
|
|
8218
|
-
|
|
8373
|
+
RULE11 = "snapshot-rewrite";
|
|
8219
8374
|
RUNNER3 = /\b(?:jest|vitest|playwright|ava)\b/;
|
|
8220
8375
|
PM_TEST_SCRIPT = /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+(?:t|tst|test|run(?:-script)?\s+\S*test\S*)(?:\s|$)/;
|
|
8221
8376
|
UPDATE_ANY = /--(?:update-?[sS]napshots?|test-update-snapshots|snapshot-update)\b/;
|
|
@@ -8225,12 +8380,12 @@ var init_snapshot_rewrite = __esm({
|
|
|
8225
8380
|
REGEN_SCRIPT = /\b(?:update|regen(?:erate)?|re?bless)[-_.]?(?:golden|snapshots?|baselines?|expected)\b/i;
|
|
8226
8381
|
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
8382
|
snapshotRewrite = {
|
|
8228
|
-
id:
|
|
8383
|
+
id: RULE11,
|
|
8229
8384
|
surface: ["command", "file"],
|
|
8230
8385
|
certainty: "mechanical",
|
|
8231
8386
|
run(changes, policy) {
|
|
8232
8387
|
const out3 = [];
|
|
8233
|
-
const warn = (input) => out3.push(makeFinding(
|
|
8388
|
+
const warn = (input) => out3.push(makeFinding(RULE11, policy, { ...input, defaultSeverity: "warn" }));
|
|
8234
8389
|
for (const c of changes) {
|
|
8235
8390
|
if (c.kind === "command") {
|
|
8236
8391
|
for (const seg of segments(c.raw)) {
|
|
@@ -8278,16 +8433,16 @@ function isSnapshotChange(c, policy) {
|
|
|
8278
8433
|
if (isProtected(c.path, policy, "snapshots")) return true;
|
|
8279
8434
|
return c.oldPath != null && isProtected(c.oldPath, policy, "snapshots");
|
|
8280
8435
|
}
|
|
8281
|
-
var
|
|
8436
|
+
var RULE12, COMMIT_VIEWS, snapshotOnlyRewrite;
|
|
8282
8437
|
var init_snapshot_only = __esm({
|
|
8283
8438
|
"src/detectors/snapshot-only.ts"() {
|
|
8284
8439
|
"use strict";
|
|
8285
8440
|
init_policy();
|
|
8286
8441
|
init_finding();
|
|
8287
|
-
|
|
8442
|
+
RULE12 = "snapshot-only-rewrite";
|
|
8288
8443
|
COMMIT_VIEWS = ["staged", "range"];
|
|
8289
8444
|
snapshotOnlyRewrite = {
|
|
8290
|
-
id:
|
|
8445
|
+
id: RULE12,
|
|
8291
8446
|
surface: ["file"],
|
|
8292
8447
|
certainty: "mechanical",
|
|
8293
8448
|
run(changes, policy, view) {
|
|
@@ -8300,7 +8455,7 @@ var init_snapshot_only = __esm({
|
|
|
8300
8455
|
if (!first) return [];
|
|
8301
8456
|
const names = snaps.map((c) => c.path).slice(0, 3).join(", ");
|
|
8302
8457
|
return [
|
|
8303
|
-
makeFinding(
|
|
8458
|
+
makeFinding(RULE12, policy, {
|
|
8304
8459
|
file: first.path,
|
|
8305
8460
|
message: `Only recorded expectations changed in this diff (${snaps.length} snapshot file${snaps.length === 1 ? "" : "s"}, no code change to explain them).`,
|
|
8306
8461
|
evidence: names + (snaps.length > 3 ? ", \u2026" : ""),
|
|
@@ -8474,7 +8629,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
8474
8629
|
const weakened2 = directionalWeakening(before.assertions, after.assertions);
|
|
8475
8630
|
if (weakened2) {
|
|
8476
8631
|
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(
|
|
8632
|
+
return makeFinding(RULE13, policy, {
|
|
8478
8633
|
file: path,
|
|
8479
8634
|
message,
|
|
8480
8635
|
evidence: `${weakened2.before.text} -> ${weakened2.after.text}`,
|
|
@@ -8483,7 +8638,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
8483
8638
|
}
|
|
8484
8639
|
const removed = pureRemoval(before.assertions, after.assertions);
|
|
8485
8640
|
if (removed) {
|
|
8486
|
-
return makeFinding(
|
|
8641
|
+
return makeFinding(RULE13, policy, {
|
|
8487
8642
|
file: path,
|
|
8488
8643
|
message: `Assertion removed from kept test "${before.identity}" without a replacement assertion.`,
|
|
8489
8644
|
evidence: removed.text,
|
|
@@ -8492,7 +8647,7 @@ function findingForBlock(path, before, after, policy) {
|
|
|
8492
8647
|
}
|
|
8493
8648
|
return null;
|
|
8494
8649
|
}
|
|
8495
|
-
var
|
|
8650
|
+
var RULE13, TEST_CALLS, SUITE_CALLS, EXACT_MATCHERS, WEAK_MATCHERS, compact, assertionWeakening;
|
|
8496
8651
|
var init_assertion_weakening = __esm({
|
|
8497
8652
|
"src/detectors/assertion-weakening.ts"() {
|
|
8498
8653
|
"use strict";
|
|
@@ -8500,14 +8655,14 @@ var init_assertion_weakening = __esm({
|
|
|
8500
8655
|
init_policy();
|
|
8501
8656
|
init_finding();
|
|
8502
8657
|
init_files();
|
|
8503
|
-
|
|
8658
|
+
RULE13 = "assertion-weakening";
|
|
8504
8659
|
TEST_CALLS = /* @__PURE__ */ new Set(["it", "test"]);
|
|
8505
8660
|
SUITE_CALLS = /* @__PURE__ */ new Set(["describe", "suite", "context"]);
|
|
8506
8661
|
EXACT_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual", "toMatchObject"]);
|
|
8507
8662
|
WEAK_MATCHERS = /* @__PURE__ */ new Set(["toBeTruthy", "toBeDefined"]);
|
|
8508
8663
|
compact = (s) => s.replace(/\s+/g, "");
|
|
8509
8664
|
assertionWeakening = {
|
|
8510
|
-
id:
|
|
8665
|
+
id: RULE13,
|
|
8511
8666
|
surface: ["file"],
|
|
8512
8667
|
certainty: "heuristic",
|
|
8513
8668
|
run(changes, policy) {
|
|
@@ -8542,6 +8697,7 @@ var init_detectors = __esm({
|
|
|
8542
8697
|
init_lint_suppression();
|
|
8543
8698
|
init_test_skip();
|
|
8544
8699
|
init_coverage_lowering();
|
|
8700
|
+
init_coverage_exclusion();
|
|
8545
8701
|
init_ci_tampering();
|
|
8546
8702
|
init_hook_tampering();
|
|
8547
8703
|
init_test_deletion();
|
|
@@ -8556,6 +8712,7 @@ var init_detectors = __esm({
|
|
|
8556
8712
|
lintSuppression,
|
|
8557
8713
|
testSkip,
|
|
8558
8714
|
coverageLowering,
|
|
8715
|
+
coverageExclusion,
|
|
8559
8716
|
ciTampering,
|
|
8560
8717
|
hookTampering,
|
|
8561
8718
|
testDeletion,
|
|
@@ -10027,7 +10184,7 @@ function drainEvents(log, offset, maxTotalBytes = MAX_EVENT_SWEEP_BYTES, io = DE
|
|
|
10027
10184
|
};
|
|
10028
10185
|
}
|
|
10029
10186
|
function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
10030
|
-
if (!isEnabled(
|
|
10187
|
+
if (!isEnabled(RULE14, policy)) return [];
|
|
10031
10188
|
const byPath = /* @__PURE__ */ new Map();
|
|
10032
10189
|
for (const e of events) {
|
|
10033
10190
|
if (!isProtected(e.path, policy)) continue;
|
|
@@ -10047,7 +10204,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
10047
10204
|
const modeChurn = modes.size > 1;
|
|
10048
10205
|
if (!contentChurn && !modeChurn) continue;
|
|
10049
10206
|
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(
|
|
10207
|
+
const f = makeFinding(RULE14, policy, {
|
|
10051
10208
|
file: path,
|
|
10052
10209
|
message: `Transient protected mutation: ${what}, leaving no net diff.`,
|
|
10053
10210
|
evidence: evs.slice(0, 4).map((e) => `${e.ts} ${e.hash ?? "gone"}${e.mode != null ? `/${(e.mode & 4095).toString(8)}` : ""}`).join(" | "),
|
|
@@ -10062,14 +10219,14 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
10062
10219
|
}
|
|
10063
10220
|
return out3;
|
|
10064
10221
|
}
|
|
10065
|
-
var
|
|
10222
|
+
var RULE14, MAX_EVENT_READ_BYTES, MAX_EVENT_SWEEP_BYTES, DEFAULT_EVENT_READ_IO;
|
|
10066
10223
|
var init_fs_events = __esm({
|
|
10067
10224
|
"src/detectors/fs-events.ts"() {
|
|
10068
10225
|
"use strict";
|
|
10069
10226
|
init_policy();
|
|
10070
10227
|
init_finding();
|
|
10071
10228
|
init_narrow();
|
|
10072
|
-
|
|
10229
|
+
RULE14 = "transient-protected-mutation";
|
|
10073
10230
|
MAX_EVENT_READ_BYTES = 4 * 1024 * 1024;
|
|
10074
10231
|
MAX_EVENT_SWEEP_BYTES = 16 * 1024 * 1024;
|
|
10075
10232
|
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.0",
|
|
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",
|