mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
452 lines
14 KiB
TypeScript
452 lines
14 KiB
TypeScript
import type { JsonValue } from "../jsonValue.js";
|
|
import {
|
|
createJavaScriptApplicationEdge,
|
|
createJavaScriptApplicationNode,
|
|
} from "./javascriptApplicationGraph.js";
|
|
import type {
|
|
ApplicationEdge,
|
|
ApplicationNode,
|
|
} from "./javascriptApplicationGraphSchemas.js";
|
|
import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js";
|
|
import type { ParsedRuntimeCapture } from "./javascriptRuntimeReconciliationCaptureParsing.js";
|
|
|
|
export interface RuntimeReconciliationEntity {
|
|
readonly kind: "target" | "frame" | "script" | "worker";
|
|
readonly section: "target" | "frames" | "scripts" | "workers";
|
|
readonly node: ApplicationNode;
|
|
readonly capture: ParsedRuntimeCapture;
|
|
readonly runtimeKey: string;
|
|
readonly location: {
|
|
readonly kind: "file" | "url" | "unresolved";
|
|
readonly value: string;
|
|
};
|
|
readonly frameKey: string | null;
|
|
readonly sourceSha256: string | null;
|
|
readonly staticKind: "renderer" | "javascript" | "worker" | "service-worker";
|
|
}
|
|
|
|
export interface RuntimeProjection {
|
|
readonly entities: readonly RuntimeReconciliationEntity[];
|
|
readonly nodes: readonly ApplicationNode[];
|
|
readonly edges: readonly ApplicationEdge[];
|
|
readonly targetNodeByEvidenceId: ReadonlyMap<string, ApplicationNode>;
|
|
readonly omittedEntities: number;
|
|
}
|
|
|
|
/** Project authorized passive capture metadata into capture-scoped JAG nodes. */
|
|
export const projectRuntimeCaptures = (
|
|
captures: readonly ParsedRuntimeCapture[],
|
|
): RuntimeProjection => {
|
|
const all = captures.flatMap(projectCapture);
|
|
const retained = all;
|
|
const retainedIds = new Set(retained.map(({ node }) => node.node_id));
|
|
const nodes = retained.map(({ node }) => node);
|
|
const edges = captures
|
|
.flatMap((capture) => runtimeEdges(capture, all))
|
|
.filter(
|
|
({ source_node_id: source, target_node_id: target }) =>
|
|
retainedIds.has(source) && retainedIds.has(target),
|
|
);
|
|
const targetNodeByEvidenceId = new Map(
|
|
retained
|
|
.filter(({ kind }) => kind === "target")
|
|
.map(({ capture, node }) => [capture.evidence.evidence_id, node]),
|
|
);
|
|
return {
|
|
entities: retained,
|
|
nodes,
|
|
edges,
|
|
targetNodeByEvidenceId,
|
|
omittedEntities: 0,
|
|
};
|
|
};
|
|
|
|
const projectCapture = (
|
|
capture: ParsedRuntimeCapture,
|
|
): RuntimeReconciliationEntity[] => {
|
|
const target = targetEntity(capture);
|
|
return [
|
|
target,
|
|
...frameEntities(capture),
|
|
...scriptEntities(capture),
|
|
...workerEntities(capture),
|
|
];
|
|
};
|
|
|
|
const targetEntity = (
|
|
capture: ParsedRuntimeCapture,
|
|
): RuntimeReconciliationEntity => {
|
|
const target = capture.inspection.target;
|
|
const location =
|
|
"unresolved_location" in target
|
|
? ({
|
|
kind: "unresolved",
|
|
value: target.unresolved_location.reported_url,
|
|
} as const)
|
|
: "url" in target
|
|
? ({ kind: "url", value: target.url } as const)
|
|
: ({ kind: "file", value: target.file_path } as const);
|
|
return runtimeEntity(capture, {
|
|
kind: "target",
|
|
nodeKind: "target",
|
|
runtimeKey: target.target_id,
|
|
targetKey: target.target_id,
|
|
frameKey: null,
|
|
scriptKey: null,
|
|
location,
|
|
staticKind: "renderer",
|
|
sourceSha256: null,
|
|
label: target.title,
|
|
properties: {
|
|
runtime_type: target.type,
|
|
...(location.kind === "unresolved"
|
|
? {
|
|
reported_url: location.value,
|
|
location_kind: "unresolved",
|
|
location_reason: "unverifiable-file-location",
|
|
}
|
|
: { location: location.value }),
|
|
attached: target.attached,
|
|
},
|
|
section: "target",
|
|
});
|
|
};
|
|
|
|
const frameEntities = (
|
|
capture: ParsedRuntimeCapture,
|
|
): RuntimeReconciliationEntity[] =>
|
|
capture.inspection.frames.map((frame) => {
|
|
const location =
|
|
"url" in frame
|
|
? ({ kind: "url", value: frame.url } as const)
|
|
: ({ kind: "file", value: frame.file_path } as const);
|
|
return runtimeEntity(capture, {
|
|
kind: "frame",
|
|
nodeKind: "frame",
|
|
runtimeKey: frame.frame_id,
|
|
targetKey: capture.inspection.target.target_id,
|
|
frameKey: frame.frame_id,
|
|
scriptKey: null,
|
|
location,
|
|
staticKind: "renderer",
|
|
sourceSha256: null,
|
|
label: location.value,
|
|
properties: {
|
|
parent_frame_id: frame.parent_frame_id,
|
|
location: location.value,
|
|
},
|
|
section: "frames",
|
|
});
|
|
});
|
|
|
|
const scriptEntities = (
|
|
capture: ParsedRuntimeCapture,
|
|
): RuntimeReconciliationEntity[] =>
|
|
capture.inspection.scripts.items.map((script) => {
|
|
const location =
|
|
"unresolved_location" in script
|
|
? ({
|
|
kind: "unresolved",
|
|
value: script.unresolved_location.reported_url,
|
|
} as const)
|
|
: "url" in script
|
|
? ({ kind: "url", value: script.url } as const)
|
|
: ({ kind: "file", value: script.file_path } as const);
|
|
return runtimeEntity(capture, {
|
|
kind: "script",
|
|
nodeKind: "runtime-script-instance",
|
|
runtimeKey: script.script_key,
|
|
targetKey: capture.inspection.target.target_id,
|
|
frameKey: script.frame_id,
|
|
scriptKey: script.script_key,
|
|
location,
|
|
staticKind: "javascript",
|
|
sourceSha256: script.source.included
|
|
? script.source.artifact.sha256
|
|
: null,
|
|
label: location.value,
|
|
properties: {
|
|
...("unresolved_location" in script
|
|
? {
|
|
location_kind: "unresolved",
|
|
reported_url: script.unresolved_location.reported_url,
|
|
location_reason: script.unresolved_location.reason,
|
|
}
|
|
: { location: location.value }),
|
|
cdp_hash: script.cdp_hash,
|
|
length: script.length,
|
|
is_module: script.is_module,
|
|
language: script.language,
|
|
source_sha256: script.source.included
|
|
? script.source.artifact.sha256
|
|
: null,
|
|
...("source_map_url" in script
|
|
? { source_map_url: script.source_map_url }
|
|
: {}),
|
|
},
|
|
section: "scripts",
|
|
...(script.source.included
|
|
? { artifactSha256: script.source.artifact.sha256 }
|
|
: {}),
|
|
});
|
|
});
|
|
|
|
const workerEntities = (
|
|
capture: ParsedRuntimeCapture,
|
|
): RuntimeReconciliationEntity[] =>
|
|
capture.inspection.workers.map((worker) => {
|
|
const location =
|
|
"url" in worker
|
|
? ({ kind: "url", value: worker.url } as const)
|
|
: ({ kind: "file", value: worker.file_path } as const);
|
|
const serviceWorker = worker.type.includes("service");
|
|
return runtimeEntity(capture, {
|
|
kind: "worker",
|
|
nodeKind: serviceWorker ? "service-worker" : "worker",
|
|
runtimeKey: worker.target_id,
|
|
targetKey: worker.target_id,
|
|
frameKey: worker.parent_frame_id,
|
|
scriptKey: null,
|
|
location,
|
|
staticKind: serviceWorker ? "service-worker" : "worker",
|
|
sourceSha256: null,
|
|
label: location.value,
|
|
properties: {
|
|
runtime_type: worker.type,
|
|
location: location.value,
|
|
attached: worker.attached,
|
|
opener_target_id: worker.opener_target_id,
|
|
parent_frame_id: worker.parent_frame_id,
|
|
},
|
|
section: "workers",
|
|
});
|
|
});
|
|
|
|
interface RuntimeEntityInput {
|
|
readonly kind: RuntimeReconciliationEntity["kind"];
|
|
readonly nodeKind: ApplicationNode["kind"];
|
|
readonly runtimeKey: string;
|
|
readonly targetKey: string;
|
|
readonly frameKey: string | null;
|
|
readonly scriptKey: string | null;
|
|
readonly location: RuntimeReconciliationEntity["location"];
|
|
readonly staticKind: RuntimeReconciliationEntity["staticKind"];
|
|
readonly sourceSha256: string | null;
|
|
readonly label: string;
|
|
readonly properties: Readonly<Record<string, JsonValue>>;
|
|
readonly section: "target" | "frames" | "scripts" | "workers";
|
|
readonly artifactSha256?: string;
|
|
}
|
|
|
|
const runtimeEntity = (
|
|
capture: ParsedRuntimeCapture,
|
|
input: RuntimeEntityInput,
|
|
): RuntimeReconciliationEntity => ({
|
|
kind: input.kind,
|
|
section: input.section,
|
|
capture,
|
|
runtimeKey: input.runtimeKey,
|
|
location: input.location,
|
|
frameKey: input.frameKey,
|
|
sourceSha256: input.sourceSha256,
|
|
staticKind: input.staticKind,
|
|
node: createJavaScriptApplicationNode({
|
|
kind: input.nodeKind,
|
|
identity: {
|
|
strategy: "runtime-instance",
|
|
stability: "capture-only",
|
|
capture_sha256: capture.captureSha256,
|
|
runtime_key: input.runtimeKey,
|
|
},
|
|
observations: [
|
|
{
|
|
label: input.label || null,
|
|
properties: input.properties,
|
|
evidence: runtimeEvidence(capture, input),
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
|
|
const runtimeEvidence = (
|
|
capture: ParsedRuntimeCapture,
|
|
input: RuntimeEntityInput,
|
|
): ApplicationGraphEvidence => ({
|
|
authority: "passive-cdp-runtime",
|
|
state: "observed",
|
|
confidence: "exact",
|
|
artifact:
|
|
input.artifactSha256 === undefined
|
|
? {
|
|
available: false,
|
|
reason:
|
|
input.section === "scripts" ? "not-observed" : "not-applicable",
|
|
detail:
|
|
input.section === "scripts"
|
|
? "Script source bytes were not included in this passive capture."
|
|
: "This runtime entity has no standalone artifact bytes.",
|
|
}
|
|
: {
|
|
available: true,
|
|
artifact_id: `art_${input.artifactSha256}`,
|
|
sha256: input.artifactSha256,
|
|
},
|
|
location: {
|
|
available: true,
|
|
value: {
|
|
kind: "runtime",
|
|
capture_sha256: capture.captureSha256,
|
|
target_key: input.targetKey,
|
|
frame_key: input.frameKey,
|
|
script_key: input.scriptKey,
|
|
},
|
|
},
|
|
extractor: {
|
|
name: capture.evidence.provider.id,
|
|
version:
|
|
capture.evidence.provider.version ??
|
|
(capture.kind === "browser" ? "2" : "1"),
|
|
operation: capture.evidence.operation,
|
|
executable_sha256: null,
|
|
},
|
|
coverage: runtimeSectionCoverage(capture, input.section),
|
|
limitations: [
|
|
"The entity was observed only within one bounded passive CDP capture.",
|
|
...(runtimeSectionComplete(capture, input.section)
|
|
? []
|
|
: [
|
|
`The runtime ${input.section} inventory was incomplete within this capture.`,
|
|
]),
|
|
],
|
|
evidence_ids: [capture.evidence.evidence_id],
|
|
});
|
|
|
|
const runtimeEdges = (
|
|
capture: ParsedRuntimeCapture,
|
|
entities: readonly RuntimeReconciliationEntity[],
|
|
): ApplicationEdge[] => {
|
|
const local = entities.filter(
|
|
({ capture: candidate }) =>
|
|
candidate.evidence.evidence_id === capture.evidence.evidence_id,
|
|
);
|
|
const target = local.find(({ kind }) => kind === "target");
|
|
if (target === undefined) return [];
|
|
const frames = new Map(
|
|
local
|
|
.filter(({ kind }) => kind === "frame")
|
|
.map((entity) => [entity.runtimeKey, entity]),
|
|
);
|
|
return local.flatMap((entity) => {
|
|
if (entity === target) return [];
|
|
const parent =
|
|
(entity.kind === "script" || entity.kind === "worker") &&
|
|
entity.frameKey !== null
|
|
? (frames.get(entity.frameKey) ?? target)
|
|
: target;
|
|
return [
|
|
createJavaScriptApplicationEdge({
|
|
source_node_id: parent.node.node_id,
|
|
target_node_id: entity.node.node_id,
|
|
relation: entity.kind === "script" ? "loads" : "contains",
|
|
properties: { basis: "passive-cdp-observation" },
|
|
evidence: runtimeRelationshipEvidence(capture, entity),
|
|
}),
|
|
];
|
|
});
|
|
};
|
|
|
|
const runtimeRelationshipEvidence = (
|
|
capture: ParsedRuntimeCapture,
|
|
entity: RuntimeReconciliationEntity,
|
|
): ApplicationGraphEvidence => ({
|
|
authority: "passive-cdp-runtime",
|
|
state: "observed",
|
|
confidence: "exact",
|
|
artifact: {
|
|
available: false,
|
|
reason: "not-applicable",
|
|
detail: "The relationship is runtime metadata, not standalone bytes.",
|
|
},
|
|
location: {
|
|
available: true,
|
|
value: {
|
|
kind: "runtime",
|
|
capture_sha256: capture.captureSha256,
|
|
target_key: capture.inspection.target.target_id,
|
|
frame_key: entity.frameKey,
|
|
script_key: entity.kind === "script" ? entity.runtimeKey : null,
|
|
},
|
|
},
|
|
extractor: {
|
|
name: capture.evidence.provider.id,
|
|
version:
|
|
capture.evidence.provider.version ??
|
|
(capture.kind === "browser" ? "2" : "1"),
|
|
operation: capture.evidence.operation,
|
|
executable_sha256: null,
|
|
},
|
|
coverage: runtimeSectionCoverage(capture, entity.section),
|
|
limitations: [
|
|
"The relationship was observed only within one bounded passive CDP capture.",
|
|
],
|
|
evidence_ids: [capture.evidence.evidence_id],
|
|
});
|
|
|
|
const runtimeSectionComplete = (
|
|
capture: ParsedRuntimeCapture,
|
|
section: RuntimeEntityInput["section"],
|
|
): boolean => {
|
|
if (section === "target") return true;
|
|
if (section === "scripts") return capture.scriptsCompleteWithinScope;
|
|
return (
|
|
!capture.inspection.completeness.truncated_sections.includes(section) &&
|
|
!capture.inspection.completeness.unavailable_sections.includes(section)
|
|
);
|
|
};
|
|
|
|
const runtimeSectionCoverage = (
|
|
capture: ParsedRuntimeCapture,
|
|
section: RuntimeEntityInput["section"],
|
|
): ApplicationGraphEvidence["coverage"] => {
|
|
if (runtimeSectionComplete(capture, section))
|
|
return {
|
|
status: "complete",
|
|
truncated: false,
|
|
omitted_count: 0,
|
|
limits: [],
|
|
};
|
|
const truncated =
|
|
section !== "target" &&
|
|
capture.inspection.completeness.truncated_sections.includes(section);
|
|
return {
|
|
status: "partial",
|
|
truncated,
|
|
omitted_count: null,
|
|
limits: truncated
|
|
? [
|
|
{
|
|
name: `passive-cdp-${section}-limit`,
|
|
value: runtimeSectionCount(capture, section),
|
|
unit: "items",
|
|
},
|
|
]
|
|
: [],
|
|
};
|
|
};
|
|
|
|
const runtimeSectionCount = (
|
|
capture: ParsedRuntimeCapture,
|
|
section: RuntimeEntityInput["section"],
|
|
): number => {
|
|
switch (section) {
|
|
case "target":
|
|
return 1;
|
|
case "frames":
|
|
return capture.inspection.frames.length;
|
|
case "scripts":
|
|
return capture.inspection.scripts.items.length;
|
|
case "workers":
|
|
return capture.inspection.workers.length;
|
|
}
|
|
};
|