fix(javascript): match exact empty literal channels and seeds
Pair the same empty IPC channel, context-bridge key, or endpoint value across application versions by its exact value, and accept an exact empty feature seed while rejecting an empty containment seed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
527b76ef78
commit
e20da1f205
|
|
@ -637,9 +637,9 @@
|
|||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "860472c8261d77c235f72eea3d1380a024df68efc30e095ab2a8a4856c050ec1",
|
||||
"tools_sha256": "d1962c65198f036b57556918d59b2a5bdf544d2cd7c8a60ff0e4fce2efd472f1",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "e9e061962e5d50b1793890284c2431206950da3947211f6986ba05a60b7cd9dc",
|
||||
"combined_sha256": "66307e8428ce3bddbf004e43d15a0d6b60965667cc1c1bb792b28a7b63837b74",
|
||||
"providers_sha256": "7949ddcf2eb5ba41b022bb3dfa1951ab96dbf5640285fa7fd5d35b0e4a5bdab3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"manifest_id": "ecm_0e087361a14f9b7a3d8e01dcb61d68cccb438e07b09c237568b678608856de98",
|
||||
"manifest_id": "ecm_cbc35f371fbe873508281f13785667c48063cf8d9dee4e8bb8e74126baf0fba7",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "d93aff007a8a5a8ade101f84d891ce969756fca753679d94b75ec8451764e8cd"
|
||||
"sha256": "79a973d8c9ac217408e7f92e4a0f776b049d74dbf1d2b0c5249dab5127de893c"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
|
|||
metadata:
|
||||
version: "26"
|
||||
tool_count: 126
|
||||
catalog_digest: "e9e061962e5d50b1793890284c2431206950da3947211f6986ba05a60b7cd9dc"
|
||||
catalog_digest: "66307e8428ce3bddbf004e43d15a0d6b60965667cc1c1bb792b28a7b63837b74"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import { expect, it } from "vitest";
|
|||
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../domain/javascriptApplicationAnalysis.js";
|
||||
import { matchJavaScriptApplicationVersions } from "../domain/javascriptApplicationVersionKeys.js";
|
||||
import { findApplicationFeatureSeeds } from "../domain/javascriptFeatureSeed.js";
|
||||
import { traceApplicationFeatureInputSchema } from "../domain/javascriptFeatureTraceSchemas.js";
|
||||
import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js";
|
||||
|
||||
const analyzeSource = async (
|
||||
|
|
@ -152,3 +154,74 @@ it("does not match an empty endpoint with a literal two-quote seed", async () =>
|
|||
'""',
|
||||
]);
|
||||
});
|
||||
|
||||
const IPC_SOURCE =
|
||||
"const { ipcMain } = require('electron'); ipcMain.handle('', () => 1); ipcMain.handle('\"\"', () => 2);";
|
||||
|
||||
const ipcChannels = (
|
||||
nodes: Awaited<ReturnType<typeof analyzeSource>>["graph"]["nodes"],
|
||||
) =>
|
||||
new Map(
|
||||
nodes
|
||||
.filter(({ kind }) => kind === "ipc-channel")
|
||||
.map((node) => [node.node_id, node.observations[0]?.properties.channel]),
|
||||
);
|
||||
|
||||
it("selects an empty IPC channel with an exact empty seed", async () => {
|
||||
const output = await analyzeSource(IPC_SOURCE);
|
||||
const channels = ipcChannels(output.graph.nodes);
|
||||
const matches = findApplicationFeatureSeeds(output.graph.nodes, {
|
||||
kind: "channel",
|
||||
value: "",
|
||||
match: "exact",
|
||||
case_sensitive: true,
|
||||
});
|
||||
expect(matches.map(({ node_id: nodeId }) => channels.get(nodeId))).toEqual([
|
||||
"",
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ kind: "channel", value: "" },
|
||||
{ kind: "string", value: "", match: "exact" },
|
||||
])("accepts an exact empty seed: %j", (seed) => {
|
||||
expect(
|
||||
traceApplicationFeatureInputSchema.shape.seed.safeParse(seed).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ kind: "string", value: "" },
|
||||
{ kind: "channel", value: "", match: "contains" },
|
||||
{ kind: "node-id", value: "", match: "exact" },
|
||||
])("rejects an empty seed that is not an exact literal: %j", (seed) => {
|
||||
const parsed = traceApplicationFeatureInputSchema.shape.seed.safeParse(seed);
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
|
||||
it("pairs the same empty IPC channel across application versions", async () => {
|
||||
const left = await analyzeSource(IPC_SOURCE);
|
||||
const right = await analyzeSource(`${IPC_SOURCE} // v2`);
|
||||
const leftChannels = ipcChannels(left.graph.nodes);
|
||||
const rightChannels = ipcChannels(right.graph.nodes);
|
||||
const matching = matchJavaScriptApplicationVersions(
|
||||
left.graph.nodes,
|
||||
right.graph.nodes,
|
||||
);
|
||||
expect(
|
||||
matching.pairs
|
||||
.filter(({ left: node }) => node.kind === "ipc-channel")
|
||||
.map(({ left: leftNode, right: rightNode }) => [
|
||||
leftChannels.get(leftNode.node_id),
|
||||
rightChannels.get(rightNode.node_id),
|
||||
]),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
["", ""],
|
||||
['""', '""'],
|
||||
]),
|
||||
);
|
||||
expect(matching.unmatchedLeft.map(({ kind }) => kind)).not.toContain(
|
||||
"ipc-channel",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -229,8 +229,11 @@ const semanticValue = (
|
|||
node: ApplicationNode,
|
||||
properties: Readonly<Record<string, JsonValue>>,
|
||||
): string | null => {
|
||||
if (node.kind === "endpoint")
|
||||
return joined(properties.endpoint_kind, properties.value);
|
||||
if (node.kind === "endpoint") {
|
||||
const kind = nonEmptyString(properties.endpoint_kind);
|
||||
const value = exactString(properties.value);
|
||||
return kind === null || value === null ? null : `${kind}\0${value}`;
|
||||
}
|
||||
if (node.kind === "storage")
|
||||
return joined(
|
||||
properties.storage_kind,
|
||||
|
|
@ -239,9 +242,13 @@ const semanticValue = (
|
|||
if (node.kind === "ipc-channel")
|
||||
return properties.resolution === "dynamic"
|
||||
? null
|
||||
: (nonEmptyString(properties.channel) ?? firstLabel(node));
|
||||
: (exactString(properties.channel) ?? firstLabel(node));
|
||||
if (node.kind === "context-bridge-api")
|
||||
return firstLabel(node) ?? nonEmptyString(properties.api_name);
|
||||
return (
|
||||
firstLabel(node) ??
|
||||
exactString(properties.api_key) ??
|
||||
nonEmptyString(properties.api_name)
|
||||
);
|
||||
if (node.kind === "native-export")
|
||||
return (
|
||||
joined(
|
||||
|
|
@ -293,6 +300,10 @@ const joined = (...values: readonly unknown[]): string | null => {
|
|||
const nonEmptyString = (value: unknown): string | null =>
|
||||
typeof value === "string" && value !== "" ? value : null;
|
||||
|
||||
/** A literal value such as `ipcMain.handle("")` keeps its exact empty text. */
|
||||
const exactString = (value: unknown): string | null =>
|
||||
typeof value === "string" ? value : null;
|
||||
|
||||
const stringList = (value: unknown): string[] =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { compareCodePoints } from "./canonicalOrdering.js";
|
||||
import type { ApplicationNode } from "./javascriptApplicationGraph.js";
|
||||
import type { ApplicationFeatureSeed } from "./javascriptFeatureTraceSchemas.js";
|
||||
import {
|
||||
featureSeedMatchMode,
|
||||
type ApplicationFeatureSeed,
|
||||
} from "./javascriptFeatureTraceSchemas.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
|
||||
/** One explicit field that matched a feature seed. */
|
||||
|
|
@ -49,7 +52,7 @@ const seedMatch = (
|
|||
]
|
||||
: [];
|
||||
if (!kindMatches(node, seed.kind)) return [];
|
||||
const mode = seed.match ?? (seed.kind === "string" ? "contains" : "exact");
|
||||
const mode = featureSeedMatchMode(seed);
|
||||
const query = normalized(seed.value, seed.case_sensitive);
|
||||
const match = candidateFields(node, seed.kind).find((candidate) => {
|
||||
const value = normalized(candidate.value, seed.case_sensitive);
|
||||
|
|
|
|||
|
|
@ -10,21 +10,43 @@ const nodeIdSchema = prefixedDigestSchema("jag_node");
|
|||
const edgeIdSchema = prefixedDigestSchema("jag_edge");
|
||||
const boundedTextSchema = z.string().min(1);
|
||||
|
||||
/** Literal starting point for one application feature trace. */
|
||||
const applicationFeatureSeedSchema = z.strictObject({
|
||||
kind: z.enum([
|
||||
"node-id",
|
||||
"route",
|
||||
"string",
|
||||
"api",
|
||||
"channel",
|
||||
"module",
|
||||
"native-export",
|
||||
]),
|
||||
value: boundedTextSchema,
|
||||
match: z.enum(["exact", "contains"]).nullable().default(null),
|
||||
case_sensitive: z.boolean().default(false),
|
||||
});
|
||||
/** Matching mode for a literal seed: string seeds default to containment. */
|
||||
export const featureSeedMatchMode = (seed: {
|
||||
readonly kind: string;
|
||||
readonly match: "exact" | "contains" | null;
|
||||
}): "exact" | "contains" =>
|
||||
seed.match ?? (seed.kind === "string" ? "contains" : "exact");
|
||||
|
||||
/**
|
||||
* Literal starting point for one application feature trace. An exact literal
|
||||
* seed may be empty, such as the channel in `ipcMain.handle("")`; an empty
|
||||
* containment seed would select every node, so it is rejected.
|
||||
*/
|
||||
const applicationFeatureSeedSchema = z
|
||||
.strictObject({
|
||||
kind: z.enum([
|
||||
"node-id",
|
||||
"route",
|
||||
"string",
|
||||
"api",
|
||||
"channel",
|
||||
"module",
|
||||
"native-export",
|
||||
]),
|
||||
value: z.string(),
|
||||
match: z.enum(["exact", "contains"]).nullable().default(null),
|
||||
case_sensitive: z.boolean().default(false),
|
||||
})
|
||||
.superRefine((seed, context) => {
|
||||
if (seed.value !== "") return;
|
||||
if (seed.kind === "node-id" || featureSeedMatchMode(seed) === "contains")
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["value"],
|
||||
message:
|
||||
'An empty seed value requires an exact literal match: use match "exact" with a non-node-id kind',
|
||||
});
|
||||
});
|
||||
|
||||
/** Evidence-backed application graph and feature seed. */
|
||||
export const traceApplicationFeatureInputSchema = z
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue