chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,381 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdir,
|
||||
readFile,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
|
||||
import {
|
||||
bundleOutputInventoryArtifactSchema,
|
||||
buildManifestArtifactSchema,
|
||||
dependencyDiffArtifactSchema,
|
||||
dependencyInventoryArtifactSchema,
|
||||
licenseReportArtifactSchema,
|
||||
provenanceArtifactSchema,
|
||||
sbomArtifactSchema,
|
||||
supplyChainVerificationArtifactSchema,
|
||||
vulnerabilityReportArtifactSchema,
|
||||
} from "./contracts/release-artifacts.ts";
|
||||
import {
|
||||
flattenPnpmDependencyTree,
|
||||
isValidSha512Integrity,
|
||||
parsePnpmLockfilePackages,
|
||||
supplyChainDigest,
|
||||
verifySupplyChainCoherence,
|
||||
} from "./lib/supply-chain.ts";
|
||||
import {
|
||||
createLocalVulnerabilityReport,
|
||||
distChecksumsText,
|
||||
LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS,
|
||||
recomputeDependencyEvidence,
|
||||
recomputeLicenseEvidence,
|
||||
} from "./lib/local-policy-evidence.ts";
|
||||
import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts";
|
||||
import { digestReleaseInputFiles } from "./lib/release-input-evidence.ts";
|
||||
import {
|
||||
buildRepositoryFileInventory,
|
||||
parseRepositoryFileInventoryPolicy,
|
||||
} from "./lib/repository-file-inventory.ts";
|
||||
import { collectDistOutputs, distSha256 } from "./lib/release-candidate.ts";
|
||||
import { deterministicSupplyChainGeneratedAt } from "./lib/supply-chain-time.ts";
|
||||
|
||||
type Document = Record<string, unknown>;
|
||||
|
||||
function isRecord(value: unknown): value is Document {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function documentValue(value: unknown, label: string): Document {
|
||||
if (!isRecord(value)) throw new Error(`${label} must be a JSON object`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function stringMap(value: unknown): Record<string, string> {
|
||||
if (!isRecord(value)) return {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function jsonDocument(file: string): Promise<Document> {
|
||||
const parsed: unknown = JSON.parse(await readFile(file, "utf8"));
|
||||
return documentValue(parsed, file);
|
||||
}
|
||||
|
||||
async function sha256File(file: string): Promise<string> {
|
||||
return createHash("sha256").update(await readFile(file)).digest("hex");
|
||||
}
|
||||
|
||||
async function optionalJson(file: string): Promise<Document | null> {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(await readFile(file, "utf8"));
|
||||
return isRecord(parsed) ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildDependencyInventory() {
|
||||
const packageJson = await jsonDocument("package.json");
|
||||
const lockfileText = await readFile("pnpm-lock.yaml", "utf8");
|
||||
const lockfileSha256 = createHash("sha256")
|
||||
.update(lockfileText)
|
||||
.digest("hex");
|
||||
const listed = spawnSync(
|
||||
"corepack",
|
||||
["pnpm", "list", "--json", "--depth", "Infinity"],
|
||||
{
|
||||
encoding: "utf8",
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
if (listed.status !== 0) {
|
||||
throw new Error(`pnpm dependency graph failed: ${listed.stderr}`);
|
||||
}
|
||||
const roots: unknown = JSON.parse(listed.stdout);
|
||||
const root = Array.isArray(roots) && isRecord(roots[0]) ? roots[0] : null;
|
||||
if (!root) throw new Error("pnpm dependency graph root is invalid");
|
||||
const flattened = await flattenPnpmDependencyTree(
|
||||
root,
|
||||
stringMap(packageJson.dependencies),
|
||||
stringMap(packageJson.devDependencies),
|
||||
);
|
||||
const lockRows = parsePnpmLockfilePackages(lockfileText);
|
||||
const lockByIdentity = new Map(
|
||||
lockRows.map((row) => [`${row.name}@${row.version}`, row]),
|
||||
);
|
||||
const failures: string[] = [];
|
||||
const dependencies = flattened.map((dependency) => {
|
||||
const identity = `${dependency.name}@${dependency.version}`;
|
||||
const lockRow = lockByIdentity.get(identity);
|
||||
if (!lockRow) failures.push(`dependency missing from lockfile: ${identity}`);
|
||||
if (lockRow && !isValidSha512Integrity(lockRow.integrity)) {
|
||||
failures.push(`dependency has invalid sha512 integrity: ${identity}`);
|
||||
}
|
||||
return {
|
||||
...dependency,
|
||||
integrity: lockRow?.integrity ?? "missing",
|
||||
};
|
||||
});
|
||||
const inventoryIds = new Set(
|
||||
dependencies.map((dependency) => `${dependency.name}@${dependency.version}`),
|
||||
);
|
||||
for (const lockRow of lockRows) {
|
||||
const identity = `${lockRow.name}@${lockRow.version}`;
|
||||
if (!inventoryIds.has(identity)) {
|
||||
failures.push(`transitive lockfile dependency omitted: ${identity}`);
|
||||
}
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
throw new Error(failures.join("\n"));
|
||||
}
|
||||
return {
|
||||
schemaVersion: 2,
|
||||
packageManager: String(packageJson.packageManager ?? ""),
|
||||
lockfileSha256,
|
||||
dependencyCount: dependencies.length,
|
||||
directDependencyCount: dependencies.filter((entry) => entry.direct).length,
|
||||
dependencies,
|
||||
};
|
||||
}
|
||||
|
||||
const packageJson = await jsonDocument("package.json");
|
||||
const buildManifest = buildManifestArtifactSchema.parse(
|
||||
await jsonDocument("artifacts/release/build-manifest.json"),
|
||||
);
|
||||
const secretScanPolicy = documentValue(
|
||||
JSON.parse(await readFile("config/security/secret-scan-policy.json", "utf8")),
|
||||
"secret scan policy",
|
||||
);
|
||||
const inventoryPolicy = parseRepositoryFileInventoryPolicy(secretScanPolicy);
|
||||
const repositoryInventory = await buildRepositoryFileInventory({
|
||||
trackedRoots: inventoryPolicy.trackedRoots,
|
||||
generatedRoots: inventoryPolicy.generatedRoots,
|
||||
optionalRoots: inventoryPolicy.optionalRoots,
|
||||
});
|
||||
const outputs = await collectDistOutputs();
|
||||
const distDigest = distSha256(outputs);
|
||||
const inventory = await buildDependencyInventory();
|
||||
const licensePolicy = JSON.parse(
|
||||
await readFile("config/security/dependency-policy.json", "utf8"),
|
||||
);
|
||||
|
||||
const baseline = await optionalJson(
|
||||
"config/security/dependency-baseline.json",
|
||||
);
|
||||
const baselineApproval = await optionalJson(
|
||||
"config/security/dependency-baseline.approval.json",
|
||||
);
|
||||
const dependencyEvidence = JSON.parse(
|
||||
await readFile(
|
||||
"config/security/dependency-change-evidence.json",
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
const skipsBaseline = process.argv.includes("--no-baseline");
|
||||
const dependencyPolicy = recomputeDependencyEvidence({
|
||||
inventory,
|
||||
baseline,
|
||||
baselineApproval,
|
||||
dependencyChangeEvidence: dependencyEvidence,
|
||||
skipBaseline: skipsBaseline,
|
||||
});
|
||||
const licenseEvidence = recomputeLicenseEvidence({
|
||||
inventory,
|
||||
policy: licensePolicy,
|
||||
});
|
||||
|
||||
const vulnerabilityReport = createLocalVulnerabilityReport(
|
||||
inventory.lockfileSha256,
|
||||
);
|
||||
|
||||
const sourceFiles = [...repositoryInventory.trackedFiles];
|
||||
const sourceSetSha256 = await digestReleaseInputFiles(sourceFiles);
|
||||
|
||||
const components = inventory.dependencies.map((dependency) => ({
|
||||
type: "library",
|
||||
"bom-ref": `pkg:npm/${encodeURIComponent(dependency.name)}@${dependency.version}`,
|
||||
name: dependency.name,
|
||||
version: dependency.version,
|
||||
scope: dependency.optional ? "optional" : "required",
|
||||
hashes: [
|
||||
{
|
||||
alg: "SHA-512",
|
||||
content: dependency.integrity.slice("sha512-".length),
|
||||
},
|
||||
],
|
||||
licenses:
|
||||
dependency.license === "NOASSERTION"
|
||||
? [{ expression: "NOASSERTION" }]
|
||||
: [{ expression: dependency.license }],
|
||||
properties: [
|
||||
{ name: "ca:direct", value: String(dependency.direct) },
|
||||
{ name: "ca:scope", value: dependency.scope },
|
||||
],
|
||||
}));
|
||||
const serialSeed = supplyChainDigest({
|
||||
lockfileSha256: inventory.lockfileSha256,
|
||||
components: components.map((component) => component["bom-ref"]),
|
||||
});
|
||||
const sbom = {
|
||||
bomFormat: "CycloneDX",
|
||||
specVersion: "1.6",
|
||||
serialNumber: `urn:uuid:${serialSeed.slice(0, 8)}-${serialSeed.slice(8, 12)}-${serialSeed.slice(12, 16)}-${serialSeed.slice(16, 20)}-${serialSeed.slice(20, 32)}`,
|
||||
version: 1,
|
||||
metadata: {
|
||||
component: {
|
||||
type: "application",
|
||||
name: String(packageJson.name ?? ""),
|
||||
version: String(packageJson.version ?? ""),
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
name: "ca:lockfileSha256",
|
||||
value: inventory.lockfileSha256,
|
||||
},
|
||||
],
|
||||
},
|
||||
components,
|
||||
dependencies: inventory.dependencies.map((dependency) => ({
|
||||
ref: `pkg:npm/${encodeURIComponent(dependency.name)}@${dependency.version}`,
|
||||
dependsOn: dependency.dependencies.map((identity) => {
|
||||
const separator = identity.lastIndexOf("@");
|
||||
return `pkg:npm/${encodeURIComponent(identity.slice(0, separator))}@${identity.slice(separator + 1)}`;
|
||||
}),
|
||||
})),
|
||||
};
|
||||
|
||||
const provenance = {
|
||||
_type: "https://in-toto.io/Statement/v1",
|
||||
subject: [{ name: "dist", digest: { sha256: distDigest } }],
|
||||
predicateType: "https://slsa.dev/provenance/v1",
|
||||
predicate: {
|
||||
buildDefinition: {
|
||||
buildType: "https://vite.dev/build/v1",
|
||||
externalParameters: {
|
||||
nodeVersion: process.version,
|
||||
packageManager: String(packageJson.packageManager ?? ""),
|
||||
},
|
||||
internalParameters: {
|
||||
sourceSetSha256,
|
||||
},
|
||||
resolvedDependencies: [
|
||||
{
|
||||
uri: "pnpm-lock.yaml",
|
||||
digest: { sha256: inventory.lockfileSha256 },
|
||||
},
|
||||
],
|
||||
},
|
||||
runDetails: {
|
||||
builder: { id: "local:clean-architecture-frontend-template" },
|
||||
metadata: { invocationId: "LOCAL_UNSIGNED" },
|
||||
},
|
||||
materials: {
|
||||
lockfileSha256: inventory.lockfileSha256,
|
||||
sourceSetSha256,
|
||||
sbomSha256: supplyChainDigest(sbom),
|
||||
},
|
||||
},
|
||||
};
|
||||
const coherence = verifySupplyChainCoherence(
|
||||
sbom,
|
||||
inventory,
|
||||
provenance,
|
||||
distDigest,
|
||||
);
|
||||
|
||||
const localFailures = [
|
||||
...licenseEvidence.failures,
|
||||
...dependencyPolicy.failures,
|
||||
...coherence.failures,
|
||||
];
|
||||
const localPassed = localFailures.length === 0;
|
||||
const verification = {
|
||||
schemaVersion: 1,
|
||||
localStatus: localPassed ? "PASS" : "FAIL",
|
||||
...LOCAL_SUPPLY_CHAIN_UNVERIFIED_DEFAULTS,
|
||||
lockfileSha256: inventory.lockfileSha256,
|
||||
sourceSetSha256,
|
||||
distSha256: distDigest,
|
||||
sbomSha256: supplyChainDigest(sbom),
|
||||
dependencyDiff: dependencyPolicy.dependencyDiff,
|
||||
highRiskReview: dependencyPolicy.highRisk,
|
||||
failures: localFailures,
|
||||
};
|
||||
const bundleReport = {
|
||||
schemaVersion: 1,
|
||||
generatedAt: deterministicSupplyChainGeneratedAt({
|
||||
generatedAt: buildManifest.generatedAt,
|
||||
sourceDateEpoch: buildManifest.buildContext.sourceDateEpoch,
|
||||
}),
|
||||
context: {
|
||||
nodeVersion: process.version,
|
||||
packageManager: String(packageJson.packageManager ?? ""),
|
||||
runnerImage:
|
||||
process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
|
||||
},
|
||||
outputs,
|
||||
};
|
||||
const dependencyDiffReport = dependencyPolicy.report;
|
||||
const licenseReport = licenseEvidence.report;
|
||||
|
||||
await mkdir("artifacts/performance", { recursive: true });
|
||||
await mkdir("artifacts/release", { recursive: true });
|
||||
await mkdir("artifacts/security", { recursive: true });
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/performance/bundle.json",
|
||||
schema: bundleOutputInventoryArtifactSchema,
|
||||
value: bundleReport,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/release/dependency-inventory.json",
|
||||
schema: dependencyInventoryArtifactSchema,
|
||||
value: inventory,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/release/sbom.cdx.json",
|
||||
schema: sbomArtifactSchema,
|
||||
value: sbom,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/release/provenance.json",
|
||||
schema: provenanceArtifactSchema,
|
||||
value: provenance,
|
||||
});
|
||||
await writeFile(
|
||||
"artifacts/release/checksums.txt",
|
||||
distChecksumsText(outputs),
|
||||
);
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/security/dependency-diff.json",
|
||||
schema: dependencyDiffArtifactSchema,
|
||||
value: dependencyDiffReport,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/security/license-report.json",
|
||||
schema: licenseReportArtifactSchema,
|
||||
value: licenseReport,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/security/vulnerability-report.json",
|
||||
schema: vulnerabilityReportArtifactSchema,
|
||||
value: vulnerabilityReport,
|
||||
});
|
||||
await writeValidatedJsonArtifact({
|
||||
path: "artifacts/security/supply-chain-verification.json",
|
||||
schema: supplyChainVerificationArtifactSchema,
|
||||
value: verification,
|
||||
});
|
||||
|
||||
if (!localPassed) {
|
||||
process.stderr.write(
|
||||
`Local supply-chain verification failed:\n- ${localFailures.join("\n- ")}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(
|
||||
`Supply chain: LOCAL PASS (${inventory.dependencyCount} dependencies); promotion=${verification.promotionStatus}\n`,
|
||||
);
|
||||
Reference in New Issue
Block a user