tamperward 1.10.1 → 1.10.3
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 +62 -16
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -272,6 +272,19 @@ function diffWorktree(opts = {}) {
|
|
|
272
272
|
(c) => enrich(c, (p) => blobAt("HEAD", p, opts.cwd), (p) => fromDisk(p, opts.cwd))
|
|
273
273
|
);
|
|
274
274
|
}
|
|
275
|
+
function diffWorktreeWithUntracked(opts = {}) {
|
|
276
|
+
const tracked = diffWorktree(opts);
|
|
277
|
+
const others = git(["ls-files", "--others", "--exclude-standard", "-z"], opts.cwd).split("\0").filter(Boolean);
|
|
278
|
+
const seen = new Set(tracked.filter((c) => c.kind === "file").map((c) => c.path));
|
|
279
|
+
const untracked = others.filter((rel) => !seen.has(rel)).map(
|
|
280
|
+
(rel) => enrich(
|
|
281
|
+
{ kind: "file", path: rel, oldPath: null, op: "add", before: null, after: null, binary: false, hunks: [] },
|
|
282
|
+
() => null,
|
|
283
|
+
(p) => fromDisk(p, opts.cwd)
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
return [...tracked, ...untracked];
|
|
287
|
+
}
|
|
275
288
|
|
|
276
289
|
// src/detectors/finding.ts
|
|
277
290
|
function severityOf(rule, policy, fallback = "block") {
|
|
@@ -316,7 +329,16 @@ var LITERAL = [
|
|
|
316
329
|
{ re: /(?:^|\s)--no-verify(?:\s|$)/, why: "--no-verify skips the pre-commit/pre-push hooks" },
|
|
317
330
|
{ re: /(?:^|\s)--no-hooks(?:\s|$)/, why: "--no-hooks disables hook execution" },
|
|
318
331
|
{ re: /(?:^|[\s;&|=])HUSKY=0(?:\s|$)/, why: "HUSKY=0 disables all Husky hooks" },
|
|
319
|
-
{ re: /\bHUSKY_SKIP_HOOKS=1\b/, why: "HUSKY_SKIP_HOOKS=1 skips Husky hooks" }
|
|
332
|
+
{ re: /\bHUSKY_SKIP_HOOKS=1\b/, why: "HUSKY_SKIP_HOOKS=1 skips Husky hooks" },
|
|
333
|
+
// git-native hook disabling. core.hooksPath is strictly MORE general than the
|
|
334
|
+
// flags above: it turns off husky, lefthook, plain .git/hooks and pre-commit
|
|
335
|
+
// alike, and it was reachable in three spellings the literal list missed
|
|
336
|
+
// (-c, `git config`, and GIT_CONFIG_* env injection). (P1-9, external review.)
|
|
337
|
+
{ re: /\bcore\.hooksPath\b/i, why: "core.hooksPath redirects git away from the installed hooks" },
|
|
338
|
+
{ re: /\bGIT_CONFIG_KEY_\d+\s*=\s*["']?core\.hooksPath/i, why: "GIT_CONFIG_* injection sets core.hooksPath" },
|
|
339
|
+
{ re: /(?:^|[\s;&|=])LEFTHOOK=0(?:\s|$)/, why: "LEFTHOOK=0 disables Lefthook hooks" },
|
|
340
|
+
{ re: /\bSKIP_SIMPLE_GIT_HOOKS=1\b/, why: "SKIP_SIMPLE_GIT_HOOKS=1 skips simple-git-hooks" },
|
|
341
|
+
{ re: /\bPRE_COMMIT_ALLOW_NO_CONFIG=1\b/, why: "PRE_COMMIT_ALLOW_NO_CONFIG=1 lets pre-commit no-op" }
|
|
320
342
|
];
|
|
321
343
|
var SHORT_N = /^-[a-z]*n[a-z]*$/i;
|
|
322
344
|
var noVerify = {
|
|
@@ -867,6 +889,9 @@ var CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|play
|
|
|
867
889
|
var YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
|
|
868
890
|
var INVOKES_TOOL = /(?:^\s*|[;&|`]\s*|\$\(\s*|\b(?:npx|yarn|pnpm|bunx?)\s+)(?:jest|vitest|eslint|tsc|playwright|pytest|tamperward)\b/;
|
|
869
891
|
var INVOKES_SCRIPT = /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|tests|lint|typecheck|type-check|coverage)\b|\b(?:make|cargo|go)\s+test\b|\bgradle\w*\s+(?:test|check)\b/;
|
|
892
|
+
function isActiveWorkflow(path) {
|
|
893
|
+
return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(path);
|
|
894
|
+
}
|
|
870
895
|
var invokesCheck = (line) => INVOKES_TOOL.test(line) || INVOKES_SCRIPT.test(line);
|
|
871
896
|
function commandCore(line) {
|
|
872
897
|
return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses):\s*/, "").replace(/\s+/g, " ").trim();
|
|
@@ -897,6 +922,17 @@ var ciTampering = {
|
|
|
897
922
|
const out2 = [];
|
|
898
923
|
for (const c of changes) {
|
|
899
924
|
if (c.kind !== "file") continue;
|
|
925
|
+
if (c.op === "rename" && c.oldPath && isActiveWorkflow(c.oldPath) && !isActiveWorkflow(c.path)) {
|
|
926
|
+
out2.push(
|
|
927
|
+
makeFinding(RULE5, policy, {
|
|
928
|
+
file: c.path,
|
|
929
|
+
message: `A CI workflow was renamed so it no longer runs (${c.oldPath} \u2192 ${c.path}).`,
|
|
930
|
+
evidence: `${c.oldPath} \u2192 ${c.path}`,
|
|
931
|
+
remediation: "Restore the path and extension. GitHub runs only *.yml/*.yaml directly under .github/workflows \u2014 renaming outside that disables the checks as surely as deleting them."
|
|
932
|
+
})
|
|
933
|
+
);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
900
936
|
if (!isProtected(c.path, policy, "ci")) continue;
|
|
901
937
|
for (const l of addedLines(c)) {
|
|
902
938
|
const coe = l.content.match(/^\s*-?\s*continue-on-error:\s*(.+?)\s*$/);
|
|
@@ -1247,7 +1283,7 @@ var testDeletion = {
|
|
|
1247
1283
|
remediation: "Restoring the path. Renaming a spec out of the glob silently removes it from the suite."
|
|
1248
1284
|
})
|
|
1249
1285
|
);
|
|
1250
|
-
} else if (c.op === "modify" && isTest && c.before != null && c.after != null) {
|
|
1286
|
+
} else if ((c.op === "modify" || c.op === "rename") && isTest && c.before != null && c.after != null) {
|
|
1251
1287
|
const before = countTestBlocks(c.before);
|
|
1252
1288
|
const after = countTestBlocks(c.after);
|
|
1253
1289
|
if (after < before) {
|
|
@@ -1326,7 +1362,7 @@ var testContentRemoval = {
|
|
|
1326
1362
|
}
|
|
1327
1363
|
}
|
|
1328
1364
|
for (const c of changes) {
|
|
1329
|
-
if (c.kind !== "file" || c.op !== "modify" || c.before == null || c.after == null) continue;
|
|
1365
|
+
if (c.kind !== "file" || c.op !== "modify" && c.op !== "rename" || c.before == null || c.after == null) continue;
|
|
1330
1366
|
if (!isProtected(c.path, policy, "tests")) continue;
|
|
1331
1367
|
if (isProtected(c.path, policy, "snapshots")) continue;
|
|
1332
1368
|
if (countTestBlocks(c.after) < countTestBlocks(c.before)) continue;
|
|
@@ -1652,8 +1688,17 @@ function wrap(text, width) {
|
|
|
1652
1688
|
if (cur) out2.push(cur);
|
|
1653
1689
|
return out2.length ? out2 : [""];
|
|
1654
1690
|
}
|
|
1691
|
+
function stripControl(v) {
|
|
1692
|
+
let outStr = "";
|
|
1693
|
+
for (const ch of v) {
|
|
1694
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
1695
|
+
const control = cp < 32 && cp !== 9 && cp !== 10 && cp !== 13 || cp === 127 || cp >= 128 && cp <= 159;
|
|
1696
|
+
outStr += control ? "\uFFFD" : ch;
|
|
1697
|
+
}
|
|
1698
|
+
return outStr;
|
|
1699
|
+
}
|
|
1655
1700
|
function clip(s, max) {
|
|
1656
|
-
const one = s.replace(/\s+/g, " ").trim();
|
|
1701
|
+
const one = stripControl(s).replace(/\s+/g, " ").trim();
|
|
1657
1702
|
return one.length > max ? one.slice(0, Math.max(1, max - 1)) + "\u2026" : one;
|
|
1658
1703
|
}
|
|
1659
1704
|
function orderFindings(findings) {
|
|
@@ -1840,7 +1885,7 @@ function report(input) {
|
|
|
1840
1885
|
|
|
1841
1886
|
// src/cli/check.ts
|
|
1842
1887
|
function check(opts) {
|
|
1843
|
-
let policy = loadPolicy(opts.cwd);
|
|
1888
|
+
let policy = opts.policyOverride ?? loadPolicy(opts.cwd);
|
|
1844
1889
|
const cwd = opts.cwd ?? process.cwd();
|
|
1845
1890
|
let changes;
|
|
1846
1891
|
let layer;
|
|
@@ -1850,8 +1895,8 @@ function check(opts) {
|
|
|
1850
1895
|
layer = "local";
|
|
1851
1896
|
view = "staged";
|
|
1852
1897
|
} else if (opts.worktree) {
|
|
1853
|
-
changes = diffWorktree({ cwd: opts.cwd });
|
|
1854
|
-
layer = "local";
|
|
1898
|
+
changes = opts.includeUntracked ? diffWorktreeWithUntracked({ cwd: opts.cwd }) : diffWorktree({ cwd: opts.cwd });
|
|
1899
|
+
layer = opts.ciLayer ? "ci" : "local";
|
|
1855
1900
|
view = "worktree";
|
|
1856
1901
|
} else if (opts.diff) {
|
|
1857
1902
|
const [base, head] = opts.diff.split(/\.{2,3}/);
|
|
@@ -1863,7 +1908,7 @@ function check(opts) {
|
|
|
1863
1908
|
changes = diffRange(base, head, { cwd: opts.cwd });
|
|
1864
1909
|
layer = "ci";
|
|
1865
1910
|
view = "range";
|
|
1866
|
-
policy = loadPolicyAt(mergeBaseOf(base, head, { cwd: opts.cwd }), opts.cwd) ?? defaultPolicy();
|
|
1911
|
+
policy = opts.policyOverride ?? loadPolicyAt(mergeBaseOf(base, head, { cwd: opts.cwd }), opts.cwd) ?? defaultPolicy();
|
|
1867
1912
|
} else {
|
|
1868
1913
|
process.stderr.write("tamperward: specify --staged, --worktree, or --diff <base>...<head>\n");
|
|
1869
1914
|
return 2;
|
|
@@ -2820,7 +2865,7 @@ function runVerify(opts) {
|
|
|
2820
2865
|
const out2 = (s) => void process.stdout.write(s + "\n");
|
|
2821
2866
|
let policy;
|
|
2822
2867
|
try {
|
|
2823
|
-
policy = loadPolicy(cwd);
|
|
2868
|
+
policy = opts.policyOverride ?? loadPolicy(cwd);
|
|
2824
2869
|
} catch (e) {
|
|
2825
2870
|
out2(`verify: cannot load policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed`);
|
|
2826
2871
|
return 2;
|
|
@@ -2932,12 +2977,13 @@ function runEnvelope(opts) {
|
|
|
2932
2977
|
err(`tamperward run: cannot resolve trusted base ${opts.base ?? "HEAD"} \u2014 failing closed.`);
|
|
2933
2978
|
return 2;
|
|
2934
2979
|
}
|
|
2980
|
+
let frozenPolicy;
|
|
2935
2981
|
let frozenCmd;
|
|
2936
2982
|
let frozenBudget;
|
|
2937
2983
|
try {
|
|
2938
|
-
|
|
2939
|
-
frozenCmd = opts.cmd ??
|
|
2940
|
-
frozenBudget = opts.budget ??
|
|
2984
|
+
frozenPolicy = loadPolicyAt(base, cwd) ?? defaultPolicy();
|
|
2985
|
+
frozenCmd = opts.cmd ?? frozenPolicy.verify?.command;
|
|
2986
|
+
frozenBudget = opts.budget ?? frozenPolicy.verify?.budget;
|
|
2941
2987
|
} catch (e) {
|
|
2942
2988
|
err(`tamperward run: cannot load the entry-time policy (${e instanceof Error ? e.message : String(e)}) \u2014 failing closed.`);
|
|
2943
2989
|
return 2;
|
|
@@ -2971,12 +3017,12 @@ tamperward run \u2014 agent exit ${agentExit}; HISTORY_REWRITE \u2192 ENFORCEMEN
|
|
|
2971
3017
|
if (head !== base) {
|
|
2972
3018
|
out(`
|
|
2973
3019
|
[envelope] policy check over committed changes (${base.slice(0, 10)}...${head.slice(0, 10)}):`);
|
|
2974
|
-
diffCode = runCheck({ diff: `${base}...${head}`, cwd });
|
|
3020
|
+
diffCode = runCheck({ diff: `${base}...${head}`, cwd, policyOverride: frozenPolicy });
|
|
2975
3021
|
}
|
|
2976
|
-
out("\n[envelope] policy check over the working tree:");
|
|
2977
|
-
const workCode = runCheck({ worktree: true, cwd });
|
|
3022
|
+
out("\n[envelope] policy check over the working tree (frozen policy, untracked included, no local ledger):");
|
|
3023
|
+
const workCode = runCheck({ worktree: true, cwd, policyOverride: frozenPolicy, includeUntracked: true, ciLayer: true });
|
|
2978
3024
|
out("\n[envelope] pristine verification against the trusted base:");
|
|
2979
|
-
const verifyCode = runVerify({ cwd, base, cmd: frozenCmd, budget: frozenBudget });
|
|
3025
|
+
const verifyCode = runVerify({ cwd, base, cmd: frozenCmd, budget: frozenBudget, policyOverride: frozenPolicy });
|
|
2980
3026
|
const cannot = diffCode === 2 || workCode === 2 || verifyCode === 2;
|
|
2981
3027
|
const blocked = diffCode === 1 || workCode === 1 || verifyCode === 1;
|
|
2982
3028
|
const enforcement = cannot ? 2 : blocked ? 1 : 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.3",
|
|
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",
|