Sample hardening run, owned fixtures.

This audit shows what the Fortifie hardening agent did on apps we built and own. The scan findings, applied protections and verify checks below are real outputs from our own backtest, not a live customer build and not customer telemetry.

Hardening run · sample audit

What the agent did to this build.

You hand Fortifie a build. It reads what the build gives away, plans a protection path from what it found, applies it, and reports what held. This is that run, laid out step by step on fixtures we own.

Sample · owned fixtures Run complete · verify passed

Scanned build

VulnDemoowned fixture · 1.2.3

Profile

Standardheavy layers opt-in

Findings

172 critical · 10 high · 3 med · 2 low

Protections applied

8+ 5 recommended to you

Verify

10 / 10checks held

Secrets recoverable

1 → 0static lift, before → after

The run, end to end

5 stages

Every stage is automated and gated. The agent only touches builds you own or are authorized to modify, and never hands back the internals of a protection.

  1. 1
    Authorize

    The run refuses to start without an authorization record for the build. Owned sources carry an OWNED marker; third-party code is never rewritten.

  2. 2
    Scan

    Reads the build with the same public tools an attacker uses. The app is never launched. On the website and the hosted MCP, the build is uploaded to Fortifie and deleted after the run. Output: the findings below.

  3. 3
    Plan

    Maps each finding to the protections that actually address it, and decides what to leave opt-in given the profile and launch-cost budget.

  4. 4
    Protect

    Applies the curated path: symbol strip, string and secret encryption, control-flow flattening, integrity sealing, runtime anti-debug, packing.

  5. 5
    Verify

    Re-reads the protected build and reports which protections held, with measured evidence. Checks that fail are surfaced, not hidden.

Stage 2 · what the scan found

Newest severity first. Open a finding to read what it means and what the agent recommends. Every row is a real finding from the VulnDemo fixture scan.

SeverityClassFinding
Criticalentitlement_weakness
Shipped with get-task-allow / library-validation disabled

Debug entitlements left on in a production build widen the attack surface for attaching and injecting.

Recommend Ship with Hardened Runtime and without get-task-allow. Drop disable-library-validation unless a verified plugin model needs it; never ship allow-dyld-environment-variables on a paid app.

Criticalleaked_secrets
Secrets / keys embedded in the shipped bundle

Static scan recovered credential-shaped strings from the bundle. Embedded secrets turn a licence bypass into account or supply-chain compromise.

Recommend Remove secrets from the client and rotate anything exposed; move privileged calls server-side. The protect engine binds any residual to the runtime environment so a static lift recovers nothing usable.

Highats_exceptions
App Transport Security disabled

Blanket ATS exceptions make a local HTTPS proxy MITM trivial against the licence traffic.

Recommend Remove blanket ATS exceptions; pin licence endpoints; pair with a signed licence payload so a proxy that sees traffic still cannot forge a grant.

Highelectron_asar_license
Electron .asar contains Pro-gate logic in plain JS

The unlock decision lives in renderer/main JavaScript inside the asar, readable without a patch.

Recommend Move the licence decision server-side; do not keep unlock booleans in client JS. Protect any residual check with JS hardening and remove secrets from the asar before ship.

Highlocal_state_license
Unlock state is a local key, editable without a patch

UserDefaults / flat-file keys name licence or trial state and can be flipped on disk.

Recommend Never let on-disk state alone grant Pro. Move the decision behind a signed server lease; if you cache, HMAC/sign it and bind it to device + grant. The signed-grant cache rejects unsigned or edited state.

Highlocal_store_sqlite_msgpack
Licence state in an unprotected SQLite / msgpack store

Authoritative licence state kept in an editable local store.

Recommend Do not keep authoritative state in editable SQLite / MessagePack. If a local cache is required, sign it and bind it to a server grant; prefer the Keychain or the signed activation cache.

Highsparkle_update_feed
Sparkle update feed without EdDSA signatures

An unsigned update channel is an update-channel hijack risk.

Recommend Require Sparkle EdDSA (SUPublicEDKey + sparkle:edSignature on every enclosure); pin the feed to HTTPS; never let clients choose the download URL.

Highsupabase_rls
Supabase backend referenced, row-level-security exposure risk

Row-level security that is too loose lets an authenticated user write licence or release rows.

Recommend Audit RLS so no authenticated user can write entitlement or release rows; move the licence decision to a server function; treat the anon key as public and never authoritative.

Hightrusted_license_json
Success-flag licence check next to a validate URL

The classic MITM-unlock pattern: trust a boolean instead of a server signature.

Recommend Verify a server signature over the payload, not just a flag. The signed one-shot lease binds the unlock to a server-held secret a proxy cannot forge, and the useful feature becomes a keyed computation.

Highunsigned_license_cache
Unsigned licence cache already grants Pro (app.asar!/license.json)

A dropped-in cache with an unsigned activated field unlocks features.

Recommend Sign cached state (Ed25519 or HMAC over canonical fields) and refuse to load an unsigned or edited cache. The activation cache already does this.

