feat(electron): report custom URL protocol registrations

Expose static Electron protocol call facts through the shared application graph and existing CLI/MCP Evidence views. Preserve canonical case-insensitive scheme identity, original spellings, exact source provenance and explicit unknowns; keep registration success and runtime routing unproven.

Integration fixes also enforce module-only privileged API ownership, reuse the shared finding-key owner, and align the owning type documentation with the Evidence semantics. Validation and bounded real Obsidian CLI/MCP measurements are recorded in the PR.

Co-authored-by: UnknownHacker1 <35694178+UnknownHacker1@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
Abdelrahman Teimaandgensokyo-dev authored and GitHub committed 2026-10-31 02:23:47 +08:00
1 parent 69bf61f7c7
commit 47fa6ab379
24 files changed
+1134 -11

No files matched your search

@@ -220,6 +220,10 @@ export const summarizeElectronBoundaries = (
moduleKind: finding.module_kind,
}),
).length,
protocol_registrations: javascript.reduce(
(count, { electron }) => count + electron.protocols.length,
0,
),
};
};
@@ -2,6 +2,7 @@ import type { ElectronIpcContext } from "./ElectronBoundaryAnalysis.js";
import type { JavaScriptArtifactGraphContext } from "./JavaScriptArtifactGraphContext.js";
import { addElectronIpcBoundaries } from "./ElectronBoundaryGraphIpc.js";
import { addElectronNativeBoundaries } from "./ElectronBoundaryGraphNative.js";
import { addElectronProtocolBoundaries } from "./ElectronBoundaryGraphProtocols.js";
import { addElectronWindowBoundaries } from "./ElectronBoundaryGraphWindows.js";
import { addElectronCallPaths } from "./ElectronBoundaryGraphCallPaths.js";
@@ -13,5 +14,6 @@ export const addElectronBoundaries = (
addElectronWindowBoundaries(context);
addElectronIpcBoundaries(context, ipc);
addElectronNativeBoundaries(context);
addElectronProtocolBoundaries(context);
addElectronCallPaths(context, ipc);
};
@@ -0,0 +1,129 @@
import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js";
import type { ElectronProtocolFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import {
artifactLocalIdentity,
javascriptAnalysisCoverage,
type JavaScriptArtifactGraphContext,
type JavaScriptArtifactGraphCoverage,
} from "./JavaScriptArtifactGraphContext.js";
import { astObservationEvidence } from "./JavaScriptArtifactGraphEvidence.js";
import {
addElectronInferenceEdge,
electronFindingSourceNode,
electronObservationIdentity,
} from "./ElectronBoundaryGraphContext.js";
interface ProtocolInput {
readonly context: JavaScriptArtifactGraphContext;
readonly file: JavaScriptArtifactFile;
readonly value: ElectronProtocolFinding;
readonly coverage: JavaScriptArtifactGraphCoverage;
}
const URL_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
const REGISTRATION_LIMITATION =
"A registration call is present in source; successful registration, control-flow reachability and operating-system or session routing are not established.";
/** Project custom URL scheme registrations as schemes the application claims. */
export const addElectronProtocolBoundaries = (
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.protocols)
addProtocolRegistration({ context, file, value, coverage });
}
};
const addProtocolRegistration = (input: ProtocolInput): void => {
const { context, file, value, coverage } = input;
const source = electronFindingSourceNode(
context,
file,
value.module_key,
value.module_runtime,
);
if (source === undefined) return;
const literal = value.scheme.status === "literal";
const raw = value.scheme.value;
// RFC 3986: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ).
const scheme =
literal && typeof raw === "string" && URL_SCHEME.test(raw)
? raw.toLowerCase()
: null;
// A non-string or malformed literal is observed exactly but names no scheme.
const invalid = literal && scheme === null;
const facts = {
scheme,
scheme_expression: value.scheme.expression,
scheme_literal: literal ? raw : null,
resolution:
scheme !== null ? "literal" : invalid ? "invalid-literal" : "dynamic",
api: value.api,
role: value.role,
session: value.session,
enabled_privileges: value.enabled_privileges,
};
const limitations =
scheme !== null
? [REGISTRATION_LIMITATION]
: invalid
? [
REGISTRATION_LIMITATION,
"The scheme argument is a literal that is not a valid URL scheme, so no URL can target it as written.",
]
: [
REGISTRATION_LIMITATION,
"The scheme expression is dynamic; its runtime value remains unknown.",
];
// Scheme names are case-insensitive; retain their spelling separately from
// canonical identity. One node per scheme across the artifact, so one both
// privileged and handled is one entry point with several registrations.
const node = context.accumulator.addNode({
kind: "url-scheme",
identity:
scheme === null
? artifactLocalIdentity(
file.sha256,
"electron-dynamic-url-scheme",
// Identical bytes at two paths are separate occurrences whose
// expressions can resolve differently.
`${file.path}:${value.api}:${javascriptSourceRangeKey(value.location)}`,
)
: electronObservationIdentity(
context,
"electron-url-scheme",
`literal:${scheme}`,
),
observations: [
{
label: scheme ?? value.scheme.expression ?? String(value.scheme.value),
properties: facts,
evidence: astObservationEvidence({
sha256: file.sha256,
path: file.path,
range: value.location,
operation: "observe-protocol-registration",
coverage,
limitations,
}),
},
],
});
addElectronInferenceEdge(context, {
source,
target: node,
file,
range: value.location,
coverage,
// Granting privileges exposes the scheme; the other roles claim its links.
relation: value.role === "privileged-scheme" ? "exposes" : "handles",
operation: "map-protocol-registration",
properties: facts,
limitations,
});
};
@@ -668,7 +668,8 @@ const findingCount = (analysis: JavaScriptStaticAnalysis): number =>
analysis.electron.ipc.length +
analysis.electron.sender_validations.length +
analysis.electron.utility_processes.length +
analysis.electron.native_addon_bindings.length;
analysis.electron.native_addon_bindings.length +
analysis.electron.protocols.length;
const optionalString = (value: unknown): string | null =>
typeof value === "string" && value.length > 0 ? value : null;
@@ -366,7 +366,8 @@ const graphLimitations = (
javascript.electron.ipc.length +
javascript.electron.sender_validations.length +
javascript.electron.utility_processes.length +
javascript.electron.native_addon_bindings.length),
javascript.electron.native_addon_bindings.length +
javascript.electron.protocols.length),
0,
);
return [