@xemahq/edition-tooling 0.0.0-stage → 0.1.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/LICENSE +175 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/edition.d.ts +4 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +58 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
- package/dist/lib/exit-codes.d.ts +6 -0
- package/dist/lib/exit-codes.d.ts.map +1 -0
- package/dist/lib/exit-codes.js +21 -0
- package/dist/lib/exit-codes.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +10 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +49 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +70 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +166 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/edition/README.md +291 -0
- package/edition/adapter-bundled-versions.mjs +128 -0
- package/edition/adapter-host-versions.mjs +76 -0
- package/edition/assemble-producer-input.mjs +1423 -0
- package/edition/assemble-third-party-input.mjs +348 -0
- package/edition/authored-source-check.mjs +141 -0
- package/edition/authority-binding.mjs +23 -0
- package/edition/authorization-base.mjs +357 -0
- package/edition/biome-content.mjs +327 -0
- package/edition/build-biome-index.mjs +180 -0
- package/edition/build-ledger-completeness.mjs +67 -0
- package/edition/build-ledger.mjs +107 -0
- package/edition/build-platform-service-index.mjs +292 -0
- package/edition/build-runtime-image-source-index.mjs +1197 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/candidate-edition.mjs +735 -0
- package/edition/canonical-evidence-json.mjs +82 -0
- package/edition/canonical-repository-url.mjs +61 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-evidence.mjs +228 -0
- package/edition/catalogue-fetch.mjs +124 -0
- package/edition/catalogue-lock-field.mjs +41 -0
- package/edition/catalogue-pin.mjs +71 -0
- package/edition/catalogue.mjs +237 -0
- package/edition/chart-set-source-commit.mjs +97 -0
- package/edition/check-committed-image-evidence.mjs +412 -0
- package/edition/checkout-indexed-source-repositories.mjs +283 -0
- package/edition/collected-refusals.mjs +59 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/contract-authority.mjs +122 -0
- package/edition/cosign-certificate-constraints.mjs +50 -0
- package/edition/cross-repo-build-inputs.mjs +158 -0
- package/edition/customer-definition.mjs +72 -0
- package/edition/customer-edition-verbs.mjs +610 -0
- package/edition/customer-flags.mjs +15 -0
- package/edition/definition-checkout.mjs +96 -0
- package/edition/develop-build-record.mjs +122 -0
- package/edition/develop-catalogue-free-resolve.mjs +282 -0
- package/edition/develop-catalogue-free-vendor.mjs +118 -0
- package/edition/develop-channel-evidence.mjs +23 -0
- package/edition/develop-channel-record.mjs +49 -0
- package/edition/develop-channel-satellites.mjs +65 -0
- package/edition/develop-chart-roster.mjs +96 -0
- package/edition/develop-chart-set.mjs +147 -0
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-invariants.mjs +142 -0
- package/edition/develop-edition-resolve.mjs +250 -0
- package/edition/develop-edition.mjs +332 -0
- package/edition/develop-engine-image.mjs +97 -0
- package/edition/develop-freeze.mjs +304 -0
- package/edition/develop-git-bundles.mjs +296 -0
- package/edition/develop-graph.mjs +34 -0
- package/edition/develop-image-coordinate.mjs +85 -0
- package/edition/develop-image-probe.mjs +122 -0
- package/edition/develop-image-record.mjs +260 -0
- package/edition/develop-resolve-report.mjs +159 -0
- package/edition/develop-select-records.mjs +90 -0
- package/edition/develop-selection-line.mjs +29 -0
- package/edition/develop-selection-pins.mjs +36 -0
- package/edition/develop-snapshot-inputs.mjs +112 -0
- package/edition/develop-store-root.mjs +20 -0
- package/edition/dispatched-runtime-components.mjs +172 -0
- package/edition/dist-check.mjs +1174 -0
- package/edition/distribution-evidence-set.mjs +1064 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +529 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-ceiling.mjs +182 -0
- package/edition/edition-channel.mjs +27 -0
- package/edition/edition-chart-set.mjs +104 -0
- package/edition/edition-config.mjs +374 -0
- package/edition/edition-constants.mjs +110 -0
- package/edition/edition-contract-parse.mjs +179 -0
- package/edition/edition-contract.json +100 -0
- package/edition/edition-image-seal.mjs +115 -0
- package/edition/edition-kind.mjs +10 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +78 -0
- package/edition/edition-pin-verbs.mjs +183 -0
- package/edition/edition-pin.mjs +17 -0
- package/edition/edition-pinned-inputs.mjs +116 -0
- package/edition/edition-protocol.mjs +285 -0
- package/edition/edition-publisher.mjs +21 -0
- package/edition/edition-seal.mjs +257 -0
- package/edition/edition-source-pins.mjs +211 -0
- package/edition/edition-unpin.mjs +16 -0
- package/edition/edition-workspace.mjs +205 -0
- package/edition/evidence-checkout.mjs +439 -0
- package/edition/evidence-grammars.mjs +18 -0
- package/edition/evidence-input-mode.mjs +136 -0
- package/edition/evidence-trust-pin.mjs +392 -0
- package/edition/evidence-trust-policy.mjs +374 -0
- package/edition/external-source-walk.mjs +486 -0
- package/edition/first-party-source-ids.mjs +62 -0
- package/edition/fleet-coordinate-finality.mjs +363 -0
- package/edition/fulcio-certificate-extensions.mjs +278 -0
- package/edition/git-json.mjs +54 -0
- package/edition/git-source-auth.mjs +131 -0
- package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1328 -0
- package/edition/image-lock-service-rows.mjs +26 -0
- package/edition/image-subject-identity.mjs +19 -0
- package/edition/images-from-record.mjs +216 -0
- package/edition/indexes-at-pins.mjs +13 -0
- package/edition/infra-images-lock.mjs +105 -0
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-contract-authority.mjs +526 -0
- package/edition/kernel-contract-source-path-set.mjs +96 -0
- package/edition/kernel-package.mjs +237 -0
- package/edition/kernel-pin-source.mjs +63 -0
- package/edition/kernel-protocol-choice.mjs +239 -0
- package/edition/kernel-signer-kind.mjs +23 -0
- package/edition/lib.mjs +3322 -0
- package/edition/locked-bundle-signer.mjs +297 -0
- package/edition/map-with-concurrency.mjs +19 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +421 -0
- package/edition/oci-image-source-evidence.mjs +377 -0
- package/edition/operation-grant-admission.mjs +285 -0
- package/edition/operation-requirements.mjs +282 -0
- package/edition/oras-process.mjs +35 -0
- package/edition/oras.mjs +664 -0
- package/edition/pin-source.mjs +242 -0
- package/edition/pinned-cosign.mjs +86 -0
- package/edition/plan-image-evidence-refresh.mjs +428 -0
- package/edition/platform-lock-readability.mjs +903 -0
- package/edition/platform-service-class.mjs +85 -0
- package/edition/platform-service-ownership.mjs +91 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/previous-image-lock.mjs +25 -0
- package/edition/produce-distribution-completeness-evidence.mjs +317 -0
- package/edition/produce-edition-catalogue-evidence.mjs +252 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
- package/edition/produce-image-source-evidence.mjs +788 -0
- package/edition/producer-evidence-bundle.mjs +201 -0
- package/edition/producer-evidence-index-file.mjs +4 -0
- package/edition/record-refused.mjs +11 -0
- package/edition/registry-client-factory.mjs +21 -0
- package/edition/registry-credentials.mjs +225 -0
- package/edition/release-catalogue.mjs +48 -0
- package/edition/release-channel-guard.mjs +108 -0
- package/edition/release-edition-artifact.mjs +152 -0
- package/edition/release-edition.mjs +54 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +109 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +376 -0
- package/edition/resolve-customer-edition.mjs +374 -0
- package/edition/resolve-distribution.mjs +2642 -0
- package/edition/resolve-edition.mjs +192 -0
- package/edition/resolve-images.mjs +2344 -0
- package/edition/resolve-infra-images.mjs +88 -0
- package/edition/seal-image-digests.mjs +954 -0
- package/edition/sealed-distribution-lock.mjs +22 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/shallow-checkout.mjs +18 -0
- package/edition/sigstore-bundle-media-type.mjs +4 -0
- package/edition/source-authority.mjs +713 -0
- package/edition/source-closure.mjs +115 -0
- package/edition/store-publisher-trust.mjs +40 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/third-party-admission.mjs +118 -0
- package/edition/third-party-bundle-registry.mjs +349 -0
- package/edition/third-party-certificate-extensions.mjs +58 -0
- package/edition/third-party-image-pins.mjs +41 -0
- package/edition/third-party-pins-trusted.mjs +24 -0
- package/edition/third-party-producer-evidence.mjs +76 -0
- package/edition/third-party-source-check.mjs +28 -0
- package/edition/third-party-source-context.mjs +41 -0
- package/edition/third-party-source-evidence.mjs +326 -0
- package/edition/third-party-source-flags.mjs +148 -0
- package/edition/third-party-source-pins.mjs +270 -0
- package/edition/third-party-source-uniqueness.mjs +54 -0
- package/edition/third-party-source-walk.mjs +118 -0
- package/edition/third-party-source-writer.mjs +228 -0
- package/edition/third-party-subjects.mjs +128 -0
- package/edition/trust-policy-arming-verb.mjs +60 -0
- package/edition/trust-policy-arming.mjs +144 -0
- package/edition/trust-policy-image-ownership.mjs +85 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-digest-pin.mjs +17 -0
- package/edition/verifier-files.mjs +8 -0
- package/edition/verifier-kernel-schemas.mjs +22 -0
- package/edition/verifier-provenance.mjs +64 -0
- package/edition/verify-catalogue-evidence.mjs +247 -0
- package/edition/verify-image-evidence.mjs +493 -0
- package/edition/verify-image-platforms.mjs +523 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +287 -0
- package/package.json +83 -4
- package/README.md +0 -3
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `xema-edition evidence oci <attach-plan|validate|backfill-plan>`: the OCI
|
|
3
|
+
// form of image source evidence, for a caller that runs `oras` itself.
|
|
4
|
+
//
|
|
5
|
+
// attach-plan --evidence-directory <abs dir> [--input <file>]
|
|
6
|
+
// validate --evidence-directory <abs dir> --manifest <file> --subject-digest <digest>
|
|
7
|
+
// [--input <file> --cosign <abs path> [--now <epoch-ms>]]
|
|
8
|
+
// backfill-plan --distribution <id>… --producer-repository-root <abs dir>…
|
|
9
|
+
//
|
|
10
|
+
// attach-plan and validate take --data-root <dir> --kernel <dir> (vendor mode) or --contract-authority <file>
|
|
11
|
+
// (public-input mode: a publisher's run with the kernel this CLI bundles); backfill-plan is vendor mode only.
|
|
12
|
+
//
|
|
13
|
+
// The library reads and decides; it never writes to a registry. `attach-plan`
|
|
14
|
+
// validates a produced evidence directory and prints the exact `oras attach`
|
|
15
|
+
// operands; `validate` holds a pulled artifact to its manifest (and, with
|
|
16
|
+
// `--input`, to the producer input, the signer and a remaining lifetime);
|
|
17
|
+
// `backfill-plan` joins the committed evidence of the producing repositories
|
|
18
|
+
// to the locked subjects and to what each registry already serves.
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
22
|
+
|
|
23
|
+
import { REFERRERS_DISTRIBUTION_SPEC } from '@xemahq/oci-closure';
|
|
24
|
+
|
|
25
|
+
import { MIN_REMAINING_TTL_SECONDS } from './check-committed-image-evidence.mjs';
|
|
26
|
+
import {
|
|
27
|
+
expectedProducerSource,
|
|
28
|
+
indexCommittedProducerEvidence,
|
|
29
|
+
parseEvidenceCliFlags,
|
|
30
|
+
} from './distribution-evidence-set.mjs';
|
|
31
|
+
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
32
|
+
import {
|
|
33
|
+
WorkspaceRoot,
|
|
34
|
+
configureEditionWorkspaceFromArgv,
|
|
35
|
+
dataRoot,
|
|
36
|
+
editionsRoot,
|
|
37
|
+
isEntryModule,
|
|
38
|
+
} from './edition-workspace.mjs';
|
|
39
|
+
import { SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
|
|
40
|
+
import { EvidenceInputMode, configureEvidenceInputMode } from './evidence-input-mode.mjs';
|
|
41
|
+
import { canonicalJson, loadValidatedImageLocks } from './image-evidence.mjs';
|
|
42
|
+
import { compareStrings, loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
|
|
43
|
+
import {
|
|
44
|
+
IMAGE_SOURCE_EVIDENCE_ARTIFACT_TYPE,
|
|
45
|
+
createRegistryClientResolver,
|
|
46
|
+
resolveImageSourceEvidenceReferrer,
|
|
47
|
+
validateMaterializedImageSourceEvidence,
|
|
48
|
+
writeInvalidReferrerReport,
|
|
49
|
+
} from './oci-image-source-evidence.mjs';
|
|
50
|
+
import { assertProducerInput, buildImageSourceStatements, cosignVerifyAttestationArgs, createCosignVerifier, readPinnedContractAuthority } from './produce-image-source-evidence.mjs';
|
|
51
|
+
import {
|
|
52
|
+
producerEvidenceLayerFiles,
|
|
53
|
+
producerStatementMatchesInput,
|
|
54
|
+
readProducerEvidenceDirectory,
|
|
55
|
+
} from './producer-evidence-bundle.mjs';
|
|
56
|
+
import { splitImageRef } from './registry-credentials.mjs';
|
|
57
|
+
|
|
58
|
+
const OCI_CREATED_ANNOTATION = 'org.opencontainers.image.created';
|
|
59
|
+
|
|
60
|
+
/** What `backfill-plan` reports for one locked subject. */
|
|
61
|
+
export const OciBackfillStatus = Object.freeze({
|
|
62
|
+
MissingGit: 'missing-git',
|
|
63
|
+
Present: 'present',
|
|
64
|
+
Missing: 'missing',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const USAGE = 'usage: xema-edition evidence oci <attach-plan|validate|backfill-plan> [options]';
|
|
68
|
+
|
|
69
|
+
function assertAbsolute(value, label) {
|
|
70
|
+
if (typeof value !== 'string' || !isAbsolute(value)) {
|
|
71
|
+
throw new Error(`${label} must be an absolute path`);
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One evidence artifact describes one image: one repository, one manifest digest. */
|
|
77
|
+
function inputSubject(input, expectedContractAuthority) {
|
|
78
|
+
assertProducerInput(input, expectedContractAuthority);
|
|
79
|
+
const repositories = new Set(input.subjects.map((subject) => subject.imageRepository));
|
|
80
|
+
const digests = new Set(input.subjects.map((subject) => subject.manifestDigest));
|
|
81
|
+
if (repositories.size !== 1 || digests.size !== 1) {
|
|
82
|
+
throw new Error('one OCI evidence artifact requires exactly one image repository and manifest digest');
|
|
83
|
+
}
|
|
84
|
+
return { imageRepository: [...repositories][0], subjectDigest: [...digests][0] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function expectedStatements(input, schemas, expectedContractAuthority) {
|
|
88
|
+
return new Map(
|
|
89
|
+
buildImageSourceStatements(input, schemas, expectedContractAuthority).map((statement) => [
|
|
90
|
+
`${statement.predicate.source.kind}:${statement.predicate.source.id}`,
|
|
91
|
+
statement,
|
|
92
|
+
]),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The `oras attach` operands of one produced evidence directory. With `input`
|
|
98
|
+
* every record must be the statement that input builds; without it the image
|
|
99
|
+
* is the one every record names.
|
|
100
|
+
*/
|
|
101
|
+
export function planOciEvidenceAttach({ evidenceDirectory, input, schemas, expectedContractAuthority }) {
|
|
102
|
+
assertAbsolute(evidenceDirectory, 'the evidence directory');
|
|
103
|
+
const evidence = readProducerEvidenceDirectory({ directory: evidenceDirectory, schemas });
|
|
104
|
+
if (evidence.records.length === 0) {
|
|
105
|
+
throw new Error(`${evidenceDirectory} holds no producer evidence`);
|
|
106
|
+
}
|
|
107
|
+
let image;
|
|
108
|
+
if (input === undefined) {
|
|
109
|
+
const [{ artifact }] = evidence.records.map((record) => record.statement.predicate);
|
|
110
|
+
image = { imageRepository: artifact.imageRepository, subjectDigest: artifact.manifestDigest };
|
|
111
|
+
} else {
|
|
112
|
+
image = inputSubject(input, expectedContractAuthority);
|
|
113
|
+
const expected = expectedStatements(input, schemas, expectedContractAuthority);
|
|
114
|
+
for (const record of evidence.records) {
|
|
115
|
+
const expectedStatement = expected.get(record.key);
|
|
116
|
+
if (expectedStatement === undefined || !producerStatementMatchesInput(record.statement, expectedStatement)) {
|
|
117
|
+
throw new Error(`${record.key} produced evidence is not bound to the input`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const record of evidence.records) {
|
|
122
|
+
const { artifact } = record.statement.predicate;
|
|
123
|
+
if (artifact.imageRepository !== image.imageRepository || artifact.manifestDigest !== image.subjectDigest) {
|
|
124
|
+
throw new Error(`${record.key} evidence is not bound to ${image.imageRepository}@${image.subjectDigest}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const created = evidence.records[0].statement.predicate.producerRun.issuedAt;
|
|
128
|
+
if (evidence.records.some((record) => record.statement.predicate.producerRun.issuedAt !== created)) {
|
|
129
|
+
throw new Error('one OCI evidence artifact cannot carry multiple issuance times');
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
schemaVersion: 1,
|
|
133
|
+
kind: 'oci-attach-plan',
|
|
134
|
+
evidenceDirectory,
|
|
135
|
+
...image,
|
|
136
|
+
subject: `${image.imageRepository}@${image.subjectDigest}`,
|
|
137
|
+
artifactType: IMAGE_SOURCE_EVIDENCE_ARTIFACT_TYPE,
|
|
138
|
+
distributionSpec: REFERRERS_DISTRIBUTION_SPEC,
|
|
139
|
+
annotations: { [OCI_CREATED_ANNOTATION]: created },
|
|
140
|
+
files: producerEvidenceLayerFiles(evidence.records),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Hold a pulled evidence artifact to its manifest. With `input` it also
|
|
146
|
+
* answers whether the artifact can be reused instead of producing again: the
|
|
147
|
+
* statements are the ones the input builds, every bundle verifies against the
|
|
148
|
+
* declared signer, and enough lifetime remains.
|
|
149
|
+
*/
|
|
150
|
+
export function assessMaterializedOciEvidence({
|
|
151
|
+
evidenceDirectory,
|
|
152
|
+
manifest,
|
|
153
|
+
subjectDigest,
|
|
154
|
+
schemas,
|
|
155
|
+
input,
|
|
156
|
+
expectedContractAuthority,
|
|
157
|
+
runCosign,
|
|
158
|
+
now = Date.now(),
|
|
159
|
+
}) {
|
|
160
|
+
assertAbsolute(evidenceDirectory, 'the evidence directory');
|
|
161
|
+
if (!SHA256_DIGEST_PATTERN.test(subjectDigest ?? '')) {
|
|
162
|
+
throw new Error('the subject digest must be sha256:<64 hex>');
|
|
163
|
+
}
|
|
164
|
+
const evidence = validateMaterializedImageSourceEvidence({
|
|
165
|
+
directory: evidenceDirectory,
|
|
166
|
+
manifest,
|
|
167
|
+
subjectDigest,
|
|
168
|
+
schemas,
|
|
169
|
+
});
|
|
170
|
+
const base = {
|
|
171
|
+
schemaVersion: 1,
|
|
172
|
+
kind: 'oci-evidence',
|
|
173
|
+
subjectDigest,
|
|
174
|
+
created: evidence.created,
|
|
175
|
+
subjects: evidence.records.map((record) => record.key),
|
|
176
|
+
};
|
|
177
|
+
if (input === undefined) {
|
|
178
|
+
return { ...base, matchesInput: null, remainingSeconds: null, reusable: null };
|
|
179
|
+
}
|
|
180
|
+
if (typeof runCosign !== 'function') {
|
|
181
|
+
throw new Error('a cosign verifier is required to assess evidence against an input');
|
|
182
|
+
}
|
|
183
|
+
const image = inputSubject(input, expectedContractAuthority);
|
|
184
|
+
if (image.subjectDigest !== subjectDigest) {
|
|
185
|
+
throw new Error(`the input describes ${image.subjectDigest}, not ${subjectDigest}`);
|
|
186
|
+
}
|
|
187
|
+
const expected = expectedStatements(input, schemas, expectedContractAuthority);
|
|
188
|
+
let matchesInput = evidence.records.length === expected.size;
|
|
189
|
+
let oldestExpiry = Number.POSITIVE_INFINITY;
|
|
190
|
+
for (const record of evidence.records) {
|
|
191
|
+
const expectedStatement = expected.get(record.key);
|
|
192
|
+
matchesInput &&=
|
|
193
|
+
expectedStatement !== undefined && producerStatementMatchesInput(record.statement, expectedStatement);
|
|
194
|
+
oldestExpiry = Math.min(oldestExpiry, Date.parse(record.statement.predicate.producerRun.expiresAt));
|
|
195
|
+
// A bundle that does not verify is a refusal, never "not reusable".
|
|
196
|
+
runCosign(
|
|
197
|
+
cosignVerifyAttestationArgs({
|
|
198
|
+
bundlePath: join(evidenceDirectory, record.descriptor.bundlePath),
|
|
199
|
+
signer: input.signer,
|
|
200
|
+
certificateWorkflowSha: record.statement.predicate.producerRun.producerWorkflowSha,
|
|
201
|
+
predicateType: record.statement.predicateType,
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const remainingSeconds = Math.floor((oldestExpiry - now) / 1000);
|
|
206
|
+
return {
|
|
207
|
+
...base,
|
|
208
|
+
matchesInput,
|
|
209
|
+
remainingSeconds,
|
|
210
|
+
reusable: matchesInput && remainingSeconds >= MIN_REMAINING_TTL_SECONDS,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* For every locked subject of the named editions: its committed evidence and
|
|
216
|
+
* whether the registry already serves it as a referrer. A `missing` item
|
|
217
|
+
* carries the attach plan the caller runs.
|
|
218
|
+
*/
|
|
219
|
+
export async function planOciEvidenceBackfill({
|
|
220
|
+
distributionIds,
|
|
221
|
+
dataRoot: data,
|
|
222
|
+
editionsRoot: editions,
|
|
223
|
+
producerRepositoryRoots,
|
|
224
|
+
schemas,
|
|
225
|
+
expectedContractAuthority,
|
|
226
|
+
registryClientFactory,
|
|
227
|
+
}) {
|
|
228
|
+
const gitIndex = indexCommittedProducerEvidence({
|
|
229
|
+
producerRepositoryRoots,
|
|
230
|
+
schemas,
|
|
231
|
+
onDefect: ({ directory, reason }) => {
|
|
232
|
+
throw new Error(`Git evidence at ${directory} is unusable: ${reason}`);
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
const clientFor = createRegistryClientResolver({ dataRoot: data, factory: registryClientFactory });
|
|
236
|
+
const work = new Map();
|
|
237
|
+
const missingGit = new Map();
|
|
238
|
+
for (const distributionId of distributionIds) {
|
|
239
|
+
assertCanonicalDistributionId(distributionId);
|
|
240
|
+
const { imageLock, repositoryPins, thirdPartySources } = await loadValidatedImageLocks(distributionId, data, schemas, {
|
|
241
|
+
editionsRoot: editions,
|
|
242
|
+
});
|
|
243
|
+
for (const subject of imageLock.subjects) {
|
|
244
|
+
if (thirdPartySources.has(subject.repo)) {
|
|
245
|
+
// Evidence is attached to the registry image of a producer this fleet owns. A third-party image belongs to its
|
|
246
|
+
// publisher (its contentHash is not a tag here): its evidence is the data root's committed evidence, bound by its pin.
|
|
247
|
+
process.stderr.write(`[oci-evidence] ${distributionId} ${subject.kind}:${subject.id} is a third-party subject (${subject.repo}): no OCI evidence is attached to a publisher's image\n`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const source = expectedProducerSource({
|
|
251
|
+
subject,
|
|
252
|
+
repositoryPin: repositoryPins.get(subject.repo),
|
|
253
|
+
sourceHashAlgorithm: imageLock.sourceHashAlgorithm,
|
|
254
|
+
});
|
|
255
|
+
const matches = gitIndex.get(canonicalJson(source)) ?? [];
|
|
256
|
+
if (matches.length > 1) {
|
|
257
|
+
throw new Error(`current ${subject.kind}:${subject.id} requires exactly one Git bundle; found ${matches.length}`);
|
|
258
|
+
}
|
|
259
|
+
if (matches.length === 0) {
|
|
260
|
+
missingGit.set(`${subject.kind}:${subject.id}\0${subject.sourceCommit}`, {
|
|
261
|
+
status: OciBackfillStatus.MissingGit,
|
|
262
|
+
subjectKind: subject.kind,
|
|
263
|
+
subjectId: subject.id,
|
|
264
|
+
imageRepository: subject.imageRef,
|
|
265
|
+
subjectDigest: null,
|
|
266
|
+
artifactDigest: null,
|
|
267
|
+
});
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const [match] = matches;
|
|
271
|
+
const subjectDigest = match.statement.predicate.artifact.manifestDigest;
|
|
272
|
+
const key = `${subject.imageRef}@${subjectDigest}`;
|
|
273
|
+
const incumbent = work.get(key);
|
|
274
|
+
if (incumbent && incumbent.directory !== match.directory) {
|
|
275
|
+
throw new Error(`${key} resolves to multiple Git evidence directories`);
|
|
276
|
+
}
|
|
277
|
+
work.set(key, { subject, directory: match.directory, imageRepository: subject.imageRef, subjectDigest });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const items = [...missingGit.values()];
|
|
281
|
+
for (const item of [...work.values()].sort((left, right) =>
|
|
282
|
+
compareStrings(left.imageRepository, right.imageRepository),
|
|
283
|
+
)) {
|
|
284
|
+
const { authority, repository } = splitImageRef(item.imageRepository);
|
|
285
|
+
const client = clientFor(authority);
|
|
286
|
+
const served = await client.getManifestByTag(repository, item.subject.contentHash);
|
|
287
|
+
if (served.digest !== item.subjectDigest) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`${item.imageRepository}:${item.subject.contentHash} serves ${served.digest}, not signed ${item.subjectDigest}`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const existing = await resolveImageSourceEvidenceReferrer({
|
|
293
|
+
client,
|
|
294
|
+
imageRepository: item.imageRepository,
|
|
295
|
+
subjectDigest: item.subjectDigest,
|
|
296
|
+
schemas,
|
|
297
|
+
expectedContractAuthority,
|
|
298
|
+
authorityBound: true, // first-party subject: bound to the contract authority on every channel (authority-binding.mjs)
|
|
299
|
+
onInvalidReferrer: writeInvalidReferrerReport,
|
|
300
|
+
});
|
|
301
|
+
const reported = {
|
|
302
|
+
subjectKind: item.subject.kind,
|
|
303
|
+
subjectId: item.subject.id,
|
|
304
|
+
imageRepository: item.imageRepository,
|
|
305
|
+
subjectDigest: item.subjectDigest,
|
|
306
|
+
};
|
|
307
|
+
items.push(
|
|
308
|
+
existing === null
|
|
309
|
+
? {
|
|
310
|
+
status: OciBackfillStatus.Missing,
|
|
311
|
+
...reported,
|
|
312
|
+
artifactDigest: null,
|
|
313
|
+
attach: planOciEvidenceAttach({ evidenceDirectory: item.directory, schemas }),
|
|
314
|
+
}
|
|
315
|
+
: { status: OciBackfillStatus.Present, ...reported, artifactDigest: existing.artifactDigest },
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return { schemaVersion: 1, kind: 'oci-backfill-plan', items };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const readJsonFile = (path, label) => {
|
|
322
|
+
try {
|
|
323
|
+
return JSON.parse(readFileSync(resolve(path), 'utf8'));
|
|
324
|
+
} catch (error) {
|
|
325
|
+
throw new Error(`${label} ${path} is not readable JSON: ${error.message}`);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* `attach-plan` and `validate` in one of the two input modes: the vendor's pinned authority and kernel checkout, or
|
|
331
|
+
* a publisher's `--contract-authority` file with the kernel this CLI bundles.
|
|
332
|
+
*/
|
|
333
|
+
async function ociInputMode(argv, operation) {
|
|
334
|
+
const mode = await configureEvidenceInputMode(argv, { verb: `oci ${operation}` });
|
|
335
|
+
if (mode.mode === EvidenceInputMode.PublicInput) {
|
|
336
|
+
return { rest: mode.rest, schemas: mode.schemas, expectedContractAuthority: mode.contractAuthority };
|
|
337
|
+
}
|
|
338
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
339
|
+
return { rest: mode.rest, schemas, expectedContractAuthority: readPinnedContractAuthority({ dataRoot: dataRoot(), schemas }) };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function main() {
|
|
343
|
+
const [operation, ...argv] = process.argv.slice(2);
|
|
344
|
+
if (operation === 'attach-plan') {
|
|
345
|
+
const { rest, schemas, expectedContractAuthority } = await ociInputMode(argv, operation);
|
|
346
|
+
const flags = parseEvidenceCliFlags(rest, { single: ['evidence-directory', 'input'] });
|
|
347
|
+
const input = flags.optional('input');
|
|
348
|
+
process.stdout.write(
|
|
349
|
+
serializeJson(
|
|
350
|
+
planOciEvidenceAttach({
|
|
351
|
+
evidenceDirectory: flags.require('evidence-directory'),
|
|
352
|
+
input: input === undefined ? undefined : readJsonFile(input, 'producer input'),
|
|
353
|
+
schemas,
|
|
354
|
+
expectedContractAuthority,
|
|
355
|
+
}),
|
|
356
|
+
),
|
|
357
|
+
);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (operation === 'validate') {
|
|
361
|
+
const { rest, schemas, expectedContractAuthority } = await ociInputMode(argv, operation);
|
|
362
|
+
const flags = parseEvidenceCliFlags(rest, {
|
|
363
|
+
single: ['evidence-directory', 'manifest', 'subject-digest', 'input', 'cosign', 'now'],
|
|
364
|
+
});
|
|
365
|
+
const input = flags.optional('input');
|
|
366
|
+
const cosign = flags.optional('cosign');
|
|
367
|
+
const now = flags.optional('now');
|
|
368
|
+
if ((input === undefined) !== (cosign === undefined)) {
|
|
369
|
+
throw new Error('--input and --cosign are given together or not at all');
|
|
370
|
+
}
|
|
371
|
+
if (now !== undefined && !/^\d+$/.test(now)) {
|
|
372
|
+
throw new Error('--now must be epoch milliseconds');
|
|
373
|
+
}
|
|
374
|
+
process.stdout.write(
|
|
375
|
+
serializeJson(
|
|
376
|
+
assessMaterializedOciEvidence({
|
|
377
|
+
evidenceDirectory: flags.require('evidence-directory'),
|
|
378
|
+
manifest: readJsonFile(flags.require('manifest'), 'OCI manifest'),
|
|
379
|
+
subjectDigest: flags.require('subject-digest'),
|
|
380
|
+
schemas,
|
|
381
|
+
input: input === undefined ? undefined : readJsonFile(input, 'producer input'),
|
|
382
|
+
expectedContractAuthority,
|
|
383
|
+
runCosign: cosign === undefined ? undefined : createCosignVerifier(cosign).runCosign,
|
|
384
|
+
now: now === undefined ? Date.now() : Number(now),
|
|
385
|
+
}),
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (operation === 'backfill-plan') {
|
|
391
|
+
const rest = configureEditionWorkspaceFromArgv(argv, [
|
|
392
|
+
WorkspaceRoot.Data,
|
|
393
|
+
WorkspaceRoot.Editions,
|
|
394
|
+
WorkspaceRoot.Kernel,
|
|
395
|
+
]);
|
|
396
|
+
const flags = parseEvidenceCliFlags(rest, { repeated: ['distribution', 'producer-repository-root'] });
|
|
397
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
398
|
+
const plan = await planOciEvidenceBackfill({
|
|
399
|
+
distributionIds: flags.requireMany('distribution'),
|
|
400
|
+
dataRoot: dataRoot(),
|
|
401
|
+
editionsRoot: editionsRoot(),
|
|
402
|
+
producerRepositoryRoots: flags.requireMany('producer-repository-root').map((root) => resolve(root)),
|
|
403
|
+
schemas,
|
|
404
|
+
expectedContractAuthority: readPinnedContractAuthority({ dataRoot: dataRoot(), schemas }),
|
|
405
|
+
});
|
|
406
|
+
process.stdout.write(serializeJson(plan));
|
|
407
|
+
// 2: something is still to be produced or attached.
|
|
408
|
+
if (plan.items.some((item) => item.status !== OciBackfillStatus.Present)) {
|
|
409
|
+
process.exitCode = 2;
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
throw new Error(USAGE);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (isEntryModule(import.meta.url)) {
|
|
417
|
+
main().catch((error) => {
|
|
418
|
+
console.error(`[oci-evidence] ${error.message}`);
|
|
419
|
+
process.exitCode = 1;
|
|
420
|
+
});
|
|
421
|
+
}
|