Advance N-023 through N-026 audited parity

This commit is contained in:
mes123456
2026-08-12 15:23:35 -04:00
parent b3cefaeec5
commit 86136139e2
30 changed files with 5573 additions and 66 deletions

View File

@@ -0,0 +1,66 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import factory from "../../web/app/src/vendor/blender/web_engine.js";
const root = new URL("../../", import.meta.url);
const wasmBinary = fs.readFileSync(new URL("web/app/src/vendor/blender/web_engine.wasm", root));
const fixture = fs.readFileSync(new URL("tests/files/web/basic_scene.blend", root));
function open(engine, handle, bytes) {
const pointer = engine._malloc(bytes.byteLength);
try {
engine.HEAPU8.set(bytes, pointer);
assert.equal(engine._web_engine_open_blend(handle, pointer, bytes.byteLength), 0,
engine.UTF8ToString(engine._web_engine_last_error_message()));
}
finally { engine._free(pointer); }
}
function snapshot(engine, handle) {
const dataOut = engine._malloc(4), lengthOut = engine._malloc(4);
try {
assert.equal(engine._web_engine_get_scene_snapshot(handle, dataOut, lengthOut), 0,
engine.UTF8ToString(engine._web_engine_last_error_message()));
const pointer = engine.HEAPU32[dataOut >>> 2], length = engine.HEAPU32[lengthOut >>> 2];
return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length)));
}
finally { engine._free(dataOut); engine._free(lengthOut); }
}
const supportedEditors = new Set([
"VIEW_3D", "OUTLINER", "PROPERTIES", "UV_IMAGE", "NODE", "GRAPH", "DOPE_SHEET", "NLA",
"SPREADSHEET", "SEQUENCER", "CLIP",
]);
const regionKinds = new Set(["HEADER", "MAIN", "TOOLBAR", "SIDEBAR", "FOOTER"]);
const engine = await factory({ wasmBinary: wasmBinary.slice() });
const handle = engine._web_engine_create();
open(engine, handle, fixture);
const scene = snapshot(engine, handle);
assert.equal(scene.editorWorkflowStatus, "AVAILABLE");
const workflow = scene.editorWorkflow;
assert.equal(workflow.schemaVersion, 1);
assert.ok(workflow.workspaces.length > 0 && workflow.workspaces.length <= 64);
let areaCount = 0, regionCount = 0;
for (const workspace of workflow.workspaces) {
assert.ok(workspace.areas.some((area) => area.id === workspace.activeAreaId));
assert.equal(workspace.revision, scene.revision);
areaCount += workspace.areas.length;
for (const area of workspace.areas) {
assert.ok(supportedEditors.has(area.editor), `unsupported editor ${area.editor}`);
assert.ok(area.rect.x >= 0 && area.rect.y >= 0 && area.rect.width > 0 && area.rect.height > 0);
assert.ok(area.rect.x + area.rect.width <= 1 && area.rect.y + area.rect.height <= 1);
assert.ok(area.regions.some((region) => region.kind === "MAIN"));
regionCount += area.regions.length;
for (const region of area.regions) assert.ok(regionKinds.has(region.kind));
}
}
assert.ok(areaCount <= 1024 && regionCount <= 4096);
const activeWorkspace = workflow.workspaces.find((workspace) => workspace.id === workflow.context.workspaceId);
const activeArea = activeWorkspace?.areas.find((area) => area.id === workflow.context.activeAreaId);
assert.equal(activeArea?.editor, workflow.context.activeEditor);
assert.equal(workflow.context.revision, scene.revision);
assert.equal(workflow.context.activeObjectId, scene.activeObjectId);
assert.deepEqual(workflow.context.selection, scene.activeObjectId === null ? [] : [scene.activeObjectId]);
assert.deepEqual(workflow.keymaps, []);
engine._web_engine_destroy(handle);
process.stdout.write(`editor-main-reader-ok workspaces=${workflow.workspaces.length} areas=${areaCount} regions=${regionCount}\n`);

View File

