rea-agents 2.5.0 → 3.0.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 +14 -4
- package/bridge/ghidra/ReaGhidraBridge.java +477 -4
- package/bridge/hopper_bridge.py +54 -4
- package/dist/application/AnalysisContextQueries.js +130 -0
- package/dist/application/AnalysisSnapshotCache.js +4 -2
- package/dist/application/ArtifactInventory/scanCanonical.js +1 -1
- package/dist/application/BinarySession.js +5 -4
- package/dist/application/BinarySessionComposition.js +21 -0
- package/dist/application/BinarySessionRecords.js +79 -9
- package/dist/application/CapabilityInventory.js +16 -2
- package/dist/application/ClientRegistrationStatus.js +4 -1
- package/dist/application/DirectAnalysis.js +6 -2
- package/dist/application/EnhancedTools.js +37 -7
- package/dist/application/EvidenceLedger.js +45 -16
- package/dist/application/JavaScriptArtifactAnalysis.js +13 -4
- package/dist/application/LazyAnalysisProvider.js +77 -0
- package/dist/application/LoopbackReplay.js +1 -1
- package/dist/application/NativeApiInspection.js +80 -0
- package/dist/application/SessionProviderRouter.js +6 -6
- package/dist/application/SetupClientConfiguration.js +4 -2
- package/dist/application/runtime.js +26 -9
- package/dist/browser/PlaywrightBrowserScenarioProvider.js +8 -2
- package/dist/catalogIdentity.js +3 -3
- package/dist/cli/artifactCommands.js +5 -3
- package/dist/cli/coreAnalysisCommands.js +41 -0
- package/dist/cliCommandNames.js +1 -0
- package/dist/contracts/artifactToolContracts.js +8 -3
- package/dist/contracts/enhancedInputs.js +10 -0
- package/dist/contracts/functionWorkflowToolContracts.js +30 -0
- package/dist/contracts/promptContracts.js +14 -15
- package/dist/contracts/toolContractExamples.js +3 -0
- package/dist/contracts/toolContracts.js +21 -4
- package/dist/contracts/toolEffects.js +4 -0
- package/dist/contracts/toolOutputSchemaGroups.js +50 -10
- package/dist/domain/analysisErrorPresentation.js +8 -2
- package/dist/domain/analysisErrorProjection.js +1 -0
- package/dist/domain/bytecodeProvider.js +179 -0
- package/dist/domain/conformancePackage.js +169 -0
- package/dist/domain/conformanceReplay.js +104 -0
- package/dist/domain/conformanceTrustGate.js +193 -0
- package/dist/domain/customProtocolCapture.js +137 -0
- package/dist/domain/eventProcessTree.js +214 -0
- package/dist/domain/evidenceBundle.js +47 -0
- package/dist/domain/hopperStartupFailure.js +1 -1
- package/dist/domain/hopperValues.js +4 -1
- package/dist/domain/inputIssueProjection.js +7 -1
- package/dist/domain/javascriptSemanticAnalysis.js +8 -12
- package/dist/domain/javascriptSourceParser.js +15 -0
- package/dist/domain/javascriptStaticAnalysis.js +8 -12
- package/dist/domain/mobileApplicationInvestigation.js +159 -0
- package/dist/domain/nativeApiBoundary.js +130 -0
- package/dist/domain/objcSwiftMetadata.js +150 -0
- package/dist/domain/packageAnalysis.js +123 -0
- package/dist/domain/peInspection.js +196 -0
- package/dist/domain/protocolCapture.js +243 -0
- package/dist/dotnet/ManagedStaticProvider.js +34 -5
- package/dist/generatedMcpToolCatalog.js +37 -0
- package/dist/generatedPackageMetadata.js +4 -4
- package/dist/ghidra/GhidraDefaults.js +1 -1
- package/dist/ghidra/GhidraFunctionValues.js +27 -1
- package/dist/ghidra/GhidraProviderCapabilities.js +40 -38
- package/dist/hopper/HopperClient.js +26 -2
- package/dist/hopper/HopperProvider.js +42 -30
- package/dist/hopper/HopperRequestQueue.js +32 -4
- package/dist/hopper/HopperResponseStream.js +2 -2
- package/dist/hopper/protocol.js +63 -21
- package/dist/main/transport.js +2 -5
- package/dist/main.js +14 -12
- package/dist/mcpDoctor.js +2 -2
- package/dist/mcpStartupPolicy.js +14 -0
- package/dist/server/LazyToolCatalog.js +134 -0
- package/dist/server/createServer.js +64 -171
- package/dist/server/hydrateServerTools.js +191 -0
- package/dist/server/registerEnhancedTools.js +13 -3
- package/dist/server/registerEvidenceResources.js +50 -101
- package/dist/server/registerInvestigationTools.js +1 -2
- package/dist/server/registerJavaScriptApplicationGraphResource.js +0 -4
- package/dist/server/registerReconstructionObligationLedgerResource.js +0 -7
- package/dist/server/registerReconstructionReadinessResource.js +0 -7
- package/dist/server/registerSessionRecordTools.js +82 -12
- package/dist/server/registerSessionStatusTool.js +1 -1
- package/dist/server/registerSessionTools.js +21 -1
- package/dist/server/toolRegistrationOptions.js +21 -6
- package/dist/server/toolResult.js +12 -0
- package/package.json +21 -10
- package/scripts/package-runner-bootstrap.mjs +72 -0
- package/scripts/rea.mjs +11 -1
- package/skills/reverse-engineer-anything/SKILL.md +10 -3
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import canonicalize from "canonicalize";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { evidenceEnvelopeSchema } from "./evidence.js";
|
|
5
|
+
import { evidenceBundleSchema } from "./evidenceBundle.js";
|
|
6
|
+
/** Schema version for the conformance package format. */
|
|
7
|
+
export const CONFORMANCE_PACKAGE_VERSION = 1;
|
|
8
|
+
/** A stable, path-independent identifier for a conformance package. */
|
|
9
|
+
export const conformancePackageIdSchema = z
|
|
10
|
+
.string()
|
|
11
|
+
.regex(/^cp_[a-f0-9]{64}$/u);
|
|
12
|
+
/** Identifier for a single scenario within a package. */
|
|
13
|
+
export const scenarioIdSchema = z
|
|
14
|
+
.string()
|
|
15
|
+
.regex(/^[A-Za-z][A-Za-z0-9._-]{0,99}$/u);
|
|
16
|
+
/** Deterministic manifest for a scenario fixture. */
|
|
17
|
+
export const scenarioManifestSchema = z.strictObject({
|
|
18
|
+
scenario_id: scenarioIdSchema,
|
|
19
|
+
name: z.string().min(1),
|
|
20
|
+
description: z.string().min(1),
|
|
21
|
+
/** Source-owned fixture path relative to the repository root. */
|
|
22
|
+
fixture_path: z.string().min(1),
|
|
23
|
+
/** Expected exit code or null if unspecified. */
|
|
24
|
+
expected_exit_code: z.number().int().nullable(),
|
|
25
|
+
/** Expected output patterns that must appear in the capture. */
|
|
26
|
+
expected_patterns: z.array(z.string()).default([]),
|
|
27
|
+
});
|
|
28
|
+
/** Replay plan describing how a scenario is replayed deterministically. */
|
|
29
|
+
export const replayPlanSchema = z.strictObject({
|
|
30
|
+
scenario_id: scenarioIdSchema,
|
|
31
|
+
/** Ordered steps to execute during replay. */
|
|
32
|
+
steps: z
|
|
33
|
+
.array(z.strictObject({
|
|
34
|
+
step_id: z.string().min(1),
|
|
35
|
+
action: z.string().min(1),
|
|
36
|
+
arguments: z.array(z.string()).default([]),
|
|
37
|
+
timeout_ms: z.number().int().positive(),
|
|
38
|
+
}))
|
|
39
|
+
.min(1)
|
|
40
|
+
.max(100),
|
|
41
|
+
/** Environment variables to set (never inherit host paths). */
|
|
42
|
+
environment: z.record(z.string(), z.string()).default({}),
|
|
43
|
+
});
|
|
44
|
+
/** Shim plan for intercepting observable effects. */
|
|
45
|
+
export const shimPlanSchema = z.strictObject({
|
|
46
|
+
scenario_id: scenarioIdSchema,
|
|
47
|
+
/** Shims to install, each intercepting a named effect. */
|
|
48
|
+
shims: z
|
|
49
|
+
.array(z.strictObject({
|
|
50
|
+
shim_id: z.string().min(1),
|
|
51
|
+
kind: z.enum(["filesystem", "network", "process", "signal"]),
|
|
52
|
+
target: z.string().min(1),
|
|
53
|
+
policy: z.enum(["observe", "allow", "block", "emulate"]),
|
|
54
|
+
}))
|
|
55
|
+
.min(0)
|
|
56
|
+
.max(50),
|
|
57
|
+
});
|
|
58
|
+
/** Expected evidence for a scenario, checked against the captured run. */
|
|
59
|
+
export const expectedEvidenceSchema = z.strictObject({
|
|
60
|
+
scenario_id: scenarioIdSchema,
|
|
61
|
+
/** Expected evidence envelopes. */
|
|
62
|
+
envelopes: z.array(evidenceEnvelopeSchema).min(0).max(100),
|
|
63
|
+
/** Expected evidence bundle. */
|
|
64
|
+
bundle: evidenceBundleSchema.nullable(),
|
|
65
|
+
/** Required dimensions that must be present in the evidence. */
|
|
66
|
+
required_dimensions: z.array(z.string().min(1)).default([]),
|
|
67
|
+
});
|
|
68
|
+
/** Verifier contract describing how to validate conformance. */
|
|
69
|
+
export const verifierContractSchema = z.strictObject({
|
|
70
|
+
scenario_id: scenarioIdSchema,
|
|
71
|
+
/** Dimensions to verify. */
|
|
72
|
+
dimensions: z
|
|
73
|
+
.array(z.strictObject({
|
|
74
|
+
name: z.string().min(1),
|
|
75
|
+
required: z.boolean().default(true),
|
|
76
|
+
comparison: z.enum(["exact", "semantic", "fuzzy"]),
|
|
77
|
+
}))
|
|
78
|
+
.min(1)
|
|
79
|
+
.max(50),
|
|
80
|
+
/** Tolerance for timing differences in milliseconds. */
|
|
81
|
+
timing_tolerance_ms: z.number().int().nonnegative().default(0),
|
|
82
|
+
});
|
|
83
|
+
/** Top-level conformance package manifest. */
|
|
84
|
+
export const conformancePackageSchema = z.strictObject({
|
|
85
|
+
schema_version: z.literal(CONFORMANCE_PACKAGE_VERSION),
|
|
86
|
+
package_id: conformancePackageIdSchema,
|
|
87
|
+
name: z.string().min(1),
|
|
88
|
+
description: z.string().min(1),
|
|
89
|
+
created_at: z.string().datetime(),
|
|
90
|
+
/** Scenario manifests. */
|
|
91
|
+
scenarios: z.array(scenarioManifestSchema).min(1).max(100),
|
|
92
|
+
/** Replay plans keyed by scenario_id. */
|
|
93
|
+
replay_plans: z.array(replayPlanSchema).min(1).max(100),
|
|
94
|
+
/** Shim plans keyed by scenario_id. */
|
|
95
|
+
shim_plans: z.array(shimPlanSchema).default([]),
|
|
96
|
+
/** Expected evidence for each scenario. */
|
|
97
|
+
expected_evidence: z.array(expectedEvidenceSchema).min(1).max(100),
|
|
98
|
+
/** Verifier contracts for each scenario. */
|
|
99
|
+
verifier_contracts: z.array(verifierContractSchema).min(1).max(100),
|
|
100
|
+
});
|
|
101
|
+
/** Validate a conformance package and cross-check internal references. */
|
|
102
|
+
export function validateConformancePackage(input) {
|
|
103
|
+
const parsed = conformancePackageSchema.safeParse(input);
|
|
104
|
+
if (!parsed.success)
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: {
|
|
108
|
+
kind: "invalid_package",
|
|
109
|
+
message: parsed.error.issues[0]?.message ?? "invalid package",
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const pkg = parsed.data;
|
|
113
|
+
// Check for duplicate scenario IDs
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
for (const s of pkg.scenarios) {
|
|
116
|
+
if (seen.has(s.scenario_id))
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
error: { kind: "duplicate_scenario", scenario_id: s.scenario_id },
|
|
120
|
+
};
|
|
121
|
+
seen.add(s.scenario_id);
|
|
122
|
+
}
|
|
123
|
+
// Check that replay plans reference existing scenarios
|
|
124
|
+
for (const rp of pkg.replay_plans) {
|
|
125
|
+
if (!seen.has(rp.scenario_id))
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: {
|
|
129
|
+
kind: "scenario_not_found",
|
|
130
|
+
scenario_id: rp.scenario_id,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Check that expected evidence references existing scenarios
|
|
135
|
+
for (const ee of pkg.expected_evidence) {
|
|
136
|
+
if (!seen.has(ee.scenario_id))
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: {
|
|
140
|
+
kind: "scenario_not_found",
|
|
141
|
+
scenario_id: ee.scenario_id,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Check that verifier contracts reference existing scenarios
|
|
146
|
+
for (const vc of pkg.verifier_contracts) {
|
|
147
|
+
if (!seen.has(vc.scenario_id))
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
error: {
|
|
151
|
+
kind: "scenario_not_found",
|
|
152
|
+
scenario_id: vc.scenario_id,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, value: pkg };
|
|
157
|
+
}
|
|
158
|
+
/** Compute a deterministic package ID from the canonical JSON. */
|
|
159
|
+
export function computePackageId(pkg) {
|
|
160
|
+
const json = canonicalize(pkg);
|
|
161
|
+
if (!json)
|
|
162
|
+
throw new Error("failed to canonicalize package");
|
|
163
|
+
return `cp_${createHash("sha256").update(json).digest("hex")}`;
|
|
164
|
+
}
|
|
165
|
+
/** Create a conformance package with an auto-computed package_id. */
|
|
166
|
+
export function createConformancePackage(pkg) {
|
|
167
|
+
const package_id = computePackageId(pkg);
|
|
168
|
+
return { package_id, ...pkg };
|
|
169
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { conformancePackageSchema, } from "./conformancePackage.js";
|
|
3
|
+
import { trustGateResultSchema } from "./conformanceTrustGate.js";
|
|
4
|
+
/** Result of a single scenario replay. */
|
|
5
|
+
export const scenarioReplayResultSchema = z.strictObject({
|
|
6
|
+
scenario_id: z.string().min(1),
|
|
7
|
+
status: z.enum(["pass", "fail", "error", "skipped"]),
|
|
8
|
+
exit_code: z.number().int().nullable(),
|
|
9
|
+
duration_ms: z.number().int().nonnegative(),
|
|
10
|
+
output: z.string().default(""),
|
|
11
|
+
error: z.string().nullable(),
|
|
12
|
+
});
|
|
13
|
+
/** Result of replaying an entire conformance package. */
|
|
14
|
+
export const packageReplayResultSchema = z.strictObject({
|
|
15
|
+
package_id: z.string().min(1),
|
|
16
|
+
total_scenarios: z.number().int().positive(),
|
|
17
|
+
passed: z.number().int().nonnegative(),
|
|
18
|
+
failed: z.number().int().nonnegative(),
|
|
19
|
+
errored: z.number().int().nonnegative(),
|
|
20
|
+
skipped: z.number().int().nonnegative(),
|
|
21
|
+
scenario_results: z.array(scenarioReplayResultSchema),
|
|
22
|
+
trust_gate_results: z.array(trustGateResultSchema),
|
|
23
|
+
drift_detected: z.boolean(),
|
|
24
|
+
first_drift: z
|
|
25
|
+
.strictObject({
|
|
26
|
+
scenario_id: z.string().min(1),
|
|
27
|
+
dimension: z.string().min(1),
|
|
28
|
+
message: z.string(),
|
|
29
|
+
})
|
|
30
|
+
.nullable(),
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Default no-op scenario runner that skips all scenarios.
|
|
34
|
+
* Real CI replays inject an actual runner.
|
|
35
|
+
*/
|
|
36
|
+
async function defaultRunner() {
|
|
37
|
+
return {
|
|
38
|
+
scenario_id: "unknown",
|
|
39
|
+
status: "skipped",
|
|
40
|
+
exit_code: null,
|
|
41
|
+
duration_ms: 0,
|
|
42
|
+
output: "",
|
|
43
|
+
error: "no runner provided",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Replay a conformance package from a clean checkout.
|
|
48
|
+
* Runs each scenario, evaluates trust gates, and detects drift
|
|
49
|
+
* between the captured run and the committed package.
|
|
50
|
+
*/
|
|
51
|
+
export async function replayConformancePackage(packageInput, actualEvidence, runner = defaultRunner, options = {}) {
|
|
52
|
+
const parsed = conformancePackageSchema.safeParse(packageInput);
|
|
53
|
+
if (!parsed.success)
|
|
54
|
+
throw new TypeError(`invalid conformance package: ${parsed.error.issues[0]?.message ?? "unknown"}`);
|
|
55
|
+
const pkg = parsed.data;
|
|
56
|
+
const scenarioResults = [];
|
|
57
|
+
let passed = 0;
|
|
58
|
+
let failed = 0;
|
|
59
|
+
let errored = 0;
|
|
60
|
+
let skipped = 0;
|
|
61
|
+
for (const scenario of pkg.scenarios) {
|
|
62
|
+
const result = await runner(scenario.scenario_id, scenario.fixture_path);
|
|
63
|
+
scenarioResults.push(result);
|
|
64
|
+
switch (result.status) {
|
|
65
|
+
case "pass":
|
|
66
|
+
passed++;
|
|
67
|
+
break;
|
|
68
|
+
case "fail":
|
|
69
|
+
failed++;
|
|
70
|
+
break;
|
|
71
|
+
case "error":
|
|
72
|
+
errored++;
|
|
73
|
+
break;
|
|
74
|
+
case "skipped":
|
|
75
|
+
skipped++;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const trustGateResults = evaluatePackageTrustGates(pkg, actualEvidence, options);
|
|
80
|
+
const driftGates = trustGateResults.filter((r) => r.verdict === "fail");
|
|
81
|
+
const drift_detected = driftGates.length > 0;
|
|
82
|
+
const first_drift_gate = driftGates[0];
|
|
83
|
+
const first_drift = first_drift_gate?.first_divergence
|
|
84
|
+
? {
|
|
85
|
+
scenario_id: first_drift_gate.scenario_id,
|
|
86
|
+
dimension: first_drift_gate.first_divergence.dimension,
|
|
87
|
+
message: first_drift_gate.first_divergence.message,
|
|
88
|
+
}
|
|
89
|
+
: null;
|
|
90
|
+
return {
|
|
91
|
+
package_id: pkg.package_id,
|
|
92
|
+
total_scenarios: pkg.scenarios.length,
|
|
93
|
+
passed,
|
|
94
|
+
failed,
|
|
95
|
+
errored,
|
|
96
|
+
skipped,
|
|
97
|
+
scenario_results: scenarioResults,
|
|
98
|
+
trust_gate_results: trustGateResults,
|
|
99
|
+
drift_detected,
|
|
100
|
+
first_drift,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Import trust gate evaluator for replay. */
|
|
104
|
+
import { evaluatePackageTrustGates } from "./conformanceTrustGate.js";
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Result of comparing a single dimension. */
|
|
3
|
+
export const dimensionResultSchema = z.strictObject({
|
|
4
|
+
name: z.string().min(1),
|
|
5
|
+
status: z.enum(["match", "mismatch", "unknown", "truncated"]),
|
|
6
|
+
message: z.string().default(""),
|
|
7
|
+
evidence_ids: z.array(z.string()).default([]),
|
|
8
|
+
});
|
|
9
|
+
/** Result of evaluating a trust gate for one scenario. */
|
|
10
|
+
export const trustGateResultSchema = z.strictObject({
|
|
11
|
+
scenario_id: z.string().min(1),
|
|
12
|
+
verdict: z.enum(["pass", "fail", "unknown"]),
|
|
13
|
+
dimension_results: z.array(dimensionResultSchema),
|
|
14
|
+
first_divergence: z
|
|
15
|
+
.strictObject({
|
|
16
|
+
dimension: z.string().min(1),
|
|
17
|
+
evidence_id: z.string().min(1),
|
|
18
|
+
message: z.string(),
|
|
19
|
+
})
|
|
20
|
+
.nullable(),
|
|
21
|
+
});
|
|
22
|
+
/** Dimension that is volatile due to timing or normalization. */
|
|
23
|
+
const VOLATILE_DIMENSIONS = new Set([
|
|
24
|
+
"timing",
|
|
25
|
+
"timestamp",
|
|
26
|
+
"pid",
|
|
27
|
+
"ppid",
|
|
28
|
+
"duration_ms",
|
|
29
|
+
"created_at",
|
|
30
|
+
"updated_at",
|
|
31
|
+
]);
|
|
32
|
+
/** Dimension that carries semantic content. */
|
|
33
|
+
const SEMANTIC_DIMENSIONS = new Set([
|
|
34
|
+
"exit_code",
|
|
35
|
+
"stdout",
|
|
36
|
+
"stderr",
|
|
37
|
+
"filesystem",
|
|
38
|
+
"process_tree",
|
|
39
|
+
"shim_events",
|
|
40
|
+
"protocol_events",
|
|
41
|
+
"event_journal",
|
|
42
|
+
]);
|
|
43
|
+
/** Check if a dimension name is volatile (timing/normalization noise). */
|
|
44
|
+
export function isVolatileDimension(name) {
|
|
45
|
+
return VOLATILE_DIMENSIONS.has(name);
|
|
46
|
+
}
|
|
47
|
+
/** Check if a dimension name carries semantic content. */
|
|
48
|
+
export function isSemanticDimension(name) {
|
|
49
|
+
return SEMANTIC_DIMENSIONS.has(name);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compare two values for a dimension, classifying semantic diffs
|
|
53
|
+
* from timing/normalization noise.
|
|
54
|
+
*
|
|
55
|
+
* Volatile dimensions (timing, timestamps, PIDs) are always treated
|
|
56
|
+
* as matches because their values are expected to differ across
|
|
57
|
+
* runs and do not carry semantic meaning.
|
|
58
|
+
*/
|
|
59
|
+
export function compareDimension(dimensionName, expected, actual, options = {}) {
|
|
60
|
+
// Volatile dimensions: always match regardless of values
|
|
61
|
+
if (isVolatileDimension(dimensionName)) {
|
|
62
|
+
return {
|
|
63
|
+
name: dimensionName,
|
|
64
|
+
status: "match",
|
|
65
|
+
message: "volatile dimension ignored",
|
|
66
|
+
evidence_ids: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (options.truncated) {
|
|
70
|
+
return {
|
|
71
|
+
name: dimensionName,
|
|
72
|
+
status: "truncated",
|
|
73
|
+
message: "evidence was truncated",
|
|
74
|
+
evidence_ids: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (expected === undefined && actual === undefined) {
|
|
78
|
+
return {
|
|
79
|
+
name: dimensionName,
|
|
80
|
+
status: "unknown",
|
|
81
|
+
message: "both expected and actual are undefined",
|
|
82
|
+
evidence_ids: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (expected === undefined || actual === undefined) {
|
|
86
|
+
return {
|
|
87
|
+
name: dimensionName,
|
|
88
|
+
status: "mismatch",
|
|
89
|
+
message: `expected ${expected === undefined ? "present" : "absent"}, actual ${actual === undefined ? "absent" : "present"}`,
|
|
90
|
+
evidence_ids: [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Semantic comparison for structured objects
|
|
94
|
+
if (typeof expected === "object" && typeof actual === "object") {
|
|
95
|
+
const expectedJson = JSON.stringify(sortKeys(expected));
|
|
96
|
+
const actualJson = JSON.stringify(sortKeys(actual));
|
|
97
|
+
if (expectedJson === actualJson) {
|
|
98
|
+
return {
|
|
99
|
+
name: dimensionName,
|
|
100
|
+
status: "match",
|
|
101
|
+
message: "",
|
|
102
|
+
evidence_ids: [],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
name: dimensionName,
|
|
107
|
+
status: "mismatch",
|
|
108
|
+
message: "semantic content differs",
|
|
109
|
+
evidence_ids: [],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Primitive comparison
|
|
113
|
+
if (expected === actual) {
|
|
114
|
+
return {
|
|
115
|
+
name: dimensionName,
|
|
116
|
+
status: "match",
|
|
117
|
+
message: "",
|
|
118
|
+
evidence_ids: [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
name: dimensionName,
|
|
123
|
+
status: "mismatch",
|
|
124
|
+
message: `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
|
125
|
+
evidence_ids: [],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Recursively sort object keys for deterministic comparison. */
|
|
129
|
+
function sortKeys(value) {
|
|
130
|
+
if (value === null || typeof value !== "object")
|
|
131
|
+
return value;
|
|
132
|
+
if (Array.isArray(value))
|
|
133
|
+
return value.map(sortKeys);
|
|
134
|
+
const sorted = {};
|
|
135
|
+
for (const key of Object.keys(value).sort()) {
|
|
136
|
+
sorted[key] = sortKeys(value[key]);
|
|
137
|
+
}
|
|
138
|
+
return sorted;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Evaluate a trust gate for one scenario by comparing expected
|
|
142
|
+
* evidence against the actual captured run.
|
|
143
|
+
*/
|
|
144
|
+
export function evaluateTrustGate(contract, expectedEvidence, actualEvidence, options = {}) {
|
|
145
|
+
const dimensionResults = [];
|
|
146
|
+
let firstDivergence = null;
|
|
147
|
+
for (const dim of contract.dimensions) {
|
|
148
|
+
if (!dim.required)
|
|
149
|
+
continue;
|
|
150
|
+
const expectedValue = expectedEvidence[dim.name];
|
|
151
|
+
const actualValue = actualEvidence[dim.name];
|
|
152
|
+
const result = compareDimension(dim.name, expectedValue, actualValue, options);
|
|
153
|
+
dimensionResults.push(result);
|
|
154
|
+
if (result.status === "mismatch" && firstDivergence === null) {
|
|
155
|
+
firstDivergence = {
|
|
156
|
+
dimension: dim.name,
|
|
157
|
+
evidence_id: result.evidence_ids[0] ?? "unknown",
|
|
158
|
+
message: result.message,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const hasFail = dimensionResults.some((r) => r.status === "mismatch" || r.status === "truncated");
|
|
163
|
+
const hasUnknown = dimensionResults.some((r) => r.status === "unknown");
|
|
164
|
+
const verdict = hasFail
|
|
165
|
+
? "fail"
|
|
166
|
+
: hasUnknown
|
|
167
|
+
? "unknown"
|
|
168
|
+
: "pass";
|
|
169
|
+
return {
|
|
170
|
+
scenario_id: contract.scenario_id,
|
|
171
|
+
verdict,
|
|
172
|
+
dimension_results: dimensionResults,
|
|
173
|
+
first_divergence: firstDivergence,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Evaluate trust gates for all scenarios in a conformance package.
|
|
178
|
+
* Rejects runs that differ in required dimensions. Unknown/truncated
|
|
179
|
+
* evidence is never treated as equivalence.
|
|
180
|
+
*/
|
|
181
|
+
export function evaluatePackageTrustGates(pkg, actualEvidence, options = {}) {
|
|
182
|
+
const results = [];
|
|
183
|
+
for (const contract of pkg.verifier_contracts) {
|
|
184
|
+
const expected = pkg.expected_evidence.find((e) => e.scenario_id === contract.scenario_id);
|
|
185
|
+
if (!expected)
|
|
186
|
+
continue;
|
|
187
|
+
const expectedEvidence = expected.envelopes[0] ?? {};
|
|
188
|
+
const actual = actualEvidence[contract.scenario_id] ?? {};
|
|
189
|
+
const result = evaluateTrustGate(contract, expectedEvidence, actual, options);
|
|
190
|
+
results.push(result);
|
|
191
|
+
}
|
|
192
|
+
return results;
|
|
193
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { jsonValueSchema } from "./jsonValue.js";
|
|
3
|
+
/** Supported transport types for custom protocol capture. */
|
|
4
|
+
export const transportTypeSchema = z.enum([
|
|
5
|
+
"tcp",
|
|
6
|
+
"udp",
|
|
7
|
+
"ipc",
|
|
8
|
+
"unix-socket",
|
|
9
|
+
"named-pipe",
|
|
10
|
+
"xpc",
|
|
11
|
+
]);
|
|
12
|
+
/** Direction of a protocol frame. */
|
|
13
|
+
export const frameDirectionSchema = z.enum(["sent", "received", "intercepted"]);
|
|
14
|
+
/** A captured protocol frame. */
|
|
15
|
+
export const protocolFrameSchema = z.strictObject({
|
|
16
|
+
/** Monotonic sequence number. */
|
|
17
|
+
sequence: z.number().int().nonnegative(),
|
|
18
|
+
/** Timestamp in milliseconds. */
|
|
19
|
+
at_ms: z.number().int().nonnegative(),
|
|
20
|
+
/** Transport type. */
|
|
21
|
+
transport: transportTypeSchema,
|
|
22
|
+
/** Direction of the frame. */
|
|
23
|
+
direction: frameDirectionSchema,
|
|
24
|
+
/** Source process identity. */
|
|
25
|
+
source_pid: z.number().int().nullable(),
|
|
26
|
+
/** Destination process identity. */
|
|
27
|
+
dest_pid: z.number().int().nullable(),
|
|
28
|
+
/** Endpoint address (IP:port, socket path, pipe name). */
|
|
29
|
+
source_endpoint: z.string().nullable(),
|
|
30
|
+
/** Destination endpoint. */
|
|
31
|
+
dest_endpoint: z.string().nullable(),
|
|
32
|
+
/** Raw frame data as base64. */
|
|
33
|
+
raw_data: z.string().nullable(),
|
|
34
|
+
/** Decoded/parsed frame content if known. */
|
|
35
|
+
decoded_content: jsonValueSchema.nullable(),
|
|
36
|
+
/** Frame size in bytes. */
|
|
37
|
+
size: z.number().int().nonnegative(),
|
|
38
|
+
/** Whether the frame was truncated. */
|
|
39
|
+
truncated: z.boolean().default(false),
|
|
40
|
+
});
|
|
41
|
+
/** Authentication flow stage. */
|
|
42
|
+
export const authStageSchema = z.enum([
|
|
43
|
+
"init",
|
|
44
|
+
"challenge",
|
|
45
|
+
"response",
|
|
46
|
+
"token_exchange",
|
|
47
|
+
"renewal",
|
|
48
|
+
"revocation",
|
|
49
|
+
"failure",
|
|
50
|
+
"success",
|
|
51
|
+
]);
|
|
52
|
+
/** Token lifecycle event. */
|
|
53
|
+
export const tokenLifecycleEventSchema = z.enum([
|
|
54
|
+
"issued",
|
|
55
|
+
"refreshed",
|
|
56
|
+
"expired",
|
|
57
|
+
"revoked",
|
|
58
|
+
"renewed",
|
|
59
|
+
]);
|
|
60
|
+
/** A captured authentication flow event. */
|
|
61
|
+
export const authFlowEventSchema = z.strictObject({
|
|
62
|
+
/** Monotonic sequence number. */
|
|
63
|
+
sequence: z.number().int().nonnegative(),
|
|
64
|
+
/** Timestamp in milliseconds. */
|
|
65
|
+
at_ms: z.number().int().nonnegative(),
|
|
66
|
+
/** Authentication stage. */
|
|
67
|
+
stage: authStageSchema,
|
|
68
|
+
/** Protocol used. */
|
|
69
|
+
protocol: z.string().min(1),
|
|
70
|
+
/** Token type if applicable. */
|
|
71
|
+
token_type: z.string().nullable(),
|
|
72
|
+
/** Token lifecycle event. */
|
|
73
|
+
token_lifecycle: tokenLifecycleEventSchema.nullable(),
|
|
74
|
+
/** Correlation ID linking challenge-response pairs. */
|
|
75
|
+
correlation_id: z.string().nullable(),
|
|
76
|
+
/** Whether credentials were detected and redacted. */
|
|
77
|
+
credentials_redacted: z.boolean().default(false),
|
|
78
|
+
/** Whether the authentication succeeded. */
|
|
79
|
+
succeeded: z.boolean().default(false),
|
|
80
|
+
/** Error message if authentication failed. */
|
|
81
|
+
error: z.string().nullable(),
|
|
82
|
+
});
|
|
83
|
+
/** A captured protocol session. */
|
|
84
|
+
export const customProtocolCaptureSchema = z.strictObject({
|
|
85
|
+
/** Transport type for this session. */
|
|
86
|
+
transport: transportTypeSchema,
|
|
87
|
+
/** Captured frames in order. */
|
|
88
|
+
frames: z.array(protocolFrameSchema).min(0).max(100_000),
|
|
89
|
+
/** Authentication flow events. */
|
|
90
|
+
auth_events: z.array(authFlowEventSchema).default([]),
|
|
91
|
+
/** Whether any frame was truncated. */
|
|
92
|
+
has_truncated: z.boolean().default(false),
|
|
93
|
+
/** Whether credentials were detected. */
|
|
94
|
+
credentials_detected: z.boolean().default(false),
|
|
95
|
+
});
|
|
96
|
+
/** Correlate a frame with process identity. */
|
|
97
|
+
export function correlateProcessIdentity(frame, pid) {
|
|
98
|
+
return frame.source_pid === pid || frame.dest_pid === pid;
|
|
99
|
+
}
|
|
100
|
+
/** Check if a frame contains credential-like content. */
|
|
101
|
+
export function looksLikeCredentialFrame(frame) {
|
|
102
|
+
const patterns = [
|
|
103
|
+
"password",
|
|
104
|
+
"token",
|
|
105
|
+
"secret",
|
|
106
|
+
"api_key",
|
|
107
|
+
"apikey",
|
|
108
|
+
"authorization",
|
|
109
|
+
"credential",
|
|
110
|
+
];
|
|
111
|
+
const content = JSON.stringify(frame.decoded_content ?? "").toLowerCase();
|
|
112
|
+
return patterns.some((p) => content.includes(p));
|
|
113
|
+
}
|
|
114
|
+
/** Filter frames by transport type. */
|
|
115
|
+
export function framesByTransport(frames, transport) {
|
|
116
|
+
return frames.filter((f) => f.transport === transport);
|
|
117
|
+
}
|
|
118
|
+
/** Get all authentication flow events for a specific protocol. */
|
|
119
|
+
export function authEventsByProtocol(events, protocol) {
|
|
120
|
+
return events.filter((e) => e.protocol === protocol);
|
|
121
|
+
}
|
|
122
|
+
/** Get successful authentication events. */
|
|
123
|
+
export function successfulAuthEvents(events) {
|
|
124
|
+
return events.filter((e) => e.succeeded);
|
|
125
|
+
}
|
|
126
|
+
/** Get failed authentication events. */
|
|
127
|
+
export function failedAuthEvents(events) {
|
|
128
|
+
return events.filter((e) => !e.succeeded);
|
|
129
|
+
}
|
|
130
|
+
/** Compute the duration of an authentication flow in milliseconds. */
|
|
131
|
+
export function authFlowDuration(events) {
|
|
132
|
+
if (events.length === 0)
|
|
133
|
+
return null;
|
|
134
|
+
const first = events[0];
|
|
135
|
+
const last = events[events.length - 1];
|
|
136
|
+
return last.at_ms - first.at_ms;
|
|
137
|
+
}
|