rea-agents 5.0.0 → 6.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 +157 -650
- package/bridge/mitmproxy/capture.py +210 -0
- package/bridge/native/rea_lldb_tracer.py +303 -0
- package/bridge/process/ProcessRunTokenReader.swift +263 -0
- package/bridge/process/ProcessRunTokenReaderMain.swift +35 -0
- package/bridge/pwndbg/core_context.py +101 -0
- package/bridge/pwndbg/launch.py +39 -0
- package/bridge/pwndbg/launch_impl.py +34 -0
- package/bridge/pwntools/decoder_runtime.py +59 -0
- package/bridge/pwntools/layout.py +39 -0
- package/bridge/pwntools/layout_impl.py +309 -0
- package/bridge/pwntools/recorded_crash.py +39 -0
- package/bridge/pwntools/recorded_crash_impl.py +143 -0
- package/dist/android/JadxProvider.js +1 -0
- package/dist/application/CapabilityInventory.js +21 -0
- package/dist/application/DirectAnalysis.js +97 -23
- package/dist/application/EvidenceBundleFiles.js +1 -1
- package/dist/application/ReconstructionObligationCandidates.js +5 -1
- package/dist/application/WebNetworkCapturePort.js +1 -0
- package/dist/application/WebNetworkCaptureService.js +132 -0
- package/dist/application/WindowsCapabilities.js +1 -1
- package/dist/application/binary/AnalysisSnapshotCache.js +49 -6
- package/dist/application/binary/BinarySession.js +38 -10
- package/dist/application/binary/BinarySessionOpen.js +16 -3
- package/dist/application/binary/BinarySessionRecords.js +20 -0
- package/dist/application/binaryDiagnostics/BinaryLayoutPort.js +1 -0
- package/dist/application/binaryDiagnostics/BinaryLayoutService.js +57 -0
- package/dist/application/binaryDiagnostics/RecordedCrashPort.js +1 -0
- package/dist/application/binaryDiagnostics/RecordedCrashService.js +60 -0
- package/dist/application/evm/EvmInterfacePort.js +1 -0
- package/dist/application/evm/EvmInterfaceService.js +62 -0
- package/dist/application/javascript/JavaScriptApplicationEvidence.js +1 -2
- package/dist/application/javascript/JavaScriptApplicationService.js +15 -2
- package/dist/application/javascript/JavaScriptArtifactAnalysis.js +42 -23
- package/dist/application/javascript/JavaScriptArtifactGraphDocuments.js +26 -2
- package/dist/application/javascript/JavaScriptArtifactPathResolution.js +74 -24
- package/dist/application/javascript/JavaScriptArtifactReconstruction.js +23 -10
- package/dist/application/javascript/JavaScriptSemanticGraphAsyncProjection.js +7 -7
- package/dist/application/javascript/JavaScriptSemanticGraphBuilder.js +145 -70
- package/dist/application/javascript/JavaScriptSemanticGraphChildProcessProjection.js +14 -15
- package/dist/application/javascript/JavaScriptSemanticGraphConstruction.js +41 -1
- package/dist/application/javascript/JavaScriptSemanticGraphDataProjection.js +7 -7
- package/dist/application/javascript/JavaScriptSemanticGraphFlowProjection.js +5 -9
- package/dist/application/javascript/JavaScriptSemanticGraphObjectProjection.js +3 -3
- package/dist/application/javascript/JavaScriptSemanticGraphProjection.js +118 -12
- package/dist/application/javascript/JavaScriptSemanticGraphResourceProjection.js +3 -3
- package/dist/application/javascript/JavaScriptSemanticGraphValueProjection.js +5 -5
- package/dist/application/{ProcessCli.js → process/ProcessCli.js} +10 -10
- package/dist/application/{ProcessEvidence.js → process/ProcessEvidence.js} +4 -4
- package/dist/artifacts/ArtifactProvider.js +30 -3
- package/dist/artifacts/ArtifactProviderMetadata.js +2 -1
- package/dist/artifacts/ZipArtifactReader.js +3 -0
- package/dist/artifacts/apple/DylibResolutionReader.js +259 -0
- package/dist/artifacts/apple/MachoLoadCommandReader.js +294 -0
- package/dist/{application → artifacts/extraction}/ArtifactExtraction.js +10 -10
- package/dist/{application → artifacts/inventory}/ArtifactGraphConstruction.js +3 -3
- package/dist/{application → artifacts/inventory}/ArtifactInventory.js +4 -4
- package/dist/{application/ArtifactInventory → artifacts/inventory}/classify.js +2 -2
- package/dist/{application/ArtifactInventory → artifacts/inventory}/reader.js +5 -5
- package/dist/{application/ArtifactInventory → artifacts/inventory}/scanCanonical.js +3 -3
- package/dist/{application/ArtifactInventory → artifacts/inventory}/scanReader.js +5 -5
- package/dist/artifacts/readStableArtifact.js +20 -4
- package/dist/browser/CdpCaptureEventHandlers.js +90 -2
- package/dist/browser/CdpCaptureEvents.js +9 -0
- package/dist/browser/PlaywrightElectronActiveProvider.js +12 -3
- package/dist/browser/history/CaptureFailures.js +101 -0
- package/dist/browser/history/CaptureFormatError.js +10 -0
- package/dist/browser/history/CaptureRedaction.js +65 -0
- package/dist/browser/history/CaptureRelease.js +12 -0
- package/dist/browser/history/HarCapture.js +165 -0
- package/dist/browser/history/HarCaptureAdapter.js +20 -0
- package/dist/browser/history/HarCaptureProcess.js +57 -0
- package/dist/browser/history/HarJson.js +91 -0
- package/dist/browser/history/HarSchema.js +72 -0
- package/dist/browser/history/HistoricalCaptureDecoder.js +186 -0
- package/dist/browser/history/HistoricalCaptureFormatAdapter.js +1 -0
- package/dist/browser/history/MitmproxyCaptureAdapter.js +42 -0
- package/dist/cli/artifactCommands.js +26 -1
- package/dist/cli/binaryDiagnosticsCommands.js +39 -0
- package/dist/cli/coreAnalysisCommands.js +2 -2
- package/dist/cli/coreBinaryCommands.js +8 -8
- package/dist/cli/electronCommands.js +16 -9
- package/dist/cli/evmCommands.js +24 -0
- package/dist/cli/firmwareCommands.js +6 -3
- package/dist/cli/javascriptApplicationAnalysis.js +16 -3
- package/dist/cli/jsonOutput.js +133 -0
- package/dist/cli/nativeCallCommands.js +35 -0
- package/dist/cli/processCommands.js +1 -1
- package/dist/cli/streamedJsonOutput.js +56 -0
- package/dist/cli/utilityCommands.js +2 -0
- package/dist/cli/webNetworkCaptureCommands.js +40 -0
- package/dist/cli.js +9 -3
- package/dist/cliBrowserScenarioCommands.js +2 -2
- package/dist/cliCommandNames.js +6 -0
- package/dist/cliJsonInput.js +30 -1
- package/dist/cliOutput.js +21 -6
- package/dist/composition/binaryDiagnostics.js +8 -0
- package/dist/composition/evm.js +4 -0
- package/dist/composition/webNetworkCaptures.js +6 -0
- package/dist/contracts/artifactToolContracts.js +6 -0
- package/dist/contracts/browserCaptureToolInputSchema.js +43 -0
- package/dist/contracts/browserToolContracts.js +4 -3
- package/dist/contracts/errorSchemas.js +8 -1
- package/dist/contracts/evm/evmToolContracts.js +20 -0
- package/dist/contracts/investigationExamples.js +2 -2
- package/dist/contracts/managed/managedToolContracts.js +7 -3
- package/dist/contracts/native/binaryDiagnosticsToolContracts.js +42 -0
- package/dist/contracts/native/nativeToolContracts.js +17 -1
- package/dist/contracts/officialToolContracts.js +12 -9
- package/dist/contracts/process/processCaptureExample.js +2 -0
- package/dist/contracts/sessionLifecycleInputs.js +22 -5
- package/dist/contracts/sessionStatusContract.js +10 -2
- package/dist/contracts/sessionToolContracts.js +8 -1
- package/dist/contracts/sessionToolSchemas.js +10 -3
- package/dist/contracts/toolContractExamples.js +1 -1
- package/dist/contracts/toolContracts.js +6 -0
- package/dist/contracts/toolEffects.js +30 -0
- package/dist/contracts/toolOutputSchemaGroups.js +5 -1
- package/dist/contracts/toolOutputSchemaPrimitives.js +1 -1
- package/dist/contracts/webNetworkCaptureToolContracts.js +24 -0
- package/dist/domain/analysisErrorBase.js +15 -0
- package/dist/domain/analysisErrorCore.js +59 -5
- package/dist/domain/analysisErrorPresentation.js +29 -1
- package/dist/domain/analysisErrorProjection.js +52 -2
- package/dist/domain/analysisSnapshot.js +166 -2
- package/dist/domain/apple/dyldPaths.js +81 -0
- package/dist/domain/apple/dylibResolution.js +367 -0
- package/dist/domain/apple/dylibResolutionFindings.js +75 -0
- package/dist/domain/artifactPathSyntax.js +1 -1
- package/dist/domain/browserObservation.js +1 -1
- package/dist/domain/browserObservationSchemas.js +13 -0
- package/dist/domain/browserScenarioValues.js +9 -2
- package/dist/domain/callPathSchemas.js +1 -1
- package/dist/domain/canonicalDigest.js +13 -1
- package/dist/domain/changedBehavior.js +3 -3
- package/dist/domain/comparisonStatus.js +2 -30
- package/dist/domain/conformanceTrustGate.js +0 -16
- package/dist/domain/evidence.js +17 -5
- package/dist/domain/evm/evmInterface.js +63 -0
- package/dist/domain/explicitSensitiveFailure.js +92 -0
- package/dist/domain/explicitSensitiveValues.js +10 -0
- package/dist/domain/firmware/firmwareAnalysis.js +8 -1
- package/dist/domain/javascript/electronActiveObservation.js +9 -4
- package/dist/domain/javascript/htmlArtifactReferences.js +40 -0
- package/dist/domain/javascript/javascriptExportShapeComparison.js +21 -5
- package/dist/domain/javascript/javascriptExportShapeVariants.js +0 -10
- package/dist/domain/javascript/javascriptSemanticGraph.js +21 -5
- package/dist/domain/jsonValue.js +4 -0
- package/dist/domain/localPath.js +2 -0
- package/dist/domain/native/binaryLayout.js +357 -0
- package/dist/domain/native/nativeCallObservation.js +164 -0
- package/dist/domain/native/objcSwiftMetadata.js +0 -39
- package/dist/domain/native/recordedCrash.js +266 -0
- package/dist/domain/native/recordedCrashNoteCoverage.js +73 -0
- package/dist/domain/{processCapture.js → process/processCapture.js} +139 -2
- package/dist/domain/{processCaptureExample.js → process/processCaptureExample.js} +5 -0
- package/dist/domain/process/processCaptureParsing.js +54 -0
- package/dist/domain/{processCaptureValidation.js → process/processCaptureValidation.js} +18 -2
- package/dist/domain/{processComparison.js → process/processComparison.js} +1 -1
- package/dist/domain/{processObservation.js → process/processObservation.js} +1 -1
- package/dist/domain/{processTraceSpecification.js → process/processTraceSpecification.js} +1 -1
- package/dist/domain/providerSelectionError.js +4 -2
- package/dist/domain/reconstructionVerification.js +3 -3
- package/dist/domain/reconstructionVerificationSchemas.js +1 -1
- package/dist/domain/residualUnknown.js +3 -2
- package/dist/domain/staticRuntimeCorrelation.js +3 -3
- package/dist/domain/webCaptureDiff.js +1 -0
- package/dist/domain/webNetworkCapture.js +118 -0
- package/dist/domain/webNetworkCaptureCoordinates.js +58 -0
- package/dist/dotnet/ManagedExceptionRegionValidation.js +41 -0
- package/dist/dotnet/ManagedMemberInspector.js +4 -2
- package/dist/dotnet/ManagedMemberInspectorCore.js +38 -32
- package/dist/dotnet/ManagedMemberInstructionDecoder.js +23 -7
- package/dist/dotnet/ManagedMemberRows.js +14 -3
- package/dist/dotnet/ManagedMetadataHeaps.js +20 -2
- package/dist/dotnet/ManagedMetadataInventory.js +38 -28
- package/dist/dotnet/ManagedMetadataInventoryRows.js +24 -10
- package/dist/dotnet/ManagedMethodBodyReader.js +7 -1
- package/dist/dotnet/ManagedNativeBoundaryHelpers.js +15 -3
- package/dist/dotnet/ManagedNativeBoundaryInspector.js +3 -1
- package/dist/dotnet/ManagedPeReader.js +11 -0
- package/dist/evm/EvmBytecodeCarrier.js +21 -0
- package/dist/evm/EvmWorkerLimits.js +40 -0
- package/dist/evm/EvmoleFailures.js +126 -0
- package/dist/evm/EvmoleInterfaceAdapter.js +53 -0
- package/dist/evm/EvmoleInterfaceProvider.js +273 -0
- package/dist/evm/EvmoleInterfaceWorker.js +87 -0
- package/dist/evm/EvmoleRelease.js +16 -0
- package/dist/firmware/FirmwareCommand.js +1 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/ghidra/GhidraLauncher.js +1 -0
- package/dist/ghidra/GhidraProvider.js +2 -2
- package/dist/ghidra/GhidraProviderCapabilities.js +1 -1
- package/dist/hopper/BridgeLauncher.js +2 -0
- package/dist/hopper/LinuxPrivateDisplayProbe.js +4 -2
- package/dist/javascript/recovery/WakaruCommand.js +3 -0
- package/dist/javascript/sourceMaps/SourceMapCommand.js +1 -0
- package/dist/native/CommandRunner.js +2 -1
- package/dist/native/LldbCallTracer.js +278 -0
- package/dist/native/NativeCallObservation.js +135 -0
- package/dist/native/NativeMacOSProvider.js +19 -3
- package/dist/native/NativeMacOSProviderMetadata.js +8 -3
- package/dist/native/pwndbg/PwndbgCoreContext.js +160 -0
- package/dist/native/pwntools/PwntoolsDecoder.js +203 -0
- package/dist/native/pwntools/PwntoolsFailures.js +180 -0
- package/dist/native/pwntools/PwntoolsLayoutProvider.js +36 -0
- package/dist/native/pwntools/PwntoolsRecordedCrashProvider.js +75 -0
- package/dist/native/pwntools/PwntoolsRelease.js +19 -0
- package/dist/native/pwntools/PwntoolsResourceLimits.js +76 -0
- package/dist/native/pwntools/RecordedCrashSourceBindings.js +46 -0
- package/dist/native/pwntools/RecordedCrashStructureBindings.js +101 -0
- package/dist/process/DarwinProcessRunTokenReader.js +294 -0
- package/dist/process/OwnedCommand.js +113 -0
- package/dist/process/ProcessOwnership.js +263 -33
- package/dist/process/ProcessOwnershipObservation.js +161 -24
- package/dist/process/ProviderProcess.js +30 -3
- package/dist/{application → process/capture}/ProcessCaptureCapability.js +15 -0
- package/dist/{application → process/capture}/ProcessCaptureError.js +17 -1
- package/dist/process/capture/ProcessCaptureLifecycle.js +615 -0
- package/dist/{application → process/capture}/ProcessHarness.js +195 -29
- package/dist/{application → process/capture}/ProcessSampling.js +2 -2
- package/dist/server/createServer.js +22 -0
- package/dist/server/promptCompletion.js +10 -3
- package/dist/server/registerBinaryDiagnosticsTools.js +17 -0
- package/dist/server/registerEvidenceTools.js +1 -1
- package/dist/server/registerEvmTools.js +17 -0
- package/dist/server/registerProcessComparisonTool.js +1 -1
- package/dist/server/registerRecordedCrashTools.js +17 -0
- package/dist/server/registerSessionTools.js +2 -2
- package/dist/server/registerWebNetworkCaptureTool.js +28 -0
- package/dist/server/sessionAvailabilityPolicy.js +3 -0
- package/dist/server/sessionToolPolicies.js +1 -1
- package/dist/server/toolResult.js +1 -1
- package/native/windows/build/manifest.json +2 -2
- package/native/windows/build/rea-windows-x64.node +0 -0
- package/package.json +37 -9
- package/scripts/rea.mjs +11 -3
- package/skills/reverse-engineer-anything/SKILL.md +23 -3
- package/skills/reverse-engineer-anything/references/native-and-artifacts.md +29 -0
- package/third_party/README.md +60 -0
- package/third_party/evmole/LICENSE +21 -0
- package/third_party/evmole/README.md +22 -0
- package/third_party/pwndbg/LICENSE.md +21 -0
- package/third_party/pwndbg/README.md +23 -0
- package/third_party/pwntools/README.md +40 -0
- package/dist/application/AuthorizedArtifactInventory.js +0 -7
- package/dist/application/ProcessCaptureLifecycle.js +0 -299
- package/dist/application/StoreFileLock.js +0 -124
- package/dist/contracts/processCaptureExample.js +0 -2
- package/dist/domain/bytecodeProvider.js +0 -178
- package/dist/domain/customProtocolCapture.js +0 -137
- package/dist/domain/eventProcessTree.js +0 -225
- package/dist/domain/mobileApplicationInvestigation.js +0 -159
- package/dist/domain/packageAnalysis.js +0 -123
- package/dist/domain/peInspection.js +0 -197
- package/dist/domain/processCaptureParsing.js +0 -21
- package/dist/domain/protocolCapture.js +0 -217
- package/dist/domain/runtimeIdentification.js +0 -212
- package/dist/generatedMcpToolCatalog.js +0 -37
- /package/dist/application/{ArtifactExtractionDestination.js → artifacts/ArtifactExtractionDestination.js} +0 -0
- /package/dist/{application/ArtifactInventory → artifacts/inventory}/policy.js +0 -0
- /package/dist/{application/ArtifactInventory → artifacts/inventory}/types.js +0 -0
- /package/dist/domain/{processEvidenceProvider.js → process/processEvidenceProvider.js} +0 -0
- /package/dist/domain/{processScenario.js → process/processScenario.js} +0 -0
- /package/dist/domain/{processTraceComparison.js → process/processTraceComparison.js} +0 -0
- /package/dist/domain/{processTraceDimensionProjection.js → process/processTraceDimensionProjection.js} +0 -0
- /package/dist/domain/{processTraceEvaluation.js → process/processTraceEvaluation.js} +0 -0
- /package/dist/{application → process/capture}/FilesystemSnapshot.js +0 -0
- /package/dist/{application → process/capture}/ProcessCaptureEnvironment.js +0 -0
- /package/dist/{application → process/capture}/ProcessCaptureJournal.js +0 -0
- /package/dist/{application → process/capture}/ProcessFilesystemEffects.js +0 -0
- /package/dist/{application → process/capture}/ProcessNormalization.js +0 -0
- /package/dist/{application → process/capture}/ProcessScenarioRuntimeValidation.js +0 -0
- /package/dist/{application → process/capture}/ProcessTimer.js +0 -0
- /package/dist/{application → process/capture}/TerminalRenderer.js +0 -0
|
@@ -9,7 +9,7 @@ import { BrowserObservationError } from "../domain/browserObservationError.js";
|
|
|
9
9
|
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
|
10
10
|
import { err, ok } from "../domain/result.js";
|
|
11
11
|
import { cleanupOwnedProcessGroup, cleanupWindowsProcessTree, verifyNoTokenOwnedProcesses, } from "../process/ProcessOwnership.js";
|
|
12
|
-
import { observeOwnedProcessLineage } from "../process/ProcessOwnershipObservation.js";
|
|
12
|
+
import { observeOwnedProcessLineage, prepareProcessOwnershipInspection, systemProcessOwnershipHost, } from "../process/ProcessOwnershipObservation.js";
|
|
13
13
|
import { selectCapturedProcessGroupIds } from "../process/ProcessOwnershipProcessTree.js";
|
|
14
14
|
import { runElectronActions, readApplicationState, runWithExecutionLimits, } from "./PlaywrightElectronActiveActions.js";
|
|
15
15
|
const OPERATION = "capture_electron_scenario";
|
|
@@ -101,8 +101,12 @@ export class PlaywrightElectronActiveProvider {
|
|
|
101
101
|
try {
|
|
102
102
|
if (options.signal?.aborted === true)
|
|
103
103
|
throw new BrowserObservationError(OPERATION, "cancelled");
|
|
104
|
+
await prepareProcessOwnershipInspection(options.signal);
|
|
104
105
|
const startupDeadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
105
106
|
const paths = await canonicalPaths(input);
|
|
107
|
+
const captureBaseline = await systemProcessOwnershipHost.captureBaseline?.(options.signal);
|
|
108
|
+
if (options.signal?.aborted)
|
|
109
|
+
throw new BrowserObservationError(OPERATION, "cancelled");
|
|
106
110
|
application = await electron.launch({
|
|
107
111
|
executablePath: paths.executable,
|
|
108
112
|
cwd: paths.root,
|
|
@@ -122,13 +126,18 @@ export class PlaywrightElectronActiveProvider {
|
|
|
122
126
|
expectedParentPid: process.pid,
|
|
123
127
|
expectedCommand: paths.executable,
|
|
124
128
|
sweepTokenOwnedProcesses: true,
|
|
129
|
+
...(captureBaseline === undefined ? {} : { captureBaseline }),
|
|
125
130
|
};
|
|
126
131
|
const actions = await runElectronActions(application, input, options);
|
|
127
132
|
const state = await runWithExecutionLimits(readApplicationState(application), options.signal);
|
|
128
133
|
outcome = ok(createResult(paths, input, actions, state));
|
|
129
134
|
}
|
|
130
135
|
catch (cause) {
|
|
131
|
-
outcome = err(
|
|
136
|
+
outcome = err(options.signal?.aborted === true &&
|
|
137
|
+
(cause === options.signal.reason ||
|
|
138
|
+
(cause instanceof Error && cause.name === "AbortError"))
|
|
139
|
+
? new BrowserObservationError(OPERATION, "cancelled", { cause })
|
|
140
|
+
: providerError(cause));
|
|
132
141
|
}
|
|
133
142
|
if (application !== undefined) {
|
|
134
143
|
if (ownership !== undefined)
|
|
@@ -200,7 +209,7 @@ const cleanupElectronProcesses = async (ownership, lineage) => {
|
|
|
200
209
|
return result;
|
|
201
210
|
signaled ||= result.signaled;
|
|
202
211
|
}
|
|
203
|
-
const remaining = await verifyNoTokenOwnedProcesses(ownership.runId);
|
|
212
|
+
const remaining = await verifyNoTokenOwnedProcesses(ownership.runId, undefined, ownership.captureBaseline);
|
|
204
213
|
if (!remaining.cleaned)
|
|
205
214
|
return remaining;
|
|
206
215
|
return { cleaned: true, signaled };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
|
2
|
+
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
|
3
|
+
import { AnalysisAccessDeniedError, AnalysisArtifactChangedError, AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisInputError, AnalysisOutputError, AnalysisTimeoutError, } from "../../domain/analysisErrorCore.js";
|
|
4
|
+
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
|
5
|
+
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
|
6
|
+
import { WEB_NETWORK_CAPTURE_LIMITS, } from "../../domain/webNetworkCapture.js";
|
|
7
|
+
import { OwnedCommandFailure } from "../../process/OwnedCommand.js";
|
|
8
|
+
const OPERATION = "inspect_web_network_capture";
|
|
9
|
+
/** Preserve the origin and typed cause of a historical capture failure. */
|
|
10
|
+
export const historicalCaptureFailure = (input, cause, phase, options) => {
|
|
11
|
+
if (cause instanceof AnalysisError)
|
|
12
|
+
return cause;
|
|
13
|
+
if (cause instanceof OwnedCommandFailure) {
|
|
14
|
+
if (cause.cleanupFailure !== null)
|
|
15
|
+
return new ProviderCleanupError(input.format, cause.resources, {
|
|
16
|
+
reason: cause.cleanupFailure,
|
|
17
|
+
previous_error: {
|
|
18
|
+
failure_kind: cause.reason,
|
|
19
|
+
message: cause.message,
|
|
20
|
+
exit_code: cause.snapshot?.exitCode ?? null,
|
|
21
|
+
signal: cause.snapshot?.signal ?? null,
|
|
22
|
+
stdout: cause.snapshot?.stdout.text ?? null,
|
|
23
|
+
stderr: cause.snapshot?.stderr.text ?? null,
|
|
24
|
+
diagnostic_truncated: cause.snapshot?.diagnosticTruncated ?? null,
|
|
25
|
+
},
|
|
26
|
+
}, { operation: OPERATION, cause });
|
|
27
|
+
if (cause.reason === "cancelled")
|
|
28
|
+
return new AnalysisCancelledError(OPERATION);
|
|
29
|
+
if (cause.reason === "timeout")
|
|
30
|
+
return new AnalysisTimeoutError(OPERATION, WEB_NETWORK_CAPTURE_LIMITS.timeoutMs);
|
|
31
|
+
if (cause.reason === "output-limit")
|
|
32
|
+
return new AnalysisOutputError(OPERATION, cause.message);
|
|
33
|
+
}
|
|
34
|
+
if (options?.signal?.aborted)
|
|
35
|
+
return new AnalysisCancelledError(OPERATION);
|
|
36
|
+
if (phase === "capture-read" &&
|
|
37
|
+
cause instanceof ArtifactReaderFailure &&
|
|
38
|
+
cause.reason === "integrity")
|
|
39
|
+
return new AnalysisArtifactChangedError(OPERATION, input.capture_path, cause.message, { cause });
|
|
40
|
+
if (phase === "capture-read" &&
|
|
41
|
+
cause instanceof ArtifactReaderFailure &&
|
|
42
|
+
cause.reason === "cancelled")
|
|
43
|
+
return new AnalysisCancelledError(OPERATION);
|
|
44
|
+
if (phase === "capture-read" &&
|
|
45
|
+
cause instanceof ArtifactReaderFailure &&
|
|
46
|
+
(cause.reason === "limit" ||
|
|
47
|
+
cause.reason === "format" ||
|
|
48
|
+
cause.reason === "path"))
|
|
49
|
+
return new AnalysisInputError(OPERATION, { cause }, [
|
|
50
|
+
{
|
|
51
|
+
path: ["capture_path"],
|
|
52
|
+
reason: cause.reason === "limit" ? "out_of_range" : "invalid_format",
|
|
53
|
+
message: cause.reason === "limit"
|
|
54
|
+
? `${cause.message}. Select a capture within the ${WEB_NETWORK_CAPTURE_LIMITS.inputBytes}-byte input budget.`
|
|
55
|
+
: cause.message,
|
|
56
|
+
...(cause.reason === "limit"
|
|
57
|
+
? {
|
|
58
|
+
expected: {
|
|
59
|
+
maximum_capture_bytes: WEB_NETWORK_CAPTURE_LIMITS.inputBytes,
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
: {}),
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
if (phase === "capture-read" &&
|
|
66
|
+
cause instanceof Error &&
|
|
67
|
+
"code" in cause &&
|
|
68
|
+
(cause.code === "EACCES" || cause.code === "EPERM"))
|
|
69
|
+
return new AnalysisAccessDeniedError(OPERATION, input.capture_path, cause.code, { cause });
|
|
70
|
+
if (phase === "capture-read" &&
|
|
71
|
+
cause instanceof Error &&
|
|
72
|
+
"code" in cause &&
|
|
73
|
+
["ENOENT", "ENOTDIR"].includes(String(cause.code)))
|
|
74
|
+
return new AnalysisInputError(OPERATION, { cause }, [
|
|
75
|
+
{
|
|
76
|
+
path: ["capture_path"],
|
|
77
|
+
reason: "invalid_value",
|
|
78
|
+
message: `Selected capture could not be read (${String(cause.code)}): ${input.capture_path}.`,
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
return new ProviderAdapterError(input.format, OPERATION, {
|
|
82
|
+
cause,
|
|
83
|
+
diagnostics: {
|
|
84
|
+
phase,
|
|
85
|
+
capture_path: input.capture_path,
|
|
86
|
+
reason: cause instanceof Error ? cause.message : String(cause),
|
|
87
|
+
...(cause instanceof ArtifactReaderFailure
|
|
88
|
+
? { failure_kind: cause.reason }
|
|
89
|
+
: {}),
|
|
90
|
+
...(cause instanceof OwnedCommandFailure && cause.snapshot !== null
|
|
91
|
+
? {
|
|
92
|
+
exit_code: cause.snapshot.exitCode ?? null,
|
|
93
|
+
signal: cause.snapshot.signal ?? null,
|
|
94
|
+
stdout: cause.snapshot.stdout.text,
|
|
95
|
+
stderr: cause.snapshot.stderr.text,
|
|
96
|
+
diagnostic_truncated: cause.snapshot.diagnosticTruncated ?? false,
|
|
97
|
+
}
|
|
98
|
+
: {}),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Expected malformed, unsupported or oversized producer data at a historical decoder boundary. */
|
|
2
|
+
export class CaptureFormatError extends Error {
|
|
3
|
+
reason;
|
|
4
|
+
pointer;
|
|
5
|
+
constructor(reason, message, pointer = "") {
|
|
6
|
+
super(message);
|
|
7
|
+
this.reason = reason;
|
|
8
|
+
this.pointer = pointer;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const CREDENTIAL_HEADERS = new Set([
|
|
2
|
+
"authorization",
|
|
3
|
+
"proxy-authorization",
|
|
4
|
+
"cookie",
|
|
5
|
+
"set-cookie",
|
|
6
|
+
]);
|
|
7
|
+
/** Literal redaction and structural authentication exclusions preserve unrelated local evidence. */
|
|
8
|
+
export class CaptureRedaction {
|
|
9
|
+
sensitiveValues;
|
|
10
|
+
redactions = [];
|
|
11
|
+
#sensitiveByteValues;
|
|
12
|
+
constructor(sensitiveValues) {
|
|
13
|
+
this.sensitiveValues = sensitiveValues;
|
|
14
|
+
// An unpaired surrogate has no UTF-8 representation. Buffer.from would
|
|
15
|
+
// invent replacement-character bytes and incorrectly discard real evidence.
|
|
16
|
+
this.#sensitiveByteValues = sensitiveValues.flatMap((value) => {
|
|
17
|
+
const encoded = Buffer.from(value, "utf8");
|
|
18
|
+
return encoded.toString("utf8") === value ? [encoded] : [];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** Preserve exact text unless explicitly sensitive or known transport URL userinfo. */
|
|
22
|
+
text(value, pointer, transportUrl = false) {
|
|
23
|
+
let result = value;
|
|
24
|
+
if (transportUrl) {
|
|
25
|
+
const match = /^([\x00-\x20]*(?:[a-z][a-z0-9+.-]*:)?\/\/)([^/?#]*@)/i.exec(result);
|
|
26
|
+
if (match !== null) {
|
|
27
|
+
result = `${match[1]}${result.slice(match[0].length)}`;
|
|
28
|
+
this.redactions.push({ pointer, reason: "transport-credential" });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (this.sensitiveValues.some((literal) => value.includes(literal) || result.includes(literal))) {
|
|
32
|
+
this.redactions.push({ pointer, reason: "explicit-sensitive-value" });
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
/** Remove a credential field's value without retaining its digest or encoded form. */
|
|
38
|
+
credential(pointer) {
|
|
39
|
+
this.redactions.push({ pointer, reason: "transport-credential" });
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** Known HAR credential surfaces; unknown extensions are ordinary caller-selected evidence. */
|
|
43
|
+
harCredential(pointer, parent) {
|
|
44
|
+
return ((/^\/(request|response)\/headers\/\d+\/value$/.test(pointer) &&
|
|
45
|
+
typeof parent.name === "string" &&
|
|
46
|
+
CREDENTIAL_HEADERS.has(parent.name.toLowerCase())) ||
|
|
47
|
+
/^\/(request|response)\/cookies\/\d+\/value$/.test(pointer));
|
|
48
|
+
}
|
|
49
|
+
/** Only actual request/redirect fields and known transport header URLs declare URL semantics. */
|
|
50
|
+
harTransportUrl(pointer, parent) {
|
|
51
|
+
return (pointer === "/request/url" ||
|
|
52
|
+
pointer === "/response/redirectURL" ||
|
|
53
|
+
(/^\/(request|response)\/headers\/\d+\/value$/.test(pointer) &&
|
|
54
|
+
typeof parent?.name === "string" &&
|
|
55
|
+
["location", "referer", "origin"].includes(parent.name.toLowerCase())));
|
|
56
|
+
}
|
|
57
|
+
/** Preserve bytes unless an explicitly declared UTF-8 literal occurs in them. */
|
|
58
|
+
sensitiveBytes(bytes) {
|
|
59
|
+
return this.#sensitiveByteValues.some((value) => bytes.includes(value));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Escape a producer property for an RFC 6901 location. */
|
|
63
|
+
export const capturePointer = (parent, key) => `${parent}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
64
|
+
/** Structural record guard does not duck-type number/byte sidecar markers. */
|
|
65
|
+
export const captureObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Exact upstream package profile for unchanged offline HAR validation and numeric parsing. */
|
|
2
|
+
export const HAR_CAPTURE_PROVIDER_IDENTITY = {
|
|
3
|
+
id: "har-schema/lossless-json",
|
|
4
|
+
name: "REA HAR capture adapter",
|
|
5
|
+
version: "har-schema@2.0.0;jsonc-parser@3.3.1;lossless-json@4.3.1",
|
|
6
|
+
};
|
|
7
|
+
/** Native executable profile verified with unchanged mitmproxy raw-state decoding. */
|
|
8
|
+
export const MITMPROXY_CAPTURE_PROVIDER_IDENTITY = {
|
|
9
|
+
id: "mitmproxy-native-tnetstring",
|
|
10
|
+
name: "REA offline native mitmproxy adapter",
|
|
11
|
+
version: "12.2.3",
|
|
12
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isSafeNumber, LosslessNumber } from "lossless-json";
|
|
3
|
+
import { parseHarJson } from "./HarJson.js";
|
|
4
|
+
import { WEB_NETWORK_CAPTURE_LIMITS, } from "../../domain/webNetworkCapture.js";
|
|
5
|
+
import { CaptureFormatError } from "./CaptureFormatError.js";
|
|
6
|
+
import { CaptureRedaction, captureObject, capturePointer, } from "./CaptureRedaction.js";
|
|
7
|
+
import { createHarValidator } from "./HarSchema.js";
|
|
8
|
+
import { HAR_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js";
|
|
9
|
+
/** Decode a retained HAR with exact numeric lexemes and unchanged upstream schema validation. */
|
|
10
|
+
export const decodeHarCapture = (text, sensitiveValues) => {
|
|
11
|
+
const raw = parseHarJson(text);
|
|
12
|
+
const validation = projectHar(raw, [], false);
|
|
13
|
+
createHarValidator()(validation.reported);
|
|
14
|
+
if (!captureObject(raw) ||
|
|
15
|
+
!captureObject(raw.log) ||
|
|
16
|
+
!Array.isArray(raw.log.entries))
|
|
17
|
+
throw new CaptureFormatError("format", "HAR must contain log.entries.");
|
|
18
|
+
if (raw.log.version !== "1.2")
|
|
19
|
+
throw new CaptureFormatError("unsupported", "Only the pinned HAR 1.2 schema profile is supported.", "/log/version");
|
|
20
|
+
const entries = raw.log.entries;
|
|
21
|
+
const recordsExcluded = ["log", "entries"].some((key) => sensitiveValues.some((literal) => key.includes(literal)));
|
|
22
|
+
const container = projectHar({ ...raw, log: { ...raw.log, entries: null } }, sensitiveValues, true);
|
|
23
|
+
const records = entries.map((entry, ordinal) => {
|
|
24
|
+
// Validate each original record even when a structural ancestor excludes it.
|
|
25
|
+
const projected = projectHar(entry, sensitiveValues, true);
|
|
26
|
+
const projection = recordsExcluded
|
|
27
|
+
? {
|
|
28
|
+
reported: null,
|
|
29
|
+
numeric_literals: [],
|
|
30
|
+
binary_fields: [],
|
|
31
|
+
redactions: [{ pointer: "", reason: "explicit-sensitive-value" }],
|
|
32
|
+
}
|
|
33
|
+
: projected;
|
|
34
|
+
return {
|
|
35
|
+
ordinal,
|
|
36
|
+
location: recordsExcluded
|
|
37
|
+
? { kind: "unknown", reason: "explicit-sensitive-value" }
|
|
38
|
+
: { kind: "json-pointer", pointer: `/log/entries/${ordinal}` },
|
|
39
|
+
...projection,
|
|
40
|
+
limitations: [
|
|
41
|
+
"HAR text is producer-decoded Unicode. Original body bytes remain unknown unless content explicitly uses valid base64; declared sizes are preserved independently of retained bytes. mitmproxy 12.2.3 SaveHar may report null optional postData.text; that null is retained and omitted only from the upstream validation copy.",
|
|
42
|
+
...(recordsExcluded
|
|
43
|
+
? [
|
|
44
|
+
"This record's payload and sidecars are excluded because its structural ancestor property was explicitly marked sensitive.",
|
|
45
|
+
]
|
|
46
|
+
: []),
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
decoder: { ...HAR_CAPTURE_PROVIDER_IDENTITY },
|
|
52
|
+
container: {
|
|
53
|
+
reported: container.reported,
|
|
54
|
+
numeric_literals: container.numeric_literals,
|
|
55
|
+
redactions: container.redactions,
|
|
56
|
+
records_pointer: recordsExcluded ? null : "/log/entries",
|
|
57
|
+
},
|
|
58
|
+
total_records: records.length,
|
|
59
|
+
records,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const projectHar = (value, sensitiveValues, redact) => {
|
|
63
|
+
const redactor = new CaptureRedaction(sensitiveValues);
|
|
64
|
+
const numeric_literals = [];
|
|
65
|
+
const binary_fields = [];
|
|
66
|
+
const visit = (item, pointer, depth, parent) => {
|
|
67
|
+
if (depth > WEB_NETWORK_CAPTURE_LIMITS.depth)
|
|
68
|
+
throw new CaptureFormatError("input-limit", "HAR exceeds the 64-level complete-evidence nesting budget.", pointer);
|
|
69
|
+
if (redact &&
|
|
70
|
+
parent !== undefined &&
|
|
71
|
+
redactor.harCredential(pointer, parent))
|
|
72
|
+
return redactor.credential(pointer);
|
|
73
|
+
if (item instanceof LosslessNumber) {
|
|
74
|
+
if (redact && redactor.text(item.value, pointer) === null)
|
|
75
|
+
return null;
|
|
76
|
+
numeric_literals.push({
|
|
77
|
+
pointer,
|
|
78
|
+
producer_type: "json-number",
|
|
79
|
+
literal: item.value,
|
|
80
|
+
});
|
|
81
|
+
return isSafeNumber(item.value) ? Number(item.value) : null;
|
|
82
|
+
}
|
|
83
|
+
if (typeof item === "string") {
|
|
84
|
+
if (redact &&
|
|
85
|
+
pointer === "/response/content/text" &&
|
|
86
|
+
parent?.encoding === "base64") {
|
|
87
|
+
const bytes = decodeHarBase64(item, pointer);
|
|
88
|
+
const contentBase64 = bytes.toString("base64");
|
|
89
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
90
|
+
const hidden = redactor.sensitiveBytes(bytes) ||
|
|
91
|
+
sensitiveValues.some((literal) => item.includes(literal) ||
|
|
92
|
+
contentBase64.includes(literal) ||
|
|
93
|
+
sha256.includes(literal));
|
|
94
|
+
binary_fields.push(hidden
|
|
95
|
+
? {
|
|
96
|
+
pointer,
|
|
97
|
+
representation: "har-base64-content",
|
|
98
|
+
state: "redacted",
|
|
99
|
+
content_base64: null,
|
|
100
|
+
bytes: null,
|
|
101
|
+
sha256: null,
|
|
102
|
+
}
|
|
103
|
+
: {
|
|
104
|
+
pointer,
|
|
105
|
+
representation: "har-base64-content",
|
|
106
|
+
state: "retained",
|
|
107
|
+
content_base64: contentBase64,
|
|
108
|
+
bytes: bytes.length,
|
|
109
|
+
sha256,
|
|
110
|
+
});
|
|
111
|
+
if (hidden) {
|
|
112
|
+
redactor.redactions.push({
|
|
113
|
+
pointer,
|
|
114
|
+
reason: "explicit-sensitive-value",
|
|
115
|
+
});
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return redact
|
|
120
|
+
? redactor.text(item, pointer, redactor.harTransportUrl(pointer, parent))
|
|
121
|
+
: item;
|
|
122
|
+
}
|
|
123
|
+
if (item === null || typeof item === "boolean")
|
|
124
|
+
return item;
|
|
125
|
+
if (Array.isArray(item))
|
|
126
|
+
return item.map((child, index) => visit(child, capturePointer(pointer, String(index)), depth + 1));
|
|
127
|
+
if (captureObject(item))
|
|
128
|
+
return Object.fromEntries(Object.entries(item).flatMap(([key, child]) => {
|
|
129
|
+
if (redact &&
|
|
130
|
+
sensitiveValues.some((literal) => key.includes(literal))) {
|
|
131
|
+
const beforeNumbers = numeric_literals.length;
|
|
132
|
+
const beforeBinaries = binary_fields.length;
|
|
133
|
+
const beforeRedactions = redactor.redactions.length;
|
|
134
|
+
visit(child, capturePointer(pointer, key), depth + 1, item);
|
|
135
|
+
numeric_literals.splice(beforeNumbers);
|
|
136
|
+
binary_fields.splice(beforeBinaries);
|
|
137
|
+
redactor.redactions.splice(beforeRedactions);
|
|
138
|
+
redactor.redactions.push({
|
|
139
|
+
pointer,
|
|
140
|
+
reason: "explicit-sensitive-value",
|
|
141
|
+
scope: "property-name",
|
|
142
|
+
});
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
return [
|
|
146
|
+
[key, visit(child, capturePointer(pointer, key), depth + 1, item)],
|
|
147
|
+
];
|
|
148
|
+
}));
|
|
149
|
+
throw new CaptureFormatError("format", "HAR parser returned a non-JSON value.", pointer);
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
reported: visit(value, "", 0),
|
|
153
|
+
numeric_literals,
|
|
154
|
+
binary_fields,
|
|
155
|
+
redactions: redactor.redactions,
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
const decodeHarBase64 = (value, pointer) => {
|
|
159
|
+
const compact = value.replace(/[\t\n\r ]/g, "");
|
|
160
|
+
const bytes = Buffer.from(compact, "base64");
|
|
161
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(compact) ||
|
|
162
|
+
bytes.toString("base64") !== compact)
|
|
163
|
+
throw new CaptureFormatError("format", "HAR content declares base64 but its text is not valid canonical base64 (ASCII whitespace is allowed).", pointer);
|
|
164
|
+
return bytes;
|
|
165
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { HAR_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js";
|
|
3
|
+
/** HAR's parser/schema packages run in one bounded Node heap, without network or target execution. */
|
|
4
|
+
export class HarCaptureAdapter {
|
|
5
|
+
format = "har";
|
|
6
|
+
identity = HAR_CAPTURE_PROVIDER_IDENTITY;
|
|
7
|
+
async command(requestPath) {
|
|
8
|
+
return {
|
|
9
|
+
command: process.execPath,
|
|
10
|
+
arguments: [
|
|
11
|
+
"--max-old-space-size=192",
|
|
12
|
+
"--max-semi-space-size=8",
|
|
13
|
+
"--v8-pool-size=1",
|
|
14
|
+
"--single-threaded",
|
|
15
|
+
fileURLToPath(new URL("./HarCaptureProcess.js", import.meta.url)),
|
|
16
|
+
requestPath,
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { TextDecoder } from "node:util";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
|
|
5
|
+
import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js";
|
|
6
|
+
import { CaptureFormatError } from "./CaptureFormatError.js";
|
|
7
|
+
import { decodeHarCapture } from "./HarCapture.js";
|
|
8
|
+
const requestSchema = z.strictObject({
|
|
9
|
+
snapshot_path: z.string(),
|
|
10
|
+
reply_path: z.string(),
|
|
11
|
+
sensitive_values: z.array(z.string().min(1)),
|
|
12
|
+
});
|
|
13
|
+
/** Decode untrusted HAR in an owned, bounded heap rather than the long-lived MCP process. */
|
|
14
|
+
const main = async () => {
|
|
15
|
+
const request = requestSchema.parse(JSON.parse(await readFile(process.argv[2] ?? "", "utf8")));
|
|
16
|
+
let reply;
|
|
17
|
+
try {
|
|
18
|
+
const snapshot = await readStableArtifact(request.snapshot_path, WEB_NETWORK_CAPTURE_LIMITS.inputBytes);
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(snapshot.bytes);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new CaptureFormatError("format", "HAR is not valid UTF-8 JSON.");
|
|
25
|
+
}
|
|
26
|
+
reply = {
|
|
27
|
+
ok: true,
|
|
28
|
+
value: decodeHarCapture(text, request.sensitive_values),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (cause) {
|
|
32
|
+
reply =
|
|
33
|
+
cause instanceof CaptureFormatError
|
|
34
|
+
? {
|
|
35
|
+
ok: false,
|
|
36
|
+
reason: cause.reason,
|
|
37
|
+
message: cause.message,
|
|
38
|
+
pointer: cause.pointer,
|
|
39
|
+
}
|
|
40
|
+
: {
|
|
41
|
+
ok: false,
|
|
42
|
+
reason: "decoder",
|
|
43
|
+
message: `HAR decoder failed unexpectedly (${cause instanceof Error ? cause.name : "unknown exception"}).`,
|
|
44
|
+
pointer: "",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
let text = JSON.stringify(reply);
|
|
48
|
+
if (Buffer.byteLength(text) > WEB_NETWORK_CAPTURE_LIMITS.outputBytes)
|
|
49
|
+
text = JSON.stringify({
|
|
50
|
+
ok: false,
|
|
51
|
+
reason: "limit",
|
|
52
|
+
message: "HAR reply exceeds the complete-evidence output budget.",
|
|
53
|
+
pointer: "",
|
|
54
|
+
});
|
|
55
|
+
await writeFile(request.reply_path, text, { flag: "wx", mode: 0o600 });
|
|
56
|
+
};
|
|
57
|
+
await main();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { visit } from "jsonc-parser";
|
|
2
|
+
import { LosslessNumber } from "lossless-json";
|
|
3
|
+
import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js";
|
|
4
|
+
import { CaptureFormatError } from "./CaptureFormatError.js";
|
|
5
|
+
/** Materialize upstream JSON events without duplicate folding, prototype setters or numeric rounding. */
|
|
6
|
+
export const parseHarJson = (text) => {
|
|
7
|
+
const frames = [];
|
|
8
|
+
let result;
|
|
9
|
+
const valuePointer = () => {
|
|
10
|
+
const parent = frames.at(-1);
|
|
11
|
+
if (parent === undefined)
|
|
12
|
+
return "";
|
|
13
|
+
if (parent.kind === "array")
|
|
14
|
+
return `${parent.pointer}/${parent.items.length}`;
|
|
15
|
+
return parent.pendingKey === undefined
|
|
16
|
+
? parent.pointer
|
|
17
|
+
: `${parent.pointer}/${parent.pendingKey.replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
18
|
+
};
|
|
19
|
+
const checkDepth = () => {
|
|
20
|
+
if (frames.length > WEB_NETWORK_CAPTURE_LIMITS.depth)
|
|
21
|
+
throw new CaptureFormatError("input-limit", "HAR exceeds the 64-level complete-evidence nesting budget.", valuePointer());
|
|
22
|
+
};
|
|
23
|
+
const append = (value) => {
|
|
24
|
+
const parent = frames.at(-1);
|
|
25
|
+
if (parent === undefined)
|
|
26
|
+
result = value;
|
|
27
|
+
else if (parent.kind === "array")
|
|
28
|
+
parent.items.push(value);
|
|
29
|
+
else {
|
|
30
|
+
if (parent.pendingKey === undefined)
|
|
31
|
+
throw new TypeError("JSON visitor omitted an object property identity.");
|
|
32
|
+
parent.members.set(parent.pendingKey, value);
|
|
33
|
+
delete parent.pendingKey;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
visit(text, {
|
|
37
|
+
onObjectBegin: () => {
|
|
38
|
+
checkDepth();
|
|
39
|
+
frames.push({
|
|
40
|
+
kind: "object",
|
|
41
|
+
pointer: valuePointer(),
|
|
42
|
+
members: new Map(),
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
onObjectProperty: (key) => {
|
|
46
|
+
if (key === "__proto__")
|
|
47
|
+
throw new CaptureFormatError("unsupported", "HAR object contains a __proto__ member that the current JSON schema boundary cannot preserve.");
|
|
48
|
+
const frame = frames.at(-1);
|
|
49
|
+
if (frame?.kind !== "object")
|
|
50
|
+
throw new TypeError("JSON visitor emitted a property outside an object.");
|
|
51
|
+
if (frame.members.has(key))
|
|
52
|
+
throw new CaptureFormatError("format", "HAR JSON contains duplicate object members.");
|
|
53
|
+
frame.pendingKey = key;
|
|
54
|
+
},
|
|
55
|
+
onObjectEnd: () => {
|
|
56
|
+
const frame = frames.pop();
|
|
57
|
+
if (frame?.kind !== "object")
|
|
58
|
+
throw new TypeError("JSON visitor ended an unexpected object.");
|
|
59
|
+
append(Object.fromEntries(frame.members));
|
|
60
|
+
},
|
|
61
|
+
onArrayBegin: () => {
|
|
62
|
+
checkDepth();
|
|
63
|
+
frames.push({ kind: "array", pointer: valuePointer(), items: [] });
|
|
64
|
+
},
|
|
65
|
+
onArrayEnd: () => {
|
|
66
|
+
const frame = frames.pop();
|
|
67
|
+
if (frame?.kind !== "array")
|
|
68
|
+
throw new TypeError("JSON visitor ended an unexpected array.");
|
|
69
|
+
append(frame.items);
|
|
70
|
+
},
|
|
71
|
+
onLiteralValue: (value, offset, length) => {
|
|
72
|
+
checkDepth();
|
|
73
|
+
if (typeof value === "number")
|
|
74
|
+
append(new LosslessNumber(text.slice(offset, offset + length)));
|
|
75
|
+
else if (value === null ||
|
|
76
|
+
typeof value === "boolean" ||
|
|
77
|
+
typeof value === "string")
|
|
78
|
+
append(value);
|
|
79
|
+
else
|
|
80
|
+
throw new TypeError("JSON visitor emitted an unsupported literal.");
|
|
81
|
+
},
|
|
82
|
+
onError: () => {
|
|
83
|
+
throw new CaptureFormatError("format", "HAR is malformed JSON.");
|
|
84
|
+
},
|
|
85
|
+
}, {
|
|
86
|
+
disallowComments: true,
|
|
87
|
+
allowTrailingComma: false,
|
|
88
|
+
allowEmptyContent: false,
|
|
89
|
+
});
|
|
90
|
+
return result;
|
|
91
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Ajv } from "ajv";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { CaptureFormatError } from "./CaptureFormatError.js";
|
|
5
|
+
/** Validate using the unchanged, pinned upstream HAR 1.2 draft-06 schemas. */
|
|
6
|
+
export const createHarValidator = () => {
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const schemas = z
|
|
9
|
+
.record(z.string(), z.record(z.string(), z.unknown()))
|
|
10
|
+
.parse(require("har-schema"));
|
|
11
|
+
const ajv = new Ajv({ strict: false, allErrors: false, ownProperties: true });
|
|
12
|
+
ajv.addMetaSchema(require("ajv/dist/refs/json-schema-draft-06.json"));
|
|
13
|
+
const plugin = require("ajv-formats");
|
|
14
|
+
if (typeof plugin !== "function")
|
|
15
|
+
throw new Error("Pinned AJV formats plugin is unavailable");
|
|
16
|
+
plugin(ajv);
|
|
17
|
+
for (const schema of Object.values(schemas))
|
|
18
|
+
ajv.addSchema(schema);
|
|
19
|
+
const validate = ajv.getSchema("har.json#");
|
|
20
|
+
if (validate === undefined)
|
|
21
|
+
throw new Error("Pinned HAR root schema is unavailable");
|
|
22
|
+
return (value) => {
|
|
23
|
+
allowMitmproxyAbsentPostData(value);
|
|
24
|
+
if (!validate(value)) {
|
|
25
|
+
const error = validate.errors?.[0];
|
|
26
|
+
throw new CaptureFormatError("format", `HAR schema validation failed: ${error?.keyword ?? "unknown constraint"}.`, error?.instancePath ?? "");
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/** Observed SaveHar 12.2.3 compatibility: validate omitted optional text while retaining reported null. */
|
|
31
|
+
const allowMitmproxyAbsentPostData = (value) => {
|
|
32
|
+
const parsed = z
|
|
33
|
+
.object({
|
|
34
|
+
log: z.object({
|
|
35
|
+
creator: z.object({
|
|
36
|
+
name: z.literal("mitmproxy"),
|
|
37
|
+
version: z.literal("12.2.3"),
|
|
38
|
+
}),
|
|
39
|
+
entries: z.array(z.unknown()),
|
|
40
|
+
}),
|
|
41
|
+
})
|
|
42
|
+
.safeParse(value);
|
|
43
|
+
if (!parsed.success ||
|
|
44
|
+
typeof value !== "object" ||
|
|
45
|
+
value === null ||
|
|
46
|
+
!("log" in value))
|
|
47
|
+
return;
|
|
48
|
+
const log = value.log;
|
|
49
|
+
if (typeof log !== "object" ||
|
|
50
|
+
log === null ||
|
|
51
|
+
!("entries" in log) ||
|
|
52
|
+
!Array.isArray(log.entries))
|
|
53
|
+
return;
|
|
54
|
+
for (const entry of log.entries) {
|
|
55
|
+
const candidate = entry;
|
|
56
|
+
if (typeof candidate !== "object" ||
|
|
57
|
+
candidate === null ||
|
|
58
|
+
!("request" in candidate))
|
|
59
|
+
continue;
|
|
60
|
+
const request = candidate.request;
|
|
61
|
+
if (typeof request !== "object" ||
|
|
62
|
+
request === null ||
|
|
63
|
+
!("postData" in request))
|
|
64
|
+
continue;
|
|
65
|
+
const body = request.postData;
|
|
66
|
+
if (typeof body === "object" &&
|
|
67
|
+
body !== null &&
|
|
68
|
+
"text" in body &&
|
|
69
|
+
body.text === null)
|
|
70
|
+
delete body.text;
|
|
71
|
+
}
|
|
72
|
+
};
|