tamperward 1.9.0 → 1.10.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/dist/cli/index.js +215 -135
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -153,7 +153,7 @@ function parseHunk(lines, start) {
|
|
|
153
153
|
const oldLines = m[2] !== void 0 ? Number(m[2]) : 1;
|
|
154
154
|
const newStart = Number(m[3]);
|
|
155
155
|
const newLines = m[4] !== void 0 ? Number(m[4]) : 1;
|
|
156
|
-
const
|
|
156
|
+
const out2 = [];
|
|
157
157
|
let oldLine = oldStart;
|
|
158
158
|
let newLine = newStart;
|
|
159
159
|
let i = start + 1;
|
|
@@ -164,20 +164,20 @@ function parseHunk(lines, start) {
|
|
|
164
164
|
const tag = l[0];
|
|
165
165
|
const content = l.slice(1);
|
|
166
166
|
if (tag === "+") {
|
|
167
|
-
|
|
167
|
+
out2.push({ type: "add", content, oldLine: null, newLine });
|
|
168
168
|
newLine++;
|
|
169
169
|
} else if (tag === "-") {
|
|
170
|
-
|
|
170
|
+
out2.push({ type: "del", content, oldLine, newLine: null });
|
|
171
171
|
oldLine++;
|
|
172
172
|
} else if (tag === " ") {
|
|
173
|
-
|
|
173
|
+
out2.push({ type: "context", content, oldLine, newLine });
|
|
174
174
|
oldLine++;
|
|
175
175
|
newLine++;
|
|
176
176
|
} else {
|
|
177
177
|
break;
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
-
return { hunk: { oldStart, oldLines, newStart, newLines, lines:
|
|
180
|
+
return { hunk: { oldStart, oldLines, newStart, newLines, lines: out2 }, next: i };
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
// src/git/build.ts
|
|
@@ -190,9 +190,9 @@ function git(args, cwd) {
|
|
|
190
190
|
stdio: ["ignore", "pipe", "pipe"]
|
|
191
191
|
});
|
|
192
192
|
} catch (e) {
|
|
193
|
-
const
|
|
194
|
-
const detail = String(
|
|
195
|
-
throw new Error(detail ? `git ${args[0]}: ${detail}` :
|
|
193
|
+
const err2 = e;
|
|
194
|
+
const detail = String(err2.stderr ?? "").trim();
|
|
195
|
+
throw new Error(detail ? `git ${args[0]}: ${detail}` : err2.message ?? `git ${args[0]} failed`);
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
function blobAt(rev, path, cwd) {
|
|
@@ -324,7 +324,7 @@ var noVerify = {
|
|
|
324
324
|
surface: ["command"],
|
|
325
325
|
certainty: "mechanical",
|
|
326
326
|
run(changes, policy) {
|
|
327
|
-
const
|
|
327
|
+
const out2 = [];
|
|
328
328
|
for (const c of changes) {
|
|
329
329
|
if (c.kind !== "command") continue;
|
|
330
330
|
for (const seg of segments(c.raw)) {
|
|
@@ -341,7 +341,7 @@ var noVerify = {
|
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
if (why) {
|
|
344
|
-
|
|
344
|
+
out2.push(
|
|
345
345
|
makeFinding(RULE, policy, {
|
|
346
346
|
message: `Command bypasses git hooks: ${why}.`,
|
|
347
347
|
evidence: seg,
|
|
@@ -351,7 +351,7 @@ var noVerify = {
|
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
|
-
return
|
|
354
|
+
return out2;
|
|
355
355
|
}
|
|
356
356
|
};
|
|
357
357
|
|
|
@@ -373,13 +373,13 @@ import picomatch from "picomatch";
|
|
|
373
373
|
var POLICY_FILE = ".tamperward.yml";
|
|
374
374
|
var BLOCK_SINCE = {};
|
|
375
375
|
function applyVersionGates(rules, version, gates = BLOCK_SINCE) {
|
|
376
|
-
const
|
|
376
|
+
const out2 = { ...rules };
|
|
377
377
|
for (const [rule, since] of Object.entries(gates)) {
|
|
378
|
-
if (since > version &&
|
|
379
|
-
|
|
378
|
+
if (since > version && out2[rule]?.severity === "block") {
|
|
379
|
+
out2[rule] = { ...out2[rule], severity: "warn" };
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
|
-
return
|
|
382
|
+
return out2;
|
|
383
383
|
}
|
|
384
384
|
var cache = /* @__PURE__ */ new Map();
|
|
385
385
|
function matcher(glob) {
|
|
@@ -410,12 +410,12 @@ function isIgnored(path, policy) {
|
|
|
410
410
|
return matchesAny(path, policy.ignore);
|
|
411
411
|
}
|
|
412
412
|
function mergeProtected(base, user) {
|
|
413
|
-
const
|
|
413
|
+
const out2 = { ...base };
|
|
414
414
|
for (const [cat, globs] of Object.entries(user ?? {})) {
|
|
415
|
-
const have = new Set(
|
|
416
|
-
|
|
415
|
+
const have = new Set(out2[cat] ?? []);
|
|
416
|
+
out2[cat] = [...out2[cat] ?? [], ...(globs ?? []).filter((g) => !have.has(g))];
|
|
417
417
|
}
|
|
418
|
-
return
|
|
418
|
+
return out2;
|
|
419
419
|
}
|
|
420
420
|
function defaultPolicy(version = 1) {
|
|
421
421
|
const p = {
|
|
@@ -522,7 +522,7 @@ var tsAnyCast = {
|
|
|
522
522
|
surface: ["file"],
|
|
523
523
|
certainty: "mechanical",
|
|
524
524
|
run(changes, policy) {
|
|
525
|
-
const
|
|
525
|
+
const out2 = [];
|
|
526
526
|
for (const c of changes) {
|
|
527
527
|
if (c.kind !== "file" || !isCodeFile(c.path)) continue;
|
|
528
528
|
const inTest = protectedCategory(c.path, policy) === "tests";
|
|
@@ -539,7 +539,7 @@ var tsAnyCast = {
|
|
|
539
539
|
if (dDouble > 0) blockReasons.push("`as unknown as` double cast");
|
|
540
540
|
if (dSuppr > 0) blockReasons.push("@ts-ignore/@ts-expect-error/@ts-nocheck suppression");
|
|
541
541
|
if (blockReasons.length) {
|
|
542
|
-
|
|
542
|
+
out2.push(
|
|
543
543
|
makeFinding(inTest ? WARN_RULE : BLOCK_RULE, policy, {
|
|
544
544
|
file: c.path,
|
|
545
545
|
message: inTest ? `Type-checker escape in a test file: ${blockReasons.join("; ")} (test infrastructure \u2014 flagged, not blocked).` : `Type safety discarded: ${blockReasons.join("; ")}.`,
|
|
@@ -550,7 +550,7 @@ var tsAnyCast = {
|
|
|
550
550
|
);
|
|
551
551
|
}
|
|
552
552
|
if (dBroad > 0) {
|
|
553
|
-
|
|
553
|
+
out2.push(
|
|
554
554
|
makeFinding(WARN_RULE, policy, {
|
|
555
555
|
file: c.path,
|
|
556
556
|
message: `Type laundered to \`any\`: introduces ${dBroad} new \`any\`-typed value(s) in a type/generic position (e.g. : any, Record<string, any>).`,
|
|
@@ -564,7 +564,7 @@ var tsAnyCast = {
|
|
|
564
564
|
}
|
|
565
565
|
for (const l of addedLines(c)) {
|
|
566
566
|
if (NARROW_LINE.test(l.content)) {
|
|
567
|
-
|
|
567
|
+
out2.push(
|
|
568
568
|
makeFinding(inTest ? WARN_RULE : BLOCK_RULE, policy, {
|
|
569
569
|
file: c.path,
|
|
570
570
|
line: l.newLine ?? void 0,
|
|
@@ -575,7 +575,7 @@ var tsAnyCast = {
|
|
|
575
575
|
})
|
|
576
576
|
);
|
|
577
577
|
} else if (BROAD_LINE.test(l.content)) {
|
|
578
|
-
|
|
578
|
+
out2.push(
|
|
579
579
|
makeFinding(WARN_RULE, policy, {
|
|
580
580
|
file: c.path,
|
|
581
581
|
line: l.newLine ?? void 0,
|
|
@@ -588,7 +588,7 @@ var tsAnyCast = {
|
|
|
588
588
|
}
|
|
589
589
|
}
|
|
590
590
|
}
|
|
591
|
-
return
|
|
591
|
+
return out2;
|
|
592
592
|
}
|
|
593
593
|
};
|
|
594
594
|
|
|
@@ -604,13 +604,13 @@ var lintSuppression = {
|
|
|
604
604
|
surface: ["file"],
|
|
605
605
|
certainty: "mechanical",
|
|
606
606
|
run(changes, policy) {
|
|
607
|
-
const
|
|
607
|
+
const out2 = [];
|
|
608
608
|
for (const c of changes) {
|
|
609
609
|
if (c.kind !== "file" || !isCodeFile(c.path)) continue;
|
|
610
610
|
for (const l of addedLines(c)) {
|
|
611
611
|
for (const p of PATTERNS) {
|
|
612
612
|
if (p.re.test(l.content)) {
|
|
613
|
-
|
|
613
|
+
out2.push(
|
|
614
614
|
makeFinding(RULE2, policy, {
|
|
615
615
|
file: c.path,
|
|
616
616
|
line: l.newLine ?? void 0,
|
|
@@ -624,7 +624,7 @@ var lintSuppression = {
|
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
626
|
}
|
|
627
|
-
return
|
|
627
|
+
return out2;
|
|
628
628
|
}
|
|
629
629
|
};
|
|
630
630
|
|
|
@@ -647,14 +647,14 @@ var testSkip = {
|
|
|
647
647
|
surface: ["file"],
|
|
648
648
|
certainty: "mechanical",
|
|
649
649
|
run(changes, policy) {
|
|
650
|
-
const
|
|
650
|
+
const out2 = [];
|
|
651
651
|
for (const c of changes) {
|
|
652
652
|
if (c.kind !== "file") continue;
|
|
653
653
|
if (!isProtected(c.path, policy, "tests")) continue;
|
|
654
654
|
for (const l of addedLines(c)) {
|
|
655
655
|
for (const p of PATTERNS2) {
|
|
656
656
|
if (p.re.test(l.content)) {
|
|
657
|
-
|
|
657
|
+
out2.push(
|
|
658
658
|
makeFinding(RULE3, policy, {
|
|
659
659
|
file: c.path,
|
|
660
660
|
line: l.newLine ?? void 0,
|
|
@@ -668,7 +668,7 @@ var testSkip = {
|
|
|
668
668
|
}
|
|
669
669
|
}
|
|
670
670
|
}
|
|
671
|
-
return
|
|
671
|
+
return out2;
|
|
672
672
|
}
|
|
673
673
|
};
|
|
674
674
|
|
|
@@ -691,7 +691,7 @@ function numericOf(e) {
|
|
|
691
691
|
}
|
|
692
692
|
function metricsOf(obj) {
|
|
693
693
|
if (!ts2.isObjectLiteralExpression(obj)) return void 0;
|
|
694
|
-
const
|
|
694
|
+
const out2 = {};
|
|
695
695
|
let any = false;
|
|
696
696
|
for (const p of obj.properties) {
|
|
697
697
|
if (!ts2.isPropertyAssignment(p)) continue;
|
|
@@ -699,10 +699,10 @@ function metricsOf(obj) {
|
|
|
699
699
|
if (k === null || !isMetric(k)) continue;
|
|
700
700
|
const n = numericOf(p.initializer);
|
|
701
701
|
if (n === void 0) continue;
|
|
702
|
-
|
|
702
|
+
out2[k] = n;
|
|
703
703
|
any = true;
|
|
704
704
|
}
|
|
705
|
-
return any ?
|
|
705
|
+
return any ? out2 : void 0;
|
|
706
706
|
}
|
|
707
707
|
function underCoverageKey(node) {
|
|
708
708
|
for (let p = node.parent; p; p = p.parent) {
|
|
@@ -744,41 +744,41 @@ function parseThresholds(src) {
|
|
|
744
744
|
return res;
|
|
745
745
|
}
|
|
746
746
|
var eff = (t, path) => t.paths.get(norm(path)) ?? t.global;
|
|
747
|
-
function compareMetrics(scope, before, after,
|
|
747
|
+
function compareMetrics(scope, before, after, out2) {
|
|
748
748
|
if (!before) return;
|
|
749
749
|
for (const m of METRICS) {
|
|
750
750
|
const bv = before[m];
|
|
751
751
|
if (bv === void 0) continue;
|
|
752
752
|
const av = after?.[m];
|
|
753
|
-
if (av === void 0)
|
|
754
|
-
else if (av < bv)
|
|
753
|
+
if (av === void 0) out2.push(`${scope} ${m} threshold removed (was ${bv})`);
|
|
754
|
+
else if (av < bv) out2.push(`${scope} ${m} threshold lowered ${bv} \u2192 ${av}`);
|
|
755
755
|
}
|
|
756
756
|
}
|
|
757
757
|
function weakenings(before, after) {
|
|
758
|
-
const
|
|
758
|
+
const out2 = [];
|
|
759
759
|
if (before.present && !after.present) {
|
|
760
|
-
|
|
761
|
-
return
|
|
760
|
+
out2.push("the coverage threshold gate was removed");
|
|
761
|
+
return out2;
|
|
762
762
|
}
|
|
763
|
-
compareMetrics("global", before.global, after.global,
|
|
763
|
+
compareMetrics("global", before.global, after.global, out2);
|
|
764
764
|
const paths = /* @__PURE__ */ new Set([...before.paths.keys(), ...after.paths.keys()]);
|
|
765
765
|
for (const p of paths) {
|
|
766
|
-
compareMetrics(`required coverage for ${p}:`, eff(before, p), eff(after, p),
|
|
766
|
+
compareMetrics(`required coverage for ${p}:`, eff(before, p), eff(after, p), out2);
|
|
767
767
|
}
|
|
768
|
-
return
|
|
768
|
+
return out2;
|
|
769
769
|
}
|
|
770
770
|
var coverageLowering = {
|
|
771
771
|
id: RULE4,
|
|
772
772
|
surface: ["file"],
|
|
773
773
|
certainty: "mechanical",
|
|
774
774
|
run(changes, policy) {
|
|
775
|
-
const
|
|
775
|
+
const out2 = [];
|
|
776
776
|
for (const c of changes) {
|
|
777
777
|
if (c.kind !== "file") continue;
|
|
778
778
|
if (!isProtected(c.path, policy, "config")) continue;
|
|
779
779
|
if (c.after != null && c.op !== "delete") {
|
|
780
780
|
for (const reason of weakenings(parseThresholds(c.before ?? ""), parseThresholds(c.after))) {
|
|
781
|
-
|
|
781
|
+
out2.push(
|
|
782
782
|
makeFinding(RULE4, policy, {
|
|
783
783
|
file: c.path,
|
|
784
784
|
message: `Coverage gate weakened: ${reason}.`,
|
|
@@ -789,7 +789,7 @@ var coverageLowering = {
|
|
|
789
789
|
}
|
|
790
790
|
} else if (c.op === "delete" || c.after === "" && (c.before ?? "").length > 0) {
|
|
791
791
|
if (parseThresholds(c.before ?? "").present) {
|
|
792
|
-
|
|
792
|
+
out2.push(
|
|
793
793
|
makeFinding(RULE4, policy, {
|
|
794
794
|
file: c.path,
|
|
795
795
|
message: "Coverage gate weakened: the coverage config was deleted.",
|
|
@@ -803,7 +803,7 @@ var coverageLowering = {
|
|
|
803
803
|
const removed = removedLines(c);
|
|
804
804
|
const removedCoverage = removed.find((l) => /--coverage\b/.test(l.content));
|
|
805
805
|
if (removedCoverage && !added.some((l) => /--coverage\b/.test(l.content))) {
|
|
806
|
-
|
|
806
|
+
out2.push(
|
|
807
807
|
makeFinding(RULE4, policy, {
|
|
808
808
|
file: c.path,
|
|
809
809
|
message: "The --coverage flag was removed from a test script.",
|
|
@@ -814,7 +814,7 @@ var coverageLowering = {
|
|
|
814
814
|
}
|
|
815
815
|
for (const l of added) {
|
|
816
816
|
if (/--passWithNoTests\b/.test(l.content)) {
|
|
817
|
-
|
|
817
|
+
out2.push(
|
|
818
818
|
makeFinding(RULE4, policy, {
|
|
819
819
|
file: c.path,
|
|
820
820
|
line: l.newLine ?? void 0,
|
|
@@ -837,7 +837,7 @@ var coverageLowering = {
|
|
|
837
837
|
if (!m) continue;
|
|
838
838
|
const ov = oldNums.get(m[1]);
|
|
839
839
|
if (ov !== void 0 && Number(m[2]) < ov) {
|
|
840
|
-
|
|
840
|
+
out2.push(
|
|
841
841
|
makeFinding(RULE4, policy, {
|
|
842
842
|
file: c.path,
|
|
843
843
|
line: l.newLine ?? void 0,
|
|
@@ -851,7 +851,7 @@ var coverageLowering = {
|
|
|
851
851
|
}
|
|
852
852
|
}
|
|
853
853
|
const seen = /* @__PURE__ */ new Set();
|
|
854
|
-
return
|
|
854
|
+
return out2.filter((f) => {
|
|
855
855
|
const k = `${f.rule}|${f.message}`;
|
|
856
856
|
if (seen.has(k)) return false;
|
|
857
857
|
seen.add(k);
|
|
@@ -894,7 +894,7 @@ var ciTampering = {
|
|
|
894
894
|
surface: ["file"],
|
|
895
895
|
certainty: "mechanical",
|
|
896
896
|
run(changes, policy) {
|
|
897
|
-
const
|
|
897
|
+
const out2 = [];
|
|
898
898
|
for (const c of changes) {
|
|
899
899
|
if (c.kind !== "file") continue;
|
|
900
900
|
if (!isProtected(c.path, policy, "ci")) continue;
|
|
@@ -902,7 +902,7 @@ var ciTampering = {
|
|
|
902
902
|
const coe = l.content.match(/^\s*-?\s*continue-on-error:\s*(.+?)\s*$/);
|
|
903
903
|
const cond = l.content.match(/^\s*-?\s*if:\s*(.+?)\s*$/);
|
|
904
904
|
if (coe && isTruthy(coe[1])) {
|
|
905
|
-
|
|
905
|
+
out2.push(
|
|
906
906
|
makeFinding(RULE5, policy, {
|
|
907
907
|
file: c.path,
|
|
908
908
|
line: l.newLine ?? void 0,
|
|
@@ -912,7 +912,7 @@ var ciTampering = {
|
|
|
912
912
|
})
|
|
913
913
|
);
|
|
914
914
|
} else if (cond && isAlwaysFalse(cond[1])) {
|
|
915
|
-
|
|
915
|
+
out2.push(
|
|
916
916
|
makeFinding(RULE5, policy, {
|
|
917
917
|
file: c.path,
|
|
918
918
|
line: l.newLine ?? void 0,
|
|
@@ -931,7 +931,7 @@ var ciTampering = {
|
|
|
931
931
|
const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? /^\s*-?\s*run:/.test(l.content) : true) && invokesCheck(l.content);
|
|
932
932
|
if (!isUsesLine && !isCheckCommand) continue;
|
|
933
933
|
if (afterCores.has(commandCore(l.content))) continue;
|
|
934
|
-
|
|
934
|
+
out2.push(
|
|
935
935
|
makeFinding(RULE5, policy, {
|
|
936
936
|
file: c.path,
|
|
937
937
|
message: isUsesLine ? "A CI check step (test/lint/typecheck) was removed." : "A CI check command was removed from a run block.",
|
|
@@ -941,7 +941,7 @@ var ciTampering = {
|
|
|
941
941
|
);
|
|
942
942
|
}
|
|
943
943
|
}
|
|
944
|
-
return
|
|
944
|
+
return out2;
|
|
945
945
|
}
|
|
946
946
|
};
|
|
947
947
|
|
|
@@ -1044,13 +1044,13 @@ var hookTampering = {
|
|
|
1044
1044
|
surface: ["file", "command"],
|
|
1045
1045
|
certainty: "mechanical",
|
|
1046
1046
|
run(changes, policy) {
|
|
1047
|
-
const
|
|
1047
|
+
const out2 = [];
|
|
1048
1048
|
for (const c of changes) {
|
|
1049
1049
|
if (c.kind === "file") {
|
|
1050
1050
|
const targetsHook = isProtected(c.path, policy, "hooks") || (c.oldPath ? isProtected(c.oldPath, policy, "hooks") : false);
|
|
1051
1051
|
if (!targetsHook) continue;
|
|
1052
1052
|
if (c.op === "delete") {
|
|
1053
|
-
|
|
1053
|
+
out2.push(
|
|
1054
1054
|
makeFinding(RULE6, policy, {
|
|
1055
1055
|
file: c.path,
|
|
1056
1056
|
message: "A protected hook/policy file was deleted.",
|
|
@@ -1059,7 +1059,7 @@ var hookTampering = {
|
|
|
1059
1059
|
})
|
|
1060
1060
|
);
|
|
1061
1061
|
} else if (c.op === "rename") {
|
|
1062
|
-
|
|
1062
|
+
out2.push(
|
|
1063
1063
|
makeFinding(RULE6, policy, {
|
|
1064
1064
|
file: c.path,
|
|
1065
1065
|
message: `A protected hook/policy file was renamed (${c.oldPath} \u2192 ${c.path}).`,
|
|
@@ -1069,7 +1069,7 @@ var hookTampering = {
|
|
|
1069
1069
|
);
|
|
1070
1070
|
} else if (c.path.endsWith(POLICY_FILE) && c.before != null && c.after != null && policyWeakening(c.before, c.after) !== null) {
|
|
1071
1071
|
for (const reason of policyWeakening(c.before, c.after) ?? []) {
|
|
1072
|
-
|
|
1072
|
+
out2.push(
|
|
1073
1073
|
makeFinding(RULE6, policy, {
|
|
1074
1074
|
file: c.path,
|
|
1075
1075
|
message: `The policy was weakened: ${reason}.`,
|
|
@@ -1084,7 +1084,7 @@ var hookTampering = {
|
|
|
1084
1084
|
const live = added.filter((l) => !isShellComment(l.content));
|
|
1085
1085
|
const earlyExit = added.find((l) => /^\s*exit\s+0\s*$/.test(l.content));
|
|
1086
1086
|
if (earlyExit) {
|
|
1087
|
-
|
|
1087
|
+
out2.push(
|
|
1088
1088
|
makeFinding(RULE6, policy, {
|
|
1089
1089
|
file: c.path,
|
|
1090
1090
|
line: earlyExit.newLine ?? void 0,
|
|
@@ -1099,7 +1099,7 @@ var hookTampering = {
|
|
|
1099
1099
|
const m = l.content.match(HOOK_INVOCATION);
|
|
1100
1100
|
if (!m) continue;
|
|
1101
1101
|
if (live.some((a) => a.content.includes(m[0]))) continue;
|
|
1102
|
-
|
|
1102
|
+
out2.push(
|
|
1103
1103
|
makeFinding(RULE6, policy, {
|
|
1104
1104
|
file: c.path,
|
|
1105
1105
|
message: `A check invocation (\`${m[0]}\`) was removed from a protected hook.`,
|
|
@@ -1119,7 +1119,7 @@ var hookTampering = {
|
|
|
1119
1119
|
for (const l of addedLines(c)) {
|
|
1120
1120
|
const m = l.content.match(INLINE);
|
|
1121
1121
|
if (m && m[2] === "warn" && removedSeverity.get(m[1]) === "block") {
|
|
1122
|
-
|
|
1122
|
+
out2.push(
|
|
1123
1123
|
makeFinding(RULE6, policy, {
|
|
1124
1124
|
file: c.path,
|
|
1125
1125
|
line: l.newLine ?? void 0,
|
|
@@ -1129,7 +1129,7 @@ var hookTampering = {
|
|
|
1129
1129
|
})
|
|
1130
1130
|
);
|
|
1131
1131
|
} else if (/enabled:\s*false\b/.test(l.content)) {
|
|
1132
|
-
|
|
1132
|
+
out2.push(
|
|
1133
1133
|
makeFinding(RULE6, policy, {
|
|
1134
1134
|
file: c.path,
|
|
1135
1135
|
line: l.newLine ?? void 0,
|
|
@@ -1158,7 +1158,7 @@ var hookTampering = {
|
|
|
1158
1158
|
why = "a shell rewrite (tee/sed/cp/dd/install/mv) replaces a protected hook in place";
|
|
1159
1159
|
}
|
|
1160
1160
|
if (why) {
|
|
1161
|
-
|
|
1161
|
+
out2.push(
|
|
1162
1162
|
makeFinding(RULE6, policy, {
|
|
1163
1163
|
message: `Hook tampering via shell: ${why}.`,
|
|
1164
1164
|
evidence: seg,
|
|
@@ -1169,7 +1169,7 @@ var hookTampering = {
|
|
|
1169
1169
|
}
|
|
1170
1170
|
}
|
|
1171
1171
|
}
|
|
1172
|
-
return
|
|
1172
|
+
return out2;
|
|
1173
1173
|
}
|
|
1174
1174
|
};
|
|
1175
1175
|
|
|
@@ -1197,19 +1197,19 @@ function countTestBlocks(src) {
|
|
|
1197
1197
|
return n;
|
|
1198
1198
|
}
|
|
1199
1199
|
function significantLines(src) {
|
|
1200
|
-
const
|
|
1200
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
1201
1201
|
for (const raw of src.split("\n")) {
|
|
1202
1202
|
const l = raw.trim();
|
|
1203
|
-
if (l.length >= 10 && !/^(import\b|export\s|\/\/|\*|\/\*|}\)?;?$)/.test(l))
|
|
1203
|
+
if (l.length >= 10 && !/^(import\b|export\s|\/\/|\*|\/\*|}\)?;?$)/.test(l)) out2.add(l);
|
|
1204
1204
|
}
|
|
1205
|
-
return
|
|
1205
|
+
return out2;
|
|
1206
1206
|
}
|
|
1207
1207
|
var testDeletion = {
|
|
1208
1208
|
id: RULE7,
|
|
1209
1209
|
surface: ["file", "command"],
|
|
1210
1210
|
certainty: "mechanical",
|
|
1211
1211
|
run(changes, policy) {
|
|
1212
|
-
const
|
|
1212
|
+
const out2 = [];
|
|
1213
1213
|
const addedTestLines = /* @__PURE__ */ new Set();
|
|
1214
1214
|
let addedTestBlocks = 0;
|
|
1215
1215
|
for (const c of changes) {
|
|
@@ -1230,7 +1230,7 @@ var testDeletion = {
|
|
|
1230
1230
|
const isTest = isProtected(c.path, policy, "tests");
|
|
1231
1231
|
if (c.op === "delete" && isTest) {
|
|
1232
1232
|
if (c.before != null && isRelocation(c.before)) continue;
|
|
1233
|
-
|
|
1233
|
+
out2.push(
|
|
1234
1234
|
makeFinding(RULE7, policy, {
|
|
1235
1235
|
file: c.path,
|
|
1236
1236
|
message: "A test file was deleted.",
|
|
@@ -1239,7 +1239,7 @@ var testDeletion = {
|
|
|
1239
1239
|
})
|
|
1240
1240
|
);
|
|
1241
1241
|
} else if (c.op === "rename" && c.oldPath && isProtected(c.oldPath, policy, "tests") && !isTest) {
|
|
1242
|
-
|
|
1242
|
+
out2.push(
|
|
1243
1243
|
makeFinding(RULE7, policy, {
|
|
1244
1244
|
file: c.path,
|
|
1245
1245
|
message: `A test file was renamed out of the test glob (${c.oldPath} \u2192 ${c.path}).`,
|
|
@@ -1251,7 +1251,7 @@ var testDeletion = {
|
|
|
1251
1251
|
const before = countTestBlocks(c.before);
|
|
1252
1252
|
const after = countTestBlocks(c.after);
|
|
1253
1253
|
if (after < before) {
|
|
1254
|
-
|
|
1254
|
+
out2.push(
|
|
1255
1255
|
makeFinding(RULE7, policy, {
|
|
1256
1256
|
file: c.path,
|
|
1257
1257
|
message: `Test blocks removed: ${before} \u2192 ${after} it()/test() in this spec.`,
|
|
@@ -1286,7 +1286,7 @@ var testDeletion = {
|
|
|
1286
1286
|
}
|
|
1287
1287
|
}
|
|
1288
1288
|
if (why) {
|
|
1289
|
-
|
|
1289
|
+
out2.push(
|
|
1290
1290
|
makeFinding(RULE7, policy, {
|
|
1291
1291
|
message: `Test removed via shell: ${why}.`,
|
|
1292
1292
|
evidence,
|
|
@@ -1297,7 +1297,7 @@ var testDeletion = {
|
|
|
1297
1297
|
}
|
|
1298
1298
|
}
|
|
1299
1299
|
}
|
|
1300
|
-
return
|
|
1300
|
+
return out2;
|
|
1301
1301
|
}
|
|
1302
1302
|
};
|
|
1303
1303
|
|
|
@@ -1306,19 +1306,19 @@ var RULE8 = "test-content-removal";
|
|
|
1306
1306
|
var MIN_REMOVED_LINES = 3;
|
|
1307
1307
|
var ws = (s) => s.replace(/\s+/g, "");
|
|
1308
1308
|
function significantLinesOrdered(src) {
|
|
1309
|
-
const
|
|
1309
|
+
const out2 = [];
|
|
1310
1310
|
for (const raw of src.split("\n")) {
|
|
1311
1311
|
const l = raw.trim();
|
|
1312
|
-
if (l.length >= 10 && !/^(import\b|export\s|\/\/|\*|\/\*|}\)?;?$)/.test(l))
|
|
1312
|
+
if (l.length >= 10 && !/^(import\b|export\s|\/\/|\*|\/\*|}\)?;?$)/.test(l)) out2.push(l);
|
|
1313
1313
|
}
|
|
1314
|
-
return
|
|
1314
|
+
return out2;
|
|
1315
1315
|
}
|
|
1316
1316
|
var testContentRemoval = {
|
|
1317
1317
|
id: RULE8,
|
|
1318
1318
|
surface: ["file"],
|
|
1319
1319
|
certainty: "mechanical",
|
|
1320
1320
|
run(changes, policy) {
|
|
1321
|
-
const
|
|
1321
|
+
const out2 = [];
|
|
1322
1322
|
let keptPool = "";
|
|
1323
1323
|
for (const c of changes) {
|
|
1324
1324
|
if (c.kind === "file" && c.after != null && isProtected(c.path, policy, "tests")) {
|
|
@@ -1341,7 +1341,7 @@ var testContentRemoval = {
|
|
|
1341
1341
|
gone.push(line);
|
|
1342
1342
|
}
|
|
1343
1343
|
if (gone.length >= MIN_REMOVED_LINES && netDrop >= MIN_REMOVED_LINES) {
|
|
1344
|
-
|
|
1344
|
+
out2.push(
|
|
1345
1345
|
makeFinding(RULE8, policy, {
|
|
1346
1346
|
file: c.path,
|
|
1347
1347
|
message: `Test content removed from a surviving spec: ${gone.length} significant line(s) gone with no test block deleted.`,
|
|
@@ -1351,7 +1351,7 @@ var testContentRemoval = {
|
|
|
1351
1351
|
);
|
|
1352
1352
|
}
|
|
1353
1353
|
}
|
|
1354
|
-
return
|
|
1354
|
+
return out2;
|
|
1355
1355
|
}
|
|
1356
1356
|
};
|
|
1357
1357
|
|
|
@@ -1374,8 +1374,8 @@ var snapshotRewrite = {
|
|
|
1374
1374
|
surface: ["command", "file"],
|
|
1375
1375
|
certainty: "mechanical",
|
|
1376
1376
|
run(changes, policy) {
|
|
1377
|
-
const
|
|
1378
|
-
const warn = (input) =>
|
|
1377
|
+
const out2 = [];
|
|
1378
|
+
const warn = (input) => out2.push(makeFinding(RULE9, policy, { ...input, defaultSeverity: "warn" }));
|
|
1379
1379
|
for (const c of changes) {
|
|
1380
1380
|
if (c.kind === "command") {
|
|
1381
1381
|
for (const seg of segments(c.raw)) {
|
|
@@ -1408,7 +1408,7 @@ var snapshotRewrite = {
|
|
|
1408
1408
|
remediation: "If the intended output genuinely changed, a human should confirm the new expectation against the requirement; if the test was failing, fix the code instead of re-recording the expectation from it."
|
|
1409
1409
|
});
|
|
1410
1410
|
}
|
|
1411
|
-
return
|
|
1411
|
+
return out2;
|
|
1412
1412
|
}
|
|
1413
1413
|
};
|
|
1414
1414
|
|
|
@@ -1474,18 +1474,18 @@ function activeChanges(changes, policy) {
|
|
|
1474
1474
|
}
|
|
1475
1475
|
function evaluate(changes, policy, detectors = allDetectors, view) {
|
|
1476
1476
|
const active = activeChanges(changes, policy);
|
|
1477
|
-
const
|
|
1477
|
+
const out2 = [];
|
|
1478
1478
|
for (const d of detectors) {
|
|
1479
1479
|
if (!isEnabled(d.id, policy)) continue;
|
|
1480
1480
|
try {
|
|
1481
|
-
|
|
1481
|
+
out2.push(...d.run(active, policy, view));
|
|
1482
1482
|
} catch (e) {
|
|
1483
1483
|
process.stderr.write(`tamperward: detector "${d.id}" errored and was skipped: ${String(e)}
|
|
1484
1484
|
`);
|
|
1485
1485
|
}
|
|
1486
1486
|
}
|
|
1487
1487
|
const seen = /* @__PURE__ */ new Set();
|
|
1488
|
-
const deduped =
|
|
1488
|
+
const deduped = out2.filter((f) => {
|
|
1489
1489
|
const k = key(f);
|
|
1490
1490
|
if (seen.has(k)) return false;
|
|
1491
1491
|
seen.add(k);
|
|
@@ -1571,16 +1571,16 @@ function ledgerPath(cwd, policy) {
|
|
|
1571
1571
|
function readLedger(cwd, policy) {
|
|
1572
1572
|
const p = ledgerPath(cwd, policy);
|
|
1573
1573
|
if (!existsSync2(p)) return [];
|
|
1574
|
-
const
|
|
1574
|
+
const out2 = [];
|
|
1575
1575
|
for (const line of readFileSync3(p, "utf8").split("\n")) {
|
|
1576
1576
|
if (!line.trim()) continue;
|
|
1577
1577
|
try {
|
|
1578
1578
|
const e = JSON.parse(line);
|
|
1579
|
-
if (e && typeof e.fingerprint === "string")
|
|
1579
|
+
if (e && typeof e.fingerprint === "string") out2.push(e);
|
|
1580
1580
|
} catch {
|
|
1581
1581
|
}
|
|
1582
1582
|
}
|
|
1583
|
-
return
|
|
1583
|
+
return out2;
|
|
1584
1584
|
}
|
|
1585
1585
|
function appendEntry(cwd, policy, e) {
|
|
1586
1586
|
const p = ledgerPath(cwd, policy);
|
|
@@ -1639,18 +1639,18 @@ function paint(s, code, on) {
|
|
|
1639
1639
|
return on ? code + s + RESET : s;
|
|
1640
1640
|
}
|
|
1641
1641
|
function wrap(text, width) {
|
|
1642
|
-
const
|
|
1642
|
+
const out2 = [];
|
|
1643
1643
|
let cur = "";
|
|
1644
1644
|
for (const w of text.split(/\s+/).filter(Boolean)) {
|
|
1645
1645
|
if (cur === "") cur = w;
|
|
1646
1646
|
else if (cur.length + 1 + w.length <= width) cur += " " + w;
|
|
1647
1647
|
else {
|
|
1648
|
-
|
|
1648
|
+
out2.push(cur);
|
|
1649
1649
|
cur = w;
|
|
1650
1650
|
}
|
|
1651
1651
|
}
|
|
1652
|
-
if (cur)
|
|
1653
|
-
return
|
|
1652
|
+
if (cur) out2.push(cur);
|
|
1653
|
+
return out2.length ? out2 : [""];
|
|
1654
1654
|
}
|
|
1655
1655
|
function clip(s, max) {
|
|
1656
1656
|
const one = s.replace(/\s+/g, " ").trim();
|
|
@@ -1682,15 +1682,15 @@ function renderText(input, opts) {
|
|
|
1682
1682
|
const blocks = findings.filter((f) => f.severity === "block");
|
|
1683
1683
|
const warns = findings.filter((f) => f.severity === "warn");
|
|
1684
1684
|
const c = opts.colour;
|
|
1685
|
-
const
|
|
1685
|
+
const out2 = [];
|
|
1686
1686
|
if (findings.length === 0) {
|
|
1687
1687
|
return `${paint("tamperward: clean", GREEN, c)} \u2014 no integrity findings (${scopePhrase(scanned, ignoredFiles)}).
|
|
1688
1688
|
`;
|
|
1689
1689
|
}
|
|
1690
1690
|
const headColour = blocks.length ? RED : YELLOW;
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1691
|
+
out2.push(paint(`tamperward: ${countsPhrase(blocks.length, warns.length)}`, BOLD + headColour, c));
|
|
1692
|
+
out2.push(paint(`(${scopePhrase(scanned, ignoredFiles)})`, DIM, c));
|
|
1693
|
+
out2.push("");
|
|
1694
1694
|
const LABEL = "sign-off".length;
|
|
1695
1695
|
const pad = (k) => (k + " ".repeat(LABEL)).slice(0, LABEL);
|
|
1696
1696
|
const body = Math.max(20, opts.width - LABEL - 6);
|
|
@@ -1698,22 +1698,22 @@ function renderText(input, opts) {
|
|
|
1698
1698
|
const isBlock = f.severity === "block";
|
|
1699
1699
|
const mark = paint(isBlock ? "BLOCK" : "warn ", isBlock ? BOLD + RED : YELLOW, c);
|
|
1700
1700
|
const loc = f.file ? `${f.file}${f.line ? `:${f.line}` : ""}` : "(command)";
|
|
1701
|
-
|
|
1702
|
-
for (const l of wrap(f.message, opts.width - 4))
|
|
1701
|
+
out2.push(` ${mark} ${paint(f.rule, BOLD, c)} ${paint(loc, DIM, c)}`);
|
|
1702
|
+
for (const l of wrap(f.message, opts.width - 4)) out2.push(` ${l}`);
|
|
1703
1703
|
const row = (key2, lines) => {
|
|
1704
|
-
lines.forEach((l, i) =>
|
|
1704
|
+
lines.forEach((l, i) => out2.push(` ${paint(pad(i === 0 ? key2 : ""), DIM, c)} ${l}`));
|
|
1705
1705
|
};
|
|
1706
1706
|
row("evidence", [paint(clip(f.evidence, body), DIM, c)]);
|
|
1707
1707
|
row("instead", wrap(f.remediation, body));
|
|
1708
1708
|
if (f.signoff.required) row("sign-off", [f.signoff.command]);
|
|
1709
|
-
|
|
1709
|
+
out2.push("");
|
|
1710
1710
|
}
|
|
1711
1711
|
if (blocks.length > 0) {
|
|
1712
|
-
|
|
1712
|
+
out2.push(
|
|
1713
1713
|
wrap(SIGNOFF_NOTE, opts.width - 2).map((l) => paint(l, DIM, c)).join("\n")
|
|
1714
1714
|
);
|
|
1715
1715
|
}
|
|
1716
|
-
return
|
|
1716
|
+
return out2.join("\n").replace(/\n+$/, "") + "\n";
|
|
1717
1717
|
}
|
|
1718
1718
|
|
|
1719
1719
|
// src/cli/render/github.ts
|
|
@@ -1761,7 +1761,7 @@ function renderSummary(input, env = process.env) {
|
|
|
1761
1761
|
No integrity findings (${scope}).
|
|
1762
1762
|
`;
|
|
1763
1763
|
}
|
|
1764
|
-
const
|
|
1764
|
+
const out2 = [
|
|
1765
1765
|
`## Tamperward: ${countsPhrase(blocks.length, warns.length)}`,
|
|
1766
1766
|
"",
|
|
1767
1767
|
`${scope}. Every finding below is also annotated on the diff in **Files changed**.`,
|
|
@@ -1771,21 +1771,21 @@ No integrity findings (${scope}).
|
|
|
1771
1771
|
];
|
|
1772
1772
|
for (const f of findings) {
|
|
1773
1773
|
const sev = f.severity === "block" ? "**BLOCK**" : "warn";
|
|
1774
|
-
|
|
1774
|
+
out2.push(
|
|
1775
1775
|
`| ${sev} | \`${cell(f.rule)}\` | ${locationCell(f, env)} | ${cell(f.message)} | ${cell(f.remediation)} |`
|
|
1776
1776
|
);
|
|
1777
1777
|
}
|
|
1778
1778
|
const needSignoff = findings.filter((f) => f.signoff.required);
|
|
1779
1779
|
if (needSignoff.length) {
|
|
1780
|
-
|
|
1780
|
+
out2.push("", "### Clearing a blocking finding", "", SIGNOFF_NOTE, "");
|
|
1781
1781
|
const seen = /* @__PURE__ */ new Set();
|
|
1782
1782
|
for (const f of needSignoff) {
|
|
1783
1783
|
if (seen.has(f.signoff.command)) continue;
|
|
1784
1784
|
seen.add(f.signoff.command);
|
|
1785
|
-
|
|
1785
|
+
out2.push(`- \`${cell(f.signoff.command)}\``);
|
|
1786
1786
|
}
|
|
1787
1787
|
}
|
|
1788
|
-
return
|
|
1788
|
+
return out2.join("\n") + "\n";
|
|
1789
1789
|
}
|
|
1790
1790
|
function writeSummary(md, env = process.env) {
|
|
1791
1791
|
const path = env.GITHUB_STEP_SUMMARY;
|
|
@@ -1939,8 +1939,8 @@ function synthFileChange(displayPath, before, after) {
|
|
|
1939
1939
|
try {
|
|
1940
1940
|
raw = execFileSync2("git", ["diff", "--no-index", "--no-color", a, b], { encoding: "utf8" });
|
|
1941
1941
|
} catch (e) {
|
|
1942
|
-
const
|
|
1943
|
-
raw =
|
|
1942
|
+
const err2 = e;
|
|
1943
|
+
raw = err2.stdout ? String(err2.stdout) : "";
|
|
1944
1944
|
}
|
|
1945
1945
|
} finally {
|
|
1946
1946
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -2074,7 +2074,7 @@ function ptreePath(cwd, sessionId) {
|
|
|
2074
2074
|
}
|
|
2075
2075
|
var sha = (buf) => createHash2("sha256").update(buf).digest("hex").slice(0, 16);
|
|
2076
2076
|
function snapshotProtected(cwd, policy, prev) {
|
|
2077
|
-
const
|
|
2077
|
+
const out2 = {};
|
|
2078
2078
|
const walk = (rel) => {
|
|
2079
2079
|
const abs2 = rel ? join6(cwd, rel) : cwd;
|
|
2080
2080
|
let names;
|
|
@@ -2096,7 +2096,7 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
2096
2096
|
else if (st.isFile() && isProtected(r, policy)) {
|
|
2097
2097
|
const p = prev?.[r];
|
|
2098
2098
|
if (p && p.mtimeMs === st.mtimeMs && p.size === st.size && p.mode === st.mode) {
|
|
2099
|
-
|
|
2099
|
+
out2[r] = p;
|
|
2100
2100
|
} else {
|
|
2101
2101
|
let hash = "";
|
|
2102
2102
|
try {
|
|
@@ -2104,13 +2104,13 @@ function snapshotProtected(cwd, policy, prev) {
|
|
|
2104
2104
|
} catch {
|
|
2105
2105
|
hash = "unreadable";
|
|
2106
2106
|
}
|
|
2107
|
-
|
|
2107
|
+
out2[r] = { hash, mode: st.mode, size: st.size, mtimeMs: st.mtimeMs };
|
|
2108
2108
|
}
|
|
2109
2109
|
}
|
|
2110
2110
|
}
|
|
2111
2111
|
};
|
|
2112
2112
|
walk("");
|
|
2113
|
-
return
|
|
2113
|
+
return out2;
|
|
2114
2114
|
}
|
|
2115
2115
|
function driftBetween(expected, current) {
|
|
2116
2116
|
const changed = [];
|
|
@@ -2237,9 +2237,9 @@ function runWatch(args) {
|
|
|
2237
2237
|
else if (args[i] === "--log" && args[i + 1]) log = args[++i];
|
|
2238
2238
|
}
|
|
2239
2239
|
const policy = loadPolicy(dir);
|
|
2240
|
-
const
|
|
2241
|
-
startWatcher(dir,
|
|
2242
|
-
process.stdout.write(`tamperward watch: recording protected-file events under ${dir} -> ${
|
|
2240
|
+
const out2 = log ?? defaultEventLog(dir);
|
|
2241
|
+
startWatcher(dir, out2, policy);
|
|
2242
|
+
process.stdout.write(`tamperward watch: recording protected-file events under ${dir} -> ${out2}
|
|
2243
2243
|
`);
|
|
2244
2244
|
return -1;
|
|
2245
2245
|
}
|
|
@@ -2274,7 +2274,7 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
2274
2274
|
arr.push(e);
|
|
2275
2275
|
byPath.set(e.path, arr);
|
|
2276
2276
|
}
|
|
2277
|
-
const
|
|
2277
|
+
const out2 = [];
|
|
2278
2278
|
const strict = process.env.TAMPERWARD_TRANSIENT === "block";
|
|
2279
2279
|
for (const [path, evs] of byPath) {
|
|
2280
2280
|
if (persistentPaths.has(path)) continue;
|
|
@@ -2297,9 +2297,9 @@ function transientFindings(events, persistentPaths, policy, finalHash) {
|
|
|
2297
2297
|
f.severity = "block";
|
|
2298
2298
|
f.signoff.required = true;
|
|
2299
2299
|
}
|
|
2300
|
-
|
|
2300
|
+
out2.push(f);
|
|
2301
2301
|
}
|
|
2302
|
-
return
|
|
2302
|
+
return out2;
|
|
2303
2303
|
}
|
|
2304
2304
|
|
|
2305
2305
|
// src/cli/hook.ts
|
|
@@ -2775,10 +2775,10 @@ function materialize(cwd, dest) {
|
|
|
2775
2775
|
continue;
|
|
2776
2776
|
}
|
|
2777
2777
|
if (!st.isFile()) continue;
|
|
2778
|
-
const
|
|
2779
|
-
mkdirSync6(dirname4(
|
|
2780
|
-
cpSync(src,
|
|
2781
|
-
chmodSync2(
|
|
2778
|
+
const out2 = join9(dest, rel);
|
|
2779
|
+
mkdirSync6(dirname4(out2), { recursive: true });
|
|
2780
|
+
cpSync(src, out2);
|
|
2781
|
+
chmodSync2(out2, st.mode);
|
|
2782
2782
|
}
|
|
2783
2783
|
const nm = join9(cwd, "node_modules");
|
|
2784
2784
|
if (existsSync8(nm)) symlinkSync(nm, join9(dest, "node_modules"), "dir");
|
|
@@ -2792,9 +2792,9 @@ function overlayPristine(cwd, base, dest, policy) {
|
|
|
2792
2792
|
if (!isOverlay(rel)) continue;
|
|
2793
2793
|
baseProtected.add(rel);
|
|
2794
2794
|
const content = execFileSync4("git", ["show", `${base}:${rel}`], { cwd, maxBuffer: 1 << 28 });
|
|
2795
|
-
const
|
|
2796
|
-
mkdirSync6(dirname4(
|
|
2797
|
-
writeFileSync6(
|
|
2795
|
+
const out2 = join9(dest, rel);
|
|
2796
|
+
mkdirSync6(dirname4(out2), { recursive: true });
|
|
2797
|
+
writeFileSync6(out2, content);
|
|
2798
2798
|
restored++;
|
|
2799
2799
|
}
|
|
2800
2800
|
let kept = 0;
|
|
@@ -2817,17 +2817,17 @@ function runSuite(dir, cmd, budgetSecs) {
|
|
|
2817
2817
|
}
|
|
2818
2818
|
function runVerify(opts) {
|
|
2819
2819
|
const cwd = opts.cwd ?? process.cwd();
|
|
2820
|
-
const
|
|
2820
|
+
const out2 = (s) => void process.stdout.write(s + "\n");
|
|
2821
2821
|
let policy;
|
|
2822
2822
|
try {
|
|
2823
2823
|
policy = loadPolicy(cwd);
|
|
2824
2824
|
} catch (e) {
|
|
2825
|
-
|
|
2825
|
+
out2(`verify: cannot load policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
|
|
2826
2826
|
return 2;
|
|
2827
2827
|
}
|
|
2828
2828
|
const cmd = opts.cmd ?? policy.verify?.command;
|
|
2829
2829
|
if (!cmd) {
|
|
2830
|
-
|
|
2830
|
+
out2("verify: no suite command \u2014 set policy `verify: { command: ... }` or pass --cmd");
|
|
2831
2831
|
return 2;
|
|
2832
2832
|
}
|
|
2833
2833
|
const budget = opts.budget ?? policy.verify?.budget ?? 300;
|
|
@@ -2835,7 +2835,7 @@ function runVerify(opts) {
|
|
|
2835
2835
|
try {
|
|
2836
2836
|
base = resolveBase(opts.base ?? "HEAD", cwd);
|
|
2837
2837
|
} catch {
|
|
2838
|
-
|
|
2838
|
+
out2(`verify: cannot resolve base rev "${opts.base ?? "HEAD"}" \u2014 failing closed`);
|
|
2839
2839
|
return 2;
|
|
2840
2840
|
}
|
|
2841
2841
|
const work = mkdtempSync2(join9(tmpdir2(), "tw-verify-"));
|
|
@@ -2851,7 +2851,7 @@ function runVerify(opts) {
|
|
|
2851
2851
|
[restored, kept] = overlayPristine(cwd, base, priDir, policy);
|
|
2852
2852
|
} catch (e) {
|
|
2853
2853
|
if (!opts.keep) rmSync2(work, { recursive: true, force: true });
|
|
2854
|
-
|
|
2854
|
+
out2(`verify: could not materialize (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
|
|
2855
2855
|
return 2;
|
|
2856
2856
|
}
|
|
2857
2857
|
const visible = runSuite(visDir, cmd, budget);
|
|
@@ -2873,7 +2873,7 @@ function runVerify(opts) {
|
|
|
2873
2873
|
code = 1;
|
|
2874
2874
|
}
|
|
2875
2875
|
if (opts.json) {
|
|
2876
|
-
|
|
2876
|
+
out2(
|
|
2877
2877
|
JSON.stringify({
|
|
2878
2878
|
verdict: verdict2,
|
|
2879
2879
|
base,
|
|
@@ -2893,8 +2893,8 @@ function runVerify(opts) {
|
|
|
2893
2893
|
SUITE_RED: `suite red: the visible suite fails (exit ${visible.exit}) \u2014 fix the code first (pristine exit ${pristine.exit}).`,
|
|
2894
2894
|
BUDGET_EXCEEDED: `budget exceeded (${budget}s): could not verify \u2014 failing closed, not open.`
|
|
2895
2895
|
};
|
|
2896
|
-
|
|
2897
|
-
if (kept > 0)
|
|
2896
|
+
out2(`tamperward verify \u2014 ${lines[verdict2]}`);
|
|
2897
|
+
if (kept > 0) out2(`(${kept} protected file(s) added since ${base.slice(0, 10)} were kept in the pristine run.)`);
|
|
2898
2898
|
}
|
|
2899
2899
|
return code;
|
|
2900
2900
|
}
|
|
@@ -2912,6 +2912,76 @@ function parseVerify(args) {
|
|
|
2912
2912
|
return o;
|
|
2913
2913
|
}
|
|
2914
2914
|
|
|
2915
|
+
// src/cli/run.ts
|
|
2916
|
+
import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "node:child_process";
|
|
2917
|
+
var out = (s) => process.stdout.write(s + "\n");
|
|
2918
|
+
var err = (s) => process.stderr.write(s + "\n");
|
|
2919
|
+
function git3(args, cwd) {
|
|
2920
|
+
return execFileSync5("git", args, { cwd, encoding: "utf8", maxBuffer: 1 << 28 });
|
|
2921
|
+
}
|
|
2922
|
+
function runEnvelope(opts) {
|
|
2923
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2924
|
+
if (!opts.argv.length) {
|
|
2925
|
+
err("tamperward run: no agent command given (usage: tamperward run [options] -- <command...>)");
|
|
2926
|
+
return 2;
|
|
2927
|
+
}
|
|
2928
|
+
let base;
|
|
2929
|
+
try {
|
|
2930
|
+
base = git3(["rev-parse", "--verify", `${opts.base ?? "HEAD"}^{commit}`], cwd).trim();
|
|
2931
|
+
} catch {
|
|
2932
|
+
err(`tamperward run: cannot resolve trusted base ${opts.base ?? "HEAD"} \u2014 failing closed.`);
|
|
2933
|
+
return 2;
|
|
2934
|
+
}
|
|
2935
|
+
if (!opts.allowDirty) {
|
|
2936
|
+
const dirty = git3(["status", "--porcelain"], cwd).trim();
|
|
2937
|
+
if (dirty) {
|
|
2938
|
+
err("tamperward run: working tree is dirty before the agent starts \u2014 the envelope cannot");
|
|
2939
|
+
err("attribute changes. Commit or stash first, or pass --allow-dirty to own the risk.");
|
|
2940
|
+
return 2;
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
out(`tamperward run \u2014 trusted base ${base.slice(0, 10)}; agent exit is untrusted.`);
|
|
2944
|
+
const agent = spawnSync2(opts.argv[0], opts.argv.slice(1), { cwd, stdio: "inherit" });
|
|
2945
|
+
const agentExit = agent.status ?? 1;
|
|
2946
|
+
const head = git3(["rev-parse", "HEAD"], cwd).trim();
|
|
2947
|
+
let diffCode = 0;
|
|
2948
|
+
if (head !== base) {
|
|
2949
|
+
out(`
|
|
2950
|
+
[envelope] policy check over committed changes (${base.slice(0, 10)}...${head.slice(0, 10)}):`);
|
|
2951
|
+
diffCode = runCheck({ diff: `${base}...${head}`, cwd });
|
|
2952
|
+
}
|
|
2953
|
+
out("\n[envelope] policy check over the working tree:");
|
|
2954
|
+
const workCode = runCheck({ worktree: true, cwd });
|
|
2955
|
+
out("\n[envelope] pristine verification against the trusted base:");
|
|
2956
|
+
const verifyCode = runVerify({ cwd, base, cmd: opts.cmd, budget: opts.budget });
|
|
2957
|
+
const cannot = diffCode === 2 || workCode === 2 || verifyCode === 2;
|
|
2958
|
+
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1;
|
|
2959
|
+
const enforcement = cannot ? 2 : blocked ? 1 : 0;
|
|
2960
|
+
const verdict2 = enforcement === 2 ? "CANNOT_ADJUDICATE (fail closed)" : enforcement === 1 ? "ENFORCEMENT_FAILED \u2014 the tree the runtime released does not stand" : agentExit !== 0 ? `agent exited ${agentExit} (enforcement clean)` : "GREEN MEANS GREEN";
|
|
2961
|
+
out(`
|
|
2962
|
+
tamperward run \u2014 agent exit ${agentExit}; checks diff=${diffCode} worktree=${workCode} verify=${verifyCode} \u2192 ${verdict2}`);
|
|
2963
|
+
return enforcement !== 0 ? enforcement : agentExit;
|
|
2964
|
+
}
|
|
2965
|
+
function parseRun(args) {
|
|
2966
|
+
const o = { argv: [] };
|
|
2967
|
+
for (let i = 0; i < args.length; i++) {
|
|
2968
|
+
const a = args[i];
|
|
2969
|
+
if (a === "--") {
|
|
2970
|
+
o.argv = args.slice(i + 1);
|
|
2971
|
+
break;
|
|
2972
|
+
} else if (a === "--base") o.base = args[++i];
|
|
2973
|
+
else if (a === "--cmd") o.cmd = args[++i];
|
|
2974
|
+
else if (a === "--budget") o.budget = Number(args[++i]);
|
|
2975
|
+
else if (a === "--allow-dirty") o.allowDirty = true;
|
|
2976
|
+
else if (a === "--cwd") o.cwd = args[++i];
|
|
2977
|
+
else {
|
|
2978
|
+
o.argv = args.slice(i);
|
|
2979
|
+
break;
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
return o;
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2915
2985
|
// src/cli/index.ts
|
|
2916
2986
|
function parseAllow(args) {
|
|
2917
2987
|
const o = {};
|
|
@@ -2992,6 +3062,14 @@ Formats:
|
|
|
2992
3062
|
from the trusted base; a visible-green /
|
|
2993
3063
|
pristine-red result is a MASKED FAILURE
|
|
2994
3064
|
(exit 1); cannot-verify fails closed (2)
|
|
3065
|
+
tamperward run [opts] -- <agent cmd...> enforcement envelope: record the trusted
|
|
3066
|
+
[--base R] [--cmd C] base, run the agent, treat its exit as
|
|
3067
|
+
[--budget S] [--allow-dirty] untrusted, then re-adjudicate the tree it
|
|
3068
|
+
left (policy over base...HEAD and the
|
|
3069
|
+
worktree, plus verify). Exit: agent's code
|
|
3070
|
+
when clean; 1 on any blocking finding or
|
|
3071
|
+
masked failure; 2 when it cannot
|
|
3072
|
+
adjudicate (fails closed)
|
|
2995
3073
|
tamperward allow <rule> --reason "..." record a human sign-off (local audit ledger)
|
|
2996
3074
|
tamperward init [--dry-run] wire the policy file plus every
|
|
2997
3075
|
enforcement point: Claude Code hooks,
|
|
@@ -3017,6 +3095,8 @@ function main(argv2) {
|
|
|
3017
3095
|
return runInit(parseInit(rest));
|
|
3018
3096
|
case "verify":
|
|
3019
3097
|
return runVerify(parseVerify(rest));
|
|
3098
|
+
case "run":
|
|
3099
|
+
return runEnvelope(parseRun(rest));
|
|
3020
3100
|
case void 0:
|
|
3021
3101
|
case "-h":
|
|
3022
3102
|
case "--help":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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",
|