@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import factory from "../../web/app/src/vendor/blender/web_engine.js";
const root = new URL("../../", import.meta.url);
const wasmBinary = fs.readFileSync(new URL("web/app/src/vendor/blender/web_engine.wasm", root));
const fixture = fs.readFileSync(new URL("tests/files/web/image_resource_matrix.blend", root));
function open(engine, handle, bytes) {
const pointer = engine._malloc(bytes.byteLength);
try {
engine.HEAPU8.set(bytes, pointer);
assert.equal(engine._web_engine_open_blend(handle, pointer, bytes.byteLength), 0,
engine.UTF8ToString(engine._web_engine_last_error_message()));
}
finally { engine._free(pointer); }
}
function snapshot(engine, handle) {
const dataOut = engine._malloc(4), lengthOut = engine._malloc(4);
try {
assert.equal(engine._web_engine_get_scene_snapshot(handle, dataOut, lengthOut), 0,
engine.UTF8ToString(engine._web_engine_last_error_message()));
const pointer = engine.HEAPU32[dataOut >>> 2], length = engine.HEAPU32[lengthOut >>> 2];
return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length)));
}
finally { engine._free(dataOut); engine._free(lengthOut); }
}
const engine = await factory({ wasmBinary: wasmBinary.slice() });
const handle = engine._web_engine_create();
open(engine, handle, fixture);
const scene = snapshot(engine, handle);
assert.equal(scene.libraryStatus, "AVAILABLE");
assert.equal(scene.libraries.length, 1);
const library = scene.libraries[0];
assert.equal(library.id, "library:image_resource_library.blend");
assert.equal(library.name, "image_resource_library.blend");
assert.equal(library.sourcePath, "//resources/image_resource_library.blend");
assert.equal(library.packed, false);
assert.equal(library.status, "EXTERNAL_REQUIRED");
assert.equal(library.errorCode, "LINKED_LIBRARY_RESOURCE_REQUIRED");
assert.deepEqual(library.dependencyIds, []);
assert.equal(library.readOnly, true);
engine._web_engine_destroy(handle);
process.stdout.write("library-main-reader-ok relative-path=passed readonly=passed dependency-inventory=passed\n");

View File

