refactor: adapter 구현중..
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { cp, mkdtemp, readFile, rm, symlink } from "node:fs/promises";
|
||||
import { cp, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -119,6 +119,40 @@ it(
|
||||
failures: [],
|
||||
}),
|
||||
);
|
||||
|
||||
const outsideRoot = await mkdtemp(path.join(tmpdir(), "security-followup-outside-"));
|
||||
try {
|
||||
await mkdir(path.join(outsideRoot, "config/security"), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(outsideRoot, "config/security/dependency-policy.json"),
|
||||
'{"contradictoryCheckoutCanary":"FAIL"}\n',
|
||||
);
|
||||
const outsideVerification = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
path.join(sourceRoot, "scripts/verify-archived-local-evidence.ts"),
|
||||
"--archive",
|
||||
archivePath,
|
||||
"--sha256",
|
||||
expectedSha256,
|
||||
],
|
||||
{
|
||||
cwd: outsideRoot,
|
||||
encoding: "utf8",
|
||||
timeout: 120_000,
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
expect(
|
||||
outsideVerification.status,
|
||||
`${outsideVerification.stdout}\n${outsideVerification.stderr}`,
|
||||
).toBe(0);
|
||||
expect(outsideVerification.stdout).toContain(
|
||||
"Archived local evidence verification: PASS",
|
||||
);
|
||||
} finally {
|
||||
await rm(outsideRoot, { recursive: true, force: true });
|
||||
}
|
||||
} finally {
|
||||
await rm(fixtureRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -164,6 +164,35 @@ jobs:
|
||||
artifacts/security/supply-chain-coherence.json \\
|
||||
artifacts/security/supply-chain-verification.json \\
|
||||
artifacts/security/vulnerability-report.json \\
|
||||
config/security/dependency-baseline.approval.json \\
|
||||
config/security/dependency-baseline.json \\
|
||||
config/security/dependency-change-evidence.json \\
|
||||
config/security/dependency-policy.json \\
|
||||
config/security/secret-scan-policy.json \\
|
||||
config/security/vulnerability-exceptions.json \\
|
||||
config/security/vulnerability-policy.json \\
|
||||
schemas/artifacts/build-manifest.schema.json \\
|
||||
schemas/artifacts/dependency-inventory.schema.json \\
|
||||
schemas/artifacts/supply-chain-verification.schema.json \\
|
||||
scripts/contracts/release-artifacts.ts \\
|
||||
scripts/create-release-candidate.ts \\
|
||||
scripts/generate-supply-chain.ts \\
|
||||
scripts/lib/build-manifest-outputs.ts \\
|
||||
scripts/lib/json-schema.ts \\
|
||||
scripts/lib/local-policy-evidence.ts \\
|
||||
scripts/lib/local-release-evidence.ts \\
|
||||
scripts/lib/release-candidate.ts \\
|
||||
scripts/lib/release-input-evidence.ts \\
|
||||
scripts/lib/release-runtime-coherence.ts \\
|
||||
scripts/lib/repository-file-inventory.ts \\
|
||||
scripts/lib/secret-scan-evaluator.ts \\
|
||||
scripts/lib/secret-scan-policy.ts \\
|
||||
scripts/lib/secret-scan.ts \\
|
||||
scripts/lib/supply-chain.ts \\
|
||||
scripts/lib/validated-json-artifact.ts \\
|
||||
src/contracts/release-artifacts.ts \\
|
||||
src/features/installed-contract-contributions.ts \\
|
||||
src/features/installed-feature-contracts.ts \\
|
||||
artifacts/release/release-candidate.json
|
||||
node scripts/verify-ci-candidate-archive.ts --archive ".release/release-candidate-\${{ gitea.run_id }}-\${{ gitea.run_attempt }}.tar.gz" --github-output "$GITHUB_OUTPUT"
|
||||
- name: Upload release candidate
|
||||
@@ -330,8 +359,10 @@ jobs:
|
||||
PROMOTION_CLEANUP_TOKEN: \${{ steps.finalize.outputs.cleanup_token }}
|
||||
PROMOTION_RUNNER_TEMP_DEV: \${{ steps.finalize.outputs.runner_temp_dev }}
|
||||
PROMOTION_RUNNER_TEMP_INO: \${{ steps.finalize.outputs.runner_temp_ino }}
|
||||
PROMOTION_STAGING_DEV: \${{ steps.finalize.outputs.staging_dev }}
|
||||
PROMOTION_STAGING_INO: \${{ steps.finalize.outputs.staging_ino }}
|
||||
run: |
|
||||
if [ -n "$PROMOTION_STAGING_ROOT" ] && [ -n "$PROMOTION_CLEANUP_TOKEN" ]; then
|
||||
if [ -n "$PROMOTION_STAGING_ROOT" ] && [ -n "$PROMOTION_CLEANUP_TOKEN" ] && [ -n "$PROMOTION_RUNNER_TEMP_DEV" ] && [ -n "$PROMOTION_RUNNER_TEMP_INO" ] && [ -n "$PROMOTION_STAGING_DEV" ] && [ -n "$PROMOTION_STAGING_INO" ]; then
|
||||
node scripts/cleanup-verified-promotion.ts
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
bundleOutputInventoryArtifactSchema,
|
||||
bundlePerformanceArtifactSchema,
|
||||
} from "../../scripts/contracts/release-artifacts.ts";
|
||||
|
||||
const sha256 = "a".repeat(64);
|
||||
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: "2026-08-02T00:00:00.000Z",
|
||||
context: {
|
||||
nodeVersion: "v24.14.0",
|
||||
packageManager: "pnpm@11.17.0",
|
||||
runnerImage: "fixture@sha256:abc",
|
||||
},
|
||||
outputs: [
|
||||
{ path: "dist/assets/entry.js", bytes: 100, gzipBytes: 50, sha256 },
|
||||
{ path: "dist/assets/lazy.js", bytes: 80, gzipBytes: 40, sha256 },
|
||||
{ path: "dist/index.html", bytes: 20, gzipBytes: 15, sha256 },
|
||||
],
|
||||
} as const;
|
||||
|
||||
const completed = {
|
||||
...inventory,
|
||||
measurements: {
|
||||
initialJsGzipBytes: 50,
|
||||
lazyChunks: [{ path: "assets/lazy.js", gzipBytes: 40 }],
|
||||
},
|
||||
classification: {
|
||||
initialFiles: ["assets/entry.js"],
|
||||
lazyFiles: ["assets/lazy.js"],
|
||||
missingImports: [],
|
||||
},
|
||||
missingOutputs: [],
|
||||
thresholds: { initialJsGzipBytes: 60, lazyChunkGzipBytes: 45 },
|
||||
results: {
|
||||
initialPassed: true,
|
||||
lazyResults: [
|
||||
{ path: "assets/lazy.js", gzipBytes: 40, threshold: 45, passed: true },
|
||||
],
|
||||
passed: true,
|
||||
},
|
||||
fixtures: [
|
||||
{ name: "initial-js-over-budget", passed: true },
|
||||
{ name: "lazy-chunk-over-budget", passed: true },
|
||||
],
|
||||
passed: true,
|
||||
} as const;
|
||||
|
||||
describe("bundle artifact contracts", () => {
|
||||
it("separates the raw output inventory from final budget evidence", () => {
|
||||
expect(bundleOutputInventoryArtifactSchema.parse(inventory)).toEqual(inventory);
|
||||
expect(() => bundleOutputInventoryArtifactSchema.parse(completed)).toThrow();
|
||||
expect(bundlePerformanceArtifactSchema.parse(completed)).toEqual(completed);
|
||||
expect(() => bundlePerformanceArtifactSchema.parse(inventory)).toThrow();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["initial total", { measurements: { ...completed.measurements, initialJsGzipBytes: 49 } }],
|
||||
["lazy classification", { classification: { ...completed.classification, lazyFiles: [] } }],
|
||||
["missing output", { missingOutputs: ["assets/missing.js"], passed: true }],
|
||||
["threshold result", { results: { ...completed.results, initialPassed: false } }],
|
||||
["lazy result", { results: { ...completed.results, lazyResults: [] } }],
|
||||
["fixture identity", { fixtures: [{ name: "other", passed: true }, completed.fixtures[1]] }],
|
||||
["overall result", { passed: false }],
|
||||
] as const)("rejects inconsistent %s evidence", (_name, mutation) => {
|
||||
expect(() =>
|
||||
bundlePerformanceArtifactSchema.parse({ ...completed, ...mutation })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("publishes final evidence only through the validated atomic writer", async () => {
|
||||
const source = await readFile("scripts/check-bundle.ts", "utf8");
|
||||
expect(source).toContain("bundleOutputInventoryArtifactSchema.parse");
|
||||
expect(source).toContain("writeValidatedJsonArtifact");
|
||||
expect(source).not.toMatch(/\bwriteFile\s*\(/u);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("provider output limiter", () => {
|
||||
it("counts secret-bearing bytes without retaining or forwarding them", async () => {
|
||||
const { createProviderOutputLimiter } = await import(
|
||||
"../../scripts/lib/provider-output-limiter.ts"
|
||||
);
|
||||
const exceeded = vi.fn();
|
||||
const limiter = createProviderOutputLimiter(64, exceeded);
|
||||
const credential = Buffer.from("provider-credential-must-not-reach-ci");
|
||||
const stdout = vi.spyOn(process.stdout, "write");
|
||||
const stderr = vi.spyOn(process.stderr, "write");
|
||||
try {
|
||||
limiter.consume(credential);
|
||||
expect(limiter.bytes()).toBe(credential.byteLength);
|
||||
expect(exceeded).not.toHaveBeenCalled();
|
||||
expect(stdout).not.toHaveBeenCalled();
|
||||
expect(stderr).not.toHaveBeenCalled();
|
||||
expect(JSON.stringify(limiter)).not.toContain(credential.toString("utf8"));
|
||||
} finally {
|
||||
stdout.mockRestore();
|
||||
stderr.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("signals once when aggregate stdout and stderr exceed the byte budget", async () => {
|
||||
const { createProviderOutputLimiter } = await import(
|
||||
"../../scripts/lib/provider-output-limiter.ts"
|
||||
);
|
||||
const exceeded = vi.fn();
|
||||
const limiter = createProviderOutputLimiter(5, exceeded);
|
||||
limiter.consume(Buffer.from("abc"));
|
||||
limiter.consume("def");
|
||||
limiter.consume("ignored");
|
||||
|
||||
expect(limiter.bytes()).toBe(6);
|
||||
expect(exceeded).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,6 @@ import {
|
||||
evaluatePromotionEvidence,
|
||||
providerEvidenceSignaturePayload,
|
||||
providerPublicKeyFingerprint,
|
||||
providerVerificationArtifactSchema,
|
||||
} from "../../scripts/lib/provider-evidence.ts";
|
||||
import {
|
||||
createReleaseCandidateManifest,
|
||||
@@ -32,7 +31,6 @@ import {
|
||||
verifyReleaseCandidate,
|
||||
} from "../../scripts/lib/release-candidate.ts";
|
||||
import { deterministicSupplyChainGeneratedAt } from "../../scripts/lib/supply-chain-time.ts";
|
||||
import { verifyPromotionInputs } from "../../scripts/lib/promotion-verifier.ts";
|
||||
import {
|
||||
indexCiGateContract,
|
||||
loadCiGateContract,
|
||||
@@ -57,11 +55,6 @@ const sourceIdentity = Object.freeze({
|
||||
revision: "a".repeat(40),
|
||||
sourceSetSha256: "b".repeat(64),
|
||||
});
|
||||
const localIdentity = Object.freeze({
|
||||
sourceRevision: sourceIdentity.revision,
|
||||
sourceSetSha256: sourceIdentity.sourceSetSha256,
|
||||
assessmentSha256: "c".repeat(64),
|
||||
});
|
||||
const expectedProviderContext = Object.freeze({
|
||||
run: Object.freeze({ id: "fixture-run", attempt: 1 }),
|
||||
source: sourceIdentity,
|
||||
@@ -73,6 +66,14 @@ const expectedProviderContext = Object.freeze({
|
||||
}),
|
||||
vulnerabilityInvocationNonce: "5".repeat(64),
|
||||
provenanceInvocationNonce: "6".repeat(64),
|
||||
secretScanAttestation: Object.freeze({
|
||||
status: "PASS" as const,
|
||||
localEvidenceAssessmentSha256: "7".repeat(64),
|
||||
sourceSetSha256: sourceIdentity.sourceSetSha256,
|
||||
policySha256: "8".repeat(64),
|
||||
sarifSha256: "9".repeat(64),
|
||||
scanInputSha256: "a".repeat(64),
|
||||
}),
|
||||
});
|
||||
|
||||
function signedProviderEvidence(
|
||||
@@ -118,6 +119,7 @@ function providerPair(input: Readonly<{
|
||||
run: { ...expectedProviderContext.run, invocationNonce: expectedProviderContext.vulnerabilityInvocationNonce },
|
||||
source: expectedProviderContext.source,
|
||||
candidate,
|
||||
secretScanAttestation: expectedProviderContext.secretScanAttestation,
|
||||
findings: [],
|
||||
}, "fixture-vulnerability-key", input.vulnerabilityKeys.privateKey, vulnerabilityFingerprint),
|
||||
provenanceAttestation: signedProviderEvidence({
|
||||
@@ -143,264 +145,7 @@ function providerTrust(
|
||||
return { keyId, publicKey, publicKeyFingerprint };
|
||||
}
|
||||
|
||||
async function createMinimalCandidateTree(root: string) {
|
||||
const rawLockfile = "lockfileVersion: '9.0'\n";
|
||||
const rawLockfileSha256 = createHash("sha256")
|
||||
.update(rawLockfile)
|
||||
.digest("hex");
|
||||
await mkdir(path.join(root, "dist"), { recursive: true });
|
||||
await writeFile(path.join(root, "dist/app.js"), "immutable\n");
|
||||
await writeFile(path.join(root, "pnpm-lock.yaml"), rawLockfile);
|
||||
for (const file of RELEASE_CANDIDATE_EVIDENCE_PATHS) {
|
||||
if (file === "pnpm-lock.yaml") continue;
|
||||
await mkdir(path.dirname(path.join(root, file)), { recursive: true });
|
||||
const value =
|
||||
file === "artifacts/release/dependency-inventory.json"
|
||||
? { lockfileSha256: rawLockfileSha256 }
|
||||
: file === "artifacts/security/supply-chain-verification.json"
|
||||
? { localStatus: "PASS" }
|
||||
: { fixture: file };
|
||||
await writeFile(path.join(root, file), `${JSON.stringify(value)}\n`);
|
||||
}
|
||||
const manifest = await createReleaseCandidateManifest(root);
|
||||
await writeFile(
|
||||
path.join(root, "artifacts/release/release-candidate.json"),
|
||||
`${JSON.stringify(manifest)}\n`,
|
||||
);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
async function writeProviderEnvironment(
|
||||
root: string,
|
||||
candidate: Awaited<ReturnType<typeof createReleaseCandidateManifest>>,
|
||||
overrides: Readonly<{ distSha256?: string }> = {},
|
||||
) {
|
||||
const vulnerabilityKeys = generateKeyPairSync("ed25519");
|
||||
const provenanceKeys = generateKeyPairSync("ed25519");
|
||||
const archiveBytes = "fixture archive\n";
|
||||
const candidateIdentity = {
|
||||
archiveSha256: createHash("sha256").update(archiveBytes).digest("hex"),
|
||||
bundleSha256: candidate.bundleSha256,
|
||||
distSha256: overrides.distSha256 ?? candidate.distSha256,
|
||||
lockfileSha256: candidate.lockfileSha256,
|
||||
};
|
||||
const vulnerabilityReport = signedProviderEvidence(
|
||||
{
|
||||
schemaVersion: 2,
|
||||
evidenceType: "vulnerability-report",
|
||||
provider: "fixture-vulnerability-provider",
|
||||
issuedAt: "2026-08-02T01:00:00.000Z",
|
||||
expiresAt: "2026-08-02T02:00:00.000Z",
|
||||
run: { id: "fixture-run", attempt: 1, invocationNonce: "5".repeat(64) },
|
||||
source: sourceIdentity,
|
||||
candidate: candidateIdentity,
|
||||
findings: [],
|
||||
},
|
||||
"fixture-vulnerability-key",
|
||||
vulnerabilityKeys.privateKey,
|
||||
providerPublicKeyFingerprint(vulnerabilityKeys.publicKey),
|
||||
);
|
||||
const provenanceAttestation = signedProviderEvidence(
|
||||
{
|
||||
schemaVersion: 2,
|
||||
evidenceType: "provenance-attestation",
|
||||
provider: "fixture-provenance-provider",
|
||||
signer: "fixture-workload-identity",
|
||||
issuedAt: "2026-08-02T01:00:00.000Z",
|
||||
expiresAt: "2026-08-02T02:00:00.000Z",
|
||||
run: { id: "fixture-run", attempt: 1, invocationNonce: "6".repeat(64) },
|
||||
source: sourceIdentity,
|
||||
candidate: candidateIdentity,
|
||||
subject: { name: "dist", digest: { sha256: candidateIdentity.distSha256 } },
|
||||
},
|
||||
"fixture-provenance-key",
|
||||
provenanceKeys.privateKey,
|
||||
providerPublicKeyFingerprint(provenanceKeys.publicKey),
|
||||
);
|
||||
await mkdir(path.join(root, "provider"), { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(path.join(root, "provider/candidate.tar.gz"), archiveBytes),
|
||||
writeFile(
|
||||
path.join(root, "provider/vulnerability.json"),
|
||||
`${JSON.stringify(vulnerabilityReport)}\n`,
|
||||
),
|
||||
writeFile(
|
||||
path.join(root, "provider/provenance.json"),
|
||||
`${JSON.stringify(provenanceAttestation)}\n`,
|
||||
),
|
||||
writeFile(
|
||||
path.join(root, "provider/vulnerability.pem"),
|
||||
vulnerabilityKeys.publicKey
|
||||
.export({ type: "spki", format: "pem" })
|
||||
.toString(),
|
||||
),
|
||||
writeFile(
|
||||
path.join(root, "provider/provenance.pem"),
|
||||
provenanceKeys.publicKey
|
||||
.export({ type: "spki", format: "pem" })
|
||||
.toString(),
|
||||
),
|
||||
]);
|
||||
return {
|
||||
CANDIDATE_ARCHIVE_PATH: "provider/candidate.tar.gz",
|
||||
CANDIDATE_ARCHIVE_SHA256: createHash("sha256")
|
||||
.update(archiveBytes)
|
||||
.digest("hex"),
|
||||
CI_RUN_ID: "fixture-run",
|
||||
CI_RUN_ATTEMPT: "1",
|
||||
EXPECTED_SOURCE_REVISION: sourceIdentity.revision,
|
||||
VULNERABILITY_INVOCATION_NONCE: "5".repeat(64),
|
||||
PROVENANCE_INVOCATION_NONCE: "6".repeat(64),
|
||||
VULNERABILITY_REPORT_PATH: "provider/vulnerability.json",
|
||||
PROVENANCE_ATTESTATION_PATH: "provider/provenance.json",
|
||||
VULNERABILITY_PUBLIC_KEY_PATH: "provider/vulnerability.pem",
|
||||
VULNERABILITY_KEY_ID: "fixture-vulnerability-key",
|
||||
PROVENANCE_PUBLIC_KEY_PATH: "provider/provenance.pem",
|
||||
PROVENANCE_KEY_ID: "fixture-provenance-key",
|
||||
} satisfies NodeJS.ProcessEnv;
|
||||
}
|
||||
|
||||
describe("supply-chain policy", () => {
|
||||
it("emits a strict role-bound v3 verification record from exact input bytes", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "promotion-verification-v3-"));
|
||||
try {
|
||||
const manifest = await createMinimalCandidateTree(root);
|
||||
const environment = await writeProviderEnvironment(root, manifest);
|
||||
const report = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment,
|
||||
verifyLocalEvidence: async () => ({
|
||||
status: "PASS" as const,
|
||||
identity: localIdentity,
|
||||
failures: [] as const,
|
||||
}),
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
expect(providerVerificationArtifactSchema.parse(report)).toEqual(
|
||||
expect.objectContaining({
|
||||
schemaVersion: 3,
|
||||
artifactType: "provider-verification",
|
||||
candidate: expect.objectContaining({
|
||||
archiveSha256: environment.CANDIDATE_ARCHIVE_SHA256,
|
||||
}),
|
||||
providerEvidence: expect.objectContaining({
|
||||
vulnerabilityReportSha256: createHash("sha256")
|
||||
.update(await readFile(path.join(root, environment.VULNERABILITY_REPORT_PATH!)))
|
||||
.digest("hex"),
|
||||
provenanceAttestationSha256: createHash("sha256")
|
||||
.update(await readFile(path.join(root, environment.PROVENANCE_ATTESTATION_PATH!)))
|
||||
.digest("hex"),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("wires candidate files, PEM trust, env report paths, and mutation checks", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "promotion-wiring-"));
|
||||
try {
|
||||
const manifest = await createMinimalCandidateTree(root);
|
||||
const validEnvironment = await writeProviderEnvironment(root, manifest);
|
||||
const acceptLocalEvidence = async () => ({
|
||||
status: "PASS" as const,
|
||||
identity: localIdentity,
|
||||
failures: [] as const,
|
||||
});
|
||||
const valid = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment: validEnvironment,
|
||||
verifyLocalEvidence: acceptLocalEvidence,
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
const absent = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment: {},
|
||||
verifyLocalEvidence: acceptLocalEvidence,
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
const replayedNonce = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment: {
|
||||
...validEnvironment,
|
||||
VULNERABILITY_INVOCATION_NONCE: "9".repeat(64),
|
||||
},
|
||||
verifyLocalEvidence: acceptLocalEvidence,
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
const wrongEnvironment = await writeProviderEnvironment(root, manifest, {
|
||||
distSha256: "3".repeat(64),
|
||||
});
|
||||
const wrongDigest = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment: wrongEnvironment,
|
||||
verifyLocalEvidence: acceptLocalEvidence,
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
await writeFile(path.join(root, "dist/app.js"), "mutated\n");
|
||||
const postAttestationMutation = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment: validEnvironment,
|
||||
verifyLocalEvidence: acceptLocalEvidence,
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
|
||||
expect({
|
||||
valid: valid.status,
|
||||
absent: absent.status,
|
||||
wrongDigest: wrongDigest.status,
|
||||
replayedNonce: replayedNonce.status,
|
||||
postAttestationMutation: postAttestationMutation.status,
|
||||
}).toEqual({
|
||||
valid: "PASS",
|
||||
absent: "FAIL_UNVERIFIED",
|
||||
wrongDigest: "FAIL_UNVERIFIED",
|
||||
replayedNonce: "FAIL_UNVERIFIED",
|
||||
postAttestationMutation: "FAIL_UNVERIFIED",
|
||||
});
|
||||
expect(replayedNonce.failures).toContain("vulnerability report invocation nonce mismatch");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a self-consistent candidate that merely claims localStatus PASS", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "promotion-local-status-"));
|
||||
try {
|
||||
const manifest = await createMinimalCandidateTree(root);
|
||||
const environment = await writeProviderEnvironment(root, manifest);
|
||||
const localVerificationPath = path.join(
|
||||
root,
|
||||
"artifacts/security/supply-chain-verification.json",
|
||||
);
|
||||
const before = await readFile(localVerificationPath, "utf8");
|
||||
const result = await verifyPromotionInputs({
|
||||
artifactType: "provider-verification",
|
||||
repositoryRoot: root,
|
||||
environment,
|
||||
nowEpochMs: () => NOW,
|
||||
});
|
||||
|
||||
expect(result.status).toBe("FAIL_UNVERIFIED");
|
||||
expect(result.failures).toEqual(
|
||||
expect.arrayContaining([
|
||||
"local evidence assessment is missing or invalid",
|
||||
"local supply-chain evidence is not PASS",
|
||||
]),
|
||||
);
|
||||
expect(await readFile(localVerificationPath, "utf8")).toBe(before);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("derives a stable supply-chain timestamp from the immutable build epoch", () => {
|
||||
const input = {
|
||||
generatedAt: "2026-08-01T00:00:00.000Z",
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { lstat, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
loadCiGateContract,
|
||||
parseCiGateContract,
|
||||
} from "../../scripts/contracts/ci-gates.ts";
|
||||
import { generateCiWorkflow } from "../../scripts/generate-ci-workflow.ts";
|
||||
import { validatePackageScriptGraph } from "../../scripts/lib/package-script-graph.ts";
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe("selective Task 3 contract closure", () => {
|
||||
it("builds a private offline aggregate-cgroup provider launch without argv secrets", async () => {
|
||||
const {
|
||||
encodeProviderBwrapInput,
|
||||
encodeProviderScopeFrame,
|
||||
formatProviderCgroupUnitName,
|
||||
systemctlKillProviderArguments,
|
||||
systemdRunProviderArguments,
|
||||
} = await import("../../scripts/lib/provider-cgroup.ts");
|
||||
const unit = formatProviderCgroupUnitName(
|
||||
"vulnerability",
|
||||
42,
|
||||
"0123456789abcdef01234567",
|
||||
);
|
||||
const command = "node provider.mjs --token command-secret";
|
||||
const credential = "credential-secret";
|
||||
const launch = systemdRunProviderArguments(
|
||||
unit,
|
||||
1_800_000,
|
||||
1_200,
|
||||
"/trusted/node",
|
||||
"/workspace/scripts/lib/provider-scope-wrapper.ts",
|
||||
"/exact/report.json",
|
||||
12,
|
||||
34,
|
||||
);
|
||||
|
||||
expect(launch).toEqual(expect.arrayContaining([
|
||||
"--scope",
|
||||
"--property=MemoryMax=1073741824",
|
||||
"--property=MemorySwapMax=0",
|
||||
"--property=TasksMax=64",
|
||||
"--property=CPUQuota=100%",
|
||||
"--property=KillMode=control-group",
|
||||
"/trusted/node",
|
||||
"/workspace/scripts/lib/provider-scope-wrapper.ts",
|
||||
]));
|
||||
expect(launch.join("\0")).not.toContain(command);
|
||||
expect(launch.join("\0")).not.toContain(credential);
|
||||
expect(
|
||||
encodeProviderBwrapInput(
|
||||
["--unshare-net", "--bind", "/exact/report.json", "/exact/report.json"],
|
||||
{ PROVIDER_COMMAND: command, PROVIDER_CREDENTIAL: credential },
|
||||
),
|
||||
).toEqual(expect.any(Buffer));
|
||||
expect(systemctlKillProviderArguments(unit)).toEqual([
|
||||
"--user",
|
||||
"kill",
|
||||
"--kill-whom=all",
|
||||
"--signal=SIGKILL",
|
||||
unit,
|
||||
]);
|
||||
const frame = encodeProviderScopeFrame({
|
||||
bwrapInput: Buffer.from("private-bwrap-vector\0"),
|
||||
reportPath: "/exact/report.json",
|
||||
reportDev: 12,
|
||||
reportIno: 34,
|
||||
});
|
||||
expect(frame.readUInt32BE(0)).toBe(frame.byteLength - 4);
|
||||
expect(frame.subarray(4).toString("utf8")).toContain(
|
||||
Buffer.from("private-bwrap-vector\0").toString("base64"),
|
||||
);
|
||||
expect(launch.join("\0")).not.toContain("private-bwrap-vector");
|
||||
});
|
||||
|
||||
it("removes only the pinned raw inode during parent-loss cleanup", async () => {
|
||||
const { cleanupOwnedProviderReport } = await import(
|
||||
"../../scripts/lib/provider-raw-cleanup.ts"
|
||||
);
|
||||
const root = await mkdtemp(path.join(tmpdir(), "provider-raw-cleanup-"));
|
||||
roots.push(root);
|
||||
const reportPath = path.join(root, "raw.json");
|
||||
const originalPath = path.join(root, "original.json");
|
||||
await writeFile(reportPath, "owned\n");
|
||||
const identity = await lstat(reportPath);
|
||||
await rename(reportPath, originalPath);
|
||||
await writeFile(reportPath, "unrelated\n");
|
||||
|
||||
await expect(cleanupOwnedProviderReport({
|
||||
reportPath,
|
||||
reportDev: identity.dev,
|
||||
reportIno: identity.ino,
|
||||
})).resolves.toBe(false);
|
||||
await expect(readFile(reportPath, "utf8")).resolves.toBe("unrelated\n");
|
||||
|
||||
await rm(reportPath);
|
||||
await rename(originalPath, reportPath);
|
||||
await expect(cleanupOwnedProviderReport({
|
||||
reportPath,
|
||||
reportDev: identity.dev,
|
||||
reportIno: identity.ino,
|
||||
})).resolves.toBe(true);
|
||||
await expect(lstat(reportPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(readdir(root)).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("uses early liveness EOF to clean the pinned raw file without waiting for a command frame", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "provider-scope-eof-"));
|
||||
roots.push(root);
|
||||
const reportPath = path.join(root, "raw.json");
|
||||
await writeFile(reportPath, "partial\n");
|
||||
const identity = await lstat(reportPath);
|
||||
const child = spawn(process.execPath, [
|
||||
path.resolve("scripts/lib/provider-scope-wrapper.ts"),
|
||||
"1",
|
||||
reportPath,
|
||||
String(identity.dev),
|
||||
String(identity.ino),
|
||||
], { stdio: ["pipe", "pipe", "pipe"] });
|
||||
const completion = waitForChildResult(child);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 75));
|
||||
expect(child.exitCode).toBeNull();
|
||||
await expect(readFile(reportPath, "utf8")).resolves.toBe("partial\n");
|
||||
child.stdin.end();
|
||||
const result = await within(completion, 1_000, "provider scope EOF close");
|
||||
expect(result).toEqual({ code: 125, signal: null });
|
||||
await expect(lstat(reportPath)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
await expect(readdir(root)).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("tracks npm run-script dependencies instead of bypassing the graph", () => {
|
||||
expect(validatePackageScriptGraph({ root: "npm run-script missing" }, "root"))
|
||||
.toContain("package script missing: root -> missing");
|
||||
});
|
||||
|
||||
it("accepts only the canonical exact-count authority and rejects orphan retention", async () => {
|
||||
const canonical = await loadCiGateContract(process.cwd());
|
||||
expect(canonical.gates).toHaveLength(26);
|
||||
expect(canonical.commands).toHaveLength(81);
|
||||
expect(canonical.gates.reduce((sum, gate) => sum + gate.commandIds.length, 0)).toBe(93);
|
||||
expect(canonical.artifacts).toHaveLength(105);
|
||||
expect(canonical.stages).toHaveLength(5);
|
||||
expect(canonical.retention.classes).toHaveLength(5);
|
||||
|
||||
const orphan = JSON.parse(JSON.stringify(canonical)) as Record<string, any>;
|
||||
orphan.retention.classes.push({ id: "unused", policy: "never referenced" });
|
||||
expect(() => parseCiGateContract(orphan)).toThrow(/five canonical retention|orphan retention/u);
|
||||
});
|
||||
|
||||
it("rejects the retired validate-candidate-archive grammar", async () => {
|
||||
const canonical = JSON.parse(
|
||||
JSON.stringify(await loadCiGateContract(process.cwd())),
|
||||
) as Record<string, any>;
|
||||
canonical.jobs.find((job: Record<string, any>) => job.id === "vulnerability_provider")
|
||||
.steps.splice(4, 0, {
|
||||
kind: "validate-candidate-archive",
|
||||
archivePath: ".release/candidate/release-candidate.tar.gz",
|
||||
});
|
||||
expect(() => parseCiGateContract(canonical)).toThrow(
|
||||
/invalid discriminator|forbidden|canonical job step sequence/iu,
|
||||
);
|
||||
});
|
||||
|
||||
it("publishes a generated workflow as exactly 0644 under a restrictive umask", async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), "ci-workflow-mode-"));
|
||||
roots.push(root);
|
||||
await mkdir(path.join(root, ".gitea/workflows"), { recursive: true });
|
||||
const previous = process.umask(0o777);
|
||||
try {
|
||||
const contract = await loadCiGateContract(process.cwd());
|
||||
await generateCiWorkflow({ root, contract, check: false });
|
||||
} finally {
|
||||
process.umask(previous);
|
||||
}
|
||||
const target = path.join(root, ".gitea/workflows/quality-gates.yml");
|
||||
const metadata = await lstat(target);
|
||||
expect(metadata.mode & 0o777).toBe(0o644);
|
||||
expect((await readFile(target, "utf8")).startsWith("# GENERATED FILE")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForChildResult(
|
||||
child: ReturnType<typeof spawn>,
|
||||
): Promise<Readonly<{ code: number | null; signal: NodeJS.Signals | null }>> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
child.once("error", reject);
|
||||
child.once("close", (code, signal) => resolve({ code, signal }));
|
||||
});
|
||||
}
|
||||
|
||||
async function within<T>(operation: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
operation,
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
timer = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,25 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("validated JSON artifact writer", () => {
|
||||
it("returns the exact schema-validated bytes used by durable publication", async () => {
|
||||
const artifact = await import("../../scripts/lib/validated-json-artifact.ts") as
|
||||
Record<string, unknown>;
|
||||
expect(artifact.serializeValidatedJsonArtifact).toBeTypeOf("function");
|
||||
const serializeValidatedJsonArtifact = artifact.serializeValidatedJsonArtifact as (
|
||||
input: Readonly<{ path: string; schema: z.ZodType; value: unknown }>,
|
||||
) => Buffer;
|
||||
expect(serializeValidatedJsonArtifact({
|
||||
path: "/unused/artifact.json",
|
||||
schema: z.object({ value: z.string() }).strict(),
|
||||
value: { value: "sealed" },
|
||||
})).toEqual(Buffer.from('{\n "value": "sealed"\n}\n'));
|
||||
expect(() => serializeValidatedJsonArtifact({
|
||||
path: "/unused/artifact.json",
|
||||
schema: z.object({ value: z.string() }).strict(),
|
||||
value: { value: 7 },
|
||||
})).toThrow();
|
||||
});
|
||||
|
||||
it("syncs an O_NOFOLLOW exclusive temp and its directory around rename", async () => {
|
||||
const events: string[] = [];
|
||||
let openFlags = 0;
|
||||
|
||||
Reference in New Issue
Block a user