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.
Files changed (2) hide show
  1. package/dist/cli/index.js +106 -6
  2. 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
- { re: /\b[tb]\.Skip(?:f|Now)?\(/, why: "a runtime t.Skip()/Skipf()/SkipNow() call" },
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.0",
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",