@kungfu-tech/buildchain 2.12.6 → 2.12.7-alpha.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.
Files changed (34) hide show
  1. package/actions/promote-buildchain-ref/README.md +7 -3
  2. package/bin/buildchain.mjs +78 -0
  3. package/dist/site/agent-index.json +1 -0
  4. package/dist/site/artifact-schemas.json +1 -0
  5. package/dist/site/buildchain-contract.json +137 -37
  6. package/dist/site/buildchain-site.json +68 -12
  7. package/dist/site/capability-registry.json +6 -5
  8. package/dist/site/cli-registry.json +36 -0
  9. package/dist/site/controller-registry.json +104 -13
  10. package/dist/site/kfd-claims.json +251 -15
  11. package/dist/site/kfd-upstream-aggregate.json +1 -1
  12. package/dist/site/manual-registry.json +15 -1
  13. package/dist/site/node-api-registry.json +43 -4
  14. package/dist/site/page-registry.json +54 -7
  15. package/dist/site/public-surface-audit.json +150 -17
  16. package/dist/site/publication-authority-registry.json +754 -0
  17. package/dist/site/publication-registry.json +4 -4
  18. package/dist/site/release-provenance.json +4 -0
  19. package/dist/site/site-manifest.json +14 -5
  20. package/dist/site/workflow-registry.json +108 -10
  21. package/docs/MAP.md +2 -1
  22. package/docs/publication-artifacts.md +12 -10
  23. package/docs/publication-authority.md +204 -0
  24. package/package.json +5 -1
  25. package/packages/core/buildchain-kfd-claims.js +5 -0
  26. package/packages/core/buildchain-publication-authority.js +79 -0
  27. package/packages/core/controller-evidence.js +7 -7
  28. package/packages/core/index.js +28 -0
  29. package/packages/core/publication-authority.js +725 -0
  30. package/packages/core/publication-control-plane-audit.js +133 -0
  31. package/scripts/assemble-self-publication-admission.mjs +182 -0
  32. package/scripts/audit-publication-control-plane.mjs +406 -0
  33. package/scripts/check-inventory.mjs +20 -2
  34. package/scripts/generate-site-bundle.mjs +16 -0
@@ -0,0 +1,133 @@
1
+ import { createPublicationControlPlaneAudit, publicationAuthorityDigest } from "./publication-authority.js";
2
+
3
+ function fact(id, pass, observed) {
4
+ return {
5
+ id,
6
+ status: pass ? "pass" : "fail",
7
+ digest: publicationAuthorityDigest({ id, observed }),
8
+ };
9
+ }
10
+
11
+ export function evaluatePublicationControlPlaneSnapshot({
12
+ repository,
13
+ workflowPath,
14
+ publisherWorkflowPath = workflowPath,
15
+ environment,
16
+ branch,
17
+ packageName,
18
+ publisherMode = "npm-trusted-publisher",
19
+ snapshot,
20
+ observedAt,
21
+ expiresAt,
22
+ } = {}) {
23
+ const workflowFilename = String(publisherWorkflowPath || "").split("/").pop();
24
+ const providerEnvironment = environment === "none" ? "" : environment;
25
+ const actions = snapshot?.actions || {};
26
+ const branchPolicy = snapshot?.branch || {};
27
+ const environmentPolicy = snapshot?.environment || {};
28
+ const oidc = snapshot?.oidc || {};
29
+ const publisher = snapshot?.publisher || {};
30
+ const runner = snapshot?.runner || {};
31
+ const npmIdentityPass = publisher.packageName === packageName &&
32
+ publisher.provider === "github" &&
33
+ publisher.repository === repository &&
34
+ publisher.workflowFilename === workflowFilename &&
35
+ publisher.environment === providerEnvironment &&
36
+ publisher.longLivedWorkflowCredentialPresent === false;
37
+ const publisherPass = publisherMode === "npm-trusted-publisher"
38
+ ? npmIdentityPass && (
39
+ (publisher.enforcement === "audited-control-plane" && publisher.allowPublish === true) ||
40
+ (publisher.enforcement === "provider-at-transaction" &&
41
+ publisher.authorizationDeferred === true &&
42
+ publisher.configurationRead === false)
43
+ )
44
+ : publisherMode === "github-token"
45
+ ? publisher.provider === "github-token" &&
46
+ publisher.repository === repository &&
47
+ publisher.workflowPath === publisherWorkflowPath &&
48
+ publisher.permissionScoped === true &&
49
+ publisher.longLivedWorkflowCredentialPresent === false
50
+ : publisherMode === "oidc-role"
51
+ ? publisher.provider !== "" &&
52
+ publisher.repository === repository &&
53
+ publisher.workflowPath === publisherWorkflowPath &&
54
+ publisher.environment === environment &&
55
+ publisher.trustQualifying === true &&
56
+ /^[0-9a-f]{64}$/i.test(String(publisher.roleDigest || "").replace(/^sha256:/, "")) &&
57
+ publisher.longLivedWorkflowCredentialPresent === false
58
+ : false;
59
+ const credentialIsolationPass = publisherMode === "github-token"
60
+ ? oidc.githubTokenJobScoped === true && oidc.longLivedCredentialPresent === false
61
+ : oidc.workflowPath === publisherWorkflowPath &&
62
+ oidc.environment === providerEnvironment &&
63
+ oidc.idTokenJobScoped === true &&
64
+ oidc.longLivedCredentialPresent === false;
65
+ const configuredBranchPolicyPass = branchPolicy.ref === branch &&
66
+ branchPolicy.strict === true &&
67
+ Number(branchPolicy.requiredApprovals || 0) >= 1 &&
68
+ branchPolicy.requireConversationResolution === true &&
69
+ branchPolicy.enforceAdmins === true;
70
+ const providerTransactionBranchPass = branchPolicy.ref === branch &&
71
+ branchPolicy.policyMode === "provider-enforced-transaction" &&
72
+ branchPolicy.protected === true &&
73
+ branchPolicy.enforcementLevel === "everyone" &&
74
+ Array.isArray(branchPolicy.requiredStatusChecks) &&
75
+ branchPolicy.requiredStatusChecks.includes("check") &&
76
+ branchPolicy.requiredCheckPassed === true &&
77
+ branchPolicy.sourceSha === branchPolicy.headSha &&
78
+ branchPolicy.mergedPullRequest === true &&
79
+ branchPolicy.baseRef === branch &&
80
+ branchPolicy.headRepository === repository &&
81
+ Number(branchPolicy.approvalCount || 0) >= 1 &&
82
+ branchPolicy.independentApproval === true;
83
+ const facts = [
84
+ fact(
85
+ "actions-policy",
86
+ actions.defaultWorkflowPermissions === "read" && actions.canApprovePullRequestReviews === false,
87
+ actions,
88
+ ),
89
+ fact(
90
+ "branch-policy",
91
+ configuredBranchPolicyPass || providerTransactionBranchPass,
92
+ branchPolicy,
93
+ ),
94
+ fact(
95
+ "environment-policy",
96
+ environment === "none"
97
+ ? environmentPolicy.declared === false && environmentPolicy.exists === false
98
+ : environmentPolicy.name === environment &&
99
+ environmentPolicy.declared === true &&
100
+ environmentPolicy.exists === true &&
101
+ environmentPolicy.protected === true &&
102
+ (environmentPolicy.reviewRequired !== true || environmentPolicy.preventSelfReview === true),
103
+ environmentPolicy,
104
+ ),
105
+ fact(
106
+ "oidc-policy",
107
+ credentialIsolationPass,
108
+ oidc,
109
+ ),
110
+ fact(
111
+ "publisher-policy",
112
+ publisherPass,
113
+ publisher,
114
+ ),
115
+ fact(
116
+ "runner-policy",
117
+ runner.class === "ephemeral" &&
118
+ runner.label === "ubuntu-24.04" &&
119
+ runner.githubHosted === true &&
120
+ runner.selfHostedAuthorized === false,
121
+ runner,
122
+ ),
123
+ ];
124
+ return createPublicationControlPlaneAudit({
125
+ repository,
126
+ workflowPath,
127
+ publisherWorkflowPath,
128
+ environment,
129
+ facts,
130
+ observedAt,
131
+ expiresAt,
132
+ });
133
+ }
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import { execFileSync } from "node:child_process";
5
+ import path from "node:path";
6
+
7
+ import {
8
+ createPublicationAdmission,
9
+ createPublicationArtifactManifestSet,
10
+ createPublicationGateDecision,
11
+ createRunnerProvenance,
12
+ } from "../packages/core/publication-authority.js";
13
+
14
+ function required(name) {
15
+ const value = String(process.env[name] || "").trim();
16
+ if (!value) throw new Error(`${name} is required`);
17
+ return value;
18
+ }
19
+
20
+ function sha256(value) {
21
+ return crypto.createHash("sha256").update(String(value)).digest("hex");
22
+ }
23
+
24
+ function filesNamed(root, name) {
25
+ const matches = [];
26
+ const stack = [root];
27
+ while (stack.length) {
28
+ const current = stack.pop();
29
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
30
+ const full = path.join(current, entry.name);
31
+ if (entry.isDirectory()) stack.push(full);
32
+ else if (entry.name === name) matches.push(full);
33
+ }
34
+ }
35
+ return matches.sort();
36
+ }
37
+
38
+ function oneJson(root, name) {
39
+ const matches = filesNamed(root, name);
40
+ if (matches.length !== 1) throw new Error(`expected exactly one ${name} under ${root}, found ${matches.length}`);
41
+ return JSON.parse(fs.readFileSync(matches[0], "utf8"));
42
+ }
43
+
44
+ function payloadFor(manifest, root) {
45
+ const artifactRoot = path.join(root, manifest.artifactName);
46
+ if (!fs.existsSync(artifactRoot)) throw new Error(`candidate payload artifact is missing: ${manifest.artifactName}`);
47
+ return {
48
+ artifactName: manifest.artifactName,
49
+ files: (manifest.files || []).filter((entry) => !entry.path.startsWith(".buildchain/")).map((entry) => {
50
+ if (path.isAbsolute(entry.path) || entry.path.includes("\\") || entry.path.split("/").includes("..")) {
51
+ throw new Error(`candidate payload manifest contains an unsafe path: ${entry.path}`);
52
+ }
53
+ const file = path.join(artifactRoot, entry.path);
54
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
55
+ throw new Error(`candidate payload file is missing: ${manifest.artifactName}/${entry.path}`);
56
+ }
57
+ return {
58
+ path: entry.path,
59
+ size: fs.statSync(file).size,
60
+ sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"),
61
+ };
62
+ }),
63
+ };
64
+ }
65
+
66
+ async function sourceTree(repository, sourceSha, token) {
67
+ const response = await fetch(`${required("GITHUB_API_URL")}/repos/${repository}/git/commits/${sourceSha}`, {
68
+ headers: {
69
+ accept: "application/vnd.github+json",
70
+ authorization: `Bearer ${token}`,
71
+ "x-github-api-version": "2022-11-28",
72
+ },
73
+ });
74
+ if (!response.ok) throw new Error(`could not resolve admitted source tree: GitHub API ${response.status}`);
75
+ const commit = await response.json();
76
+ return String(commit.tree?.sha || "");
77
+ }
78
+
79
+ function writeBundle(outputDir, values) {
80
+ fs.mkdirSync(outputDir, { recursive: true });
81
+ for (const [name, value] of Object.entries(values)) {
82
+ fs.writeFileSync(path.join(outputDir, `${name}.json`), `${JSON.stringify(value, null, 2)}\n`);
83
+ }
84
+ if (process.env.GITHUB_OUTPUT) {
85
+ const output = fs.createWriteStream(process.env.GITHUB_OUTPUT, { flags: "a" });
86
+ for (const [name, value] of Object.entries(values)) {
87
+ output.write(`${name.replaceAll("_", "-")}-json=${JSON.stringify(value)}\n`);
88
+ }
89
+ output.end();
90
+ }
91
+ }
92
+
93
+ async function main() {
94
+ const evidenceRoot = process.env.BUILDCHAIN_EVIDENCE_ROOT || ".buildchain/publication-evidence";
95
+ const runtimeRoot = process.env.BUILDCHAIN_RUNTIME_ROOT || ".buildchain/authority-runtime";
96
+ const repository = required("BUILDCHAIN_REPOSITORY");
97
+ const sourceSha = required("BUILDCHAIN_SOURCE_SHA").toLowerCase();
98
+ const token = required("GITHUB_TOKEN");
99
+ const passport = oneJson(path.join(evidenceRoot, "passport"), "release-candidate-passport.json");
100
+ const controllerReceipt = oneJson(path.join(evidenceRoot, "controller"), "release-candidate-receipt.json");
101
+ const controlPlaneAudit = JSON.parse(fs.readFileSync(required("BUILDCHAIN_CONTROL_PLANE_AUDIT_PATH"), "utf8"));
102
+ const registry = JSON.parse(fs.readFileSync(path.join(runtimeRoot, "dist/site/publication-authority-registry.json"), "utf8"));
103
+ const runtimeSha = execFileSync("git", ["-C", runtimeRoot, "rev-parse", "HEAD"], { encoding: "utf8" }).trim().toLowerCase();
104
+ const treeSha = await sourceTree(repository, sourceSha, token);
105
+ if (!treeSha || treeSha !== passport.source?.treeHash) {
106
+ throw new Error(`admitted source tree does not match release candidate: ${treeSha || "missing"}`);
107
+ }
108
+
109
+ const manifests = filesNamed(path.join(evidenceRoot, "manifests"), "manifest.json")
110
+ .map((file) => JSON.parse(fs.readFileSync(file, "utf8")));
111
+ const artifactSet = createPublicationArtifactManifestSet({
112
+ repository,
113
+ sourceSha: passport.source?.headSha,
114
+ sourceTreeSha: treeSha,
115
+ manifests,
116
+ payloads: manifests.map((manifest) => payloadFor(manifest, path.join(evidenceRoot, "payloads"))),
117
+ });
118
+ const gateAggregate = createPublicationGateDecision({
119
+ sourceSha,
120
+ profile: process.env.BUILDCHAIN_GATE_PROFILE || "buildchain-self-publication",
121
+ required: false,
122
+ rationale: process.env.BUILDCHAIN_GATE_RATIONALE || "Buildchain self-publication has no consumer-owned Shifu Gate registry.",
123
+ policy: { scope: "buildchain-self-publication", repository },
124
+ });
125
+ const runnerProvenance = createRunnerProvenance({
126
+ runnerClass: "ephemeral",
127
+ os: required("RUNNER_OS"),
128
+ architecture: required("RUNNER_ARCH"),
129
+ imageDigest: sha256(`${process.env.ImageOS || "unknown"}|${process.env.ImageVersion || "unknown"}`),
130
+ measurementDigest: sha256([
131
+ process.env.GITHUB_WORKFLOW,
132
+ process.env.GITHUB_JOB,
133
+ process.env.GITHUB_RUN_ID,
134
+ process.env.GITHUB_RUN_ATTEMPT,
135
+ process.env.RUNNER_ENVIRONMENT,
136
+ ].join("|")),
137
+ isolation: "github-hosted-single-job",
138
+ });
139
+ const packageJson = JSON.parse(fs.readFileSync(path.join(runtimeRoot, "package.json"), "utf8"));
140
+ const issuedAt = new Date();
141
+ const admission = createPublicationAdmission({
142
+ registryDigest: registry.registryDigest,
143
+ workflowPath: required("BUILDCHAIN_AUTHORITY_WORKFLOW_PATH"),
144
+ publisherWorkflowPath: required("BUILDCHAIN_PUBLISHER_WORKFLOW_PATH"),
145
+ repository,
146
+ sourceSha,
147
+ runtimeSha,
148
+ contractDigest: controllerReceipt.runtime?.contractDigest,
149
+ policyDigest: gateAggregate.policyDigest,
150
+ controllerReceiptDigest: controllerReceipt.digest,
151
+ runnerProvenanceDigest: runnerProvenance.receiptDigest,
152
+ controlPlaneAuditDigest: controlPlaneAudit.receiptDigest,
153
+ gateAggregateDigest: gateAggregate.digest,
154
+ environment: process.env.BUILDCHAIN_PUBLICATION_ENVIRONMENT || "none",
155
+ product: process.env.BUILDCHAIN_PUBLICATION_PRODUCT || "Buildchain",
156
+ target: process.env.BUILDCHAIN_PUBLICATION_TARGET || `npm:${packageJson.name}`,
157
+ version: process.env.BUILDCHAIN_PUBLICATION_VERSION || packageJson.version,
158
+ channel: required("BUILDCHAIN_PUBLICATION_CHANNEL"),
159
+ artifactDigest: artifactSet.manifestSetDigest,
160
+ nonce: `${required("GITHUB_RUN_ID")}:${required("GITHUB_RUN_ATTEMPT")}:${sourceSha}`,
161
+ issuedAt: issuedAt.toISOString(),
162
+ expiresAt: new Date(issuedAt.getTime() + 10 * 60 * 1000).toISOString(),
163
+ });
164
+ const bindingNames = [
165
+ "repository", "publisherWorkflowPath", "sourceSha", "runtimeSha", "contractDigest", "policyDigest",
166
+ "controllerReceiptDigest", "gateAggregateDigest", "environment", "product", "target", "version", "channel",
167
+ "artifactDigest",
168
+ ];
169
+ const expected = Object.fromEntries(bindingNames.map((name) => [name, admission[name]]));
170
+ writeBundle(process.env.BUILDCHAIN_OUTPUT_DIR || ".buildchain/publication-authority/auto", {
171
+ admission,
172
+ runner_provenance: runnerProvenance,
173
+ control_plane_audit: controlPlaneAudit,
174
+ gate_aggregate: gateAggregate,
175
+ expected,
176
+ });
177
+ }
178
+
179
+ main().catch((error) => {
180
+ console.error(`assemble self publication admission: ${error.message}`);
181
+ process.exitCode = 1;
182
+ });