mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 13:26:10 +00:00
feat(android): trace callers of external DEX methods (#8154)
Resolve exact external method targets from directly encoded DEX invokes. Preserve complete caller identity, bounded position evidence and follow-up selectors. Report aggregate position coverage independently of the caller inventory, and reuse bounded prototype facts within the exact session. Absorbs #8096. Fixes #7811. Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>
This commit is contained in:
1 parent
675d4076de
commit
0ffed7a48c
20 files changed
+1472
-76
No files matched your search
+160
-43
@@ -11,6 +11,7 @@ import { parseAndroidManifestComponents } from "./AndroidManifestComponents.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { AndroidNativeDeclarations } from "../domain/android/androidNativeDeclarations.js";
|
||||
@@ -19,11 +20,11 @@ import {
|
||||
ANDROID_JNI_REPORT_BYTES,
|
||||
projectAndroidJniBindings,
|
||||
} from "../domain/android/androidJniBindings.js";
|
||||
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
jadxAppSchema,
|
||||
type JadxClassPage,
|
||||
jadxClassSchema,
|
||||
jadxExternalReferenceSummarySchema,
|
||||
jadxLoadSchema,
|
||||
jadxMethodSchema,
|
||||
jadxXrefsSchema,
|
||||
@@ -68,7 +69,7 @@ const observedPackageSchema =
|
||||
components: observedAndroidManifestComponentsSchema,
|
||||
});
|
||||
|
||||
const methodsOf = (summary: ClassSummary) => {
|
||||
const methodsOf = (summary: Pick<ClassSummary, "methods">) => {
|
||||
const seen = new Map<string, number>();
|
||||
return summary.methods.map((method) => {
|
||||
const index = seen.get(method.name) ?? 0;
|
||||
@@ -137,26 +138,33 @@ const inputFailure = (
|
||||
]);
|
||||
};
|
||||
|
||||
const classSummary = async (
|
||||
tools: JadxToolPort,
|
||||
request: Exclude<
|
||||
AndroidRequest,
|
||||
{
|
||||
operation:
|
||||
| "inspect_android_package"
|
||||
| "search_android_classes"
|
||||
| "inspect_android_jni_bindings";
|
||||
}
|
||||
>,
|
||||
): Promise<ClassSummary> => {
|
||||
// Only class inspection reports DEX static values; method and reference
|
||||
// lookups keep the member-only summary so large constants cannot block them.
|
||||
const raw = await tools.json("get_class_summary", {
|
||||
class_name: request.input.class_name,
|
||||
...(request.operation === "inspect_android_class"
|
||||
? { dex_static_values: true }
|
||||
: {}),
|
||||
});
|
||||
type ClassRequest = Exclude<
|
||||
AndroidRequest,
|
||||
{
|
||||
operation:
|
||||
| "inspect_android_package"
|
||||
| "search_android_classes"
|
||||
| "inspect_android_jni_bindings";
|
||||
}
|
||||
>;
|
||||
type ReferenceSummary = Pick<ClassSummary, "full_name" | "methods"> & {
|
||||
readonly reference_target?: "external";
|
||||
};
|
||||
|
||||
const parsedClassSummary = (
|
||||
raw: JsonValue,
|
||||
request: ClassRequest,
|
||||
): ClassSummary => {
|
||||
if (
|
||||
raw !== null &&
|
||||
typeof raw === "object" &&
|
||||
!Array.isArray(raw) &&
|
||||
"reference_target" in raw
|
||||
)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
"External prototypes are not an APK class definition",
|
||||
);
|
||||
if (
|
||||
raw !== null &&
|
||||
typeof raw === "object" &&
|
||||
@@ -197,6 +205,68 @@ const classSummary = async (
|
||||
return value;
|
||||
};
|
||||
|
||||
const classSummary = async (
|
||||
tools: JadxToolPort,
|
||||
request: ClassRequest,
|
||||
): Promise<ClassSummary> =>
|
||||
parsedClassSummary(
|
||||
await tools.json("get_class_summary", {
|
||||
class_name: request.input.class_name,
|
||||
...(request.operation === "inspect_android_class"
|
||||
? { dex_static_values: true }
|
||||
: {}),
|
||||
}),
|
||||
request,
|
||||
);
|
||||
|
||||
const referenceSummary = async (
|
||||
tools: JadxToolPort,
|
||||
request: Extract<AndroidRequest, { operation: "trace_android_references" }>,
|
||||
): Promise<ReferenceSummary> => {
|
||||
if (request.input.method_name === undefined)
|
||||
return classSummary(tools, request);
|
||||
const raw = await tools.json("get_class_summary", {
|
||||
class_name: request.input.class_name,
|
||||
method_name: request.input.method_name,
|
||||
reference_target: true,
|
||||
});
|
||||
if (
|
||||
raw === null ||
|
||||
typeof raw !== "object" ||
|
||||
Array.isArray(raw) ||
|
||||
raw.reference_target !== "external"
|
||||
)
|
||||
return parsedClassSummary(raw, request);
|
||||
const parsed = jadxExternalReferenceSummarySchema.safeParse(raw);
|
||||
if (!parsed.success)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
"JADX external prototype inventory is malformed",
|
||||
{ cause: parsed.error },
|
||||
);
|
||||
const value = parsed.data;
|
||||
if (
|
||||
value.class_name !== request.input.class_name ||
|
||||
value.observed_method_count !== value.prototypes.length ||
|
||||
value.prototypes.some(
|
||||
(method) =>
|
||||
method.name !== request.input.method_name ||
|
||||
method.raw_name !== method.name,
|
||||
) ||
|
||||
new Set(value.prototypes.map((method) => method.dex_descriptor)).size !==
|
||||
value.prototypes.length
|
||||
)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
"JADX external prototypes do not match the selected method inventory",
|
||||
);
|
||||
return {
|
||||
full_name: value.class_name,
|
||||
methods: value.prototypes,
|
||||
reference_target: "external",
|
||||
};
|
||||
};
|
||||
|
||||
const inspectPackage = async (
|
||||
tools: JadxToolPort,
|
||||
loaded: z.infer<typeof jadxLoadSchema>,
|
||||
@@ -286,7 +356,7 @@ const searchClasses = async (
|
||||
};
|
||||
|
||||
const matchingMethods = (
|
||||
summary: ClassSummary,
|
||||
summary: ReferenceSummary,
|
||||
request: Extract<
|
||||
AndroidRequest,
|
||||
{ operation: "inspect_android_method" | "trace_android_references" }
|
||||
@@ -304,6 +374,12 @@ const matchingMethods = (
|
||||
return candidates;
|
||||
};
|
||||
|
||||
const overloadChoices = (
|
||||
candidates: ReturnType<typeof matchingMethods>,
|
||||
external = false,
|
||||
): string =>
|
||||
`Select one of ${candidates.length} overloads: ${candidates.map((method) => `${method.overload_index}: ${method.reported_signature}${external ? ` [${method.dex_descriptor}]` : ""}`).join("; ")}`;
|
||||
|
||||
const inspectMethod = async (
|
||||
tools: JadxToolPort,
|
||||
request: Extract<AndroidRequest, { operation: "inspect_android_method" }>,
|
||||
@@ -312,11 +388,7 @@ const inspectMethod = async (
|
||||
): Promise<JsonValue> => {
|
||||
const candidates = matchingMethods(summary, request);
|
||||
if (candidates.length > 1 && request.input.overload_index === undefined)
|
||||
inputFailure(
|
||||
request,
|
||||
"overload_index",
|
||||
`Select one of ${candidates.length} overloads: ${candidates.map((method) => `${method.overload_index}: ${method.reported_signature}`).join("; ")}`,
|
||||
);
|
||||
inputFailure(request, "overload_index", overloadChoices(candidates));
|
||||
const index = request.input.overload_index ?? 0;
|
||||
const selected =
|
||||
candidates[index] ??
|
||||
@@ -414,6 +486,13 @@ export const analyzeJadxRequest = async (
|
||||
return inspectPackage(tools, loaded, engine);
|
||||
if (request.operation === "search_android_classes")
|
||||
return searchClasses(tools, request, loaded, engine);
|
||||
if (request.operation === "trace_android_references")
|
||||
return traceReferences(
|
||||
tools,
|
||||
request,
|
||||
await referenceSummary(tools, request),
|
||||
engine,
|
||||
);
|
||||
const summary = await classSummary(tools, request);
|
||||
if (request.operation === "inspect_android_class")
|
||||
return androidResultSchemas.inspect_android_class.parse({
|
||||
@@ -433,18 +512,17 @@ export const analyzeJadxRequest = async (
|
||||
...fieldQueries(summary),
|
||||
]),
|
||||
});
|
||||
if (request.operation === "inspect_android_method")
|
||||
return inspectMethod(tools, request, summary, context);
|
||||
return traceReferences(tools, request, summary, engine);
|
||||
return inspectMethod(tools, request, summary, context);
|
||||
};
|
||||
|
||||
const traceReferences = async (
|
||||
tools: JadxToolPort,
|
||||
request: Extract<AndroidRequest, { operation: "trace_android_references" }>,
|
||||
summary: ClassSummary,
|
||||
summary: ReferenceSummary,
|
||||
engine: Engine,
|
||||
): Promise<JsonValue> => {
|
||||
const methodName = request.input.method_name;
|
||||
const external = summary.reference_target === "external";
|
||||
const candidates = matchingMethods(summary, request);
|
||||
if (methodName === undefined && request.input.overload_index !== undefined)
|
||||
inputFailure(
|
||||
@@ -456,7 +534,7 @@ const traceReferences = async (
|
||||
inputFailure(
|
||||
request,
|
||||
"overload_index",
|
||||
`Select one of ${candidates.length} overloads: ${candidates.map((method) => `${method.overload_index}: ${method.reported_signature}`).join("; ")}`,
|
||||
overloadChoices(candidates, external),
|
||||
);
|
||||
const index = request.input.overload_index ?? 0;
|
||||
const selected = methodName === undefined ? undefined : candidates[index];
|
||||
@@ -475,6 +553,7 @@ const traceReferences = async (
|
||||
methodName === undefined ? "get_xrefs_to_class" : "get_xrefs_to_method",
|
||||
{
|
||||
class_name: request.input.class_name,
|
||||
...(external ? { reference_target: true } : {}),
|
||||
...(methodName === undefined
|
||||
? {}
|
||||
: { method_name: methodName, overload_index: index }),
|
||||
@@ -485,6 +564,20 @@ const traceReferences = async (
|
||||
);
|
||||
if (
|
||||
xrefs.target !== expectedTarget ||
|
||||
(external &&
|
||||
(xrefs.reference_target !== "external" ||
|
||||
xrefs.reference_scope !== "direct_dex_invokes" ||
|
||||
xrefs.dex_descriptor !== selected?.dex_descriptor ||
|
||||
xrefs.items.some(
|
||||
(item) =>
|
||||
item.kind !== "method" ||
|
||||
item.source_selector === undefined ||
|
||||
item.source_selector.class_name !== item.containing_class ||
|
||||
item.source_selector.method_name !== item.name ||
|
||||
item.direct_invokes === undefined ||
|
||||
item.direct_invokes_exhaustive === undefined,
|
||||
))) ||
|
||||
(!external && xrefs.reference_target === "external") ||
|
||||
(selected !== undefined &&
|
||||
(xrefs.overload_index !== index ||
|
||||
xrefs.reported_signature !== selected.reported_signature)) ||
|
||||
@@ -495,15 +588,23 @@ const traceReferences = async (
|
||||
request.operation,
|
||||
"JADX references are incomplete or refer to another target",
|
||||
);
|
||||
return androidResultSchemas.trace_android_references.parse({
|
||||
engine,
|
||||
class_name: summary.full_name,
|
||||
method_name: methodName ?? null,
|
||||
overload_index: selected?.overload_index ?? null,
|
||||
reported_signature: selected?.reported_signature ?? null,
|
||||
dex_descriptor: selected?.dex_descriptor ?? null,
|
||||
total: xrefs.total,
|
||||
references: xrefs.items.map((reference) => ({
|
||||
const positions = {
|
||||
complete_using_nodes: 0,
|
||||
partial_using_nodes: 0,
|
||||
unknown_using_nodes: 0,
|
||||
recorded_sites: 0,
|
||||
};
|
||||
const references = xrefs.items.map((reference) => {
|
||||
positions.recorded_sites += reference.direct_invokes?.length ?? 0;
|
||||
if (
|
||||
reference.direct_invokes === undefined ||
|
||||
reference.direct_invokes_exhaustive === undefined
|
||||
)
|
||||
positions.unknown_using_nodes++;
|
||||
else if (reference.direct_invokes_exhaustive)
|
||||
positions.complete_using_nodes++;
|
||||
else positions.partial_using_nodes++;
|
||||
return {
|
||||
kind: reference.kind,
|
||||
name: reference.name,
|
||||
reported_full_name: reference.full_name,
|
||||
@@ -511,9 +612,25 @@ const traceReferences = async (
|
||||
top_class: reference.top_class,
|
||||
source_line: null,
|
||||
dex_offset: null,
|
||||
...(reference.source_selector === undefined
|
||||
? {}
|
||||
: { source_selector: reference.source_selector }),
|
||||
direct_invokes: reference.direct_invokes ?? null,
|
||||
direct_invokes_exhaustive: reference.direct_invokes_exhaustive ?? null,
|
||||
})),
|
||||
};
|
||||
});
|
||||
return androidResultSchemas.trace_android_references.parse({
|
||||
engine,
|
||||
target_definition: external ? "external" : "defined",
|
||||
...(external ? { reference_scope: "direct_dex_invokes" } : {}),
|
||||
class_name: summary.full_name,
|
||||
method_name: methodName ?? null,
|
||||
overload_index: selected?.overload_index ?? null,
|
||||
reported_signature: selected?.reported_signature ?? null,
|
||||
dex_descriptor: selected?.dex_descriptor ?? null,
|
||||
total: xrefs.total,
|
||||
direct_invokes_coverage: positions,
|
||||
references,
|
||||
coverage: "complete",
|
||||
reference_unit: "using_node",
|
||||
});
|
||||
|
||||
+31
-11
@@ -2,7 +2,10 @@ import { z } from "zod";
|
||||
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
|
||||
import { dexStaticValueSchema } from "../domain/android/androidAnalysis.js";
|
||||
import {
|
||||
androidMethodSelectorSchema,
|
||||
dexStaticValueSchema,
|
||||
} from "../domain/android/androidAnalysis.js";
|
||||
import {
|
||||
dexDescriptorSchema,
|
||||
dexTypeSchema,
|
||||
@@ -27,9 +30,15 @@ const jadxInputFailureSchema = z.strictObject({
|
||||
field: z.enum(["class_name", "method_name", "overload_index"]),
|
||||
message: z.string().min(1),
|
||||
});
|
||||
const jadxReferenceLimitSchema = z.strictObject({
|
||||
kind: z.literal("reference-output-limit"),
|
||||
stage: z.enum(["method_inventory", "caller_inventory"]),
|
||||
limit_bytes: z.int().positive().safe(),
|
||||
});
|
||||
const bridgeFailureSchema = z.discriminatedUnion("kind", [
|
||||
jadxInputFailureSchema,
|
||||
jadxMemoryFailureSchema,
|
||||
jadxReferenceLimitSchema,
|
||||
]);
|
||||
|
||||
/** Arbitrary upstream failure text stays text unless it matches a named bridge contract. */
|
||||
@@ -87,6 +96,15 @@ export const parseJadxClassPage = (value: JsonValue): JadxClassPage => {
|
||||
return value as JadxClassPage;
|
||||
};
|
||||
|
||||
const jadxMethodMetadataSchema = z.object({
|
||||
name: z.string(),
|
||||
raw_name: z.string().min(1),
|
||||
signature: z.string(),
|
||||
dex_descriptor: dexDescriptorSchema,
|
||||
is_constructor: z.boolean(),
|
||||
def_pos: z.int(),
|
||||
});
|
||||
|
||||
export const jadxClassSchema = z.object({
|
||||
full_name: z.string().min(1),
|
||||
raw_name: z.string().min(1),
|
||||
@@ -94,16 +112,7 @@ export const jadxClassSchema = z.object({
|
||||
method_count: count,
|
||||
field_count: count,
|
||||
inner_class_count: count,
|
||||
methods: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
raw_name: z.string().min(1),
|
||||
signature: z.string(),
|
||||
dex_descriptor: dexDescriptorSchema,
|
||||
is_constructor: z.boolean(),
|
||||
def_pos: z.int(),
|
||||
}),
|
||||
),
|
||||
methods: z.array(jadxMethodMetadataSchema),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
@@ -118,6 +127,13 @@ export const jadxClassSchema = z.object({
|
||||
/** Original binary names, index-aligned with the display names above. */
|
||||
inner_class_raw_names: z.array(z.string()),
|
||||
});
|
||||
/** Reference-only prototypes are not an external class definition or field inventory. */
|
||||
export const jadxExternalReferenceSummarySchema = z.object({
|
||||
reference_target: z.literal("external"),
|
||||
class_name: z.string().min(1),
|
||||
observed_method_count: count,
|
||||
prototypes: z.array(jadxMethodMetadataSchema).min(1),
|
||||
});
|
||||
export const jadxMethodSchema = z.object({
|
||||
class_name: z.string(),
|
||||
method_name: z.string(),
|
||||
@@ -132,6 +148,9 @@ export const jadxMethodSchema = z.object({
|
||||
});
|
||||
export const jadxXrefsSchema = z.object({
|
||||
target: z.string(),
|
||||
reference_target: z.literal("external").optional(),
|
||||
reference_scope: z.literal("direct_dex_invokes").optional(),
|
||||
dex_descriptor: dexDescriptorSchema.optional(),
|
||||
overload_index: count.optional(),
|
||||
reported_signature: z.string().optional(),
|
||||
total: count,
|
||||
@@ -145,6 +164,7 @@ export const jadxXrefsSchema = z.object({
|
||||
containing_class: z.string(),
|
||||
top_class: z.string(),
|
||||
def_pos: z.int(),
|
||||
source_selector: androidMethodSelectorSchema.optional(),
|
||||
direct_invokes_exhaustive: z.boolean().optional(),
|
||||
direct_invokes: z
|
||||
.array(
|
||||
|
||||
@@ -10,7 +10,7 @@ export const JADX_RELEASE = Object.freeze({
|
||||
/** Identity of REA's metadata bridge, distinct from the producing engine. */
|
||||
export const JADX_BRIDGE_IDENTITY = Object.freeze({
|
||||
name: "rea-jadx-bridge",
|
||||
version: "9",
|
||||
version: "10",
|
||||
});
|
||||
|
||||
/** Remediation when `REA_JADX_MCP_JAR` is missing or not an absolute path. */
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
type JsonValue,
|
||||
} from "../domain/jsonValue.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
|
||||
import type { OwnedProviderProcessSpawnOptions } from "../process/ProviderProcess.js";
|
||||
import { analyzeJadxRequest, type JadxToolPort } from "./JadxAnalysis.js";
|
||||
import { readAndroidNativeInventory } from "./AndroidNativeInventory.js";
|
||||
@@ -287,11 +288,10 @@ export class JadxSession {
|
||||
calls: this.#observedCalls.map(({ complete }) => complete),
|
||||
},
|
||||
subject: target,
|
||||
limitations: [
|
||||
...(request.operation === "inspect_android_jni_bindings"
|
||||
limitations:
|
||||
request.operation === "inspect_android_jni_bindings"
|
||||
? []
|
||||
: [...JADX_LIMITATIONS, ...androidMappingLimitations(mapping)]),
|
||||
],
|
||||
: [...JADX_LIMITATIONS, ...androidMappingLimitations(mapping)],
|
||||
locations:
|
||||
request.operation === "inspect_android_package"
|
||||
? [{ kind: "artifact-path", path: "AndroidManifest.xml" }]
|
||||
@@ -382,6 +382,17 @@ export class JadxSession {
|
||||
if (envelope.isError === true) {
|
||||
const failure = parseJadxFailure(text);
|
||||
if (failure !== undefined) {
|
||||
if (failure.kind === "reference-output-limit")
|
||||
throw new AnalysisResourceConstraintError(
|
||||
request.operation,
|
||||
"memory",
|
||||
"External DEX reference metadata exceeds the bounded transport representation allowance.",
|
||||
{ stage: failure.stage, limit_bytes: failure.limit_bytes },
|
||||
fixedAllowanceOptions(
|
||||
`The external DEX ${failure.stage} representation allowance of ${String(failure.limit_bytes)} bytes`,
|
||||
"report",
|
||||
),
|
||||
);
|
||||
if (name === "load_apk" && failure.kind === "jvm-memory-failure")
|
||||
throw new AnalysisResourceConstraintError(
|
||||
request.operation,
|
||||
|
||||
@@ -261,16 +261,20 @@ export const registerAndroidCommands = (
|
||||
});
|
||||
cli.command(CLI_COMMANDS.traceAndroidReferences, {
|
||||
description:
|
||||
"Trace incoming APK using nodes for a class or selected method",
|
||||
"Trace APK using nodes for a defined class/method or an externally invoked DEX method",
|
||||
args: z.object({ path, className }),
|
||||
options: z.object({
|
||||
methodName: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Declared method name; omit for class references"),
|
||||
.describe(
|
||||
"Defined or directly referenced external method; omit for APK class references",
|
||||
),
|
||||
overloadIndex: cliIntegerArgument(0)
|
||||
.optional()
|
||||
.describe("Index among same-name methods from inspect-android-class"),
|
||||
.describe(
|
||||
"Index among defined methods or observed external prototypes listed by an ambiguous query",
|
||||
),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
execute(
|
||||
|
||||
@@ -167,7 +167,7 @@ export const ANDROID_TOOL_CONTRACTS = [
|
||||
name: "trace_android_references",
|
||||
...toolContractMetadata("trace_android_references"),
|
||||
description:
|
||||
"Trace incoming static using nodes to an exact Android class or selected method overload. The total counts containing nodes, not individual invoke instructions. Use inspect_android_class to choose an overload_index when names collide. Source lines and the containing node dex_offset remain unknown. direct_invokes carries exact DEX code-unit instruction offsets when available; direct_invokes_exhaustive false marks byte-bounded partial instruction evidence, and null means coverage is unknown. Using-node coverage remains separate; no runtime calls are inferred.",
|
||||
"Trace incoming static using nodes to an exact Android class or selected method overload. A method target may be an external class directly named by DEX invokes: target_definition external and reference_scope direct_dex_invokes identify this narrower inventory, without inventing a class definition or body. Ambiguous external requests list observed DEX prototypes; select overload_index explicitly. For APK-defined targets use inspect_android_class. The total counts containing nodes, not individual invokes. External callers include source_selector for inspect_android_method. Source lines and the containing node dex_offset remain unknown. direct_invokes carries exact DEX code-unit offsets when available; direct_invokes_exhaustive false marks byte-bounded partial instruction evidence, and null means unknown. No reflection, JNI or runtime dispatch is inferred.",
|
||||
kind: "android-provider",
|
||||
inputSchema: androidInputSchemas.trace_android_references,
|
||||
outputSchema: evidenceResultOf(
|
||||
@@ -178,6 +178,15 @@ export const ANDROID_TOOL_CONTRACTS = [
|
||||
title: "Find references to a class",
|
||||
input: { path: "/tmp/Example.apk", class_name: "example.MainActivity" },
|
||||
},
|
||||
{
|
||||
title: "Find APK callers of an observed external API prototype",
|
||||
input: {
|
||||
path: "/tmp/Example.apk",
|
||||
class_name: "android.app.NotificationManager",
|
||||
method_name: "notify",
|
||||
overload_index: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly ToolContract[];
|
||||
@@ -58,6 +58,9 @@ export const androidInputSchemas = {
|
||||
}),
|
||||
trace_android_references: z.strictObject({
|
||||
...classTarget,
|
||||
class_name: classTarget.class_name.describe(
|
||||
"Exact APK-defined class, or an externally referenced DEX class when method_name is selected",
|
||||
),
|
||||
method_name: z
|
||||
.string()
|
||||
.min(1)
|
||||
@@ -70,7 +73,7 @@ export const androidInputSchemas = {
|
||||
.nonnegative()
|
||||
.optional()
|
||||
.describe(
|
||||
"Index among same-name methods from inspect_android_class; requires method_name",
|
||||
"Index among same-name APK methods or observed external DEX prototypes; requires method_name",
|
||||
),
|
||||
}),
|
||||
} as const;
|
||||
@@ -199,6 +202,12 @@ const method = z.strictObject({
|
||||
is_constructor: z.boolean(),
|
||||
dex_descriptor: dexDescriptorSchema,
|
||||
});
|
||||
/** Exact member selector shared with follow-up APK method inspection. */
|
||||
export const androidMethodSelectorSchema = z.strictObject({
|
||||
class_name: z.string().min(1),
|
||||
method_name: z.string().min(1),
|
||||
overload_index: z.int().nonnegative(),
|
||||
});
|
||||
const engine = z.strictObject({
|
||||
name: z.string().min(1),
|
||||
version: z.string().nullable(),
|
||||
@@ -276,12 +285,26 @@ export const androidResultSchemas = {
|
||||
}),
|
||||
trace_android_references: z.strictObject({
|
||||
...common,
|
||||
/** Older retained Evidence omits this fact; omission does not mean external. */
|
||||
target_definition: z.enum(["defined", "external"]).exactOptional(),
|
||||
/** External results inventory directly encoded invokes, not reflection or dispatch. */
|
||||
reference_scope: z.literal("direct_dex_invokes").exactOptional(),
|
||||
class_name: z.string(),
|
||||
method_name: z.string().nullable(),
|
||||
overload_index: z.int().nonnegative().nullable(),
|
||||
reported_signature: z.string().nullable(),
|
||||
dex_descriptor: dexDescriptorSchema.nullable(),
|
||||
total: z.int().nonnegative(),
|
||||
/** Position coverage is separate from the complete using-node inventory.
|
||||
* Historical Evidence may omit this aggregate; omission is unexamined. */
|
||||
direct_invokes_coverage: z
|
||||
.strictObject({
|
||||
complete_using_nodes: z.int().nonnegative(),
|
||||
partial_using_nodes: z.int().nonnegative(),
|
||||
unknown_using_nodes: z.int().nonnegative(),
|
||||
recorded_sites: z.int().nonnegative(),
|
||||
})
|
||||
.exactOptional(),
|
||||
references: z.array(
|
||||
z.strictObject({
|
||||
kind: z.string(),
|
||||
@@ -291,6 +314,8 @@ export const androidResultSchemas = {
|
||||
top_class: z.string(),
|
||||
source_line: z.null(),
|
||||
dex_offset: z.null(),
|
||||
/** An explicit follow-up selector accepted by inspect_android_method. */
|
||||
source_selector: androidMethodSelectorSchema.exactOptional(),
|
||||
/** false means transport-bounded instruction evidence; null is unknown. */
|
||||
direct_invokes_exhaustive: z.boolean().nullable(),
|
||||
/** Raw direct invoke instructions inside this using node, when available. */
|
||||
|
||||
Reference in new issue
Block a user