tamperward 2.25.0 → 2.25.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 +195 -42
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -526,7 +526,22 @@ function defaultPolicy(version = 1) {
526
526
  "**/codecov.yml",
527
527
  "**/.codecov.yml"
528
528
  ],
529
- ci: [".github/workflows/**"],
529
+ // A workflow's checks can live in a composite action it `uses:` (the workflow
530
+ // keeps `uses: ./.github/actions/test` while the action's `run: npm test`
531
+ // becomes `echo ok`), and in the entry file of a CI system that is not GitHub
532
+ // Actions at all. Each is read by ci-tampering: the action's `runs.steps` get
533
+ // the workflow's removal/neutralisation pass, the other systems' entry files
534
+ // the generic check-line pass and no trigger logic (issue #437).
535
+ ci: [
536
+ ".github/workflows/**",
537
+ ".github/actions/**/action.y?(a)ml",
538
+ ".gitlab-ci.yml",
539
+ ".circleci/config.yml",
540
+ "Jenkinsfile",
541
+ "azure-pipelines.yml",
542
+ "bitbucket-pipelines.yml",
543
+ ".travis.yml"
544
+ ],
530
545
  // Recorded expected outputs. An assertion stored as data is still an assertion;
531
546
  // rewriting it from current output is the snapshot-update move the affordance
532
547
  // experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
@@ -3456,9 +3471,62 @@ function assertedType(node) {
3456
3471
  while (ts.isParenthesizedTypeNode(t)) t = t.type;
3457
3472
  return t;
3458
3473
  }
3459
- function isDoubleCast(node) {
3474
+ function bareKind(type) {
3475
+ let x = type;
3476
+ while (ts.isParenthesizedTypeNode(x)) x = x.type;
3477
+ switch (x.kind) {
3478
+ case ts.SyntaxKind.AnyKeyword:
3479
+ return "any";
3480
+ case ts.SyntaxKind.NeverKeyword:
3481
+ return "never";
3482
+ case ts.SyntaxKind.UnknownKeyword:
3483
+ return "unknown";
3484
+ case ts.SyntaxKind.ObjectKeyword:
3485
+ return "objectish";
3486
+ }
3487
+ if (ts.isTypeLiteralNode(x) && x.members.length === 0) return "empty";
3488
+ if (ts.isTypeReferenceNode(x) && ts.isIdentifier(x.typeName) && !x.typeArguments) {
3489
+ if (x.typeName.text === "Object") return "objectish";
3490
+ if (x.typeName.text === "const") return "const";
3491
+ }
3492
+ return null;
3493
+ }
3494
+ function kindOfType(type, resolveName) {
3495
+ const bare = bareKind(type);
3496
+ if (bare) return bare;
3497
+ let x = type;
3498
+ while (ts.isParenthesizedTypeNode(x)) x = x.type;
3499
+ if (ts.isTypeReferenceNode(x) && ts.isIdentifier(x.typeName) && !x.typeArguments) return resolveName(x.typeName.text);
3500
+ return null;
3501
+ }
3502
+ function buildAliasMap(sf) {
3503
+ const decls = /* @__PURE__ */ new Map();
3504
+ const collect2 = (node) => {
3505
+ if (ts.isTypeAliasDeclaration(node) && (!node.typeParameters || node.typeParameters.length === 0)) decls.set(node.name.text, node.type);
3506
+ ts.forEachChild(node, collect2);
3507
+ };
3508
+ collect2(sf);
3509
+ const resolve16 = (name, seen) => {
3510
+ if (seen.has(name)) return null;
3511
+ seen.add(name);
3512
+ const t = decls.get(name);
3513
+ return t ? kindOfType(t, (n) => resolve16(n, seen)) : null;
3514
+ };
3515
+ const resolved = /* @__PURE__ */ new Map();
3516
+ for (const name of decls.keys()) {
3517
+ const k = resolve16(name, /* @__PURE__ */ new Set());
3518
+ if (k) resolved.set(name, k);
3519
+ }
3520
+ return resolved;
3521
+ }
3522
+ function assertedLaunderKind(node, aliasMap) {
3523
+ return kindOfType(assertedType(node), (n) => aliasMap.get(n) ?? null);
3524
+ }
3525
+ function isDoubleCast(node, aliasMap) {
3460
3526
  const inner = unparenthesized(node.expression);
3461
- return (ts.isAsExpression(inner) || ts.isTypeAssertionExpression(inner)) && assertedType(inner).kind === ts.SyntaxKind.UnknownKeyword;
3527
+ if (!(ts.isAsExpression(inner) || ts.isTypeAssertionExpression(inner))) return false;
3528
+ const k = assertedLaunderKind(inner, aliasMap ?? EMPTY_ALIASES);
3529
+ return k != null && DOUBLE_CAST_INNER.has(k);
3462
3530
  }
3463
3531
  function surfaceOf(path, src) {
3464
3532
  const sf = parseSource(path, src, scriptKind(path));
@@ -3467,20 +3535,26 @@ function surfaceOf(path, src) {
3467
3535
  if (diagnostics.length > 0) return null;
3468
3536
  const surface = { type: 0, nonNull: 0, assertions: [] };
3469
3537
  const lines = src.split("\n");
3538
+ const aliasMap = buildAliasMap(sf);
3470
3539
  const record = (kind, node) => {
3471
3540
  const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
3472
3541
  surface.assertions.push({ kind, line, text: node.getText(sf).replace(/\s+/g, " "), lineText: (lines[line - 1] ?? "").trim() });
3473
3542
  if (kind === "type") surface.type++;
3474
3543
  else surface.nonNull++;
3475
3544
  };
3545
+ const launderInner = /* @__PURE__ */ new Set();
3546
+ const collectInners = (node) => {
3547
+ if ((ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) && isDoubleCast(node, aliasMap)) launderInner.add(unparenthesized(node.expression));
3548
+ ts.forEachChild(node, collectInners);
3549
+ };
3550
+ collectInners(sf);
3476
3551
  const isRow4OrHonest = (node) => {
3477
- const type = assertedType(node);
3478
- if (type.kind === ts.SyntaxKind.AnyKeyword || type.kind === ts.SyntaxKind.UnknownKeyword) return true;
3479
- return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && type.typeName.text === "const";
3552
+ const k = assertedLaunderKind(node, aliasMap);
3553
+ return k === "any" || k === "never" || k === "unknown" || k === "const";
3480
3554
  };
3481
3555
  const visit = (node) => {
3482
3556
  if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
3483
- if (!isDoubleCast(node) && !isRow4OrHonest(node)) record("type", node);
3557
+ if (!isDoubleCast(node, aliasMap) && !isRow4OrHonest(node) && !launderInner.has(node)) record("type", node);
3484
3558
  } else if (ts.isNonNullExpression(node)) {
3485
3559
  record("non-null", node);
3486
3560
  }
@@ -3515,7 +3589,7 @@ function unmatchedAfter(before, after) {
3515
3589
  }
3516
3590
  return out3;
3517
3591
  }
3518
- var RULE2, DECLARATION, OUT_OF_BUDGET_PATH, GENERATED_HEADER, HEADER_LINES, tsCastGrowth;
3592
+ var RULE2, DECLARATION, OUT_OF_BUDGET_PATH, GENERATED_HEADER, HEADER_LINES, DOUBLE_CAST_INNER, EMPTY_ALIASES, tsCastGrowth;
3519
3593
  var init_ts_cast_growth = __esm({
3520
3594
  "src/detectors/ts-cast-growth.ts"() {
3521
3595
  "use strict";
@@ -3529,6 +3603,8 @@ var init_ts_cast_growth = __esm({
3529
3603
  OUT_OF_BUDGET_PATH = /(?:^|\/)(?:node_modules|vendor|third_party|dist|build|generated|__generated__)\/|\.(?:generated|gen)\.[cm]?[jt]sx?$/;
3530
3604
  GENERATED_HEADER = /@generated\b|\bAUTO-?GENERATED\b|\bDO NOT EDIT\b/i;
3531
3605
  HEADER_LINES = 20;
3606
+ DOUBLE_CAST_INNER = /* @__PURE__ */ new Set(["any", "never", "unknown", "empty", "objectish"]);
3607
+ EMPTY_ALIASES = /* @__PURE__ */ new Map();
3532
3608
  tsCastGrowth = {
3533
3609
  id: RULE2,
3534
3610
  surface: ["file"],
@@ -3570,13 +3646,51 @@ var init_ts_cast_growth = __esm({
3570
3646
  });
3571
3647
 
3572
3648
  // src/detectors/ts-any-cast.ts
3573
- function countAny(src) {
3574
- const r = { cast: 0, broad: 0, double: 0 };
3649
+ function commentIsLiveDirective(text) {
3650
+ let inner = text;
3651
+ if (inner.startsWith("//")) inner = inner.replace(/^\/+/, "");
3652
+ else inner = inner.replace(/^\/\*+/, "").replace(/\*+\/$/, "");
3653
+ for (const raw of inner.split("\n")) {
3654
+ if (DIRECTIVE_START.test(raw.replace(/^\s*\*+/, "").trim())) return true;
3655
+ }
3656
+ return false;
3657
+ }
3658
+ function countLiveDirectives(src) {
3659
+ let n = 0;
3660
+ try {
3661
+ const scanner = ts.createScanner(
3662
+ ts.ScriptTarget.Latest,
3663
+ /* skipTrivia */
3664
+ false,
3665
+ ts.LanguageVariant.Standard,
3666
+ src
3667
+ );
3668
+ let tok = scanner.scan();
3669
+ while (tok !== ts.SyntaxKind.EndOfFileToken) {
3670
+ if (tok === ts.SyntaxKind.SingleLineCommentTrivia || tok === ts.SyntaxKind.MultiLineCommentTrivia) {
3671
+ if (commentIsLiveDirective(scanner.getTokenText())) n++;
3672
+ }
3673
+ tok = scanner.scan();
3674
+ }
3675
+ } catch {
3676
+ }
3677
+ return n;
3678
+ }
3679
+ function countAny(src, path) {
3680
+ const r = { cast: 0, never: 0, broad: 0, double: 0, suppress: 0, jsdoc: 0 };
3681
+ r.suppress = countLiveDirectives(src);
3682
+ r.jsdoc = JS_FILE.test(path) ? countMatches(src, JSDOC_ANY_CAST) : 0;
3575
3683
  try {
3576
3684
  const sf = parseSource("f.ts", src);
3577
3685
  if (!sf) return r;
3686
+ const aliasMap = buildAliasMap(sf);
3578
3687
  const visit = (node) => {
3579
- if ((ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) && isDoubleCast(node)) r.double++;
3688
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
3689
+ const k = assertedLaunderKind(node, aliasMap);
3690
+ if (k === "any") r.cast++;
3691
+ else if (k === "never") r.never++;
3692
+ if (isDoubleCast(node, aliasMap)) r.double++;
3693
+ }
3580
3694
  if (node.kind === ts.SyntaxKind.AnyKeyword) {
3581
3695
  let target = node;
3582
3696
  let p = target.parent;
@@ -3585,8 +3699,7 @@ function countAny(src) {
3585
3699
  p = p.parent;
3586
3700
  }
3587
3701
  const isCast = p && ts.isAsExpression(p) && p.type === target || p && ts.isTypeAssertionExpression(p) && p.type === target;
3588
- if (isCast) r.cast++;
3589
- else r.broad++;
3702
+ if (!isCast) r.broad++;
3590
3703
  }
3591
3704
  ts.forEachChild(node, visit);
3592
3705
  };
@@ -3595,22 +3708,33 @@ function countAny(src) {
3595
3708
  }
3596
3709
  return r;
3597
3710
  }
3598
- function lineHasDoubleCast(line) {
3711
+ function lineHasLiveDirective(line) {
3712
+ return countLiveDirectives(line) > 0 || FRAGMENT_DIRECTIVE.test(line);
3713
+ }
3714
+ function lineHasRow4Cast(line, aliasMap) {
3599
3715
  const sf = parseSource("line.ts", line);
3600
3716
  if (!sf) return false;
3601
3717
  let found = false;
3602
3718
  const visit = (n) => {
3603
3719
  if (found) return;
3604
- if ((ts.isAsExpression(n) || ts.isTypeAssertionExpression(n)) && isDoubleCast(n)) {
3605
- found = true;
3606
- return;
3720
+ if (ts.isAsExpression(n) || ts.isTypeAssertionExpression(n)) {
3721
+ const k = assertedLaunderKind(n, aliasMap);
3722
+ if (k === "any" || k === "never" || isDoubleCast(n, aliasMap)) {
3723
+ found = true;
3724
+ return;
3725
+ }
3607
3726
  }
3608
3727
  ts.forEachChild(n, visit);
3609
3728
  };
3610
3729
  visit(sf);
3611
3730
  return found;
3612
3731
  }
3613
- var BLOCK_RULE, WARN_RULE, DOUBLE_CAST, SUPPRESS, JSDOC_ANY_CAST, JS_FILE, countMatches, NARROW_LINE, BROAD_LINE, BLOCK_REMEDIATION, WARN_REMEDIATION, tsAnyCast;
3732
+ function aliasMapForAddedLines(c) {
3733
+ const snippet = addedLines(c).map((l) => l.content).join("\n");
3734
+ const sf = parseSource("lines.ts", snippet);
3735
+ return sf ? buildAliasMap(sf) : /* @__PURE__ */ new Map();
3736
+ }
3737
+ var BLOCK_RULE, WARN_RULE, JSDOC_ANY_CAST, JS_FILE, countMatches, DIRECTIVE_START, NARROW_LINE, BROAD_LINE, FRAGMENT_DIRECTIVE, BLOCK_REMEDIATION, WARN_REMEDIATION, tsAnyCast;
3614
3738
  var init_ts_any_cast = __esm({
3615
3739
  "src/detectors/ts-any-cast.ts"() {
3616
3740
  "use strict";
@@ -3622,13 +3746,13 @@ var init_ts_any_cast = __esm({
3622
3746
  init_ts_cast_growth();
3623
3747
  BLOCK_RULE = "ts-any-cast";
3624
3748
  WARN_RULE = "ts-any-launder";
3625
- DOUBLE_CAST = /\bas\s+\(*\s*unknown\s*\)*\s+as\b/g;
3626
- SUPPRESS = /@ts-(?:ignore|expect-error|nocheck)\b/g;
3627
- JSDOC_ANY_CAST = /\/\*\*\s*@type\s*\{\s*any\s*\}\s*\*\/\s*\(/g;
3749
+ JSDOC_ANY_CAST = /\/\*\*\s*@type\s*\{\s*(?:any|\*|\?)\s*\}\s*\*\/\s*\(/g;
3628
3750
  JS_FILE = /\.(?:js|jsx|mjs|cjs)$/;
3629
3751
  countMatches = (s, re) => (s.match(re) || []).length;
3630
- NARROW_LINE = /\bas\s+\(*\s*any\b|<\s*any\s*>|@ts-(?:ignore|expect-error|nocheck)\b/;
3752
+ DIRECTIVE_START = /^@ts-(?:ignore|expect-error|nocheck)\b/;
3753
+ NARROW_LINE = /\bas\s+\(*\s*any\b|<\s*any\s*>/;
3631
3754
  BROAD_LINE = /:\s*any\b|<[^<>]*\bany\b[^<>]*>/;
3755
+ FRAGMENT_DIRECTIVE = /^\s*\*?\s*@ts-(?:ignore|expect-error|nocheck)\b/;
3632
3756
  BLOCK_REMEDIATION = "Fix the underlying type instead of silencing the checker; do not cast to `any`.";
3633
3757
  WARN_REMEDIATION = "Prefer a precise type or `unknown` + a guard over `any` here \u2014 flagged for review.";
3634
3758
  tsAnyCast = {
@@ -3642,15 +3766,17 @@ var init_ts_any_cast = __esm({
3642
3766
  const inTest = protectedCategory(c.path, policy) === "tests";
3643
3767
  if (c.after != null && c.op !== "delete") {
3644
3768
  const before = c.before ?? "";
3645
- const a = countAny(c.after);
3646
- const b = countAny(before);
3769
+ const a = countAny(c.after, c.path);
3770
+ const b = countAny(before, c.path);
3647
3771
  const dCast = a.cast - b.cast;
3772
+ const dNever = a.never - b.never;
3648
3773
  const dBroad = a.broad - b.broad;
3649
- const dDouble = Math.max(a.double - b.double, countMatches(c.after, DOUBLE_CAST) - countMatches(before, DOUBLE_CAST));
3650
- const dSuppr = countMatches(c.after, SUPPRESS) - countMatches(before, SUPPRESS);
3651
- const dJsdoc = JS_FILE.test(c.path) ? countMatches(c.after, JSDOC_ANY_CAST) - countMatches(before, JSDOC_ANY_CAST) : 0;
3774
+ const dDouble = a.double - b.double;
3775
+ const dSuppr = a.suppress - b.suppress;
3776
+ const dJsdoc = a.jsdoc - b.jsdoc;
3652
3777
  const blockReasons = [];
3653
3778
  if (dCast > 0) blockReasons.push("`as any` cast");
3779
+ if (dNever > 0) blockReasons.push("`as never` cast");
3654
3780
  if (dDouble > 0) blockReasons.push("`as unknown as` double cast");
3655
3781
  if (dSuppr > 0) blockReasons.push("@ts-ignore/@ts-expect-error/@ts-nocheck suppression");
3656
3782
  if (dJsdoc > 0) blockReasons.push("JSDoc `@type {any}` cast");
@@ -3679,8 +3805,9 @@ var init_ts_any_cast = __esm({
3679
3805
  continue;
3680
3806
  }
3681
3807
  const jsdocCast = JS_FILE.test(c.path) ? new RegExp(JSDOC_ANY_CAST.source) : null;
3808
+ const aliasMap = aliasMapForAddedLines(c);
3682
3809
  for (const l of addedLines(c)) {
3683
- if (NARROW_LINE.test(l.content) || lineHasDoubleCast(l.content) || jsdocCast && jsdocCast.test(l.content)) {
3810
+ if (NARROW_LINE.test(l.content) || lineHasRow4Cast(l.content, aliasMap) || lineHasLiveDirective(l.content) || jsdocCast && jsdocCast.test(l.content)) {
3684
3811
  out3.push(
3685
3812
  makeFinding(inTest ? WARN_RULE : BLOCK_RULE, policy, {
3686
3813
  file: c.path,
@@ -5839,6 +5966,8 @@ function survives(removedCore, isUses, afterCores, addedCores) {
5839
5966
  if (w && (canonical(w[1]) === r || canonical(w[1]).startsWith(r + " "))) neutralised ??= raw;
5840
5967
  }
5841
5968
  }
5969
+ const maskOf = (args) => (" " + args).match(NEUTRALISING_SUFFIX)?.[0]?.trim() ?? null;
5970
+ const priorMask = r.includes(" ") ? maskOf(r.slice(r.indexOf(" ") + 1)) : null;
5842
5971
  let respelled = false;
5843
5972
  for (const raw of addedCores) {
5844
5973
  if (!kind || checkKind(raw) !== kind) continue;
@@ -5853,7 +5982,9 @@ function survives(removedCore, isUses, afterCores, addedCores) {
5853
5982
  continue;
5854
5983
  }
5855
5984
  const args = a.slice(a.indexOf(" ") + 1 || a.length);
5856
- if (a.includes(" ") && (NEUTRALISING_SUFFIX.test(" " + args) || narrowed(args) || runnerNarrowed(a, args))) neutralised ??= raw;
5985
+ const mask = maskOf(args);
5986
+ const newMask = mask !== null && mask !== priorMask;
5987
+ if (a.includes(" ") && (newMask || narrowed(args) || runnerNarrowed(a, args))) neutralised ??= raw;
5857
5988
  else respelled = true;
5858
5989
  }
5859
5990
  if (respelled) return { state: "kept" };
@@ -6077,7 +6208,7 @@ function isActiveWorkflow(path) {
6077
6208
  return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(path);
6078
6209
  }
6079
6210
  function commandCore(line) {
6080
- return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses):\s*/, "").replace(/\s+/g, " ").trim();
6211
+ return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses|script|command|before_script|after_script|sh|bash|pwsh|powershell):\s*/, "").replace(/\s+/g, " ").trim();
6081
6212
  }
6082
6213
  function expressionOf(raw) {
6083
6214
  const v = uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim();
@@ -6293,6 +6424,12 @@ function triggerNarrowings(before, after, opts) {
6293
6424
  }
6294
6425
  return out3;
6295
6426
  }
6427
+ function localUses(line) {
6428
+ const wf = line.match(REUSED_WORKFLOW)?.[1];
6429
+ if (wf) return [wf];
6430
+ const action = line.match(LOCAL_ACTION)?.[1];
6431
+ return action ? [`${action}/action.yml`, `${action}/action.yaml`] : [];
6432
+ }
6296
6433
  function literalValue(raw) {
6297
6434
  const v = foldExpressions(uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim());
6298
6435
  return v.includes(EXPR_TOKEN) || v.includes(MATRIX_TOKEN) ? null : v;
@@ -6327,7 +6464,7 @@ function ancestorOf(lines, i) {
6327
6464
  while (h >= 0 && (lines[h].trim() === "" || /^\s*#/.test(lines[h]) || indentOf(lines[h]) >= ind)) h--;
6328
6465
  return h >= 0 ? h : null;
6329
6466
  }
6330
- 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;
6467
+ var RULE7, USES, CHECK, YAML_KEY, isCompositeAction, isGithubFile, GH_RUN_KEY, GENERIC_RUN_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, LOCAL_ACTION, MANIFEST, WORKDIR_LINE, REF_LINE, CHECKOUT, ciTampering;
6331
6468
  var init_ci_tampering = __esm({
6332
6469
  "src/detectors/ci-tampering.ts"() {
6333
6470
  "use strict";
@@ -6344,6 +6481,10 @@ var init_ci_tampering = __esm({
6344
6481
  USES = /^\s*-\s*uses:/;
6345
6482
  CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
6346
6483
  YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
6484
+ isCompositeAction = (path) => /(^|\/)\.github\/actions\/.+\/action\.ya?ml$/.test(path);
6485
+ isGithubFile = (path) => /(^|\/)\.github\//.test(path);
6486
+ GH_RUN_KEY = /^\s*-?\s*run:/;
6487
+ GENERIC_RUN_KEY = /^\s*-?\s*(?:run|script|command|before_script|after_script|sh|bash|pwsh|powershell):/;
6347
6488
  invokesCheck2 = (line) => invokesCheck(line) || INVOKES_TOOL.test(commandCore(line));
6348
6489
  uncommented = (v) => v.replace(/\s+#.*$/, "").trim();
6349
6490
  SHELL_ERREXIT = /(?:^|\s)-[a-zA-Z]*e[a-zA-Z]*(?:\s|$)|-o\s+errexit\b/;
@@ -6374,7 +6515,8 @@ var init_ci_tampering = __esm({
6374
6515
  pathsMatchSource = (globs4, sources) => sources.some((s) => admits(globs4, s));
6375
6516
  ignoresEverySource = (globs4, sources) => sources.length > 0 && sources.every((s) => admits(globs4, s));
6376
6517
  CODE_PROBES = ["src/index.ts", "src/index.js", "lib/index.js", "index.ts", "main.go", "src/main.py", "src/lib.rs", "packages/a/src/index.ts", "test/a.test.ts"];
6377
- REUSED_WORKFLOW = /^\s*uses:\s*\.\/(\.github\/workflows\/[^\s@]+)/;
6518
+ REUSED_WORKFLOW = /^\s*-?\s*uses:\s*\.\/(\.github\/workflows\/[^\s@]+)/;
6519
+ LOCAL_ACTION = /^\s*-?\s*uses:\s*\.\/(\.github\/actions\/[^\s@]+?)\/?\s*(?:#.*)?$/;
6378
6520
  MANIFEST = /(?:^|\/)(?:package\.json|pyproject\.toml|setup\.py|setup\.cfg|Cargo\.toml|go\.mod|Makefile|build\.gradle(?:\.kts)?|pom\.xml|Gemfile|deno\.jsonc?)$/;
6379
6521
  WORKDIR_LINE = /^\s*-?\s*working-directory:\s*(.+?)\s*$/;
6380
6522
  REF_LINE = /^\s*ref:\s*(.+?)\s*$/;
@@ -6404,7 +6546,8 @@ var init_ci_tampering = __esm({
6404
6546
  if (!isProtected(c.path, policy, "ci")) continue;
6405
6547
  const afterLines = c.after != null ? c.after.split("\n") : null;
6406
6548
  const addedAt = new Set(addedLines(c).map((l) => l.newLine).filter((n) => n != null));
6407
- for (const l of addedLines(c)) {
6549
+ const github = isGithubFile(c.path);
6550
+ for (const l of github ? addedLines(c) : []) {
6408
6551
  const coe = l.content.match(/^\s*-?\s*continue-on-error:\s*(.+?)\s*$/);
6409
6552
  const cond = l.content.match(/^\s*-?\s*if:\s*(.+?)\s*$/);
6410
6553
  if (!coe && !cond) continue;
@@ -6436,7 +6579,7 @@ var init_ci_tampering = __esm({
6436
6579
  );
6437
6580
  }
6438
6581
  }
6439
- if (afterLines) {
6582
+ if (afterLines && github) {
6440
6583
  for (const b of runBlocks(afterLines)) {
6441
6584
  if (!b.body.some(invokesCheck2)) continue;
6442
6585
  const propagates = b.body.some((l) => /\bexit\s+"?\$/.test(l));
@@ -6514,7 +6657,7 @@ var init_ci_tampering = __esm({
6514
6657
  })
6515
6658
  );
6516
6659
  }
6517
- if (c.before != null && c.after != null) {
6660
+ if (c.before != null && c.after != null && !isCompositeAction(c.path)) {
6518
6661
  triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
6519
6662
  for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
6520
6663
  out3.push(
@@ -6532,17 +6675,27 @@ var init_ci_tampering = __esm({
6532
6675
  const afterCores = (afterCommands ?? addedLines(c).map((l) => l.content)).map(commandCore);
6533
6676
  const addedCores = addedLines(c).map((l) => commandCore(afterCommands && l.newLine != null ? afterCommands[l.newLine - 1] : l.content));
6534
6677
  for (const l of afterLines ?? []) {
6535
- const reused = l.match(REUSED_WORKFLOW)?.[1];
6536
- const content = reused ? afterByPath.get(reused) : void 0;
6537
- if (content == null || reused === c.path) continue;
6538
- const cores = commandLines(content.split("\n")).map(commandCore);
6539
- afterCores.push(...cores);
6540
- addedCores.push(...cores);
6678
+ for (const reused of localUses(l)) {
6679
+ const content = afterByPath.get(reused);
6680
+ if (content == null || reused === c.path) continue;
6681
+ const cores = commandLines(content.split("\n")).map(commandCore);
6682
+ afterCores.push(...cores);
6683
+ addedCores.push(...cores);
6684
+ }
6685
+ }
6686
+ if (!github && c.after == null) {
6687
+ for (const o of changes) {
6688
+ if (o.kind !== "file" || o.op !== "add" || o.after == null || !isProtected(o.path, policy, "ci")) continue;
6689
+ const cores = commandLines(o.after.split("\n")).map(commandCore);
6690
+ afterCores.push(...cores);
6691
+ addedCores.push(...cores);
6692
+ }
6541
6693
  }
6694
+ const runKey = github ? GH_RUN_KEY : GENERIC_RUN_KEY;
6542
6695
  for (const l of removedLines(c)) {
6543
6696
  if (/^\s*#/.test(l.content)) continue;
6544
6697
  const isUsesLine = usesCheck(l.content);
6545
- const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? /^\s*-?\s*run:/.test(l.content) : true) && invokesCheck2(l.content);
6698
+ const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? runKey.test(l.content) : true) && invokesCheck2(l.content);
6546
6699
  if (!isUsesLine && !isCheckCommand) continue;
6547
6700
  const s = survives(commandCore(l.content), isUsesLine, afterCores, addedCores);
6548
6701
  if (s.state === "kept") continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.25.0",
3
+ "version": "2.25.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",