212 lines
5.8 KiB
TypeScript
212 lines
5.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
createJavaScriptApplicationGraph,
|
|
createJavaScriptApplicationNode,
|
|
type ApplicationGraphEvidence,
|
|
type ApplicationNode,
|
|
type JavaScriptApplicationGraph,
|
|
} from "../../../src/domain/javascriptApplicationGraph.js";
|
|
import {
|
|
APPLICATION_GRAPH_DIGESTS,
|
|
artifactEvidence,
|
|
inferredArtifactEvidence,
|
|
runtimeEvidence,
|
|
} from "../../fixtures/javascriptApplicationGraph.js";
|
|
|
|
const completeCoverage = {
|
|
status: "complete" as const,
|
|
truncated: false,
|
|
omitted_count: 0,
|
|
limits: [],
|
|
};
|
|
|
|
const unknownEvidence = (
|
|
limitations: string[] = ["The extractor could not classify this fact."],
|
|
): ApplicationGraphEvidence => ({
|
|
authority: "unknown",
|
|
state: "unknown",
|
|
confidence: "unknown",
|
|
artifact: {
|
|
available: false,
|
|
reason: "unknown",
|
|
detail: "Artifact provenance is unknown.",
|
|
},
|
|
location: {
|
|
available: false,
|
|
reason: "unknown",
|
|
detail: "Source location is unknown.",
|
|
},
|
|
extractor: {
|
|
name: "test",
|
|
version: "1",
|
|
operation: "unknown",
|
|
executable_sha256: null,
|
|
},
|
|
coverage: {
|
|
status: "unknown",
|
|
truncated: false,
|
|
omitted_count: null,
|
|
limits: [],
|
|
},
|
|
limitations,
|
|
evidence_ids: [],
|
|
});
|
|
|
|
const graphForNode = (node: ApplicationNode): JavaScriptApplicationGraph =>
|
|
createJavaScriptApplicationGraph({
|
|
schema: "JavaScriptApplicationGraph",
|
|
schema_version: 1,
|
|
root_node_ids: [node.node_id],
|
|
nodes: [node],
|
|
edges: [],
|
|
coverage: completeCoverage,
|
|
limitations: [],
|
|
});
|
|
|
|
const contentNode = (
|
|
evidence: ApplicationGraphEvidence,
|
|
properties: Record<string, unknown> = {},
|
|
): ApplicationNode =>
|
|
createJavaScriptApplicationNode({
|
|
kind: "javascript-module",
|
|
identity: {
|
|
strategy: "content-digest",
|
|
stability: "global-exact",
|
|
sha256: APPLICATION_GRAPH_DIGESTS.asar,
|
|
},
|
|
observations: [{ label: "module", properties, evidence }],
|
|
});
|
|
|
|
describe("JavaScript Application Graph", () => {
|
|
it("requires every identity strategy to have compatible supporting evidence", () => {
|
|
const mismatchedPath = createJavaScriptApplicationNode({
|
|
kind: "javascript-asset",
|
|
identity: {
|
|
strategy: "canonical-path",
|
|
stability: "artifact-version",
|
|
artifact_sha256: APPLICATION_GRAPH_DIGESTS.asar,
|
|
path: "dist/expected.js",
|
|
},
|
|
observations: [
|
|
{
|
|
label: null,
|
|
properties: {},
|
|
evidence: artifactEvidence(
|
|
APPLICATION_GRAPH_DIGESTS.asar,
|
|
"dist/other.js",
|
|
),
|
|
},
|
|
],
|
|
});
|
|
expect(() => graphForNode(mismatchedPath)).toThrow(
|
|
/identity is not supported/u,
|
|
);
|
|
|
|
const runtimeWithStaticEvidence = createJavaScriptApplicationNode({
|
|
kind: "runtime-script-instance",
|
|
identity: {
|
|
strategy: "runtime-instance",
|
|
stability: "capture-only",
|
|
capture_sha256: APPLICATION_GRAPH_DIGESTS.capture,
|
|
runtime_key: "script-preload",
|
|
},
|
|
observations: [
|
|
{
|
|
label: null,
|
|
properties: {},
|
|
evidence: artifactEvidence(
|
|
APPLICATION_GRAPH_DIGESTS.asar,
|
|
"dist/preload.js",
|
|
"ast-static-analysis",
|
|
),
|
|
},
|
|
],
|
|
});
|
|
expect(() => graphForNode(runtimeWithStaticEvidence)).toThrow(
|
|
/identity is not supported/u,
|
|
);
|
|
|
|
const mismatchedDigest = contentNode(
|
|
artifactEvidence("7".repeat(64), "dist/module.js"),
|
|
);
|
|
expect(() => graphForNode(mismatchedDigest)).toThrow(
|
|
/identity is not supported/u,
|
|
);
|
|
});
|
|
|
|
it("prevents static authorities from claiming runtime locations", () => {
|
|
const evidence = artifactEvidence(
|
|
APPLICATION_GRAPH_DIGESTS.asar,
|
|
"dist/module.js",
|
|
"ast-static-analysis",
|
|
);
|
|
evidence.location = runtimeEvidence("script-module").location;
|
|
expect(() => graphForNode(contentNode(evidence))).toThrow(
|
|
/cannot claim a runtime location/u,
|
|
);
|
|
});
|
|
|
|
it("enforces epistemic truthfulness and content-addressed artifacts", () => {
|
|
const inferredAsObserved = inferredArtifactEvidence(
|
|
APPLICATION_GRAPH_DIGESTS.asar,
|
|
"dist/module.js",
|
|
);
|
|
inferredAsObserved.state = "observed";
|
|
inferredAsObserved.confidence = "exact";
|
|
expect(() => contentNode(inferredAsObserved)).toThrow(
|
|
/must remain inferred/u,
|
|
);
|
|
|
|
const inferenceWithoutLimit = inferredArtifactEvidence(
|
|
APPLICATION_GRAPH_DIGESTS.asar,
|
|
"dist/module.js",
|
|
);
|
|
inferenceWithoutLimit.limitations = [];
|
|
expect(() => contentNode(inferenceWithoutLimit)).toThrow(
|
|
/Inferred facts require an explicit limitation/u,
|
|
);
|
|
|
|
const unknownWithoutLimit = unknownEvidence([]);
|
|
expect(() => contentNode(unknownWithoutLimit)).toThrow(
|
|
/require unknown confidence and an explicit limitation/u,
|
|
);
|
|
|
|
const missingArtifact = artifactEvidence(
|
|
APPLICATION_GRAPH_DIGESTS.asar,
|
|
"dist/module.js",
|
|
);
|
|
missingArtifact.artifact = {
|
|
available: false,
|
|
reason: "not-observed",
|
|
detail: "Bytes were not observed.",
|
|
};
|
|
expect(() => contentNode(missingArtifact)).toThrow(
|
|
/require a content digest/u,
|
|
);
|
|
|
|
const mismatchedArtifact = artifactEvidence(
|
|
APPLICATION_GRAPH_DIGESTS.asar,
|
|
"dist/module.js",
|
|
);
|
|
if (mismatchedArtifact.artifact.available)
|
|
mismatchedArtifact.artifact.artifact_id = `art_${"8".repeat(64)}`;
|
|
expect(() => contentNode(mismatchedArtifact)).toThrow(
|
|
/same SHA-256 digest/u,
|
|
);
|
|
|
|
const unlocatedObservation = artifactEvidence(
|
|
APPLICATION_GRAPH_DIGESTS.asar,
|
|
"dist/module.js",
|
|
);
|
|
unlocatedObservation.location = {
|
|
available: false,
|
|
reason: "not-observed",
|
|
detail: "The exact location was not recorded.",
|
|
};
|
|
expect(() => contentNode(unlocatedObservation)).toThrow(
|
|
/Observed facts require an actionable location/u,
|
|
);
|
|
});
|
|
});
|