mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 16:26:36 +00:00
fix(evidence): preserve subjects in derived workflows (#9492)
* fix(javascript): preserve verified trace evidence subjects Co-authored-by: Felix <3937061056@qq.com> * fix(evidence): preserve selected subject names in views * fix(evidence): preserve subjects across related workflows * fix(evidence): retain inventory projection subjects * fix(managed): validate projected graph root identity * style: format evidence and native command code * fix(javascript): validate static graph root identity * test(javascript): update feature trace graph expectations --------- Co-authored-by: Felix <3937061056@qq.com>
This commit is contained in:
1 parent
4d7c6efe9a
commit
52fe342c73
28 files changed
+615
-151
No files matched your search
@@ -7,9 +7,9 @@ import type { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
createEvidence,
|
||||
evidenceSubjectTarget,
|
||||
type Evidence,
|
||||
type EvidenceProvider,
|
||||
type EvidenceSubjectTarget,
|
||||
} from "../domain/evidence.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { ParsedInventorySet } from "../domain/artifactInventoryEvidence.js";
|
||||
@@ -42,7 +42,6 @@ interface InventoryProjectionOptions<
|
||||
readonly operation: string;
|
||||
readonly predicateType: string;
|
||||
readonly provider: EvidenceProvider;
|
||||
readonly subjectFormat: (first: Evidence) => EvidenceSubjectTarget["format"];
|
||||
}
|
||||
|
||||
/** Resolve source inventory once, then wrap its family projection in Evidence. */
|
||||
@@ -70,13 +69,7 @@ export const projectInventoryEvidence = <
|
||||
const first = source.value.evidence[0];
|
||||
return ok({
|
||||
evidence: createEvidence(
|
||||
first?.subject === null || first?.subject === undefined
|
||||
? undefined
|
||||
: {
|
||||
path: first.subject.local_path,
|
||||
sha256: result.root_sha256,
|
||||
format: options.subjectFormat(first),
|
||||
},
|
||||
evidenceSubjectTarget(first?.subject ?? null),
|
||||
options.provider,
|
||||
{
|
||||
predicateType: options.predicateType,
|
||||
|
||||
@@ -79,10 +79,32 @@ const applicationWithUnknowns = (
|
||||
],
|
||||
}),
|
||||
);
|
||||
const root = createJavaScriptApplicationNode({
|
||||
kind: "artifact",
|
||||
identity: {
|
||||
strategy: "content-digest",
|
||||
stability: "global-exact",
|
||||
sha256: APPLICATION_GRAPH_DIGESTS.asar,
|
||||
},
|
||||
observations: [
|
||||
{
|
||||
label: "managed root",
|
||||
properties: {},
|
||||
evidence: artifactEvidence(
|
||||
APPLICATION_GRAPH_DIGESTS.asar,
|
||||
"artifact-root",
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
const graph = createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: base.root_node_ids,
|
||||
nodes: [...base.nodes, ...unknownNodes],
|
||||
root_node_ids: [root.node_id],
|
||||
nodes: [
|
||||
...base.nodes.filter(({ node_id }) => node_id !== root.node_id),
|
||||
...unknownNodes,
|
||||
root,
|
||||
],
|
||||
edges: base.edges,
|
||||
coverage: completeCoverage,
|
||||
limitations: base.limitations,
|
||||
|
||||
@@ -21,6 +21,7 @@ import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import { safeParseEvidence } from "../../domain/evidenceValidation.js";
|
||||
import {
|
||||
createEvidence,
|
||||
evidenceSubjectTarget,
|
||||
rebindEvidenceToTargetPath,
|
||||
type Evidence,
|
||||
type EvidenceSubjectTarget,
|
||||
@@ -327,21 +328,11 @@ export const projectRetainedAnalysis = (
|
||||
if (!retained.ok) return retained;
|
||||
// Duplicate semantic records retain their original ledger metadata. Detail
|
||||
// selection changes presentation, not the historical lookup or payload owner.
|
||||
const subject = evidence.subject;
|
||||
return projectResolvedAnalysisView(
|
||||
{ source, view },
|
||||
prepareAnalysisViewProjection(view),
|
||||
retained.value,
|
||||
subject === null
|
||||
? undefined
|
||||
: {
|
||||
path: subject.local_path,
|
||||
sha256: subject.digest.sha256,
|
||||
format: subject.format,
|
||||
...(subject.architecture === null
|
||||
? {}
|
||||
: { architecture: subject.architecture }),
|
||||
},
|
||||
evidenceSubjectTarget(evidence.subject),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -383,17 +374,7 @@ const createAnalysisViewEvidence = (
|
||||
parent: Evidence,
|
||||
result: AnalysisViewResult,
|
||||
): Evidence => {
|
||||
const target =
|
||||
parent.subject === null
|
||||
? undefined
|
||||
: {
|
||||
path: parent.subject.local_path,
|
||||
sha256: parent.subject.digest.sha256,
|
||||
format: parent.subject.format,
|
||||
...(parent.subject.architecture === null
|
||||
? {}
|
||||
: { architecture: parent.subject.architecture }),
|
||||
};
|
||||
const target = evidenceSubjectTarget(parent.subject);
|
||||
const nativeFunctionView =
|
||||
result.kind === "native" ||
|
||||
(result.kind === "summary" &&
|
||||
|
||||
@@ -2,6 +2,33 @@ import { expect, it, vi } from "vitest";
|
||||
import { AndroidAnalysisService } from "./AndroidAnalysisService.js";
|
||||
import type { AndroidAnalysisPort } from "./AndroidAnalysisPort.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { createEvidence } from "../../domain/evidence.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import { summarizeRetainedAndroidSearch } from "./AndroidSearchEvidenceSummary.js";
|
||||
|
||||
it("retains the selected subject in bounded search Evidence", () => {
|
||||
const source = createEvidence(
|
||||
{
|
||||
path: "/fixture/app.apk",
|
||||
sha256: "a".repeat(64),
|
||||
format: "apk",
|
||||
name: "Selected app",
|
||||
},
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{
|
||||
operation: "search_android_classes",
|
||||
parameters: {},
|
||||
result: { matches: [] },
|
||||
rawResult: { complete: true },
|
||||
},
|
||||
);
|
||||
const result = summarizeRetainedAndroidSearch(source, { pages: 1 }, () =>
|
||||
ok("added"),
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.subject).toEqual(source.subject);
|
||||
expect(result.value.evidence_links).toContain(source.evidence_id);
|
||||
});
|
||||
|
||||
it("retains selector correction issues before target acquisition", async () => {
|
||||
const execute = vi.fn<AndroidAnalysisPort["execute"]>();
|
||||
|
||||
@@ -25,6 +25,5 @@ export const projectAndroidApplicationEvidence = (
|
||||
operation: OPERATION,
|
||||
predicateType: "rea.android-application-graph",
|
||||
provider: ANDROID_APPLICATION_PROVIDER,
|
||||
subjectFormat: () => "apk",
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { EvidenceWriter } from "../investigation/InvestigationRecordPort.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
createEvidence,
|
||||
evidenceSubjectTarget,
|
||||
type Evidence,
|
||||
} from "../../domain/evidence.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import { ok, type Result } from "../../domain/result.js";
|
||||
@@ -18,30 +22,16 @@ export const summarizeRetainedAndroidSearch = (
|
||||
if (recordEvidence === undefined) return ok(evidence);
|
||||
const recorded = recordEvidence(evidence);
|
||||
if (!recorded.ok) return recorded;
|
||||
const subject = evidence.subject;
|
||||
return ok(
|
||||
createEvidence(
|
||||
subject === null
|
||||
? undefined
|
||||
: {
|
||||
path: subject.local_path,
|
||||
sha256: subject.digest.sha256,
|
||||
format: subject.format,
|
||||
...(subject.architecture === null
|
||||
? {}
|
||||
: { architecture: subject.architecture }),
|
||||
},
|
||||
evidence.provider,
|
||||
{
|
||||
operation: evidence.operation,
|
||||
parameters: evidence.parameters,
|
||||
result: evidence.normalized_result,
|
||||
rawResult: { ...summary, complete_evidence_id: evidence.evidence_id },
|
||||
confidence: evidence.confidence,
|
||||
limitations: evidence.limitations,
|
||||
locations: evidence.locations,
|
||||
evidenceLinks: [evidence.evidence_id],
|
||||
},
|
||||
),
|
||||
createEvidence(evidenceSubjectTarget(evidence.subject), evidence.provider, {
|
||||
operation: evidence.operation,
|
||||
parameters: evidence.parameters,
|
||||
result: evidence.normalized_result,
|
||||
rawResult: { ...summary, complete_evidence_id: evidence.evidence_id },
|
||||
confidence: evidence.confidence,
|
||||
limitations: evidence.limitations,
|
||||
locations: evidence.locations,
|
||||
evidenceLinks: [evidence.evidence_id],
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -25,6 +25,5 @@ export const projectAppleApplicationEvidence = (
|
||||
operation: OPERATION,
|
||||
predicateType: "rea.apple-application-graph",
|
||||
provider: APPLE_APPLICATION_PROVIDER,
|
||||
subjectFormat: (first) => first.subject?.format ?? "unknown",
|
||||
});
|
||||
};
|
||||
@@ -25,6 +25,5 @@ export const projectHarmonyApplicationEvidence = (
|
||||
operation: OPERATION,
|
||||
predicateType: "rea.harmony-application-graph",
|
||||
provider: HARMONY_APPLICATION_PROVIDER,
|
||||
subjectFormat: (first) => first.subject?.format ?? "unknown",
|
||||
});
|
||||
};
|
||||
@@ -3,7 +3,12 @@ import {
|
||||
JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER,
|
||||
MANAGED_WORKFLOW_PROVIDER,
|
||||
} from "../../domain/investigationProviders.js";
|
||||
import { isImmutableEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
isImmutableEvidence,
|
||||
evidenceSubjectTarget,
|
||||
type Evidence,
|
||||
type EvidenceSubjectTarget,
|
||||
} from "../../domain/evidence.js";
|
||||
import { readEvidence } from "../../domain/evidenceValidation.js";
|
||||
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import {
|
||||
@@ -35,6 +40,32 @@ export interface ApplicationGraphEvidenceSource {
|
||||
readonly semanticGraph: JavaScriptSemanticGraph | null;
|
||||
}
|
||||
|
||||
/** Carry a subject only when it identifies the graph's root artifact. */
|
||||
export const applicationGraphSubjectTarget = (
|
||||
source: ApplicationGraphEvidenceSource,
|
||||
): EvidenceSubjectTarget | undefined => {
|
||||
const subject = source.evidence.subject;
|
||||
const staticRootObserved =
|
||||
source.kind === "static-application" &&
|
||||
source.graph.nodes.some(
|
||||
(node) =>
|
||||
node.kind === "artifact" &&
|
||||
node.identity.strategy === "content-digest" &&
|
||||
node.identity.sha256 === source.rootArtifactSha256 &&
|
||||
node.observations.some(
|
||||
({ evidence }) => evidence.extractor.operation === "inventory-root",
|
||||
),
|
||||
);
|
||||
return evidenceSubjectTarget(
|
||||
staticRootObserved ||
|
||||
(source.kind === "managed-application" &&
|
||||
subject?.format === "pe" &&
|
||||
subject.digest.sha256 === source.rootArtifactSha256)
|
||||
? subject
|
||||
: null,
|
||||
);
|
||||
};
|
||||
|
||||
/** Caller-owned Evidence failed validation at the application graph boundary. */
|
||||
export interface ApplicationGraphEvidenceInputFailure {
|
||||
readonly issues: readonly AnalysisInputIssue[];
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
createEvidence,
|
||||
type Evidence,
|
||||
type EvidenceObservation,
|
||||
type EvidenceSubjectTarget,
|
||||
} from "../../domain/evidence.js";
|
||||
import type { ApplicationVersionComparisonResult } from "../../domain/javascript/javascriptApplicationVersionComparisonSchemas.js";
|
||||
import type { JavaScriptExportShapeComparisonResult } from "../../domain/javascript/javascriptExportShapeComparisonSchemas.js";
|
||||
@@ -12,10 +13,11 @@ import { JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER } from "../../domain/investiga
|
||||
|
||||
/** Create derived Evidence for one bounded application-graph feature trace. */
|
||||
export const createApplicationFeatureTraceEvidence = (
|
||||
subject: EvidenceSubjectTarget | undefined,
|
||||
parameters: EvidenceObservation["parameters"],
|
||||
result: ApplicationFeatureTraceResult,
|
||||
): Evidence =>
|
||||
createEvidence(undefined, JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, {
|
||||
createEvidence(subject, JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, {
|
||||
predicateType: "rea.application-feature-trace",
|
||||
operation: "trace_application_feature",
|
||||
parameters,
|
||||
@@ -30,10 +32,11 @@ export const createApplicationFeatureTraceEvidence = (
|
||||
|
||||
/** Create derived Evidence for one bounded JavaScript semantic trace. */
|
||||
export const createJavaScriptSemanticTraceEvidence = (
|
||||
subject: EvidenceSubjectTarget | undefined,
|
||||
parameters: EvidenceObservation["parameters"],
|
||||
result: JavaScriptSemanticTraceResult,
|
||||
): Evidence =>
|
||||
createEvidence(undefined, JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, {
|
||||
createEvidence(subject, JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, {
|
||||
predicateType: "rea.javascript-semantic-trace",
|
||||
operation: "trace_javascript_semantics",
|
||||
parameters,
|
||||
|
||||
@@ -36,6 +36,7 @@ import { compareSourceToBundle } from "../../domain/javascript/sourceToBundleCom
|
||||
import type { SourceToBundleComparisonResult } from "../../domain/javascript/sourceToBundleComparisonSchemas.js";
|
||||
import {
|
||||
applicationGraphEvidenceInputError,
|
||||
applicationGraphSubjectTarget,
|
||||
parseApplicationGraphEvidence,
|
||||
parseNativeApplicationEvidence,
|
||||
type ApplicationGraphEvidenceSource,
|
||||
@@ -132,6 +133,7 @@ export const traceApplicationFeatureEvidence = (
|
||||
direction: input.direction,
|
||||
});
|
||||
const evidence = createApplicationFeatureTraceEvidence(
|
||||
applicationGraphSubjectTarget(source),
|
||||
{
|
||||
application_evidence_id: source.evidence.evidence_id,
|
||||
native_evidence_ids: nativeEvidence.map(({ evidence_id: id }) => id),
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { expect, it, vi } from "vitest";
|
||||
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
createEvidence,
|
||||
parseEvidence,
|
||||
rebindEvidenceToTargetPath,
|
||||
type Evidence,
|
||||
} from "../../domain/evidence.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import { createJavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import { createJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import {
|
||||
JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE,
|
||||
JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
|
||||
@@ -14,18 +23,165 @@ import {
|
||||
} from "./JavaScriptApplicationWorkflowService.js";
|
||||
import { reconcileJavaScriptRuntimeEvidence } from "./JavaScriptRuntimeReconciliationService.js";
|
||||
import { traceJavaScriptSemanticsEvidence } from "./JavaScriptSemanticTraceService.js";
|
||||
import {
|
||||
inspectAnalysisView,
|
||||
projectRetainedAnalysis,
|
||||
} from "../analysisView/AnalysisViewService.js";
|
||||
|
||||
const reference = ({ evidence_id }: Evidence) => ({
|
||||
kind: "retained-evidence" as const,
|
||||
evidence_id,
|
||||
});
|
||||
|
||||
it("keeps a static trace subject unknown when legacy graph root evidence is omitted", () => {
|
||||
const original = JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application;
|
||||
const result = javascriptApplicationAnalysisResultSchema.parse(
|
||||
original.normalized_result,
|
||||
);
|
||||
const { graph_id: _graphId, ...graphInput } = result.graph;
|
||||
const graph = createJavaScriptApplicationGraph({
|
||||
...graphInput,
|
||||
nodes: graphInput.nodes.filter(({ kind }) => kind !== "artifact"),
|
||||
root_node_ids: graphInput.nodes
|
||||
.filter(({ kind }) => kind === "javascript-asset")
|
||||
.map(({ node_id }) => node_id),
|
||||
edges: [],
|
||||
});
|
||||
const { graph_id: _semanticId, ...semanticInput } = result.semantic_graph;
|
||||
const semanticGraph = createJavaScriptSemanticGraph({
|
||||
...semanticInput,
|
||||
application_graph_id: graph.graph_id,
|
||||
});
|
||||
if (original.subject === null)
|
||||
throw new Error("Expected application subject");
|
||||
const legacy = createEvidence(
|
||||
{
|
||||
path: original.subject.local_path,
|
||||
sha256: original.subject.digest.sha256,
|
||||
format: original.subject.format,
|
||||
},
|
||||
original.provider,
|
||||
{
|
||||
operation: original.operation,
|
||||
predicateType: original.predicate_type,
|
||||
parameters: original.parameters,
|
||||
result: { ...result, graph, semantic_graph: semanticGraph },
|
||||
confidence: "derived",
|
||||
authority: "shipped-artifact",
|
||||
},
|
||||
);
|
||||
const trace = traceApplicationFeatureEvidence({
|
||||
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
|
||||
application: legacy,
|
||||
});
|
||||
if (!trace.ok) throw trace.error;
|
||||
expect(trace.value.evidence.subject).toBeNull();
|
||||
});
|
||||
|
||||
const native = createEvidence(
|
||||
{ path: "/example/addon.node", sha256: "e".repeat(64), format: "elf" },
|
||||
{ id: "fixture", name: "Native fixture", version: "1" },
|
||||
{ operation: "procedure_info", parameters: {}, result: {} },
|
||||
);
|
||||
|
||||
it("preserves the verified application subject in both trace workflows", async () => {
|
||||
const original = JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application;
|
||||
if (original.subject === null)
|
||||
throw new Error("Expected application subject");
|
||||
const application = createEvidence(
|
||||
{
|
||||
path: original.subject.local_path,
|
||||
sha256: original.subject.digest.sha256,
|
||||
format: original.subject.format,
|
||||
architecture: "x86_64",
|
||||
name: "caller-selected-name",
|
||||
},
|
||||
original.provider,
|
||||
{
|
||||
operation: original.operation,
|
||||
predicateType: original.predicate_type,
|
||||
parameters: original.parameters,
|
||||
result: original.normalized_result,
|
||||
confidence: "derived",
|
||||
authority: "shipped-artifact",
|
||||
},
|
||||
);
|
||||
const feature = traceApplicationFeatureEvidence({
|
||||
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
|
||||
application,
|
||||
});
|
||||
const semantic = traceJavaScriptSemanticsEvidence({
|
||||
application,
|
||||
query: {
|
||||
seed: { kind: "property", name: "unobserved" },
|
||||
direction: "ownership",
|
||||
},
|
||||
});
|
||||
for (const result of [feature, semantic]) {
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.evidence.subject).toEqual(application.subject);
|
||||
expect(result.value.evidence.limitations).not.toContain(
|
||||
"Artifact identity is unavailable for this observation.",
|
||||
);
|
||||
}
|
||||
if (!feature.ok) throw feature.error;
|
||||
const view = await inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: feature.value.evidence },
|
||||
view: { kind: "summary" },
|
||||
});
|
||||
if (!view.ok) throw view.error;
|
||||
expect(view.value.subject).toEqual(application.subject);
|
||||
const selected = parseEvidence({
|
||||
...feature.value.evidence,
|
||||
subject: { ...feature.value.evidence.subject, name: "selected display" },
|
||||
});
|
||||
const retained = projectRetainedAnalysis(
|
||||
selected,
|
||||
{
|
||||
recordEvidence: () => ok("added"),
|
||||
evidenceById: () => feature.value.evidence,
|
||||
},
|
||||
{ kind: "summary" },
|
||||
);
|
||||
if (!retained.ok) throw retained.error;
|
||||
expect(retained.value.subject?.name).toBe("selected display");
|
||||
expect(
|
||||
rebindEvidenceToTargetPath(feature.value.evidence, {
|
||||
path: application.subject!.local_path,
|
||||
sha256: "f".repeat(64),
|
||||
format: application.subject!.format,
|
||||
architecture: "x86_64",
|
||||
name: "wrong artifact",
|
||||
}),
|
||||
).toBe(feature.value.evidence);
|
||||
});
|
||||
|
||||
it("does not attribute a multi-source reconciliation graph to its envelope subject", () => {
|
||||
const original = JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.right;
|
||||
const reconciliation = createEvidence(
|
||||
{ path: "/other/artifact", sha256: "f".repeat(64), format: "file" },
|
||||
original.provider,
|
||||
{
|
||||
operation: original.operation,
|
||||
predicateType: original.predicate_type,
|
||||
parameters: original.parameters,
|
||||
result: original.normalized_result,
|
||||
confidence: "inferred",
|
||||
authority: "analyst-inference",
|
||||
evidenceLinks: original.evidence_links,
|
||||
},
|
||||
);
|
||||
const result = traceApplicationFeatureEvidence({
|
||||
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
|
||||
application: reconciliation,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.evidence.subject).toBeNull();
|
||||
expect(result.value.evidence.limitations).toContain(
|
||||
"Artifact identity is unavailable for this observation.",
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves feature native arrays without changing source identity or unknowns", () => {
|
||||
const input = {
|
||||
...JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
applicationGraphEvidenceInputError,
|
||||
applicationGraphSubjectTarget,
|
||||
parseApplicationGraphEvidence,
|
||||
} from "./JavaScriptApplicationEvidenceGraph.js";
|
||||
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
|
||||
@@ -63,6 +64,7 @@ export const traceJavaScriptSemanticsEvidence = (
|
||||
};
|
||||
const { allowed_relations, ...queryParameters } = input.query;
|
||||
const evidence = createJavaScriptSemanticTraceEvidence(
|
||||
applicationGraphSubjectTarget(source),
|
||||
{
|
||||
application_evidence_id: source.evidence.evidence_id,
|
||||
query:
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
createEvidence,
|
||||
evidenceSubjectTarget,
|
||||
type Evidence,
|
||||
type EvidenceSubjectTarget,
|
||||
} from "../../domain/evidence.js";
|
||||
@@ -90,12 +91,5 @@ const subjectTarget = (
|
||||
input.boundaries?.evidence.subject;
|
||||
if (subject === undefined || subject === null || subject.format !== "pe")
|
||||
return undefined;
|
||||
return {
|
||||
path: subject.local_path,
|
||||
sha256: subject.digest.sha256,
|
||||
format: "pe",
|
||||
...(subject.architecture === null
|
||||
? {}
|
||||
: { architecture: subject.architecture }),
|
||||
};
|
||||
return evidenceSubjectTarget(subject);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { createEvidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
createJavaScriptApplicationGraph,
|
||||
createJavaScriptApplicationNode,
|
||||
createJavaScriptApplicationEdge,
|
||||
} from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import { createWebTextArtifact } from "../../domain/webContentArtifact.js";
|
||||
import {
|
||||
@@ -63,11 +64,46 @@ const asset = createJavaScriptApplicationNode({
|
||||
},
|
||||
],
|
||||
});
|
||||
const root = createJavaScriptApplicationNode({
|
||||
kind: "artifact",
|
||||
identity: {
|
||||
strategy: "content-digest",
|
||||
stability: "global-exact",
|
||||
sha256: applicationSha256,
|
||||
},
|
||||
observations: [
|
||||
{
|
||||
label: "artifact root",
|
||||
properties: { format: "directory" },
|
||||
evidence: {
|
||||
...graphEvidence,
|
||||
artifact: {
|
||||
available: true,
|
||||
artifact_id: `art_${applicationSha256}`,
|
||||
sha256: applicationSha256,
|
||||
},
|
||||
location: {
|
||||
available: true,
|
||||
value: { kind: "artifact-path", path: "artifact-root" },
|
||||
},
|
||||
extractor: { ...graphEvidence.extractor, operation: "inventory-root" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const graph = createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: [asset.node_id],
|
||||
nodes: [asset],
|
||||
edges: [],
|
||||
root_node_ids: [root.node_id],
|
||||
nodes: [asset, root],
|
||||
edges: [
|
||||
createJavaScriptApplicationEdge({
|
||||
source_node_id: root.node_id,
|
||||
target_node_id: asset.node_id,
|
||||
relation: "contains",
|
||||
properties: { path: "renderer.js" },
|
||||
evidence: graphEvidence,
|
||||
}),
|
||||
],
|
||||
coverage: completeCoverage,
|
||||
limitations: [],
|
||||
});
|
||||
|
||||
@@ -883,7 +883,9 @@ it("projects typed module facts without source bodies and keeps source coverage
|
||||
|
||||
it("counts retained range observations including repeats and skips unavailable locations", () => {
|
||||
const analysis = analysisViewJavaScriptAnalysis();
|
||||
const original = analysis.graph.nodes[0];
|
||||
const original = analysis.graph.nodes.find(
|
||||
({ kind }) => kind === "javascript-asset",
|
||||
);
|
||||
const observed = original?.observations[0];
|
||||
if (!original || !observed) throw new Error("missing module observation");
|
||||
const range = {
|
||||
@@ -929,6 +931,7 @@ it("counts retained range observations including repeats and skips unavailable l
|
||||
...application,
|
||||
nodes: [node],
|
||||
root_node_ids: [node.node_id],
|
||||
edges: [],
|
||||
});
|
||||
const parent = javascriptParent(
|
||||
analysisViewBindJavaScriptGraphs(analysis, graph),
|
||||
@@ -997,7 +1000,9 @@ it("validates typed module selectors without coercing null or empty paths", () =
|
||||
|
||||
it("keeps mixed observation support distinct instead of promoting a module to observed", () => {
|
||||
const analysis = analysisViewJavaScriptAnalysis();
|
||||
const original = analysis.graph.nodes[0];
|
||||
const original = analysis.graph.nodes.find(
|
||||
({ kind }) => kind === "javascript-asset",
|
||||
);
|
||||
const observed = original?.observations[0];
|
||||
if (!original || !observed) throw new Error("missing module observation");
|
||||
const inferred = {
|
||||
@@ -1025,6 +1030,7 @@ it("keeps mixed observation support distinct instead of promoting a module to ob
|
||||
...application,
|
||||
nodes: [node],
|
||||
root_node_ids: [node.node_id],
|
||||
edges: [],
|
||||
});
|
||||
const result = projectAnalysisView(
|
||||
javascriptParent(analysisViewBindJavaScriptGraphs(analysis, graph)),
|
||||
|
||||
@@ -1144,7 +1144,7 @@ export const sourceToBundlePageViewResultSchema = z.strictObject({
|
||||
/** Compact retained feature-trace summary; pages carry the same trace context. */
|
||||
export const featureTraceSummaryViewResultSchema = z.strictObject({
|
||||
...summaryViewResultBase,
|
||||
artifact: z.null(),
|
||||
artifact: artifactSchema.nullable(),
|
||||
parent_operation: z.literal("trace_application_feature"),
|
||||
summary: featureTraceSummarySchema,
|
||||
});
|
||||
@@ -1157,7 +1157,7 @@ const featureTracePage = <
|
||||
...viewResultBase,
|
||||
...featureTraceViewContextSchema.shape,
|
||||
kind: z.literal("feature-trace"),
|
||||
artifact: z.null(),
|
||||
artifact: artifactSchema.nullable(),
|
||||
parent_operation: z.literal("trace_application_feature"),
|
||||
view: featureTraceViewCollections[collection].request,
|
||||
items: featureTraceViewCollections[collection].rows,
|
||||
|
||||
@@ -317,7 +317,7 @@ export const projectFeatureTraceView = (
|
||||
parent_evidence_id: parent.evidenceId,
|
||||
parent_operation: OPERATION,
|
||||
parent_digest: parent.evidenceId.slice(3),
|
||||
artifact: null,
|
||||
artifact: parent.artifact ?? null,
|
||||
limitations: boundedViewMessages(
|
||||
limitations,
|
||||
parent.evidenceId,
|
||||
|
||||
+40
-17
@@ -184,16 +184,40 @@ export interface EvidenceSubjectTarget {
|
||||
readonly sha256: string;
|
||||
readonly format: z.infer<typeof subjectSchema>["format"];
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
/** Preserve an existing Evidence subject's display name when deriving observations. */
|
||||
readonly name?: string;
|
||||
}
|
||||
|
||||
/** Preserve the complete subject when deriving a new Evidence observation. */
|
||||
export const evidenceSubjectTarget = (
|
||||
subject: Evidence["subject"],
|
||||
): EvidenceSubjectTarget | undefined =>
|
||||
subject === null
|
||||
? undefined
|
||||
: {
|
||||
path: subject.local_path,
|
||||
sha256: subject.digest.sha256,
|
||||
format: subject.format,
|
||||
name: subject.name,
|
||||
...(subject.architecture === null
|
||||
? {}
|
||||
: { architecture: subject.architecture }),
|
||||
};
|
||||
|
||||
const sameEvidenceSubjectTargetIdentity = (
|
||||
subject: EvidenceSubjectTarget,
|
||||
target: EvidenceSubjectTarget,
|
||||
): boolean =>
|
||||
subject.sha256 === target.sha256 &&
|
||||
subject.format === target.format &&
|
||||
(subject.architecture ?? null) === (target.architecture ?? null);
|
||||
|
||||
/** Rebind a matching immutable artifact subject to the caller's selected path. */
|
||||
export const rebindEvidenceSubjectTargetPath = (
|
||||
subject: EvidenceSubjectTarget,
|
||||
target: EvidenceSubjectTarget,
|
||||
): EvidenceSubjectTarget =>
|
||||
subject.sha256 === target.sha256 &&
|
||||
subject.format === target.format &&
|
||||
(subject.architecture ?? null) === (target.architecture ?? null)
|
||||
sameEvidenceSubjectTargetIdentity(subject, target)
|
||||
? { ...subject, path: target.path }
|
||||
: subject;
|
||||
|
||||
@@ -324,21 +348,18 @@ export const rebindEvidenceToTargetPath = (
|
||||
): Evidence => {
|
||||
const subject = evidence.subject;
|
||||
if (subject === null) return evidence;
|
||||
const rebound = rebindEvidenceSubjectTargetPath(
|
||||
{
|
||||
path: subject.local_path,
|
||||
sha256: subject.digest.sha256,
|
||||
format: subject.format,
|
||||
...(subject.architecture === null
|
||||
? {}
|
||||
: { architecture: subject.architecture }),
|
||||
},
|
||||
target,
|
||||
);
|
||||
if (rebound.path === subject.local_path) return evidence;
|
||||
if (
|
||||
!sameEvidenceSubjectTargetIdentity(evidenceSubjectTarget(subject)!, target)
|
||||
)
|
||||
return evidence;
|
||||
if (
|
||||
target.path === subject.local_path &&
|
||||
(target.name === undefined || target.name === subject.name)
|
||||
)
|
||||
return evidence;
|
||||
const reboundSubject = subjectSchema.parse({
|
||||
...subject,
|
||||
name: basename(target.path) || "artifact",
|
||||
name: target.name ?? (basename(target.path) || "artifact"),
|
||||
local_path: target.path,
|
||||
});
|
||||
const reboundEvidence = { ...evidence, subject: reboundSubject };
|
||||
@@ -466,7 +487,9 @@ export const evidenceEnvelopeForAdmission = (
|
||||
target === undefined
|
||||
? null
|
||||
: {
|
||||
name: basename(target.path) || "artifact",
|
||||
name:
|
||||
("name" in target ? target.name : undefined) ??
|
||||
(basename(target.path) || "artifact"),
|
||||
digest: { sha256: target.sha256 },
|
||||
format: target.format,
|
||||
architecture: target.architecture ?? null,
|
||||
|
||||
@@ -132,6 +132,27 @@ function* checkApplicationBindingsSteps(
|
||||
try {
|
||||
for (const node of result.graph.nodes) {
|
||||
applicationNodeIds.add(node.node_id);
|
||||
if (
|
||||
node.kind === "artifact" &&
|
||||
(node.identity.strategy !== "content-digest" ||
|
||||
node.identity.sha256 !== result.root_artifact_sha256)
|
||||
) {
|
||||
for (const observation of node.observations) {
|
||||
if (observation.evidence.extractor.operation === "inventory-root") {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["root_artifact_sha256"],
|
||||
message:
|
||||
"Application artifact root must match the declared root SHA-256",
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (++examined === 4096) {
|
||||
examined = 0;
|
||||
yield;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (++examined === 4096) {
|
||||
examined = 0;
|
||||
yield;
|
||||
|
||||
@@ -94,48 +94,65 @@ const managedGraphRequestSchema = getEvidenceInputContract(
|
||||
).frameSchema;
|
||||
|
||||
/** Managed-code projection result containing a validated application graph. */
|
||||
export const managedApplicationGraphResultSchema = z.strictObject({
|
||||
projection_id: prefixedDigestSchema("magp"),
|
||||
root_artifact_sha256: digestSchema,
|
||||
source_evidence: z.strictObject({
|
||||
managed_artifact_evidence_id: evidenceIdSchema.nullable(),
|
||||
managed_members_evidence_id: evidenceIdSchema.nullable(),
|
||||
managed_native_boundaries_evidence_id: evidenceIdSchema.nullable(),
|
||||
}),
|
||||
summary: z.strictObject({
|
||||
graph_nodes: z.int().min(0),
|
||||
graph_edges: z.int().min(0),
|
||||
assemblies: z.int().min(0),
|
||||
modules: z.int().min(0),
|
||||
types: z.int().min(0),
|
||||
methods: z.int().min(0),
|
||||
fields: z.int().min(0),
|
||||
pinvoke_imports: z.int().min(0),
|
||||
native_implementations: z.int().min(0),
|
||||
// Graphs projected before call and field-access edges existed omit these
|
||||
// counters; null means those relations were not projected, not zero.
|
||||
calls: z
|
||||
.int()
|
||||
.min(0)
|
||||
.nullable()
|
||||
.default(null)
|
||||
.describe("calls edges; null for graphs projected before calls edges"),
|
||||
field_accesses: z
|
||||
.int()
|
||||
.min(0)
|
||||
.nullable()
|
||||
.default(null)
|
||||
.describe(
|
||||
"accesses edges; null for graphs projected before accesses edges",
|
||||
),
|
||||
}),
|
||||
graph: javascriptApplicationGraphSchema,
|
||||
coverage: z.strictObject({
|
||||
status: z.enum(["complete-within-inputs", "partial"]),
|
||||
}),
|
||||
evidence_links: z.array(evidenceIdSchema).min(1).max(3),
|
||||
limitations: z.array(textSchema),
|
||||
});
|
||||
export const managedApplicationGraphResultSchema = z
|
||||
.strictObject({
|
||||
projection_id: prefixedDigestSchema("magp"),
|
||||
root_artifact_sha256: digestSchema,
|
||||
source_evidence: z.strictObject({
|
||||
managed_artifact_evidence_id: evidenceIdSchema.nullable(),
|
||||
managed_members_evidence_id: evidenceIdSchema.nullable(),
|
||||
managed_native_boundaries_evidence_id: evidenceIdSchema.nullable(),
|
||||
}),
|
||||
summary: z.strictObject({
|
||||
graph_nodes: z.int().min(0),
|
||||
graph_edges: z.int().min(0),
|
||||
assemblies: z.int().min(0),
|
||||
modules: z.int().min(0),
|
||||
types: z.int().min(0),
|
||||
methods: z.int().min(0),
|
||||
fields: z.int().min(0),
|
||||
pinvoke_imports: z.int().min(0),
|
||||
native_implementations: z.int().min(0),
|
||||
// Graphs projected before call and field-access edges existed omit these
|
||||
// counters; null means those relations were not projected, not zero.
|
||||
calls: z
|
||||
.int()
|
||||
.min(0)
|
||||
.nullable()
|
||||
.default(null)
|
||||
.describe("calls edges; null for graphs projected before calls edges"),
|
||||
field_accesses: z
|
||||
.int()
|
||||
.min(0)
|
||||
.nullable()
|
||||
.default(null)
|
||||
.describe(
|
||||
"accesses edges; null for graphs projected before accesses edges",
|
||||
),
|
||||
}),
|
||||
graph: javascriptApplicationGraphSchema,
|
||||
coverage: z.strictObject({
|
||||
status: z.enum(["complete-within-inputs", "partial"]),
|
||||
}),
|
||||
evidence_links: z.array(evidenceIdSchema).min(1).max(3),
|
||||
limitations: z.array(textSchema),
|
||||
})
|
||||
.superRefine((result, context) => {
|
||||
const [rootId] = result.graph.root_node_ids;
|
||||
const root = result.graph.nodes.find(({ node_id }) => node_id === rootId);
|
||||
if (
|
||||
result.graph.root_node_ids.length !== 1 ||
|
||||
root?.kind !== "artifact" ||
|
||||
root.identity.strategy !== "content-digest" ||
|
||||
root.identity.sha256 !== result.root_artifact_sha256
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["root_artifact_sha256"],
|
||||
message:
|
||||
"Managed graph root must identify the declared root artifact SHA-256.",
|
||||
});
|
||||
});
|
||||
|
||||
export type ManagedApplicationGraphResult = z.infer<
|
||||
typeof managedApplicationGraphResultSchema
|
||||
|
||||
@@ -771,15 +771,16 @@ describe("application workflow CLI parity", () => {
|
||||
expect(traced).toMatchObject({
|
||||
operation: "trace_application_feature",
|
||||
predicate_type: "rea.application-feature-trace",
|
||||
subject: JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application.subject,
|
||||
normalized_result: {
|
||||
seed: { kind: "module", value: "renderer.js", match: "exact" },
|
||||
summary: {
|
||||
matched_seeds: 1,
|
||||
traced_nodes: 1,
|
||||
traced_edges: 0,
|
||||
traced_nodes: 2,
|
||||
traced_edges: 1,
|
||||
terminal_paths: 0,
|
||||
native_handoffs: 0,
|
||||
observed_facts: 1,
|
||||
observed_facts: 3,
|
||||
inferred_facts: 0,
|
||||
unknown_facts: 0,
|
||||
unavailable_facts: 0,
|
||||
@@ -809,7 +810,7 @@ describe("application workflow CLI parity", () => {
|
||||
predicate_type: "rea.application-version-comparison",
|
||||
normalized_result: {
|
||||
summary: {
|
||||
unchanged: 0,
|
||||
unchanged: 1,
|
||||
added: 2,
|
||||
removed: 0,
|
||||
changed: 1,
|
||||
@@ -852,7 +853,7 @@ describe("application workflow CLI parity", () => {
|
||||
reference_inventory_state: "complete",
|
||||
application_graph_status: "complete",
|
||||
retained_source_files: 1,
|
||||
retained_application_nodes: 1,
|
||||
retained_application_nodes: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -890,6 +891,7 @@ describe("application workflow CLI parity", () => {
|
||||
expect(traced).toMatchObject({
|
||||
operation: "trace_javascript_semantics",
|
||||
predicate_type: "rea.javascript-semantic-trace",
|
||||
subject: analyzed.value.subject,
|
||||
normalized_result: {
|
||||
source_evidence_id: analyzed.value.evidence_id,
|
||||
source_graph_id: result.semantic_graph.graph_id,
|
||||
@@ -936,6 +938,7 @@ it("traces all endpoint carriers and their response consumers through CLI and MC
|
||||
const cliEvidence = parseEvidence(
|
||||
await runCliWithRequestFile(root, "trace-javascript-semantics", input),
|
||||
);
|
||||
expect(cliEvidence.subject).toEqual(application.subject);
|
||||
const mcpResponse = await client.callTool({
|
||||
name: "trace_javascript_semantics",
|
||||
arguments: input,
|
||||
@@ -1920,6 +1923,11 @@ const runCliWithRequestFile = async (
|
||||
/** The CLI keeps complete trace JSON; the shared view owner selects from the saved file. */
|
||||
const assertTraceFileView = async (root: string, traced: unknown) => {
|
||||
const parent = parseEvidence(traced);
|
||||
if (parent.subject === null) throw new Error("Expected trace subject");
|
||||
const artifact = {
|
||||
path: parent.subject.local_path,
|
||||
sha256: parent.subject.digest.sha256,
|
||||
};
|
||||
const tracePath = join(root, "trace.json");
|
||||
await writeFile(tracePath, JSON.stringify(parent));
|
||||
const source = { kind: "evidence-file", path: tracePath };
|
||||
@@ -1934,7 +1942,8 @@ const assertTraceFileView = async (root: string, traced: unknown) => {
|
||||
kind: "summary",
|
||||
parent_evidence_id: parent.evidence_id,
|
||||
parent_operation: "trace_application_feature",
|
||||
summary: { summary: { matched_seeds: 1, traced_nodes: 1 } },
|
||||
artifact,
|
||||
summary: { summary: { matched_seeds: 1, traced_nodes: 2 } },
|
||||
},
|
||||
});
|
||||
expect(
|
||||
@@ -1949,8 +1958,9 @@ const assertTraceFileView = async (root: string, traced: unknown) => {
|
||||
).toMatchObject({
|
||||
normalized_result: {
|
||||
kind: "feature-trace",
|
||||
artifact,
|
||||
items: [{ seed: true, observation_count: 1 }],
|
||||
coverage: { total: 1, exhausted: true },
|
||||
coverage: { total: 2, exhausted: false },
|
||||
},
|
||||
});
|
||||
// Without a session ledger the CLI cannot retain a parent for a selected view.
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { projectAndroidApplicationEvidence } from "../../../../src/application/android/AndroidApplicationService.js";
|
||||
import { androidApplicationProjectionResultSchema } from "../../../../src/domain/android/androidApplication.js";
|
||||
import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGraph.js";
|
||||
import { parseEvidence } from "../../../../src/domain/evidence.js";
|
||||
|
||||
describe("Android application projection", () => {
|
||||
it("projects deterministic APK components and explicit bridge hypotheses", async () => {
|
||||
@@ -172,6 +173,23 @@ describe("Android application projection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the selected inventory subject through application projection", async () => {
|
||||
const root = await createTestTempDirectory("rea-android-subject-");
|
||||
const path = join(root, "Fixture.apk");
|
||||
const writer = new ZipWriter(new Uint8ArrayWriter());
|
||||
await writer.add("AndroidManifest.xml", new TextReader("manifest"));
|
||||
await writeFile(path, await writer.close());
|
||||
const inventory = await inventoryApplicationPackage(path);
|
||||
const selected = parseEvidence({
|
||||
...inventory,
|
||||
subject: { ...inventory.subject, name: "Selected application" },
|
||||
});
|
||||
const result = projectAndroidApplicationEvidence({
|
||||
inventory_evidence: [selected],
|
||||
});
|
||||
expect(requireSuccessfulProjection(result).subject).toEqual(selected.subject);
|
||||
});
|
||||
|
||||
it("projects APK v1 metadata candidates at any META-INF depth", async () => {
|
||||
const root = await createTestTempDirectory("rea-android-v1-metadata-");
|
||||
const path = join(root, "NestedSigning.apk");
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { createJavaScriptSemanticGraph } from "../../../src/domain/javascript/javascriptSemanticGraph.js";
|
||||
import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import { parseApplicationGraphEvidence } from "../../../src/application/javascript/JavaScriptApplicationEvidenceGraph.js";
|
||||
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js";
|
||||
@@ -45,6 +47,40 @@ cliTest(
|
||||
});
|
||||
expect(cliResponse.exitCode, cliResponse.stderr).toBe(0);
|
||||
const cliEvidence = parseEvidence(cliResponse.json);
|
||||
const parsed = javascriptApplicationAnalysisResultSchema.parse(
|
||||
cliEvidence.normalized_result,
|
||||
);
|
||||
const { graph_id: _id, ...semantic } = parsed.semantic_graph;
|
||||
const foreignSha = "f".repeat(64);
|
||||
const forged = createEvidence(
|
||||
{ path: parsed.input_path, sha256: foreignSha, format: parsed.format },
|
||||
cliEvidence.provider,
|
||||
{
|
||||
operation: cliEvidence.operation,
|
||||
predicateType: cliEvidence.predicate_type,
|
||||
parameters: cliEvidence.parameters,
|
||||
confidence: "derived",
|
||||
authority: "shipped-artifact",
|
||||
result: {
|
||||
...parsed,
|
||||
root_artifact_sha256: foreignSha,
|
||||
semantic_graph: createJavaScriptSemanticGraph({
|
||||
...semantic,
|
||||
root_artifact_sha256: foreignSha,
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(parseApplicationGraphEvidence(forged)).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
issues: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: ["normalized_result", "root_artifact_sha256"],
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
for (const command of ["analyze", "analyze-javascript-application"]) {
|
||||
const relativeResponse = await cli.run({
|
||||
|
||||
@@ -163,7 +163,21 @@ async function runInlineWorkflowScenarios(
|
||||
expect(tracedInline.structuredContent).toMatchObject({
|
||||
normalized_result: {
|
||||
seed: { kind: "module", value: "renderer.js", match: "exact" },
|
||||
summary: { matched_seeds: 1, traced_nodes: 1, unknown_facts: 0 },
|
||||
summary: {
|
||||
matched_seeds: 1,
|
||||
traced_nodes: 2,
|
||||
traced_edges: 1,
|
||||
unknown_facts: 0,
|
||||
},
|
||||
graph: {
|
||||
nodes: expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "artifact" }),
|
||||
expect.objectContaining({ kind: "javascript-asset" }),
|
||||
]),
|
||||
edges: expect.arrayContaining([
|
||||
expect.objectContaining({ relation: "contains" }),
|
||||
]),
|
||||
},
|
||||
coverage: {
|
||||
status: "complete-within-source",
|
||||
source_graph_status: "complete",
|
||||
|
||||
@@ -143,7 +143,13 @@ it.each(["full", "compact"] as const)(
|
||||
expect(summary.evidence.normalized_result).toMatchObject({
|
||||
kind: "summary",
|
||||
parent_operation: "trace_application_feature",
|
||||
artifact: null,
|
||||
artifact:
|
||||
parent.subject === null
|
||||
? null
|
||||
: {
|
||||
path: parent.subject.local_path,
|
||||
sha256: parent.subject.digest.sha256,
|
||||
},
|
||||
summary: {
|
||||
trace_id: full.trace_id,
|
||||
source_evidence_id: full.source_evidence_id,
|
||||
|
||||
@@ -35,7 +35,7 @@ const scenarios = [
|
||||
name: "trace_application_feature",
|
||||
input: JAVASCRIPT_FEATURE_TRACE_EXAMPLE,
|
||||
sourceIds: [application.evidence_id],
|
||||
facts: { summary: { matched_seeds: 1, traced_nodes: 1 } },
|
||||
facts: { summary: { matched_seeds: 1, traced_nodes: 2, traced_edges: 1 } },
|
||||
},
|
||||
{
|
||||
name: "trace_javascript_semantics",
|
||||
|
||||
@@ -273,6 +273,85 @@ describe("managed application graph projection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves only a managed graph's matching subject in feature traces", () => {
|
||||
const { artifactEvidence } = createManagedInteropEvidence();
|
||||
const selectedArtifact = parseEvidence({
|
||||
...artifactEvidence,
|
||||
subject: { ...artifactEvidence.subject, name: "selected managed image" },
|
||||
});
|
||||
const projected = projectManagedApplicationGraphEvidence({
|
||||
managed_artifact: selectedArtifact,
|
||||
});
|
||||
if (!projected.ok) throw projected.error;
|
||||
const parsed = parseEvidence(projected.value);
|
||||
expect(parsed.subject).toEqual(selectedArtifact.subject);
|
||||
const query = {
|
||||
seed: {
|
||||
kind: "string" as const,
|
||||
value: "MessageBoxW",
|
||||
match: "exact" as const,
|
||||
case_sensitive: true,
|
||||
},
|
||||
direction: "incoming" as const,
|
||||
};
|
||||
const trace = traceApplicationFeatureEvidence({
|
||||
application: parsed,
|
||||
...query,
|
||||
});
|
||||
if (!trace.ok) throw trace.error;
|
||||
expect(trace.value.evidence.subject).toEqual(selectedArtifact.subject);
|
||||
const foreign = createEvidence(
|
||||
{ path: "/other.exe", sha256: "f".repeat(64), format: "pe" },
|
||||
parsed.provider,
|
||||
{
|
||||
operation: parsed.operation,
|
||||
predicateType: parsed.predicate_type,
|
||||
parameters: parsed.parameters,
|
||||
result: parsed.normalized_result,
|
||||
confidence: "inferred",
|
||||
authority: "analyst-inference",
|
||||
evidenceLinks: parsed.evidence_links,
|
||||
},
|
||||
);
|
||||
const foreignTrace = traceApplicationFeatureEvidence({
|
||||
application: foreign,
|
||||
...query,
|
||||
});
|
||||
if (!foreignTrace.ok) throw foreignTrace.error;
|
||||
expect(foreignTrace.value.evidence.subject).toBeNull();
|
||||
const contradictory = createEvidence(
|
||||
{ path: "/other.exe", sha256: "f".repeat(64), format: "pe" },
|
||||
parsed.provider,
|
||||
{
|
||||
operation: parsed.operation,
|
||||
predicateType: parsed.predicate_type,
|
||||
parameters: parsed.parameters,
|
||||
result: {
|
||||
...(parsed.normalized_result as Record<string, unknown>),
|
||||
root_artifact_sha256: "f".repeat(64),
|
||||
},
|
||||
confidence: "inferred",
|
||||
authority: "analyst-inference",
|
||||
evidenceLinks: parsed.evidence_links,
|
||||
},
|
||||
);
|
||||
const contradictoryTrace = traceApplicationFeatureEvidence({
|
||||
application: contradictory,
|
||||
...query,
|
||||
});
|
||||
expect(contradictoryTrace).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisInputError",
|
||||
issues: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: ["application", "normalized_result", "root_artifact_sha256"],
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed application graph request", () => {
|
||||
it("accepts any nonempty set of inline sources and rejects duplicates", () => {
|
||||
const evidence = MANAGED_APPLICATION_GRAPH_EXAMPLE.managed_members;
|
||||
|
||||
Reference in new issue
Block a user