@@ -0,0 +1,36 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import ts from "../../web/node_modules/typescript/lib/typescript.js";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "release-evidence-"));
const require = createRequire(import.meta.url);
try {
for (const name of ["capability-gates", "release-gate"]) {
const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8");
const result = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` });
fs.writeFileSync(path.join(temporary, `${name}.cjs`), result.outputText.replace('require("./capability-gates")', 'require("./capability-gates.cjs")'));
}
const module = require(path.join(temporary, "release-gate.cjs"));
const reportPath = path.join(root, "docs/status/release-evidence.json");
const ledgerPath = path.join(root, "docs/status/parity-ledger.json");
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
assert.equal(report.sourceSha256, crypto.createHash("sha256").update(fs.readFileSync(ledgerPath)).digest("hex"), "release report is stale relative to parity ledger");
const parsed = module.parseReleaseManifest(report);
const evaluation = module.evaluateReleaseManifest(parsed);
assert.equal(evaluation.status, "BLOCKED");
assert.ok(evaluation.missing.includes("performance.geometry10M"));
assert.ok(evaluation.missing.includes("faults.deviceLoss"));
assert.equal(evaluation.missing.includes("browser.chromium"), false);
assert.ok(parsed.evidence.records.length >= 7);
assert.equal(parsed.evidence.records.some((record) => record.fields.includes("faults.zipBomb")), true);
process.stdout.write(`release-evidence-check-ok records=${parsed.evidence.records.length} missing=${evaluation.missing.length} status=${evaluation.status}\n`);
}
finally {
fs.rmSync(temporary, { recursive: true, force: true });
}

View File

@@ -8,6 +8,7 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..")
const dist = path.join(root, "web/dist");
const notices = JSON.parse(fs.readFileSync(path.join(root, "docs/web/third-party-notices.json"), "utf8"));
const packageLock = JSON.parse(fs.readFileSync(path.join(root, "web/package-lock.json"), "utf8"));
const sbom = JSON.parse(fs.readFileSync(path.join(root, "docs/web/sbom.spdx.json"), "utf8"));
for (const dependency of ["react", "react-dom", "three", "vite", "typescript", "@playwright/test"]) {
const normalize = (value) => value.toLowerCase().replace("@playwright/test", "playwright").replace(/\.js$/, "").replace(/[^a-z0-9]/g, "");
const covered = notices.packages.some((entry) => normalize(entry.name) === normalize(dependency));
@@ -16,6 +17,9 @@ for (const dependency of ["react", "react-dom", "three", "vite", "typescript", "
assert.ok(packageLock.lockfileVersion >= 3, "npm lockfile must use an integrity-bearing format");
assert.ok(fs.existsSync(path.join(root, "blender-5.2.0/COPYING")), "Blender GPL text is missing");
assert.ok(fs.existsSync(path.join(root, "web/app/src/vendor/three/LICENSE")), "Three.js license is missing");
assert.equal(sbom.spdxVersion, "SPDX-2.3", "SPDX SBOM schema is missing");
assert.ok(sbom.packages.length >= Object.keys(packageLock.packages).length, "SPDX SBOM omits locked dependencies");
assert.equal(new Set(sbom.packages.map((item) => item.SPDXID)).size, sbom.packages.length, "SPDX IDs must be unique");
assert.ok(fs.existsSync(path.join(dist, "index.html")), "offline dist is missing; run npm build first");
const files = [];

View File

@@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import factory from "../../web/app/src/vendor/blender/web_engine.js";
const root = new URL("../../", import.meta.url);
const wasmBinary = fs.readFileSync(new URL("web/app/src/vendor/blender/web_engine.wasm", root));
const fixture = fs.readFileSync(new URL("tests/files/web/script_scene.blend", root));
function open(engine, handle, bytes) {
const pointer = engine._malloc(bytes.byteLength);
try {
engine.HEAPU8.set(bytes, pointer);
assert.equal(engine._web_engine_open_blend(handle, pointer, bytes.byteLength), 0,
engine.UTF8ToString(engine._web_engine_last_error_message()));
}
finally { engine._free(pointer); }
}
function snapshot(engine, handle) {
const dataOut = engine._malloc(4), lengthOut = engine._malloc(4);
try {
assert.equal(engine._web_engine_get_scene_snapshot(handle, dataOut, lengthOut), 0,
engine.UTF8ToString(engine._web_engine_last_error_message()));
const pointer = engine.HEAPU32[dataOut >>> 2], length = engine.HEAPU32[lengthOut >>> 2];
return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length)));
}
finally { engine._free(dataOut); engine._free(lengthOut); }
}
const engine = await factory({ wasmBinary: wasmBinary.slice() });
const handle = engine._web_engine_create();
open(engine, handle, fixture);
const scene = snapshot(engine, handle);
assert.equal(scene.scriptSourceStatus, "AVAILABLE");
assert.equal(scene.scriptSources.schemaVersion, 1);
assert.equal(scene.scriptSources.sources.length, 3);
assert.equal(scene.nonMeshData.some((data) => data.id.startsWith("text:")), false);
const byName = new Map(scene.scriptSources.sources.map((source) => [source.name, source]));
for (const source of byName.values()) {
assert.equal(source.byteLength, Buffer.byteLength(source.source));
assert.equal(source.lineCount, source.source.split("\n").length);
assert.equal(source.sourceSha256, crypto.createHash("sha256").update(source.source).digest("hex"));
assert.equal(source.readOnly, true);
assert.equal(source.executionStatus, "BLOCKED");
}
assert.equal(byName.get("InternalSafe.py").source, "value = 7\nprint(value)\n");
assert.equal(byName.get("InternalSafe.py").internal, true);
assert.equal(byName.get("InternalSafe.py").errorCode, "SCRIPT_SANDBOX_UNAVAILABLE");
assert.equal(byName.get("ModuleAutorun.py").moduleAutorunRequested, true);
assert.equal(byName.get("ModuleAutorun.py").errorCode, "SCRIPT_POLICY_DENIED");
assert.equal(byName.get("ExternalProject.py").sourcePath, "//scripts/external_project.py");
assert.equal(byName.get("ExternalProject.py").internal, false);
engine._web_engine_destroy(handle);
process.stdout.write("script-main-reader-ok full-source=passed sha256=passed autorun-default-deny=passed\n");

View File

@@ -0,0 +1,52 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const ledgerPath = path.join(root, "docs/status/parity-ledger.json");
const outputPath = path.join(root, "docs/status/release-evidence.json");
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
const evidence = {
browser: { chromium: false },
runtime: { offline: false, workerRestart: false, opfsRecovery: false },
performance: { geometry1M: false, geometry10M: false, texture4K: false, texture8K: false, longMedia: false, simulationCache: false },
faults: { oom: false, deviceLoss: false, networkInterrupt: false, malformedBlend: false, zipBomb: false },
provenance: { license: false, sbom: false, sourceOffer: false, deterministicPackage: false },
records: [],
};
function run(id, fields, command, artifacts = []) {
const started = Date.now();
const result = spawnSync("bash", ["-lc", command], { cwd: root, encoding: "utf8", env: { ...process.env, WEB_TEST_PORT: process.env.WEB_TEST_PORT ?? "5326" }, maxBuffer: 16 * 1024 * 1024 });
const combined = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
if (result.status !== 0) {
process.stderr.write(`${combined}\n`);
throw new Error(`${id} failed with exit code ${result.status}`);
}
for (const field of fields) {
const [group, key] = field.split(".");
evidence[group][key] = true;
}
evidence.records.push({ id, fields, command, exitCode: 0, durationMs: Date.now() - started, output: combined.slice(-4096) || `${id} passed`, artifactSha256: artifacts.filter((file) => fs.existsSync(path.join(root, file))).map((file) => sha256(path.join(root, file))) });
process.stdout.write(`${id}-ok durationMs=${Date.now() - started}\n`);
}
run("sbom", ["provenance.license", "provenance.sbom"], "npm --prefix web run release:sbom", ["docs/web/sbom.spdx.json", "docs/web/third-party-notices.json", "web/package-lock.json"]);
run("chromium", ["browser.chromium", "runtime.offline", "runtime.workerRestart", "runtime.opfsRecovery"], "npm --prefix web run test:e2e -- --workers=1 && npm --prefix web run test:browser", ["web/app/src/vendor/blender/web_engine.wasm"]);
run("geometry-1m", ["performance.geometry1M"], "npm --prefix web run test:release-performance", ["web/app/src/vendor/blender/web_engine.wasm"]);
run("malformed-blend", ["faults.malformedBlend"], "npm --prefix web run test:malicious-blends", ["tests/files/web/basic_scene.blend"]);
run("zip-bomb", ["faults.zipBomb"], "WEB_TEST_PORT=5323 npm --prefix web run test:asset-library", ["web/protocol/asset-library-io.ts"]);
run("release-package", [], "npm --prefix web run test:release-package", ["docs/web/sbom.spdx.json", "web/app/src/vendor/blender/web_engine.wasm"]);
run("offline-reproducibility", ["provenance.sourceOffer", "provenance.deterministicPackage"], "npm --prefix web run release:offline", ["release/blender-web-offline.tar.gz", "release/blender-web-corresponding-source.tar.gz", "release/SHA256SUMS.txt"]);
const ledgerBytes = fs.readFileSync(ledgerPath);
const ledger = JSON.parse(ledgerBytes);
const manifest = { schemaVersion: 3, source: "docs/status/parity-ledger.json", sourceSha256: crypto.createHash("sha256").update(ledgerBytes).digest("hex"), generatedAt: new Date().toISOString(), families: ledger.families, evidence };
assert.ok(manifest.families.some((family) => family.status === "BLOCKED"));
assert.equal(manifest.evidence.performance.geometry10M, false);
assert.equal(manifest.evidence.faults.deviceLoss, false);
fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`);
process.stdout.write(`release-evidence-ok records=${evidence.records.length} status=BLOCKED sha256=${sha256(outputPath)}\n`);

View File

@@ -8,11 +8,13 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..")
const releaseRoot = path.join(root, "release");
if (path.basename(releaseRoot) !== "release" || path.dirname(releaseRoot) !== root) throw new Error("unsafe release target");
const bundle = path.join(releaseRoot, "blender-web-offline");
execFileSync(process.execPath, [path.join(root, "tools/web/generate-sbom.mjs")], { stdio: "inherit" });
fs.rmSync(bundle, { recursive: true, force: true });
fs.mkdirSync(bundle, { recursive: true });
fs.cpSync(path.join(root, "web/dist"), path.join(bundle, "app"), { recursive: true });
fs.copyFileSync(path.join(root, "blender-5.2.0/COPYING"), path.join(bundle, "COPYING"));
fs.copyFileSync(path.join(root, "docs/web/third-party-notices.json"), path.join(bundle, "third-party-notices.json"));
fs.copyFileSync(path.join(root, "docs/web/sbom.spdx.json"), path.join(bundle, "sbom.spdx.json"));
fs.copyFileSync(path.join(root, "blender-5.2.0/extern/opensubdiv-source/LICENSE.txt"), path.join(bundle, "LICENSE-OpenSubdiv.txt"));
fs.copyFileSync(path.join(root, "blender-5.2.0/extern/gmp-source/COPYING.LESSERv3"), path.join(bundle, "LICENSE-GMP-LGPLv3.txt"));
fs.writeFileSync(path.join(bundle, "README.txt"), [

View File

@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const lockPath = path.join(root, "web/package-lock.json");
const outputPath = path.join(root, "docs/web/sbom.spdx.json");
const lockBytes = fs.readFileSync(lockPath);
const lock = JSON.parse(lockBytes);
const noticesBytes = fs.readFileSync(path.join(root, "docs/web/third-party-notices.json"));
const notices = JSON.parse(noticesBytes);
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
const spdxId = (value) => `SPDXRef-${value.replace(/[^A-Za-z0-9.-]/g, "-")}-${sha256(value).slice(0, 12)}`;
const packages = [];
const rootId = "SPDXRef-Package-blender-web-editor";
packages.push({ SPDXID: rootId, name: lock.name, versionInfo: lock.version, downloadLocation: "NOASSERTION", filesAnalyzed: false, licenseConcluded: "NOASSERTION", licenseDeclared: "NOASSERTION", copyrightText: "NOASSERTION", checksums: [{ algorithm: "SHA256", checksumValue: sha256(lockBytes) }] });
for (const [packagePath, entry] of Object.entries(lock.packages).sort(([left], [right]) => left.localeCompare(right))) {
if (!packagePath || !entry.version) continue;
const marker = packagePath.lastIndexOf("node_modules/");
const name = entry.name ?? packagePath.slice(marker + "node_modules/".length);
const item = { SPDXID: spdxId(`npm-${packagePath}`), name, versionInfo: entry.version, downloadLocation: entry.resolved ?? "NOASSERTION", filesAnalyzed: false, licenseConcluded: "NOASSERTION", licenseDeclared: "NOASSERTION", copyrightText: "NOASSERTION", externalRefs: [{ referenceCategory: "PACKAGE-MANAGER", referenceType: "purl", referenceLocator: `pkg:npm/${encodeURIComponent(name)}@${entry.version}` }] };
if (typeof entry.integrity === "string" && entry.integrity.startsWith("sha512-")) item.checksums = [{ algorithm: "SHA512", checksumValue: Buffer.from(entry.integrity.slice(7), "base64").toString("hex") }];
packages.push(item);
}
for (const notice of notices.packages.filter((item) => !item.source.includes("node_modules"))) {
packages.push({ SPDXID: spdxId(`vendored-${notice.name}-${notice.version}`), name: notice.name, versionInfo: notice.version, downloadLocation: "NOASSERTION", filesAnalyzed: false, licenseConcluded: "NOASSERTION", licenseDeclared: notice.license, copyrightText: "NOASSERTION", sourceInfo: notice.source });
}
packages.sort((left, right) => left.SPDXID.localeCompare(right.SPDXID));
const document = {
spdxVersion: "SPDX-2.3",
dataLicense: "CC0-1.0",
SPDXID: "SPDXRef-DOCUMENT",
name: "blender-web-editor-sbom",
documentNamespace: `https://blender-web.local/spdx/${sha256(Buffer.concat([lockBytes, noticesBytes]))}`,
creationInfo: { created: "1970-01-01T00:00:00Z", creators: ["Tool: tools/web/generate-sbom.mjs"] },
documentDescribes: [rootId],
packages,
relationships: packages.filter((item) => item.SPDXID !== rootId).map((item) => ({ spdxElementId: rootId, relationshipType: "DEPENDS_ON", relatedSpdxElement: item.SPDXID })),
};
fs.writeFileSync(outputPath, `${JSON.stringify(document, null, 2)}\n`);
assert.equal(new Set(packages.map((item) => item.SPDXID)).size, packages.length);
assert.ok(packages.length >= Object.keys(lock.packages).length);
process.stdout.write(`sbom-ok packages=${packages.length} sha256=${sha256(fs.readFileSync(outputPath))}\n`);

View File

@@ -0,0 +1,32 @@
import pathlib
import sys
import bpy
SOURCES = {
"InternalSafe.py": "value = 7\nprint(value)\n",
"ModuleAutorun.py": "def register():\n return 'blocked'\n",
"ExternalProject.py": "message = 'project relative'\n",
}
def main(output_path):
bpy.ops.wm.read_factory_settings(use_empty=True)
for name, source in SOURCES.items():
text = bpy.data.texts.new(name)
text.write(source)
bpy.data.texts["ModuleAutorun.py"].use_module = True
bpy.data.texts["ExternalProject.py"].filepath = "//scripts/external_project.py"
path = pathlib.Path(output_path).resolve()
path.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.wm.save_as_mainfile(filepath=str(path), compress=False)
print(f"script-fixture-generated path={path} sources={len(SOURCES)}")
if __name__ == "__main__":
arguments = sys.argv[sys.argv.index("--") + 1:]
if len(arguments) != 1:
raise SystemExit("usage: blender -b --python generate-script-fixture.py -- output.blend")
main(arguments[0])