tamperward 2.24.0 → 2.24.2
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/dist/cli/index.js +106 -6
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -3715,6 +3715,22 @@ var init_lint_suppression = __esm({
|
|
|
3715
3715
|
|
|
3716
3716
|
// src/detectors/test-skip.ts
|
|
3717
3717
|
import { posix as posix2 } from "node:path";
|
|
3718
|
+
function pyMarkAliases(source) {
|
|
3719
|
+
const out3 = /* @__PURE__ */ new Set();
|
|
3720
|
+
for (const m of source.matchAll(PY_MARK_ALIAS)) out3.add(m[1]);
|
|
3721
|
+
return out3;
|
|
3722
|
+
}
|
|
3723
|
+
function pyAliasDecoratorHits(c) {
|
|
3724
|
+
const added = addedLines(c);
|
|
3725
|
+
const aliases = pyMarkAliases(c.after ?? added.map((l) => l.content).join("\n"));
|
|
3726
|
+
if (aliases.size === 0) return [];
|
|
3727
|
+
const hits = [];
|
|
3728
|
+
for (const l of added) {
|
|
3729
|
+
const m = PY_DECORATOR.exec(l.content);
|
|
3730
|
+
if (m && aliases.has(m[1])) hits.push({ line: l.newLine ?? void 0, evidence: l.content.trim() });
|
|
3731
|
+
}
|
|
3732
|
+
return hits;
|
|
3733
|
+
}
|
|
3718
3734
|
function matchesOutsideString(p, content, lang) {
|
|
3719
3735
|
if (p.comment) return p.re.test(content);
|
|
3720
3736
|
const re = p.re.global ? p.re : new RegExp(p.re.source, p.re.flags + "g");
|
|
@@ -4046,7 +4062,7 @@ function astSkipHits(c, resolvers) {
|
|
|
4046
4062
|
}
|
|
4047
4063
|
return { hits, authoritative: true, unowned: analysis.unowned };
|
|
4048
4064
|
}
|
|
4049
|
-
var RULE4, acc, JS_RUNNER, JS_MOD, PATTERNS2, JS_RUNNERS, JS_CHAIN_MODIFIERS, JS_DEFAULT_RUNNER_MODULES, JS_TEST_MODULES, RUNNER2, NON_RUNNER, UNKNOWN, withCause, MODULE_EXTENSIONS, RunnerResolver, ModuleBinder, NO_AST, testSkip;
|
|
4065
|
+
var RULE4, acc, JS_RUNNER, JS_MOD, GO_SKIP_PATTERNS, PY_MARK, PY_CONFTEST_PATTERNS, PY_MARK_ALIAS, PY_DECORATOR, PATTERNS2, JS_RUNNERS, JS_CHAIN_MODIFIERS, JS_DEFAULT_RUNNER_MODULES, JS_TEST_MODULES, RUNNER2, NON_RUNNER, UNKNOWN, withCause, MODULE_EXTENSIONS, RunnerResolver, ModuleBinder, NO_AST, testSkip;
|
|
4050
4066
|
var init_test_skip = __esm({
|
|
4051
4067
|
"src/detectors/test-skip.ts"() {
|
|
4052
4068
|
"use strict";
|
|
@@ -4060,6 +4076,21 @@ var init_test_skip = __esm({
|
|
|
4060
4076
|
acc = (names) => `(?:\\s*\\.\\s*(?:${names})(?![\\w$])|\\s*\\[\\s*['"\`](?:${names})['"\`]\\s*\\])`;
|
|
4061
4077
|
JS_RUNNER = "\\b(?:it|test|describe|suite)";
|
|
4062
4078
|
JS_MOD = "concurrent|sequential|shuffle|serial|parallel";
|
|
4079
|
+
GO_SKIP_PATTERNS = [
|
|
4080
|
+
{ re: /\b\w+\.Skip(?:f|Now)?\(/, why: "a runtime t.Skip()/Skipf()/SkipNow() call" },
|
|
4081
|
+
{ re: /\bT\(\)\.Skip(?:f|Now)?\(/, why: "a runtime T().Skip()/Skipf()/SkipNow() call (testify suite)" }
|
|
4082
|
+
];
|
|
4083
|
+
PY_MARK = "(?:\\w+\\.)?mark\\.(?:skip|skipif|xfail)\\b";
|
|
4084
|
+
PY_CONFTEST_PATTERNS = [
|
|
4085
|
+
{ re: /\bdef\s+pytest_collection_modifyitems\s*\(/, why: "a pytest_collection_modifyitems hook (collected tests are filtered or marked before they run)" },
|
|
4086
|
+
{ re: /\bdef\s+pytest_pycollect_makeitem\s*\(/, why: "a pytest_pycollect_makeitem hook (what counts as a test is decided here)" },
|
|
4087
|
+
{ re: /\bdef\s+pytest_runtest_makereport\s*\(/, why: "a pytest_runtest_makereport hook (the test report can be rewritten)" },
|
|
4088
|
+
{ re: new RegExp("\\.add_marker\\(\\s*" + PY_MARK), why: "an add_marker(mark.skip/skipif/xfail) call (the test is skipped programmatically)" },
|
|
4089
|
+
{ re: new RegExp("^\\s*\\w+\\s*=\\s*" + PY_MARK), why: "a skip/skipif/xfail marker bound to a name (an aliased decorator)" },
|
|
4090
|
+
{ re: /\b\w+\.outcome\s*=\s*['"](?:passed|skipped)['"]/, why: "a report outcome rewritten to passed/skipped" }
|
|
4091
|
+
];
|
|
4092
|
+
PY_MARK_ALIAS = new RegExp("^\\s*(\\w+)\\s*=\\s*" + PY_MARK, "gm");
|
|
4093
|
+
PY_DECORATOR = /^\s*@(\w+)\s*(?:\(|$)/;
|
|
4063
4094
|
PATTERNS2 = {
|
|
4064
4095
|
js: [
|
|
4065
4096
|
// The skip/only/todo family on a runner, by dot-or-bracket access, with one optional
|
|
@@ -4103,10 +4134,13 @@ var init_test_skip = __esm({
|
|
|
4103
4134
|
{ re: /@(?:unittest\.)?(?:skip|skipIf|skipUnless|expectedFailure)\b/, why: "a unittest skip/expectedFailure decorator" },
|
|
4104
4135
|
{ re: /\bself\.skipTest\(/, why: "a runtime self.skipTest() call" },
|
|
4105
4136
|
{ re: /\braise\s+(?:unittest\.)?SkipTest\b/, why: "a raised SkipTest" },
|
|
4106
|
-
{ re: /\b__test__\s*=\s*False\b/, why: "__test__ = False hides the test from collection" }
|
|
4137
|
+
{ re: /\b__test__\s*=\s*False\b/, why: "__test__ = False hides the test from collection" },
|
|
4138
|
+
...PY_CONFTEST_PATTERNS
|
|
4139
|
+
// #441
|
|
4107
4140
|
],
|
|
4108
4141
|
go: [
|
|
4109
|
-
|
|
4142
|
+
...GO_SKIP_PATTERNS,
|
|
4143
|
+
// #441: any receiver, and testify's T()
|
|
4110
4144
|
{ re: /\bif\s+testing\.Short\(\)/, why: "a testing.Short() guard (the body is skipped under -short)" },
|
|
4111
4145
|
{ re: /^\s*\/\/\s*(?:go:build|\+build)\s+ignore\b/, why: "a build-ignore constraint (the file is excluded from the test run)", comment: true }
|
|
4112
4146
|
],
|
|
@@ -4440,6 +4474,20 @@ var init_test_skip = __esm({
|
|
|
4440
4474
|
);
|
|
4441
4475
|
}
|
|
4442
4476
|
}
|
|
4477
|
+
if (lang === "py") {
|
|
4478
|
+
for (const hit of pyAliasDecoratorHits(c)) {
|
|
4479
|
+
if (hit.line != null) astHitLines.add(hit.line);
|
|
4480
|
+
out3.push(
|
|
4481
|
+
makeFinding(RULE4, policy, {
|
|
4482
|
+
file: c.path,
|
|
4483
|
+
line: hit.line,
|
|
4484
|
+
message: "Test skipped or narrowed: a decorator aliasing a pytest skip/skipif/xfail marker.",
|
|
4485
|
+
evidence: hit.evidence,
|
|
4486
|
+
remediation: "Make the test pass rather than skipping it. If it is genuinely obsolete, a human must sign off."
|
|
4487
|
+
})
|
|
4488
|
+
);
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4443
4491
|
for (const l of addedLines(c)) {
|
|
4444
4492
|
if (l.newLine != null && astHitLines.has(l.newLine)) continue;
|
|
4445
4493
|
const comment = isCommentLine(l.content.trim(), lang);
|
|
@@ -7365,6 +7413,50 @@ function syntheticBase(over) {
|
|
|
7365
7413
|
}
|
|
7366
7414
|
return base;
|
|
7367
7415
|
}
|
|
7416
|
+
function freshLefthookBase(after) {
|
|
7417
|
+
const base = {};
|
|
7418
|
+
for (const [section, v] of Object.entries(after)) {
|
|
7419
|
+
if (!isObj(v)) continue;
|
|
7420
|
+
for (const group of ["commands", "scripts"]) {
|
|
7421
|
+
const g = v[group];
|
|
7422
|
+
if (!isObj(g)) continue;
|
|
7423
|
+
for (const [name, cfg] of Object.entries(g)) {
|
|
7424
|
+
if (!isObj(cfg)) continue;
|
|
7425
|
+
const run = cfg.run ?? cfg.runner;
|
|
7426
|
+
if (!namesGate(name) && !namesGate(run)) continue;
|
|
7427
|
+
const sec = base[section] ??= {};
|
|
7428
|
+
const grp = sec[group] ??= {};
|
|
7429
|
+
grp[name] = { run: typeof run === "string" ? liveOrCanonical(run) : CANONICAL_GATE };
|
|
7430
|
+
}
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
return base;
|
|
7434
|
+
}
|
|
7435
|
+
function freshPreCommitBase(after) {
|
|
7436
|
+
const stagesOf = (v) => Array.isArray(v) && v.map(String).some((s) => COMMIT_STAGES.has(s)) ? v.map(String) : null;
|
|
7437
|
+
const defaults = stagesOf(after.default_stages);
|
|
7438
|
+
const repos = [];
|
|
7439
|
+
for (const repo of Array.isArray(after.repos) ? after.repos : []) {
|
|
7440
|
+
if (!isObj(repo)) continue;
|
|
7441
|
+
const hooks = [];
|
|
7442
|
+
for (const h of Array.isArray(repo.hooks) ? repo.hooks : []) {
|
|
7443
|
+
if (!isObj(h)) continue;
|
|
7444
|
+
if (![repo.repo, h.id, h.entry, h.name].some(namesGate)) continue;
|
|
7445
|
+
const fresh = { id: h.id, name: h.name };
|
|
7446
|
+
if (h.entry !== void 0) {
|
|
7447
|
+
const args = Array.isArray(h.args) ? h.args.map(String).join(" ") : "";
|
|
7448
|
+
fresh.entry = liveOrCanonical(`${typeof h.entry === "string" ? h.entry : ""} ${args}`.trim());
|
|
7449
|
+
}
|
|
7450
|
+
const stages = stagesOf(h.stages);
|
|
7451
|
+
if (stages) fresh.stages = stages;
|
|
7452
|
+
hooks.push(fresh);
|
|
7453
|
+
}
|
|
7454
|
+
repos.push({ repo: repo.repo, hooks });
|
|
7455
|
+
}
|
|
7456
|
+
const base = { repos };
|
|
7457
|
+
if (defaults) base.default_stages = defaults;
|
|
7458
|
+
return base;
|
|
7459
|
+
}
|
|
7368
7460
|
function configFindings(c, changes, policy, ctx) {
|
|
7369
7461
|
if (c.after == null) return null;
|
|
7370
7462
|
let reasons;
|
|
@@ -7374,8 +7466,11 @@ function configFindings(c, changes, policy, ctx) {
|
|
|
7374
7466
|
if (overBefore === null) return null;
|
|
7375
7467
|
const base = lefthookBase(c, changes, ctx) ?? syntheticBase(overAfter ?? {});
|
|
7376
7468
|
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));
|
|
7469
|
+
} else if (c.before == null) {
|
|
7470
|
+
const after = parseDoc(c.after);
|
|
7471
|
+
if (after === null) return [];
|
|
7472
|
+
reasons = isLefthook(c.path) ? lefthookWeakening(freshLefthookBase(after), after) : preCommitWeakening(freshPreCommitBase(after), after);
|
|
7377
7473
|
} else {
|
|
7378
|
-
if (c.before == null) return null;
|
|
7379
7474
|
const before = parseDoc(c.before);
|
|
7380
7475
|
const after = parseDoc(c.after);
|
|
7381
7476
|
if (before === null) return null;
|
|
@@ -7433,7 +7528,7 @@ function huskyRuntimeWrite(path, content, ctx) {
|
|
|
7433
7528
|
(i) => i.identity === "tamperward check --staged" && i.state === "live" && i.pin === TW_VERSION
|
|
7434
7529
|
);
|
|
7435
7530
|
}
|
|
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;
|
|
7531
|
+
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;
|
|
7437
7532
|
var init_hook_tampering = __esm({
|
|
7438
7533
|
"src/detectors/hook-tampering.ts"() {
|
|
7439
7534
|
"use strict";
|
|
@@ -7469,6 +7564,10 @@ var init_hook_tampering = __esm({
|
|
|
7469
7564
|
envMechanism = (k) => ENV_MECHANISM.find(([re]) => re.test(k))?.[1] ?? null;
|
|
7470
7565
|
mentionsGate = (v) => /\btamperward\b/.test(JSON.stringify(v) ?? "");
|
|
7471
7566
|
carriesGate = (s) => EVENTS.some((e) => gateCandidates(s, e).length > 0);
|
|
7567
|
+
CANONICAL_GATE = "npx tamperward check --staged";
|
|
7568
|
+
COMMIT_STAGES = /* @__PURE__ */ new Set(["pre-commit", "pre-push", "commit", "push"]);
|
|
7569
|
+
namesGate = (v) => typeof v === "string" && /\btamperward\b/.test(v);
|
|
7570
|
+
liveOrCanonical = (cmd) => gateOf(cmd) !== null ? cmd : CANONICAL_GATE;
|
|
7472
7571
|
HUSKY_SHIM = '#!/usr/bin/env sh\n. "$(dirname "$0")/h"';
|
|
7473
7572
|
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"]);
|
|
7474
7573
|
HUSKY_H_KNOWN = [
|
|
@@ -7508,7 +7607,7 @@ They WILL FAIL in v10.0.0
|
|
|
7508
7607
|
"`;
|
|
7509
7608
|
stripTrailingNl = (s) => s.endsWith("\n") ? s.slice(0, -1) : s;
|
|
7510
7609
|
isHuskyScript = (path) => /(?:^|\/)\.husky\/[^/]+$/.test(path);
|
|
7511
|
-
isHookNote = (path) => /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
|
|
7610
|
+
isHookNote = (path) => !/(?:^|\/)\.husky\/_\//.test(path) && /\.(?:md|markdown|txt)$|(?:^|\/)\.git(?:ignore|attributes)$/i.test(path);
|
|
7512
7611
|
hookTampering = {
|
|
7513
7612
|
id: RULE8,
|
|
7514
7613
|
surface: ["file", "command"],
|
|
@@ -7582,6 +7681,7 @@ They WILL FAIL in v10.0.0
|
|
|
7582
7681
|
out3.push(...semantic);
|
|
7583
7682
|
} else if (!c.path.endsWith(POLICY_FILE)) {
|
|
7584
7683
|
if ((c.op === "add" || c.op === "modify") && huskyRuntimeWrite(c.path, c.after, ctx)) continue;
|
|
7684
|
+
if (isHookNote(c.path)) continue;
|
|
7585
7685
|
const husky = isHuskyScript(c.path);
|
|
7586
7686
|
const opts = husky ? { errexit: true } : {};
|
|
7587
7687
|
const beforePin = c.before != null && c.after != null ? initScriptPin(c.before) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.24.
|
|
3
|
+
"version": "2.24.2",
|
|
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",
|