refactor: adapter 구현중..
This commit is contained in:
@@ -31,6 +31,8 @@ import {
|
||||
import { assertMatchesJsonSchema } from "./json-schema.ts";
|
||||
import {
|
||||
LOCAL_EVIDENCE_ASSESSMENT_PATH,
|
||||
LOCAL_EVIDENCE_POLICY_INPUT_PATHS,
|
||||
LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS,
|
||||
RELEASE_CANDIDATE_EVIDENCE_PATHS,
|
||||
RELEASE_CANDIDATE_MANIFEST_PATH,
|
||||
collectDistOutputs,
|
||||
@@ -54,9 +56,12 @@ import {
|
||||
parseRepositoryFileInventoryPolicy,
|
||||
} from "./repository-file-inventory.ts";
|
||||
import {
|
||||
parseSecretScanPolicy,
|
||||
secretScanSarifSchema,
|
||||
evaluateRepositorySecretScan,
|
||||
verifyStoredSecretScan,
|
||||
} from "./secret-scan-evaluator.ts";
|
||||
import { secretScanRules } from "./secret-scan.ts";
|
||||
import {
|
||||
isValidSha512Integrity,
|
||||
parsePnpmLockfilePackages,
|
||||
@@ -301,37 +306,10 @@ export async function verifyLocalSupplyChainEvidence(
|
||||
export const LOCAL_EVIDENCE_VERIFIER_ID =
|
||||
"clean-architecture-frontend-template/local-evidence-verifier";
|
||||
export const LOCAL_EVIDENCE_VERIFIER_VERSION = "1";
|
||||
export const LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS = Object.freeze([
|
||||
"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/supply-chain.ts",
|
||||
"scripts/lib/validated-json-artifact.ts",
|
||||
"src/contracts/release-artifacts.ts",
|
||||
] as const);
|
||||
export const LOCAL_EVIDENCE_POLICY_INPUT_PATHS = Object.freeze([
|
||||
"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",
|
||||
...LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS,
|
||||
] as const);
|
||||
export {
|
||||
LOCAL_EVIDENCE_POLICY_INPUT_PATHS,
|
||||
LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS,
|
||||
} from "./release-candidate.ts";
|
||||
|
||||
export async function createLocalEvidenceAssessment(
|
||||
repositoryRoot = process.cwd(),
|
||||
@@ -360,7 +338,7 @@ export async function createLocalEvidenceAssessment(
|
||||
files: evidenceInputs,
|
||||
};
|
||||
const evaluated = await evaluateProducerLocalChecks(root, candidate);
|
||||
const [build, release, provenance, supply, sbomDocument, policyInputs] = await Promise.all([
|
||||
const [build, release, provenance, supply, sbomDocument, policyInputs, secretScan] = await Promise.all([
|
||||
readJson(root, "artifacts/release/build-manifest.json").then((value) =>
|
||||
buildManifestArtifactSchema.parse(value),
|
||||
),
|
||||
@@ -381,6 +359,7 @@ export async function createLocalEvidenceAssessment(
|
||||
digestInput(root, policyPath),
|
||||
),
|
||||
),
|
||||
evaluateRepositorySecretScan({ repositoryRoot: root }),
|
||||
]);
|
||||
const identityFailures: string[] = [];
|
||||
if (build.commitSha !== release.commitSha) {
|
||||
@@ -421,6 +400,15 @@ export async function createLocalEvidenceAssessment(
|
||||
if (verifierSources.length !== LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS.length) {
|
||||
throw new Error("local assessment verifier source set is incomplete");
|
||||
}
|
||||
const secretPolicy = policyInputs.find(
|
||||
({ path: policyPath }) => policyPath === "config/security/secret-scan-policy.json",
|
||||
);
|
||||
const secretSarif = evidenceInputs.find(
|
||||
({ path: evidencePath }) => evidencePath === "artifacts/security/scan.sarif",
|
||||
);
|
||||
if (!secretPolicy || !secretSarif) {
|
||||
throw new Error("local assessment secret scan inputs are incomplete");
|
||||
}
|
||||
return localEvidenceAssessmentArtifactSchema.parse({
|
||||
schemaVersion: 1,
|
||||
artifactType: "local-evidence-assessment",
|
||||
@@ -440,6 +428,11 @@ export async function createLocalEvidenceAssessment(
|
||||
lockfileSha256: candidate.lockfileSha256,
|
||||
sbomSha256: sbom.sha256,
|
||||
},
|
||||
secretScan: {
|
||||
policySha256: secretPolicy.sha256,
|
||||
sarifSha256: secretSarif.sha256,
|
||||
scanInputSha256: secretScan.scanInputSha256,
|
||||
},
|
||||
policyInputs,
|
||||
evidenceInputs,
|
||||
checks,
|
||||
@@ -458,6 +451,7 @@ type LocalCheckName =
|
||||
async function evaluateProducerLocalChecks(
|
||||
root: string,
|
||||
candidate: ReleaseCandidateManifest,
|
||||
options: Readonly<{ archived?: boolean }> = {},
|
||||
): Promise<Readonly<{
|
||||
checks: Readonly<Record<LocalCheckName, "PASS" | "FAIL">>;
|
||||
failures: readonly string[];
|
||||
@@ -490,13 +484,16 @@ async function evaluateProducerLocalChecks(
|
||||
};
|
||||
|
||||
await evaluate("release", async () => {
|
||||
const [build, release, stored] = await Promise.all([
|
||||
const [build, release, runtime, stored] = await Promise.all([
|
||||
readJson(root, "artifacts/release/build-manifest.json").then((value) =>
|
||||
buildManifestArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "dist/release-manifest.json").then((value) =>
|
||||
releaseManifestArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "dist/config.json").then((value) =>
|
||||
runtimeConfigArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/release/verification.json").then((value) =>
|
||||
releaseVerificationArtifactSchema.parse(value),
|
||||
),
|
||||
@@ -522,30 +519,104 @@ async function evaluateProducerLocalChecks(
|
||||
) {
|
||||
diagnostics.push("stored release verification is not a coherent PASS");
|
||||
}
|
||||
if (!runtime.BUILD_ID || !runtime.RELEASE_ID) {
|
||||
diagnostics.push("runtime release identity is missing");
|
||||
} else {
|
||||
const apiContractVersion =
|
||||
release.schemaVersion === 1 && "API_CONTRACT_VERSION" in runtime
|
||||
? runtime.API_CONTRACT_VERSION
|
||||
: undefined;
|
||||
const coherence = await verifyReleaseRuntimeCoherence({
|
||||
release,
|
||||
runtime: {
|
||||
BUILD_ID: runtime.BUILD_ID,
|
||||
RELEASE_ID: runtime.RELEASE_ID,
|
||||
CONFIG_SCHEMA_VERSION: runtime.CONFIG_SCHEMA_VERSION,
|
||||
...(apiContractVersion === undefined
|
||||
? {}
|
||||
: { API_CONTRACT_VERSION: apiContractVersion }),
|
||||
},
|
||||
contractPackages: EXPECTED_CONTRACT_SET_PACKAGES,
|
||||
});
|
||||
diagnostics.push(...coherence.mismatches.map((item) => `runtime:${item}`));
|
||||
}
|
||||
diagnostics.push(...(await verifyBuildManifestOutputs(build, { repositoryRoot: root })));
|
||||
return diagnostics;
|
||||
});
|
||||
await evaluate("supplyChain", async () => {
|
||||
const [supply, coherence] = await Promise.all([
|
||||
const [inventory, sbom, provenance, supply, coherence, lockfileBytes] = await Promise.all([
|
||||
readJson(root, "artifacts/release/dependency-inventory.json").then((value) =>
|
||||
dependencyInventoryArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/release/sbom.cdx.json").then((value) =>
|
||||
sbomArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/release/provenance.json").then((value) =>
|
||||
provenanceArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/security/supply-chain-verification.json").then((value) =>
|
||||
supplyChainVerificationArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(root, "artifacts/security/supply-chain-coherence.json").then((value) =>
|
||||
supplyChainCoherenceReportSchema.parse(value),
|
||||
),
|
||||
readFile(path.join(root, "pnpm-lock.yaml")),
|
||||
]);
|
||||
const diagnostics: string[] = [];
|
||||
const lockfileSha256 = createHash("sha256").update(lockfileBytes).digest("hex");
|
||||
const outputs = await collectDistOutputs(root);
|
||||
const currentDistSha256 = distSha256(outputs);
|
||||
const sbomSha256 = supplyChainDigest(sbom);
|
||||
const independentlyCoherent = verifySupplyChainCoherence(
|
||||
sbom,
|
||||
inventory,
|
||||
provenance,
|
||||
currentDistSha256,
|
||||
);
|
||||
if (
|
||||
supply.localStatus !== "PASS" ||
|
||||
supply.failures.length > 0 ||
|
||||
supply.distSha256 !== candidate.distSha256 ||
|
||||
supply.lockfileSha256 !== candidate.lockfileSha256 ||
|
||||
supply.sbomSha256 !== sbomSha256 ||
|
||||
supply.sourceSetSha256 !== provenance.predicate.materials.sourceSetSha256 ||
|
||||
inventory.lockfileSha256 !== lockfileSha256 ||
|
||||
candidate.lockfileSha256 !== lockfileSha256 ||
|
||||
candidate.distSha256 !== currentDistSha256 ||
|
||||
coherence.status !== "PASS" ||
|
||||
coherence.failures.length > 0 ||
|
||||
coherence.distSha256 !== candidate.distSha256 ||
|
||||
coherence.lockfileSha256 !== candidate.lockfileSha256
|
||||
coherence.lockfileSha256 !== candidate.lockfileSha256 ||
|
||||
coherence.sbomSha256 !== sbomSha256 ||
|
||||
coherence.dependencyCount !== inventory.dependencies.length ||
|
||||
independentlyCoherent.failures.length > 0
|
||||
) {
|
||||
diagnostics.push("stored supply-chain evidence is not a coherent PASS");
|
||||
}
|
||||
diagnostics.push(...verifyLocalSupplyChainDefaults(supply));
|
||||
diagnostics.push(
|
||||
...verifyStoredDistChecksums(
|
||||
outputs,
|
||||
await readFile(path.join(root, "artifacts/release/checksums.txt"), "utf8"),
|
||||
),
|
||||
);
|
||||
const lockRows = parsePnpmLockfilePackages(lockfileBytes.toString("utf8"));
|
||||
const inventoryByIdentity = new Map(
|
||||
inventory.dependencies.map((entry) => [`${entry.name}@${entry.version}`, entry] as const),
|
||||
);
|
||||
if (lockRows.length !== inventory.dependencies.length) {
|
||||
diagnostics.push("transitive dependency count differs from lockfile");
|
||||
}
|
||||
for (const lockRow of lockRows) {
|
||||
const dependency = inventoryByIdentity.get(`${lockRow.name}@${lockRow.version}`);
|
||||
if (
|
||||
!dependency ||
|
||||
dependency.integrity !== lockRow.integrity ||
|
||||
!isValidSha512Integrity(lockRow.integrity)
|
||||
) {
|
||||
diagnostics.push(`lockfile inventory integrity mismatch: ${lockRow.name}@${lockRow.version}`);
|
||||
}
|
||||
}
|
||||
return diagnostics;
|
||||
});
|
||||
await evaluate("dependencyPolicy", async () => {
|
||||
@@ -596,6 +667,25 @@ async function evaluateProducerLocalChecks(
|
||||
);
|
||||
});
|
||||
await evaluate("secretScan", async () => {
|
||||
if (options.archived) {
|
||||
const policy = parseSecretScanPolicy(
|
||||
await readJson(root, "config/security/secret-scan-policy.json"),
|
||||
);
|
||||
const sarif = secretScanSarifSchema.parse(
|
||||
await readJson(root, "artifacts/security/scan.sarif"),
|
||||
);
|
||||
const diagnostics: string[] = [];
|
||||
if (
|
||||
policy.trackedRoots.length === 0 ||
|
||||
policy.generatedRoots.length === 0 ||
|
||||
sarif.runs[0]!.results.length > 0 ||
|
||||
JSON.stringify(sarif.runs[0]!.tool.driver.rules.map(({ id }) => id)) !==
|
||||
JSON.stringify(secretScanRules().map(({ id }) => id))
|
||||
) {
|
||||
diagnostics.push("archived secret scan is not an independently valid PASS");
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
const evaluation = await evaluateRepositorySecretScan({ repositoryRoot: root });
|
||||
return verifyStoredSecretScan(
|
||||
evaluation,
|
||||
@@ -640,6 +730,11 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
|
||||
sourceRevision: string;
|
||||
sourceSetSha256: string;
|
||||
assessmentSha256: string;
|
||||
secretScan: Readonly<{
|
||||
policySha256: string;
|
||||
sarifSha256: string;
|
||||
scanInputSha256: string;
|
||||
}>;
|
||||
}>;
|
||||
failures: readonly string[];
|
||||
}>> {
|
||||
@@ -696,6 +791,25 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
|
||||
if (JSON.stringify(policyPaths) !== JSON.stringify(LOCAL_EVIDENCE_POLICY_INPUT_PATHS)) {
|
||||
failures.push("local assessment policyInputs exact set mismatch");
|
||||
}
|
||||
for (const policyInput of assessment.policyInputs) {
|
||||
try {
|
||||
const bytes = await readFile(path.join(extractionRoot, policyInput.path));
|
||||
const member = extractedManifest.files.find(
|
||||
({ path: memberPath }) => memberPath === policyInput.path,
|
||||
);
|
||||
if (
|
||||
!member ||
|
||||
member.bytes !== policyInput.bytes ||
|
||||
member.sha256 !== policyInput.sha256 ||
|
||||
bytes.byteLength !== policyInput.bytes ||
|
||||
createHash("sha256").update(bytes).digest("hex") !== policyInput.sha256
|
||||
) {
|
||||
failures.push(`archived policy input binding mismatch: ${policyInput.path}`);
|
||||
}
|
||||
} catch {
|
||||
failures.push(`archived policy input is missing or invalid: ${policyInput.path}`);
|
||||
}
|
||||
}
|
||||
const verifierSources = assessment.policyInputs.filter(({ path: policyPath }) =>
|
||||
(LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS as readonly string[]).includes(policyPath),
|
||||
);
|
||||
@@ -715,6 +829,12 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
|
||||
const sbom = extractedManifest.files.find(
|
||||
({ path: memberPath }) => memberPath === "artifacts/release/sbom.cdx.json",
|
||||
);
|
||||
const secretPolicy = extractedManifest.files.find(
|
||||
({ path: memberPath }) => memberPath === "config/security/secret-scan-policy.json",
|
||||
);
|
||||
const secretSarif = extractedManifest.files.find(
|
||||
({ path: memberPath }) => memberPath === "artifacts/security/scan.sarif",
|
||||
);
|
||||
if (
|
||||
assessment.candidate.distSha256 !== extractedManifest.distSha256 ||
|
||||
assessment.candidate.lockfileSha256 !== extractedManifest.lockfileSha256 ||
|
||||
@@ -723,10 +843,54 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
|
||||
) {
|
||||
failures.push("local assessment candidate digest binding mismatch");
|
||||
}
|
||||
if (
|
||||
!secretPolicy ||
|
||||
!secretSarif ||
|
||||
assessment.secretScan.policySha256 !== secretPolicy.sha256 ||
|
||||
assessment.secretScan.sarifSha256 !== secretSarif.sha256
|
||||
) {
|
||||
failures.push("local assessment secret scan artifact binding mismatch");
|
||||
}
|
||||
if (assessment.status !== "PASS" || Object.values(assessment.checks).includes("FAIL")) {
|
||||
failures.push("local evidence assessment is not PASS");
|
||||
}
|
||||
|
||||
try {
|
||||
const [supply, coherence] = await Promise.all([
|
||||
readJson(extractionRoot, "artifacts/security/supply-chain-verification.json").then(
|
||||
(value) => supplyChainVerificationArtifactSchema.parse(value),
|
||||
),
|
||||
readJson(extractionRoot, "artifacts/security/supply-chain-coherence.json").then(
|
||||
(value) => supplyChainCoherenceReportSchema.parse(value),
|
||||
),
|
||||
]);
|
||||
if (
|
||||
supply.localStatus !== "PASS" ||
|
||||
supply.failures.length > 0 ||
|
||||
coherence.status !== "PASS" ||
|
||||
coherence.failures.length > 0
|
||||
) {
|
||||
failures.push("archived supply-chain subordinate evidence is not PASS");
|
||||
}
|
||||
} catch {
|
||||
failures.push("archived supply-chain subordinate evidence is missing or invalid");
|
||||
}
|
||||
|
||||
const independent = await evaluateProducerLocalChecks(
|
||||
extractionRoot,
|
||||
extractedManifest,
|
||||
{ archived: true },
|
||||
);
|
||||
if (
|
||||
independent.failures.length > 0 ||
|
||||
JSON.stringify(independent.checks) !== JSON.stringify(assessment.checks)
|
||||
) {
|
||||
failures.push(
|
||||
...independent.failures.map((failure) => `archived local check:${failure}`),
|
||||
);
|
||||
failures.push("archived local checks do not independently reproduce assessment PASS");
|
||||
}
|
||||
|
||||
const identities = await readArchivedIdentities(extractionRoot, failures);
|
||||
if (
|
||||
identities.buildRevision !== assessment.source.revision ||
|
||||
@@ -751,6 +915,7 @@ export async function verifyArchivedLocalEvidence(input: Readonly<{
|
||||
sourceRevision: passingAssessment.source.revision,
|
||||
sourceSetSha256: passingAssessment.source.sourceSetSha256,
|
||||
assessmentSha256,
|
||||
secretScan: passingAssessment.secretScan,
|
||||
})
|
||||
: null,
|
||||
failures: uniqueFailures,
|
||||
|
||||
Reference in New Issue
Block a user