150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { evidenceSchema, providerSchema } from "../evidence.js";
|
|
import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js";
|
|
import { digestSchema } from "../digests.js";
|
|
import { prefixedDigestSchema } from "../digests.js";
|
|
|
|
const evidenceIdSchema = prefixedDigestSchema("ev");
|
|
const nodeIdSchema = prefixedDigestSchema("jag_node");
|
|
const edgeIdSchema = prefixedDigestSchema("jag_edge");
|
|
const boundedTextSchema = z.string().min(1);
|
|
|
|
/** 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");
|
|
|
|
const literalSeedKinds = [
|
|
"route",
|
|
"string",
|
|
"api",
|
|
"channel",
|
|
"module",
|
|
"native-export",
|
|
] as const;
|
|
|
|
/**
|
|
* Literal starting point for one application feature trace. An empty value,
|
|
* such as the channel in `ipcMain.handle("")`, must be an explicit exact
|
|
* literal seed: empty containment would select every node.
|
|
*/
|
|
const applicationFeatureSeedSchema = z.union([
|
|
z.strictObject({
|
|
kind: z.enum(["node-id", ...literalSeedKinds]),
|
|
value: boundedTextSchema,
|
|
match: z.enum(["exact", "contains"]).nullable().default(null),
|
|
case_sensitive: z.boolean().default(false),
|
|
}),
|
|
z.strictObject({
|
|
kind: z.enum(literalSeedKinds),
|
|
value: z.literal(""),
|
|
match: z.literal("exact"),
|
|
case_sensitive: z.boolean().default(false),
|
|
}),
|
|
]);
|
|
|
|
/** Evidence-backed application graph and feature seed. */
|
|
export const traceApplicationFeatureInputSchema = z
|
|
.strictObject({
|
|
application: evidenceSchema,
|
|
native_observations: z.array(evidenceSchema).default([]),
|
|
seed: applicationFeatureSeedSchema,
|
|
direction: z.enum(["outgoing", "incoming", "both"]).default("both"),
|
|
})
|
|
.superRefine((input, context) => {
|
|
const ids = input.native_observations.map(({ evidence_id: id }) => id);
|
|
if (new Set(ids).size !== ids.length)
|
|
context.addIssue({
|
|
code: "custom",
|
|
path: ["native_observations"],
|
|
message: "Native observations must be unique",
|
|
});
|
|
});
|
|
|
|
const seedMatchSchema = z.strictObject({
|
|
node_id: nodeIdSchema,
|
|
kind: z.string().min(1),
|
|
basis: z.enum(["node-id", "label", "identity", "property"]),
|
|
field: z.string().min(1),
|
|
});
|
|
|
|
const tracePathSchema = z.strictObject({
|
|
path_id: prefixedDigestSchema("jatp"),
|
|
start_node_id: nodeIdSchema,
|
|
end_node_id: nodeIdSchema,
|
|
end_kind: z.string().min(1),
|
|
node_ids: z.array(nodeIdSchema).min(1),
|
|
edge_ids: z.array(edgeIdSchema),
|
|
authorities: z.array(z.string().min(1)),
|
|
contains_inference: z.boolean(),
|
|
});
|
|
|
|
const nativeHandoffSchema = z.strictObject({
|
|
native_node_id: nodeIdSchema,
|
|
artifact_sha256: digestSchema,
|
|
artifact_path: z.string().min(1).nullable(),
|
|
export_node_ids: z.array(nodeIdSchema),
|
|
requested_exports: z.array(boundedTextSchema),
|
|
status: z.enum(["evidence-linked", "requires-provider-analysis"]),
|
|
providers: z.array(providerSchema),
|
|
evidence_ids: z.array(evidenceIdSchema),
|
|
recommended_tools: z.array(
|
|
z.enum([
|
|
"open_binary",
|
|
"binary_overview",
|
|
"search_procedures",
|
|
"analyze_function",
|
|
"xrefs",
|
|
]),
|
|
),
|
|
});
|
|
|
|
/** Evidence-preserving complete reachable subgraph and native-analysis handoffs. */
|
|
export const applicationFeatureTraceResultSchema = z.strictObject({
|
|
trace_id: prefixedDigestSchema("jatr"),
|
|
source_evidence_id: evidenceIdSchema,
|
|
source_graph_id: prefixedDigestSchema("jag"),
|
|
seed: applicationFeatureSeedSchema,
|
|
direction: z.enum(["outgoing", "incoming", "both"]),
|
|
seed_matches: z.array(seedMatchSchema),
|
|
graph: javascriptApplicationGraphSchema.nullable(),
|
|
paths: z.array(tracePathSchema),
|
|
native_handoffs: z.array(nativeHandoffSchema),
|
|
summary: z.strictObject({
|
|
matched_seeds: z.number().int().min(0),
|
|
traced_nodes: z.number().int().min(0),
|
|
traced_edges: z.number().int().min(0),
|
|
terminal_paths: z.number().int().min(0),
|
|
native_handoffs: z.number().int().min(0),
|
|
observed_facts: z.number().int().min(0),
|
|
inferred_facts: z.number().int().min(0),
|
|
unknown_facts: z.number().int().min(0),
|
|
unavailable_facts: z.number().int().min(0),
|
|
}),
|
|
coverage: z.strictObject({
|
|
status: z.enum(["complete-within-source", "partial", "no-match"]),
|
|
source_graph_status: z.enum([
|
|
"complete",
|
|
"partial",
|
|
"unknown",
|
|
"unavailable",
|
|
]),
|
|
total_seed_matches: z.number().int().min(0),
|
|
}),
|
|
evidence_links: z.array(evidenceIdSchema).min(1),
|
|
limitations: z.array(boundedTextSchema),
|
|
});
|
|
|
|
export type TraceApplicationFeatureInput = z.infer<
|
|
typeof traceApplicationFeatureInputSchema
|
|
>;
|
|
export type ApplicationFeatureSeed = z.infer<
|
|
typeof applicationFeatureSeedSchema
|
|
>;
|
|
export type ApplicationFeatureTraceResult = z.infer<
|
|
typeof applicationFeatureTraceResultSchema
|
|
>;
|