@xemahq/xema 0.54.0 → 0.56.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/dist/edition/verify-image-evidence.bundle.mjs +1996 -1012
- package/dist/edition/verify-image-evidence.bundle.provenance.json +584 -500
- package/dist/runtime/lock-loader.d.ts +2 -1
- package/dist/runtime/lock-loader.d.ts.map +1 -1
- package/dist/runtime/lock-loader.js +15 -5
- package/dist/runtime/lock-loader.js.map +1 -1
- package/edition/README.md +8 -3
- package/edition/biome-content.mjs +4 -4
- package/edition/candidate-edition.mjs +8 -3
- package/edition/check-committed-image-evidence.mjs +44 -15
- package/edition/customer-edition-verbs.mjs +15 -4
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-resolve.mjs +18 -10
- package/edition/develop-edition.mjs +30 -9
- package/edition/develop-freeze.mjs +24 -1
- package/edition/develop-git-bundles.mjs +201 -0
- package/edition/develop-graph.mjs +5 -3
- package/edition/develop-image-probe.mjs +6 -4
- package/edition/develop-image-record.mjs +194 -0
- package/edition/develop-record-ancestry.mjs +23 -5
- package/edition/develop-select-records.mjs +29 -21
- package/edition/develop-selection-line.mjs +30 -0
- package/edition/develop-selection-pins.mjs +30 -0
- package/edition/dist-check.mjs +45 -9
- package/edition/distribution-evidence-set.mjs +14 -9
- package/edition/edition-image-seal.mjs +2 -1
- package/edition/edition-protocol.mjs +6 -20
- package/edition/external-source-walk.mjs +37 -15
- package/edition/image-evidence.mjs +6 -2
- package/edition/image-lock-service-rows.mjs +4 -4
- package/edition/image-subject-identity.mjs +24 -0
- package/edition/images-from-record.mjs +46 -110
- package/edition/lib.mjs +24 -26
- package/edition/locked-bundle-signer.mjs +52 -8
- package/edition/resolve-customer-edition.mjs +1 -1
- package/edition/resolve-distribution.mjs +84 -21
- package/edition/resolve-edition.mjs +2 -1
- package/edition/resolve-images.mjs +224 -12
- package/edition/seal-image-digests.mjs +24 -13
- package/edition/third-party-bundle-registry.mjs +73 -17
- package/edition/third-party-source-evidence.mjs +39 -14
- package/edition/third-party-source-writer.mjs +2 -18
- package/edition/trust-policy-arming.mjs +17 -6
- package/edition/verify-image-platforms.mjs +2 -0
- package/edition/workspace-sections.mjs +11 -1
- package/package.json +3 -3
|
@@ -66,31 +66,56 @@ export function parseProvenanceStatement(value) {
|
|
|
66
66
|
const readEvidenceFile = (root, path, label, maxBytes) => readBoundedNoFollowFile(root, path, label, maxBytes);
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* pinned repository and workflow. Throws naming the row and the field.
|
|
69
|
+
* A keyless signer row of `channel`, held to everything such a row must be: the channel signed from that channel's ref, the
|
|
70
|
+
* GitHub Actions issuer and the exact identity of its own workflow. Throws naming the row and the field.
|
|
72
71
|
*/
|
|
73
|
-
export function
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
throw new Error(`${label}: the trust policy has ${rows.length} rows with policyId "${policyId}"; exactly one is required`);
|
|
77
|
-
}
|
|
78
|
-
const [row] = rows;
|
|
79
|
-
const ref = schemas.EVIDENCE_CHANNEL_REFS[THIRD_PARTY_EVIDENCE_CHANNEL];
|
|
80
|
-
if (row.channel !== THIRD_PARTY_EVIDENCE_CHANNEL || row.githubWorkflow?.ref !== ref) {
|
|
72
|
+
export function assertChannelSignerRow({ row, channel, schemas, label }) {
|
|
73
|
+
const ref = schemas.EVIDENCE_CHANNEL_REFS[channel];
|
|
74
|
+
if (row.channel !== channel || row.githubWorkflow?.ref !== ref) {
|
|
81
75
|
throw new Error(
|
|
82
|
-
`${label}: row "${policyId}" is not a ${
|
|
76
|
+
`${label}: row "${row.policyId}" is not a ${channel} row signed from ${ref} ` +
|
|
83
77
|
`(channel ${JSON.stringify(row.channel)}, ref ${JSON.stringify(row.githubWorkflow?.ref)})`,
|
|
84
78
|
);
|
|
85
79
|
}
|
|
86
80
|
if (row.oidcIssuer !== GITHUB_ACTIONS_OIDC_ISSUER) {
|
|
87
|
-
throw new Error(`${label}: row "${policyId}" oidcIssuer ${JSON.stringify(row.oidcIssuer)} is not ${GITHUB_ACTIONS_OIDC_ISSUER}`);
|
|
81
|
+
throw new Error(`${label}: row "${row.policyId}" oidcIssuer ${JSON.stringify(row.oidcIssuer)} is not ${GITHUB_ACTIONS_OIDC_ISSUER}`);
|
|
88
82
|
}
|
|
89
83
|
const { repository, path } = row.githubWorkflow;
|
|
90
84
|
const identity = `https://github.com/${repository}/${path}@${ref}`;
|
|
91
85
|
if (row.certificateIdentity !== identity) {
|
|
92
|
-
throw new Error(`${label}: row "${policyId}" certificateIdentity ${JSON.stringify(row.certificateIdentity)} is not the exact workflow identity ${identity}`);
|
|
86
|
+
throw new Error(`${label}: row "${row.policyId}" certificateIdentity ${JSON.stringify(row.certificateIdentity)} is not the exact workflow identity ${identity}`);
|
|
93
87
|
}
|
|
88
|
+
return row;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The ONE `channel` row that signs the SLSA provenance of `repository`'s biome bundles, held to `assertChannelSignerRow`.
|
|
93
|
+
* None, or several, is refused naming the repository and the channel.
|
|
94
|
+
*/
|
|
95
|
+
export function bundleSignerRowFor({ policy, repository, channel, schemas, label }) {
|
|
96
|
+
const rows = policy.policies.filter(
|
|
97
|
+
(row) => row.channel === channel && row.predicateTypes?.includes(SLSA_PROVENANCE_V1) && row.githubWorkflow?.repository === repository,
|
|
98
|
+
);
|
|
99
|
+
if (rows.length !== 1) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`${label}: the trust policy has ${rows.length} ${channel} rows authorizing ${SLSA_PROVENANCE_V1} for ${repository}; exactly one bundle-signer row is required`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return assertChannelSignerRow({ row: rows[0], channel, schemas, label });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The trust-policy row `policyId` names, held to everything a third-party signer row must be: one row, the release
|
|
109
|
+
* channel signed from refs/heads/main, the GitHub Actions issuer, the exact identity of its own workflow, and the
|
|
110
|
+
* pinned repository and workflow. Throws naming the row and the field.
|
|
111
|
+
*/
|
|
112
|
+
export function thirdPartySignerRow({ policy, policyId, source, schemas, label }) {
|
|
113
|
+
const rows = policy.policies.filter((row) => row.policyId === policyId);
|
|
114
|
+
if (rows.length !== 1) {
|
|
115
|
+
throw new Error(`${label}: the trust policy has ${rows.length} rows with policyId "${policyId}"; exactly one is required`);
|
|
116
|
+
}
|
|
117
|
+
const row = assertChannelSignerRow({ row: rows[0], channel: THIRD_PARTY_EVIDENCE_CHANNEL, schemas, label });
|
|
118
|
+
const { repository, path } = row.githubWorkflow;
|
|
94
119
|
if (repository !== source.repository) {
|
|
95
120
|
throw new Error(`${label}: row "${policyId}" signs for ${repository}, but the source is pinned to ${source.repository}`);
|
|
96
121
|
}
|
|
@@ -16,8 +16,8 @@ import { bundleImageNames, fetchBundleManifest, fetchBundleProvenance, verifyBun
|
|
|
16
16
|
import {
|
|
17
17
|
PROVENANCE_BUNDLE_FILE,
|
|
18
18
|
PROVENANCE_STATEMENT_FILE,
|
|
19
|
-
SLSA_PROVENANCE_V1,
|
|
20
19
|
THIRD_PARTY_EVIDENCE_CHANNEL,
|
|
20
|
+
bundleSignerRowFor,
|
|
21
21
|
digestOfOciRef,
|
|
22
22
|
provenanceDirectoryForDigest,
|
|
23
23
|
thirdPartySignerRow,
|
|
@@ -28,22 +28,6 @@ import { assertThirdPartyUniqueness } from './third-party-source-uniqueness.mjs'
|
|
|
28
28
|
|
|
29
29
|
const compare = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
|
|
30
30
|
|
|
31
|
-
/** The one release-channel row that signs the biome bundle's provenance for `repository`. */
|
|
32
|
-
function bundleSignerRow(policy, repository) {
|
|
33
|
-
const rows = policy.policies.filter(
|
|
34
|
-
(row) =>
|
|
35
|
-
row.channel === THIRD_PARTY_EVIDENCE_CHANNEL &&
|
|
36
|
-
row.predicateTypes.includes(SLSA_PROVENANCE_V1) &&
|
|
37
|
-
row.githubWorkflow?.repository === repository,
|
|
38
|
-
);
|
|
39
|
-
if (rows.length !== 1) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`the trust policy has ${rows.length} ${THIRD_PARTY_EVIDENCE_CHANNEL} rows authorizing ${SLSA_PROVENANCE_V1} for ${repository}; exactly one bundle-signer row is required`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
return rows[0];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
31
|
/** The one release-channel row whose imageRepositories lists `imageRef`. */
|
|
48
32
|
function imageSignerRow(policy, schemas, imageRef) {
|
|
49
33
|
const rows = policyCandidates(policy, schemas.IMAGE_PRODUCER_PREDICATE_TYPE, THIRD_PARTY_EVIDENCE_CHANNEL, imageRef);
|
|
@@ -116,7 +100,7 @@ async function collectImage({ image, request, context, clientFor, services }) {
|
|
|
116
100
|
export async function planThirdPartySourceWrite({ request, context, clientFor, withProducerWorkflowPin, dataRoot, verifyOnRegistry = verifyBundleOnRegistry }) {
|
|
117
101
|
const { policy, schemas } = context;
|
|
118
102
|
const repositoryUrl = `https://github.com/${request.repository}`;
|
|
119
|
-
const row =
|
|
103
|
+
const row = bundleSignerRowFor({ policy, repository: request.repository, channel: THIRD_PARTY_EVIDENCE_CHANNEL, schemas, label: `third-party source "${request.name}" bundle signer` });
|
|
120
104
|
const workflowPath = row.githubWorkflow.path;
|
|
121
105
|
// The row must be exact (release channel, refs/heads/main, the GitHub issuer, its own workflow identity) before cosign runs.
|
|
122
106
|
thirdPartySignerRow({ policy, policyId: row.policyId, source: { repository: request.repository, producerWorkflow: { path: workflowPath } }, schemas, label: `third-party source "${request.name}" bundle signer` });
|
|
@@ -38,6 +38,13 @@
|
|
|
38
38
|
// channel neither arm nor disarm the other: a policy that authorizes the three
|
|
39
39
|
// stages on the release channel and pins both binaries is ARMED for a release
|
|
40
40
|
// edition whatever it says about develop.
|
|
41
|
+
//
|
|
42
|
+
// ONLY A STAGE ROW ARMS A CHANNEL: a row counts toward a channel's arming when it
|
|
43
|
+
// authorizes one of the stage predicates judged here. A bundle-signer row (SLSA
|
|
44
|
+
// provenance of a third-party bundle, e.g. the develop row of a git-sourced
|
|
45
|
+
// biome) authorizes bundle verification and arms no stage, so a channel whose
|
|
46
|
+
// only rows are bundle signers is UNARMED, exactly like a channel with no row.
|
|
47
|
+
// One stage row on the channel still makes it PARTIAL until every stage is met.
|
|
41
48
|
|
|
42
49
|
export const TrustPolicyArmingState = Object.freeze({
|
|
43
50
|
ARMED: 'armed',
|
|
@@ -81,8 +88,11 @@ export function assessTrustPolicyArming(policy, schemas, channel, { consumesCata
|
|
|
81
88
|
stages.push(['edition-catalogue', schemas.EDITION_CATALOGUE_PREDICATE_TYPE]);
|
|
82
89
|
}
|
|
83
90
|
}
|
|
84
|
-
const
|
|
85
|
-
const
|
|
91
|
+
const stagePredicateTypes = stages.map(([, predicateType]) => predicateType);
|
|
92
|
+
const stageRows = policy.policies.filter(
|
|
93
|
+
(candidate) => candidate.channel === channel && candidate.predicateTypes.some((predicateType) => stagePredicateTypes.includes(predicateType)),
|
|
94
|
+
);
|
|
95
|
+
const authorized = stages.filter(([, predicateType]) => stageRows.some((candidate) => candidate.predicateTypes.includes(predicateType)));
|
|
86
96
|
|
|
87
97
|
if (policy.policies.length === 0) {
|
|
88
98
|
unmet.push(
|
|
@@ -120,12 +130,13 @@ export function assessTrustPolicyArming(policy, schemas, channel, { consumesCata
|
|
|
120
130
|
policy.policies.length === 0 &&
|
|
121
131
|
policy.trustedCosignBinaryDigests.length === 0 &&
|
|
122
132
|
policy.trustedGitBinaryDigests.length === 0;
|
|
123
|
-
// The channel
|
|
124
|
-
// binary pins are
|
|
125
|
-
|
|
133
|
+
// The channel has no stage row while the policy has rows (another channel's,
|
|
134
|
+
// or bundle signers): those rows and the binary pins are not half of this
|
|
135
|
+
// channel's arming.
|
|
136
|
+
const noStageRow = stageRows.length === 0 && policy.policies.length > 0;
|
|
126
137
|
const state = armed
|
|
127
138
|
? TrustPolicyArmingState.ARMED
|
|
128
|
-
: bootstrap ||
|
|
139
|
+
: bootstrap || noStageRow
|
|
129
140
|
? TrustPolicyArmingState.UNARMED
|
|
130
141
|
: TrustPolicyArmingState.PARTIAL;
|
|
131
142
|
|
|
@@ -434,6 +434,8 @@ export async function verifyDistributionPlatforms({
|
|
|
434
434
|
return {
|
|
435
435
|
subjectKind: subject.kind,
|
|
436
436
|
subjectId: subject.id,
|
|
437
|
+
subjectImageRef: subject.imageRef,
|
|
438
|
+
subjectContentHash: subject.contentHash,
|
|
437
439
|
coordinate,
|
|
438
440
|
manifestDigest: published.indexDigest,
|
|
439
441
|
multiArchIndex: published.isIndex,
|
|
@@ -118,6 +118,9 @@ export const FirstPartyImageOrigin = Object.freeze({
|
|
|
118
118
|
|
|
119
119
|
const IMAGE_ORIGINS = Object.values(FirstPartyImageOrigin);
|
|
120
120
|
|
|
121
|
+
/** An issue key, `<PROJECT>-<number>`: the ticket that removes a member's current-resolution exception. */
|
|
122
|
+
const TICKET = /^[A-Z][A-Z0-9]*-[1-9][0-9]*$/;
|
|
123
|
+
|
|
121
124
|
/**
|
|
122
125
|
* `firstPartySources`: every first-party repository, keyed by its source
|
|
123
126
|
* checkout handle, with its declared source id (the identity a runtime subject
|
|
@@ -126,6 +129,9 @@ const IMAGE_ORIGINS = Object.values(FirstPartyImageOrigin);
|
|
|
126
129
|
* is checked canonical and under `sourceRepositoryUrlPrefix` with the whole
|
|
127
130
|
* config (edition-config.mjs). `imageOrigin` (FirstPartyImageOrigin) is optional
|
|
128
131
|
* here and has no default: `--images-from` refuses a source that does not declare it.
|
|
132
|
+
* `currentResolution` (a ticket, `<PROJECT>-<number>`) is optional and has no default: a member that declares it keeps
|
|
133
|
+
* the CURRENT develop resolution instead of a signed build record until that ticket removes it
|
|
134
|
+
* (develop-current-resolution.mjs); a member without it never does.
|
|
129
135
|
*/
|
|
130
136
|
export function parseFirstPartySources(value) {
|
|
131
137
|
const where = '"firstPartySources"';
|
|
@@ -136,17 +142,21 @@ export function parseFirstPartySources(value) {
|
|
|
136
142
|
repoHandle(repo, `${where} key`);
|
|
137
143
|
if (typeof entry === 'string') {throw new RetiredFirstPartySourceShapeError(repo, entry);}
|
|
138
144
|
const at = `${where}["${repo}"]`;
|
|
139
|
-
exactObject(entry, at, ['sourceId', 'repositoryUrl'], ['imageOrigin']);
|
|
145
|
+
exactObject(entry, at, ['sourceId', 'repositoryUrl'], ['imageOrigin', 'currentResolution']);
|
|
140
146
|
if (typeof entry.sourceId !== 'string' || !SOURCE_ID.test(entry.sourceId)) {
|
|
141
147
|
fail(`${at}.sourceId`, `must be a lowercase kebab-case source id; got ${JSON.stringify(entry.sourceId)}`);
|
|
142
148
|
}
|
|
143
149
|
if (Object.hasOwn(entry, 'imageOrigin') && !IMAGE_ORIGINS.includes(entry.imageOrigin)) {
|
|
144
150
|
fail(`${at}.imageOrigin`, `must be one of ${IMAGE_ORIGINS.join(', ')}; got ${JSON.stringify(entry.imageOrigin)}`);
|
|
145
151
|
}
|
|
152
|
+
if (Object.hasOwn(entry, 'currentResolution') && (typeof entry.currentResolution !== 'string' || !TICKET.test(entry.currentResolution))) {
|
|
153
|
+
fail(`${at}.currentResolution`, `must be the ticket that removes the exception, <PROJECT>-<number>; got ${JSON.stringify(entry.currentResolution)}`);
|
|
154
|
+
}
|
|
146
155
|
return [repo, Object.freeze({
|
|
147
156
|
sourceId: entry.sourceId,
|
|
148
157
|
repositoryUrl: text(entry.repositoryUrl, `${at}.repositoryUrl`),
|
|
149
158
|
...(Object.hasOwn(entry, 'imageOrigin') ? { imageOrigin: entry.imageOrigin } : {}),
|
|
159
|
+
...(Object.hasOwn(entry, 'currentResolution') ? { currentResolution: entry.currentResolution } : {}),
|
|
150
160
|
})];
|
|
151
161
|
});
|
|
152
162
|
uniqueBy(entries, `${where} sourceId`, ([, entry]) => entry.sourceId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/xema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/xema-dev/xema-cli.git",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@xemahq/distribution-source-hash": "^0.2.2",
|
|
62
62
|
"@xemahq/dsl": "^0.28.0",
|
|
63
63
|
"@xemahq/identity-client": "^0.36.0",
|
|
64
|
-
"@xemahq/contracts": "^0.
|
|
64
|
+
"@xemahq/contracts": "^0.119.0",
|
|
65
65
|
"@xemahq/kernel-state-runtime": "^0.8.0",
|
|
66
66
|
"@xemahq/license-verifier": "^0.2.0",
|
|
67
67
|
"@xemahq/release-policy": "^17.6.0",
|
|
@@ -100,6 +100,6 @@
|
|
|
100
100
|
"typecheck": "tsc --noEmit"
|
|
101
101
|
},
|
|
102
102
|
"xemaRelease": {
|
|
103
|
-
"sourceDigest": "sha256:
|
|
103
|
+
"sourceDigest": "sha256:80acaaf576705b4a71ecd5e67794fea62c6fc834652a2830bbd03a2d855d2409"
|
|
104
104
|
}
|
|
105
105
|
}
|