Test a Tauri app
cxg pentest run --target-type tauri reads a Tauri app’s Rust source and
GuardLink threat model and writes a map of its IPC surface: every registered
command, the parameters it takes, the capability grants, and the
security-relevant tauri.conf.json fields. Add --tauri-prove and cxg also
builds a test harness from the app’s own crate and drives the commands with
hostile arguments. This guide runs both against a small app with two deliberate
flaws.
flowchart TB
RS["Rust source: #[tauri::command]<br/>and generate_handler!"] --> MAP["IPC surface map:<br/>ipc://command hypotheses"]
CAP["capabilities/*.json<br/>and tauri.conf.json"] --> MAP
GL["guardlink findings.sarif"] --> MAP
MAP --> R1["report: tauri_surface_mapped,<br/>nothing invoked"]
MAP -->|"--tauri-prove"| H["copy of the app, test module,<br/>headless MockRuntime"]
H --> R2["report: tauri_proved,<br/>confirmed, refuted, skipped"]
class R1,R2 emphasis
Before you start
Section titled “Before you start”cxgand the pipeline installed:cxg pentest install. The web-application pentest lists the rest of the setup, but a Tauri run needs less of it: no running target URL, no captured auth profile, no--app-cmd, and no AI CLI, because neither the map nor the proof writes templates.guardlink, to write the app’sfindings.sarif. See feed guardlink findings into cxg.- For
--tauri-proveonly: a Rust toolchain and everything the app needs to build, including frontend assets and icons its compile-time context reads.
The app under test
Section titled “The app under test”The example is a Tauri 2 app whose src-tauri/src/lib.rs registers three
commands. read_note joins a renderer-supplied name onto a directory without
checking it, and export_all lets the renderer name its own plan:
// @asset App.Notes (#notes) -- "Note files under the app's notes directory"// @threat Path_Traversal (#traversal) [high] cwe:CWE-22 -- "A renderer-supplied name escapes the notes directory"// @threat Privilege_Escalation (#privesc) [high] cwe:CWE-285 -- "The renderer chooses its own plan"
// @exposes #notes to #traversal [high] -- "name is joined onto the notes directory unchecked"#[tauri::command]fn read_note(name: String) -> Result<String, String> { let path = Path::new("notes").join(name); fs::read_to_string(path).map_err(|e| e.to_string())}
// @exposes #notes to #privesc [high] -- "the plan comes from the renderer"#[tauri::command]fn export_all(plan: String) -> Result<String, String> { if plan == "admin" { Ok("exported".into()) } else { Err("export needs the admin plan".into()) }}
#[tauri::command]fn app_version() -> String { "0.1.0".into()}
pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![read_note, export_all, app_version]) .run(tauri::generate_context!()) .expect("error while running tauri application");}Its one capability file, src-tauri/capabilities/default.json, grants
core:default to the main window, and tauri.conf.json sets
withGlobalTauri: true.
Map the IPC surface
Section titled “Map the IPC surface”-
From the app’s repository root, write the threat model where cxg reads it:
Terminal window $ guardlink sarif . -p notes-app -o whitebox/findings.sarif✓ Wrote SARIF to whitebox/findings.sarifSARIF: 2 result(s) — 2 error(s), 0 warning(s) -
Run the map.
--targetis not needed, because the run never navigates anywhere:Terminal window cxg pentest run --target-type tauri --codebase .[1] guardlink: 2 SARIF hypotheses, 2 inline, 0 endpoints[1h] guardlink declared context: 2 result(s); chains from codeFlows on 0, typed notes from relatedLocations on 0, a function name on 2. @comment and @validates are still read from source comments.· 1 capability file(s) read[1f] code graph: not used — no code graph has been built for this codebase; build one with `bravos graph build ~/notes-app` to use it[1a] tauri surface: 3 registered command(s) of 3 defined, 1 capability file(s)[1c] routeless threats: 2 correlated to candidate command(s), 0 review-only[scope] audit log → ~/.cert-x-gen/sessions/pentest-20261019-215549/audit.jsonl[scope] ⚠ no --attestation supplied. For real engagements, include written authorization text via --attestation or scope.yaml.[tauri] IPC surface mapped: 3 command(s), 1 capability file(s). NOT live-probed — see the enumerated_not_live_probed caveat.report → ~/.cert-x-gen/sessions/pentest-20261019-215549/report.jsonThe offensive-tooling banner and run header that come first are omitted. The session directory is timestamped and differs on every run. The command exits
0.
[1c] is the threat-model join. A GuardLink threat with no HTTP route is
matched to the registered commands in the same source file, so both @exposes
lines above land on commands. A threat with no command match would be listed
under review_only_threats with a reason.
Read the map
Section titled “Read the map”report.json holds the surface under tauri_ipc_surface, one entry per
registered command:
{ "command": "read_note", "http_path": "ipc://read_note", "file": "~/notes-app/src-tauri/src/lib.rs", "line": 9, "params": "name: String", "reachable_from_renderer": true, "reachable_via": "__TAURI_INTERNALS__.invoke"}reachable_from_renderer records that the command is registered in
generate_handler!. It is a fact about the source, not a test that a
particular window can invoke the command. The grants are under
tauri_capabilities, and the configuration fields under tauri_security:
"tauri_security": { "with_global_tauri": true, "csp": null, "dangerous_remote_domain_ipc_access": null, "freeze_prototype": null}These are configuration facts to review, not confirmed findings. Commands that are registered but never defined, and registrations cxg could not resolve, are reported as extraction diagnostics, so treat the map as a reading of the source rather than exhaustive runtime discovery.
Prove commands in process
Section titled “Prove commands in process”--tauri-prove turns the map into a test. cxg copies the app to the session’s
tauri-harness/ directory, injects a test module beside the
generate_handler! call, enables Tauri’s test feature and runs cargo test.
The module drives each registered command through Tauri’s real IPC dispatch on
a headless MockRuntime, with arguments chosen from each parameter’s name and
type. Your source tree is not edited.
cxg pentest run --target-type tauri --codebase . \ --tauri-prove --tauri-prove-timeout 1200[tauri] --tauri-prove: building in-process IPC proof harness (cargo test against the app crate)… ✓ CONFIRMED ipc://export_all command 'export_all' made an authorization decision from the renderer-controlled argument 'plan': th[tauri] proof: 1 confirmed, 1 refuted, 0 unevaluated, 1 skippedreport → ~/.cert-x-gen/sessions/pentest-20261019-215559/report.jsonTruncated before the [tauri] lines, which repeat the map run above. The
confirmed line is cut short by cxg itself. The command exits 2, because a
finding was confirmed. --tauri-prove-timeout is the budget in seconds for the
harness build and run together, and 1200 is its default.
The status is now tauri_proved. That status alone does not mean a command
was exercised; the verdicts are in the report:
| Command | Where it lands | What the report says |
|---|---|---|
export_all |
confirmed_findings |
made an authorization decision from the renderer-controlled argument ‘plan’: the privileged value ‘admin’ succeeded while every benign value was rejected |
read_note |
mitigation_verifications |
rejected a ‘../’ path argument (“No such file or directory (os error 2)”); path confinement held for this probe |
app_version |
tauri_skipped_commands |
no probeable argument (no path/authz parameter cxg could drive) |
The probes are deterministic, not AI-written, and they confirm only on
definitive evidence. A path parameter gets a ../ chain aimed at a sentinel
file cxg plants, and confirms only if the sentinel’s content comes back. An
authorization-shaped parameter gets privileged and benign values, and confirms
only if a privileged value succeeds where every benign one was rejected.
read_note is the lesson in this run. It is written to be vulnerable, and the
probe still came back refuted: the joined path did not reach the sentinel, and
the handler returned an error. A refutation covers the hostile arguments tried,
not the whole command. Read caveats, ambiguous and tauri_skipped_commands
beside the findings.
A harness that does not build is reported, not hidden. A first run of this
example without an icons/icon.png failed in Tauri’s generate_context! macro,
and the report said so: status tauri_proved, the caveat
in_process_prove_unavailable, and every command listed in
tauri_skipped_commands with the compiler error as its reason. Nothing was
refuted. Fix the build and run again.
Related
Section titled “Related”- Pentest an Electron desktop app, for the desktop framework cxg drives live.
- Pentest a web application, for any HTTP backend the
app talks to. That backend is a separate
--target-type webtarget. cxg pentest rundocuments every flag.

