Files
reverse-engineering-anything/docs/release-exposure.md
T
Matt Van Horn a7c705bbe6 fix(audit): keep examining a page loaded as a script as HTML
A script element naming an .html member promoted that member to a
script, and the text pass then chose one reading. The page's own script
elements were never read, so an inline map in it was lost and a shipped
sourcesContent went unreported.

Run the script reading and the HTML reading independently, and list the
script target allowance among the cases that leave source map coverage
undecided.
2026-11-01 21:56:40 -08:00

66 lines
8.9 KiB
Markdown

# 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.
```bash
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.
```json
{
"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 | |
`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. 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. Among binary package members, only a PE is read further, for its debug records. 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.
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.
## 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`.