rea-agents 0.3.0 → 0.5.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 +139 -21
- package/bridge/hopper_bridge.py +156 -14
- 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 +174 -14
- package/dist/application/CompositeProvider.js +73 -0
- package/dist/application/DirectAnalysis.js +44 -3
- package/dist/application/Doctor.js +35 -6
- package/dist/application/EnhancedTools.js +10 -7
- package/dist/application/EvidenceBundleCommands.js +51 -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 +105 -0
- package/dist/application/ProcessSampling.js +284 -0
- package/dist/application/RealHopperAssertions.js +116 -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 +229 -21
- package/dist/cliEvidenceCommands.js +68 -0
- package/dist/cliLogging.js +21 -0
- 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 +156 -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 +199 -5
- 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 +35 -8
- 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/referenceSourcePolicy.js +1 -0
- package/dist/domain/residualUnknown.js +239 -0
- package/dist/domain/staticRuntimeCorrelation.js +375 -0
- package/dist/hopper/BridgeLauncher.js +39 -3
- package/dist/hopper/HopperClient.js +14 -5
- package/dist/hopper/HopperProvider.js +57 -22
- package/dist/hopper/protocol.js +13 -2
- 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 +18 -1
- package/scripts/rea.mjs +5 -1
- package/skills/rea-analysis/SKILL.md +77 -2
|
@@ -1,11 +1,192 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import
|
|
2
|
+
import canonicalize from "canonicalize";
|
|
3
|
+
import { evidenceSchema, parseEvidence } from "./evidence.js";
|
|
4
|
+
import { residualUnknownSchema, } from "./residualUnknown.js";
|
|
5
|
+
const artifactManifestSchema = evidenceSchema.shape.subject.unwrap().pick({
|
|
6
|
+
digest: true,
|
|
7
|
+
format: true,
|
|
8
|
+
architecture: true,
|
|
9
|
+
});
|
|
10
|
+
const providerManifestSchema = evidenceSchema.shape.provider;
|
|
11
|
+
const environmentManifestSchema = evidenceSchema.shape.environment.unwrap();
|
|
12
|
+
const scenarioManifestSchema = z.object({
|
|
13
|
+
evidence_id: evidenceSchema.shape.evidence_id,
|
|
14
|
+
operation: z.string().min(1),
|
|
15
|
+
authority: z.literal("controlled-replay"),
|
|
16
|
+
});
|
|
17
|
+
const captureManifestSchema = z.object({
|
|
18
|
+
evidence_id: evidenceSchema.shape.evidence_id,
|
|
19
|
+
predicate_type: z.string().min(1),
|
|
20
|
+
});
|
|
3
21
|
export const evidenceBundleSchema = z.object({
|
|
4
|
-
bundle_version: z.literal(
|
|
22
|
+
bundle_version: z.literal(2),
|
|
23
|
+
artifacts: z.array(artifactManifestSchema),
|
|
24
|
+
providers: z.array(providerManifestSchema),
|
|
25
|
+
environments: z.array(environmentManifestSchema),
|
|
26
|
+
scenarios: z.array(scenarioManifestSchema),
|
|
27
|
+
captures: z.array(captureManifestSchema),
|
|
28
|
+
unknowns: z.array(residualUnknownSchema),
|
|
5
29
|
records: z.array(evidenceSchema),
|
|
6
30
|
});
|
|
7
31
|
/** Project records into a deterministic bundle whose order has no semantics. */
|
|
8
|
-
export const createEvidenceBundle = (records) =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
32
|
+
export const createEvidenceBundle = (records, unknowns = []) => {
|
|
33
|
+
const sortedRecords = [...records].sort((left, right) => left.evidence_id.localeCompare(right.evidence_id));
|
|
34
|
+
return {
|
|
35
|
+
bundle_version: 2,
|
|
36
|
+
artifacts: uniqueSorted(sortedRecords.flatMap(({ subject }) => subject === null
|
|
37
|
+
? []
|
|
38
|
+
: [
|
|
39
|
+
{
|
|
40
|
+
digest: subject.digest,
|
|
41
|
+
format: subject.format,
|
|
42
|
+
architecture: subject.architecture,
|
|
43
|
+
},
|
|
44
|
+
])),
|
|
45
|
+
providers: uniqueSorted(sortedRecords.map(({ provider }) => provider)),
|
|
46
|
+
environments: uniqueSorted(sortedRecords.flatMap(({ environment }) => environment === null ? [] : [environment])),
|
|
47
|
+
scenarios: sortedRecords.flatMap((evidence) => evidence.authority === "controlled-replay"
|
|
48
|
+
? [
|
|
49
|
+
{
|
|
50
|
+
evidence_id: evidence.evidence_id,
|
|
51
|
+
operation: evidence.operation,
|
|
52
|
+
authority: evidence.authority,
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
: []),
|
|
56
|
+
captures: sortedRecords.flatMap((evidence) => evidence.predicate_type.startsWith("rea.process-capture/")
|
|
57
|
+
? [
|
|
58
|
+
{
|
|
59
|
+
evidence_id: evidence.evidence_id,
|
|
60
|
+
predicate_type: evidence.predicate_type,
|
|
61
|
+
},
|
|
62
|
+
]
|
|
63
|
+
: []),
|
|
64
|
+
unknowns: [...unknowns].sort((left, right) => left.unknown_id.localeCompare(right.unknown_id) ||
|
|
65
|
+
left.revision - right.revision),
|
|
66
|
+
records: sortedRecords,
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/** Parse records, verify semantic IDs, and reject inconsistent manifests. */
|
|
70
|
+
export const parseEvidenceBundle = (input) => {
|
|
71
|
+
const parsed = evidenceBundleSchema.parse(input);
|
|
72
|
+
const recordIds = parsed.records.map(({ evidence_id: id }) => id);
|
|
73
|
+
if (new Set(recordIds).size !== recordIds.length)
|
|
74
|
+
throw new TypeError("Evidence bundle contains duplicate record IDs");
|
|
75
|
+
validateUnknownGraph(parsed.unknowns, parsed.records);
|
|
76
|
+
const canonical = createEvidenceBundle(parsed.records.map(parseEvidence), parsed.unknowns);
|
|
77
|
+
if (JSON.stringify(parsed) !== JSON.stringify(canonical))
|
|
78
|
+
throw new TypeError("Evidence bundle manifests are not canonical");
|
|
79
|
+
return canonical;
|
|
80
|
+
};
|
|
81
|
+
const validateUnknownGraph = (unknowns, records) => {
|
|
82
|
+
const evidenceById = new Map(records.map((record) => [record.evidence_id, record]));
|
|
83
|
+
const histories = new Map();
|
|
84
|
+
for (const unknown of unknowns) {
|
|
85
|
+
const history = histories.get(unknown.unknown_id) ?? [];
|
|
86
|
+
history.push(unknown);
|
|
87
|
+
histories.set(unknown.unknown_id, history);
|
|
88
|
+
}
|
|
89
|
+
const unknownById = new Map();
|
|
90
|
+
for (const [id, unordered] of histories) {
|
|
91
|
+
const history = [...unordered].sort((left, right) => left.revision - right.revision);
|
|
92
|
+
for (const [index, revision] of history.entries()) {
|
|
93
|
+
if (revision.revision !== index + 1)
|
|
94
|
+
throw new TypeError(`Residual unknown ${id} revision history has a gap`);
|
|
95
|
+
if (index > 0 &&
|
|
96
|
+
revision.previous_revision_digest !==
|
|
97
|
+
history[index - 1]?.revision_digest)
|
|
98
|
+
throw new TypeError(`Residual unknown ${id} revision chain is broken`);
|
|
99
|
+
}
|
|
100
|
+
const head = history.at(-1);
|
|
101
|
+
if (head === undefined)
|
|
102
|
+
throw new TypeError("Residual unknown revision history is empty");
|
|
103
|
+
unknownById.set(id, head);
|
|
104
|
+
}
|
|
105
|
+
for (const unknown of unknowns) {
|
|
106
|
+
const referencedEvidence = [
|
|
107
|
+
...unknown.supporting_evidence_ids,
|
|
108
|
+
...unknown.contradicting_evidence_ids,
|
|
109
|
+
...unknown.mutation_evidence_ids,
|
|
110
|
+
...(unknown.resolution?.evidence_ids ?? []),
|
|
111
|
+
];
|
|
112
|
+
for (const evidenceId of referencedEvidence)
|
|
113
|
+
if (!evidenceById.has(evidenceId))
|
|
114
|
+
throw new TypeError(`Residual unknown references missing evidence ${evidenceId}`);
|
|
115
|
+
for (const relationship of unknown.relationships)
|
|
116
|
+
if (!unknownById.has(relationship.unknown_id))
|
|
117
|
+
throw new TypeError(`Residual unknown references missing unknown ${relationship.unknown_id}`);
|
|
118
|
+
if (unknown.resolution?.disposition === "verified")
|
|
119
|
+
validateVerifiedResolution(unknown, evidenceById);
|
|
120
|
+
}
|
|
121
|
+
rejectDependencyCycles(unknownById);
|
|
122
|
+
};
|
|
123
|
+
const validateVerifiedResolution = (unknown, evidenceById) => {
|
|
124
|
+
if (unknown.contradicting_evidence_ids.length > 0)
|
|
125
|
+
throw new TypeError(`Residual unknown ${unknown.unknown_id} retains contradicting evidence`);
|
|
126
|
+
const qualifies = unknown.resolution?.evidence_ids.some((evidenceId) => {
|
|
127
|
+
const evidence = evidenceById.get(evidenceId);
|
|
128
|
+
return (unknown.supporting_evidence_ids.includes(evidenceId) &&
|
|
129
|
+
evidence !== undefined &&
|
|
130
|
+
evidence.predicate_type !== "rea.residual-unknown-mutation/v1" &&
|
|
131
|
+
evidenceQualifies(unknown, evidence));
|
|
132
|
+
});
|
|
133
|
+
if (qualifies !== true)
|
|
134
|
+
throw new TypeError(`Residual unknown ${unknown.unknown_id} has no qualifying resolution evidence`);
|
|
135
|
+
};
|
|
136
|
+
const evidenceQualifies = (unknown, evidence) => {
|
|
137
|
+
if (unknown.required_authority !== null &&
|
|
138
|
+
evidence.authority !== unknown.required_authority)
|
|
139
|
+
return false;
|
|
140
|
+
const confidenceRank = { inferred: 1, derived: 2, observed: 3 };
|
|
141
|
+
if (confidenceRank[evidence.confidence] <
|
|
142
|
+
confidenceRank[unknown.required_confidence])
|
|
143
|
+
return false;
|
|
144
|
+
const requirement = unknown.required_environment;
|
|
145
|
+
if (requirement === null)
|
|
146
|
+
return true;
|
|
147
|
+
if (evidence.environment === null)
|
|
148
|
+
return false;
|
|
149
|
+
return ((requirement.id === null || requirement.id === evidence.environment.id) &&
|
|
150
|
+
(requirement.platform === null ||
|
|
151
|
+
requirement.platform === evidence.environment.platform) &&
|
|
152
|
+
(requirement.architecture === null ||
|
|
153
|
+
requirement.architecture === evidence.environment.architecture) &&
|
|
154
|
+
(requirement.isolation === null ||
|
|
155
|
+
requirement.isolation === evidence.environment.isolation));
|
|
156
|
+
};
|
|
157
|
+
const rejectDependencyCycles = (unknownById) => {
|
|
158
|
+
const visited = new Set();
|
|
159
|
+
const active = new Set();
|
|
160
|
+
const visit = (id) => {
|
|
161
|
+
if (active.has(id))
|
|
162
|
+
throw new TypeError("Residual unknown dependency graph contains a cycle");
|
|
163
|
+
if (visited.has(id))
|
|
164
|
+
return;
|
|
165
|
+
active.add(id);
|
|
166
|
+
const unknown = unknownById.get(id);
|
|
167
|
+
if (unknown === undefined)
|
|
168
|
+
throw new TypeError("Residual unknown dependency graph is inconsistent");
|
|
169
|
+
for (const relationship of unknown.relationships)
|
|
170
|
+
if (relationship.type === "depends-on")
|
|
171
|
+
visit(relationship.unknown_id);
|
|
172
|
+
active.delete(id);
|
|
173
|
+
visited.add(id);
|
|
174
|
+
};
|
|
175
|
+
for (const id of unknownById.keys())
|
|
176
|
+
visit(id);
|
|
177
|
+
};
|
|
178
|
+
/** Encode a validated bundle as byte-stable RFC 8785 canonical JSON. */
|
|
179
|
+
export const serializeEvidenceBundle = (bundle) => {
|
|
180
|
+
const serialized = canonicalize(parseEvidenceBundle(bundle));
|
|
181
|
+
if (serialized === undefined)
|
|
182
|
+
throw new TypeError("Evidence bundle canonicalization failed");
|
|
183
|
+
return serialized;
|
|
184
|
+
};
|
|
185
|
+
const uniqueSorted = (values) => {
|
|
186
|
+
const unique = new Map();
|
|
187
|
+
for (const value of values)
|
|
188
|
+
unique.set(JSON.stringify(value), value);
|
|
189
|
+
return [...unique.entries()]
|
|
190
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
191
|
+
.map(([, value]) => value);
|
|
192
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { diffLines } from "diff";
|
|
2
|
+
import { parseFunctionEvidence, } from "./functionDossierEvidence.js";
|
|
3
|
+
import { commentProjection, combineCoverage, functionMatch, identityProjection, isAutoName, normalizeCfg, project, referenceProjection, sorted, stringAndNameProjection, } from "./functionComparisonNormalization.js";
|
|
4
|
+
import { functionComparisonResultSchema, } from "./functionComparisonSchemas.js";
|
|
5
|
+
import { canonicalJson, dimensionResult, overallStatus, summarize, unresolvedDimension as buildUnresolvedDimension, } from "./functionComparisonResults.js";
|
|
6
|
+
export { functionComparisonInputSchema, functionComparisonResultSchema, } from "./functionComparisonSchemas.js";
|
|
7
|
+
const unresolvedDimension = (...[dimension, status, links, leftCount, rightCount, limitations]) => buildUnresolvedDimension({
|
|
8
|
+
dimension,
|
|
9
|
+
status,
|
|
10
|
+
links,
|
|
11
|
+
leftCount,
|
|
12
|
+
rightCount,
|
|
13
|
+
limitations,
|
|
14
|
+
});
|
|
15
|
+
/** Compare two explicit function Evidence page sets without fuzzy matching. */
|
|
16
|
+
export const compareFunctions = (leftInput, rightInput, offset, limit) => {
|
|
17
|
+
const left = parseFunctionEvidence(leftInput);
|
|
18
|
+
const right = parseFunctionEvidence(rightInput);
|
|
19
|
+
const links = [
|
|
20
|
+
...left.evidence.map(({ evidence_id: id }) => id),
|
|
21
|
+
...right.evidence.map(({ evidence_id: id }) => id),
|
|
22
|
+
];
|
|
23
|
+
const providersDiffer = canonicalJson(left.provider) !== canonicalJson(right.provider);
|
|
24
|
+
const dimensions = compareDimensions(left, right, links, providersDiffer);
|
|
25
|
+
const match = functionMatch(left, right);
|
|
26
|
+
const changes = dimensions.filter(({ status }) => status !== "unchanged");
|
|
27
|
+
const page = changes.slice(offset, offset + limit);
|
|
28
|
+
return functionComparisonResultSchema.parse({
|
|
29
|
+
status: overallStatus(dimensions, match.status),
|
|
30
|
+
function_match: match,
|
|
31
|
+
left_subject_sha256: left.subject.digest.sha256,
|
|
32
|
+
right_subject_sha256: right.subject.digest.sha256,
|
|
33
|
+
summary: summarize(dimensions),
|
|
34
|
+
dimensions,
|
|
35
|
+
changes: {
|
|
36
|
+
items: page,
|
|
37
|
+
offset,
|
|
38
|
+
limit,
|
|
39
|
+
total: changes.length,
|
|
40
|
+
next_offset: offset + page.length < changes.length ? offset + page.length : null,
|
|
41
|
+
},
|
|
42
|
+
limitations: [
|
|
43
|
+
...new Set([
|
|
44
|
+
...left.limitations.map((item) => `Left: ${item}`),
|
|
45
|
+
...right.limitations.map((item) => `Right: ${item}`),
|
|
46
|
+
...(providersDiffer
|
|
47
|
+
? [
|
|
48
|
+
"Provider-specific pseudocode and assembly representations were not equated.",
|
|
49
|
+
]
|
|
50
|
+
: []),
|
|
51
|
+
]),
|
|
52
|
+
].sort((a, b) => a.localeCompare(b)),
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const compareDimensions = (left, right, links, providersDiffer) => [
|
|
56
|
+
compareIdentity(left, right, links, providersDiffer),
|
|
57
|
+
compareText("pseudocode", left.pseudocode.text, right.pseudocode.text, left.pseudocode.complete, right.pseudocode.complete, links, providersDiffer),
|
|
58
|
+
compareText("assembly", left.collections.assembly.items.join("\n"), right.collections.assembly.items.join("\n"), left.collections.assembly.complete, right.collections.assembly.complete, links, providersDiffer, left.collections.assembly.truncated || right.collections.assembly.truncated, "Assembly is opaque provider text; relocation normalization is unavailable."),
|
|
59
|
+
compareComments(left, right, links, providersDiffer),
|
|
60
|
+
compareCalls(left, right, links, providersDiffer),
|
|
61
|
+
compareReferences(left, right, links, providersDiffer),
|
|
62
|
+
compareStringsAndNames(left, right, links, providersDiffer),
|
|
63
|
+
compareCfg(left, right, links, providersDiffer),
|
|
64
|
+
];
|
|
65
|
+
const compareText = (...[dimension, left, right, leftComplete, rightComplete, links, providersDiffer, explicitTruncation, limitation,]) => {
|
|
66
|
+
const truncated = explicitTruncation ?? (!leftComplete || !rightComplete);
|
|
67
|
+
if (!leftComplete || !rightComplete || providersDiffer)
|
|
68
|
+
return unresolvedDimension(dimension, truncated ? "truncated" : "unknown", links, left.length, right.length, [
|
|
69
|
+
...(limitation === undefined ? [] : [limitation]),
|
|
70
|
+
...(providersDiffer
|
|
71
|
+
? ["Exact text comparison requires one provider identity."]
|
|
72
|
+
: []),
|
|
73
|
+
]);
|
|
74
|
+
const changes = diffLines(left, right, {
|
|
75
|
+
timeout: 100,
|
|
76
|
+
maxEditLength: 10_000,
|
|
77
|
+
});
|
|
78
|
+
if (changes === undefined)
|
|
79
|
+
return unresolvedDimension(dimension, "unknown", links, left.length, right.length, ["Bounded line diff exceeded its time or edit-distance limit."]);
|
|
80
|
+
const delta = changes.reduce((summary, change) => ({
|
|
81
|
+
added_lines: summary.added_lines + (change.added ? (change.count ?? 0) : 0),
|
|
82
|
+
removed_lines: summary.removed_lines + (change.removed ? (change.count ?? 0) : 0),
|
|
83
|
+
hunks: summary.hunks + (change.added || change.removed ? 1 : 0),
|
|
84
|
+
}), { added_lines: 0, removed_lines: 0, hunks: 0 });
|
|
85
|
+
return dimensionResult({
|
|
86
|
+
dimension,
|
|
87
|
+
status: delta.hunks === 0 ? "unchanged" : "changed",
|
|
88
|
+
left,
|
|
89
|
+
right,
|
|
90
|
+
links,
|
|
91
|
+
providersDiffer,
|
|
92
|
+
leftCount: [...left].length,
|
|
93
|
+
rightCount: [...right].length,
|
|
94
|
+
textDelta: delta,
|
|
95
|
+
limitations: limitation === undefined ? [] : [limitation],
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const compareCollections = (...[dimension, left, right, leftCoverage, rightCoverage, links, providersDiffer,]) => {
|
|
99
|
+
if (!leftCoverage.complete || !rightCoverage.complete)
|
|
100
|
+
return unresolvedDimension(dimension, leftCoverage.truncated || rightCoverage.truncated
|
|
101
|
+
? "truncated"
|
|
102
|
+
: "unknown", links, left.length, right.length, []);
|
|
103
|
+
return compareValues(dimension, sorted(left), sorted(right), links, true, true, providersDiffer);
|
|
104
|
+
};
|
|
105
|
+
const compareComments = (left, right, links, providersDiffer) => {
|
|
106
|
+
const leftValues = commentProjection(left);
|
|
107
|
+
const rightValues = commentProjection(right);
|
|
108
|
+
if (leftValues === null || rightValues === null)
|
|
109
|
+
return unresolvedDimension("comments", "unknown", links, left.collections.comments.items.length, right.collections.comments.items.length, ["Comment locations could not be normalized relative to the function."]);
|
|
110
|
+
return compareCollections("comments", leftValues, rightValues, left.collections.comments, right.collections.comments, links, providersDiffer);
|
|
111
|
+
};
|
|
112
|
+
const compareStringsAndNames = (left, right, links, providersDiffer) => {
|
|
113
|
+
const leftValues = stringAndNameProjection(left);
|
|
114
|
+
const rightValues = stringAndNameProjection(right);
|
|
115
|
+
if (leftValues === null || rightValues === null)
|
|
116
|
+
return unresolvedDimension("strings_names", "unknown", links, null, null, [
|
|
117
|
+
"Reference source locations could not be normalized to function offsets.",
|
|
118
|
+
]);
|
|
119
|
+
return compareCollections("strings_names", leftValues, rightValues, combineCoverage(left.collections.referenced_strings, left.collections.referenced_names), combineCoverage(right.collections.referenced_strings, right.collections.referenced_names), links, providersDiffer);
|
|
120
|
+
};
|
|
121
|
+
const compareReferences = (left, right, links, providersDiffer) => {
|
|
122
|
+
const leftCoverage = combineCoverage(left.collections.incoming_references, left.collections.outgoing_references);
|
|
123
|
+
const rightCoverage = combineCoverage(right.collections.incoming_references, right.collections.outgoing_references);
|
|
124
|
+
if (!leftCoverage.complete || !rightCoverage.complete)
|
|
125
|
+
return unresolvedDimension("references", leftCoverage.truncated || rightCoverage.truncated
|
|
126
|
+
? "truncated"
|
|
127
|
+
: "unknown", links, leftCoverage.items.length, rightCoverage.items.length, []);
|
|
128
|
+
const leftProjection = referenceProjection(left);
|
|
129
|
+
const rightProjection = referenceProjection(right);
|
|
130
|
+
if (leftProjection.length === 0 && rightProjection.length === 0)
|
|
131
|
+
return compareValues("references", leftProjection, rightProjection, links, true, true, providersDiffer);
|
|
132
|
+
if (canonicalJson(leftProjection) !== canonicalJson(rightProjection))
|
|
133
|
+
return dimensionResult({
|
|
134
|
+
dimension: "references",
|
|
135
|
+
status: "changed",
|
|
136
|
+
left: leftProjection,
|
|
137
|
+
right: rightProjection,
|
|
138
|
+
links,
|
|
139
|
+
providersDiffer,
|
|
140
|
+
leftCount: leftProjection.length,
|
|
141
|
+
rightCount: rightProjection.length,
|
|
142
|
+
textDelta: null,
|
|
143
|
+
limitations: [
|
|
144
|
+
"Reference endpoints differ; exact reference kinds remain unavailable.",
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
return unresolvedDimension("references", "unknown", links, leftProjection.length, rightProjection.length, ["Reference kinds are unavailable in the current dossier contract."]);
|
|
148
|
+
};
|
|
149
|
+
const compareIdentity = (left, right, links, providersDiffer) => {
|
|
150
|
+
if (isAutoName(left.procedure.name) || isAutoName(right.procedure.name))
|
|
151
|
+
return unresolvedDimension("identity", "unknown", links, null, null, [
|
|
152
|
+
"Address-derived function names are not stable cross-version identity.",
|
|
153
|
+
]);
|
|
154
|
+
return compareValues("identity", identityProjection(left), identityProjection(right), links, true, true, providersDiffer);
|
|
155
|
+
};
|
|
156
|
+
const compareCalls = (left, right, links, providersDiffer) => {
|
|
157
|
+
const leftValues = callProjection(left);
|
|
158
|
+
const rightValues = callProjection(right);
|
|
159
|
+
if ([...leftValues, ...rightValues].some(({ name }) => isAutoName(name)))
|
|
160
|
+
return unresolvedDimension("calls", "unknown", links, leftValues.length, rightValues.length, ["Address-derived callee or caller names cannot be matched safely."]);
|
|
161
|
+
return compareCollections("calls", leftValues, rightValues, combineCoverage(left.collections.callers, left.collections.callees), combineCoverage(right.collections.callers, right.collections.callees), links, providersDiffer);
|
|
162
|
+
};
|
|
163
|
+
const callProjection = (snapshot) => [
|
|
164
|
+
...project(snapshot.collections.callers, ({ name }) => ({
|
|
165
|
+
direction: "in",
|
|
166
|
+
name,
|
|
167
|
+
})),
|
|
168
|
+
...project(snapshot.collections.callees, ({ name }) => ({
|
|
169
|
+
direction: "out",
|
|
170
|
+
name,
|
|
171
|
+
})),
|
|
172
|
+
];
|
|
173
|
+
const compareCfg = (left, right, links, providersDiffer) => {
|
|
174
|
+
const leftBlocks = left.collections.basic_blocks;
|
|
175
|
+
const rightBlocks = right.collections.basic_blocks;
|
|
176
|
+
if (!leftBlocks.complete || !rightBlocks.complete)
|
|
177
|
+
return unresolvedDimension("cfg", leftBlocks.truncated || rightBlocks.truncated ? "truncated" : "unknown", links, leftBlocks.items.length, rightBlocks.items.length, []);
|
|
178
|
+
const leftGraph = normalizeCfg(leftBlocks.items);
|
|
179
|
+
const rightGraph = normalizeCfg(rightBlocks.items);
|
|
180
|
+
if (leftGraph === null || rightGraph === null)
|
|
181
|
+
return unresolvedDimension("cfg", "unknown", links, leftBlocks.items.length, rightBlocks.items.length, ["CFG addresses could not be normalized to local block indices."]);
|
|
182
|
+
return compareValues("cfg", leftGraph, rightGraph, links, true, true, providersDiffer);
|
|
183
|
+
};
|
|
184
|
+
const compareValues = (...[dimension, left, right, links, leftComplete, rightComplete, providersDiffer,]) => {
|
|
185
|
+
if (!leftComplete || !rightComplete)
|
|
186
|
+
return unresolvedDimension(dimension, "unknown", links, null, null, []);
|
|
187
|
+
const leftJson = canonicalJson(left);
|
|
188
|
+
const rightJson = canonicalJson(right);
|
|
189
|
+
return dimensionResult({
|
|
190
|
+
dimension,
|
|
191
|
+
status: leftJson === rightJson ? "unchanged" : "changed",
|
|
192
|
+
left,
|
|
193
|
+
right,
|
|
194
|
+
links,
|
|
195
|
+
providersDiffer,
|
|
196
|
+
leftCount: Array.isArray(left) ? left.length : null,
|
|
197
|
+
rightCount: Array.isArray(right) ? right.length : null,
|
|
198
|
+
textDelta: null,
|
|
199
|
+
limitations: [],
|
|
200
|
+
});
|
|
201
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import canonicalize from "canonicalize";
|
|
2
|
+
export const identityProjection = (snapshot) => ({
|
|
3
|
+
name: snapshot.procedure.name,
|
|
4
|
+
signature: snapshot.procedure.signature,
|
|
5
|
+
locals: snapshot.procedure.locals
|
|
6
|
+
.map(({ description }) => description)
|
|
7
|
+
.sort(),
|
|
8
|
+
});
|
|
9
|
+
export const functionMatch = (left, right) => {
|
|
10
|
+
if (!isAutoName(left.procedure.name) &&
|
|
11
|
+
left.procedure.name === right.procedure.name)
|
|
12
|
+
return {
|
|
13
|
+
status: "matched",
|
|
14
|
+
method: "symbol",
|
|
15
|
+
left_name: left.procedure.name,
|
|
16
|
+
right_name: right.procedure.name,
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
status: left.procedure.name === right.procedure.name ? "ambiguous" : "mismatched",
|
|
20
|
+
method: "explicit",
|
|
21
|
+
left_name: left.procedure.name,
|
|
22
|
+
right_name: right.procedure.name,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export const normalizeCfg = (blocks) => {
|
|
26
|
+
const parsed = blocks.map((block) => ({
|
|
27
|
+
block,
|
|
28
|
+
start: parseAddress(block.start),
|
|
29
|
+
end: parseAddress(block.end),
|
|
30
|
+
}));
|
|
31
|
+
if (parsed.some(({ start, end }) => start === null || end === null))
|
|
32
|
+
return null;
|
|
33
|
+
const ordered = parsed.sort((left, right) => (left.start ?? 0n) < (right.start ?? 0n) ? -1 : 1);
|
|
34
|
+
if (new Set(ordered.map(({ block }) => block.start)).size !== ordered.length)
|
|
35
|
+
return null;
|
|
36
|
+
if (ordered.some(({ start, end }) => (end ?? 0n) <= (start ?? 0n)))
|
|
37
|
+
return null;
|
|
38
|
+
const indices = new Map(ordered.map(({ block }, index) => [block.start, index]));
|
|
39
|
+
const graph = [];
|
|
40
|
+
for (const { block, start, end } of ordered) {
|
|
41
|
+
const successors = block.successors.map((address) => indices.get(address));
|
|
42
|
+
if (successors.some((index) => index === undefined))
|
|
43
|
+
return null;
|
|
44
|
+
graph.push({
|
|
45
|
+
size: String((end ?? 0n) - (start ?? 0n)),
|
|
46
|
+
successors: successors.sort((left, right) => (left ?? 0) - (right ?? 0)),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return graph;
|
|
50
|
+
};
|
|
51
|
+
export const referenceProjection = (snapshot) => sorted([
|
|
52
|
+
...snapshot.collections.incoming_references.items.map((item) => ({
|
|
53
|
+
direction: "in",
|
|
54
|
+
source: item.source_procedure?.name ?? null,
|
|
55
|
+
target: item.target_procedure?.name ?? null,
|
|
56
|
+
})),
|
|
57
|
+
...snapshot.collections.outgoing_references.items.map((item) => ({
|
|
58
|
+
direction: "out",
|
|
59
|
+
source: item.source_procedure?.name ?? null,
|
|
60
|
+
target: item.target_procedure?.name ?? null,
|
|
61
|
+
})),
|
|
62
|
+
]);
|
|
63
|
+
export const commentProjection = (snapshot) => {
|
|
64
|
+
const values = snapshot.collections.comments.items.map(({ address, kind, text }) => ({
|
|
65
|
+
offset: relativeAddress(address, snapshot.procedure.address),
|
|
66
|
+
kind,
|
|
67
|
+
text,
|
|
68
|
+
}));
|
|
69
|
+
return values.some(({ offset }) => offset === null) ? null : sorted(values);
|
|
70
|
+
};
|
|
71
|
+
export const stringAndNameProjection = (snapshot) => {
|
|
72
|
+
const values = [
|
|
73
|
+
...snapshot.collections.referenced_strings.items.map(({ source_address: sourceAddress, value }) => ({
|
|
74
|
+
kind: "string",
|
|
75
|
+
source_offset: relativeAddress(sourceAddress, snapshot.procedure.address),
|
|
76
|
+
value,
|
|
77
|
+
})),
|
|
78
|
+
...snapshot.collections.referenced_names.items.map(({ source_address: sourceAddress, value }) => ({
|
|
79
|
+
kind: "name",
|
|
80
|
+
source_offset: relativeAddress(sourceAddress, snapshot.procedure.address),
|
|
81
|
+
value,
|
|
82
|
+
})),
|
|
83
|
+
];
|
|
84
|
+
return values.some(({ source_offset: offset }) => offset === null)
|
|
85
|
+
? null
|
|
86
|
+
: sorted(values);
|
|
87
|
+
};
|
|
88
|
+
export const sorted = (values) => [...values].sort((left, right) => canonicalJson(left).localeCompare(canonicalJson(right)));
|
|
89
|
+
export const combineCoverage = (left, right) => ({
|
|
90
|
+
items: [...left.items, ...right.items],
|
|
91
|
+
total: left.total === null || right.total === null
|
|
92
|
+
? null
|
|
93
|
+
: left.total + right.total,
|
|
94
|
+
complete: left.complete && right.complete,
|
|
95
|
+
truncated: left.truncated || right.truncated,
|
|
96
|
+
});
|
|
97
|
+
export const project = (collection, mapper) => collection.items.map(mapper);
|
|
98
|
+
export const isAutoName = (name) => /^(?:sub_[0-9a-f]+|fcn\.)/iu.test(name);
|
|
99
|
+
const parseAddress = (value) => /^0x[0-9a-f]+$/iu.test(value) ? BigInt(value) : null;
|
|
100
|
+
const relativeAddress = (value, base) => {
|
|
101
|
+
const parsed = parseAddress(value);
|
|
102
|
+
const parsedBase = parseAddress(base);
|
|
103
|
+
return parsed === null || parsedBase === null || parsed < parsedBase
|
|
104
|
+
? null
|
|
105
|
+
: String(parsed - parsedBase);
|
|
106
|
+
};
|
|
107
|
+
const canonicalJson = (value) => {
|
|
108
|
+
const encoded = canonicalize(value);
|
|
109
|
+
if (encoded === undefined)
|
|
110
|
+
throw new TypeError("Function normalization could not canonicalize data");
|
|
111
|
+
return encoded;
|
|
112
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import canonicalize from "canonicalize";
|
|
3
|
+
export const dimensionResult = (input) => ({
|
|
4
|
+
dimension: input.dimension,
|
|
5
|
+
status: input.status,
|
|
6
|
+
left_digest: digest(input.left),
|
|
7
|
+
right_digest: digest(input.right),
|
|
8
|
+
left_count: input.leftCount,
|
|
9
|
+
right_count: input.rightCount,
|
|
10
|
+
text_delta: input.textDelta,
|
|
11
|
+
conclusion_kind: input.status === "changed" && input.providersDiffer
|
|
12
|
+
? "contradiction"
|
|
13
|
+
: "derived_relationship",
|
|
14
|
+
evidence_links: [...input.links],
|
|
15
|
+
limitations: [...input.limitations],
|
|
16
|
+
});
|
|
17
|
+
export const unresolvedDimension = (input) => ({
|
|
18
|
+
dimension: input.dimension,
|
|
19
|
+
status: input.status,
|
|
20
|
+
left_digest: null,
|
|
21
|
+
right_digest: null,
|
|
22
|
+
left_count: input.leftCount,
|
|
23
|
+
right_count: input.rightCount,
|
|
24
|
+
text_delta: null,
|
|
25
|
+
conclusion_kind: "unresolved_branch",
|
|
26
|
+
evidence_links: [...input.links],
|
|
27
|
+
limitations: [...input.limitations],
|
|
28
|
+
});
|
|
29
|
+
export const overallStatus = (dimensions, match) => {
|
|
30
|
+
if (dimensions.some(({ status }) => status === "truncated"))
|
|
31
|
+
return "truncated";
|
|
32
|
+
if (match === "ambiguous" ||
|
|
33
|
+
match === "unknown" ||
|
|
34
|
+
dimensions.some(({ status }) => status === "unknown"))
|
|
35
|
+
return "unknown";
|
|
36
|
+
if (match === "mismatched")
|
|
37
|
+
return "changed";
|
|
38
|
+
return dimensions.some(({ status }) => status === "changed")
|
|
39
|
+
? "changed"
|
|
40
|
+
: "unchanged";
|
|
41
|
+
};
|
|
42
|
+
export const summarize = (dimensions) => ({
|
|
43
|
+
unchanged: dimensions.filter(({ status }) => status === "unchanged").length,
|
|
44
|
+
changed: dimensions.filter(({ status }) => status === "changed").length,
|
|
45
|
+
truncated: dimensions.filter(({ status }) => status === "truncated").length,
|
|
46
|
+
unknown: dimensions.filter(({ status }) => status === "unknown").length,
|
|
47
|
+
});
|
|
48
|
+
export const canonicalJson = (value) => {
|
|
49
|
+
const encoded = canonicalize(value);
|
|
50
|
+
if (encoded === undefined)
|
|
51
|
+
throw new TypeError("Function comparison could not canonicalize data");
|
|
52
|
+
return encoded;
|
|
53
|
+
};
|
|
54
|
+
const digest = (value) => createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { evidenceSchema } from "./evidence.js";
|
|
3
|
+
const evidenceInput = z.union([
|
|
4
|
+
evidenceSchema,
|
|
5
|
+
z.array(evidenceSchema).min(1).max(100),
|
|
6
|
+
]);
|
|
7
|
+
const comparisonStatusSchema = z.enum([
|
|
8
|
+
"unchanged",
|
|
9
|
+
"added",
|
|
10
|
+
"removed",
|
|
11
|
+
"changed",
|
|
12
|
+
"truncated",
|
|
13
|
+
"unknown",
|
|
14
|
+
]);
|
|
15
|
+
const dimensionNameSchema = z.enum([
|
|
16
|
+
"identity",
|
|
17
|
+
"pseudocode",
|
|
18
|
+
"assembly",
|
|
19
|
+
"comments",
|
|
20
|
+
"calls",
|
|
21
|
+
"references",
|
|
22
|
+
"strings_names",
|
|
23
|
+
"cfg",
|
|
24
|
+
]);
|
|
25
|
+
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
|
|
26
|
+
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
27
|
+
/** Strict bounded input for explicit function-to-function comparison. */
|
|
28
|
+
export const functionComparisonInputSchema = z.object({
|
|
29
|
+
left: evidenceInput,
|
|
30
|
+
right: evidenceInput,
|
|
31
|
+
offset: z.number().int().min(0).default(0),
|
|
32
|
+
limit: z.number().int().min(1).max(100).default(100),
|
|
33
|
+
unknown_registry_approved: z.literal(true).optional(),
|
|
34
|
+
});
|
|
35
|
+
const textDeltaSchema = z.object({
|
|
36
|
+
added_lines: z.number().int().min(0),
|
|
37
|
+
removed_lines: z.number().int().min(0),
|
|
38
|
+
hunks: z.number().int().min(0),
|
|
39
|
+
});
|
|
40
|
+
const functionDimensionSchema = z.object({
|
|
41
|
+
dimension: dimensionNameSchema,
|
|
42
|
+
status: comparisonStatusSchema,
|
|
43
|
+
left_digest: digestSchema.nullable(),
|
|
44
|
+
right_digest: digestSchema.nullable(),
|
|
45
|
+
left_count: z.number().int().min(0).nullable(),
|
|
46
|
+
right_count: z.number().int().min(0).nullable(),
|
|
47
|
+
text_delta: textDeltaSchema.nullable(),
|
|
48
|
+
conclusion_kind: z.enum([
|
|
49
|
+
"derived_relationship",
|
|
50
|
+
"contradiction",
|
|
51
|
+
"unresolved_branch",
|
|
52
|
+
]),
|
|
53
|
+
evidence_links: z.array(evidenceIdSchema).min(2).max(200),
|
|
54
|
+
limitations: z.array(z.string()),
|
|
55
|
+
});
|
|
56
|
+
/** Deterministic dimension-classified function comparison Evidence payload. */
|
|
57
|
+
export const functionComparisonResultSchema = z.object({
|
|
58
|
+
status: comparisonStatusSchema,
|
|
59
|
+
function_match: z.object({
|
|
60
|
+
status: z.enum(["matched", "mismatched", "ambiguous", "unknown"]),
|
|
61
|
+
method: z.enum(["explicit", "symbol"]),
|
|
62
|
+
left_name: z.string(),
|
|
63
|
+
right_name: z.string(),
|
|
64
|
+
}),
|
|
65
|
+
left_subject_sha256: digestSchema,
|
|
66
|
+
right_subject_sha256: digestSchema,
|
|
67
|
+
summary: z.object({
|
|
68
|
+
unchanged: z.number().int().min(0),
|
|
69
|
+
changed: z.number().int().min(0),
|
|
70
|
+
truncated: z.number().int().min(0),
|
|
71
|
+
unknown: z.number().int().min(0),
|
|
72
|
+
}),
|
|
73
|
+
dimensions: z.array(functionDimensionSchema).length(8),
|
|
74
|
+
changes: z.object({
|
|
75
|
+
items: z.array(functionDimensionSchema).max(100),
|
|
76
|
+
offset: z.number().int().min(0),
|
|
77
|
+
limit: z.number().int().min(1).max(100),
|
|
78
|
+
total: z.number().int().min(0),
|
|
79
|
+
next_offset: z.number().int().min(0).nullable(),
|
|
80
|
+
}),
|
|
81
|
+
limitations: z.array(z.string()),
|
|
82
|
+
});
|