@intentius/chant 0.124.32 → 0.124.34
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/ci-jobs.d.ts +4 -0
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +9 -2
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +18 -3
- package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
- package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-health.js +150 -0
- package/dist/cli/handlers/lifecycle.d.ts +26 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.js +1 -1
- package/dist/cli/handlers/pr.d.ts +2 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +124 -2
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +277 -74
- package/dist/cli/handlers/release.d.ts +16 -1
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +83 -11
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +99 -22
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +30 -1
- package/dist/cli/registry.d.ts +21 -5
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/deploy-inputs.d.ts +91 -0
- package/dist/components/deploy-inputs.d.ts.map +1 -0
- package/dist/components/deploy-inputs.js +184 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +9 -0
- package/dist/components/gate-plan.d.ts +8 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +6 -0
- package/dist/components/pr-pipeline.d.ts +33 -10
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +42 -11
- package/dist/components/pr-run.d.ts +1 -1
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/promote-rounds.d.ts +102 -0
- package/dist/components/promote-rounds.d.ts.map +1 -0
- package/dist/components/promote-rounds.js +174 -0
- package/dist/components/release-compile.d.ts +37 -2
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +64 -8
- package/dist/components/release-gate.d.ts +30 -0
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +92 -42
- package/dist/components/release-promote.d.ts +17 -3
- package/dist/components/release-promote.d.ts.map +1 -1
- package/dist/components/release-promote.js +79 -18
- package/dist/components/verbs/bom-writer.d.ts +1 -1
- package/dist/components/verbs/bom-writer.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
- package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
- package/dist/components/wave-plan.d.ts +10 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +32 -4
- package/dist/graph-declared.d.ts +4 -0
- package/dist/graph-declared.d.ts.map +1 -1
- package/dist/graph-declared.js +11 -1
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +32 -7
- package/dist/health.d.ts +110 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +105 -0
- package/dist/health.schema.json +191 -0
- package/dist/lexicon.d.ts +27 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/merge-record.d.ts +104 -0
- package/dist/lifecycle/merge-record.d.ts.map +1 -0
- package/dist/lifecycle/merge-record.js +102 -0
- package/dist/lifecycle/publish-attachments.d.ts +8 -5
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
- package/dist/lifecycle/publish-attachments.js +27 -13
- package/dist/lifecycle/publish-npm.d.ts +3 -2
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +4 -3
- package/dist/lifecycle/release-at.d.ts +40 -0
- package/dist/lifecycle/release-at.d.ts.map +1 -0
- package/dist/lifecycle/release-at.js +83 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +23 -12
- package/dist/lifecycle/release-publish.d.ts +28 -12
- package/dist/lifecycle/release-publish.d.ts.map +1 -1
- package/dist/lifecycle/release-publish.js +29 -17
- package/dist/lifecycle/release-store.d.ts +25 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +40 -13
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +1 -0
- package/dist/lint/rules/op/index.d.ts +2 -1
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.js +4 -2
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
- package/dist/op/activities/activity-contracts.d.ts +5 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +3 -0
- package/dist/op/activities/shell.d.ts +38 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +89 -3
- package/dist/op/builders.d.ts +7 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +7 -0
- package/dist/op/runtime.d.ts +2 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/pr-loop.d.ts +14 -3
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +15 -5
- package/dist/pr-report-url.d.ts +2 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/runner-start.d.ts +179 -0
- package/dist/runner-start.d.ts.map +1 -0
- package/dist/runner-start.js +277 -0
- package/dist/wave-runtime.mjs +236 -229
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +13 -2
- package/dist/workspace/check.schema.json +88 -2
- package/dist/workspace/checks/ci-workflow.d.ts +17 -0
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/checks/ci-workflow.js +123 -0
- package/dist/workspace/checks/security.d.ts +50 -0
- package/dist/workspace/checks/security.d.ts.map +1 -0
- package/dist/workspace/checks/security.js +305 -0
- package/dist/workspace/checks.d.ts +2 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +8 -0
- package/dist/workspace/ci-builds.d.ts +163 -0
- package/dist/workspace/ci-builds.d.ts.map +1 -0
- package/dist/workspace/ci-builds.js +315 -0
- package/dist/workspace/ci-green-cli.d.ts +2 -1
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +25 -5
- package/dist/workspace/ci-green-forge.d.ts +12 -0
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +12 -0
- package/dist/workspace/ci-green-workflow.d.ts +55 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +128 -1
- package/dist/workspace/ci-green.d.ts +89 -4
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +310 -23
- package/dist/workspace/ci-init.d.ts +100 -0
- package/dist/workspace/ci-init.d.ts.map +1 -0
- package/dist/workspace/ci-init.js +434 -0
- package/dist/workspace/ci-last-green.schema.json +49 -0
- package/dist/workspace/ci-list.schema.json +35 -1
- package/dist/workspace/ci-run-index.d.ts +1 -1
- package/dist/workspace/ci-run-index.d.ts.map +1 -1
- package/dist/workspace/ci-run-index.schema.json +1 -1
- package/dist/workspace/ci-run.d.ts +12 -6
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +72 -16
- package/dist/workspace/ci-runner-start.d.ts +77 -0
- package/dist/workspace/ci-runner-start.d.ts.map +1 -0
- package/dist/workspace/ci-runner-start.js +185 -0
- package/dist/workspace/ci-serve.d.ts +76 -7
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +153 -19
- package/dist/workspace/ci-workflow.d.ts +91 -0
- package/dist/workspace/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/ci-workflow.js +33 -0
- package/dist/workspace/declaration.d.ts +35 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +12 -2
- package/dist/workspace/declaration.schema.json +51 -0
- package/dist/workspace/init.js +2 -2
- package/dist/workspace/ls.d.ts +2 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -1
- package/dist/workspace/ls.schema.json +2 -1
- package/dist/workspace/member-pipeline.d.ts +1 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +2 -0
- package/dist/workspace/merge-record.schema.json +49 -0
- package/dist/workspace/pr-report.schema.json +3 -3
- package/dist/workspace/reason-codes.d.ts +26 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +28 -0
- package/dist/workspace/release-candidates.d.ts +129 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -0
- package/dist/workspace/release-candidates.js +233 -0
- package/dist/workspace/release-candidates.schema.json +353 -0
- package/dist/workspace/release-workflow.d.ts +31 -0
- package/dist/workspace/release-workflow.d.ts.map +1 -0
- package/dist/workspace/release-workflow.js +150 -0
- package/dist/workspace/security.d.ts +40 -0
- package/dist/workspace/security.d.ts.map +1 -0
- package/dist/workspace/security.js +99 -0
- package/package.json +8 -1
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
- package/src/ci-jobs.ts +11 -2
- package/src/cli/handlers/build.test.ts +36 -0
- package/src/cli/handlers/build.ts +18 -3
- package/src/cli/handlers/lifecycle-health.test.ts +123 -0
- package/src/cli/handlers/lifecycle-health.ts +157 -0
- package/src/cli/handlers/lifecycle.ts +1 -1
- package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
- package/src/cli/handlers/pr-record.test.ts +201 -0
- package/src/cli/handlers/pr.test.ts +22 -0
- package/src/cli/handlers/pr.ts +127 -4
- package/src/cli/handlers/promote.test.ts +17 -0
- package/src/cli/handlers/promote.ts +296 -84
- package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
- package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
- package/src/cli/handlers/release.e2e.test.ts +19 -28
- package/src/cli/handlers/release.ts +81 -11
- package/src/cli/main.ts +99 -22
- package/src/cli/parse-args.ts +22 -1
- package/src/cli/registry.ts +21 -5
- package/src/components/deploy-inputs.test.ts +108 -0
- package/src/components/deploy-inputs.ts +225 -0
- package/src/components/driver.ts +11 -0
- package/src/components/gate-plan.ts +9 -0
- package/src/components/pr-pipeline.ts +56 -15
- package/src/components/pr-run.ts +1 -1
- package/src/components/promote-rounds.test.ts +220 -0
- package/src/components/promote-rounds.ts +232 -0
- package/src/components/release-compile.test.ts +34 -0
- package/src/components/release-compile.ts +89 -8
- package/src/components/release-gate.test.ts +47 -0
- package/src/components/release-gate.ts +111 -46
- package/src/components/release-promote.test.ts +55 -2
- package/src/components/release-promote.ts +93 -23
- package/src/components/verbs/bom-writer.ts +1 -1
- package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
- package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
- package/src/components/wave-plan.ts +32 -4
- package/src/graph-declared-exports.test.ts +40 -0
- package/src/graph-declared.ts +9 -1
- package/src/graph-detail.test.ts +57 -0
- package/src/graph-detail.ts +33 -7
- package/src/health.schema.json +191 -0
- package/src/health.test.ts +62 -0
- package/src/health.ts +178 -0
- package/src/lexicon.ts +41 -1
- package/src/lifecycle/merge-record.test.ts +111 -0
- package/src/lifecycle/merge-record.ts +154 -0
- package/src/lifecycle/publish-attachments.ts +25 -13
- package/src/lifecycle/publish-npm.ts +4 -3
- package/src/lifecycle/publish-targets.test.ts +34 -4
- package/src/lifecycle/release-at.test.ts +48 -0
- package/src/lifecycle/release-at.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +41 -0
- package/src/lifecycle/release-cut.ts +22 -11
- package/src/lifecycle/release-publish.test.ts +43 -13
- package/src/lifecycle/release-publish.ts +38 -18
- package/src/lifecycle/release-store.ts +57 -13
- package/src/lifecycle/run-ledger.test.ts +13 -0
- package/src/lifecycle/run-ledger.ts +1 -0
- package/src/lint/rules/op/index.ts +4 -2
- package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
- package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
- package/src/op/activities/activity-contracts.ts +3 -0
- package/src/op/activities/shell-container.e2e.test.ts +67 -0
- package/src/op/activities/shell.test.ts +44 -1
- package/src/op/activities/shell.ts +110 -3
- package/src/op/builders.ts +7 -0
- package/src/op/runtime.ts +2 -0
- package/src/op/step-console.test.ts +4 -2
- package/src/pr-loop.test.ts +29 -0
- package/src/pr-loop.ts +26 -7
- package/src/pr-report-url.ts +2 -2
- package/src/runner-start-bundle.test.ts +29 -0
- package/src/runner-start.test.ts +179 -0
- package/src/runner-start.ts +329 -0
- package/src/workspace/__fixtures__/contract-repo.ts +11 -2
- package/src/workspace/check-contract.test.ts +4 -2
- package/src/workspace/check.schema.json +88 -2
- package/src/workspace/checks/ci-workflow.test.ts +150 -0
- package/src/workspace/checks/ci-workflow.ts +129 -0
- package/src/workspace/checks/security.test.ts +138 -0
- package/src/workspace/checks/security.ts +305 -0
- package/src/workspace/checks.ts +8 -0
- package/src/workspace/ci-builds.e2e.test.ts +208 -0
- package/src/workspace/ci-builds.test.ts +174 -0
- package/src/workspace/ci-builds.ts +385 -0
- package/src/workspace/ci-green-builds.test.ts +180 -0
- package/src/workspace/ci-green-cli.test.ts +54 -0
- package/src/workspace/ci-green-cli.ts +26 -5
- package/src/workspace/ci-green-forge.ts +24 -0
- package/src/workspace/ci-green-workflow.test.ts +84 -1
- package/src/workspace/ci-green-workflow.ts +139 -2
- package/src/workspace/ci-green.test.ts +5 -2
- package/src/workspace/ci-green.ts +359 -27
- package/src/workspace/ci-init.test.ts +159 -0
- package/src/workspace/ci-init.ts +445 -0
- package/src/workspace/ci-last-green.schema.json +49 -0
- package/src/workspace/ci-list.schema.json +35 -1
- package/src/workspace/ci-report-url.test.ts +6 -1
- package/src/workspace/ci-run-index.schema.json +1 -1
- package/src/workspace/ci-run-index.ts +1 -1
- package/src/workspace/ci-run.test.ts +24 -7
- package/src/workspace/ci-run.ts +78 -16
- package/src/workspace/ci-runner-start.test.ts +151 -0
- package/src/workspace/ci-runner-start.ts +193 -0
- package/src/workspace/ci-serve-builds.test.ts +86 -0
- package/src/workspace/ci-serve.test.ts +59 -0
- package/src/workspace/ci-serve.ts +177 -22
- package/src/workspace/ci-workflow.ts +71 -0
- package/src/workspace/declaration.schema.json +51 -0
- package/src/workspace/declaration.test.ts +17 -0
- package/src/workspace/declaration.ts +53 -3
- package/src/workspace/init.ts +2 -2
- package/src/workspace/ls.schema.json +2 -1
- package/src/workspace/ls.ts +3 -2
- package/src/workspace/member-pipeline.ts +2 -1
- package/src/workspace/merge-record.schema.json +49 -0
- package/src/workspace/pr-report.schema.json +3 -3
- package/src/workspace/reason-codes.test.ts +4 -0
- package/src/workspace/reason-codes.ts +28 -0
- package/src/workspace/release-candidates.schema.json +353 -0
- package/src/workspace/release-candidates.test.ts +106 -0
- package/src/workspace/release-candidates.ts +315 -0
- package/src/workspace/release-workflow.ts +167 -0
- package/src/workspace/security.test.ts +70 -0
- package/src/workspace/security.ts +118 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner-start statement (#4147): a provisioner's signed word that it
|
|
3
|
+
* started a runner for one CI job, which the job checks before it runs.
|
|
4
|
+
*
|
|
5
|
+
* A persistent self-hosted runner keeps files and processes between jobs,
|
|
6
|
+
* so a plan job that ran a pull request's code can leave something behind
|
|
7
|
+
* for the next apply. An apply job that requires this statement refuses to
|
|
8
|
+
* run, before it trades for any credentials, on a runner that no trusted
|
|
9
|
+
* provisioner says it started for this job.
|
|
10
|
+
*
|
|
11
|
+
* It differs from the Op-run statement (#4044, ./workspace/trust/op-run-statement.ts)
|
|
12
|
+
* in three ways. It is signed before the job runs, not when a run settles.
|
|
13
|
+
* The provisioner that starts the runner signs it, so the key never enters
|
|
14
|
+
* the job. And it names the CI job, run and attempt, so a statement issued
|
|
15
|
+
* for one job does not verify in another.
|
|
16
|
+
*
|
|
17
|
+
* The statement is an in-toto Statement v1 in a DSSE envelope
|
|
18
|
+
* (./workspace/trust/dsse.ts), signed with an Ed25519 key that
|
|
19
|
+
* `.chant/trust.json` lists under `runners`, whose principal holds the
|
|
20
|
+
* `runner` role there, the same keys and role a ci tag needs (#4031):
|
|
21
|
+
*
|
|
22
|
+
* - `subject`: the commit the job runs, by `digest.gitCommit`.
|
|
23
|
+
* - `predicate`: the signer's principal; `ci` with the system (`github`,
|
|
24
|
+
* `gitlab`, `forgejo`), the repository, and the run, attempt and job ids;
|
|
25
|
+
* `runner` with the runner's name and `singleJob: true`; and `issuedAt`.
|
|
26
|
+
*
|
|
27
|
+
* The job finds it in `CHANT_RUNNER_START` (the envelope's JSON) or in the
|
|
28
|
+
* file `CHANT_RUNNER_START_FILE` names, else in
|
|
29
|
+
* {@link DEFAULT_RUNNER_START_FILE}. The job's own ids come from the forge's
|
|
30
|
+
* variables ({@link ciJobFromEnv}).
|
|
31
|
+
*
|
|
32
|
+
* This module needs only `node:crypto` and `node:fs`, so a bundled stage can
|
|
33
|
+
* import it alone:
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { verifyRunnerStartForJob } from "@intentius/chant/runner-start";
|
|
37
|
+
*
|
|
38
|
+
* const trust = JSON.parse(readFileSync(".chant/trust.json", "utf8"));
|
|
39
|
+
* const v = verifyRunnerStartForJob({ runners: trust.runners ?? [], roles: trust.roles ?? {} });
|
|
40
|
+
* if (v.status !== "verified") throw new Error(v.reason);
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* `chant ci runner-start verify` and `chant ci run --require-runner-start`
|
|
44
|
+
* read the runners and roles from `.chant/trust.json` at base, which also
|
|
45
|
+
* drops a runner entry the signers file lists (./workspace/trust/policy.ts).
|
|
46
|
+
* A caller that passes trust.json's lists itself skips that check.
|
|
47
|
+
*/
|
|
48
|
+
import type { KeyObject } from "node:crypto";
|
|
49
|
+
import { type DsseEnvelope } from "./workspace/trust/dsse.js";
|
|
50
|
+
export declare const RUNNER_START_PREDICATE = "https://intentius.io/chant/runner-start/v1";
|
|
51
|
+
/** The in-toto Statement v1 type, as ./workspace/trust/evidence.ts has it. */
|
|
52
|
+
declare const STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
53
|
+
/** The role a runner-start statement's signer must hold in `.chant/trust.json`, as for a ci tag (#4031). */
|
|
54
|
+
export declare const RUNNER_START_SIGNER_ROLE = "runner";
|
|
55
|
+
/** The variable that holds the statement's envelope as JSON. */
|
|
56
|
+
export declare const RUNNER_START_ENV = "CHANT_RUNNER_START";
|
|
57
|
+
/** The variable that names a file holding the envelope, in place of {@link DEFAULT_RUNNER_START_FILE}. */
|
|
58
|
+
export declare const RUNNER_START_FILE_ENV = "CHANT_RUNNER_START_FILE";
|
|
59
|
+
/** Where the job looks for the envelope when neither variable is set. */
|
|
60
|
+
export declare const DEFAULT_RUNNER_START_FILE = "/run/chant/runner-start.json";
|
|
61
|
+
export type CiSystem = "github" | "gitlab" | "forgejo";
|
|
62
|
+
/** The CI job a runner was started for, or the job reading the statement. */
|
|
63
|
+
export interface CiJobIdentity {
|
|
64
|
+
system: CiSystem;
|
|
65
|
+
/** `owner/name` on GitHub and Forgejo, the project path on GitLab. */
|
|
66
|
+
repo: string;
|
|
67
|
+
/** The run: `GITHUB_RUN_ID`, or `CI_PIPELINE_ID` on GitLab. */
|
|
68
|
+
run: string;
|
|
69
|
+
/** The run's attempt: `GITHUB_RUN_ATTEMPT`. GitLab gives a retried job a new job id, so it is always `1` there. */
|
|
70
|
+
attempt: string;
|
|
71
|
+
/** The job: `GITHUB_JOB`, the job's key in the workflow, or `CI_JOB_ID` on GitLab. */
|
|
72
|
+
job: string;
|
|
73
|
+
/** The full commit id the job runs: `GITHUB_SHA`, or `CI_COMMIT_SHA`. */
|
|
74
|
+
commit: string;
|
|
75
|
+
/**
|
|
76
|
+
* The runner's name: `RUNNER_NAME` on GitHub and Forgejo. Required there,
|
|
77
|
+
* since `GITHUB_JOB` repeats across a matrix's jobs and the runner's name
|
|
78
|
+
* tells them apart. Not compared on GitLab, where `CI_JOB_ID` is unique.
|
|
79
|
+
*/
|
|
80
|
+
runner?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface RunnerStartStatement {
|
|
83
|
+
_type: typeof STATEMENT_TYPE;
|
|
84
|
+
subject: [{
|
|
85
|
+
name: string;
|
|
86
|
+
digest: {
|
|
87
|
+
gitCommit: string;
|
|
88
|
+
};
|
|
89
|
+
}];
|
|
90
|
+
predicateType: typeof RUNNER_START_PREDICATE;
|
|
91
|
+
predicate: {
|
|
92
|
+
signer: string;
|
|
93
|
+
ci: {
|
|
94
|
+
system: CiSystem;
|
|
95
|
+
repo: string;
|
|
96
|
+
run: string;
|
|
97
|
+
attempt: string;
|
|
98
|
+
job: string;
|
|
99
|
+
};
|
|
100
|
+
runner: {
|
|
101
|
+
name?: string;
|
|
102
|
+
singleJob: true;
|
|
103
|
+
};
|
|
104
|
+
issuedAt: string;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** A key that may sign the statement, as `.chant/trust.json` lists it under `runners`. */
|
|
108
|
+
export interface RunnerStartKey {
|
|
109
|
+
principal: string;
|
|
110
|
+
/** `ssh-ed25519 <base64>`. */
|
|
111
|
+
key: string;
|
|
112
|
+
}
|
|
113
|
+
/** Whoever signs: the provisioner's key and the principal `.chant/trust.json` lists it for. */
|
|
114
|
+
export interface RunnerStartSigner {
|
|
115
|
+
principal: string;
|
|
116
|
+
key: KeyObject;
|
|
117
|
+
publicKey: string;
|
|
118
|
+
}
|
|
119
|
+
/** The job the forge's variables name, or why they name none. GitHub's names are also Forgejo's; `FORGEJO_ACTIONS` or `GITEA_ACTIONS` marks Forgejo, as in ./notify.ts. */
|
|
120
|
+
export declare function ciJobFromEnv(env?: Record<string, string | undefined>): {
|
|
121
|
+
ok: true;
|
|
122
|
+
job: CiJobIdentity;
|
|
123
|
+
} | {
|
|
124
|
+
ok: false;
|
|
125
|
+
reason: string;
|
|
126
|
+
};
|
|
127
|
+
/** The statement `signer` signs for a runner started for `job`. Throws for a job it cannot name. */
|
|
128
|
+
export declare function buildRunnerStartStatement(job: CiJobIdentity, signer: string, issuedAt?: Date): RunnerStartStatement;
|
|
129
|
+
/** The envelope the provisioner hands the runner, signed with its key. */
|
|
130
|
+
export declare function signRunnerStart(job: CiJobIdentity, signer: RunnerStartSigner, issuedAt?: Date): DsseEnvelope;
|
|
131
|
+
export type RunnerStartVerdict = {
|
|
132
|
+
status: "verified";
|
|
133
|
+
signer: string;
|
|
134
|
+
keyid: string;
|
|
135
|
+
issuedAt: string;
|
|
136
|
+
reason: string;
|
|
137
|
+
} | {
|
|
138
|
+
status: "missing" | "invalid" | "untrusted" | "mismatch" | "stale";
|
|
139
|
+
reason: string;
|
|
140
|
+
};
|
|
141
|
+
export interface VerifyRunnerStartInput {
|
|
142
|
+
/** The envelope, as JSON text or parsed. Undefined when the job found none. */
|
|
143
|
+
envelope: unknown;
|
|
144
|
+
/** The job reading it. */
|
|
145
|
+
job: CiJobIdentity;
|
|
146
|
+
/** `runners` from `.chant/trust.json` at base. */
|
|
147
|
+
runners: readonly RunnerStartKey[];
|
|
148
|
+
/** `roles` from `.chant/trust.json` at base. */
|
|
149
|
+
roles: Readonly<Record<string, readonly string[]>>;
|
|
150
|
+
/** Refuse a statement issued longer ago than this. Default: no limit, since a statement names one job attempt. */
|
|
151
|
+
maxAgeSeconds?: number;
|
|
152
|
+
now?: Date;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Verify a runner-start statement for `job`: a signature by a key listed
|
|
156
|
+
* under `runners` whose principal holds the `runner` role, over a statement
|
|
157
|
+
* for this system, repository, run, attempt, job, commit and, on GitHub and
|
|
158
|
+
* Forgejo, runner. Never throws.
|
|
159
|
+
*/
|
|
160
|
+
export declare function verifyRunnerStart(input: VerifyRunnerStartInput): RunnerStartVerdict;
|
|
161
|
+
/** The envelope's JSON the job was given: `CHANT_RUNNER_START`, else the file `CHANT_RUNNER_START_FILE` names, else {@link DEFAULT_RUNNER_START_FILE}. Undefined when there is none. */
|
|
162
|
+
export declare function readRunnerStart(env?: Record<string, string | undefined>, readFile?: (path: string) => string): string | undefined;
|
|
163
|
+
/**
|
|
164
|
+
* One call for a job: find its statement ({@link readRunnerStart}), name the
|
|
165
|
+
* job from the forge's variables ({@link ciJobFromEnv}) and verify one
|
|
166
|
+
* against the other ({@link verifyRunnerStart}). Never throws.
|
|
167
|
+
*/
|
|
168
|
+
export declare function verifyRunnerStartForJob(input: {
|
|
169
|
+
runners: readonly RunnerStartKey[];
|
|
170
|
+
roles: Readonly<Record<string, readonly string[]>>;
|
|
171
|
+
env?: Record<string, string | undefined>;
|
|
172
|
+
/** The commit the job runs, in place of the forge's variable. */
|
|
173
|
+
commit?: string;
|
|
174
|
+
readFile?: (path: string) => string;
|
|
175
|
+
maxAgeSeconds?: number;
|
|
176
|
+
now?: Date;
|
|
177
|
+
}): RunnerStartVerdict;
|
|
178
|
+
export {};
|
|
179
|
+
//# sourceMappingURL=runner-start.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner-start.d.ts","sourceRoot":"","sources":["../src/runner-start.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAsD,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE/G,eAAO,MAAM,sBAAsB,+CAA+C,CAAC;AAEnF,8EAA8E;AAC9E,QAAA,MAAM,cAAc,oCAAoC,CAAC;AAEzD,4GAA4G;AAC5G,eAAO,MAAM,wBAAwB,WAAW,CAAC;AAEjD,gEAAgE;AAChE,eAAO,MAAM,gBAAgB,uBAAuB,CAAC;AAErD,0GAA0G;AAC1G,eAAO,MAAM,qBAAqB,4BAA4B,CAAC;AAE/D,yEAAyE;AACzE,eAAO,MAAM,yBAAyB,iCAAiC,CAAC;AAExE,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEvD,6EAA6E;AAC7E,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,QAAQ,CAAC;IACjB,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAC;IACZ,mHAAmH;IACnH,OAAO,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,OAAO,cAAc,CAAC;IAC7B,OAAO,EAAE,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE;YAAE,SAAS,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;IAC3D,aAAa,EAAE,OAAO,sBAAsB,CAAC;IAC7C,SAAS,EAAE;QACT,MAAM,EAAE,MAAM,CAAC;QACf,EAAE,EAAE;YAAE,MAAM,EAAE,QAAQ,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;QAClF,MAAM,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,IAAI,CAAA;SAAE,CAAC;QAC3C,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,0FAA0F;AAC1F,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,+FAA+F;AAC/F,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,SAAS,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAKD,2KAA2K;AAC3K,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CA4BpJ;AAaD,oGAAoG;AACpG,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,IAAiB,GAAG,oBAAoB,CAc/H;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ,GAAE,IAAiB,GAAG,YAAY,CAGxH;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvF;IAAE,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AA+B3F,MAAM,WAAW,sBAAsB;IACrC,+EAA+E;IAC/E,QAAQ,EAAE,OAAO,CAAC;IAClB,0BAA0B;IAC1B,GAAG,EAAE,aAAa,CAAC;IACnB,kDAAkD;IAClD,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC,gDAAgD;IAChD,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IACnD,kHAAkH;IAClH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,kBAAkB,CAiDnF;AAED,wLAAwL;AACxL,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,EAAE,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAuC,GAAG,MAAM,GAAG,SAAS,CAQ9K;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ,GAAG,kBAAkB,CAarB"}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner-start statement (#4147): a provisioner's signed word that it
|
|
3
|
+
* started a runner for one CI job, which the job checks before it runs.
|
|
4
|
+
*
|
|
5
|
+
* A persistent self-hosted runner keeps files and processes between jobs,
|
|
6
|
+
* so a plan job that ran a pull request's code can leave something behind
|
|
7
|
+
* for the next apply. An apply job that requires this statement refuses to
|
|
8
|
+
* run, before it trades for any credentials, on a runner that no trusted
|
|
9
|
+
* provisioner says it started for this job.
|
|
10
|
+
*
|
|
11
|
+
* It differs from the Op-run statement (#4044, ./workspace/trust/op-run-statement.ts)
|
|
12
|
+
* in three ways. It is signed before the job runs, not when a run settles.
|
|
13
|
+
* The provisioner that starts the runner signs it, so the key never enters
|
|
14
|
+
* the job. And it names the CI job, run and attempt, so a statement issued
|
|
15
|
+
* for one job does not verify in another.
|
|
16
|
+
*
|
|
17
|
+
* The statement is an in-toto Statement v1 in a DSSE envelope
|
|
18
|
+
* (./workspace/trust/dsse.ts), signed with an Ed25519 key that
|
|
19
|
+
* `.chant/trust.json` lists under `runners`, whose principal holds the
|
|
20
|
+
* `runner` role there, the same keys and role a ci tag needs (#4031):
|
|
21
|
+
*
|
|
22
|
+
* - `subject`: the commit the job runs, by `digest.gitCommit`.
|
|
23
|
+
* - `predicate`: the signer's principal; `ci` with the system (`github`,
|
|
24
|
+
* `gitlab`, `forgejo`), the repository, and the run, attempt and job ids;
|
|
25
|
+
* `runner` with the runner's name and `singleJob: true`; and `issuedAt`.
|
|
26
|
+
*
|
|
27
|
+
* The job finds it in `CHANT_RUNNER_START` (the envelope's JSON) or in the
|
|
28
|
+
* file `CHANT_RUNNER_START_FILE` names, else in
|
|
29
|
+
* {@link DEFAULT_RUNNER_START_FILE}. The job's own ids come from the forge's
|
|
30
|
+
* variables ({@link ciJobFromEnv}).
|
|
31
|
+
*
|
|
32
|
+
* This module needs only `node:crypto` and `node:fs`, so a bundled stage can
|
|
33
|
+
* import it alone:
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { verifyRunnerStartForJob } from "@intentius/chant/runner-start";
|
|
37
|
+
*
|
|
38
|
+
* const trust = JSON.parse(readFileSync(".chant/trust.json", "utf8"));
|
|
39
|
+
* const v = verifyRunnerStartForJob({ runners: trust.runners ?? [], roles: trust.roles ?? {} });
|
|
40
|
+
* if (v.status !== "verified") throw new Error(v.reason);
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* `chant ci runner-start verify` and `chant ci run --require-runner-start`
|
|
44
|
+
* read the runners and roles from `.chant/trust.json` at base, which also
|
|
45
|
+
* drops a runner entry the signers file lists (./workspace/trust/policy.ts).
|
|
46
|
+
* A caller that passes trust.json's lists itself skips that check.
|
|
47
|
+
*/
|
|
48
|
+
import { readFileSync } from "node:fs";
|
|
49
|
+
import { IN_TOTO_PAYLOAD_TYPE, signEnvelope, verifyEnvelope } from "./workspace/trust/dsse.js";
|
|
50
|
+
export const RUNNER_START_PREDICATE = "https://intentius.io/chant/runner-start/v1";
|
|
51
|
+
/** The in-toto Statement v1 type, as ./workspace/trust/evidence.ts has it. */
|
|
52
|
+
const STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
53
|
+
/** The role a runner-start statement's signer must hold in `.chant/trust.json`, as for a ci tag (#4031). */
|
|
54
|
+
export const RUNNER_START_SIGNER_ROLE = "runner";
|
|
55
|
+
/** The variable that holds the statement's envelope as JSON. */
|
|
56
|
+
export const RUNNER_START_ENV = "CHANT_RUNNER_START";
|
|
57
|
+
/** The variable that names a file holding the envelope, in place of {@link DEFAULT_RUNNER_START_FILE}. */
|
|
58
|
+
export const RUNNER_START_FILE_ENV = "CHANT_RUNNER_START_FILE";
|
|
59
|
+
/** Where the job looks for the envelope when neither variable is set. */
|
|
60
|
+
export const DEFAULT_RUNNER_START_FILE = "/run/chant/runner-start.json";
|
|
61
|
+
const FULL_COMMIT = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
62
|
+
const SYSTEMS = ["github", "gitlab", "forgejo"];
|
|
63
|
+
/** The job the forge's variables name, or why they name none. GitHub's names are also Forgejo's; `FORGEJO_ACTIONS` or `GITEA_ACTIONS` marks Forgejo, as in ./notify.ts. */
|
|
64
|
+
export function ciJobFromEnv(env = process.env) {
|
|
65
|
+
const need = (names) => {
|
|
66
|
+
const missing = names.filter((n) => !env[n]);
|
|
67
|
+
return missing.length > 0 ? `the job's environment does not set ${missing.join(", ")}` : undefined;
|
|
68
|
+
};
|
|
69
|
+
if (env.GITLAB_CI === "true" || env.CI_PIPELINE_ID) {
|
|
70
|
+
const gap = need(["CI_PROJECT_PATH", "CI_PIPELINE_ID", "CI_JOB_ID", "CI_COMMIT_SHA"]);
|
|
71
|
+
if (gap)
|
|
72
|
+
return { ok: false, reason: gap };
|
|
73
|
+
return { ok: true, job: { system: "gitlab", repo: env.CI_PROJECT_PATH, run: env.CI_PIPELINE_ID, attempt: "1", job: env.CI_JOB_ID, commit: env.CI_COMMIT_SHA } };
|
|
74
|
+
}
|
|
75
|
+
if (env.GITHUB_ACTIONS === "true" || env.FORGEJO_ACTIONS === "true" || env.GITEA_ACTIONS === "true") {
|
|
76
|
+
const system = env.FORGEJO_ACTIONS === "true" || env.GITEA_ACTIONS === "true" ? "forgejo" : "github";
|
|
77
|
+
const gap = need(["GITHUB_REPOSITORY", "GITHUB_RUN_ID", "GITHUB_JOB", "GITHUB_SHA"]);
|
|
78
|
+
if (gap)
|
|
79
|
+
return { ok: false, reason: gap };
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
job: {
|
|
83
|
+
system,
|
|
84
|
+
repo: env.GITHUB_REPOSITORY,
|
|
85
|
+
run: env.GITHUB_RUN_ID,
|
|
86
|
+
attempt: env.GITHUB_RUN_ATTEMPT || "1",
|
|
87
|
+
job: env.GITHUB_JOB,
|
|
88
|
+
commit: env.GITHUB_SHA,
|
|
89
|
+
...(env.RUNNER_NAME ? { runner: env.RUNNER_NAME } : {}),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { ok: false, reason: "this is not a GitHub, Forgejo or GitLab job: none of GITHUB_ACTIONS, FORGEJO_ACTIONS, GITEA_ACTIONS or GITLAB_CI is set" };
|
|
94
|
+
}
|
|
95
|
+
/** Why `job` cannot be signed for, or undefined. */
|
|
96
|
+
function jobProblem(job) {
|
|
97
|
+
if (!SYSTEMS.includes(job.system))
|
|
98
|
+
return `the CI system is ${JSON.stringify(job.system)}, not github, gitlab or forgejo`;
|
|
99
|
+
for (const field of ["repo", "run", "attempt", "job"]) {
|
|
100
|
+
if (typeof job[field] !== "string" || job[field].length === 0)
|
|
101
|
+
return `the job names no ${field}`;
|
|
102
|
+
}
|
|
103
|
+
if (!FULL_COMMIT.test(job.commit))
|
|
104
|
+
return `the job's commit ${JSON.stringify(job.commit)} is not a full commit id`;
|
|
105
|
+
if (job.system !== "gitlab" && !job.runner)
|
|
106
|
+
return `a ${job.system} job needs the runner's name, since GITHUB_JOB repeats across a matrix's jobs`;
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
/** The statement `signer` signs for a runner started for `job`. Throws for a job it cannot name. */
|
|
110
|
+
export function buildRunnerStartStatement(job, signer, issuedAt = new Date()) {
|
|
111
|
+
const problem = jobProblem(job);
|
|
112
|
+
if (problem)
|
|
113
|
+
throw new Error(`no runner-start statement: ${problem}`);
|
|
114
|
+
return {
|
|
115
|
+
_type: STATEMENT_TYPE,
|
|
116
|
+
subject: [{ name: job.commit, digest: { gitCommit: job.commit } }],
|
|
117
|
+
predicateType: RUNNER_START_PREDICATE,
|
|
118
|
+
predicate: {
|
|
119
|
+
signer,
|
|
120
|
+
ci: { system: job.system, repo: job.repo, run: job.run, attempt: job.attempt, job: job.job },
|
|
121
|
+
runner: { ...(job.runner ? { name: job.runner } : {}), singleJob: true },
|
|
122
|
+
issuedAt: issuedAt.toISOString(),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** The envelope the provisioner hands the runner, signed with its key. */
|
|
127
|
+
export function signRunnerStart(job, signer, issuedAt = new Date()) {
|
|
128
|
+
const statement = buildRunnerStartStatement(job, signer.principal, issuedAt);
|
|
129
|
+
return signEnvelope(IN_TOTO_PAYLOAD_TYPE, Buffer.from(JSON.stringify(statement), "utf8"), signer.key, signer.publicKey);
|
|
130
|
+
}
|
|
131
|
+
const normalise = (name) => name.normalize("NFKC").trim().toLowerCase();
|
|
132
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
133
|
+
const onlyKeys = (v, keys) => Object.keys(v).every((k) => keys.includes(k));
|
|
134
|
+
const isText = (v) => typeof v === "string" && v.length > 0;
|
|
135
|
+
/** The statement in `raw`, or why it is not one. Strict, so a verifier never acts on a field it does not know. */
|
|
136
|
+
function parseStatement(raw) {
|
|
137
|
+
if (!isObject(raw) || !onlyKeys(raw, ["_type", "subject", "predicateType", "predicate"]))
|
|
138
|
+
return "not a statement with only _type, subject, predicateType and predicate";
|
|
139
|
+
if (raw._type !== STATEMENT_TYPE)
|
|
140
|
+
return `_type is not ${STATEMENT_TYPE}`;
|
|
141
|
+
if (raw.predicateType !== RUNNER_START_PREDICATE)
|
|
142
|
+
return `predicateType is not ${RUNNER_START_PREDICATE}`;
|
|
143
|
+
const subject = raw.subject;
|
|
144
|
+
if (!Array.isArray(subject) || subject.length !== 1)
|
|
145
|
+
return "subject is not one commit";
|
|
146
|
+
const s = subject[0];
|
|
147
|
+
if (!isObject(s) || !onlyKeys(s, ["name", "digest"]) || !isObject(s.digest) || !onlyKeys(s.digest, ["gitCommit"]))
|
|
148
|
+
return "subject is not one commit";
|
|
149
|
+
if (typeof s.name !== "string" || !FULL_COMMIT.test(s.name) || s.digest.gitCommit !== s.name)
|
|
150
|
+
return "the subject is not a full commit id named by itself";
|
|
151
|
+
const p = raw.predicate;
|
|
152
|
+
if (!isObject(p) || !onlyKeys(p, ["signer", "ci", "runner", "issuedAt"]))
|
|
153
|
+
return "predicate has fields other than signer, ci, runner and issuedAt";
|
|
154
|
+
if (!isText(p.signer))
|
|
155
|
+
return "predicate.signer is missing";
|
|
156
|
+
const ci = p.ci;
|
|
157
|
+
if (!isObject(ci) || !onlyKeys(ci, ["system", "repo", "run", "attempt", "job"]))
|
|
158
|
+
return "predicate.ci has fields other than system, repo, run, attempt and job";
|
|
159
|
+
if (!SYSTEMS.includes(ci.system))
|
|
160
|
+
return "predicate.ci.system is not github, gitlab or forgejo";
|
|
161
|
+
for (const field of ["repo", "run", "attempt", "job"])
|
|
162
|
+
if (!isText(ci[field]))
|
|
163
|
+
return `predicate.ci.${field} is missing`;
|
|
164
|
+
const runner = p.runner;
|
|
165
|
+
if (!isObject(runner) || !onlyKeys(runner, ["name", "singleJob"]) || runner.singleJob !== true)
|
|
166
|
+
return "predicate.runner does not say singleJob: true";
|
|
167
|
+
if (runner.name !== undefined && !isText(runner.name))
|
|
168
|
+
return "predicate.runner.name is not a name";
|
|
169
|
+
if (!isText(p.issuedAt) || Number.isNaN(Date.parse(p.issuedAt)))
|
|
170
|
+
return "predicate.issuedAt is not a time";
|
|
171
|
+
return raw;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Verify a runner-start statement for `job`: a signature by a key listed
|
|
175
|
+
* under `runners` whose principal holds the `runner` role, over a statement
|
|
176
|
+
* for this system, repository, run, attempt, job, commit and, on GitHub and
|
|
177
|
+
* Forgejo, runner. Never throws.
|
|
178
|
+
*/
|
|
179
|
+
export function verifyRunnerStart(input) {
|
|
180
|
+
const { job } = input;
|
|
181
|
+
const what = `the ${job.system} job ${job.job} of run ${job.run} (attempt ${job.attempt})`;
|
|
182
|
+
if (input.envelope === undefined || input.envelope === null || input.envelope === "") {
|
|
183
|
+
return { status: "missing", reason: `${what} has no runner-start statement: no provisioner says this runner was started for it` };
|
|
184
|
+
}
|
|
185
|
+
let envelope = input.envelope;
|
|
186
|
+
if (typeof envelope === "string") {
|
|
187
|
+
try {
|
|
188
|
+
envelope = JSON.parse(envelope);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return { status: "invalid", reason: `${what}: the runner-start statement is not JSON` };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const v = verifyEnvelope(envelope, input.runners);
|
|
195
|
+
if (!v.ok)
|
|
196
|
+
return { status: v.code === "envelope-untrusted" ? "untrusted" : "invalid", reason: `${what}: ${v.reason}` };
|
|
197
|
+
if (!(input.roles[RUNNER_START_SIGNER_ROLE] ?? []).some((p) => normalise(p) === normalise(v.principal))) {
|
|
198
|
+
return { status: "untrusted", reason: `${what}: the statement is signed by ${v.principal}'s key, and ${v.principal} holds no ${RUNNER_START_SIGNER_ROLE} role in .chant/trust.json at base` };
|
|
199
|
+
}
|
|
200
|
+
if (v.payloadType !== IN_TOTO_PAYLOAD_TYPE)
|
|
201
|
+
return { status: "invalid", reason: `${what}: payload type ${v.payloadType} is not ${IN_TOTO_PAYLOAD_TYPE}` };
|
|
202
|
+
let raw;
|
|
203
|
+
try {
|
|
204
|
+
raw = JSON.parse(v.payload.toString("utf8"));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return { status: "invalid", reason: `${what}: the statement's payload is not JSON` };
|
|
208
|
+
}
|
|
209
|
+
const parsed = parseStatement(raw);
|
|
210
|
+
if (typeof parsed === "string")
|
|
211
|
+
return { status: "invalid", reason: `${what}: the payload is not a ${RUNNER_START_PREDICATE} statement: ${parsed}` };
|
|
212
|
+
const p = parsed.predicate;
|
|
213
|
+
if (p.signer !== v.principal)
|
|
214
|
+
return { status: "invalid", reason: `${what}: the statement names signer ${JSON.stringify(p.signer)}, but ${v.principal}'s key signed it` };
|
|
215
|
+
const differs = [];
|
|
216
|
+
if (p.ci.system !== job.system)
|
|
217
|
+
differs.push(`it is for a ${p.ci.system} job`);
|
|
218
|
+
if (p.ci.repo !== job.repo)
|
|
219
|
+
differs.push(`it is for repository ${p.ci.repo}`);
|
|
220
|
+
if (p.ci.run !== job.run)
|
|
221
|
+
differs.push(`it is for run ${p.ci.run}`);
|
|
222
|
+
if (p.ci.attempt !== job.attempt)
|
|
223
|
+
differs.push(`it is for attempt ${p.ci.attempt}`);
|
|
224
|
+
if (p.ci.job !== job.job)
|
|
225
|
+
differs.push(`it is for job ${p.ci.job}`);
|
|
226
|
+
const commit = parsed.subject[0].name;
|
|
227
|
+
if (commit !== job.commit)
|
|
228
|
+
differs.push(`it is for commit ${commit.slice(0, 12)}, and the job runs ${job.commit.slice(0, 12)}`);
|
|
229
|
+
if (job.system !== "gitlab") {
|
|
230
|
+
if (!p.runner.name)
|
|
231
|
+
differs.push("it names no runner, and a job id alone does not tell a matrix's jobs apart");
|
|
232
|
+
else if (!job.runner)
|
|
233
|
+
differs.push(`it is for runner ${p.runner.name}, and the job's environment names no runner (RUNNER_NAME)`);
|
|
234
|
+
else if (p.runner.name !== job.runner)
|
|
235
|
+
differs.push(`it is for runner ${p.runner.name}, and this is ${job.runner}`);
|
|
236
|
+
}
|
|
237
|
+
if (differs.length > 0)
|
|
238
|
+
return { status: "mismatch", reason: `${what}: the runner-start statement signed by ${v.principal} is for another job: ${differs.join("; ")}` };
|
|
239
|
+
if (input.maxAgeSeconds !== undefined) {
|
|
240
|
+
const age = ((input.now ?? new Date()).getTime() - Date.parse(p.issuedAt)) / 1000;
|
|
241
|
+
if (age > input.maxAgeSeconds)
|
|
242
|
+
return { status: "stale", reason: `${what}: the runner-start statement was issued ${Math.round(age)}s ago, more than ${input.maxAgeSeconds}s` };
|
|
243
|
+
}
|
|
244
|
+
return { status: "verified", signer: v.principal, keyid: v.keyid, issuedAt: p.issuedAt, reason: `runner started for this job, signed by ${v.principal} (${RUNNER_START_SIGNER_ROLE})` };
|
|
245
|
+
}
|
|
246
|
+
/** The envelope's JSON the job was given: `CHANT_RUNNER_START`, else the file `CHANT_RUNNER_START_FILE` names, else {@link DEFAULT_RUNNER_START_FILE}. Undefined when there is none. */
|
|
247
|
+
export function readRunnerStart(env = process.env, readFile = (p) => readFileSync(p, "utf8")) {
|
|
248
|
+
const inline = env[RUNNER_START_ENV];
|
|
249
|
+
if (inline)
|
|
250
|
+
return inline;
|
|
251
|
+
try {
|
|
252
|
+
return readFile(env[RUNNER_START_FILE_ENV] || DEFAULT_RUNNER_START_FILE);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* One call for a job: find its statement ({@link readRunnerStart}), name the
|
|
260
|
+
* job from the forge's variables ({@link ciJobFromEnv}) and verify one
|
|
261
|
+
* against the other ({@link verifyRunnerStart}). Never throws.
|
|
262
|
+
*/
|
|
263
|
+
export function verifyRunnerStartForJob(input) {
|
|
264
|
+
const env = input.env ?? process.env;
|
|
265
|
+
const named = ciJobFromEnv(env);
|
|
266
|
+
if (!named.ok)
|
|
267
|
+
return { status: "missing", reason: `no job to check a runner-start statement for: ${named.reason}` };
|
|
268
|
+
const job = input.commit ? { ...named.job, commit: input.commit } : named.job;
|
|
269
|
+
return verifyRunnerStart({
|
|
270
|
+
envelope: readRunnerStart(env, input.readFile),
|
|
271
|
+
job,
|
|
272
|
+
runners: input.runners,
|
|
273
|
+
roles: input.roles,
|
|
274
|
+
...(input.maxAgeSeconds !== undefined ? { maxAgeSeconds: input.maxAgeSeconds } : {}),
|
|
275
|
+
...(input.now ? { now: input.now } : {}),
|
|
276
|
+
});
|
|
277
|
+
}
|