rea-agents 1.2.0 → 1.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 +95 -27
- package/bridge/hopper_bridge.py +172 -14
- package/dist/application/AnalysisSnapshotCache.js +131 -0
- package/dist/application/AnalysisSnapshotFiles.js +31 -0
- package/dist/application/ArtifactGraphConstruction.js +1 -1
- package/dist/application/ArtifactInventory.js +100 -24
- package/dist/application/AuthorizedArtifactInventory.js +16 -0
- package/dist/application/BinarySession.js +149 -36
- package/dist/application/BinarySessionPort.js +1 -0
- package/dist/application/BoundedJsonFiles.js +109 -0
- package/dist/application/CapabilityInventory.js +127 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/CrossVersionInventory.js +109 -0
- package/dist/application/CrossVersionInvestigation.js +358 -0
- package/dist/application/DirectAnalysis.js +205 -24
- package/dist/application/Doctor.js +104 -3
- package/dist/application/EvidenceBundleFiles.js +15 -108
- package/dist/application/EvidenceLedger.js +3 -2
- package/dist/application/InvestigationProviders.js +48 -0
- package/dist/application/InvestigationWorkspaceStore.js +214 -0
- package/dist/application/LinuxHopper.js +72 -48
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureAuthority.js +2 -2
- package/dist/application/ProcessCaptureError.js +25 -0
- package/dist/application/ProcessCaptureLifecycle.js +22 -2
- package/dist/application/ProcessCli.js +111 -31
- package/dist/application/ProcessHarness.js +6 -3
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +110 -0
- package/dist/application/ReferenceSourceImportEntries.js +18 -3
- package/dist/application/ReferenceSourceImportTypes.js +32 -0
- package/dist/application/Setup.js +35 -101
- package/dist/application/SetupClients.js +19 -0
- package/dist/application/SetupInstallFailure.js +53 -0
- package/dist/application/SetupPlan.js +31 -0
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/Uninstall.js +34 -24
- package/dist/application/UnknownEvidence.js +33 -0
- package/dist/application/Upgrade.js +3 -1
- package/dist/application/runtime.js +2 -2
- package/dist/artifacts/ArtifactProvider.js +15 -8
- package/dist/catalogIdentity.js +121 -0
- package/dist/cli.js +76 -13
- package/dist/cliEvidenceCommands.js +52 -12
- package/dist/cliInvestigationCommands.js +137 -0
- package/dist/cliLogging.js +20 -3
- package/dist/cliOutput.js +41 -0
- package/dist/cliPolicyCommands.js +115 -0
- package/dist/config.js +102 -27
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/errorSchemas.js +98 -0
- package/dist/contracts/promptContracts.js +256 -0
- package/dist/contracts/sessionLifecycleInputs.js +11 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +20 -7
- package/dist/contracts/toolOutputSchemas.js +70 -9
- package/dist/domain/analysisSnapshot.js +149 -0
- package/dist/domain/artifactComparison.js +38 -11
- package/dist/domain/artifactGraph.js +25 -1
- package/dist/domain/artifactInventoryEvidence.js +18 -0
- package/dist/domain/changedBehavior.js +35 -10
- package/dist/domain/errors.js +384 -32
- package/dist/domain/evidence.js +4 -2
- package/dist/domain/evidenceBundle.js +28 -0
- package/dist/domain/hopperStartupFailure.js +55 -0
- package/dist/domain/investigationWorkspace.js +341 -0
- package/dist/domain/jsonValue.js +2 -0
- package/dist/domain/nativeInspection.js +3 -2
- package/dist/domain/permissionPolicy.js +157 -0
- package/dist/domain/processCapture.js +5 -4
- package/dist/domain/processComparison.js +6 -4
- package/dist/domain/reconstructionVerification.js +1 -1
- package/dist/domain/reconstructionVerificationSchemas.js +1 -0
- package/dist/domain/staticRuntimeCorrelation.js +5 -1
- package/dist/generatedPackageMetadata.js +9 -0
- package/dist/hopper/BridgeLauncher.js +51 -20
- package/dist/hopper/HopperClient.js +45 -3
- package/dist/hopper/HopperProvider.js +1 -0
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +139 -28
- package/dist/server/createServer.js +54 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/promptCompletion.js +144 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +20 -7
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +59 -8
- package/dist/server/registerFunctionComparisonTool.js +7 -3
- package/dist/server/registerInvestigationTools.js +111 -18
- package/dist/server/registerOfficialTools.js +25 -14
- package/dist/server/registerProcessComparisonTool.js +8 -10
- package/dist/server/registerPrompts.js +67 -0
- package/dist/server/registerSessionStatusTool.js +52 -0
- package/dist/server/registerSessionTools.js +178 -22
- package/dist/server/runDerivedOperation.js +35 -0
- package/dist/server/sessionEvidence.js +2 -8
- package/dist/server/sessionToolPolicies.js +1 -42
- package/dist/server/toolResult.js +34 -6
- package/dist/serverIdentity.js +70 -0
- package/install.sh +11 -11
- package/package.json +10 -6
- package/scripts/hopper-demo-x11.py +349 -0
- package/scripts/rea.mjs +6 -3
- package/skills/rea-analysis/SKILL.md +9 -2
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import canonicalize from "canonicalize";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { evidenceBundleForTarget, evidenceBundleSchema, parseEvidenceBundle, } from "./evidenceBundle.js";
|
|
5
|
+
import { evidenceLocationSchema, providerSchema, } from "./evidence.js";
|
|
6
|
+
import { jsonObjectSchema, jsonValueSchema, } from "./jsonValue.js";
|
|
7
|
+
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
8
|
+
const architectureSchema = z.enum(["x86", "x86_64", "arm", "arm64"]);
|
|
9
|
+
const formatSchema = z.enum([
|
|
10
|
+
"hopper",
|
|
11
|
+
"mach-o",
|
|
12
|
+
"elf",
|
|
13
|
+
"pe",
|
|
14
|
+
"zip",
|
|
15
|
+
"ipa",
|
|
16
|
+
"apk",
|
|
17
|
+
"asar",
|
|
18
|
+
"dmg",
|
|
19
|
+
"pkg",
|
|
20
|
+
"plist",
|
|
21
|
+
"javascript",
|
|
22
|
+
"source-map",
|
|
23
|
+
]);
|
|
24
|
+
const subjectSchema = z.object({
|
|
25
|
+
path: z.string().min(1),
|
|
26
|
+
sha256: digestSchema,
|
|
27
|
+
format: z.enum([
|
|
28
|
+
...formatSchema.options,
|
|
29
|
+
"directory",
|
|
30
|
+
"file",
|
|
31
|
+
"unknown",
|
|
32
|
+
"mach-o-universal",
|
|
33
|
+
"javascript-bundle",
|
|
34
|
+
"entitlements",
|
|
35
|
+
]),
|
|
36
|
+
architecture: architectureSchema.nullable(),
|
|
37
|
+
});
|
|
38
|
+
const targetSchema = z.object({
|
|
39
|
+
sha256: digestSchema,
|
|
40
|
+
format: formatSchema,
|
|
41
|
+
architecture: architectureSchema.nullable(),
|
|
42
|
+
loader_args: z.array(z.string()),
|
|
43
|
+
});
|
|
44
|
+
const entrySchema = z.object({
|
|
45
|
+
query_id: z.string().regex(/^query_[a-f0-9]{64}$/u),
|
|
46
|
+
operation: z.string().min(1),
|
|
47
|
+
parameters: jsonObjectSchema,
|
|
48
|
+
execution: z.object({
|
|
49
|
+
result: jsonValueSchema,
|
|
50
|
+
raw_result: jsonValueSchema.nullable(),
|
|
51
|
+
provider: providerSchema,
|
|
52
|
+
limitations: z.array(z.string()),
|
|
53
|
+
locations: z.array(evidenceLocationSchema),
|
|
54
|
+
subject: subjectSchema.nullable(),
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
/** Provider-neutral, deterministic cache of successful immutable analysis calls. */
|
|
58
|
+
const analysisSnapshotSchema = z.object({
|
|
59
|
+
snapshot_version: z.literal(1),
|
|
60
|
+
target: targetSchema,
|
|
61
|
+
entries: z.array(entrySchema).max(10_000),
|
|
62
|
+
evidence_bundle: evidenceBundleSchema,
|
|
63
|
+
});
|
|
64
|
+
const canonicalJson = (value) => {
|
|
65
|
+
const json = jsonValueSchema.parse(value);
|
|
66
|
+
const encoded = canonicalize(json);
|
|
67
|
+
if (encoded === undefined)
|
|
68
|
+
throw new TypeError("RFC 8785 canonicalization rejected snapshot data");
|
|
69
|
+
return encoded;
|
|
70
|
+
};
|
|
71
|
+
/** Project a binary into the path-free identity used for cache invalidation. */
|
|
72
|
+
export const snapshotTarget = (target) => ({
|
|
73
|
+
sha256: target.sha256,
|
|
74
|
+
format: target.format,
|
|
75
|
+
architecture: target.architecture ?? null,
|
|
76
|
+
loader_args: [...target.loaderArgs],
|
|
77
|
+
});
|
|
78
|
+
/** Compare an open binary with the immutable identity in a snapshot. */
|
|
79
|
+
export const snapshotMatchesTarget = (snapshot, target) => snapshot.sha256 === target.sha256 &&
|
|
80
|
+
snapshot.format === target.format &&
|
|
81
|
+
snapshot.architecture === (target.architecture ?? null) &&
|
|
82
|
+
JSON.stringify(snapshot.loader_args) === JSON.stringify(target.loaderArgs);
|
|
83
|
+
/** Find exact persisted CLI evidence without starting an analysis provider. */
|
|
84
|
+
export const snapshotEvidenceForQuery = (snapshot, query) => {
|
|
85
|
+
const { target, operation, parameters, provider } = query;
|
|
86
|
+
if (!snapshotMatchesTarget(snapshot.target, target))
|
|
87
|
+
return undefined;
|
|
88
|
+
const encodedParameters = canonicalJson(parameters);
|
|
89
|
+
return snapshot.evidence_bundle.records.find((record) => record.subject?.digest.sha256 === target.sha256 &&
|
|
90
|
+
record.operation === operation &&
|
|
91
|
+
record.provider.id === provider.id &&
|
|
92
|
+
record.provider.name === provider.name &&
|
|
93
|
+
record.provider.version === provider.version &&
|
|
94
|
+
canonicalJson(record.parameters) === encodedParameters);
|
|
95
|
+
};
|
|
96
|
+
/** Compute the stable lookup key for one provider-specific analysis query. */
|
|
97
|
+
export const analysisQueryId = (target, operation, parameters, provider) => `query_${createHash("sha256")
|
|
98
|
+
.update(canonicalJson({
|
|
99
|
+
target,
|
|
100
|
+
operation,
|
|
101
|
+
parameters,
|
|
102
|
+
provider: {
|
|
103
|
+
id: provider.id,
|
|
104
|
+
name: provider.name,
|
|
105
|
+
version: provider.version,
|
|
106
|
+
},
|
|
107
|
+
}))
|
|
108
|
+
.digest("hex")}`;
|
|
109
|
+
/** Create one serializable snapshot entry from a successful provider call. */
|
|
110
|
+
export const createAnalysisSnapshotEntry = (target, operation, parameters, execution) => ({
|
|
111
|
+
query_id: analysisQueryId(target, operation, parameters, execution.provider),
|
|
112
|
+
operation,
|
|
113
|
+
parameters: jsonObjectSchema.parse(parameters),
|
|
114
|
+
execution: {
|
|
115
|
+
result: execution.result,
|
|
116
|
+
raw_result: execution.rawResult,
|
|
117
|
+
provider: execution.provider,
|
|
118
|
+
limitations: [...execution.limitations],
|
|
119
|
+
locations: [...execution.locations],
|
|
120
|
+
subject: execution.subject === null
|
|
121
|
+
? null
|
|
122
|
+
: {
|
|
123
|
+
...execution.subject,
|
|
124
|
+
architecture: execution.subject.architecture ?? null,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
/** Parse a snapshot and reject altered query IDs or non-canonical entry order. */
|
|
129
|
+
export const parseAnalysisSnapshot = (input) => {
|
|
130
|
+
const parsed = analysisSnapshotSchema.parse(input);
|
|
131
|
+
parseEvidenceBundle(parsed.evidence_bundle);
|
|
132
|
+
if (JSON.stringify(evidenceBundleForTarget(parsed.evidence_bundle, parsed.target.sha256)) !== JSON.stringify(parsed.evidence_bundle))
|
|
133
|
+
throw new TypeError("Analysis snapshot evidence contains records for another target");
|
|
134
|
+
const ids = new Set();
|
|
135
|
+
for (const entry of parsed.entries) {
|
|
136
|
+
const expected = analysisQueryId(parsed.target, entry.operation, entry.parameters, entry.execution.provider);
|
|
137
|
+
if (entry.query_id !== expected)
|
|
138
|
+
throw new TypeError("Analysis snapshot query identifier does not match");
|
|
139
|
+
if (ids.has(entry.query_id))
|
|
140
|
+
throw new TypeError("Analysis snapshot contains duplicate queries");
|
|
141
|
+
ids.add(entry.query_id);
|
|
142
|
+
}
|
|
143
|
+
const sorted = [...parsed.entries].sort((left, right) => left.query_id.localeCompare(right.query_id));
|
|
144
|
+
if (JSON.stringify(parsed.entries) !== JSON.stringify(sorted))
|
|
145
|
+
throw new TypeError("Analysis snapshot entries are not canonical");
|
|
146
|
+
return parsed;
|
|
147
|
+
};
|
|
148
|
+
/** Serialize a validated snapshot with byte-stable canonical entry ordering. */
|
|
149
|
+
export const serializeAnalysisSnapshot = (snapshot) => `${canonicalJson(parseAnalysisSnapshot(snapshot))}\n`;
|
|
@@ -11,8 +11,15 @@ const comparisonStatusSchema = z.enum([
|
|
|
11
11
|
"changed",
|
|
12
12
|
"truncated",
|
|
13
13
|
"unknown",
|
|
14
|
+
"contradiction",
|
|
15
|
+
]);
|
|
16
|
+
const changeKindSchema = z.enum([
|
|
17
|
+
"added",
|
|
18
|
+
"removed",
|
|
19
|
+
"changed",
|
|
20
|
+
"unknown",
|
|
21
|
+
"contradiction",
|
|
14
22
|
]);
|
|
15
|
-
const changeKindSchema = z.enum(["added", "removed", "changed", "unknown"]);
|
|
16
23
|
const comparisonDimensionSchema = z.enum([
|
|
17
24
|
"content",
|
|
18
25
|
"kind",
|
|
@@ -22,6 +29,7 @@ const comparisonDimensionSchema = z.enum([
|
|
|
22
29
|
"relations",
|
|
23
30
|
"metadata",
|
|
24
31
|
"availability",
|
|
32
|
+
"integrity",
|
|
25
33
|
]);
|
|
26
34
|
/** Strict Evidence-backed input for deterministic artifact comparison. */
|
|
27
35
|
export const artifactComparisonInputSchema = z.object({
|
|
@@ -68,6 +76,7 @@ export const artifactComparisonResultSchema = z.object({
|
|
|
68
76
|
removed: z.number().int().min(0),
|
|
69
77
|
changed: z.number().int().min(0),
|
|
70
78
|
unknown: z.number().int().min(0),
|
|
79
|
+
contradiction: z.number().int().min(0).default(0),
|
|
71
80
|
}),
|
|
72
81
|
changes: z.object({
|
|
73
82
|
items: z.array(artifactChangeSchema).max(500),
|
|
@@ -84,8 +93,8 @@ export const compareArtifacts = (leftEvidence, rightEvidence, offset, limit) =>
|
|
|
84
93
|
const right = parseArtifactInventoryEvidence(rightEvidence);
|
|
85
94
|
const leftComplete = left.inventory.complete;
|
|
86
95
|
const rightComplete = right.inventory.complete;
|
|
87
|
-
const leftCovered = leftComplete && left.inventory.limitations
|
|
88
|
-
const rightCovered = rightComplete && right.inventory.limitations
|
|
96
|
+
const leftCovered = leftComplete && !hasCoverageLimitation(left.inventory.limitations);
|
|
97
|
+
const rightCovered = rightComplete && !hasCoverageLimitation(right.inventory.limitations);
|
|
89
98
|
const limitations = [
|
|
90
99
|
...left.inventory.limitations.map((item) => `Left: ${item}`),
|
|
91
100
|
...right.inventory.limitations.map((item) => `Right: ${item}`),
|
|
@@ -101,13 +110,15 @@ export const compareArtifacts = (leftEvidence, rightEvidence, offset, limit) =>
|
|
|
101
110
|
return artifactComparisonResultSchema.parse({
|
|
102
111
|
status: !leftComplete || !rightComplete
|
|
103
112
|
? "truncated"
|
|
104
|
-
:
|
|
105
|
-
? "
|
|
106
|
-
:
|
|
113
|
+
: changes.some(({ classification }) => classification === "contradiction")
|
|
114
|
+
? "contradiction"
|
|
115
|
+
: !leftCovered || !rightCovered
|
|
107
116
|
? "unknown"
|
|
108
|
-
: changes.
|
|
109
|
-
? "
|
|
110
|
-
:
|
|
117
|
+
: changes.some(({ classification }) => classification === "unknown")
|
|
118
|
+
? "unknown"
|
|
119
|
+
: changes.length === 0
|
|
120
|
+
? "unchanged"
|
|
121
|
+
: "changed",
|
|
111
122
|
left_manifest_id: left.inventory.manifest.manifest_id,
|
|
112
123
|
right_manifest_id: right.inventory.manifest.manifest_id,
|
|
113
124
|
summary,
|
|
@@ -172,6 +183,13 @@ const classifyPath = (input) => {
|
|
|
172
183
|
classification: input.complete ? "removed" : "unknown",
|
|
173
184
|
dimensions: [input.complete ? "content" : "availability"],
|
|
174
185
|
};
|
|
186
|
+
if (input.leftOccurrence.hash_status === "mismatched" ||
|
|
187
|
+
input.rightOccurrence.hash_status === "mismatched")
|
|
188
|
+
return {
|
|
189
|
+
...base,
|
|
190
|
+
classification: "contradiction",
|
|
191
|
+
dimensions: ["integrity"],
|
|
192
|
+
};
|
|
175
193
|
if (input.leftOccurrence.hash_status !== "verified" ||
|
|
176
194
|
input.rightOccurrence.hash_status !== "verified" ||
|
|
177
195
|
input.leftNode === undefined ||
|
|
@@ -253,13 +271,20 @@ const relationsByPath = (inventory) => {
|
|
|
253
271
|
return output;
|
|
254
272
|
};
|
|
255
273
|
const summarize = (left, right, changes, covered) => {
|
|
256
|
-
const counts = {
|
|
274
|
+
const counts = {
|
|
275
|
+
added: 0,
|
|
276
|
+
removed: 0,
|
|
277
|
+
changed: 0,
|
|
278
|
+
unknown: 0,
|
|
279
|
+
contradiction: 0,
|
|
280
|
+
};
|
|
257
281
|
for (const change of changes)
|
|
258
282
|
counts[change.classification] += 1;
|
|
259
283
|
return {
|
|
260
284
|
unchanged: covered
|
|
261
285
|
? Math.max(0, Math.min(left.occurrences.length, right.occurrences.length) -
|
|
262
286
|
counts.changed -
|
|
287
|
+
counts.contradiction -
|
|
263
288
|
counts.unknown)
|
|
264
289
|
: 0,
|
|
265
290
|
...counts,
|
|
@@ -268,6 +293,8 @@ const summarize = (left, right, changes, covered) => {
|
|
|
268
293
|
: Math.max(counts.unknown, Math.max(left.manifest.occurrence_count, right.manifest.occurrence_count) -
|
|
269
294
|
counts.added -
|
|
270
295
|
counts.removed -
|
|
271
|
-
counts.changed
|
|
296
|
+
counts.changed -
|
|
297
|
+
counts.contradiction),
|
|
272
298
|
};
|
|
273
299
|
};
|
|
300
|
+
const hasCoverageLimitation = (limitations) => limitations.some((limitation) => !/integrity contradiction\(s\) were recorded/u.test(limitation));
|
|
@@ -5,6 +5,7 @@ const occurrenceIdSchema = z.string().regex(/^occ_[a-f0-9]{64}$/u);
|
|
|
5
5
|
const edgeIdSchema = z.string().regex(/^edge_[a-f0-9]{64}$/u);
|
|
6
6
|
const manifestIdSchema = z.string().regex(/^agm_[a-f0-9]{64}$/u);
|
|
7
7
|
const extractionIdSchema = z.string().regex(/^aex_[a-f0-9]{64}$/u);
|
|
8
|
+
const contradictionIdSchema = z.string().regex(/^ic_[a-f0-9]{64}$/u);
|
|
8
9
|
const boundedRelativePathSchema = z
|
|
9
10
|
.string()
|
|
10
11
|
.min(1)
|
|
@@ -106,10 +107,29 @@ const artifactOccurrenceSchema = z.object({
|
|
|
106
107
|
.nullable(),
|
|
107
108
|
executable: z.boolean(),
|
|
108
109
|
encrypted: z.boolean(),
|
|
109
|
-
hash_status: z.enum([
|
|
110
|
+
hash_status: z.enum([
|
|
111
|
+
"verified",
|
|
112
|
+
"mismatched",
|
|
113
|
+
"not-hashed-limit",
|
|
114
|
+
"unavailable",
|
|
115
|
+
]),
|
|
110
116
|
source_location: artifactByteRangeSchema.nullable(),
|
|
111
117
|
limitations: z.array(z.string()),
|
|
112
118
|
});
|
|
119
|
+
/** Observed bytes that contradict a container's declared integrity identity. */
|
|
120
|
+
const integrityContradictionSchema = z.object({
|
|
121
|
+
contradiction_id: contradictionIdSchema,
|
|
122
|
+
occurrence_id: occurrenceIdSchema,
|
|
123
|
+
parent_artifact_id: artifactIdSchema,
|
|
124
|
+
logical_path: boundedRelativePathSchema,
|
|
125
|
+
declared_sha256: sha256Schema,
|
|
126
|
+
observed_sha256: sha256Schema,
|
|
127
|
+
entry_kind: z.enum(["file", "slice"]),
|
|
128
|
+
unpacked: z.boolean(),
|
|
129
|
+
trust: z.literal("observed-untrusted"),
|
|
130
|
+
provenance: z.literal("container-integrity-metadata-versus-observed-bytes"),
|
|
131
|
+
limitations: z.array(z.string()).min(1),
|
|
132
|
+
});
|
|
113
133
|
/** Parent-child derivation relation between two content-addressed artifacts. */
|
|
114
134
|
const artifactEdgeSchema = z.object({
|
|
115
135
|
edge_id: edgeIdSchema,
|
|
@@ -157,6 +177,10 @@ export const artifactInventoryResultSchema = z.object({
|
|
|
157
177
|
edges: artifactPageSchema(artifactEdgeSchema),
|
|
158
178
|
limits: artifactTraversalLimitsSchema,
|
|
159
179
|
provenance: z.array(artifactCommandSchema).max(256),
|
|
180
|
+
integrity_contradictions: z
|
|
181
|
+
.array(integrityContradictionSchema)
|
|
182
|
+
.max(100)
|
|
183
|
+
.default([]),
|
|
160
184
|
limitations: z.array(z.string()),
|
|
161
185
|
});
|
|
162
186
|
/** One safely materialized artifact beneath caller-approved output root. */
|
|
@@ -39,6 +39,9 @@ const assembleInventorySet = (pages) => {
|
|
|
39
39
|
for (const page of pages) {
|
|
40
40
|
if (canonicalJson(page.inventory.manifest) !== manifestJson)
|
|
41
41
|
throw new TypeError("Artifact inventory pages do not share one manifest");
|
|
42
|
+
if (canonicalJson(page.inventory.integrity_contradictions) !==
|
|
43
|
+
canonicalJson(first.inventory.integrity_contradictions))
|
|
44
|
+
throw new TypeError("Artifact inventory pages do not share integrity contradictions");
|
|
42
45
|
for (const node of page.inventory.nodes.items)
|
|
43
46
|
mergeExact(nodes, node.artifact_id, node, "node");
|
|
44
47
|
for (const occurrence of page.inventory.occurrences.items) {
|
|
@@ -66,6 +69,7 @@ const orderedInventory = (first, pages, values) => {
|
|
|
66
69
|
nodes,
|
|
67
70
|
occurrences,
|
|
68
71
|
edges,
|
|
72
|
+
integrityContradictions: first.integrity_contradictions,
|
|
69
73
|
limitations: [
|
|
70
74
|
...new Set(pages.flatMap(({ inventory }) => inventory.limitations)),
|
|
71
75
|
].sort((left, right) => left.localeCompare(right)),
|
|
@@ -84,10 +88,24 @@ const validateCompleteInventory = (inventory) => {
|
|
|
84
88
|
validateOccurrence(occurrence, inventory, nodeIds, occurrenceIds);
|
|
85
89
|
for (const edge of inventory.edges)
|
|
86
90
|
validateEdge(edge, nodeIds, occurrenceIds);
|
|
91
|
+
for (const contradiction of inventory.integrityContradictions) {
|
|
92
|
+
if (!nodeIds.has(contradiction.parent_artifact_id) ||
|
|
93
|
+
!occurrenceIds.has(contradiction.occurrence_id))
|
|
94
|
+
throw new TypeError("Integrity contradiction references a missing graph member");
|
|
95
|
+
const expectedId = `ic_${digestCanonical({
|
|
96
|
+
root_artifact_id: inventory.manifest.root_artifact_id,
|
|
97
|
+
logical_path: contradiction.logical_path,
|
|
98
|
+
declared_sha256: contradiction.declared_sha256,
|
|
99
|
+
observed_sha256: contradiction.observed_sha256,
|
|
100
|
+
})}`;
|
|
101
|
+
if (contradiction.contradiction_id !== expectedId)
|
|
102
|
+
throw new TypeError("Integrity contradiction ID does not match its identity");
|
|
103
|
+
}
|
|
87
104
|
const graphSha256 = digestCanonical({
|
|
88
105
|
nodes: inventory.nodes,
|
|
89
106
|
occurrences: inventory.occurrences,
|
|
90
107
|
edges: inventory.edges,
|
|
108
|
+
integrity_contradictions: inventory.integrityContradictions,
|
|
91
109
|
});
|
|
92
110
|
if (graphSha256 !== inventory.manifest.graph_sha256)
|
|
93
111
|
throw new TypeError("Artifact graph commitment does not match its members");
|
|
@@ -3,13 +3,24 @@ import { artifactComparisonResultSchema } from "./artifactComparison.js";
|
|
|
3
3
|
import { evidenceSchema, parseEvidence } from "./evidence.js";
|
|
4
4
|
import { functionComparisonResultSchema } from "./functionComparison.js";
|
|
5
5
|
import { comparisonStatusSchema, processCaptureComparisonSchema, } from "./processCapture.js";
|
|
6
|
+
import { crossVersionInvestigationInputSchema, investigationRunSummarySchema, } from "./investigationWorkspace.js";
|
|
6
7
|
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
|
|
7
8
|
/** Strict bounded input for aggregating existing comparison Evidence. */
|
|
8
|
-
export const changedBehaviorInputSchema = z
|
|
9
|
-
|
|
9
|
+
export const changedBehaviorInputSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
comparisons: z.array(evidenceSchema).max(100).default([]),
|
|
12
|
+
investigation_run: crossVersionInvestigationInputSchema.optional(),
|
|
10
13
|
offset: z.number().int().min(0).default(0),
|
|
11
14
|
limit: z.number().int().min(1).max(100).default(100),
|
|
12
15
|
unknown_registry_approved: z.literal(true).optional(),
|
|
16
|
+
})
|
|
17
|
+
.superRefine((input, context) => {
|
|
18
|
+
if (input.comparisons.length > 0 ===
|
|
19
|
+
(input.investigation_run !== undefined))
|
|
20
|
+
context.addIssue({
|
|
21
|
+
code: "custom",
|
|
22
|
+
message: "Supply either existing comparisons or one investigation_run, but not both",
|
|
23
|
+
});
|
|
13
24
|
});
|
|
14
25
|
const findingSchema = z.object({
|
|
15
26
|
scope: z.enum(["runtime", "protocol", "resource", "static_candidate"]),
|
|
@@ -48,6 +59,7 @@ export const changedBehaviorResultSchema = z.object({
|
|
|
48
59
|
}),
|
|
49
60
|
evidence_links: z.array(evidenceIdSchema).min(3).max(20_100),
|
|
50
61
|
limitations: z.array(z.string()),
|
|
62
|
+
investigation_run: investigationRunSummarySchema.optional(),
|
|
51
63
|
});
|
|
52
64
|
const EXPECTED_PROVIDERS = {
|
|
53
65
|
compare_process_captures: {
|
|
@@ -113,6 +125,7 @@ export const findChangedBehavior = (comparisonsInput, offset, limit) => {
|
|
|
113
125
|
...(runtimeStatuses.length === 0
|
|
114
126
|
? ["No process comparison Evidence was supplied."]
|
|
115
127
|
: []),
|
|
128
|
+
...evidence.flatMap(artifactPaginationLimitations),
|
|
116
129
|
]),
|
|
117
130
|
});
|
|
118
131
|
};
|
|
@@ -169,10 +182,8 @@ const parseResult = (evidence) => {
|
|
|
169
182
|
return;
|
|
170
183
|
}
|
|
171
184
|
const result = artifactComparisonResultSchema.parse(evidence.normalized_result);
|
|
172
|
-
if (result.changes.offset !== 0
|
|
173
|
-
|
|
174
|
-
result.changes.items.length !== result.changes.total)
|
|
175
|
-
throw new TypeError("Changed-behavior analysis requires complete artifact comparison pagination");
|
|
185
|
+
if (result.changes.offset !== 0)
|
|
186
|
+
throw new TypeError("Changed-behavior analysis requires artifact comparison pagination from offset zero");
|
|
176
187
|
assertNestedLinks(evidence, result.changes.items.flatMap(({ evidence_links: links }) => links));
|
|
177
188
|
};
|
|
178
189
|
const assertNestedLinks = (evidence, nestedLinks) => {
|
|
@@ -236,13 +247,17 @@ const artifactFindings = (evidence) => {
|
|
|
236
247
|
dimension: `artifact:${change.logical_path}`,
|
|
237
248
|
scope: "static_candidate",
|
|
238
249
|
status: change.classification,
|
|
239
|
-
classification: change.classification === "
|
|
240
|
-
? "
|
|
241
|
-
: "
|
|
250
|
+
classification: change.classification === "contradiction"
|
|
251
|
+
? "contradiction"
|
|
252
|
+
: change.classification === "unknown"
|
|
253
|
+
? "unresolved_branch"
|
|
254
|
+
: "derived_relationship",
|
|
242
255
|
limitations: result.limitations,
|
|
243
256
|
links: change.evidence_links,
|
|
244
257
|
}));
|
|
245
|
-
return (result.status === "unknown" ||
|
|
258
|
+
return (result.status === "unknown" ||
|
|
259
|
+
result.status === "truncated" ||
|
|
260
|
+
result.changes.next_offset !== null) &&
|
|
246
261
|
!changes.some(({ classification }) => classification === "unresolved_branch")
|
|
247
262
|
? [
|
|
248
263
|
...changes,
|
|
@@ -257,6 +272,16 @@ const artifactFindings = (evidence) => {
|
|
|
257
272
|
]
|
|
258
273
|
: changes;
|
|
259
274
|
};
|
|
275
|
+
const artifactPaginationLimitations = (evidence) => {
|
|
276
|
+
if (evidence.operation !== "compare_artifacts")
|
|
277
|
+
return [];
|
|
278
|
+
const result = artifactComparisonResultSchema.parse(evidence.normalized_result);
|
|
279
|
+
return result.changes.next_offset === null
|
|
280
|
+
? []
|
|
281
|
+
: [
|
|
282
|
+
`Artifact comparison reports ${String(result.changes.items.length)} of ${String(result.changes.total)} changes.`,
|
|
283
|
+
];
|
|
284
|
+
};
|
|
260
285
|
const makeFinding = (input) => findingSchema.parse({
|
|
261
286
|
scope: input.scope,
|
|
262
287
|
dimension: input.dimension,
|