rea-agents 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -20
- package/bridge/hopper_bridge.py +141 -13
- package/dist/application/AnalysisProvider.js +12 -1
- package/dist/application/ArtifactExtraction.js +166 -0
- package/dist/application/ArtifactGraphConstruction.js +257 -0
- package/dist/application/ArtifactInventory.js +253 -0
- package/dist/application/BinarySession.js +164 -14
- package/dist/application/CompositeProvider.js +73 -0
- package/dist/application/DirectAnalysis.js +27 -3
- package/dist/application/Doctor.js +35 -6
- package/dist/application/EnhancedTools.js +8 -7
- package/dist/application/EvidenceBundleCommands.js +29 -0
- package/dist/application/EvidenceBundleFiles.js +127 -0
- package/dist/application/EvidenceLedger.js +225 -18
- package/dist/application/FilesystemSnapshot.js +124 -0
- package/dist/application/LinuxHopper.js +186 -0
- package/dist/application/LoopbackReplay.js +195 -37
- package/dist/application/ProcessHarness.js +200 -191
- package/dist/application/ProcessNormalization.js +44 -0
- package/dist/application/ProcessOwnership.js +84 -0
- package/dist/application/ProcessSampling.js +284 -0
- package/dist/application/RealHopperAssertions.js +105 -0
- package/dist/application/ReferenceSourceImport.js +182 -0
- package/dist/application/ReferenceSourceImportEntries.js +122 -0
- package/dist/application/ReferenceSourceImportPolicy.js +73 -0
- package/dist/application/ReferenceSourceImportTypes.js +18 -0
- package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
- package/dist/application/Setup.js +192 -41
- package/dist/application/Uninstall.js +130 -0
- package/dist/application/runtime.js +8 -1
- package/dist/artifacts/ArtifactPaths.js +51 -0
- package/dist/artifacts/ArtifactProvider.js +130 -0
- package/dist/artifacts/ArtifactReader.js +9 -0
- package/dist/artifacts/AsarArtifactReader.js +62 -0
- package/dist/artifacts/DirectoryArtifactReader.js +107 -0
- package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
- package/dist/artifacts/SafeOutputTree.js +199 -0
- package/dist/artifacts/StreamBytes.js +10 -0
- package/dist/artifacts/ZipArtifactReader.js +109 -0
- package/dist/cli.js +153 -6
- package/dist/config.js +35 -1
- package/dist/contracts/artifactComparisonExample.js +95 -0
- package/dist/contracts/artifactToolContracts.js +84 -0
- package/dist/contracts/enhancedInputs.js +4 -0
- package/dist/contracts/functionComparisonExample.js +57 -0
- package/dist/contracts/investigationExamples.js +118 -0
- package/dist/contracts/nativeToolContracts.js +52 -0
- package/dist/contracts/processCaptureExample.js +25 -0
- package/dist/contracts/toolContractExamples.js +69 -0
- package/dist/contracts/toolContracts.js +99 -36
- package/dist/contracts/toolOutputSchemas.js +154 -82
- package/dist/contracts/unknownContractExamples.js +33 -0
- package/dist/domain/artifactComparison.js +273 -0
- package/dist/domain/artifactGraph.js +194 -0
- package/dist/domain/artifactInventoryEvidence.js +150 -0
- package/dist/domain/binaryTarget.js +55 -0
- package/dist/domain/bundleComparison.js +266 -0
- package/dist/domain/callPath.js +346 -0
- package/dist/domain/changedBehavior.js +294 -0
- package/dist/domain/errors.js +192 -4
- package/dist/domain/evidence.js +41 -10
- package/dist/domain/evidenceBundle.js +187 -6
- package/dist/domain/functionComparison.js +201 -0
- package/dist/domain/functionComparisonNormalization.js +112 -0
- package/dist/domain/functionComparisonResults.js +54 -0
- package/dist/domain/functionComparisonSchemas.js +82 -0
- package/dist/domain/functionDossierEvidence.js +171 -0
- package/dist/domain/hopperValues.js +14 -7
- package/dist/domain/nativeInspection.js +142 -0
- package/dist/domain/processCapture.js +152 -54
- package/dist/domain/processComparison.js +106 -0
- package/dist/domain/reconstructionUnknowns.js +90 -0
- package/dist/domain/reconstructionVerification.js +285 -0
- package/dist/domain/reconstructionVerificationSchemas.js +126 -0
- package/dist/domain/referenceSourceClassification.js +496 -0
- package/dist/domain/referenceSourceGraph.js +376 -0
- package/dist/domain/referenceSourceImportParsing.js +235 -0
- package/dist/domain/residualUnknown.js +239 -0
- package/dist/domain/staticRuntimeCorrelation.js +375 -0
- package/dist/hopper/BridgeLauncher.js +40 -3
- package/dist/hopper/HopperClient.js +14 -5
- package/dist/hopper/HopperProvider.js +57 -22
- package/dist/identity.js +1 -0
- package/dist/main.js +5 -1
- package/dist/native/CommandRunner.js +156 -0
- package/dist/native/NativeMacOSProvider.js +306 -0
- package/dist/native/NativeMachoInspection.js +135 -0
- package/dist/native/parsers/codesign.js +55 -0
- package/dist/native/parsers/demangle.js +26 -0
- package/dist/native/parsers/dyldInfo.js +25 -0
- package/dist/native/parsers/lipo.js +67 -0
- package/dist/native/parsers/otool.js +193 -0
- package/dist/native/parsers/plist.js +23 -0
- package/dist/reference/ReferenceSourceReader.js +73 -0
- package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
- package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
- package/dist/reference/ReferenceSourceReaderFile.js +119 -0
- package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
- package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
- package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
- package/dist/server/createServer.js +31 -5
- package/dist/server/recordDerivedEvidence.js +10 -0
- package/dist/server/registerArtifactComparisonTool.js +62 -0
- package/dist/server/registerArtifactTools.js +6 -0
- package/dist/server/registerBundleComparisonTool.js +47 -0
- package/dist/server/registerEnhancedTools.js +64 -12
- package/dist/server/registerEvidenceTools.js +36 -0
- package/dist/server/registerFunctionComparisonTool.js +68 -0
- package/dist/server/registerInvestigationTools.js +224 -0
- package/dist/server/registerNativeTools.js +6 -0
- package/dist/server/registerOfficialTools.js +47 -14
- package/dist/server/registerProcessComparisonTool.js +106 -0
- package/dist/server/registerSessionTools.js +179 -70
- package/dist/server/sessionEvidence.js +28 -0
- package/dist/server/sessionToolPolicies.js +64 -0
- package/dist/server/toolRegistrationOptions.js +7 -0
- package/dist/server/toolResult.js +8 -5
- package/install.sh +198 -0
- package/package.json +19 -2
- package/scripts/rea.mjs +5 -1
- package/skills/rea-analysis/SKILL.md +77 -2
- /package/dist/{application/HopperToolPort.js → domain/referenceSourcePolicy.js} +0 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
3
|
+
const artifactIdSchema = z.string().regex(/^art_[a-f0-9]{64}$/u);
|
|
4
|
+
const occurrenceIdSchema = z.string().regex(/^occ_[a-f0-9]{64}$/u);
|
|
5
|
+
const edgeIdSchema = z.string().regex(/^edge_[a-f0-9]{64}$/u);
|
|
6
|
+
const manifestIdSchema = z.string().regex(/^agm_[a-f0-9]{64}$/u);
|
|
7
|
+
const extractionIdSchema = z.string().regex(/^aex_[a-f0-9]{64}$/u);
|
|
8
|
+
const boundedRelativePathSchema = z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(4_096)
|
|
12
|
+
.refine((path) => !path.startsWith("/") &&
|
|
13
|
+
!path.includes("\\") &&
|
|
14
|
+
path.split("/").every((part) => part !== "" && part !== ".."), "Expected a normalized relative POSIX path without traversal");
|
|
15
|
+
/** Closed artifact categories used to traverse application containers. */
|
|
16
|
+
const artifactKindSchema = z.enum([
|
|
17
|
+
"container",
|
|
18
|
+
"executable",
|
|
19
|
+
"dynamic-library",
|
|
20
|
+
"framework",
|
|
21
|
+
"native-addon",
|
|
22
|
+
"javascript",
|
|
23
|
+
"source-map",
|
|
24
|
+
"plist",
|
|
25
|
+
"entitlements",
|
|
26
|
+
"resource",
|
|
27
|
+
"universal-slice",
|
|
28
|
+
"package-metadata",
|
|
29
|
+
"unknown",
|
|
30
|
+
]);
|
|
31
|
+
/** Recognized storage or application formats; unknown bytes remain explicit. */
|
|
32
|
+
const artifactFormatSchema = z.enum([
|
|
33
|
+
"dmg",
|
|
34
|
+
"zip",
|
|
35
|
+
"pkg",
|
|
36
|
+
"asar",
|
|
37
|
+
"javascript-bundle",
|
|
38
|
+
"source-map",
|
|
39
|
+
"mach-o-universal",
|
|
40
|
+
"mach-o",
|
|
41
|
+
"elf",
|
|
42
|
+
"pe",
|
|
43
|
+
"hopper",
|
|
44
|
+
"ipa",
|
|
45
|
+
"apk",
|
|
46
|
+
"plist",
|
|
47
|
+
"entitlements",
|
|
48
|
+
"directory",
|
|
49
|
+
"file",
|
|
50
|
+
"unknown",
|
|
51
|
+
]);
|
|
52
|
+
/** Exact bounded policy applied while discovering or extracting artifacts. */
|
|
53
|
+
const artifactTraversalLimitsSchema = z.object({
|
|
54
|
+
max_entries: z.number().int().min(1).max(1_000_000),
|
|
55
|
+
max_total_bytes: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
|
|
56
|
+
max_entry_bytes: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
|
|
57
|
+
max_compression_ratio: z.number().min(1).max(100_000),
|
|
58
|
+
max_depth: z.number().int().min(0).max(100),
|
|
59
|
+
max_path_bytes: z.number().int().min(1).max(65_535),
|
|
60
|
+
});
|
|
61
|
+
/** File byte interval for an artifact derived without writing it to disk. */
|
|
62
|
+
const artifactByteRangeSchema = z.object({
|
|
63
|
+
offset: z.number().int().min(0),
|
|
64
|
+
length: z.number().int().min(0),
|
|
65
|
+
});
|
|
66
|
+
/** Bounded external command provenance, including observable side effects. */
|
|
67
|
+
const artifactCommandSchema = z.object({
|
|
68
|
+
tool: z.string().min(1),
|
|
69
|
+
arguments: z.array(z.string().max(4_096)).max(1_024),
|
|
70
|
+
tool_version: z.string().nullable(),
|
|
71
|
+
executable_sha256: sha256Schema.nullable(),
|
|
72
|
+
exit_code: z.number().int().nullable(),
|
|
73
|
+
effects: z.array(z.enum(["read", "write", "mount"])).max(3),
|
|
74
|
+
});
|
|
75
|
+
/** One content-addressed application artifact in deterministic manifest order. */
|
|
76
|
+
const artifactNodeSchema = z.object({
|
|
77
|
+
artifact_id: artifactIdSchema,
|
|
78
|
+
kind: artifactKindSchema,
|
|
79
|
+
format: artifactFormatSchema,
|
|
80
|
+
sha256: sha256Schema,
|
|
81
|
+
size: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
82
|
+
media_type: z.string().min(1).nullable(),
|
|
83
|
+
architecture: z.string().min(1).nullable(),
|
|
84
|
+
executable: z.boolean(),
|
|
85
|
+
content_state: z.enum(["materialized", "embedded", "virtual"]),
|
|
86
|
+
limitations: z.array(z.string()),
|
|
87
|
+
});
|
|
88
|
+
/** One path or virtual location where an artifact occurs in its parent. */
|
|
89
|
+
const artifactOccurrenceSchema = z.object({
|
|
90
|
+
occurrence_id: occurrenceIdSchema,
|
|
91
|
+
artifact_id: artifactIdSchema.nullable(),
|
|
92
|
+
parent_occurrence_id: occurrenceIdSchema.nullable(),
|
|
93
|
+
logical_path: boundedRelativePathSchema,
|
|
94
|
+
entry_kind: z.enum(["file", "directory", "symlink", "slice"]),
|
|
95
|
+
declared_size: z
|
|
96
|
+
.number()
|
|
97
|
+
.int()
|
|
98
|
+
.min(0)
|
|
99
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
100
|
+
.nullable(),
|
|
101
|
+
compressed_size: z
|
|
102
|
+
.number()
|
|
103
|
+
.int()
|
|
104
|
+
.min(0)
|
|
105
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
106
|
+
.nullable(),
|
|
107
|
+
executable: z.boolean(),
|
|
108
|
+
encrypted: z.boolean(),
|
|
109
|
+
hash_status: z.enum(["verified", "not-hashed-limit", "unavailable"]),
|
|
110
|
+
source_location: artifactByteRangeSchema.nullable(),
|
|
111
|
+
limitations: z.array(z.string()),
|
|
112
|
+
});
|
|
113
|
+
/** Parent-child derivation relation between two content-addressed artifacts. */
|
|
114
|
+
const artifactEdgeSchema = z.object({
|
|
115
|
+
edge_id: edgeIdSchema,
|
|
116
|
+
ordinal: z.number().int().min(0),
|
|
117
|
+
parent_artifact_id: artifactIdSchema,
|
|
118
|
+
child_artifact_id: artifactIdSchema,
|
|
119
|
+
relation: z.enum([
|
|
120
|
+
"contains",
|
|
121
|
+
"extracts",
|
|
122
|
+
"slice-of",
|
|
123
|
+
"embeds",
|
|
124
|
+
"loads",
|
|
125
|
+
"maps-source",
|
|
126
|
+
"derived-from",
|
|
127
|
+
]),
|
|
128
|
+
occurrence_id: occurrenceIdSchema,
|
|
129
|
+
logical_path: boundedRelativePathSchema.nullable(),
|
|
130
|
+
producer: artifactCommandSchema.nullable(),
|
|
131
|
+
});
|
|
132
|
+
/** Stable cryptographic commitment and counts for a complete artifact graph. */
|
|
133
|
+
const artifactGraphManifestSchema = z.object({
|
|
134
|
+
schema_version: z.literal(1),
|
|
135
|
+
manifest_id: manifestIdSchema,
|
|
136
|
+
root_artifact_id: artifactIdSchema,
|
|
137
|
+
root_sha256: sha256Schema,
|
|
138
|
+
root_format: artifactFormatSchema,
|
|
139
|
+
graph_sha256: sha256Schema,
|
|
140
|
+
node_count: z.number().int().min(1),
|
|
141
|
+
occurrence_count: z.number().int().min(1),
|
|
142
|
+
edge_count: z.number().int().min(0),
|
|
143
|
+
});
|
|
144
|
+
/** Independently bounded offset page used for deterministic graph traversal. */
|
|
145
|
+
const artifactPageSchema = (item) => z.object({
|
|
146
|
+
items: z.array(item).max(500),
|
|
147
|
+
offset: z.number().int().min(0),
|
|
148
|
+
limit: z.number().int().min(1).max(500),
|
|
149
|
+
total: z.number().int().min(0),
|
|
150
|
+
next_offset: z.number().int().min(0).nullable(),
|
|
151
|
+
});
|
|
152
|
+
/** Inventory observation containing stable manifest identity and bounded pages. */
|
|
153
|
+
export const artifactInventoryResultSchema = z.object({
|
|
154
|
+
manifest: artifactGraphManifestSchema,
|
|
155
|
+
nodes: artifactPageSchema(artifactNodeSchema),
|
|
156
|
+
occurrences: artifactPageSchema(artifactOccurrenceSchema),
|
|
157
|
+
edges: artifactPageSchema(artifactEdgeSchema),
|
|
158
|
+
limits: artifactTraversalLimitsSchema,
|
|
159
|
+
provenance: z.array(artifactCommandSchema).max(256),
|
|
160
|
+
limitations: z.array(z.string()),
|
|
161
|
+
});
|
|
162
|
+
/** One safely materialized artifact beneath caller-approved output root. */
|
|
163
|
+
const extractedArtifactSchema = z.object({
|
|
164
|
+
artifact_id: artifactIdSchema,
|
|
165
|
+
relative_path: boundedRelativePathSchema,
|
|
166
|
+
sha256: sha256Schema,
|
|
167
|
+
bytes_written: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
168
|
+
created: z.boolean(),
|
|
169
|
+
});
|
|
170
|
+
/** Path-independent commitment to one selected extraction operation. */
|
|
171
|
+
const artifactExtractionManifestSchema = z.object({
|
|
172
|
+
schema_version: z.literal(1),
|
|
173
|
+
extraction_id: extractionIdSchema,
|
|
174
|
+
source_manifest_id: manifestIdSchema,
|
|
175
|
+
selected_occurrence_ids: z.array(occurrenceIdSchema).min(1).max(500),
|
|
176
|
+
files_sha256: sha256Schema,
|
|
177
|
+
output_root_alias: z.literal("$OUTPUT_ROOT"),
|
|
178
|
+
});
|
|
179
|
+
/** Safe extraction observation with containment and cleanup verification. */
|
|
180
|
+
export const artifactExtractionResultSchema = z.object({
|
|
181
|
+
manifest: artifactGraphManifestSchema,
|
|
182
|
+
extraction_manifest: artifactExtractionManifestSchema,
|
|
183
|
+
output_root: z.string().min(1),
|
|
184
|
+
artifacts: artifactPageSchema(extractedArtifactSchema),
|
|
185
|
+
containment_verified: z.boolean(),
|
|
186
|
+
cleanup: z.object({
|
|
187
|
+
attempted: z.boolean(),
|
|
188
|
+
verified: z.boolean(),
|
|
189
|
+
residual_paths: z.array(boundedRelativePathSchema).max(500),
|
|
190
|
+
}),
|
|
191
|
+
limits: artifactTraversalLimitsSchema,
|
|
192
|
+
provenance: z.array(artifactCommandSchema).max(256),
|
|
193
|
+
limitations: z.array(z.string()),
|
|
194
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import canonicalize from "canonicalize";
|
|
3
|
+
import { artifactInventoryResultSchema, } from "./artifactGraph.js";
|
|
4
|
+
import { parseEvidence } from "./evidence.js";
|
|
5
|
+
/** Parse, merge, and verify a bounded set of inventory Evidence pages. */
|
|
6
|
+
export const parseArtifactInventoryEvidence = (input) => assembleInventorySet((Array.isArray(input) ? input : [input]).map((value) => {
|
|
7
|
+
const evidence = parseEvidence(value);
|
|
8
|
+
if (evidence.operation !== "inventory_artifact")
|
|
9
|
+
throw new TypeError("Artifact comparison requires inventory_artifact Evidence");
|
|
10
|
+
const inventory = artifactInventoryResultSchema.parse(evidence.normalized_result);
|
|
11
|
+
if (evidence.subject === null ||
|
|
12
|
+
evidence.subject.digest.sha256 !== inventory.manifest.root_sha256 ||
|
|
13
|
+
evidence.subject.format !== inventory.manifest.root_format)
|
|
14
|
+
throw new TypeError("Artifact inventory Evidence subject does not match its root digest");
|
|
15
|
+
validateInventoryPage(inventory);
|
|
16
|
+
return { evidence, inventory };
|
|
17
|
+
}));
|
|
18
|
+
const validateInventoryPage = (inventory) => {
|
|
19
|
+
assertUnique(inventory.nodes.items.map(({ artifact_id: id }) => id), "node ID");
|
|
20
|
+
assertUnique(inventory.occurrences.items.map(({ occurrence_id: id }) => id), "occurrence ID");
|
|
21
|
+
assertUnique(inventory.occurrences.items.map(({ logical_path: path }) => path), "logical path");
|
|
22
|
+
assertUnique(inventory.edges.items.map(({ edge_id: id }) => id), "edge ID");
|
|
23
|
+
for (const node of inventory.nodes.items)
|
|
24
|
+
if (node.artifact_id !==
|
|
25
|
+
`art_${digestCanonical({ schema_version: 1, sha256: node.sha256 })}`)
|
|
26
|
+
throw new TypeError("Artifact node ID is not content-addressed");
|
|
27
|
+
};
|
|
28
|
+
const assembleInventorySet = (pages) => {
|
|
29
|
+
if (pages.length === 0 || pages.length > 100)
|
|
30
|
+
throw new TypeError("Artifact comparison requires 1 to 100 Evidence pages");
|
|
31
|
+
const first = pages[0];
|
|
32
|
+
if (first === undefined)
|
|
33
|
+
throw new TypeError("Artifact comparison requires Evidence pages");
|
|
34
|
+
const manifestJson = canonicalJson(first.inventory.manifest);
|
|
35
|
+
const nodes = new Map();
|
|
36
|
+
const occurrences = new Map();
|
|
37
|
+
const paths = new Map();
|
|
38
|
+
const edges = new Map();
|
|
39
|
+
for (const page of pages) {
|
|
40
|
+
if (canonicalJson(page.inventory.manifest) !== manifestJson)
|
|
41
|
+
throw new TypeError("Artifact inventory pages do not share one manifest");
|
|
42
|
+
for (const node of page.inventory.nodes.items)
|
|
43
|
+
mergeExact(nodes, node.artifact_id, node, "node");
|
|
44
|
+
for (const occurrence of page.inventory.occurrences.items) {
|
|
45
|
+
mergeExact(occurrences, occurrence.occurrence_id, occurrence, "occurrence");
|
|
46
|
+
mergeExact(paths, occurrence.logical_path, occurrence, "logical path");
|
|
47
|
+
}
|
|
48
|
+
for (const edge of page.inventory.edges.items)
|
|
49
|
+
mergeExact(edges, edge.edge_id, edge, "edge");
|
|
50
|
+
}
|
|
51
|
+
const inventory = orderedInventory(first.inventory, pages, {
|
|
52
|
+
nodes,
|
|
53
|
+
occurrences,
|
|
54
|
+
edges,
|
|
55
|
+
});
|
|
56
|
+
if (inventory.complete)
|
|
57
|
+
validateCompleteInventory(inventory);
|
|
58
|
+
return { evidence: pages.map(({ evidence }) => evidence), inventory };
|
|
59
|
+
};
|
|
60
|
+
const orderedInventory = (first, pages, values) => {
|
|
61
|
+
const nodes = [...values.nodes.values()].sort((left, right) => left.artifact_id.localeCompare(right.artifact_id));
|
|
62
|
+
const occurrences = [...values.occurrences.values()].sort((left, right) => left.logical_path.localeCompare(right.logical_path, "en"));
|
|
63
|
+
const edges = [...values.edges.values()].sort((left, right) => left.edge_id.localeCompare(right.edge_id));
|
|
64
|
+
return {
|
|
65
|
+
manifest: first.manifest,
|
|
66
|
+
nodes,
|
|
67
|
+
occurrences,
|
|
68
|
+
edges,
|
|
69
|
+
limitations: [
|
|
70
|
+
...new Set(pages.flatMap(({ inventory }) => inventory.limitations)),
|
|
71
|
+
].sort((left, right) => left.localeCompare(right)),
|
|
72
|
+
complete: nodes.length === first.manifest.node_count &&
|
|
73
|
+
occurrences.length === first.manifest.occurrence_count &&
|
|
74
|
+
edges.length === first.manifest.edge_count,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const validateCompleteInventory = (inventory) => {
|
|
78
|
+
const nodeIds = new Set(inventory.nodes.map(({ artifact_id: id }) => id));
|
|
79
|
+
const occurrenceIds = new Set(inventory.occurrences.map(({ occurrence_id: id }) => id));
|
|
80
|
+
const rootNode = inventory.nodes.find(({ artifact_id: id }) => id === inventory.manifest.root_artifact_id);
|
|
81
|
+
if (rootNode?.sha256 !== inventory.manifest.root_sha256)
|
|
82
|
+
throw new TypeError("Artifact root node does not match its manifest digest");
|
|
83
|
+
for (const occurrence of inventory.occurrences)
|
|
84
|
+
validateOccurrence(occurrence, inventory, nodeIds, occurrenceIds);
|
|
85
|
+
for (const edge of inventory.edges)
|
|
86
|
+
validateEdge(edge, nodeIds, occurrenceIds);
|
|
87
|
+
const graphSha256 = digestCanonical({
|
|
88
|
+
nodes: inventory.nodes,
|
|
89
|
+
occurrences: inventory.occurrences,
|
|
90
|
+
edges: inventory.edges,
|
|
91
|
+
});
|
|
92
|
+
if (graphSha256 !== inventory.manifest.graph_sha256)
|
|
93
|
+
throw new TypeError("Artifact graph commitment does not match its members");
|
|
94
|
+
const manifestId = `agm_${digestCanonical({
|
|
95
|
+
schema_version: 1,
|
|
96
|
+
root_artifact_id: inventory.manifest.root_artifact_id,
|
|
97
|
+
graph_sha256: graphSha256,
|
|
98
|
+
})}`;
|
|
99
|
+
if (manifestId !== inventory.manifest.manifest_id)
|
|
100
|
+
throw new TypeError("Artifact manifest ID does not match its commitment");
|
|
101
|
+
};
|
|
102
|
+
const validateOccurrence = (occurrence, inventory, nodeIds, occurrenceIds) => {
|
|
103
|
+
if (occurrence.artifact_id !== null && !nodeIds.has(occurrence.artifact_id))
|
|
104
|
+
throw new TypeError("Artifact occurrence references a missing node");
|
|
105
|
+
if (occurrence.parent_occurrence_id !== null &&
|
|
106
|
+
!occurrenceIds.has(occurrence.parent_occurrence_id))
|
|
107
|
+
throw new TypeError("Artifact occurrence references a missing parent");
|
|
108
|
+
const expectedId = occurrence.logical_path === "."
|
|
109
|
+
? `occ_${digestCanonical({ root: inventory.manifest.root_artifact_id })}`
|
|
110
|
+
: `occ_${digestCanonical({
|
|
111
|
+
root_artifact_id: inventory.manifest.root_artifact_id,
|
|
112
|
+
logical_path: occurrence.logical_path,
|
|
113
|
+
entry_kind: occurrence.entry_kind,
|
|
114
|
+
})}`;
|
|
115
|
+
if (occurrence.occurrence_id !== expectedId)
|
|
116
|
+
throw new TypeError("Artifact occurrence ID does not match its identity");
|
|
117
|
+
};
|
|
118
|
+
const validateEdge = (edge, nodeIds, occurrenceIds) => {
|
|
119
|
+
if (!nodeIds.has(edge.parent_artifact_id) ||
|
|
120
|
+
!nodeIds.has(edge.child_artifact_id) ||
|
|
121
|
+
!occurrenceIds.has(edge.occurrence_id))
|
|
122
|
+
throw new TypeError("Artifact edge references a missing graph member");
|
|
123
|
+
const semantic = {
|
|
124
|
+
parent_artifact_id: edge.parent_artifact_id,
|
|
125
|
+
child_artifact_id: edge.child_artifact_id,
|
|
126
|
+
relation: edge.relation,
|
|
127
|
+
occurrence_id: edge.occurrence_id,
|
|
128
|
+
logical_path: edge.logical_path,
|
|
129
|
+
};
|
|
130
|
+
if (edge.edge_id !== `edge_${digestCanonical(semantic)}`)
|
|
131
|
+
throw new TypeError("Artifact edge ID does not match its identity");
|
|
132
|
+
};
|
|
133
|
+
const mergeExact = (output, key, value, label) => {
|
|
134
|
+
const previous = output.get(key);
|
|
135
|
+
if (previous !== undefined &&
|
|
136
|
+
canonicalJson(previous) !== canonicalJson(value))
|
|
137
|
+
throw new TypeError(`Artifact inventory has conflicting ${label} pages`);
|
|
138
|
+
output.set(key, value);
|
|
139
|
+
};
|
|
140
|
+
const assertUnique = (values, label) => {
|
|
141
|
+
if (new Set(values).size !== values.length)
|
|
142
|
+
throw new TypeError(`Artifact inventory contains a duplicate ${label}`);
|
|
143
|
+
};
|
|
144
|
+
const digestCanonical = (value) => createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
145
|
+
const canonicalJson = (value) => {
|
|
146
|
+
const encoded = canonicalize(value);
|
|
147
|
+
if (encoded === undefined)
|
|
148
|
+
throw new TypeError("Artifact inventory could not canonicalize graph data");
|
|
149
|
+
return encoded;
|
|
150
|
+
};
|
|
@@ -28,6 +28,7 @@ export const parseBinaryTarget = async (input, cwd = process.cwd(), hostArchitec
|
|
|
28
28
|
(targetKind === undefined && path.toLowerCase().endsWith(".hop")))
|
|
29
29
|
return ok({
|
|
30
30
|
path,
|
|
31
|
+
sourcePath: canonical,
|
|
31
32
|
sha256: await sha256File(path),
|
|
32
33
|
kind: "database",
|
|
33
34
|
format: "hopper",
|
|
@@ -36,6 +37,16 @@ export const parseBinaryTarget = async (input, cwd = process.cwd(), hostArchitec
|
|
|
36
37
|
const handle = await open(path, "r");
|
|
37
38
|
let detected;
|
|
38
39
|
try {
|
|
40
|
+
const artifactFormat = await detectArtifactFormat(path, handle);
|
|
41
|
+
if (artifactFormat !== undefined)
|
|
42
|
+
return ok({
|
|
43
|
+
path,
|
|
44
|
+
sourcePath: canonical,
|
|
45
|
+
sha256: await sha256File(path),
|
|
46
|
+
kind: isArchiveFormat(artifactFormat) ? "archive" : "artifact",
|
|
47
|
+
format: artifactFormat,
|
|
48
|
+
loaderArgs: [],
|
|
49
|
+
});
|
|
39
50
|
detected = await readExecutableMetadata(handle, hostArchitecture);
|
|
40
51
|
}
|
|
41
52
|
finally {
|
|
@@ -45,6 +56,7 @@ export const parseBinaryTarget = async (input, cwd = process.cwd(), hostArchitec
|
|
|
45
56
|
return err(new BinaryTargetError(path, detected.error));
|
|
46
57
|
return ok({
|
|
47
58
|
path,
|
|
59
|
+
sourcePath: canonical,
|
|
48
60
|
sha256: await sha256File(path),
|
|
49
61
|
kind: "executable",
|
|
50
62
|
...detected.value,
|
|
@@ -54,6 +66,49 @@ export const parseBinaryTarget = async (input, cwd = process.cwd(), hostArchitec
|
|
|
54
66
|
return err(new BinaryTargetError(candidate, "path is not readable", { cause }));
|
|
55
67
|
}
|
|
56
68
|
};
|
|
69
|
+
const detectArtifactFormat = async (path, handle) => {
|
|
70
|
+
const lower = path.toLowerCase();
|
|
71
|
+
const magic = Buffer.alloc(8);
|
|
72
|
+
const observed = await handle.read(magic, 0, magic.length, 0);
|
|
73
|
+
if (observed.bytesRead >= 4 &&
|
|
74
|
+
magic[0] === 0x50 &&
|
|
75
|
+
magic[1] === 0x4b &&
|
|
76
|
+
[0x03, 0x05, 0x07].includes(magic[2] ?? -1) &&
|
|
77
|
+
[0x04, 0x06, 0x08].includes(magic[3] ?? -1)) {
|
|
78
|
+
return lower.endsWith(".ipa")
|
|
79
|
+
? "ipa"
|
|
80
|
+
: lower.endsWith(".apk")
|
|
81
|
+
? "apk"
|
|
82
|
+
: "zip";
|
|
83
|
+
}
|
|
84
|
+
const named = namedArtifactFormat(lower);
|
|
85
|
+
if (named !== undefined)
|
|
86
|
+
return named;
|
|
87
|
+
if (lower.endsWith(".pkg") &&
|
|
88
|
+
observed.bytesRead >= 4 &&
|
|
89
|
+
magic.subarray(0, 4).toString("ascii") === "xar!")
|
|
90
|
+
return "pkg";
|
|
91
|
+
if (lower.endsWith(".dmg")) {
|
|
92
|
+
const size = (await handle.stat()).size;
|
|
93
|
+
if (size >= 512) {
|
|
94
|
+
const trailer = Buffer.alloc(4);
|
|
95
|
+
const read = await handle.read(trailer, 0, trailer.length, size - 512);
|
|
96
|
+
if (read.bytesRead === 4 && trailer.toString("ascii") === "koly")
|
|
97
|
+
return "dmg";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
};
|
|
102
|
+
const namedArtifactFormat = (lowerPath) => {
|
|
103
|
+
if (lowerPath.endsWith(".asar"))
|
|
104
|
+
return "asar";
|
|
105
|
+
if (lowerPath.endsWith(".plist"))
|
|
106
|
+
return "plist";
|
|
107
|
+
if (lowerPath.endsWith(".map"))
|
|
108
|
+
return "source-map";
|
|
109
|
+
return /\.(?:m?js|cjs)$/u.test(lowerPath) ? "javascript" : undefined;
|
|
110
|
+
};
|
|
111
|
+
const isArchiveFormat = (format) => ["zip", "ipa", "apk", "asar", "dmg", "pkg"].includes(format);
|
|
57
112
|
const sha256File = async (path) => {
|
|
58
113
|
const hash = createHash("sha256");
|
|
59
114
|
for await (const chunk of createReadStream(path))
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { evidenceBundleSchema, parseEvidenceBundle, serializeEvidenceBundle, } from "./evidenceBundle.js";
|
|
4
|
+
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
|
|
5
|
+
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
6
|
+
const classificationSchema = z.enum([
|
|
7
|
+
"added",
|
|
8
|
+
"removed",
|
|
9
|
+
"changed",
|
|
10
|
+
"unknown",
|
|
11
|
+
"history_advanced",
|
|
12
|
+
"history_diverged",
|
|
13
|
+
]);
|
|
14
|
+
/** Strict bounded input for canonical Evidence bundle comparison. */
|
|
15
|
+
export const bundleComparisonInputSchema = z.object({
|
|
16
|
+
left: evidenceBundleSchema,
|
|
17
|
+
right: evidenceBundleSchema,
|
|
18
|
+
record_pairs: z
|
|
19
|
+
.array(z.object({
|
|
20
|
+
left_evidence_id: evidenceIdSchema,
|
|
21
|
+
right_evidence_id: evidenceIdSchema,
|
|
22
|
+
}))
|
|
23
|
+
.max(500)
|
|
24
|
+
.default([]),
|
|
25
|
+
offset: z.number().int().min(0).default(0),
|
|
26
|
+
limit: z.number().int().min(1).max(500).default(100),
|
|
27
|
+
});
|
|
28
|
+
const bundleChangeSchema = z.object({
|
|
29
|
+
entity: z.enum(["evidence", "residual_unknown"]),
|
|
30
|
+
key: z.string().min(1),
|
|
31
|
+
classification: classificationSchema,
|
|
32
|
+
conclusion_kind: z.enum([
|
|
33
|
+
"observed_change",
|
|
34
|
+
"contradiction",
|
|
35
|
+
"unresolved_branch",
|
|
36
|
+
]),
|
|
37
|
+
left_evidence_ids: z.array(evidenceIdSchema),
|
|
38
|
+
right_evidence_ids: z.array(evidenceIdSchema),
|
|
39
|
+
left_digest: digestSchema.nullable(),
|
|
40
|
+
right_digest: digestSchema.nullable(),
|
|
41
|
+
limitations: z.array(z.string()),
|
|
42
|
+
});
|
|
43
|
+
/** Deterministic classified delta between two canonical Evidence bundles. */
|
|
44
|
+
export const bundleComparisonResultSchema = z.object({
|
|
45
|
+
status: z.enum(["unchanged", "changed", "unknown"]),
|
|
46
|
+
left_bundle_sha256: digestSchema,
|
|
47
|
+
right_bundle_sha256: digestSchema,
|
|
48
|
+
summary: z.object({
|
|
49
|
+
records_unchanged: z.number().int().min(0),
|
|
50
|
+
records_added: z.number().int().min(0),
|
|
51
|
+
records_removed: z.number().int().min(0),
|
|
52
|
+
records_changed: z.number().int().min(0),
|
|
53
|
+
unknowns_unchanged: z.number().int().min(0),
|
|
54
|
+
unknowns_added: z.number().int().min(0),
|
|
55
|
+
unknowns_removed: z.number().int().min(0),
|
|
56
|
+
unknowns_advanced: z.number().int().min(0),
|
|
57
|
+
unknowns_diverged: z.number().int().min(0),
|
|
58
|
+
unresolved: z.number().int().min(0),
|
|
59
|
+
}),
|
|
60
|
+
changes: z.object({
|
|
61
|
+
items: z.array(bundleChangeSchema).max(500),
|
|
62
|
+
offset: z.number().int().min(0),
|
|
63
|
+
limit: z.number().int().min(1).max(500),
|
|
64
|
+
total: z.number().int().min(0),
|
|
65
|
+
next_offset: z.number().int().min(0).nullable(),
|
|
66
|
+
}),
|
|
67
|
+
limitations: z.array(z.string()),
|
|
68
|
+
});
|
|
69
|
+
const MAX_ENTITIES = 100_000;
|
|
70
|
+
/** Compare canonical bundle membership and only explicitly paired observations. */
|
|
71
|
+
export const compareBundles = (...[leftInput, rightInput, recordPairs, offset, limit]) => {
|
|
72
|
+
const left = parseEvidenceBundle(leftInput);
|
|
73
|
+
const right = parseEvidenceBundle(rightInput);
|
|
74
|
+
enforceBounds(left, right);
|
|
75
|
+
const leftDigest = bundleDigest(left);
|
|
76
|
+
const rightDigest = bundleDigest(right);
|
|
77
|
+
const recordResult = compareRecords(left, right, recordPairs);
|
|
78
|
+
const unknownResult = compareUnknowns(left, right);
|
|
79
|
+
const changes = [...recordResult.changes, ...unknownResult.changes].sort(compareChanges);
|
|
80
|
+
const page = changes.slice(offset, offset + limit);
|
|
81
|
+
const unresolved = changes.filter(({ conclusion_kind: kind }) => kind !== "observed_change").length;
|
|
82
|
+
return bundleComparisonResultSchema.parse({
|
|
83
|
+
status: leftDigest === rightDigest && changes.length === 0
|
|
84
|
+
? "unchanged"
|
|
85
|
+
: unresolved > 0
|
|
86
|
+
? "unknown"
|
|
87
|
+
: "changed",
|
|
88
|
+
left_bundle_sha256: leftDigest,
|
|
89
|
+
right_bundle_sha256: rightDigest,
|
|
90
|
+
summary: {
|
|
91
|
+
...recordResult.summary,
|
|
92
|
+
...unknownResult.summary,
|
|
93
|
+
unresolved,
|
|
94
|
+
},
|
|
95
|
+
changes: {
|
|
96
|
+
items: page,
|
|
97
|
+
offset,
|
|
98
|
+
limit,
|
|
99
|
+
total: changes.length,
|
|
100
|
+
next_offset: offset + page.length < changes.length ? offset + page.length : null,
|
|
101
|
+
},
|
|
102
|
+
limitations: changes.some(({ limitations }) => limitations.length > 0)
|
|
103
|
+
? [
|
|
104
|
+
"One-sided membership proves only bundle inclusion or omission, not behavioral absence.",
|
|
105
|
+
]
|
|
106
|
+
: [],
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
const compareRecords = (left, right, pairs) => {
|
|
110
|
+
const leftIds = new Set(left.records.map(({ evidence_id: id }) => id));
|
|
111
|
+
const rightIds = new Set(right.records.map(({ evidence_id: id }) => id));
|
|
112
|
+
const pairedLeft = new Set();
|
|
113
|
+
const pairedRight = new Set();
|
|
114
|
+
const changes = [];
|
|
115
|
+
let changed = 0;
|
|
116
|
+
for (const pair of pairs) {
|
|
117
|
+
if (!leftIds.has(pair.left_evidence_id))
|
|
118
|
+
throw new TypeError(`Explicit pair references missing left evidence ${pair.left_evidence_id}`);
|
|
119
|
+
if (!rightIds.has(pair.right_evidence_id))
|
|
120
|
+
throw new TypeError(`Explicit pair references missing right evidence ${pair.right_evidence_id}`);
|
|
121
|
+
if (pairedLeft.has(pair.left_evidence_id) ||
|
|
122
|
+
pairedRight.has(pair.right_evidence_id))
|
|
123
|
+
throw new TypeError("Explicit evidence pairs must be one-to-one");
|
|
124
|
+
pairedLeft.add(pair.left_evidence_id);
|
|
125
|
+
pairedRight.add(pair.right_evidence_id);
|
|
126
|
+
if (pair.left_evidence_id === pair.right_evidence_id)
|
|
127
|
+
continue;
|
|
128
|
+
changed += 1;
|
|
129
|
+
changes.push({
|
|
130
|
+
entity: "evidence",
|
|
131
|
+
key: `pair:${pair.left_evidence_id}:${pair.right_evidence_id}`,
|
|
132
|
+
classification: "changed",
|
|
133
|
+
conclusion_kind: "observed_change",
|
|
134
|
+
left_evidence_ids: [pair.left_evidence_id],
|
|
135
|
+
right_evidence_ids: [pair.right_evidence_id],
|
|
136
|
+
left_digest: pair.left_evidence_id.slice(3),
|
|
137
|
+
right_digest: pair.right_evidence_id.slice(3),
|
|
138
|
+
limitations: [],
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const common = [...leftIds].filter((id) => rightIds.has(id) && !pairedLeft.has(id) && !pairedRight.has(id));
|
|
142
|
+
const removed = [...leftIds].filter((id) => !rightIds.has(id) && !pairedLeft.has(id));
|
|
143
|
+
const added = [...rightIds].filter((id) => !leftIds.has(id) && !pairedRight.has(id));
|
|
144
|
+
for (const [classification, ids, side] of [
|
|
145
|
+
["removed", removed, "left"],
|
|
146
|
+
["added", added, "right"],
|
|
147
|
+
])
|
|
148
|
+
for (const id of ids)
|
|
149
|
+
changes.push({
|
|
150
|
+
entity: "evidence",
|
|
151
|
+
key: id,
|
|
152
|
+
classification,
|
|
153
|
+
conclusion_kind: "observed_change",
|
|
154
|
+
left_evidence_ids: side === "left" ? [id] : [],
|
|
155
|
+
right_evidence_ids: side === "right" ? [id] : [],
|
|
156
|
+
left_digest: side === "left" ? id.slice(3) : null,
|
|
157
|
+
right_digest: side === "right" ? id.slice(3) : null,
|
|
158
|
+
limitations: [
|
|
159
|
+
"One-sided membership does not establish behavioral absence.",
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
changes,
|
|
164
|
+
summary: {
|
|
165
|
+
records_unchanged: common.length + pairs.length - changed,
|
|
166
|
+
records_added: added.length,
|
|
167
|
+
records_removed: removed.length,
|
|
168
|
+
records_changed: changed,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
const compareUnknowns = (left, right) => {
|
|
173
|
+
const leftHistories = histories(left.unknowns);
|
|
174
|
+
const rightHistories = histories(right.unknowns);
|
|
175
|
+
const ids = [...new Set([...leftHistories.keys(), ...rightHistories.keys()])];
|
|
176
|
+
const changes = [];
|
|
177
|
+
const summary = {
|
|
178
|
+
unknowns_unchanged: 0,
|
|
179
|
+
unknowns_added: 0,
|
|
180
|
+
unknowns_removed: 0,
|
|
181
|
+
unknowns_advanced: 0,
|
|
182
|
+
unknowns_diverged: 0,
|
|
183
|
+
};
|
|
184
|
+
for (const id of ids) {
|
|
185
|
+
const leftHistory = leftHistories.get(id);
|
|
186
|
+
const rightHistory = rightHistories.get(id);
|
|
187
|
+
if (leftHistory === undefined || rightHistory === undefined) {
|
|
188
|
+
const added = leftHistory === undefined;
|
|
189
|
+
summary[added ? "unknowns_added" : "unknowns_removed"] += 1;
|
|
190
|
+
changes.push(unknownChange(id, added ? "added" : "removed", leftHistory, rightHistory));
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const leftDigests = leftHistory.map(({ revision_digest: digest }) => digest);
|
|
194
|
+
const rightDigests = rightHistory.map(({ revision_digest: digest }) => digest);
|
|
195
|
+
if (same(leftDigests, rightDigests)) {
|
|
196
|
+
summary.unknowns_unchanged += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const advanced = isPrefix(leftDigests, rightDigests) ||
|
|
200
|
+
isPrefix(rightDigests, leftDigests);
|
|
201
|
+
summary[advanced ? "unknowns_advanced" : "unknowns_diverged"] += 1;
|
|
202
|
+
changes.push(unknownChange(id, advanced ? "history_advanced" : "history_diverged", leftHistory, rightHistory));
|
|
203
|
+
}
|
|
204
|
+
return { changes, summary };
|
|
205
|
+
};
|
|
206
|
+
const unknownChange = (id, classification, left, right) => ({
|
|
207
|
+
entity: "residual_unknown",
|
|
208
|
+
key: id,
|
|
209
|
+
classification,
|
|
210
|
+
conclusion_kind: classification === "history_diverged"
|
|
211
|
+
? "contradiction"
|
|
212
|
+
: classification === "history_advanced"
|
|
213
|
+
? "observed_change"
|
|
214
|
+
: "unresolved_branch",
|
|
215
|
+
left_evidence_ids: historyEvidenceIds(left ?? []),
|
|
216
|
+
right_evidence_ids: historyEvidenceIds(right ?? []),
|
|
217
|
+
left_digest: left?.at(-1)?.revision_digest ?? null,
|
|
218
|
+
right_digest: right?.at(-1)?.revision_digest ?? null,
|
|
219
|
+
limitations: left === undefined || right === undefined
|
|
220
|
+
? ["A missing unknown history does not establish resolution."]
|
|
221
|
+
: [],
|
|
222
|
+
});
|
|
223
|
+
const histories = (unknowns) => {
|
|
224
|
+
const output = new Map();
|
|
225
|
+
for (const unknown of unknowns) {
|
|
226
|
+
const history = output.get(unknown.unknown_id) ?? [];
|
|
227
|
+
history.push(unknown);
|
|
228
|
+
output.set(unknown.unknown_id, history);
|
|
229
|
+
}
|
|
230
|
+
return output;
|
|
231
|
+
};
|
|
232
|
+
const historyEvidenceIds = (history) => [
|
|
233
|
+
...new Set(history.flatMap((item) => [
|
|
234
|
+
...item.supporting_evidence_ids,
|
|
235
|
+
...item.contradicting_evidence_ids,
|
|
236
|
+
...item.mutation_evidence_ids,
|
|
237
|
+
...(item.resolution?.evidence_ids ?? []),
|
|
238
|
+
])),
|
|
239
|
+
].sort(ascii);
|
|
240
|
+
const enforceBounds = (left, right) => {
|
|
241
|
+
const total = left.records.length +
|
|
242
|
+
right.records.length +
|
|
243
|
+
left.unknowns.length +
|
|
244
|
+
right.unknowns.length;
|
|
245
|
+
if (total > MAX_ENTITIES)
|
|
246
|
+
throw new TypeError(`Bundle comparison exceeds ${MAX_ENTITIES} entities`);
|
|
247
|
+
};
|
|
248
|
+
const bundleDigest = (bundle) => createHash("sha256").update(serializeEvidenceBundle(bundle)).digest("hex");
|
|
249
|
+
const same = (left, right) => left.length === right.length &&
|
|
250
|
+
left.every((value, index) => value === right[index]);
|
|
251
|
+
const isPrefix = (shorter, longer) => shorter.length < longer.length &&
|
|
252
|
+
shorter.every((value, index) => value === longer[index]);
|
|
253
|
+
const ascii = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
254
|
+
const compareChanges = (left, right) => ascii([
|
|
255
|
+
left.entity,
|
|
256
|
+
left.key,
|
|
257
|
+
left.classification,
|
|
258
|
+
left.left_digest ?? "",
|
|
259
|
+
left.right_digest ?? "",
|
|
260
|
+
].join("\u0000"), [
|
|
261
|
+
right.entity,
|
|
262
|
+
right.key,
|
|
263
|
+
right.classification,
|
|
264
|
+
right.left_digest ?? "",
|
|
265
|
+
right.right_digest ?? "",
|
|
266
|
+
].join("\u0000"));
|