97 lines
5.1 KiB
JavaScript
97 lines
5.1 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
import { extname, join, relative } from "node:path";
|
|
|
|
const SHA40 = /^[a-f0-9]{40}$/i;
|
|
const SHA64 = /^[a-f0-9]{64}$/i;
|
|
const VERSION = /^[1-9][0-9]*\.[0-9]+\.[0-9]+\.[0-9]+$/;
|
|
const ABSOLUTE_PATH = /(?:[A-Za-z]:[\\/](?:Users|Documents)[\\/][^\\/"'\s]+[\\/]|\/Users\/[^/"'\s]+\/|\/home\/[^/"'\s]+\/)/;
|
|
const CREDENTIAL = /\b(?:sk|key)-[A-Za-z0-9_-]{16,}\b|-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i;
|
|
const TEXT_EXTENSIONS = new Set([".cjs", ".cs", ".css", ".html", ".js", ".json", ".mjs", ".ts", ".tsx", ".txt", ".xml", ".yaml", ".yml"]);
|
|
|
|
export const DEFERRED_EXTERNAL_TASKS = Object.freeze(["TASK-WP7-03", "TASK-WP7-04"]);
|
|
|
|
export function buildFinalReleaseRecord({ appVersion, browsers, buildCommit, frozenFromCommit, recordedAt, windows }) {
|
|
const record = {
|
|
appVersion,
|
|
browsers: browsers.map(({ brand, fullVersion }) => ({ brand, fullVersion })),
|
|
buildCommit: buildCommit.toLowerCase(),
|
|
deferredExternalTasks: [...DEFERRED_EXTERNAL_TASKS],
|
|
finalRelease: true,
|
|
fixedPort: 43121,
|
|
frozenFromCommit: frozenFromCommit.toLowerCase(),
|
|
recordedAt,
|
|
releaseStatus: "first_version_internal",
|
|
schemaVersion: "1.0",
|
|
windows: { arch: windows.arch, build: windows.build, displayVersion: windows.displayVersion },
|
|
};
|
|
return validateFinalReleaseRecord(record);
|
|
}
|
|
|
|
export function validateFinalReleaseRecord(record) {
|
|
const errors = [];
|
|
if (record?.schemaVersion !== "1.0") errors.push("schemaVersion");
|
|
if (record?.releaseStatus !== "first_version_internal") errors.push("releaseStatus");
|
|
if (record?.finalRelease !== true) errors.push("finalRelease");
|
|
if (record?.fixedPort !== 43121) errors.push("fixedPort");
|
|
if (!SHA40.test(record?.buildCommit ?? "")) errors.push("buildCommit");
|
|
if (!SHA40.test(record?.frozenFromCommit ?? "")) errors.push("frozenFromCommit");
|
|
if (!Number.isFinite(Date.parse(record?.recordedAt ?? ""))) errors.push("recordedAt");
|
|
if (!Array.isArray(record?.deferredExternalTasks) || record.deferredExternalTasks.join("|") !== DEFERRED_EXTERNAL_TASKS.join("|")) errors.push("deferredExternalTasks");
|
|
if (record?.windows?.arch !== "x64" || !/^\d+\.\d+$/.test(record?.windows?.build ?? "")) errors.push("windows");
|
|
if (!Array.isArray(record?.browsers) || record.browsers.length !== 2) {
|
|
errors.push("browsers");
|
|
} else {
|
|
const brands = record.browsers.map(({ brand }) => brand).sort();
|
|
if (brands.join("|") !== "Google Chrome|Microsoft Edge") errors.push("browserBrands");
|
|
for (const browser of record.browsers) {
|
|
if (!VERSION.test(browser.fullVersion ?? "")) errors.push(`${browser.brand}.fullVersion`);
|
|
if ("path" in browser || "executablePath" in browser || "executableSha256" in browser) errors.push(`${browser.brand}.privateMetadata`);
|
|
}
|
|
}
|
|
const serialized = JSON.stringify(record);
|
|
if (ABSOLUTE_PATH.test(serialized) || CREDENTIAL.test(serialized)) errors.push("sensitiveValue");
|
|
if (errors.length > 0) throw new Error(`WP7_07_RELEASE_INVALID:${[...new Set(errors)].join(",")}`);
|
|
return record;
|
|
}
|
|
|
|
export function sha256File(path) {
|
|
return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase();
|
|
}
|
|
|
|
export function scanReleaseFiles({ roots, allowedFixturePaths = [] }) {
|
|
const allowed = new Set(allowedFixturePaths.map((value) => value.replaceAll("\\", "/")));
|
|
const findings = [];
|
|
let scannedFiles = 0;
|
|
function visit(root, current = root) {
|
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
if ([".git", ".pnpm-store", "node_modules", "bin", "obj"].includes(entry.name)) continue;
|
|
const path = join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
visit(root, path);
|
|
continue;
|
|
}
|
|
if (!entry.isFile()) continue;
|
|
scannedFiles += 1;
|
|
if (!TEXT_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue;
|
|
const logicalPath = relative(root, path).replaceAll("\\", "/");
|
|
const content = readFileSync(path, "utf8");
|
|
if (!allowed.has(logicalPath) && ABSOLUTE_PATH.test(content)) findings.push({ path: logicalPath, rule: "absolute_user_path" });
|
|
if (!allowed.has(logicalPath) && CREDENTIAL.test(content)) findings.push({ path: logicalPath, rule: "credential_shape" });
|
|
}
|
|
}
|
|
for (const root of roots) {
|
|
if (!statSync(root).isDirectory()) throw new Error(`WP7_07_SCAN_ROOT_INVALID:${root}`);
|
|
visit(root);
|
|
}
|
|
return { findings, scanned_files: scannedFiles, status: findings.length === 0 ? "passed" : "failed" };
|
|
}
|
|
|
|
export function validateFinalEvidence({ packageManifest, release, releaseSha256, scan }) {
|
|
validateFinalReleaseRecord(release);
|
|
if (!SHA64.test(releaseSha256 ?? "")) throw new Error("WP7_07_RELEASE_HASH_INVALID");
|
|
if (packageManifest?.release_status !== release.releaseStatus || !SHA64.test(packageManifest?.zip_sha256 ?? "")) throw new Error("WP7_07_PACKAGE_MANIFEST_INVALID");
|
|
if (scan?.status !== "passed" || scan.findings?.length !== 0) throw new Error("WP7_07_LEAK_SCAN_FAILED");
|
|
return { release_sha256: releaseSha256.toUpperCase(), status: "passed", zip_sha256: packageManifest.zip_sha256.toUpperCase() };
|
|
}
|