diff --git a/README.md b/README.md index d73dd73b0..28b888e3f 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,7 @@ Uninstall preserves Hopper, Node.js, evidence, captures, external evidence roots | Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile` | | Validate, canonicalize, or compare Evidence v2 bundles | `rea evidence-import`, `rea evidence-export`, or `rea compare` | | Run or resume a persistent two-version artifact analysis | `rea investigate-versions` | -| Map a local JavaScript/Electron application without executing it | `rea analyze-javascript-application` | +| Map a local JavaScript/Electron application without executing it | `rea analyze PATH --approved` or `rea analyze-javascript-application` | | Reuse immutable analysis results without relaunching a provider | Pass `--snapshot /approved/path/analysis.json` to a deep-analysis command | | Import source as historical reference | `rea import-reference-source` | | Capture or compare controlled process behavior | `rea capture-process` or `rea compare-process-captures` | @@ -276,9 +276,16 @@ rea evidence-import /absolute/path/to/evidence/bundle.json rea evidence-export /absolute/path/to/evidence/bundle.json /absolute/path/to/evidence/canonical.json rea compare /absolute/path/to/evidence/left.json /absolute/path/to/evidence/right.json rea investigate-versions /absolute/path/to/releases/v1 /absolute/path/to/releases/v2 /absolute/path/to/evidence/releases.json --yes --workspace-name releases +rea analyze /absolute/path/to/releases/app.asar --approved --json rea analyze-javascript-application /absolute/path/to/releases/app.asar --approved --json ``` +For a directory or `.asar`, generic `rea analyze` automatically selects the +static JavaScript application provider when neither `--provider` nor +`--snapshot` is supplied. The dedicated command remains available for explicit +format and resource-limit controls. Both routes require `--approved` and an +administrator-approved investigation input root. + If REA is registered with an MCP client, run approved `rea setup` while the explicit investigation-root variable is set, then restart that client. Setup copies this non-secret policy into the managed registration; changing the shell diff --git a/docs/api/functions/application_JavaScriptApplicationService.analyzeJavaScriptApplication.html b/docs/api/functions/application_JavaScriptApplicationService.analyzeJavaScriptApplication.html index 919c0efe2..a4429034a 100644 --- a/docs/api/functions/application_JavaScriptApplicationService.analyzeJavaScriptApplication.html +++ b/docs/api/functions/application_JavaScriptApplicationService.analyzeJavaScriptApplication.html @@ -26,7 +26,7 @@
  • options: ExecutionOptions = {}
  • Returns Promise<
        Result<
            | {
                analysis_profile: {
                    digest: string;
                    parameters: Record<string, JSONType>;
                    provider: { id: string; name: string; version: string };
                    provider_profile_schema_version: number;
                    schema_version: 1;
                };
                authority: | "shipped-artifact"
                | "controlled-replay"
                | "historical-reference"
                | "external-service"
                | "analyst-inference";
                confidence: "observed"
                | "derived"
                | "inferred";
                environment:
                    | {
                        architecture: string;
                        id: string;
                        isolation: "process"
                        | "none"
                        | "container"
                        | "virtual-machine";
                        platform: string;
                    }
                    | null;
                evidence_id: string;
                evidence_links: string[];
                limitations: string[];
                locations: (
                    | { address: string; kind: "address" }
                    | { end: string; kind: "address-range"; start: string }
                    | { kind: "artifact-path"; path: string }
                    | { kind: "file-offset"; offset: number }
                    | { end: number; kind: "file-offset-range"; start: number }
                )[];
                normalized_result: JSONType;
                operation: string;
                parameters: Record<string, JSONType>;
                predicate_type: string;
                provider: { id: string; name: string; version: string | null };
                raw_result: JSONType;
                schema_version: 2;
                subject:
                    | {
                        architecture: "x86"
                        | "x86_64"
                        | "arm"
                        | "arm64"
                        | null;
                        digest: { sha256: string };
                        format:
                            | "unknown"
                            | "file"
                            | "directory"
                            | "javascript"
                            | "source-map"
                            | "plist"
                            | "entitlements"
                            | "dmg"
                            | "zip"
                            | "pkg"
                            | "asar"
                            | "javascript-bundle"
                            | "mach-o-universal"
                            | "mach-o"
                            | "elf"
                            | "pe"
                            | "hopper"
                            | "analysis-database"
                            | "ipa"
                            | "apk";
                        local_path: string;
                        name: string;
                    }
                    | null;
            }
            | {
                authority: | "shipped-artifact"
                | "controlled-replay"
                | "historical-reference"
                | "external-service"
                | "analyst-inference";
                confidence: "observed"
                | "derived"
                | "inferred";
                environment:
                    | {
                        architecture: string;
                        id: string;
                        isolation: "process"
                        | "none"
                        | "container"
                        | "virtual-machine";
                        platform: string;
                    }
                    | null;
                evidence_id: string;
                evidence_links: string[];
                limitations: string[];
                locations: (
                    | { address: string; kind: "address" }
                    | { end: string; kind: "address-range"; start: string }
                    | { kind: "artifact-path"; path: string }
                    | { kind: "file-offset"; offset: number }
                    | { end: number; kind: "file-offset-range"; start: number }
                )[];
                normalized_result: JSONType;
                operation: string;
                parameters: Record<string, JSONType>;
                predicate_type: string;
                provider: { id: string; name: string; version: string | null };
                raw_result: JSONType;
                schema_version: 2;
                subject:
                    | {
                        architecture: "x86"
                        | "x86_64"
                        | "arm"
                        | "arm64"
                        | null;
                        digest: { sha256: string };
                        format:
                            | "unknown"
                            | "file"
                            | "directory"
                            | "javascript"
                            | "source-map"
                            | "plist"
                            | "entitlements"
                            | "dmg"
                            | "zip"
                            | "pkg"
                            | "asar"
                            | "javascript-bundle"
                            | "mach-o-universal"
                            | "mach-o"
                            | "elf"
                            | "pe"
                            | "hopper"
                            | "analysis-database"
                            | "ipa"
                            | "apk";
                        local_path: string;
                        name: string;
                    }
                    | null;
            },
            AnalysisError,
        >,
    >

    +
  • Defined in src/application/JavaScriptApplicationService.ts:27
  • +
  • Defined in src/application/JavaScriptApplicationService.ts:45
  • diff --git a/docs/api/functions/domain_javascriptStaticAnalysisHelpers.endpointArgument.html b/docs/api/functions/domain_javascriptStaticAnalysisHelpers.endpointArgument.html index 93aca7a69..37de7117e 100644 --- a/docs/api/functions/domain_javascriptStaticAnalysisHelpers.endpointArgument.html +++ b/docs/api/functions/domain_javascriptStaticAnalysisHelpers.endpointArgument.html @@ -14,7 +14,7 @@

    Returns string | undefined

    diff --git a/docs/api/functions/domain_javascriptStaticAnalysisHelpers.factoryRequireName.html b/docs/api/functions/domain_javascriptStaticAnalysisHelpers.factoryRequireName.html index c1ee14e43..0fbc92cc4 100644 --- a/docs/api/functions/domain_javascriptStaticAnalysisHelpers.factoryRequireName.html +++ b/docs/api/functions/domain_javascriptStaticAnalysisHelpers.factoryRequireName.html @@ -14,14 +14,14 @@

    Returns string | null

    diff --git a/docs/api/functions/domain_javascriptStaticAnalysisHelpers.moduleFactory.html b/docs/api/functions/domain_javascriptStaticAnalysisHelpers.moduleFactory.html index beedcbed5..d2c4f0cb1 100644 --- a/docs/api/functions/domain_javascriptStaticAnalysisHelpers.moduleFactory.html +++ b/docs/api/functions/domain_javascriptStaticAnalysisHelpers.moduleFactory.html @@ -14,15 +14,15 @@
    +

    Returns FunctionExpression | ArrowFunctionExpression | ObjectMethod | undefined

    diff --git a/docs/api/functions/domain_javascriptStaticAnalysisHelpers.modulePropertyName.html b/docs/api/functions/domain_javascriptStaticAnalysisHelpers.modulePropertyName.html index bb0ae392f..42f8fa495 100644 --- a/docs/api/functions/domain_javascriptStaticAnalysisHelpers.modulePropertyName.html +++ b/docs/api/functions/domain_javascriptStaticAnalysisHelpers.modulePropertyName.html @@ -14,14 +14,14 @@

    Returns string

    diff --git a/docs/api/functions/domain_webBundleAnalyzerAst.endpointArgument.html b/docs/api/functions/domain_webBundleAnalyzerAst.endpointArgument.html index 3dc31ca82..871bf3d41 100644 --- a/docs/api/functions/domain_webBundleAnalyzerAst.endpointArgument.html +++ b/docs/api/functions/domain_webBundleAnalyzerAst.endpointArgument.html @@ -14,7 +14,7 @@

    Returns string | undefined

    diff --git a/docs/api/functions/domain_webBundleAnalyzerAst.objectValue.html b/docs/api/functions/domain_webBundleAnalyzerAst.objectValue.html index fbfa77e0a..c36fa2fc1 100644 --- a/docs/api/functions/domain_webBundleAnalyzerAst.objectValue.html +++ b/docs/api/functions/domain_webBundleAnalyzerAst.objectValue.html @@ -14,14 +14,14 @@
    diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index 98faace50..bc09ed1b7 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -3,8 +3,9 @@ REA can project an operator-supplied Electron/JavaScript application directory or ASAR into [JavaScript Application Graph v1](javascript-application-graph.md) without executing application code. The target-free -`analyze_javascript_application` MCP tool and -`rea analyze-javascript-application` CLI command expose the same application +`analyze_javascript_application` MCP tool, dedicated +`rea analyze-javascript-application` CLI command, and generic +`rea analyze PATH --approved` directory/ASAR route expose the same application service and return an Evidence v2 envelope containing the graph and an Electron boundary summary. @@ -22,11 +23,18 @@ remain beneath that root after canonicalization: ```bash export REA_INVESTIGATION_INPUT_ROOTS_JSON='["/absolute/path/to/apps"]' +rea analyze /absolute/path/to/apps/app.asar --approved --json rea analyze-javascript-application /absolute/path/to/apps/app.asar \ --approved \ --json ``` +Generic `rea analyze` selects this static provider for directories and `.asar` +paths when neither `--provider` nor `--snapshot` is supplied. Use the dedicated +command when setting explicit format or reconstruction limits. Native targets, +single JavaScript files, `.app` bundles, and explicit deep-provider or snapshot +requests retain the native deep-analysis route. + When configuring an MCP client, set the variable before approved setup so REA copies the explicit non-secret root policy into the managed registration: diff --git a/scripts/verify-package-artifact.mjs b/scripts/verify-package-artifact.mjs index a0c2ca0a0..ea8e6d5f1 100644 --- a/scripts/verify-package-artifact.mjs +++ b/scripts/verify-package-artifact.mjs @@ -4,7 +4,7 @@ import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; import { json, run } from "./lib/verify-package-core.mjs"; -/** Run inventory-artifact and analyze-javascript-application against packaged fixtures. */ +/** Run artifact and JavaScript application analysis against packaged fixtures. */ export async function verifyPackageArtifactAndElectron({ cli, workspace, @@ -61,6 +61,25 @@ export async function verifyPackageArtifactAndElectron({ ?.paired_renderer_transmissions !== 1 ) throw new Error("packaged JavaScript application analysis CLI failed"); + const routedApplicationAnalysis = json( + await run( + cli, + ["analyze", applicationRoot, "--approved", "--json"], + environment, + ), + ); + assertRoutedApplicationAnalysis(routedApplicationAnalysis); return { artifactArchive }; } + +const assertRoutedApplicationAnalysis = (analysis) => { + if ( + analysis.operation !== "analyze_javascript_application" || + analysis.provider?.id !== "rea-javascript-application" || + analysis.normalized_result?.format !== "directory" || + analysis.normalized_result?.summary?.ipc?.paired_renderer_transmissions !== + 1 + ) + throw new Error("packaged routed JavaScript application analysis failed"); +}; diff --git a/src/application/JavaScriptApplicationService.ts b/src/application/JavaScriptApplicationService.ts index 7e78e82c1..0528b36ad 100644 --- a/src/application/JavaScriptApplicationService.ts +++ b/src/application/JavaScriptApplicationService.ts @@ -13,8 +13,9 @@ import { PermissionRequiredError, type AnalysisError, } from "../domain/errors.js"; -import { err, ok, type Result } from "../domain/result.js"; import type { Evidence } from "../domain/evidence.js"; +import { projectInputIssues } from "../domain/inputIssueProjection.js"; +import { err, ok, type Result } from "../domain/result.js"; import type { ExecutionOptions } from "./AnalysisProvider.js"; import { createJavaScriptApplicationEvidence } from "./JavaScriptApplicationEvidence.js"; import { reconstructJavaScriptArtifact } from "./JavaScriptArtifactReconstruction.js"; @@ -29,7 +30,14 @@ export const analyzeJavaScriptApplication = async ( options: ExecutionOptions = {}, ): Promise> => { const parsed = analyzeJavaScriptApplicationInputSchema.safeParse(rawInput); - if (!parsed.success) return err(new AnalysisInputError(OPERATION)); + if (!parsed.success) + return err( + new AnalysisInputError( + OPERATION, + undefined, + projectInputIssues(parsed.error.issues, rawInput), + ), + ); return analyzeJavaScriptApplicationValidated(authority, parsed.data, options); }; diff --git a/src/cli/coreAnalysisCommands.ts b/src/cli/coreAnalysisCommands.ts index 7a7ff036a..b7a14e3c4 100644 --- a/src/cli/coreAnalysisCommands.ts +++ b/src/cli/coreAnalysisCommands.ts @@ -1,4 +1,6 @@ import { z } from "incur"; +import { stat } from "node:fs/promises"; +import { resolve } from "node:path"; import { runDirectAnalysis } from "../application/DirectAnalysis.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; @@ -6,6 +8,7 @@ import { logCliCommand } from "../cliLogging.js"; import type { Logger } from "../logger.js"; import { directAnalysisOptions, providerSelectionOption } from "./options.js"; import type { CliInstance } from "./types.js"; +import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnalysis.js"; export const registerCoreAnalysisCommands = ( cli: CliInstance, @@ -38,20 +41,23 @@ const registerCoreCommands = (cli: CliInstance, logger: Logger): void => { .describe("Load and update a local analysis snapshot"), provider: providerSelectionOption, }); + const analyzeOptions = overviewOptions.extend({ + approved: z + .boolean() + .default(false) + .describe( + "Approve static analysis when the path is a JavaScript directory or ASAR", + ), + }); cli.command(CLI_COMMANDS.analyze, { description: "Get an overview of an app", args: z.object({ path: z.string().describe("App, program, or analysis database path"), }), - options: overviewOptions, + options: analyzeOptions, run: ({ args, options }) => logCliCommand(logger, "analyze", () => - runDirectAnalysis( - args.path, - "binary_overview", - { detail: options.detail, limit: options.limit }, - directAnalysisOptions(logger, options.snapshot, options.provider), - ), + runRoutedOverview(args.path, options, logger), ), }); cli.command(CLI_COMMANDS.inspect, { @@ -96,6 +102,45 @@ const registerCoreCommands = (cli: CliInstance, logger: Logger): void => { }); }; +const runRoutedOverview = async ( + path: string, + options: { + readonly approved: boolean; + readonly detail: "concise" | "detailed"; + readonly limit: number; + readonly snapshot?: string | undefined; + readonly provider?: string | undefined; + }, + logger: Logger, +) => { + if ( + options.provider === undefined && + options.snapshot === undefined && + (await isJavaScriptApplicationPath(path)) + ) + return runCliJavaScriptApplicationAnalysis({ + input_path: resolve(path), + approved: options.approved, + }); + return runDirectAnalysis( + path, + "binary_overview", + { detail: options.detail, limit: options.limit }, + directAnalysisOptions(logger, options.snapshot, options.provider), + ); +}; + +const isJavaScriptApplicationPath = async (path: string): Promise => { + const lower = path.toLowerCase(); + if (lower.endsWith(".asar")) return true; + if (lower.endsWith(".app")) return false; + try { + return (await stat(path)).isDirectory(); + } catch { + return false; + } +}; + const registerXrefsCommand = (cli: CliInstance, logger: Logger): void => { cli.command(CLI_COMMANDS.xrefs, { description: "List bounded references to an analyzed address", diff --git a/src/cli/javascriptApplicationAnalysis.ts b/src/cli/javascriptApplicationAnalysis.ts new file mode 100644 index 000000000..c6b9f299b --- /dev/null +++ b/src/cli/javascriptApplicationAnalysis.ts @@ -0,0 +1,24 @@ +import { analyzeJavaScriptApplication } from "../application/JavaScriptApplicationService.js"; +import { loadConfiguredPermissionAuthority } from "../application/PermissionConfiguration.js"; +import { parseConfig } from "../config.js"; +import { projectAnalysisError } from "../domain/errors.js"; +import type { JsonValue } from "../domain/jsonValue.js"; + +/** Execute the shared one-shot CLI boundary for static JavaScript analysis. */ +export const runCliJavaScriptApplicationAnalysis = async ( + input: unknown, +): Promise => { + const config = parseConfig(process.env); + if (!config.ok) return cliError(config.error); + const authority = await loadConfiguredPermissionAuthority(config.value); + if (!authority.ok) return cliError(authority.error); + const result = await analyzeJavaScriptApplication(authority.value, input); + return result.ok ? result.value : cliError(result.error); +}; + +const cliError = ( + error: Parameters[0], +): JsonValue => ({ + error: "JavaScript application analysis failed", + ...projectAnalysisError(error), +}); diff --git a/src/cliElectronCommands.ts b/src/cliElectronCommands.ts index 021420092..7bd6123fb 100644 --- a/src/cliElectronCommands.ts +++ b/src/cliElectronCommands.ts @@ -4,7 +4,6 @@ import { inspectElectronPage, listElectronTargets, } from "./application/ElectronObservationService.js"; -import { analyzeJavaScriptApplication } from "./application/JavaScriptApplicationService.js"; import { reconcileJavaScriptRuntimeEvidence } from "./application/JavaScriptRuntimeReconciliationService.js"; import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js"; import { CdpElectronProvider } from "./browser/CdpElectronProvider.js"; @@ -14,7 +13,6 @@ import { inspectElectronPageInputSchema, listElectronTargetsInputSchema, } from "./domain/electronObservation.js"; -import { analyzeJavaScriptApplicationInputSchema } from "./domain/javascriptApplicationAnalysis.js"; import { AnalysisInputError, projectAnalysisError } from "./domain/errors.js"; import type { JsonValue } from "./domain/jsonValue.js"; import type { Logger } from "./logger.js"; @@ -24,6 +22,7 @@ import { electronPageInspectionOptions, javascriptApplicationOptions, } from "./cliObservationOptions.js"; +import { runCliJavaScriptApplicationAnalysis } from "./cli/javascriptApplicationAnalysis.js"; const scopeOptions = { allowedFileRoots: z @@ -191,42 +190,29 @@ const registerJavaScriptApplicationCommand = ( }), options: javascriptApplicationOptions, run: ({ args, options }) => - logCliCommand( - logger, - CLI_COMMANDS.analyzeJavaScriptApplication, - async () => { - const context = await electronContext(); - if (!context.ok) return context.error; - const parsed = analyzeJavaScriptApplicationInputSchema.safeParse({ - input_path: args.path, - approved: options.approved, - format: options.format, - source_map_read_approved: options.sourceMapReadApproved, - limits: { - max_entries: options.maxEntries, - max_total_artifact_bytes: options.maxTotalArtifactBytes, - max_artifact_entry_bytes: options.maxArtifactEntryBytes, - max_compression_ratio: options.maxCompressionRatio, - max_depth: options.maxDepth, - max_path_bytes: options.maxPathBytes, - max_text_files: options.maxTextFiles, - max_total_text_bytes: options.maxTotalTextBytes, - max_text_file_bytes: options.maxTextFileBytes, - max_ast_nodes: options.maxAstNodes, - max_findings: options.maxFindings, - max_modules: options.maxModules, - max_source_map_sources: options.maxSourceMapSources, - max_parse_milliseconds: options.maxParseMilliseconds, - }, - }); - if (!parsed.success) - return inputError("analyze_javascript_application"); - const result = await analyzeJavaScriptApplication( - context.authority, - parsed.data, - ); - return result.ok ? result.value : cliError(result.error); - }, + logCliCommand(logger, CLI_COMMANDS.analyzeJavaScriptApplication, () => + runCliJavaScriptApplicationAnalysis({ + input_path: args.path, + approved: options.approved, + format: options.format, + source_map_read_approved: options.sourceMapReadApproved, + limits: { + max_entries: options.maxEntries, + max_total_artifact_bytes: options.maxTotalArtifactBytes, + max_artifact_entry_bytes: options.maxArtifactEntryBytes, + max_compression_ratio: options.maxCompressionRatio, + max_depth: options.maxDepth, + max_path_bytes: options.maxPathBytes, + max_text_files: options.maxTextFiles, + max_total_text_bytes: options.maxTotalTextBytes, + max_text_file_bytes: options.maxTextFileBytes, + max_ast_nodes: options.maxAstNodes, + max_findings: options.maxFindings, + max_modules: options.maxModules, + max_source_map_sources: options.maxSourceMapSources, + max_parse_milliseconds: options.maxParseMilliseconds, + }, + }), ), }); }; diff --git a/tests/electronCli.test.ts b/tests/electronCli.test.ts index 3b5fa0571..a86f67d38 100644 --- a/tests/electronCli.test.ts +++ b/tests/electronCli.test.ts @@ -117,6 +117,68 @@ describe("Electron CLI parity", () => { INTEGRATION_TEST_TIMEOUT_MS, ); + it( + "routes generic approved analysis of a directory to static JavaScript", + async () => { + const root = await mkdtemp(join(tmpdir(), "rea-electron-routed-cli-")); + temporary.push(root); + await writeElectronBoundaryFixture(root); + const environment = { + ...process.env, + REA_INVESTIGATION_INPUT_ROOTS_JSON: JSON.stringify([root]), + }; + + const analyzed = await runCli( + ["analyze", root, "--approved", "--json"], + environment, + ); + + expect(analyzed).toMatchObject({ + operation: "analyze_javascript_application", + provider: { id: "rea-javascript-application" }, + normalized_result: { + input_path: root, + format: "directory", + summary: { ipc: { paired_renderer_transmissions: 4 } }, + }, + }); + }, + INTEGRATION_TEST_TIMEOUT_MS, + ); + + it( + "requires explicit approval before generic JavaScript analysis", + async () => { + const root = await mkdtemp( + join(tmpdir(), "rea-electron-unapproved-cli-"), + ); + temporary.push(root); + await writeElectronBoundaryFixture(root); + const environment = { + ...process.env, + REA_INVESTIGATION_INPUT_ROOTS_JSON: JSON.stringify([root]), + }; + + const denied = await runCli(["analyze", root, "--json"], environment); + + expect(denied).toMatchObject({ + code: "invalid_request", + details: { + operation: "analyze_javascript_application", + issues: [ + { + path: ["approved"], + reason: "invalid_value", + expected: [true], + }, + ], + }, + }); + expect(denied).not.toHaveProperty("details.candidate_ids"); + }, + INTEGRATION_TEST_TIMEOUT_MS, + ); + it( "reads a file and returns the derived static/runtime reconciliation contract", async () => {