Files
reverse-engineering-anything/docs/release-exposure.md
T
Matt Van Horn 1d4853f073 fix(audit): keep coverage unknown for an unread packaged image
When a listed package member is a native image but its first read
fails, examinePackagedBinary returned before probePackagedImage could
mark its possible embedded package unexamined. Coverage stayed complete
and javascript.source_map_sources could report not_observed for a
payload nobody read.

Mark member coverage unknown wherever a native member goes unread,
including an encrypted one, as an image the probe could not read
already does, and say so in the limitations.
2026-11-02 21:52:24 -08:00

10 KiB

Release exposure

audit_release_exposure / audit-release-exposure reads one shipped artifact and reports what static analysis of that artifact recovers. The input is a macOS .app bundle, an install directory, an ASAR, or a single executable. Each check has a stable id, a status, the observation behind that status, and fixed remediation text.

The report does not establish that an application is secure. A check that could not be completed is unknown or not_assessed, and those counts stay beside the findings. There is no pass verdict and no letter grade.

rea audit-release-exposure /Applications/Example.app --json
rea audit-release-exposure ./dist/MyApp.app --fail_on high --json

Without --fail_on, the exit status reflects only whether the audit finished. With --fail_on high, --fail_on medium, or --fail_on low, a completed audit exits 3 when an exposed check meets that severity. high matches high. medium matches high and medium. low matches high, medium, and low. unknown and not_assessed do not change the exit status. A tool failure is still exit 1.

The CLI resolves a relative path against the caller's working directory. MCP requires an absolute filesystem path and returns the same report. MCP has no exit status.

{
  "name": "audit_release_exposure",
  "arguments": { "path": "/Applications/Example.app" }
}

What is read

The audit composes existing readers. It does not reread Electron fuse bytes itself, and it does not rewrite the artifact.

Check Exposed when Severity
javascript.source_map_sources A shipped source map has sourcesContent high
javascript.source_maps A shipped source map has no embedded sources medium
electron.run_as_node The fuse implication is honored high
electron.asar_integrity Embedded ASAR integrity is not enforced medium
electron.only_load_from_asar Loading is not restricted to app.asar medium
electron.node_cli_inspect Node CLI inspect arguments are honored medium
packaging.extractable_payload A Bun, Node SEA, PyInstaller, .NET bundle, or ASAR has a readable member low
native.debug_companions A .pdb, .dSYM, .debug, or linker .map ships beside the release medium
native.pdb_path A PE CodeView record names a PDB path low
go.build_paths Go build info or its build flags contain file paths low
rust.dependency_metadata cargo-auditable metadata is present low
text.credential_markers A text member contains a provider-prefixed marker high
native.hardening Not assessed in this audit
macho.hardened_runtime Not assessed in this audit
android.manifest Not assessed in this audit

go.build_paths reads a flag's arguments by the rules of the tool that takes them. A linker -X value is an application string, so a relative one is not a path. A rooted one cannot be told from a file path, so it is reported: -X main.route=/api/v1 is sampled like -X main.root=/srv/build, because dropping both would miss the second.

text.credential_markers matches provider-prefixed forms such as a PEM private-key header and documented vendor token prefixes. The report contains the rule id and location only. Matched bytes, previews, lengths, and hashes of matched bytes are omitted.

native.hardening, macho.hardened_runtime, and android.manifest stay not_assessed. Their next probes are inspect_binary_hardening, inspect_signature, and inspect_android_package.

