@xemahq/xema 0.25.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -37
- package/dist/assets/appliance-base/images.lock.json +126 -126
- package/dist/assets/appliance-lean/images.lock.json +123 -123
- package/dist/assets/cloud/images.lock.json +126 -126
- package/dist/assets/core/images.lock.json +120 -120
- package/dist/assets/distribution-assets.manifest.json +15 -16
- package/dist/assets/minimal/images.lock.json +108 -108
- package/dist/assets/oss/images.lock.json +126 -126
- package/dist/assets/starter/images.lock.json +123 -123
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.js +1 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +7 -1
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/edition.d.ts +3 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +46 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/serve.js +1 -0
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/up.js +1 -0
- package/dist/commands/up.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +6 -1
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/dev/infra-preflight.d.ts +3 -2
- package/dist/dev/infra-preflight.d.ts.map +1 -1
- package/dist/dev/infra-preflight.js +18 -8
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +54179 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +2871 -0
- package/dist/runtime/backing-registration.d.ts.map +1 -1
- package/dist/runtime/backing-registration.js +7 -2
- package/dist/runtime/backing-registration.js.map +1 -1
- package/dist/runtime/config/schema.d.ts +1 -0
- package/dist/runtime/config/schema.d.ts.map +1 -1
- package/dist/runtime/custody/local-custody.d.ts +12 -0
- package/dist/runtime/custody/local-custody.d.ts.map +1 -0
- package/dist/runtime/custody/local-custody.js +22 -0
- package/dist/runtime/custody/local-custody.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.js +55 -0
- package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
- package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
- package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-http-client.js +154 -0
- package/dist/runtime/custody/openbao-http-client.js.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.js +48 -0
- package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.js +86 -0
- package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +9 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +36 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +60 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +146 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/dist/runtime/env-seeder.d.ts +3 -0
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +20 -3
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts +3 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +37 -8
- package/dist/runtime/launch.js.map +1 -1
- package/dist/runtime/paths.d.ts +1 -2
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +0 -10
- package/dist/runtime/paths.js.map +1 -1
- package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
- package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
- package/dist/runtime/runner/chart-local-provenance.js +114 -0
- package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
- package/dist/runtime/runner/chart-preflight.d.ts +8 -8
- package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
- package/dist/runtime/runner/chart-preflight.js +30 -69
- package/dist/runtime/runner/chart-preflight.js.map +1 -1
- package/dist/runtime/runner/chart-pull.d.ts +14 -0
- package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
- package/dist/runtime/runner/chart-pull.js +96 -0
- package/dist/runtime/runner/chart-pull.js.map +1 -0
- package/dist/runtime/runner/chart-resolver.d.ts +4 -12
- package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
- package/dist/runtime/runner/chart-resolver.js +16 -62
- package/dist/runtime/runner/chart-resolver.js.map +1 -1
- package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
- package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
- package/dist/runtime/runner/chart-set-lock.js +55 -99
- package/dist/runtime/runner/chart-set-lock.js.map +1 -1
- package/dist/runtime/runner/helm-cli.d.ts +3 -1
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js +9 -7
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/runner/helm-runner.d.ts +4 -4
- package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
- package/dist/runtime/runner/helm-runner.js +18 -16
- package/dist/runtime/runner/helm-runner.js.map +1 -1
- package/dist/runtime/sources/kinds/git.d.ts +1 -2
- package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
- package/dist/runtime/sources/kinds/git.js +8 -5
- package/dist/runtime/sources/kinds/git.js.map +1 -1
- package/dist/runtime/sources/manifest-walk.d.ts +3 -1
- package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
- package/dist/runtime/sources/manifest-walk.js +8 -3
- package/dist/runtime/sources/manifest-walk.js.map +1 -1
- package/dist/runtime/sources/schema.d.ts +3 -0
- package/dist/runtime/sources/schema.d.ts.map +1 -1
- package/dist/runtime/sources/schema.js +6 -1
- package/dist/runtime/sources/schema.js.map +1 -1
- package/dist/runtime/supervisor.d.ts +3 -0
- package/dist/runtime/supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor.js +2 -0
- package/dist/runtime/supervisor.js.map +1 -1
- package/edition/README.md +315 -0
- package/edition/assemble-producer-input.mjs +1251 -0
- package/edition/authorization-base.mjs +347 -0
- package/edition/biome-content.mjs +268 -0
- package/edition/build-biome-index.mjs +175 -0
- package/edition/build-platform-service-index.mjs +288 -0
- package/edition/build-runtime-image-source-index.mjs +821 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-fetch.mjs +64 -0
- package/edition/catalogue.mjs +135 -0
- package/edition/chart-set-source-commit.mjs +92 -0
- package/edition/check-committed-image-evidence.mjs +383 -0
- package/edition/checkout-indexed-source-repositories.mjs +105 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/customer-definition.mjs +183 -0
- package/edition/customer-flags.mjs +7 -0
- package/edition/develop-customer-edition.mjs +99 -0
- package/edition/develop-edition-resolve.mjs +65 -0
- package/edition/develop-edition.mjs +274 -0
- package/edition/develop-freeze.mjs +173 -0
- package/edition/develop-image-coordinate.mjs +54 -0
- package/edition/develop-seal.mjs +77 -0
- package/edition/develop-snapshot-inputs.mjs +104 -0
- package/edition/dist-check.mjs +986 -0
- package/edition/distribution-evidence-set.mjs +987 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +270 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-channel.mjs +6 -0
- package/edition/edition-chart-set.mjs +93 -0
- package/edition/edition-config.mjs +207 -0
- package/edition/edition-constants.mjs +57 -0
- package/edition/edition-contract.json +23 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +69 -0
- package/edition/edition-protocol.mjs +228 -0
- package/edition/edition-workspace.mjs +176 -0
- package/edition/evidence-checkout.mjs +418 -0
- package/edition/evidence-grammars.mjs +16 -0
- package/edition/evidence-trust-pin.mjs +304 -0
- package/edition/evidence-trust-policy.mjs +256 -0
- package/edition/external-source-walk.mjs +463 -0
- package/edition/first-party-source-ids.mjs +59 -0
- package/edition/fleet-coordinate-finality.mjs +350 -0
- package/edition/git-source-auth.mjs +52 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1225 -0
- package/edition/kernel-contract-authority.mjs +443 -0
- package/edition/lib.mjs +3018 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +392 -0
- package/edition/oci-image-source-evidence.mjs +239 -0
- package/edition/oras.mjs +648 -0
- package/edition/pin-source.mjs +227 -0
- package/edition/plan-image-evidence-refresh.mjs +408 -0
- package/edition/platform-lock-readability.mjs +892 -0
- package/edition/platform-service-class.mjs +18 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/produce-distribution-completeness-evidence.mjs +311 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
- package/edition/produce-image-source-evidence.mjs +765 -0
- package/edition/producer-evidence-bundle.mjs +157 -0
- package/edition/registry-credentials.mjs +193 -0
- package/edition/release-catalogue.mjs +39 -0
- package/edition/release-channel-guard.mjs +99 -0
- package/edition/release-edition-artifact.mjs +158 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +101 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +415 -0
- package/edition/resolve-customer-edition.mjs +288 -0
- package/edition/resolve-distribution.mjs +2318 -0
- package/edition/resolve-edition.mjs +186 -0
- package/edition/resolve-images.mjs +1606 -0
- package/edition/seal-image-digests.mjs +855 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/source-authority.mjs +725 -0
- package/edition/source-closure.mjs +116 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/trust-policy-arming.mjs +120 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -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-image-evidence.mjs +430 -0
- package/edition/verify-image-platforms.mjs +495 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +195 -0
- package/examples/06-biome-sources.yaml +1 -0
- package/package.json +18 -7
- package/dist/commands/dist.d.ts +0 -16
- package/dist/commands/dist.d.ts.map +0 -1
- package/dist/commands/dist.js +0 -182
- package/dist/commands/dist.js.map +0 -1
- package/dist/runtime/distribution/available-biome.d.ts +0 -10
- package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
- package/dist/runtime/distribution/available-biome.js +0 -154
- package/dist/runtime/distribution/available-biome.js.map +0 -1
- package/dist/runtime/distribution/compose.d.ts +0 -23
- package/dist/runtime/distribution/compose.d.ts.map +0 -1
- package/dist/runtime/distribution/compose.js +0 -48
- package/dist/runtime/distribution/compose.js.map +0 -1
- package/dist/runtime/runner/chart-trust.d.ts +0 -57
- package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
- package/dist/runtime/runner/chart-trust.js +0 -270
- package/dist/runtime/runner/chart-trust.js.map +0 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Read one file under a root as exact bytes: contained in the root after
|
|
2
|
+
// realpath, opened no-follow and non-blocking (a FIFO cannot stall the open),
|
|
3
|
+
// in a parent directory that is the one the containment check resolved, a
|
|
4
|
+
// regular file within a size bound, and unchanged while it was read. Shared by
|
|
5
|
+
// every reader of operator-stated or evidence inputs; it imports nothing from
|
|
6
|
+
// the library, so any module may use it.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
closeSync,
|
|
10
|
+
constants as fsConstants,
|
|
11
|
+
fstatSync,
|
|
12
|
+
openSync,
|
|
13
|
+
readSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
statSync,
|
|
16
|
+
} from 'node:fs';
|
|
17
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
18
|
+
|
|
19
|
+
export function readBoundedNoFollowFile(root, candidate, label, maxBytes) {
|
|
20
|
+
if (
|
|
21
|
+
typeof candidate !== 'string' ||
|
|
22
|
+
candidate.length === 0 ||
|
|
23
|
+
isAbsolute(candidate)
|
|
24
|
+
) {
|
|
25
|
+
throw new Error(`${label} must be a repository-relative path`);
|
|
26
|
+
}
|
|
27
|
+
const rootReal = realpathSync(root);
|
|
28
|
+
const lexicalPath = resolve(rootReal, candidate);
|
|
29
|
+
const directoryReal = realpathSync(dirname(lexicalPath));
|
|
30
|
+
const rel = relative(rootReal, directoryReal);
|
|
31
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
32
|
+
throw new Error(`${label} escapes its evidence root`);
|
|
33
|
+
}
|
|
34
|
+
const path = join(directoryReal, basename(lexicalPath));
|
|
35
|
+
const directoryChecked = statSync(directoryReal);
|
|
36
|
+
let descriptor;
|
|
37
|
+
try {
|
|
38
|
+
descriptor = openSync(
|
|
39
|
+
path,
|
|
40
|
+
fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW | fsConstants.O_NONBLOCK,
|
|
41
|
+
);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error?.code === 'ELOOP') {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`${label} escapes its evidence root or is not a no-follow file`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
// The containment check resolved `directoryReal`; a directory swapped in
|
|
52
|
+
// under that path between the check and the open is not the one checked.
|
|
53
|
+
const directoryOpened = statSync(directoryReal);
|
|
54
|
+
if (
|
|
55
|
+
directoryOpened.dev !== directoryChecked.dev ||
|
|
56
|
+
directoryOpened.ino !== directoryChecked.ino ||
|
|
57
|
+
realpathSync(directoryReal) !== directoryReal
|
|
58
|
+
) {
|
|
59
|
+
throw new Error(`${label}'s directory changed while it was opened`);
|
|
60
|
+
}
|
|
61
|
+
const before = fstatSync(descriptor);
|
|
62
|
+
if (!before.isFile()) {
|
|
63
|
+
throw new Error(`${label} must be a no-follow regular file`);
|
|
64
|
+
}
|
|
65
|
+
if (before.size > maxBytes) {
|
|
66
|
+
throw new Error(`${label} exceeds ${maxBytes} bytes`);
|
|
67
|
+
}
|
|
68
|
+
const bytes = Buffer.alloc(before.size);
|
|
69
|
+
let offset = 0;
|
|
70
|
+
while (offset < bytes.length) {
|
|
71
|
+
const count = readSync(
|
|
72
|
+
descriptor,
|
|
73
|
+
bytes,
|
|
74
|
+
offset,
|
|
75
|
+
bytes.length - offset,
|
|
76
|
+
offset,
|
|
77
|
+
);
|
|
78
|
+
if (count === 0) {throw new Error(`${label} was truncated while reading`);}
|
|
79
|
+
offset += count;
|
|
80
|
+
}
|
|
81
|
+
if (readSync(descriptor, Buffer.alloc(1), 0, 1, offset) !== 0) {
|
|
82
|
+
throw new Error(`${label} grew while reading`);
|
|
83
|
+
}
|
|
84
|
+
const after = fstatSync(descriptor);
|
|
85
|
+
for (const field of ['dev', 'ino', 'size', 'mtimeMs', 'ctimeMs']) {
|
|
86
|
+
if (after[field] !== before[field]) {
|
|
87
|
+
throw new Error(`${label} changed while reading`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { path, bytes };
|
|
91
|
+
} finally {
|
|
92
|
+
closeSync(descriptor);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
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
|
+
// The library reads and decides; it never writes to a registry. `attach-plan`
|
|
11
|
+
// validates a produced evidence directory and prints the exact `oras attach`
|
|
12
|
+
// operands; `validate` holds a pulled artifact to its manifest (and, with
|
|
13
|
+
// `--input`, to the producer input, the signer and a remaining lifetime);
|
|
14
|
+
// `backfill-plan` joins the committed evidence of the producing repositories
|
|
15
|
+
// to the locked subjects and to what each registry already serves.
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { MIN_REMAINING_TTL_SECONDS } from './check-committed-image-evidence.mjs';
|
|
21
|
+
import {
|
|
22
|
+
expectedProducerSource,
|
|
23
|
+
indexCommittedProducerEvidence,
|
|
24
|
+
parseEvidenceCliFlags,
|
|
25
|
+
} from './distribution-evidence-set.mjs';
|
|
26
|
+
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
27
|
+
import {
|
|
28
|
+
WorkspaceRoot,
|
|
29
|
+
configureEditionWorkspaceFromArgv,
|
|
30
|
+
dataRoot,
|
|
31
|
+
editionsRoot,
|
|
32
|
+
isEntryModule,
|
|
33
|
+
} from './edition-workspace.mjs';
|
|
34
|
+
import { SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
|
|
35
|
+
import { canonicalJson, loadValidatedLocks } from './image-evidence.mjs';
|
|
36
|
+
import { compareStrings, loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
|
|
37
|
+
import {
|
|
38
|
+
IMAGE_SOURCE_EVIDENCE_ARTIFACT_TYPE,
|
|
39
|
+
createRegistryClientResolver,
|
|
40
|
+
resolveImageSourceEvidenceReferrer,
|
|
41
|
+
validateMaterializedImageSourceEvidence,
|
|
42
|
+
} from './oci-image-source-evidence.mjs';
|
|
43
|
+
import { REFERRERS_DISTRIBUTION_SPEC } from './oras.mjs';
|
|
44
|
+
import { assertProducerInput, buildImageSourceStatements, cosignVerifyAttestationArgs, createCosignVerifier, readPinnedContractAuthority } from './produce-image-source-evidence.mjs';
|
|
45
|
+
import {
|
|
46
|
+
producerEvidenceLayerFiles,
|
|
47
|
+
producerStatementMatchesInput,
|
|
48
|
+
readProducerEvidenceDirectory,
|
|
49
|
+
} from './producer-evidence-bundle.mjs';
|
|
50
|
+
import { splitImageRef } from './registry-credentials.mjs';
|
|
51
|
+
|
|
52
|
+
const OCI_CREATED_ANNOTATION = 'org.opencontainers.image.created';
|
|
53
|
+
|
|
54
|
+
/** What `backfill-plan` reports for one locked subject. */
|
|
55
|
+
export const OciBackfillStatus = Object.freeze({
|
|
56
|
+
MissingGit: 'missing-git',
|
|
57
|
+
Present: 'present',
|
|
58
|
+
Missing: 'missing',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const USAGE = 'usage: xema edition evidence oci <attach-plan|validate|backfill-plan> [options]';
|
|
62
|
+
|
|
63
|
+
function assertAbsolute(value, label) {
|
|
64
|
+
if (typeof value !== 'string' || !isAbsolute(value)) {
|
|
65
|
+
throw new Error(`${label} must be an absolute path`);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One evidence artifact describes one image: one repository, one manifest digest. */
|
|
71
|
+
function inputSubject(input, expectedContractAuthority) {
|
|
72
|
+
assertProducerInput(input, expectedContractAuthority);
|
|
73
|
+
const repositories = new Set(input.subjects.map((subject) => subject.imageRepository));
|
|
74
|
+
const digests = new Set(input.subjects.map((subject) => subject.manifestDigest));
|
|
75
|
+
if (repositories.size !== 1 || digests.size !== 1) {
|
|
76
|
+
throw new Error('one OCI evidence artifact requires exactly one image repository and manifest digest');
|
|
77
|
+
}
|
|
78
|
+
return { imageRepository: [...repositories][0], subjectDigest: [...digests][0] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function expectedStatements(input, schemas, expectedContractAuthority) {
|
|
82
|
+
return new Map(
|
|
83
|
+
buildImageSourceStatements(input, schemas, expectedContractAuthority).map((statement) => [
|
|
84
|
+
`${statement.predicate.source.kind}:${statement.predicate.source.id}`,
|
|
85
|
+
statement,
|
|
86
|
+
]),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The `oras attach` operands of one produced evidence directory. With `input`
|
|
92
|
+
* every record must be the statement that input builds; without it the image
|
|
93
|
+
* is the one every record names.
|
|
94
|
+
*/
|
|
95
|
+
export function planOciEvidenceAttach({ evidenceDirectory, input, schemas, expectedContractAuthority }) {
|
|
96
|
+
assertAbsolute(evidenceDirectory, 'the evidence directory');
|
|
97
|
+
const evidence = readProducerEvidenceDirectory({ directory: evidenceDirectory, schemas });
|
|
98
|
+
if (evidence.records.length === 0) {
|
|
99
|
+
throw new Error(`${evidenceDirectory} holds no producer evidence`);
|
|
100
|
+
}
|
|
101
|
+
let image;
|
|
102
|
+
if (input === undefined) {
|
|
103
|
+
const [{ artifact }] = evidence.records.map((record) => record.statement.predicate);
|
|
104
|
+
image = { imageRepository: artifact.imageRepository, subjectDigest: artifact.manifestDigest };
|
|
105
|
+
} else {
|
|
106
|
+
image = inputSubject(input, expectedContractAuthority);
|
|
107
|
+
const expected = expectedStatements(input, schemas, expectedContractAuthority);
|
|
108
|
+
for (const record of evidence.records) {
|
|
109
|
+
const expectedStatement = expected.get(record.key);
|
|
110
|
+
if (expectedStatement === undefined || !producerStatementMatchesInput(record.statement, expectedStatement)) {
|
|
111
|
+
throw new Error(`${record.key} produced evidence is not bound to the input`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const record of evidence.records) {
|
|
116
|
+
const { artifact } = record.statement.predicate;
|
|
117
|
+
if (artifact.imageRepository !== image.imageRepository || artifact.manifestDigest !== image.subjectDigest) {
|
|
118
|
+
throw new Error(`${record.key} evidence is not bound to ${image.imageRepository}@${image.subjectDigest}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const created = evidence.records[0].statement.predicate.producerRun.issuedAt;
|
|
122
|
+
if (evidence.records.some((record) => record.statement.predicate.producerRun.issuedAt !== created)) {
|
|
123
|
+
throw new Error('one OCI evidence artifact cannot carry multiple issuance times');
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
schemaVersion: 1,
|
|
127
|
+
kind: 'oci-attach-plan',
|
|
128
|
+
evidenceDirectory,
|
|
129
|
+
...image,
|
|
130
|
+
subject: `${image.imageRepository}@${image.subjectDigest}`,
|
|
131
|
+
artifactType: IMAGE_SOURCE_EVIDENCE_ARTIFACT_TYPE,
|
|
132
|
+
distributionSpec: REFERRERS_DISTRIBUTION_SPEC,
|
|
133
|
+
annotations: { [OCI_CREATED_ANNOTATION]: created },
|
|
134
|
+
files: producerEvidenceLayerFiles(evidence.records),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Hold a pulled evidence artifact to its manifest. With `input` it also
|
|
140
|
+
* answers whether the artifact can be reused instead of producing again: the
|
|
141
|
+
* statements are the ones the input builds, every bundle verifies against the
|
|
142
|
+
* declared signer, and enough lifetime remains.
|
|
143
|
+
*/
|
|
144
|
+
export function assessMaterializedOciEvidence({
|
|
145
|
+
evidenceDirectory,
|
|
146
|
+
manifest,
|
|
147
|
+
subjectDigest,
|
|
148
|
+
schemas,
|
|
149
|
+
input,
|
|
150
|
+
expectedContractAuthority,
|
|
151
|
+
runCosign,
|
|
152
|
+
now = Date.now(),
|
|
153
|
+
}) {
|
|
154
|
+
assertAbsolute(evidenceDirectory, 'the evidence directory');
|
|
155
|
+
if (!SHA256_DIGEST_PATTERN.test(subjectDigest ?? '')) {
|
|
156
|
+
throw new Error('the subject digest must be sha256:<64 hex>');
|
|
157
|
+
}
|
|
158
|
+
const evidence = validateMaterializedImageSourceEvidence({
|
|
159
|
+
directory: evidenceDirectory,
|
|
160
|
+
manifest,
|
|
161
|
+
subjectDigest,
|
|
162
|
+
schemas,
|
|
163
|
+
});
|
|
164
|
+
const base = {
|
|
165
|
+
schemaVersion: 1,
|
|
166
|
+
kind: 'oci-evidence',
|
|
167
|
+
subjectDigest,
|
|
168
|
+
created: evidence.created,
|
|
169
|
+
subjects: evidence.records.map((record) => record.key),
|
|
170
|
+
};
|
|
171
|
+
if (input === undefined) {
|
|
172
|
+
return { ...base, matchesInput: null, remainingSeconds: null, reusable: null };
|
|
173
|
+
}
|
|
174
|
+
if (typeof runCosign !== 'function') {
|
|
175
|
+
throw new Error('a cosign verifier is required to assess evidence against an input');
|
|
176
|
+
}
|
|
177
|
+
const image = inputSubject(input, expectedContractAuthority);
|
|
178
|
+
if (image.subjectDigest !== subjectDigest) {
|
|
179
|
+
throw new Error(`the input describes ${image.subjectDigest}, not ${subjectDigest}`);
|
|
180
|
+
}
|
|
181
|
+
const expected = expectedStatements(input, schemas, expectedContractAuthority);
|
|
182
|
+
let matchesInput = evidence.records.length === expected.size;
|
|
183
|
+
let oldestExpiry = Number.POSITIVE_INFINITY;
|
|
184
|
+
for (const record of evidence.records) {
|
|
185
|
+
const expectedStatement = expected.get(record.key);
|
|
186
|
+
matchesInput &&=
|
|
187
|
+
expectedStatement !== undefined && producerStatementMatchesInput(record.statement, expectedStatement);
|
|
188
|
+
oldestExpiry = Math.min(oldestExpiry, Date.parse(record.statement.predicate.producerRun.expiresAt));
|
|
189
|
+
// A bundle that does not verify is a refusal, never "not reusable".
|
|
190
|
+
runCosign(
|
|
191
|
+
cosignVerifyAttestationArgs({
|
|
192
|
+
bundlePath: join(evidenceDirectory, record.descriptor.bundlePath),
|
|
193
|
+
signer: input.signer,
|
|
194
|
+
certificateWorkflowSha: record.statement.predicate.producerRun.producerWorkflowSha,
|
|
195
|
+
predicateType: record.statement.predicateType,
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const remainingSeconds = Math.floor((oldestExpiry - now) / 1000);
|
|
200
|
+
return {
|
|
201
|
+
...base,
|
|
202
|
+
matchesInput,
|
|
203
|
+
remainingSeconds,
|
|
204
|
+
reusable: matchesInput && remainingSeconds >= MIN_REMAINING_TTL_SECONDS,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* For every locked subject of the named editions: its committed evidence and
|
|
210
|
+
* whether the registry already serves it as a referrer. A `missing` item
|
|
211
|
+
* carries the attach plan the caller runs.
|
|
212
|
+
*/
|
|
213
|
+
export async function planOciEvidenceBackfill({
|
|
214
|
+
distributionIds,
|
|
215
|
+
dataRoot: data,
|
|
216
|
+
editionsRoot: editions,
|
|
217
|
+
producerRepositoryRoots,
|
|
218
|
+
schemas,
|
|
219
|
+
registryClientFactory,
|
|
220
|
+
}) {
|
|
221
|
+
const gitIndex = indexCommittedProducerEvidence({
|
|
222
|
+
producerRepositoryRoots,
|
|
223
|
+
schemas,
|
|
224
|
+
onDefect: ({ directory, reason }) => {
|
|
225
|
+
throw new Error(`Git evidence at ${directory} is unusable: ${reason}`);
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
const clientFor = createRegistryClientResolver({ dataRoot: data, factory: registryClientFactory });
|
|
229
|
+
const work = new Map();
|
|
230
|
+
const missingGit = new Map();
|
|
231
|
+
for (const distributionId of distributionIds) {
|
|
232
|
+
assertCanonicalDistributionId(distributionId);
|
|
233
|
+
const { imageLock, repositoryPins } = await loadValidatedLocks(distributionId, data, schemas, {
|
|
234
|
+
editionsRoot: editions,
|
|
235
|
+
});
|
|
236
|
+
for (const subject of imageLock.subjects) {
|
|
237
|
+
const source = expectedProducerSource({
|
|
238
|
+
subject,
|
|
239
|
+
repositoryPin: repositoryPins.get(subject.repo),
|
|
240
|
+
sourceHashAlgorithm: imageLock.sourceHashAlgorithm,
|
|
241
|
+
});
|
|
242
|
+
const matches = gitIndex.get(canonicalJson(source)) ?? [];
|
|
243
|
+
if (matches.length > 1) {
|
|
244
|
+
throw new Error(`current ${subject.kind}:${subject.id} requires exactly one Git bundle; found ${matches.length}`);
|
|
245
|
+
}
|
|
246
|
+
if (matches.length === 0) {
|
|
247
|
+
missingGit.set(`${subject.kind}:${subject.id}\0${subject.sourceCommit}`, {
|
|
248
|
+
status: OciBackfillStatus.MissingGit,
|
|
249
|
+
subjectKind: subject.kind,
|
|
250
|
+
subjectId: subject.id,
|
|
251
|
+
imageRepository: subject.imageRef,
|
|
252
|
+
subjectDigest: null,
|
|
253
|
+
artifactDigest: null,
|
|
254
|
+
});
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const [match] = matches;
|
|
258
|
+
const subjectDigest = match.statement.predicate.artifact.manifestDigest;
|
|
259
|
+
const key = `${subject.imageRef}@${subjectDigest}`;
|
|
260
|
+
const incumbent = work.get(key);
|
|
261
|
+
if (incumbent && incumbent.directory !== match.directory) {
|
|
262
|
+
throw new Error(`${key} resolves to multiple Git evidence directories`);
|
|
263
|
+
}
|
|
264
|
+
work.set(key, { subject, directory: match.directory, imageRepository: subject.imageRef, subjectDigest });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const items = [...missingGit.values()];
|
|
268
|
+
for (const item of [...work.values()].sort((left, right) =>
|
|
269
|
+
compareStrings(left.imageRepository, right.imageRepository),
|
|
270
|
+
)) {
|
|
271
|
+
const { authority, repository } = splitImageRef(item.imageRepository);
|
|
272
|
+
const client = clientFor(authority);
|
|
273
|
+
const served = await client.getManifestByTag(repository, item.subject.contentHash);
|
|
274
|
+
if (served.digest !== item.subjectDigest) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`${item.imageRepository}:${item.subject.contentHash} serves ${served.digest}, not signed ${item.subjectDigest}`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const existing = await resolveImageSourceEvidenceReferrer({
|
|
280
|
+
client,
|
|
281
|
+
imageRepository: item.imageRepository,
|
|
282
|
+
subjectDigest: item.subjectDigest,
|
|
283
|
+
schemas,
|
|
284
|
+
});
|
|
285
|
+
const reported = {
|
|
286
|
+
subjectKind: item.subject.kind,
|
|
287
|
+
subjectId: item.subject.id,
|
|
288
|
+
imageRepository: item.imageRepository,
|
|
289
|
+
subjectDigest: item.subjectDigest,
|
|
290
|
+
};
|
|
291
|
+
items.push(
|
|
292
|
+
existing === null
|
|
293
|
+
? {
|
|
294
|
+
status: OciBackfillStatus.Missing,
|
|
295
|
+
...reported,
|
|
296
|
+
artifactDigest: null,
|
|
297
|
+
attach: planOciEvidenceAttach({ evidenceDirectory: item.directory, schemas }),
|
|
298
|
+
}
|
|
299
|
+
: { status: OciBackfillStatus.Present, ...reported, artifactDigest: existing.artifactDigest },
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return { schemaVersion: 1, kind: 'oci-backfill-plan', items };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const readJsonFile = (path, label) => {
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse(readFileSync(resolve(path), 'utf8'));
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw new Error(`${label} ${path} is not readable JSON: ${error.message}`);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
async function main() {
|
|
314
|
+
const [operation, ...argv] = process.argv.slice(2);
|
|
315
|
+
if (operation === 'attach-plan') {
|
|
316
|
+
const rest = configureEditionWorkspaceFromArgv(argv, [WorkspaceRoot.Data, WorkspaceRoot.Kernel]);
|
|
317
|
+
const flags = parseEvidenceCliFlags(rest, { single: ['evidence-directory', 'input'] });
|
|
318
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
319
|
+
const input = flags.optional('input');
|
|
320
|
+
process.stdout.write(
|
|
321
|
+
serializeJson(
|
|
322
|
+
planOciEvidenceAttach({
|
|
323
|
+
evidenceDirectory: flags.require('evidence-directory'),
|
|
324
|
+
input: input === undefined ? undefined : readJsonFile(input, 'producer input'),
|
|
325
|
+
schemas,
|
|
326
|
+
expectedContractAuthority: readPinnedContractAuthority({ dataRoot: dataRoot(), schemas }),
|
|
327
|
+
}),
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (operation === 'validate') {
|
|
333
|
+
const rest = configureEditionWorkspaceFromArgv(argv, [WorkspaceRoot.Data, WorkspaceRoot.Kernel]);
|
|
334
|
+
const flags = parseEvidenceCliFlags(rest, {
|
|
335
|
+
single: ['evidence-directory', 'manifest', 'subject-digest', 'input', 'cosign', 'now'],
|
|
336
|
+
});
|
|
337
|
+
const schemas = await loadKernelDistributionSchemas();
|
|
338
|
+
const input = flags.optional('input');
|
|
339
|
+
const cosign = flags.optional('cosign');
|
|
340
|
+
const now = flags.optional('now');
|
|
341
|
+
if ((input === undefined) !== (cosign === undefined)) {
|
|
342
|
+
throw new Error('--input and --cosign are given together or not at all');
|
|
343
|
+
}
|
|
344
|
+
if (now !== undefined && !/^\d+$/.test(now)) {
|
|
345
|
+
throw new Error('--now must be epoch milliseconds');
|
|
346
|
+
}
|
|
347
|
+
process.stdout.write(
|
|
348
|
+
serializeJson(
|
|
349
|
+
assessMaterializedOciEvidence({
|
|
350
|
+
evidenceDirectory: flags.require('evidence-directory'),
|
|
351
|
+
manifest: readJsonFile(flags.require('manifest'), 'OCI manifest'),
|
|
352
|
+
subjectDigest: flags.require('subject-digest'),
|
|
353
|
+
schemas,
|
|
354
|
+
input: input === undefined ? undefined : readJsonFile(input, 'producer input'),
|
|
355
|
+
expectedContractAuthority: readPinnedContractAuthority({ dataRoot: dataRoot(), schemas }),
|
|
356
|
+
runCosign: cosign === undefined ? undefined : createCosignVerifier(cosign).runCosign,
|
|
357
|
+
now: now === undefined ? Date.now() : Number(now),
|
|
358
|
+
}),
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (operation === 'backfill-plan') {
|
|
364
|
+
const rest = configureEditionWorkspaceFromArgv(argv, [
|
|
365
|
+
WorkspaceRoot.Data,
|
|
366
|
+
WorkspaceRoot.Editions,
|
|
367
|
+
WorkspaceRoot.Kernel,
|
|
368
|
+
]);
|
|
369
|
+
const flags = parseEvidenceCliFlags(rest, { repeated: ['distribution', 'producer-repository-root'] });
|
|
370
|
+
const plan = await planOciEvidenceBackfill({
|
|
371
|
+
distributionIds: flags.requireMany('distribution'),
|
|
372
|
+
dataRoot: dataRoot(),
|
|
373
|
+
editionsRoot: editionsRoot(),
|
|
374
|
+
producerRepositoryRoots: flags.requireMany('producer-repository-root').map((root) => resolve(root)),
|
|
375
|
+
schemas: await loadKernelDistributionSchemas(),
|
|
376
|
+
});
|
|
377
|
+
process.stdout.write(serializeJson(plan));
|
|
378
|
+
// 2: something is still to be produced or attached.
|
|
379
|
+
if (plan.items.some((item) => item.status !== OciBackfillStatus.Present)) {
|
|
380
|
+
process.exitCode = 2;
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
throw new Error(USAGE);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (isEntryModule(import.meta.url)) {
|
|
388
|
+
main().catch((error) => {
|
|
389
|
+
console.error(`[oci-evidence] ${error.message}`);
|
|
390
|
+
process.exitCode = 1;
|
|
391
|
+
});
|
|
392
|
+
}
|