import type { ApplicationNode } from "../../domain/javascriptApplicationGraph.js"; import type { ElectronBrowserWindowFinding, ElectronContextBridgeFinding, ElectronUtilityProcessFinding, } from "../../domain/electronStaticAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; import { artifactLocalIdentity, createElectronRoleNode, javascriptAnalysisCoverage, linkElectronRoleToAsset, type JavaScriptArtifactGraphContext, type JavaScriptArtifactGraphCoverage, } from "./JavaScriptArtifactGraphContext.js"; import { astObservationEvidence } from "./JavaScriptArtifactGraphEvidence.js"; import { addElectronAstEdge, addElectronInferenceEdge, electronFindingSourceNode, electronRangeKey, } from "./ElectronBoundaryGraphContext.js"; import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js"; interface FindingInput { readonly context: JavaScriptArtifactGraphContext; readonly file: JavaScriptArtifactFile; readonly source: ApplicationNode; readonly value: Value; readonly coverage: JavaScriptArtifactGraphCoverage; } /** Project BrowserWindow, contextBridge, preload, and utility-process facts. */ export const addElectronWindowBoundaries = ( context: JavaScriptArtifactGraphContext, ): void => { for (const analyzed of context.analysis.files) { const { file, javascript } = analyzed; if (javascript === null) continue; const coverage = javascriptAnalysisCoverage(javascript); for (const value of javascript.electron.browser_windows) { const source = electronFindingSourceNode(context, file, value.module_key); if (source !== undefined) addBrowserWindow({ context, file, source, value, coverage }); } for (const value of javascript.electron.context_bridge_apis) { const source = electronFindingSourceNode(context, file, value.module_key); if (source !== undefined) addContextBridge({ context, file, source, value, coverage }); } for (const value of javascript.electron.utility_processes) { const source = electronFindingSourceNode(context, file, value.module_key); if (source !== undefined) addUtilityProcess({ context, file, source, value, coverage }); } } }; const addBrowserWindow = ( input: FindingInput, ): void => { const { context, file, value, coverage } = input; const window = context.accumulator.addNode({ kind: "browser-window", identity: artifactLocalIdentity( file.sha256, "electron-browser-window", electronRangeKey(value.location), ), observations: [ { label: `BrowserWindow@${electronRangeKey(value.location)}`, properties: { source_path: file.path, options_status: value.options_status, web_preferences_status: value.web_preferences_status, web_preferences: value.web_preferences, preload_path: value.preload_path, preload_resolution_context: value.preload_resolution_context, absence_means_default: false, }, evidence: astObservationEvidence({ sha256: file.sha256, path: file.path, range: value.location, operation: "observe-browser-window-construction", coverage, limitations: [ "Only explicitly present webPreferences are reported; omitted properties are not interpreted as Electron defaults.", ], }), }, ], }); addElectronAstEdge(context, { source: input.source, target: window, file, range: value.location, coverage, relation: "contains", operation: "locate-browser-window-construction", properties: { source_path: file.path }, }); addWindowPreload(input, window); }; const addWindowPreload = ( input: FindingInput, window: ApplicationNode, ): void => { if (input.value.preload_path === null) return; const { preload_path: declared, preload_resolution_context: resolutionContext, } = input.value; const resolution = resolveArtifactPathByContext({ declaredPath: declared, sourcePath: input.file.path, context: resolutionContext, files: input.context.filesByPath, }); const role = createElectronRoleNode(input.context, { kind: "electron-preload", anchor: input.file, resolution, mechanism: "BrowserWindow:webPreferences.preload", range: input.value.location, coverage: input.coverage, }); addElectronInferenceEdge(input.context, { source: window, target: role, file: input.file, range: input.value.location, coverage: input.coverage, relation: "loads", operation: "associate-browser-window-preload", properties: { declared_path: declared, resolution_context: resolution.resolution_context, resolved_path: resolution.resolved_path, resolution_status: resolution.resolution_status, limitations: resolution.limitations, }, }); linkElectronRoleToAsset(input.context, { role, anchor: input.file, resolution, range: input.value.location, coverage: input.coverage, }); }; const addContextBridge = ( input: FindingInput, ): void => { const { context, file, value, coverage } = input; const bridge = context.accumulator.addNode({ kind: "context-bridge-api", identity: artifactLocalIdentity( file.sha256, "electron-context-bridge", `${value.world}:${value.api_key ?? value.api_key_expression ?? "[missing]"}:${electronRangeKey(value.location)}`, ), observations: [ { label: value.api_key, properties: { world: value.world, world_id: value.world_id, api_key: value.api_key, api_key_expression: value.api_key_expression, api_status: value.api_status, members: value.members, unknown_members: value.unknown_members, }, evidence: astObservationEvidence({ sha256: file.sha256, path: file.path, range: value.location, operation: "observe-context-bridge-api", coverage, }), }, ], }); addElectronInferenceEdge(context, { source: input.source, target: bridge, file, range: value.location, coverage, relation: "exposes", operation: "map-context-bridge-exposure", properties: { world: value.world, api_key: value.api_key, resolution: value.api_key === null ? "dynamic" : "literal", }, }); }; const addUtilityProcess = ( input: FindingInput, ): void => { const { context, file, value, coverage } = input; const utility = context.accumulator.addNode({ kind: "electron-utility", identity: artifactLocalIdentity( file.sha256, "electron-utility-process", `${value.module_path ?? value.module_expression}:${electronRangeKey(value.location)}`, ), observations: [ { label: value.service_name ?? value.module_path, properties: { module_path: value.module_path, module_resolution_context: value.module_resolution_context, module_expression: value.module_expression, service_name: value.service_name, }, evidence: astObservationEvidence({ sha256: file.sha256, path: file.path, range: value.location, operation: "observe-utility-process-fork", coverage, }), }, ], }); addElectronInferenceEdge(context, { source: input.source, target: utility, file, range: value.location, coverage, relation: "loads", operation: "map-utility-process-launch", properties: { declared_path: value.module_path, expression: value.module_expression, }, }); if (value.module_path === null) return; const resolution = resolveArtifactPathByContext({ declaredPath: value.module_path, sourcePath: file.path, context: value.module_resolution_context, files: context.filesByPath, }); const resolved = resolution.resolved_path; const target = resolved === null ? undefined : (context.assetNodes.get(resolved) ?? context.fileNodes.get(resolved)); if (target === undefined) return; addElectronInferenceEdge(context, { source: utility, target, file, range: value.location, coverage, relation: "maps_to", operation: "resolve-utility-process-entrypoint", properties: { declared_path: value.module_path, resolution_context: resolution.resolution_context, resolved_path: resolved, resolution_status: resolution.resolution_status, }, }); };