mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(evidence): separate source and presentation identity
This commit is contained in:
1 parent
3e8923c6eb
commit
2f27d3ac92
17 files changed
+214
-48
No files matched your search
@@ -33,6 +33,7 @@ import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
import { EvidenceLedger } from "../investigation/EvidenceLedger.js";
|
||||
import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js";
|
||||
|
||||
import { isImmutableJsonSnapshot } from "../../domain/immutableJson.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import {
|
||||
@@ -43,6 +44,85 @@ import {
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import * as evidenceFileSource from "./evidenceFileSource.js";
|
||||
|
||||
it("projects retained GPU source facts while delivering the caller's rebound subject", () => {
|
||||
const sha256 = "a".repeat(64);
|
||||
const original = createEvidence(
|
||||
{ path: "/original/gpu", sha256, format: "elf" },
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{
|
||||
operation: "inspect_gpu_code_objects",
|
||||
parameters: {},
|
||||
result: {
|
||||
artifact: {
|
||||
path: "/original/gpu",
|
||||
file_size: 0,
|
||||
sha256,
|
||||
bytes_read: 0,
|
||||
},
|
||||
format: "elf",
|
||||
analysis: "inventory",
|
||||
objects: [],
|
||||
kernels: [],
|
||||
ptx_artifacts: [],
|
||||
coverage: {
|
||||
discovery_scope: "supported-section-names-and-standalone-formats",
|
||||
discovery: "partial",
|
||||
payloads: "not-requested",
|
||||
examined_sections: 0,
|
||||
unknown_section_names: 0,
|
||||
},
|
||||
limitations: [],
|
||||
},
|
||||
},
|
||||
);
|
||||
const ledger = new EvidenceLedger();
|
||||
expect(ledger.record(original).ok).toBe(true);
|
||||
const retained = ledger.forAnalysis(original.evidence_id)!;
|
||||
const payload = retained.normalized_result;
|
||||
const selected = rebindEvidenceToTargetPath(original, {
|
||||
path: "/selected/gpu",
|
||||
sha256,
|
||||
format: "elf",
|
||||
});
|
||||
expect(selected.evidence_id).toBe(original.evidence_id);
|
||||
const retention = {
|
||||
recordEvidence: (evidence: Evidence) => ledger.record(evidence),
|
||||
evidenceById: (id: string) => ledger.forAnalysis(id),
|
||||
};
|
||||
for (const view of [
|
||||
{ kind: "summary" as const },
|
||||
{
|
||||
kind: "gpu-code-objects" as const,
|
||||
collection: "objects" as const,
|
||||
offset: 0,
|
||||
limit: 64,
|
||||
},
|
||||
]) {
|
||||
const result = projectRetainedAnalysis(selected, retention, view);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.subject?.local_path).toBe("/selected/gpu");
|
||||
expect(result.value.normalized_result).toMatchObject({
|
||||
parent_evidence_id: original.evidence_id,
|
||||
artifact: { path: "/original/gpu", sha256 },
|
||||
});
|
||||
}
|
||||
expect(ledger.forAnalysis(original.evidence_id)).toBe(retained);
|
||||
expect(retained.normalized_result).toBe(payload);
|
||||
expect(retained.subject?.local_path).toBe("/original/gpu");
|
||||
const mismatched = createEvidence(
|
||||
{ path: "/original/gpu", sha256: "b".repeat(64), format: "elf" },
|
||||
original.provider,
|
||||
{
|
||||
operation: original.operation,
|
||||
parameters: {},
|
||||
result: original.normalized_result,
|
||||
},
|
||||
);
|
||||
expect(summarizeRetainedAnalysis(mismatched, retention)).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisInputError" },
|
||||
});
|
||||
});
|
||||
it.each(["inline", "retained-evidence", "invalid"] as const)(
|
||||
"refuses an already cancelled %s view before source admission",
|
||||
async (kind) => {
|
||||
|
||||
@@ -208,6 +208,7 @@ const projectResolvedAnalysisView = (
|
||||
// Sealing an Evidence root deep-freezes it; only the root is registered.
|
||||
sealedResult: isImmutableJsonSnapshot(parent),
|
||||
subject: presentation.subject,
|
||||
sourceSubject: parent.subject,
|
||||
limitations: parent.limitations,
|
||||
parameters: parent.parameters,
|
||||
artifact:
|
||||
|
||||
@@ -18,7 +18,11 @@ import {
|
||||
type AnalysisViewRequest,
|
||||
type AnalysisViewResult,
|
||||
} from "../../domain/analysisView/analysisView.js";
|
||||
import { inspectAnalysisView } from "./AnalysisViewService.js";
|
||||
import {
|
||||
inspectAnalysisView,
|
||||
projectRetainedAnalysis,
|
||||
} from "./AnalysisViewService.js";
|
||||
import { EvidenceLedger } from "../investigation/EvidenceLedger.js";
|
||||
|
||||
const provider = { id: "fixture", name: "Fixture provider", version: "1" };
|
||||
const parent = (
|
||||
@@ -494,6 +498,27 @@ it("keeps context identity across authenticated target path aliases while preser
|
||||
});
|
||||
expect(second.evidence.subject?.local_path).toBe("/other/alias.elf");
|
||||
expect(second.evidence.evidence_id).not.toBe(first.evidence.evidence_id);
|
||||
const ledger = new EvidenceLedger();
|
||||
expect(ledger.record(original).ok).toBe(true);
|
||||
const retained = ledger.forAnalysis(original.evidence_id)!;
|
||||
const delivered = projectRetainedAnalysis(
|
||||
rebound,
|
||||
{
|
||||
recordEvidence: (evidence) => ledger.record(evidence),
|
||||
evidenceById: (id) => ledger.forAnalysis(id),
|
||||
},
|
||||
view,
|
||||
);
|
||||
if (!delivered.ok) throw delivered.error;
|
||||
expect(delivered.value.subject?.local_path).toBe("/other/alias.elf");
|
||||
expect(delivered.value.normalized_result).toMatchObject({
|
||||
artifact: { path: "/other/alias.elf" },
|
||||
items: {
|
||||
rows: [{ record: { subject: { local_path: "/other/alias.elf" } } }],
|
||||
},
|
||||
});
|
||||
expect(ledger.forAnalysis(original.evidence_id)).toBe(retained);
|
||||
expect(retained.subject?.local_path).toBe("/selected/fixture.elf");
|
||||
});
|
||||
|
||||
it("pages recorded empty graph levels without inventing nodes or dropping their identity", async () => {
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
} from "../webNetworkCapture.js";
|
||||
import { admitAnalysisViewData } from "./analysisViewDataAdmission.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import { analysisViewResultInputError } from "./analysisViewParent.js";
|
||||
import {
|
||||
analysisViewSourceSubject,
|
||||
analysisViewResultInputError,
|
||||
} from "./analysisViewParent.js";
|
||||
import type { AnalysisViewParent } from "./analysisView.js";
|
||||
import {
|
||||
ENTRY_BYTES,
|
||||
@@ -150,12 +153,13 @@ export const admitHarCaptureParent = (
|
||||
// Environment views cite the report path and digest, so their parent's subject must be exactly them;
|
||||
// the api-surface view keeps the admission its existing callers rely on.
|
||||
const { artifact } = parsed.data;
|
||||
const sourceSubject = analysisViewSourceSubject(parent);
|
||||
if (
|
||||
viewKind !== "api-surface" &&
|
||||
artifact.sha256 !== null &&
|
||||
(parent.subject == null ||
|
||||
parent.subject.local_path !== artifact.path ||
|
||||
parent.subject.digest.sha256 !== artifact.sha256)
|
||||
(sourceSubject == null ||
|
||||
sourceSubject.local_path !== artifact.path ||
|
||||
sourceSubject.digest.sha256 !== artifact.sha256)
|
||||
)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
|
||||
@@ -1437,8 +1437,10 @@ export interface AnalysisViewParent {
|
||||
readonly environment?: Evidence["environment"];
|
||||
/** The owner proved normalizedResult belongs to a sealed, deep-frozen Evidence snapshot. */
|
||||
readonly sealedResult?: boolean;
|
||||
/** Preserve the authenticated source identity, including explicit absence. */
|
||||
/** Subject presentation for this delivery, including explicit absence. */
|
||||
readonly subject?: Evidence["subject"];
|
||||
/** Original retained identity for report validation; omitted for direct domain callers. */
|
||||
readonly sourceSubject?: Evidence["subject"];
|
||||
readonly limitations: readonly string[];
|
||||
readonly artifact?: { readonly path: string; readonly sha256: string } | null;
|
||||
/** Parent Evidence parameters, when the parent operation records them. */
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
isImmutableJsonSnapshot,
|
||||
} from "../immutableJson.js";
|
||||
import type { AnalysisViewParent } from "./analysisView.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
import {
|
||||
analysisViewSourceSubject,
|
||||
createAnalysisViewResultReader,
|
||||
} from "./analysisViewParent.js";
|
||||
|
||||
const parent = (normalizedResult: unknown): AnalysisViewParent => ({
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
@@ -15,6 +18,14 @@ const parent = (normalizedResult: unknown): AnalysisViewParent => ({
|
||||
limitations: [],
|
||||
});
|
||||
|
||||
it("keeps explicit source absence separate from a delivery subject", () => {
|
||||
const source = parent({});
|
||||
expect(analysisViewSourceSubject(source)).toBeUndefined();
|
||||
expect(
|
||||
analysisViewSourceSubject({ ...source, sourceSubject: null }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("does not trust a caller's sealed flag or shallow freeze for mutable facts", () => {
|
||||
const item = { value: "valid" };
|
||||
const source = Object.freeze({ items: [item] });
|
||||
|
||||
@@ -67,3 +67,9 @@ export const analysisViewResultInputError = (
|
||||
{ source: { normalized_result: input } },
|
||||
{ cause },
|
||||
);
|
||||
|
||||
/** Resolve report-validation identity once, preserving explicit source absence. */
|
||||
export const analysisViewSourceSubject = (
|
||||
parent: AnalysisViewParent,
|
||||
): AnalysisViewParent["subject"] =>
|
||||
parent.sourceSubject === undefined ? parent.subject : parent.sourceSubject;
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
type DesktopApplication,
|
||||
} from "../desktopApplication.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import { parseAnalysisViewResult } from "./analysisViewParent.js";
|
||||
import {
|
||||
analysisViewSourceSubject,
|
||||
parseAnalysisViewResult,
|
||||
} from "./analysisViewParent.js";
|
||||
import {
|
||||
completeWithinViewCoverage,
|
||||
pageViewCoverage,
|
||||
@@ -181,12 +184,13 @@ export const projectDesktopApplicationView = (
|
||||
validatedParents.add(key);
|
||||
}
|
||||
}
|
||||
const sourceSubject = analysisViewSourceSubject(parent);
|
||||
if (
|
||||
parent.subject === undefined ||
|
||||
parent.subject === null ||
|
||||
parent.subject.format !== "directory" ||
|
||||
parent.subject.local_path !== report.root.path ||
|
||||
parent.subject.digest.sha256 !== report.root.snapshot_sha256
|
||||
sourceSubject === undefined ||
|
||||
sourceSubject === null ||
|
||||
sourceSubject.format !== "directory" ||
|
||||
sourceSubject.local_path !== report.root.path ||
|
||||
sourceSubject.digest.sha256 !== report.root.snapshot_sha256
|
||||
)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
|
||||
@@ -4,7 +4,10 @@ import type { AnalysisError } from "../analysisErrorBase.js";
|
||||
import { uniqueSorted } from "../canonicalOrdering.js";
|
||||
import { evmInterfaceSchema } from "../evm/evmInterface.js";
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
import { parseAnalysisViewResult } from "./analysisViewParent.js";
|
||||
import {
|
||||
analysisViewSourceSubject,
|
||||
parseAnalysisViewResult,
|
||||
} from "./analysisViewParent.js";
|
||||
import type {
|
||||
AnalysisViewParent,
|
||||
AnalysisViewRequest,
|
||||
@@ -66,11 +69,12 @@ export const projectEvmInterfaceView = (
|
||||
const parsed = parseAnalysisViewResult(evmInterfaceSchema, parent);
|
||||
if (!parsed.ok) return parsed;
|
||||
const report = parsed.value;
|
||||
const sourceSubject = analysisViewSourceSubject(parent);
|
||||
if (
|
||||
parent.subject == null ||
|
||||
parent.subject.format !== "file" ||
|
||||
parent.subject.local_path !== report.artifact.path ||
|
||||
parent.subject.digest.sha256 !== report.artifact.sha256
|
||||
sourceSubject == null ||
|
||||
sourceSubject.format !== "file" ||
|
||||
sourceSubject.local_path !== report.artifact.path ||
|
||||
sourceSubject.digest.sha256 !== report.artifact.sha256
|
||||
)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
|
||||
@@ -20,7 +20,10 @@ import {
|
||||
pageViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
import {
|
||||
analysisViewSourceSubject,
|
||||
createAnalysisViewResultReader,
|
||||
} from "./analysisViewParent.js";
|
||||
import { selectViewRows } from "./selectViewRows.js";
|
||||
|
||||
const indexSchema = z.number().int().nonnegative();
|
||||
@@ -122,11 +125,12 @@ export const projectGoBinaryView = (
|
||||
const parsed = parseReport(parent);
|
||||
if (!parsed.ok) return parsed;
|
||||
const report = parsed.value;
|
||||
const sourceSubject = analysisViewSourceSubject(parent);
|
||||
if (
|
||||
parent.subject?.local_path !== report.artifact.path ||
|
||||
parent.subject.digest.sha256 !== report.artifact.sha256 ||
|
||||
parent.subject.format !== metadataReportSubjectFormat(report) ||
|
||||
parent.subject.architecture !==
|
||||
sourceSubject?.local_path !== report.artifact.path ||
|
||||
sourceSubject.digest.sha256 !== report.artifact.sha256 ||
|
||||
sourceSubject.format !== metadataReportSubjectFormat(report) ||
|
||||
sourceSubject.architecture !==
|
||||
(binaryMetadataArchitecture(report.architecture) ?? null)
|
||||
)
|
||||
return err(
|
||||
|
||||
@@ -18,7 +18,10 @@ import type {
|
||||
UnsignedAnalysisView,
|
||||
} from "./analysisView.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
import {
|
||||
analysisViewSourceSubject,
|
||||
createAnalysisViewResultReader,
|
||||
} from "./analysisViewParent.js";
|
||||
import {
|
||||
boundedViewMessages,
|
||||
completeWithinViewCoverage,
|
||||
@@ -193,10 +196,11 @@ const readGpuFacts = (
|
||||
};
|
||||
if (reusable) checkedGpu.set(report, checked);
|
||||
}
|
||||
const sourceSubject = analysisViewSourceSubject(parent);
|
||||
if (
|
||||
parent.subject != null &&
|
||||
(parent.subject.local_path !== checked.report.artifact.path ||
|
||||
parent.subject.digest.sha256 !== checked.report.artifact.sha256)
|
||||
sourceSubject != null &&
|
||||
(sourceSubject.local_path !== checked.report.artifact.path ||
|
||||
sourceSubject.digest.sha256 !== checked.report.artifact.sha256)
|
||||
)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
|
||||
@@ -24,7 +24,10 @@ import {
|
||||
il2cppMetadataTypeSchema,
|
||||
} from "../il2cpp/il2cppMetadata.js";
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
import {
|
||||
analysisViewSourceSubject,
|
||||
createAnalysisViewResultReader,
|
||||
} from "./analysisViewParent.js";
|
||||
import type {
|
||||
AnalysisViewParent,
|
||||
AnalysisViewRequest,
|
||||
@@ -535,11 +538,12 @@ export const projectIl2cppMetadataView = (
|
||||
const parsed = parseMetadataReport(parent);
|
||||
if (!parsed.ok) return parsed;
|
||||
const { report, counts } = parsed.value;
|
||||
const sourceSubject = analysisViewSourceSubject(parent);
|
||||
if (
|
||||
parent.subject != null &&
|
||||
(parent.subject.format !== "file" ||
|
||||
parent.subject.local_path !== report.artifact.path ||
|
||||
parent.subject.digest.sha256 !== report.artifact.sha256)
|
||||
sourceSubject != null &&
|
||||
(sourceSubject.format !== "file" ||
|
||||
sourceSubject.local_path !== report.artifact.path ||
|
||||
sourceSubject.digest.sha256 !== report.artifact.sha256)
|
||||
)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { analysisViewSourceSubject } from "./analysisViewParent.js";
|
||||
import { javascriptSourceRangeSchema } from "../javascript/javascriptSourceCoordinates.js";
|
||||
import { z } from "zod";
|
||||
import { webBundleViewRequestSchema } from "./webBundleView.js";
|
||||
@@ -344,10 +345,11 @@ export const projectJavaScriptRoutesView = (
|
||||
path: identity.data.input_path,
|
||||
sha256: identity.data.root_artifact_sha256,
|
||||
};
|
||||
const sourceSubject = analysisViewSourceSubject(parent);
|
||||
if (
|
||||
parent.subject != null &&
|
||||
(parent.subject.local_path !== artifact.path ||
|
||||
parent.subject.digest.sha256 !== artifact.sha256)
|
||||
sourceSubject != null &&
|
||||
(sourceSubject.local_path !== artifact.path ||
|
||||
sourceSubject.digest.sha256 !== artifact.sha256)
|
||||
)
|
||||
return err(
|
||||
inputError(
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
} from "../model/modelArtifact.js";
|
||||
import type { AnalysisError } from "../analysisErrorBase.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
import {
|
||||
analysisViewSourceSubject,
|
||||
createAnalysisViewResultReader,
|
||||
} from "./analysisViewParent.js";
|
||||
import { isImmutableJsonSnapshot } from "../immutableJson.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
@@ -142,10 +145,11 @@ const read = (parent: AnalysisViewParent): Result<Checked, AnalysisError> => {
|
||||
const report = parsed.value;
|
||||
const reusable = isImmutableJsonSnapshot(report);
|
||||
const cached = reusable ? checked.get(report) : undefined;
|
||||
const sourceSubject = analysisViewSourceSubject(parent);
|
||||
if (
|
||||
parent.subject != null &&
|
||||
(parent.subject.local_path !== report.artifact.path ||
|
||||
parent.subject.digest.sha256 !== report.artifact.sha256)
|
||||
sourceSubject != null &&
|
||||
(sourceSubject.local_path !== report.artifact.path ||
|
||||
sourceSubject.digest.sha256 !== report.artifact.sha256)
|
||||
)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import { z } from "zod";
|
||||
import { pcapEvidenceSchemas } from "../pcap/pcapAnalysis.js";
|
||||
import { parseAnalysisViewResult } from "./analysisViewParent.js";
|
||||
import {
|
||||
analysisViewSourceSubject,
|
||||
parseAnalysisViewResult,
|
||||
} from "./analysisViewParent.js";
|
||||
import type { AnalysisError } from "../analysisErrorBase.js";
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
import type {
|
||||
@@ -113,9 +116,10 @@ const parseParent = (
|
||||
): Result<Capture, AnalysisError> => {
|
||||
const parsed = parseAnalysisViewResult(captureSchema, parent);
|
||||
if (!parsed.ok) return parsed;
|
||||
const sourceSubject = analysisViewSourceSubject(parent);
|
||||
if (
|
||||
parent.subject?.local_path !== parsed.value.target.path ||
|
||||
parent.subject.digest.sha256 !== parsed.value.target.sha256
|
||||
sourceSubject?.local_path !== parsed.value.target.path ||
|
||||
sourceSubject.digest.sha256 !== parsed.value.target.sha256
|
||||
)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
|
||||
@@ -20,7 +20,10 @@ import {
|
||||
pageViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
import {
|
||||
analysisViewSourceSubject,
|
||||
createAnalysisViewResultReader,
|
||||
} from "./analysisViewParent.js";
|
||||
import { selectViewRows } from "./selectViewRows.js";
|
||||
|
||||
const count = z.number().int().nonnegative();
|
||||
@@ -123,7 +126,7 @@ export const projectRustBinaryView = (
|
||||
const parsed = parseReport(parent);
|
||||
if (!parsed.ok) return parsed;
|
||||
const report = parsed.value;
|
||||
const subject = parent.subject;
|
||||
const subject = analysisViewSourceSubject(parent);
|
||||
if (
|
||||
subject == null ||
|
||||
subject.local_path !== report.artifact.path ||
|
||||
|
||||
@@ -3,7 +3,10 @@ import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import type { AnalysisError } from "../analysisErrorBase.js";
|
||||
import { parseAnalysisViewResult } from "./analysisViewParent.js";
|
||||
import {
|
||||
analysisViewSourceSubject,
|
||||
parseAnalysisViewResult,
|
||||
} from "./analysisViewParent.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
import {
|
||||
@@ -422,11 +425,12 @@ const parseCaptureParent = (
|
||||
const parsed = parseAnalysisViewResult(captureParser, parent);
|
||||
if (!parsed.ok) return parsed;
|
||||
const report = parsed.value;
|
||||
const sourceSubject = analysisViewSourceSubject(parent);
|
||||
if (
|
||||
report.artifact.sha256 !== null &&
|
||||
(parent.subject == null ||
|
||||
parent.subject.local_path !== report.artifact.path ||
|
||||
parent.subject.digest.sha256 !== report.artifact.sha256)
|
||||
(sourceSubject == null ||
|
||||
sourceSubject.local_path !== report.artifact.path ||
|
||||
sourceSubject.digest.sha256 !== report.artifact.sha256)
|
||||
)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
|
||||
Reference in new issue
Block a user