Source map text is counted and then dropped. A selected file is read through the stable artifact reader. Text and source-map members stay inside the allowances listed under coverage.allowances, and each kind has its own allowance, so binary members cannot use up the read that a later source map needs. Native images on disk are registered from inventory classification and are not limited to those allowances. Other files and package members are identified from a bounded header. The bytes a package member's text or map read returns decide before its name does, so a PE called payload.js.map is read as a PE. A header with no NUL byte, or one that starts with a UTF-16 byte order mark, is scanned as text, so names such as .env.production, server.key, and credentials do not depend on an extension list. UTF-16 without a byte order mark has NUL bytes and is not recognized this way. A member already known to be text, such as a script, is also scanned when it is UTF-8 holding a stray NUL. One whose header is at least an eighth NUL bytes is taken to be wide text. It is not decoded and is counted as skipped, as is UTF-32. A binary plist is decoded, so its strings, dictionary keys and data are scanned in whichever encoding each object uses. One that cannot be decoded still has its bytes scanned and is counted as skipped. A file whose header could not be read leaves text.credential_markers undecided. A packaged ELF, Mach-O, or PE image is read in full and written to a temporary copy, removed afterward, so it can be probed for an embedded package. All packaged images share one max_file_bytes allowance. An image that does not fit, or whose bytes cannot be read, is not probed and leaves member coverage unknown. A packaged PE is also read for its debug records. An SVG file is read for its scripts as an HTML file is. A script that an HTML file loads is examined as a script whatever it is called, and an HTML file loaded this way is still examined as HTML. One read before that HTML file, one that is not found, one at a location the audit cannot place, or one past the 1 MiB of resolved script paths the audit keeps (MAX_RELEASE_EXPOSURE_SCRIPT_TARGET_BYTES) leaves javascript.source_map_sources undecided. Skipped members are listed under coverage.omitted. When the selection is a file, the parent directory is listed up to a fixed entry allowance for sibling .pdb, .dSYM, .debug, and linker .map debug companions. A sibling source map that the selected file declares, such as app.js.map, or a sibling script that a selected HTML file loads, such as app.js, is not read as part of the artifact, so it leaves javascript.source_map_sources undecided.

Inventory stops at 65,536 entries or 2 GiB of hashed bytes, and member coverage then stays unknown. A declared integrity value that disagrees with the bytes does not stop the audit, because signing an unpacked native module after packaging is routine. The contradiction is recorded in the limitations and the audit continues.

A member of a package that inventory did not expand is reported as the host file path followed by the member path, for example /dist/app.dll/notes.txt. Inventory-expanded members keep their path relative to the selection.

A package member skipped by a read allowance still counts as readable for packaging.extractable_payload, because the format stores it unencrypted. An encrypted member or one that failed to read counts as unreadable.

A map read counts against the source-map allowance when it starts, so an oversized or unreadable .map uses a read as a parsed one does. A sibling .map beside a selected file spends the same allowance, after the selected artifact has been read, and past it only the bounded header is read. A sibling whose header can begin a JSON object is then counted as skipped. A .map too large to parse is a linker map when its header cannot begin a JSON object. A larger .map that begins as JSON, or that has a JavaScript name such as app.js.map, is counted as skipped. native.debug_companions identifies a companion from its name or from a .map body that is not a source map, so its exposed result has inferred confidence. A symlink with a companion name counts by that name, and its target is not read. A symlink named as a source map, script, HTML, SVG, or CSS file is counted as skipped, whatever it points at, because links are not followed and that name is all the audit knows of what it would load. The target can still be scanned as a member under its own name, where a name such as payload is not read for map directives.

Status

exposed means the check's condition was observed. not_observed means that condition was absent in a completed scan. not_applicable means the check does not apply to this artifact, for example Electron fuses on an app bundle with no Electron Framework, or on a directory whose native images were all read and hold no fuse sentinel. unknown means the check could not be decided, including a directory with no ASAR layout where those reads are missing or incomplete (an asar: false Electron build has no ASAR to find), safe fuses from an incomplete inventory, an unopened packaged native image, or a dropped image when the sample did not read an executable named as the Electron host or every executable that could be it, a missing fuse sentinel on an Electron target, a universal binary with a slice that has no sentinel or an undecided fuse, a file that changed after inventory, Electron fuses on a selected ASAR (which does not include the executable that holds them), a PE inside a package that was too large to read, Go or Rust metadata on a native image stored inside a package, and Go or Rust metadata that was not present. Absence of that metadata is not proof the binary is something else. not_assessed means this audit did not run the check.

Cancellation during a Go, Rust, fuse, or desktop read returns a typed cancellation error. Its partial observation holds the report with completed facts, and checks whose readers did not run are unknown. Cancellation during the member scan itself returns no partial report.

Severity is null unless the status is exposed.