feat(WP7-07): 增加最终发布冻结与泄漏扫描
Dada P0-A isolated Windows CI / validate-and-package (push) Canceled after 0s
Dada P0-A isolated Windows CI / validate-and-package (push) Canceled after 0s
This commit is contained in:
@@ -208,7 +208,7 @@ async function waitForHealth(child) {
|
||||
throw new Error("Packaged API did not become healthy on fixed port 43121.", { cause: lastError });
|
||||
}
|
||||
|
||||
async function verifyExtractedPackage(zipPath, packageName) {
|
||||
async function verifyExtractedPackage(zipPath, packageName, expectedSupport) {
|
||||
const extractRoot = mkdtempSync(join(tmpdir(), "dada-wp0-09-"));
|
||||
try {
|
||||
const escapedZip = zipPath.replaceAll("'", "''");
|
||||
@@ -237,19 +237,21 @@ async function verifyExtractedPackage(zipPath, packageName) {
|
||||
const health = await waitForHealth(api);
|
||||
const releaseGate = await fetch(`http://127.0.0.1:${fixedPort}/api/v1/support/check`, {
|
||||
body: JSON.stringify({
|
||||
brands: [{ brand: "Google Chrome", version: "150" }],
|
||||
full_version_list: [{ brand: "Google Chrome", version: "150.0.0.0" }],
|
||||
brands: [{ brand: expectedSupport.brand, version: String(expectedSupport.major) }],
|
||||
full_version_list: [{ brand: expectedSupport.brand, version: expectedSupport.fullVersion }],
|
||||
platform: "Windows",
|
||||
}),
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"sec-ch-ua": '"Google Chrome";v="150"',
|
||||
"sec-ch-ua-full-version-list": '"Google Chrome";v="150.0.0.0"',
|
||||
"sec-ch-ua": `"${expectedSupport.brand}";v="${expectedSupport.major}"`,
|
||||
"sec-ch-ua-full-version-list": `"${expectedSupport.brand}";v="${expectedSupport.fullVersion}"`,
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
if (releaseGate.status !== 426) throw new Error(`Candidate RELEASE.json unexpectedly passed with ${releaseGate.status}.`);
|
||||
if (releaseGate.status !== expectedSupport.statusCode) {
|
||||
throw new Error(`Packaged RELEASE.json support gate returned ${releaseGate.status}; expected ${expectedSupport.statusCode}.`);
|
||||
}
|
||||
return {
|
||||
api: { executable: "runtime/node.exe", health, pid: api.pid, release_gate: { status_code: releaseGate.status }, status: "passed" },
|
||||
native,
|
||||
@@ -291,7 +293,7 @@ function scanPackage(packageDirectory) {
|
||||
return { disallowed_matches: disallowedMatches, reparse_points: reparsePoints, scanned_files: files.length, status: disallowedMatches.length === 0 && reparsePoints.length === 0 ? "passed" : "failed" };
|
||||
}
|
||||
|
||||
export async function buildAndValidatePortablePackage({ evidenceDirectory, outputRoot }) {
|
||||
export async function buildAndValidatePortablePackage({ evidenceDirectory, outputRoot, releaseRecord }) {
|
||||
if (process.platform !== frozenRuntime.os || process.arch !== frozenRuntime.arch || process.version.slice(1) !== frozenRuntime.node) {
|
||||
throw new Error("Portable package build requires frozen Node 24.13.0 on win-x64.");
|
||||
}
|
||||
@@ -351,7 +353,8 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
|
||||
writeJson(join(packageDirectory, "LICENSES", "third-party.json"), { api: apiDependencies, runtime: { node: frozenRuntime.node }, schema_version: "1.0", worker: workerDependencies });
|
||||
|
||||
const commit = run("git", ["rev-parse", "HEAD"]);
|
||||
writeJson(join(packageDirectory, "RELEASE.json"), {
|
||||
const finalRelease = releaseRecord !== undefined;
|
||||
writeJson(join(packageDirectory, "RELEASE.json"), releaseRecord ?? {
|
||||
app_version: appVersion,
|
||||
browsers: [],
|
||||
build_commit: commit,
|
||||
@@ -360,16 +363,20 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
|
||||
windows_build: null,
|
||||
});
|
||||
writeFileSync(join(packageDirectory, "START-HERE.txt"), [
|
||||
"Dada P0-A candidate package",
|
||||
finalRelease ? "Dada P0-A first-version portable package" : "Dada P0-A candidate package",
|
||||
"",
|
||||
"This candidate is unsigned and is not a final P0-A release.",
|
||||
finalRelease
|
||||
? "This unsigned first-version package passed the local P0-A release gates recorded in RELEASE.json."
|
||||
: "This candidate is unsigned and is not a final P0-A release.",
|
||||
"Verify the adjacent SHA-256 file before first launch.",
|
||||
"Windows SmartScreen may warn on first launch because the executable is unsigned.",
|
||||
"For an antivirus alert, compare the package hash with the Gitea build record.",
|
||||
"Do not disable antivirus protection, add broad exclusions, or skip hash verification.",
|
||||
"To update, exit Dada from the tray and replace the complete program directory.",
|
||||
"Dada uses 127.0.0.1:43121 and does not support LAN or remote access.",
|
||||
"A final RELEASE.json is created only after WP-7 acceptance.",
|
||||
finalRelease
|
||||
? "Resend and Amap real-provider validation remain explicitly deferred and are not recorded as passed."
|
||||
: "A final RELEASE.json is created only after WP-7 acceptance.",
|
||||
"",
|
||||
].join("\r\n"));
|
||||
|
||||
@@ -381,7 +388,18 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
|
||||
const zipSha256 = fileSha256(zipPath);
|
||||
const shaPath = `${zipPath}.sha256`;
|
||||
writeFileSync(shaPath, `${zipSha256} ${basename(zipPath)}\n`);
|
||||
const processTree = await verifyExtractedPackage(zipPath, packageName);
|
||||
const supportBrowser = finalRelease ? releaseRecord.browsers[0] : undefined;
|
||||
const processTree = await verifyExtractedPackage(zipPath, packageName, finalRelease ? {
|
||||
brand: supportBrowser.brand,
|
||||
fullVersion: supportBrowser.fullVersion,
|
||||
major: Number.parseInt(supportBrowser.fullVersion.split(".")[0], 10),
|
||||
statusCode: 200,
|
||||
} : {
|
||||
brand: "Google Chrome",
|
||||
fullVersion: "150.0.0.0",
|
||||
major: 150,
|
||||
statusCode: 426,
|
||||
});
|
||||
const fileEntries = listFiles(packageDirectory).files.map((path) => ({
|
||||
path: relative(packageDirectory, path).replaceAll("\\", "/"),
|
||||
sha256: fileSha256(path),
|
||||
@@ -392,7 +410,7 @@ export async function buildAndValidatePortablePackage({ evidenceDirectory, outpu
|
||||
files: fileEntries,
|
||||
fixed_port: fixedPort,
|
||||
package_name: packageName,
|
||||
release_status: "candidate_unvalidated",
|
||||
release_status: finalRelease ? releaseRecord.releaseStatus : "candidate_unvalidated",
|
||||
schema_version: "1.0",
|
||||
zip_sha256: zipSha256,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
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|home)\/)/i;
|
||||
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() };
|
||||
}
|
||||
Reference in New Issue
Block a user