tamperward 2.15.0 → 2.15.1
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/README.md +10 -1
- package/dist/cli/index.js +109 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -229,10 +229,19 @@ Since **2.15.0**, `tamperward doctor` is also the one-shot installation/posture
|
|
|
229
229
|
report promised by `init`'s security model. It reuses `init`'s canonical wiring
|
|
230
230
|
planner rather than maintaining a second definition of “installed correctly”, and
|
|
231
231
|
reports named `OK`, `WARN`, or `BROKEN` checks for policy/schema, Claude hooks,
|
|
232
|
-
pre-commit,
|
|
232
|
+
pre-commit, CI authority wiring, CODEOWNERS, workflow permissions, binary/pin
|
|
233
233
|
alignment, verifier trust mode + declared inputs, platform residuals, CI verifier
|
|
234
234
|
outer time, observer health, and (with `--github`) repository authority. Use
|
|
235
235
|
`--json` for one machine-readable document with `authoritative: true|false`.
|
|
236
|
+
|
|
237
|
+
**2.15.1 closes an authority-reporting gap in that surface.** The permission check now
|
|
238
|
+
includes job-level overrides, not only workflow-root permissions; a policy schema newer
|
|
239
|
+
than the running binary is BROKEN rather than certifiable; and the exact workflow(s)
|
|
240
|
+
selected by `--workflow` or discovered as verifier authorities are also the workflow(s)
|
|
241
|
+
used for CI-wiring and token-permission posture. A custom verifier workflow therefore
|
|
242
|
+
does not get judged against an unrelated/missing generated file, and a second verifier
|
|
243
|
+
workflow with a write-scoped job cannot hide behind a safe canonical workflow.
|
|
244
|
+
|
|
236
245
|
Local early-layer gaps remain posture findings rather than silently changing the
|
|
237
246
|
existing generated-CI exit contract; hard CI/GitHub validation failures still exit 2.
|
|
238
247
|
|
package/dist/cli/index.js
CHANGED
|
@@ -8416,52 +8416,90 @@ function pinsInFile(path) {
|
|
|
8416
8416
|
}
|
|
8417
8417
|
return Array.from(src.matchAll(/\btamperward@((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))\b/g), (m) => m[1]);
|
|
8418
8418
|
}
|
|
8419
|
-
function
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
try {
|
|
8425
|
-
doc = parse3(readFileSync13(path, "utf8"));
|
|
8426
|
-
} catch (e) {
|
|
8427
|
-
return {
|
|
8428
|
-
id: "workflow-permissions",
|
|
8429
|
-
state: "BROKEN",
|
|
8430
|
-
detail: `${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`
|
|
8431
|
-
};
|
|
8419
|
+
function permissionWrites(value) {
|
|
8420
|
+
if (typeof value === "string") {
|
|
8421
|
+
if (/write/i.test(value)) return [value];
|
|
8422
|
+
if (/^(?:read-all|none)$/i.test(value)) return [];
|
|
8423
|
+
return null;
|
|
8432
8424
|
}
|
|
8433
|
-
const
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8425
|
+
const mapping = asMapping(value);
|
|
8426
|
+
if (!mapping) return null;
|
|
8427
|
+
return Object.entries(mapping).filter(([, permission]) => typeof permission === "string" && /write/i.test(permission)).map(([name, permission]) => `${name}=${String(permission)}`);
|
|
8428
|
+
}
|
|
8429
|
+
function workflowPermissionCheck(cwd, workflowRels = [".github/workflows/tamperward.yml"]) {
|
|
8430
|
+
const broken = [];
|
|
8431
|
+
const warnings = [];
|
|
8432
|
+
for (const rel of workflowRels) {
|
|
8433
|
+
const path = resolve4(cwd, rel);
|
|
8434
|
+
if (!existsSync10(path)) {
|
|
8435
|
+
broken.push(`${rel} is missing`);
|
|
8436
|
+
continue;
|
|
8437
|
+
}
|
|
8438
|
+
let doc;
|
|
8439
|
+
try {
|
|
8440
|
+
doc = parse3(readFileSync13(path, "utf8"));
|
|
8441
|
+
} catch (e) {
|
|
8442
|
+
broken.push(`${rel} is not valid YAML (${e instanceof Error ? e.message : String(e)})`);
|
|
8443
|
+
continue;
|
|
8444
|
+
}
|
|
8445
|
+
const root = asMapping(doc);
|
|
8446
|
+
if (!root) {
|
|
8447
|
+
broken.push(`${rel} is not a YAML mapping`);
|
|
8448
|
+
continue;
|
|
8449
|
+
}
|
|
8450
|
+
const rootPermissions = root.permissions;
|
|
8451
|
+
if (rootPermissions === void 0) {
|
|
8452
|
+
broken.push(`${rel}: workflow does not declare least-privilege permissions; repository defaults could grant write access`);
|
|
8453
|
+
} else {
|
|
8454
|
+
const writes = permissionWrites(rootPermissions);
|
|
8455
|
+
if (writes === null) {
|
|
8456
|
+
broken.push(`${rel}: workflow permissions shape is unsupported`);
|
|
8457
|
+
} else if (writes.length) {
|
|
8458
|
+
broken.push(`${rel}: workflow grants write permission: ${writes.join(", ")}`);
|
|
8459
|
+
} else {
|
|
8460
|
+
const mapping = asMapping(rootPermissions);
|
|
8461
|
+
if (mapping && mapping.contents !== "read") {
|
|
8462
|
+
warnings.push(`${rel}: workflow permissions contain no writes but contents: read is not declared`);
|
|
8463
|
+
}
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
8466
|
+
const jobs = asMapping(root.jobs);
|
|
8467
|
+
if (!jobs) continue;
|
|
8468
|
+
for (const [name, rawJob] of Object.entries(jobs)) {
|
|
8469
|
+
const job = asMapping(rawJob);
|
|
8470
|
+
if (!job || job.permissions === void 0) continue;
|
|
8471
|
+
const writes = permissionWrites(job.permissions);
|
|
8472
|
+
if (writes === null) {
|
|
8473
|
+
broken.push(`${rel} job "${name}": permissions shape is unsupported`);
|
|
8474
|
+
} else if (writes.length) {
|
|
8475
|
+
broken.push(`${rel} job "${name}" grants write permission: ${writes.join(", ")}`);
|
|
8476
|
+
}
|
|
8477
|
+
}
|
|
8441
8478
|
}
|
|
8442
|
-
|
|
8443
|
-
([, value]) => typeof value === "string" && /write/i.test(value)
|
|
8444
|
-
);
|
|
8445
|
-
if (writes.length) {
|
|
8479
|
+
if (broken.length) {
|
|
8446
8480
|
return {
|
|
8447
8481
|
id: "workflow-permissions",
|
|
8448
8482
|
state: "BROKEN",
|
|
8449
|
-
detail:
|
|
8483
|
+
detail: broken.join("; ")
|
|
8450
8484
|
};
|
|
8451
8485
|
}
|
|
8452
|
-
if (
|
|
8486
|
+
if (warnings.length) {
|
|
8453
8487
|
return {
|
|
8454
8488
|
id: "workflow-permissions",
|
|
8455
8489
|
state: "WARN",
|
|
8456
|
-
detail: "
|
|
8490
|
+
detail: warnings.join("; ")
|
|
8457
8491
|
};
|
|
8458
8492
|
}
|
|
8459
|
-
return {
|
|
8493
|
+
return {
|
|
8494
|
+
id: "workflow-permissions",
|
|
8495
|
+
state: "OK",
|
|
8496
|
+
detail: `${workflowRels.join(", ")}: workflow/job token permissions contain no write grants and workflow contents access is read-only`
|
|
8497
|
+
};
|
|
8460
8498
|
}
|
|
8461
|
-
function collectLocalPosture(cwd, policy) {
|
|
8499
|
+
function collectLocalPosture(cwd, policy, authorityWorkflows, explicitWorkflow = false) {
|
|
8462
8500
|
const checks = [];
|
|
8463
8501
|
checks.push(
|
|
8464
|
-
policy.version <= POLICY_VERSION ? { id: "policy", state: "OK", detail: `policy version ${policy.version} is understood (current schema ${POLICY_VERSION})` } : { id: "policy", state: "
|
|
8502
|
+
policy.version <= POLICY_VERSION ? { id: "policy", state: "OK", detail: `policy version ${policy.version} is understood (current schema ${POLICY_VERSION})` } : { id: "policy", state: "BROKEN", detail: `policy version ${policy.version} is newer than this binary's schema ${POLICY_VERSION}; authority cannot be certified for unknown policy semantics` }
|
|
8465
8503
|
);
|
|
8466
8504
|
let plan = [];
|
|
8467
8505
|
try {
|
|
@@ -8488,9 +8526,29 @@ function collectLocalPosture(cwd, policy) {
|
|
|
8488
8526
|
};
|
|
8489
8527
|
addWiring("claude-hooks", "agent", true);
|
|
8490
8528
|
addWiring("pre-commit", "pre-commit", false);
|
|
8491
|
-
|
|
8529
|
+
const canonicalWorkflow = ".github/workflows/tamperward.yml";
|
|
8530
|
+
if (authorityWorkflows?.length) {
|
|
8531
|
+
const normalized = authorityWorkflows.map((x) => x.replace(/\\/g, "/"));
|
|
8532
|
+
if (explicitWorkflow) {
|
|
8533
|
+
checks.push({
|
|
8534
|
+
id: "ci-wiring",
|
|
8535
|
+
state: "OK",
|
|
8536
|
+
detail: `explicit verifier authority workflow: ${normalized.join(", ")}; verifier step/timeout validated by doctor`
|
|
8537
|
+
});
|
|
8538
|
+
} else if (normalized.includes(canonicalWorkflow)) {
|
|
8539
|
+
addWiring("ci-wiring", "ci", true);
|
|
8540
|
+
} else {
|
|
8541
|
+
checks.push({
|
|
8542
|
+
id: "ci-wiring",
|
|
8543
|
+
state: "OK",
|
|
8544
|
+
detail: `discovered verifier authority workflow(s): ${normalized.join(", ")}; verifier step/timeout validated by doctor`
|
|
8545
|
+
});
|
|
8546
|
+
}
|
|
8547
|
+
} else {
|
|
8548
|
+
addWiring("ci-wiring", "ci", true);
|
|
8549
|
+
}
|
|
8492
8550
|
addWiring("codeowners", "codeowners", true);
|
|
8493
|
-
checks.push(workflowPermissionCheck(cwd));
|
|
8551
|
+
checks.push(workflowPermissionCheck(cwd, authorityWorkflows));
|
|
8494
8552
|
const pins = [
|
|
8495
8553
|
...pinsInFile(join13(cwd, ".claude", "settings.json")),
|
|
8496
8554
|
...pinsInFile(join13(cwd, ".git", "hooks", "pre-commit")),
|
|
@@ -8744,7 +8802,13 @@ function runDoctor(opts = {}) {
|
|
|
8744
8802
|
"policy"
|
|
8745
8803
|
);
|
|
8746
8804
|
}
|
|
8747
|
-
|
|
8805
|
+
let postureWorkflows = opts.workflow ? [opts.workflow] : void 0;
|
|
8806
|
+
const fail = (id, message) => err(
|
|
8807
|
+
message,
|
|
8808
|
+
opts,
|
|
8809
|
+
id,
|
|
8810
|
+
collectLocalPosture(cwd, policy, postureWorkflows, Boolean(opts.workflow))
|
|
8811
|
+
);
|
|
8748
8812
|
if (!policy.verify?.command) {
|
|
8749
8813
|
return fail("verifier", "trusted policy has no verify.command; generated CI cannot verify this repository");
|
|
8750
8814
|
}
|
|
@@ -8771,6 +8835,7 @@ function runDoctor(opts = {}) {
|
|
|
8771
8835
|
const requiredSecs = requiredVerifierAuthoritySeconds(policy.verify.budget);
|
|
8772
8836
|
const requiredMinutes = Math.ceil(requiredSecs / 60);
|
|
8773
8837
|
let verifyJobCount = 0;
|
|
8838
|
+
const authorityWorkflows = [];
|
|
8774
8839
|
for (const workflowRel of workflowRels) {
|
|
8775
8840
|
const workflowPath = resolve4(cwd, workflowRel);
|
|
8776
8841
|
if (!existsSync10(workflowPath)) {
|
|
@@ -8787,6 +8852,10 @@ function runDoctor(opts = {}) {
|
|
|
8787
8852
|
}
|
|
8788
8853
|
const jobs = verifyJobs(doc);
|
|
8789
8854
|
verifyJobCount += jobs.length;
|
|
8855
|
+
if (jobs.length > 0) {
|
|
8856
|
+
authorityWorkflows.push(workflowRel);
|
|
8857
|
+
postureWorkflows = [...authorityWorkflows];
|
|
8858
|
+
}
|
|
8790
8859
|
for (const { name, job } of jobs) {
|
|
8791
8860
|
const timeout = job["timeout-minutes"];
|
|
8792
8861
|
if (timeout === void 0 || timeout === null) {
|
|
@@ -8845,7 +8914,12 @@ function runDoctor(opts = {}) {
|
|
|
8845
8914
|
);
|
|
8846
8915
|
}
|
|
8847
8916
|
}
|
|
8848
|
-
const checks = collectLocalPosture(
|
|
8917
|
+
const checks = collectLocalPosture(
|
|
8918
|
+
cwd,
|
|
8919
|
+
policy,
|
|
8920
|
+
authorityWorkflows,
|
|
8921
|
+
Boolean(opts.workflow)
|
|
8922
|
+
);
|
|
8849
8923
|
checks.push({
|
|
8850
8924
|
id: "ci-verifier",
|
|
8851
8925
|
state: "OK",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tamperward",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.1",
|
|
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",
|