mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +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
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user