Skip to content

Pentest an Electron desktop app

cxg pentest --target-type electron runs the whitebox pipeline against a desktop target instead of a web one. It launches isolated instances of the app, drives their renderers over the Chrome DevTools Protocol, and adds probes for the surfaces a desktop app has and a web app does not: IPC channels, renderer configuration, and local data at rest. This guide covers how to point it at an app and the flags that only apply to desktop targets.

flowchart TB
    SRC["Electron main-process<br/>and preload sources"] --> IPC["IPC hypotheses<br/>ipc://channel"]
    SRC --> CFG["CONFIG hypotheses<br/>config://dotted.key"]
    IPC --> GEN["probe written by AI, run<br/>with hostile arguments"]
    CFG --> EXPR["one JS expression evaluated<br/>inside the live renderer"]
    UD["user-data directories<br/>cxg created for the run"] --> HOST["host probes: data at rest,<br/>permissions, update feed"]
    GEN --> R["report"]
    EXPR --> R
    HOST --> R

    class R emphasis
  • The web-application pentest setup: cxg pentest install, an AI CLI, and a guardlink findings.sarif. Everything there applies here; this page covers only the desktop differences.
  • An Electron app you can launch locally, either by a dev command or as a built bundle.

The pipeline needs to launch the app with a debugging port and a per-identity data directory, so you give it how to start the app rather than a URL to attach to. There are two ways, and they are mutually exclusive.

By dev command:

Terminal window
cxg pentest run \
--target-type electron \
--app-cmd "npm run electron:dev" \
--codebase ./app-repo \
--target https://api.example.com \
--auth desk-1

Or by built bundle, with --app-binary /Applications/Foo.app in place of --app-cmd. cxg appends --remote-debugging-port and a per-identity --user-data-dir to whichever you give it.

The CLI enforces the contract before anything launches:

error: the following required arguments were not provided:
--app-cmd <APP_CMD>

is what you get from --target-type electron with neither launch flag, and

error: the argument '--app-cmd <APP_CMD>' cannot be used with '--app-binary <APP_BINARY>'

if you pass both. --target still names the backend the app talks to, so the HTTP-layer probes have somewhere to aim.

These apply only under --target-type electron:

Flag What it does
--app-cmd / --app-binary How to launch the app. One is required; they are exclusive.
--stall-timeout Idle-time stall timeout in seconds (default 90). A renderer that goes quiet this long is treated as stalled.
--no-restart Do not relaunch a target that dies mid-scan. The default relaunches it (capped), reports the crash as a denial_of_service finding, re-probes the suspected IPC channel once, then quarantines it.
--host-scan-path Additionally scan a real installation directory for data at rest. By default the host probes read only the isolated user-data directories cxg created.

The crash-recovery default is deliberate: an IPC probe that kills the app is itself a finding, so cxg records it, brings the app back, and confirms the cause rather than ending the run. --no-restart gives you the older behaviour, where a dead target ends the scan with a truncation caveat and exit 3.

flowchart TB
    K["a probe kills the app"] --> F["recorded as a<br/>denial_of_service finding"]
    F --> Q{"--no-restart?"}
    Q -->|yes| E3["run ends at exit 3,<br/>results marked truncated"]
    Q -->|"no, the default"| RE["relaunch, re-probe the suspected<br/>channel once, then quarantine it"]
    RE --> D["the run continues, and a refutation<br/>from a restarted surface is not trusted"]

    class D emphasis

Desktop runs write the same artifacts as web runs, report.json and audit.jsonl in the session directory, and use the same exit codes. Under --no-restart, a target that dies mid-scan ends the run at exit 3 with a truncation caveat rather than the default recover-and-continue.