@codapult/guard 0.10.0 → 0.11.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/README.md +144 -4
- package/dist/adapters/agents/agent-integration.js +26 -3
- package/dist/cli/commands/guard.d.ts +2 -0
- package/dist/cli/commands/guard.js +77 -11
- package/dist/cli/index.js +2 -0
- package/dist/core/guard.d.ts +28 -1
- package/dist/core/guard.js +37 -0
- package/dist/core/history/runs.d.ts +64 -1
- package/dist/core/history/runs.js +109 -0
- package/dist/core/output/attestation.d.ts +17 -0
- package/dist/core/output/attestation.js +22 -0
- package/dist/core/policy/schemas.d.ts +47 -0
- package/dist/core/policy/schemas.js +67 -0
- package/dist/core/verification/github-actions.d.ts +25 -0
- package/dist/core/verification/github-actions.js +397 -0
- package/dist/core/verification/github-context.d.ts +8 -0
- package/dist/core/verification/github-context.js +41 -0
- package/dist/core/verification/verify.d.ts +23 -4
- package/dist/core/verification/verify.js +601 -47
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/prompts.js +7 -4
- package/dist/mcp/tools/guard.js +9 -3
- package/docs/extensions.md +79 -0
- package/docs/guard-agent-integration.md +51 -0
- package/docs/guard-ci.yml +202 -0
- package/docs/guard-fixtures.md +127 -0
- package/docs/guard-trusted-pr-attestation.yml +213 -0
- package/docs/guard.md +1125 -0
- package/docs/integrations/README.md +38 -0
- package/docs/integrations/claude-code.md +92 -0
- package/docs/integrations/codex.md +58 -0
- package/docs/integrations/cursor.md +116 -0
- package/docs/integrations/gemini-cli.md +47 -0
- package/docs/integrations/generic.md +41 -0
- package/docs/integrations/github-copilot.md +72 -0
- package/docs/policy-mutations.md +75 -0
- package/docs/releasing.md +58 -0
- package/docs/verify-guard-attestation.mjs +166 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](package.json)
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](https://registry.modelcontextprotocol.io/?q=io.github.codapult%2Fguard)
|
|
9
10
|
|
|
10
11
|
## Architecture guardrails for AI coding agents
|
|
11
12
|
|
|
@@ -388,6 +389,131 @@ review control when policy changes require a separate approval. For example, a r
|
|
|
388
389
|
`.codapult/guard/* @architecture-owners` to `CODEOWNERS`. Guard reports the policy change; the
|
|
389
390
|
hosting platform enforces reviewer authorization.
|
|
390
391
|
|
|
392
|
+
Guard also reports changes to verification-sensitive files using built-in paths for CI workflows,
|
|
393
|
+
package scripts and lockfiles, and common formatter, linter, typecheck, and test configurations.
|
|
394
|
+
Projects can extend this list in the base revision's `.codapult/guard/rules.json`:
|
|
395
|
+
|
|
396
|
+
```json
|
|
397
|
+
{
|
|
398
|
+
"version": 1,
|
|
399
|
+
"rules": [],
|
|
400
|
+
"protectedPaths": [".husky/**", "config/security-checks/**"]
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
For classified entries and warning-only paths, use the explicit `policySurface.paths` form in that
|
|
405
|
+
same base policy:
|
|
406
|
+
|
|
407
|
+
```json
|
|
408
|
+
{
|
|
409
|
+
"version": 1,
|
|
410
|
+
"rules": [],
|
|
411
|
+
"policySurface": {
|
|
412
|
+
"paths": [
|
|
413
|
+
{ "pattern": "config/security-checks/**", "kind": "custom", "severity": "error" },
|
|
414
|
+
{ "pattern": "**/.custom-ignore", "kind": "ignore-config", "severity": "warning" }
|
|
415
|
+
]
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Supported kinds are `ci-workflow`, `guard-policy`, `ownership`, `dependency-input`, `tool-config`, `ignore-config`,
|
|
421
|
+
`suppression-source`, and `custom`. `error` entries fail when `--fail-on-protected-changes` is
|
|
422
|
+
enabled; `warning` entries are reported without blocking. Built-in verification-sensitive paths
|
|
423
|
+
remain blocking. Both forms are read from the base policy during PR verification, so the PR cannot
|
|
424
|
+
remove its own path coverage.
|
|
425
|
+
|
|
426
|
+
In pull request CI, add `--fail-on-protected-changes` alongside `--policy-base`. This fails the
|
|
427
|
+
Guard invocation when a listed path changes without a matching CI integrity contract, or when a
|
|
428
|
+
contracted workflow fails its checks. It emits all matched paths in JSON output. Protect
|
|
429
|
+
the Guard workflow itself and require its status check in repository settings: a workflow edited
|
|
430
|
+
or removed by a PR cannot independently enforce its own integrity. Guard reports workflow edits,
|
|
431
|
+
but GitHub branch protection is what makes the check mandatory. The sample workflow rejects PR
|
|
432
|
+
changes to package manifests, common npm/pnpm/Yarn/Bun lockfiles, package-manager config, and
|
|
433
|
+
workspace install inputs before
|
|
434
|
+
selecting pnpm or installing dependencies. Review those changes in a separate PR. This preflight is
|
|
435
|
+
defense in depth: repository rules must still protect the workflow and require the Guard check.
|
|
436
|
+
|
|
437
|
+
Guard reports added `eslint-disable`, `@ts-ignore`, `@ts-nocheck`, `@ts-expect-error`, and
|
|
438
|
+
`biome-ignore`, and `prettier-ignore` markers in the PR diff. It reports the file and marker
|
|
439
|
+
without interpreting the tool-specific reason or whether the suppression is justified. Other
|
|
440
|
+
tools can be covered by protecting their configuration/source paths and running them in a trusted
|
|
441
|
+
CI workflow. The matched paths, markers, and scan completeness are recorded in the verification
|
|
442
|
+
result and receipt.
|
|
443
|
+
|
|
444
|
+
### Required verification stages
|
|
445
|
+
|
|
446
|
+
`agent.json` declares which stages must complete for verification to pass:
|
|
447
|
+
|
|
448
|
+
```json
|
|
449
|
+
{
|
|
450
|
+
"version": 1,
|
|
451
|
+
"tools": "auto",
|
|
452
|
+
"tooling": {},
|
|
453
|
+
"completionGate": {
|
|
454
|
+
"enabled": true,
|
|
455
|
+
"maxIterations": 3,
|
|
456
|
+
"projectChecks": true,
|
|
457
|
+
"checks": ["lint", "typecheck", "test", "build"],
|
|
458
|
+
"requiredStages": ["architecture-policy", "project-checks", "runtime"]
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Supported stages are `runtime`, `project-checks`, `adapters`, `architecture-policy`,
|
|
464
|
+
`policy-surface`, `ci-integrity`, and `verification-scope`. The compatibility default requires `architecture-policy`;
|
|
465
|
+
projects opt into additional requirements. If a required stage is skipped or cannot be confirmed,
|
|
466
|
+
the result is `incomplete`, the CLI exits nonzero, and the receipt names the incomplete stages.
|
|
467
|
+
When base policy is used, `policy-surface` is required automatically. A configured CI integrity
|
|
468
|
+
contract makes `ci-integrity` required automatically. `verification-scope` is always required:
|
|
469
|
+
Guard hashes its discovered project inputs before checks and confirms that the scope stayed stable
|
|
470
|
+
through the run.
|
|
471
|
+
|
|
472
|
+
### GitHub Actions integrity contract
|
|
473
|
+
|
|
474
|
+
Declare the exact workflow and required job in the base revision's `rules.json`:
|
|
475
|
+
|
|
476
|
+
```json
|
|
477
|
+
{
|
|
478
|
+
"version": 1,
|
|
479
|
+
"rules": [],
|
|
480
|
+
"githubActions": {
|
|
481
|
+
"workflows": [
|
|
482
|
+
{
|
|
483
|
+
"path": ".github/workflows/guard.yml",
|
|
484
|
+
"job": "guard",
|
|
485
|
+
"requiredRunIncludes": [
|
|
486
|
+
"pnpm exec codapult-guard verify",
|
|
487
|
+
"--policy-base",
|
|
488
|
+
"--fail-on-policy-change",
|
|
489
|
+
"--fail-on-protected-changes"
|
|
490
|
+
],
|
|
491
|
+
"maxPermissions": { "contents": "read", "security-events": "write" }
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Guard parses the declared workflow and checks its pull request trigger, configured filters, required
|
|
499
|
+
job, required command fragments in the Guard invocation, conditional execution, `continue-on-error`,
|
|
500
|
+
declared job dependencies, explicit permission ceilings, and full commit SHA pins for external
|
|
501
|
+
actions. Local actions cannot meet immutable SHA pinning; if a workflow uses one, set
|
|
502
|
+
`requireFullShaActions` to `false` and list its source under `protectedPaths` in the base policy.
|
|
503
|
+
Guard compares each action ref with the contracted workflow at the base revision; additions,
|
|
504
|
+
removals, and SHA changes are reported as `policy-weakened` for separate review even when the new
|
|
505
|
+
ref is itself SHA-pinned. Missing protections are reported as `policy-weakened`; malformed or
|
|
506
|
+
unreadable workflow data is `incomplete`. The result and receipt include base/current workflow
|
|
507
|
+
digests, action refs and their changes, and issues. Guard does not
|
|
508
|
+
evaluate arbitrary GitHub expressions or prove that repository settings require the check; branch
|
|
509
|
+
protection or rulesets must still require the expected check source.
|
|
510
|
+
The audit rejects `pull_request_target` on a contracted verification workflow because Guard cannot
|
|
511
|
+
prove that it never executes untrusted PR content with that event's elevated context.
|
|
512
|
+
|
|
513
|
+
The receipt records the effective Guard invocation and each project check's command and arguments.
|
|
514
|
+
Credential-like values are redacted from command evidence; treat receipts as review artifacts, not
|
|
515
|
+
as a substitute for trusted CI logs.
|
|
516
|
+
|
|
391
517
|
To make policy changes fail this invocation explicitly, add:
|
|
392
518
|
|
|
393
519
|
```bash
|
|
@@ -418,6 +544,13 @@ The package repository separately runs its own unit, typecheck, build, and packa
|
|
|
418
544
|
[`.github/workflows/ci.yml`](.github/workflows/ci.yml), while the fixture workflow tests Guard on
|
|
419
545
|
real project shapes.
|
|
420
546
|
|
|
547
|
+
To add a separately signed Guard receipt, copy
|
|
548
|
+
[`docs/guard-trusted-pr-attestation.yml`](docs/guard-trusted-pr-attestation.yml) to
|
|
549
|
+
`.github/workflows/guard-trusted-pr-attestation.yml` and follow the
|
|
550
|
+
[trusted-attestation setup guide](docs/guard.md#install-the-trusted-github-actions-workflow).
|
|
551
|
+
This optional workflow re-runs Guard; it does not replace the required `Guard` check or attest
|
|
552
|
+
project test/build results.
|
|
553
|
+
|
|
421
554
|
## CLI surface
|
|
422
555
|
|
|
423
556
|
| Command | Purpose |
|
|
@@ -460,10 +593,17 @@ production tracing system or an identity/signature ledger.
|
|
|
460
593
|
|
|
461
594
|
The manifest also contains a `verification-receipt` object. It is a compact, machine-readable
|
|
462
595
|
record of the commit, policy base and fingerprint, decision IDs, check statuses, finding counts,
|
|
463
|
-
contract issues, waiver state, and Guard version.
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
596
|
+
contract issues, waiver state, and Guard version. Its `verificationScope` contains SHA-256 hashes
|
|
597
|
+
and paths for files discovered by Guard plus changed and deleted paths; the fingerprint binds that
|
|
598
|
+
inventory to the effective policy and Guard version. The `verification-scope` stage is incomplete
|
|
599
|
+
if Guard cannot hash the full scope or if the inputs changed during the run. The receipt excludes
|
|
600
|
+
source contents and command output. It describes what Guard decided; it is not a cryptographic
|
|
601
|
+
signature or proof of CI identity. When a policy base is supplied, it separates the base diff from
|
|
602
|
+
dirty working-tree files so local verification cannot silently present an incomplete change set.
|
|
603
|
+
For portable exchange, `verify --attestation-out <file>` exports a passing receipt as an unsigned
|
|
604
|
+
in-toto Statement. The format is independent of the CI platform and signing mechanism; a trusted CI
|
|
605
|
+
signer can sign it using the mechanism supported by that platform. The attestation guide includes a
|
|
606
|
+
GitHub Actions example using Sigstore. See the [attestation guide](docs/guard.md#exporting-and-signing-a-verification-attestation).
|
|
467
607
|
|
|
468
608
|
## Security and data handling
|
|
469
609
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { dirname, resolve } from 'node:path';
|
|
3
4
|
export class GuardAgentInstallError extends Error {
|
|
4
5
|
constructor(message) {
|
|
@@ -8,6 +9,21 @@ export class GuardAgentInstallError extends Error {
|
|
|
8
9
|
}
|
|
9
10
|
const START_MARKER = '<!-- codapult-guard:start -->';
|
|
10
11
|
const END_MARKER = '<!-- codapult-guard:end -->';
|
|
12
|
+
function writeInstructionFile(path, content) {
|
|
13
|
+
const temporaryPath = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
14
|
+
try {
|
|
15
|
+
writeFileSync(temporaryPath, content, { encoding: 'utf8', flag: 'wx' });
|
|
16
|
+
renameSync(temporaryPath, path);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
try {
|
|
20
|
+
unlinkSync(temporaryPath);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// The temporary file was renamed or could not be created.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
11
27
|
const instruction = `${START_MARKER}
|
|
12
28
|
## Codapult Guard completion gate
|
|
13
29
|
|
|
@@ -56,7 +72,14 @@ export function installGuardAgentInstructions(root, target) {
|
|
|
56
72
|
assertNoSymlinkPath(root, relativePath);
|
|
57
73
|
const path = resolve(root, relativePath);
|
|
58
74
|
const nextBlock = instruction;
|
|
59
|
-
|
|
75
|
+
let existing = '';
|
|
76
|
+
try {
|
|
77
|
+
existing = readFileSync(path, 'utf8');
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error.code !== 'ENOENT')
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
60
83
|
const start = existing.indexOf(START_MARKER);
|
|
61
84
|
const end = existing.indexOf(END_MARKER);
|
|
62
85
|
const content = start >= 0 && end >= start
|
|
@@ -65,7 +88,7 @@ export function installGuardAgentInstructions(root, target) {
|
|
|
65
88
|
? `${existing.trimEnd()}\n\n${renderContent(target)}`
|
|
66
89
|
: renderContent(target);
|
|
67
90
|
mkdirSync(dirname(path), { recursive: true });
|
|
68
|
-
|
|
91
|
+
writeInstructionFile(path, content);
|
|
69
92
|
return { target, path: relativePath, action: existing ? 'updated' : 'created' };
|
|
70
93
|
}
|
|
71
94
|
export function installGuardAgentTargets(root, targetsToInstall) {
|
|
@@ -19,6 +19,8 @@ interface GuardVerifyOptions extends GuardOutputOptions {
|
|
|
19
19
|
projectChecks?: boolean | undefined;
|
|
20
20
|
policyBase?: string | undefined;
|
|
21
21
|
failOnPolicyChange?: boolean | undefined;
|
|
22
|
+
failOnProtectedChanges?: boolean | undefined;
|
|
23
|
+
attestationOut?: string | undefined;
|
|
22
24
|
}
|
|
23
25
|
interface GuardGovernanceOptions extends GuardOutputOptions {
|
|
24
26
|
strict?: boolean | undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { relative, resolve } from 'node:path';
|
|
4
4
|
import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, GuardWaiverError, findGuardRoot, discoverProjectWithMetrics, clearDiscoveryCache, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, loadGuardWaivers, updateBaseline, updateGuardWaivers, loadGuardConfig, loadGuardProposals, applyGuardProposalDecision, writeGuardProposals, validateGuardProposalApproval, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, getGuardPolicyMutationPolicy, validateGuardPolicy, writeProjectState, } from '../../core/guard.js';
|
|
5
5
|
import { guardErrorPayload } from '../../core/errors.js';
|
|
@@ -12,6 +12,15 @@ import { guardFindingsToSarif } from '../../core/output/sarif.js';
|
|
|
12
12
|
import { analyzeProjectImpact } from '../../core/analysis/impact.js';
|
|
13
13
|
import { listGuardRuns, summarizeGuardRuns } from '../../core/history/runs.js';
|
|
14
14
|
import { auditGuardGovernance } from '../../core/audit/governance.js';
|
|
15
|
+
import { guardReceiptToInToto } from '../../core/output/attestation.js';
|
|
16
|
+
function terminalSafe(value) {
|
|
17
|
+
let output = '';
|
|
18
|
+
for (const character of value) {
|
|
19
|
+
const code = character.charCodeAt(0);
|
|
20
|
+
output += code <= 0x1f || (code >= 0x7f && code <= 0x9f) ? ' ' : character;
|
|
21
|
+
}
|
|
22
|
+
return output;
|
|
23
|
+
}
|
|
15
24
|
function renderFindings(findings) {
|
|
16
25
|
for (const finding of findings) {
|
|
17
26
|
const printer = finding.severity === 'error' ? fail : finding.severity === 'warning' ? warn : info;
|
|
@@ -49,7 +58,7 @@ function loadConfigSafely(root, options = {}) {
|
|
|
49
58
|
if (options.json || options.machine)
|
|
50
59
|
console.log(JSON.stringify(payload, null, 2));
|
|
51
60
|
else
|
|
52
|
-
fail(`${payload.message} Run \`codapult-guard doctor\` for details.`);
|
|
61
|
+
fail(`${terminalSafe(payload.message)} Run \`codapult-guard doctor\` for details.`);
|
|
53
62
|
process.exitCode = 1;
|
|
54
63
|
return { invalid: true };
|
|
55
64
|
}
|
|
@@ -217,7 +226,7 @@ export function guardRunsCommand(options = {}) {
|
|
|
217
226
|
return;
|
|
218
227
|
}
|
|
219
228
|
heading('Codapult Guard Runs');
|
|
220
|
-
info(`${result.summary.total} run(s); ${result.summary.failed} failed, ${result.summary.warnings} warning, average ${result.summary.averageDurationMs} ms`);
|
|
229
|
+
info(`${result.summary.total} run(s); ${result.summary.failed} failed, ${result.summary.incomplete} incomplete, ${result.summary.policyWeakened} policy-weakened, ${result.summary.warnings} warning, average ${result.summary.averageDurationMs} ms`);
|
|
221
230
|
for (const run of result.runs) {
|
|
222
231
|
info(`${run.runId}: ${run.outcome}, gate=${run.gate}, ${run.durationMs} ms`);
|
|
223
232
|
}
|
|
@@ -312,10 +321,26 @@ export function guardVerifyCommand(options = {}) {
|
|
|
312
321
|
projectChecks: options.projectChecks,
|
|
313
322
|
policyBase: options.policyBase,
|
|
314
323
|
failOnPolicyChange: options.failOnPolicyChange,
|
|
324
|
+
failOnProtectedChanges: options.failOnProtectedChanges,
|
|
315
325
|
});
|
|
326
|
+
let attestationFailed = false;
|
|
327
|
+
if (options.attestationOut) {
|
|
328
|
+
try {
|
|
329
|
+
if (!result.run.receipt)
|
|
330
|
+
throw new Error('Guard did not produce a verification receipt.');
|
|
331
|
+
const statement = guardReceiptToInToto(result.run.receipt);
|
|
332
|
+
writeFileSync(resolve(options.attestationOut), `${JSON.stringify(statement, null, 2)}\n`);
|
|
333
|
+
if (!options.json)
|
|
334
|
+
info(`in-toto verification statement written to ${options.attestationOut}`);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
attestationFailed = true;
|
|
338
|
+
fail(error instanceof Error ? error.message : 'Could not write the in-toto statement.');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
316
341
|
if (options.json) {
|
|
317
342
|
console.log(JSON.stringify(result, null, 2));
|
|
318
|
-
process.exitCode = result.status === 'ok' ? 0 : 1;
|
|
343
|
+
process.exitCode = result.status === 'ok' && !attestationFailed ? 0 : 1;
|
|
319
344
|
return;
|
|
320
345
|
}
|
|
321
346
|
heading('Codapult Guard Verify');
|
|
@@ -330,21 +355,62 @@ export function guardVerifyCommand(options = {}) {
|
|
|
330
355
|
warn('The working-tree policy differs from the base policy; the base policy was used.');
|
|
331
356
|
}
|
|
332
357
|
}
|
|
358
|
+
if (result.policySurface) {
|
|
359
|
+
dim(`protected paths changed: ${result.policySurface.changedPaths.length}`);
|
|
360
|
+
for (const path of result.policySurface.changedPaths)
|
|
361
|
+
warn(`verification-sensitive: ${terminalSafe(path)}`);
|
|
362
|
+
for (const change of result.policySurface.changes) {
|
|
363
|
+
dim(`policy surface: ${change.kind}/${change.severity} ${terminalSafe(change.path)} (${terminalSafe(change.pattern)})`);
|
|
364
|
+
}
|
|
365
|
+
for (const marker of result.policySurface.suppressionMarkers)
|
|
366
|
+
warn(`inline suppression added: ${terminalSafe(marker)}`);
|
|
367
|
+
if (result.policySurface.workflowChanged)
|
|
368
|
+
warn('Workflow changes require host-enforced required checks and protected review.');
|
|
369
|
+
}
|
|
370
|
+
if (result.ciIntegrity) {
|
|
371
|
+
if (result.ciIntegrity.status === 'ok')
|
|
372
|
+
success('CI integrity: passed');
|
|
373
|
+
else if (result.ciIntegrity.status === 'policy-weakened') {
|
|
374
|
+
fail('CI integrity: policy weakened');
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
warn('CI integrity: incomplete');
|
|
378
|
+
}
|
|
379
|
+
for (const workflow of result.ciIntegrity.workflows) {
|
|
380
|
+
for (const change of workflow.actionChanges ?? []) {
|
|
381
|
+
warn(`[${terminalSafe(workflow.path)}] action refs changed for ${terminalSafe(change.name)}: ${terminalSafe(change.base.join(', ') || '(none)')} → ${terminalSafe(change.current.join(', ') || '(none)')}`);
|
|
382
|
+
}
|
|
383
|
+
for (const issue of workflow.issues)
|
|
384
|
+
fail(`[${terminalSafe(workflow.path)}] ${terminalSafe(issue.message)}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const receipt = result.run.receipt;
|
|
388
|
+
if (receipt?.effectiveInvocation) {
|
|
389
|
+
dim(`effective invocation: ${terminalSafe(receipt.effectiveInvocation.command)} ${terminalSafe(receipt.effectiveInvocation.args.join(' '))}`);
|
|
390
|
+
}
|
|
391
|
+
if (result.incompleteStages && result.incompleteStages.length > 0) {
|
|
392
|
+
warn(`Required stages incomplete: ${result.incompleteStages.join(', ')}`);
|
|
393
|
+
}
|
|
333
394
|
for (const [name, check] of Object.entries(result.checks)) {
|
|
334
395
|
if (check.status === 'passed')
|
|
335
|
-
success(`${name}: passed`);
|
|
396
|
+
success(`${terminalSafe(name)}: passed`);
|
|
336
397
|
else if (check.status === 'not-configured')
|
|
337
|
-
dim(`${name}: not configured`);
|
|
398
|
+
dim(`${terminalSafe(name)}: not configured`);
|
|
338
399
|
else
|
|
339
|
-
fail(`${name}: failed (exit ${check.exitCode})`);
|
|
400
|
+
fail(`${terminalSafe(name)}: failed (exit ${check.exitCode})`);
|
|
340
401
|
}
|
|
341
402
|
for (const [name, check] of Object.entries(result.adapters ?? {})) {
|
|
342
403
|
if (check.status === 'passed')
|
|
343
|
-
success(`${name}: passed`);
|
|
404
|
+
success(`${terminalSafe(name)}: passed`);
|
|
344
405
|
else if (check.status === 'not-configured')
|
|
345
|
-
dim(`${name}: not configured`);
|
|
406
|
+
dim(`${terminalSafe(name)}: not configured`);
|
|
346
407
|
else
|
|
347
|
-
fail(`${name}: failed (exit ${check.exitCode})`);
|
|
408
|
+
fail(`${terminalSafe(name)}: failed (exit ${check.exitCode})`);
|
|
409
|
+
}
|
|
410
|
+
for (const [name, evidence] of Object.entries(receipt?.checks ?? {})) {
|
|
411
|
+
if (!evidence.command)
|
|
412
|
+
continue;
|
|
413
|
+
dim(`${terminalSafe(name)} command (${evidence.execution ?? 'unknown'}): ${terminalSafe(evidence.command)}${(evidence.args?.length ?? 0) > 0 ? ` ${terminalSafe(evidence.args?.join(' ') ?? '')}` : ''}`);
|
|
348
414
|
}
|
|
349
415
|
const findings = result.architecture?.findings ?? [];
|
|
350
416
|
if (findings.length === 0)
|
|
@@ -358,7 +424,7 @@ export function guardVerifyCommand(options = {}) {
|
|
|
358
424
|
if (result.contractIssues.length > 0)
|
|
359
425
|
fail(`contracts: ${result.contractIssues.length} invalid`);
|
|
360
426
|
dim(`requirements: ${result.requirement.message}`);
|
|
361
|
-
process.exitCode = result.status === 'ok' ? 0 : 1;
|
|
427
|
+
process.exitCode = result.status === 'ok' && !attestationFailed ? 0 : 1;
|
|
362
428
|
}
|
|
363
429
|
export function guardCheckCommand(options = {}) {
|
|
364
430
|
const root = getRoot();
|
package/dist/cli/index.js
CHANGED
|
@@ -44,6 +44,8 @@ guard
|
|
|
44
44
|
.option('--changed')
|
|
45
45
|
.option('--policy-base <ref>', 'read policy artifacts from this Git ref')
|
|
46
46
|
.option('--fail-on-policy-change', 'fail when the working-tree policy differs from the base ref')
|
|
47
|
+
.option('--fail-on-protected-changes', 'fail when protected verification paths change relative to base')
|
|
48
|
+
.option('--attestation-out <file>', 'write a passing verification as an in-toto statement')
|
|
47
49
|
.option('--json')
|
|
48
50
|
.action(guardVerifyCommand);
|
|
49
51
|
guard
|
package/dist/core/guard.d.ts
CHANGED
|
@@ -3,12 +3,13 @@ import { clearDiscoveryCache, discoverProject, discoverProjectWithMetrics, findG
|
|
|
3
3
|
import { type GuardImpactAnalysis } from './analysis/impact.js';
|
|
4
4
|
import type { GuardAdapterName, GuardApprovalMode, GuardContractKind, GuardBudgetMetric, GuardRuleKind, GuardRuleStatus, GuardSeverity, GuardToolMode, GuardWaiver, GuardWaiverPolicy } from './model/types.js';
|
|
5
5
|
export type { GuardAdapterName, GuardApprovalMode, GuardContractKind, GuardBudgetMetric, GuardRuleKind, GuardRuleStatus, GuardSeverity, GuardToolMode, GuardWaiver, GuardWaiverPolicy, GuardWaiverDecision, } from './model/types.js';
|
|
6
|
-
export type GuardOutcomeStatus = 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
|
|
6
|
+
export type GuardOutcomeStatus = 'pass' | 'fail' | 'incomplete' | 'policy-weakened' | 'warning' | 'needs-review' | 'not-configured';
|
|
7
7
|
export interface GuardOutcomeInput {
|
|
8
8
|
configured?: boolean | undefined;
|
|
9
9
|
errors?: number | undefined;
|
|
10
10
|
warnings?: number | undefined;
|
|
11
11
|
needsReview?: boolean | undefined;
|
|
12
|
+
incomplete?: boolean | undefined;
|
|
12
13
|
}
|
|
13
14
|
interface GuardStateLockOptions {
|
|
14
15
|
waitMs?: number | undefined;
|
|
@@ -86,6 +87,30 @@ export interface GuardConfig {
|
|
|
86
87
|
budgets?: GuardBudget[] | undefined;
|
|
87
88
|
approval?: GuardApprovalPolicy | undefined;
|
|
88
89
|
waiverPolicy?: GuardWaiverPolicy | undefined;
|
|
90
|
+
protectedPaths?: string[] | undefined;
|
|
91
|
+
policySurface?: GuardPolicySurfacePolicy | undefined;
|
|
92
|
+
githubActions?: GuardGitHubActionsPolicy | undefined;
|
|
93
|
+
}
|
|
94
|
+
export interface GuardPolicySurfacePolicy {
|
|
95
|
+
paths: {
|
|
96
|
+
pattern: string;
|
|
97
|
+
kind: 'ci-workflow' | 'guard-policy' | 'ownership' | 'dependency-input' | 'tool-config' | 'ignore-config' | 'suppression-source' | 'custom';
|
|
98
|
+
severity: 'warning' | 'error';
|
|
99
|
+
}[];
|
|
100
|
+
}
|
|
101
|
+
export interface GuardGitHubActionsPolicy {
|
|
102
|
+
workflows: {
|
|
103
|
+
path: string;
|
|
104
|
+
job: string;
|
|
105
|
+
requiredRunIncludes: string[];
|
|
106
|
+
requiredNeeds?: string[] | undefined;
|
|
107
|
+
requirePullRequest?: boolean | undefined;
|
|
108
|
+
requireUnfilteredPullRequest?: boolean | undefined;
|
|
109
|
+
requireUnconditionalJob?: boolean | undefined;
|
|
110
|
+
requireFullShaActions?: boolean | undefined;
|
|
111
|
+
disallowContinueOnError?: boolean | undefined;
|
|
112
|
+
maxPermissions?: Record<string, 'none' | 'read' | 'write'> | undefined;
|
|
113
|
+
}[];
|
|
89
114
|
}
|
|
90
115
|
export declare function getGuardPolicyMutationPolicy(guardConfig: GuardConfig | undefined): GuardPolicyMutationPolicy;
|
|
91
116
|
export interface GuardApprovalPolicy {
|
|
@@ -155,8 +180,10 @@ export interface GuardAgentConfig {
|
|
|
155
180
|
maxIterations: number;
|
|
156
181
|
projectChecks: boolean;
|
|
157
182
|
checks: ('lint' | 'typecheck' | 'test' | 'build')[];
|
|
183
|
+
requiredStages?: GuardRequiredStage[] | undefined;
|
|
158
184
|
};
|
|
159
185
|
}
|
|
186
|
+
export type GuardRequiredStage = 'runtime' | 'project-checks' | 'adapters' | 'architecture-policy' | 'policy-surface' | 'ci-integrity' | 'verification-scope';
|
|
160
187
|
export interface GuardFinding {
|
|
161
188
|
ruleId: string;
|
|
162
189
|
severity: GuardSeverity;
|
package/dist/core/guard.js
CHANGED
|
@@ -17,6 +17,8 @@ export function classifyGuardOutcome(input) {
|
|
|
17
17
|
return 'fail';
|
|
18
18
|
if (input.needsReview)
|
|
19
19
|
return 'needs-review';
|
|
20
|
+
if (input.incomplete)
|
|
21
|
+
return 'incomplete';
|
|
20
22
|
return (input.warnings ?? 0) > 0 ? 'warning' : 'pass';
|
|
21
23
|
}
|
|
22
24
|
export function getGuardPolicyMutationPolicy(guardConfig) {
|
|
@@ -129,6 +131,7 @@ export const defaultGuardAgentConfig = {
|
|
|
129
131
|
maxIterations: 3,
|
|
130
132
|
projectChecks: true,
|
|
131
133
|
checks: ['lint', 'typecheck', 'test', 'build'],
|
|
134
|
+
requiredStages: ['architecture-policy'],
|
|
132
135
|
},
|
|
133
136
|
};
|
|
134
137
|
export class GuardAlreadyInitializedError extends Error {
|
|
@@ -428,6 +431,40 @@ export function fingerprintGuardConfig(guardConfig) {
|
|
|
428
431
|
contracts: [...(guardConfig.contracts ?? [])].sort((left, right) => left.id.localeCompare(right.id)),
|
|
429
432
|
budgets: [...(guardConfig.budgets ?? [])].sort((left, right) => left.id.localeCompare(right.id)),
|
|
430
433
|
approval: guardConfig.approval ?? defaultGuardConfig.approval,
|
|
434
|
+
...(guardConfig.protectedPaths !== undefined && guardConfig.protectedPaths.length > 0
|
|
435
|
+
? { protectedPaths: [...guardConfig.protectedPaths].sort() }
|
|
436
|
+
: {}),
|
|
437
|
+
...(guardConfig.policySurface
|
|
438
|
+
? {
|
|
439
|
+
policySurface: {
|
|
440
|
+
paths: [...guardConfig.policySurface.paths]
|
|
441
|
+
.map((entry) => ({ ...entry, severity: entry.severity ?? 'error' }))
|
|
442
|
+
.sort((left, right) => `${left.pattern}:${left.kind}:${left.severity}`.localeCompare(`${right.pattern}:${right.kind}:${right.severity}`)),
|
|
443
|
+
},
|
|
444
|
+
}
|
|
445
|
+
: {}),
|
|
446
|
+
...(guardConfig.githubActions
|
|
447
|
+
? {
|
|
448
|
+
githubActions: {
|
|
449
|
+
workflows: guardConfig.githubActions.workflows
|
|
450
|
+
.map((workflow) => ({
|
|
451
|
+
...workflow,
|
|
452
|
+
requiredNeeds: workflow.requiredNeeds
|
|
453
|
+
? [...workflow.requiredNeeds].sort()
|
|
454
|
+
: undefined,
|
|
455
|
+
requirePullRequest: workflow.requirePullRequest ?? true,
|
|
456
|
+
requireUnfilteredPullRequest: workflow.requireUnfilteredPullRequest ?? true,
|
|
457
|
+
requireUnconditionalJob: workflow.requireUnconditionalJob ?? true,
|
|
458
|
+
requireFullShaActions: workflow.requireFullShaActions ?? true,
|
|
459
|
+
disallowContinueOnError: workflow.disallowContinueOnError ?? true,
|
|
460
|
+
maxPermissions: workflow.maxPermissions
|
|
461
|
+
? Object.fromEntries(Object.entries(workflow.maxPermissions).sort(([left], [right]) => left.localeCompare(right)))
|
|
462
|
+
: undefined,
|
|
463
|
+
}))
|
|
464
|
+
.sort((left, right) => `${left.path}:${left.job}`.localeCompare(`${right.path}:${right.job}`)),
|
|
465
|
+
},
|
|
466
|
+
}
|
|
467
|
+
: {}),
|
|
431
468
|
}))
|
|
432
469
|
.digest('hex');
|
|
433
470
|
}
|
|
@@ -17,6 +17,54 @@ export interface GuardVerificationReceiptCheck {
|
|
|
17
17
|
status: 'passed' | 'failed' | 'not-configured';
|
|
18
18
|
exitCode: number;
|
|
19
19
|
durationMs: number;
|
|
20
|
+
command?: string | undefined;
|
|
21
|
+
args?: string[] | undefined;
|
|
22
|
+
execution?: 'attempted' | 'rejected' | 'not-configured' | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface GuardVerificationReceiptPolicySurface {
|
|
25
|
+
protectedPaths: string[];
|
|
26
|
+
changedPaths: string[];
|
|
27
|
+
changes?: {
|
|
28
|
+
path: string;
|
|
29
|
+
pattern: string;
|
|
30
|
+
kind: string;
|
|
31
|
+
severity: 'warning' | 'error';
|
|
32
|
+
}[];
|
|
33
|
+
suppressionMarkers: string[];
|
|
34
|
+
workflowChanged: boolean;
|
|
35
|
+
complete: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface GuardVerificationScope {
|
|
38
|
+
algorithm: 'sha256';
|
|
39
|
+
fingerprint: string;
|
|
40
|
+
complete: boolean;
|
|
41
|
+
stable: boolean;
|
|
42
|
+
files: {
|
|
43
|
+
path: string;
|
|
44
|
+
status: 'present' | 'deleted' | 'unavailable';
|
|
45
|
+
sha256?: string | undefined;
|
|
46
|
+
}[];
|
|
47
|
+
}
|
|
48
|
+
export interface GuardVerificationCiIntegrity {
|
|
49
|
+
status: 'ok' | 'policy-weakened' | 'incomplete';
|
|
50
|
+
workflows: {
|
|
51
|
+
path: string;
|
|
52
|
+
job: string;
|
|
53
|
+
digest?: string | undefined;
|
|
54
|
+
baseDigest?: string | undefined;
|
|
55
|
+
actions: string[];
|
|
56
|
+
baseActions?: string[] | undefined;
|
|
57
|
+
actionChanges?: {
|
|
58
|
+
name: string;
|
|
59
|
+
base: string[];
|
|
60
|
+
current: string[];
|
|
61
|
+
}[] | undefined;
|
|
62
|
+
status: 'ok' | 'policy-weakened' | 'incomplete';
|
|
63
|
+
issues: {
|
|
64
|
+
code: string;
|
|
65
|
+
message: string;
|
|
66
|
+
}[];
|
|
67
|
+
}[];
|
|
20
68
|
}
|
|
21
69
|
export interface GuardVerificationReceipt {
|
|
22
70
|
version: 1;
|
|
@@ -29,12 +77,25 @@ export interface GuardVerificationReceipt {
|
|
|
29
77
|
commit?: string | undefined;
|
|
30
78
|
baseRef?: string | undefined;
|
|
31
79
|
baseCommit?: string | undefined;
|
|
80
|
+
headSha?: string | undefined;
|
|
81
|
+
baseSha?: string | undefined;
|
|
82
|
+
mergeSha?: string | undefined;
|
|
83
|
+
githubEvent?: string | undefined;
|
|
32
84
|
changedFiles: string[];
|
|
33
85
|
baseChangedFiles?: string[] | undefined;
|
|
34
86
|
workingTreeFiles?: string[] | undefined;
|
|
35
87
|
workingTreeDirty: boolean;
|
|
36
88
|
};
|
|
37
89
|
policy?: GuardRunPolicy | undefined;
|
|
90
|
+
verificationScope?: GuardVerificationScope | undefined;
|
|
91
|
+
policySurface?: GuardVerificationReceiptPolicySurface | undefined;
|
|
92
|
+
ciIntegrity?: GuardVerificationCiIntegrity | undefined;
|
|
93
|
+
requiredStages?: string[] | undefined;
|
|
94
|
+
incompleteStages?: string[] | undefined;
|
|
95
|
+
effectiveInvocation?: {
|
|
96
|
+
command: string;
|
|
97
|
+
args: string[];
|
|
98
|
+
} | undefined;
|
|
38
99
|
stages: Record<string, 'ok' | 'fail' | 'skipped'>;
|
|
39
100
|
checks: Record<string, GuardVerificationReceiptCheck>;
|
|
40
101
|
findings: {
|
|
@@ -61,7 +122,7 @@ export interface GuardRunManifest {
|
|
|
61
122
|
startedAt: string;
|
|
62
123
|
completedAt: string;
|
|
63
124
|
durationMs: number;
|
|
64
|
-
outcome: 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
|
|
125
|
+
outcome: 'pass' | 'fail' | 'incomplete' | 'policy-weakened' | 'warning' | 'needs-review' | 'not-configured';
|
|
65
126
|
gate: string;
|
|
66
127
|
stages: Record<string, GuardRunStage>;
|
|
67
128
|
commit?: string | undefined;
|
|
@@ -78,6 +139,8 @@ export interface GuardRunSummary {
|
|
|
78
139
|
total: number;
|
|
79
140
|
passed: number;
|
|
80
141
|
failed: number;
|
|
142
|
+
incomplete: number;
|
|
143
|
+
policyWeakened: number;
|
|
81
144
|
warnings: number;
|
|
82
145
|
averageDurationMs: number;
|
|
83
146
|
last?: GuardRunManifest | undefined;
|