Highunsigned_license_cache
Unsigned licence cache already grants Pro (config.json)

A second unsigned cache path carries the same unlock state.

Recommend Sign cached state and refuse to load an unsigned or edited cache. The activation cache already does this.

Highunsigned_license_cache
Unsigned licence cache already grants Pro (license.json)

A third unsigned cache path carries the same unlock state.

Recommend Sign cached state and refuse to load an unsigned or edited cache. The activation cache already does this.

Mediumkeychain_as_switch
Keychain item existence used as a Pro switch

A Keychain API sits next to Pro flags; the presence of an item is treated as proof.

Recommend Store a signed lease, not a bare flag, and verify its signature plus device/server binding on every launch, so a forged item fails closed.

Mediumleaked_prompt
System prompt recoverable from the bundle

Prompt / instruction text is lifted straight out of the shipped bundle.

Recommend Treat prompts as IP: fetch them server-side behind the licence, or bind them to the runtime environment with the protect engine.

Mediumraw_license_symbols
Licence-check symbols sit in plaintext

Named licence-check symbols give an attacker an obvious target.

Recommend Spread and indirect the decision, strip and encrypt these symbols, and bind the real unlock to a keyed server computation. String protection removes the obvious target.

Lowprovenance_build_path
Build-machine paths leak in the binary

A developer username in a debug path is a provenance tell.

Recommend Strip debug paths (-fdebug-prefix-map, strip symbols) before release.

Lowprovenance_rebadge
Upstream vendor strings present

Leftover third-party identifiers are a rebadge / provenance tell.

Recommend Audit leftover strings and remove upstream identifiers you did not intend to ship.

17 findings · real output of the VulnDemo fixture scan

Stage 4 · what it applied

8 protections

The curated path for this build. Each protection is tagged by the plan it ships in; heavy layers stay opt-in because of their launch cost.

  • ChaCha20 packer + loader stubDefault

    Payload encrypted at rest; a static read sees a high-entropy blob. Addresses leaked_secrets, raw_license_symbols, leaked_prompt.

  • Symbol strip & renameDefault

    Removes named licence symbols and build-path tells. Addresses raw_license_symbols, provenance_build_path.

  • String & secret encryptionDefault

    No plaintext secrets or loader strings in the shipped file. Addresses leaked_secrets, leaked_prompt.

  • Control-flow flatteningDefault

    Spreads and indirects the licence decision so there is no single branch to patch. Addresses raw_license_symbols.

  • Integrity self-check (SHA-256 tag)Default

    Detects patching of the encrypted payload. A modified copy fails closed.

  • Runtime anti-debugDefault

    PT_DENY_ATTACH plus a sysctl P_TRACED check in the stub. Raises the cost of attaching a debugger.

  • Environment binding (env-fold)Default

    Binds residual secrets to the runtime environment so a static lift recovers nothing usable. Addresses leaked_secrets.

  • Signed activation grant + software attestationStudio+

    A signed server lease and a code-signature / CDHash seal. Rejects unsigned caches and catches re-signed clones. Addresses unsigned_license_cache, local_state_license, trusted_license_json, keychain_as_switch, provenance_rebadge.

Stage 5 · what held

measured

Verify re-reads the protected build. Measured on an owned native fixture (the licensed_tool build): stock verify failed because a lab secret was in plaintext; the packed build passed every check.

Verify checks
CheckResult
Lab secret absentpresent → none
ChaCha20 packer appliedloader + unpacker stub
Payload entropy7.9965 / 8.0 bits
Original symbols absentstub symbols only
Loader strings encryptedno plaintext
Runtime anti-debug presentPT_DENY_ATTACH + P_TRACED
Integrity surface presentSHA-256 tag + self-check
Attestation sealMATCH
Re-signed cloneMISMATCH (cdhash, bundle id)

Launch cost is honest: the packed fixture takes about 95 ms to launch versus about 5 ms unprotected, a one-time startup cost, not a per-call tax. That is why heavy layers are opt-in.

What it left to you, and what it did not claim

Judgment, stated plainly. Some protections are deliberately off, and some findings can only be fixed in your code or on your server.

Left opt-in or out, on purpose

  • Heavy and dual-use layers stay off by default because of their launch cost; you turn them on per build
  • The SoftPOS profile forces those layers off entirely
  • The sharpest custom techniques are Business-only and are never detailed in a scan response or a run audit

Recommended to you (not a packer fix)

  • Move the licence decision server-side and rotate any leaked secret
  • Sign the Sparkle feed (EdDSA) and audit Supabase row-level security
  • Remove blanket ATS exceptions; ship Hardened Runtime without debug entitlements

This raises attacker cost and time. It does not make reverse engineering impossible: a valid credential still runs the real path, and a determined attacker with enough budget can still recover these layers.

How this looks on a real build

On a live engagement the agent runs this against your own build, and the findings, the applied path and the verify results are yours, not a fixture like this one. After you ship, the same account surfaces crack canaries and threat events on your private dashboard, and the run data recommends stronger protect settings on your next signed release.