refactor: adapter 구현중..

This commit is contained in:
DongHyeonka
2026-08-13 16:02:21 +09:00
parent 30ceac23c1
commit 4dc033cf33
72 changed files with 13370 additions and 1549 deletions
+200 -35
View File
@@ -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,