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:
morlutoandFelix authored and GitHub committed 2026-11-09 08:12:17 +08:00
1 parent 4d7c6efe9a
commit 52fe342c73
28 files changed
+615 -151

No files matched your search

@@ -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: