From 9f52011eb07f36b467d9febe016633410782ccbf Mon Sep 17 00:00:00 2001 From: Isaac Bell <2613157+IsaacBell@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:12:43 -0400 Subject: [PATCH 01/11] am-i-hacked: system scan renamed system-scan.sh with dark corners, Python venv integrity check, --max-findings (default 1000), faster scans - bin/host-audit.sh is now bin/system-scan.sh; host-audit.sh stays as a shim. - --system also scans a fixed list of places where tooling installs code outside any project (venvs, uv/pip/npx caches, cargo, bun, deno, go, gem bins). Absent folders are skipped silently. - New venv integrity check: files that no longer match their package RECORD hashes, RECORD paths leaving the venv, unowned files, code-running .pth and sitecustomize hooks. Unreadable files and folders, symlinks and newline names are reported, never skipped. - --max-findings N, default raised from 100 to 1000; total and per-rule counts printed past the cap. - Hex and unicode escape findings in dependency folders are one summary line. Null-byte warnings on binary files are gone. - README cut from 368 to 139 lines; docs landing page covers the whole toolset. --- README.md | 6 +- apps/am-i-hacked/CHANGELOG.md | 16 + apps/am-i-hacked/README.md | 355 +---- apps/am-i-hacked/bin/host-audit.sh | 1344 +---------------- apps/am-i-hacked/bin/scanner.sh | 178 ++- apps/am-i-hacked/bin/system-scan.sh | 1452 ++++++++++++++++++ apps/am-i-hacked/bin/venv-integrity.sh | 410 ++++++ apps/am-i-hacked/test/host-audit.bats | 1589 +------------------- apps/am-i-hacked/test/scanner.bats | 81 +- apps/am-i-hacked/test/system-scan.bats | 1619 +++++++++++++++++++++ apps/am-i-hacked/test/venv-integrity.bats | 312 ++++ docs/index.html | 146 +- docs/llms.txt | 93 +- 13 files changed, 4215 insertions(+), 3386 deletions(-) create mode 100644 apps/am-i-hacked/bin/system-scan.sh create mode 100644 apps/am-i-hacked/bin/venv-integrity.sh create mode 100644 apps/am-i-hacked/test/system-scan.bats create mode 100644 apps/am-i-hacked/test/venv-integrity.bats diff --git a/README.md b/README.md index e8dc1d2..a9b03bb 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ The skills, in [`skills/`](skills/): [quarantine-review](skills/quarantine-revie ## Why this one? - **Instant.** One command, no signup, no account, no API key: `pnpx am-i-hacked .` -- **CI by copy-paste.** Exit code `1` on findings; the [package README](apps/am-i-hacked/README.md#-usage) has the workflow snippet. -- **Catches what advisory scanners cannot.** `npm audit`, OSV-Scanner and similar tools match your dependencies against published advisories. They cannot see an attack nobody has reported yet, or one that lives in the repository itself: a `.vscode/tasks.json` that runs when you open the folder, a script saved as a font file, a stealer that starts itself at login. `am-i-hacked` reads the files for those indicators before you open, install or run anything. Use both. +- **CI by copy-paste.** Exit code `1` on findings; the [package README](apps/am-i-hacked/README.md#install-and-run) has the workflow snippet. +- **Catches what advisory scanners cannot.** `npm audit`, OSV-Scanner and similar tools match your dependencies against published advisories. They cannot see an unreported attack, or one that lives in the repository itself: a `.vscode/tasks.json` that runs on folder open, a script saved as a font file, a stealer that starts at login. `am-i-hacked` reads the files for those indicators before you open, install or run anything. Use both. ## What it does not do @@ -61,7 +61,7 @@ mise run check # shellcheck, shfmt check and the bats tests: what CI runs ## Semgrep -[`secure-semgrep`](apps/secure-semgrep/README.md) is the repository's static-analysis pack: bundled, owned rules for AI agents and bash, plus Semgrep loadout packs you can point at any codebase. It lives in [`apps/secure-semgrep`](apps/secure-semgrep/README.md) and publishes to npm as `secure-semgrep`, mirroring how `am-i-hacked` is published. +[`secure-semgrep`](apps/secure-semgrep/README.md) is the repository's static-analysis pack: bundled, owned rules for AI agents and bash, plus Semgrep loadout packs you can point at any codebase. It publishes to npm as `secure-semgrep`. Run it over this repository (review mode records findings without breaking the build): diff --git a/apps/am-i-hacked/CHANGELOG.md b/apps/am-i-hacked/CHANGELOG.md index 53a6958..f5bf47d 100644 --- a/apps/am-i-hacked/CHANGELOG.md +++ b/apps/am-i-hacked/CHANGELOG.md @@ -2,6 +2,22 @@ Newest first. Default-behavior changes are marked **CHANGED**. +## Unreleased + +**BLUF:** The system scan now checks the fixed places tooling installs code outside any project, the folder scan verifies Python virtualenv integrity, and the findings cap rises to 1000. `bin/host-audit.sh` is renamed to `bin/system-scan.sh`, with the old name kept as a shim. + +### Added +- The system scan (`--system`) now also scans a fixed list of "dark corners" where tooling installs code outside any project: Python virtualenvs, the uv and pip caches, pyenv, pipx, the npx cache, the pnpm global store, cargo, bun, deno, and the go and gem bin folders. Each is scanned with the folder scan when present and skipped silently when it does not exist. +- Python virtualenv integrity check in the folder scan: files that no longer match their package `RECORD` hashes are a `HIGH` finding; `RECORD` paths that leave the venv, files no package owns, `.pth` and `sitecustomize` hooks that run code, unreadable files and folders (reported rather than skipped), and symlinks and file names containing newlines are all reported. +- `--max-findings N` sets the maximum findings printed, default raised from 100 to 1000. **CHANGED:** when findings exceed the cap, the true total and a per-rule count are printed, and higher-severity findings are kept first. + +### Changed +- **CHANGED:** hex and unicode escape findings inside dependency folders (`site-packages`, `node_modules`, `vendor`) are reported as one summary count instead of one finding per match. +- **CHANGED:** `bin/host-audit.sh` is now `bin/system-scan.sh`. `bin/host-audit.sh` remains as a shim, and the `host` command still works. + +### Fixed +- The `ignored null byte in input` warnings printed on binary files are no longer emitted. + ## 2.0.1 - 2026-10-01 **BLUF:** A security release. The scan no longer trusts the tree it is reviewing: a scanned repo's `.git/config` could make it run a command, and payloads in dot-directories, in folders named in `.ignore` or `.rgignore`, in tracked files matched by `.gitignore`, and in tracked build output were not read. It also fixes a skip on extensionless scripts. diff --git a/apps/am-i-hacked/README.md b/apps/am-i-hacked/README.md index 1561d8e..973a032 100644 --- a/apps/am-i-hacked/README.md +++ b/apps/am-i-hacked/README.md @@ -1,87 +1,46 @@ -# πŸ•΅οΈ Am I Hacked? +# am-i-hacked [![npm version](https://img.shields.io/npm/v/am-i-hacked)](https://www.npmjs.com/package/am-i-hacked) [![CI](https://github.com/IsaacBell/secure-devtools/actions/workflows/ci.yml/badge.svg)](https://github.com/IsaacBell/secure-devtools/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/npm/l/am-i-hacked)](LICENSE) -> *Check the code you're about to run, and the machine you're running it on.* +`am-i-hacked` checks the code you are about to run, and the machine you run it on, for signs of a supply-chain or dev-environment attack. It is an indicator-of-compromise scanner, not antivirus: a clean result means no warning signs were found, not that anything is proven safe. ```sh -pnpx am-i-hacked . # or: npx am-i-hacked . +pnpx am-i-hacked . # or: npx am-i-hacked . ``` -**Status:** actively maintained; 2.0.0 is the current release. macOS and Linux. Formerly -published as `am-i-compromised`; the old command name still works. See -[Renamed from am-i-compromised](#renamed-from-am-i-compromised). - -## 🌟 Highlights - -- **Built from real, observed attacks.** The detections come from attacks seen in the wild, - not guesses. The clipboard-stealer, editor auto-run, disguised-asset and persistence checks - each model a specific attack chain. The generic rules (`eval`, child processes, obfuscation) - flag the building blocks those attacks use, so they are broader and noisier. -- **Two modes, one tool.** Scan a project for malicious code, or audit *this machine* for the - things source scans can't see: launch agents, shell startup files, AI-tool config, and - running processes. -- **Catches clipboard and keystroke stealers.** Capture code paired with a Telegram, Discord, - Slack or webhook endpoint is flagged, along with the LaunchAgent that starts it. -- **Watches your AI tools.** Flags a local proxy set as your model's base URL, hooks that run - code from `node_modules` or a writable path, switched-off permission prompts, and plain-text keys. -- **Zero npm runtime dependencies.** Plain shell, so there is no install-time tree to audit. -- **Honest about noise.** Reviewed lines are marked safe with a reason, and suppressed findings - are still listed on every run. -- **Fits your workflow.** Exit code `1` on findings, so it drops into a dev script or CI. A - guarded `git pull` (`safe-pull`) is included. - -## ℹ️ Overview - -`am-i-hacked` is a small [indicator-of-compromise](https://en.wikipedia.org/wiki/Indicator_of_compromise) -checker. It looks for warning signs that should make you look closer. It is not antivirus, -and it does not look up known viruses. A clean result means it found no warning signs; it -does not prove the code or the machine is safe. - -Some threats never touch a source tree. A clipboard stealer started by a hidden LaunchAgent -lives in your home directory, where a project scan cannot see it. `host` covers that side. - -Why this one? It runs instantly with one command and no signup, account or API key, and it goes -into CI by copy-paste (below). And it catches what advisory scanners cannot. Tools such as -`npm audit` and OSV-Scanner match your dependencies against reported vulnerabilities. They cannot see an attack nobody has reported -yet, or one that lives in the repository itself: an editor task that runs when you open the -folder, a script saved as a font file, a stealer started at login. `am-i-hacked` reads the files -for those indicators before you open, install or run anything. Use both. - -## πŸš€ Usage +**Status:** 2.0.1, macOS and Linux. Formerly published as `am-i-compromised`; the old command name still works. Changes are in [CHANGELOG.md](CHANGELOG.md). + +## Install and run + +No install needed: ```sh -# Scan a project (defaults to the current directory) -pnpx am-i-hacked . # npx am-i-hacked . works too +pnpx am-i-hacked . # scan a folder (default: the current directory) +pnpx am-i-hacked --system # also audit this machine +``` -# Check a folder's AI-tool config (.claude/settings*.json, .mcp.json). Defaults to the current directory. -pnpx am-i-hacked host [path/to/folder] +As a dev dependency: -# Audit the whole machine as well: login items and their code signatures, crontab, -# shell startup files, user-level AI-tool config, running processes. -# Read-only, no root or sudo, does not need ripgrep. -pnpx am-i-hacked --system # same as: host --system [folder] +```sh +pnpm add -D am-i-hacked # or: npm install --save-dev am-i-hacked ``` -Exit code `0` means nothing found, `1` means findings to review or a missing requirement (bash -4.2+ or `rg`, or `jq` when a `package.json` is present; the message names it), `2` means a -usage error. Add -`--verbose` to `host` to also list informational entries and every login item with its signer. +| Scan | Needs | +| --- | --- | +| Folder (`am-i-hacked `) | `bash` 4.2+, `rg` (ripgrep), and `jq` to inspect `package.json` scripts | +| System (`--system`) | `bash` 3.2+, `jq`. Read-only; no ripgrep, root or sudo | + +macOS ships bash 3.2, so the folder scan re-runs itself under a newer bash when one is installed (mise, nix, asdf, Homebrew, MacPorts, Linuxbrew) and otherwise says what to install. Get `rg` and `jq` with `brew install ripgrep jq` or `apt-get install ripgrep jq`. -In `package.json`, run the scan before your dev server: +Before a dev server: ```json -{ - "scripts": { - "dev": "am-i-hacked . && next dev" - } -} +{ "scripts": { "dev": "am-i-hacked . && next dev" } } ``` -In CI, for example a GitHub Actions job on `ubuntu-latest`. The runner has `bash` and `jq` but -no pnpm, and `ripgrep` is installed first in case the image lacks it: +In CI (GitHub Actions on `ubuntu-latest`; the runner has bash and jq but no pnpm and no ripgrep): ```yaml - uses: pnpm/action-setup@v4 @@ -91,210 +50,53 @@ no pnpm, and `ripgrep` is installed first in case the image lacks it: - run: pnpx am-i-hacked@2 . ``` -More below: how to read the output, how to suppress a reviewed finding, the host audit, and -[safe-pull](#guarded-pull-safe-pull). System-wide Linux units and `/etc/cron.*` are not covered yet. - -## ⬇️ Installation - -No install needed with `pnpx` / `npx`. To keep it in a project: - -```sh -pnpm add -D am-i-hacked # or: npm install --save-dev am-i-hacked -``` - -macOS and Linux. Requirements: +## What it checks -| Command | Needs | -| --- | --- | -| `am-i-hacked ` | `bash` 4.2+, `rg` ([ripgrep](https://github.com/BurntSushi/ripgrep)), `jq` (to inspect `package.json` scripts; without it the scan fails closed when a `package.json` is present) | -| `am-i-hacked host` | `bash` 3.2+ (the macOS default works). `jq` is optional but needed to read AI-tool hooks and MCP servers | -| `safe-pull` | `bash`, `git` | - -macOS ships bash 3.2. The project scan re-runs itself under a newer bash if you have one, looking -first at the `bash` on your `PATH` (mise, nix, asdf) and then at the Homebrew, MacPorts and -Linuxbrew locations, and otherwise tells you what to install (for example `brew install bash`). -Install `rg` and `jq` -with `brew install ripgrep jq` or `apt-get install ripgrep jq`. - -## πŸ”’ Privacy +Both scans are read-only and make no network calls. Scanned source languages: JS/TS, Python, Rust, Ruby, C, C++, C#. -The scanner runs entirely on your machine and sends nothing anywhere. The project scan and host audit make no network calls; the source is open under MIT, so anyone can verify this. Findings stay in your terminal β€” it is safe to run on private or sensitive code. - -The package installs these commands: - -| Command | What it runs | -| --- | --- | -| `am-i-hacked` | the scanner (and `host`, the machine audit) | -| `aih` | the same scanner (short command) | -| `am-i-compromised` | the same scanner, kept so existing scripts keep working | -| `aic` | the same scanner (short form of the old name) | -| `security-gate` | the same scanner (good for project scripts) | -| `scanner` | the same scanner (short; may collide with other tools) | -| `safe-pull` | the guarded `git pull` | - -## πŸ”Ž What the project scan detects - -- Dynamic code execution (`eval`, `new Function`, ...), child-process execution, direct network - module access, runtime global mutation -- Encoded or obfuscated payloads (`atob`, hex/unicode escapes, `_0x...` string tables), and - unusually long source lines -- Suspicious `package.json` scripts (scanned with `jq`) -- Editor config that runs code unprompted: a `runOn` trigger or `task.allowAutomaticTasks: true` - in `.vscode/*.json` or `.idea/tasks.json`, and download-and-run commands there (`curl | sh`, - `powershell`, `osascript`, `base64 -d`), which covers an MCP `stdio` server that fetches and - runs a payload -- Executable payloads disguised as binary assets (JavaScript inside a `.woff2`, `.png`, ...) -- Clipboard, keystroke or screen capture paired with exfiltration, see - [below](#clipboardkeyloggerexfil-detection) -- Environment files (`.env`, `.env.*`) tracked in the git index - -It scans JS/TS/Python/Rust/Ruby/C/C++/C# sources out of the box and skips `node_modules` and VCS -dirs. Build output (bundled code from webpack, esbuild, or ncc) is scanned when committed; untracked build output that `.gitignore` lists is skipped. **In bundled code, only high-signal patterns are reported as findings** β€” dynamic code execution and network-access patterns routine in minified code are tracked but not flagged; string-table obfuscation, capture/exfiltration and bot tokens still are. - -Official Yarn releases in `.yarn/releases` are verified by SHA256 checksum against a table of 71 official releases instead of content-scanned; a mismatch fails. Where a bare regex would be noisy it wants context: a decode call only -trips near an execution call or a long literal, and a lone ANSI color escape is not a payload. - -## πŸ–₯️ Host audit - -`am-i-hacked host` is read-only. It never changes anything and never prints secret values. - -By default it checks one folder (the argument, or the current directory): the AI-tool config in -that folder. `--system` (alias `--full-system-scan`) adds every row below. Login items are not -read unless you ask, because most runs are about the project in front of you. - -| Area | What it looks for | +| Scan | Checks | | --- | --- | -| Persistence (`--system`) | launchd (macOS), user systemd units and XDG autostart (Linux): entries that capture the clipboard, keys or screen, or send to Telegram/Discord/webhooks; scripts in user-writable places; entries that point at a missing program; entries that inject a library or turn off TLS checks; `com.apple.*` labels planted in your own directories; unreadable or unparsable plists. On macOS, also capture-tool folders in Application Support | -| Crontab (`--system`) | a remote script piped to a shell, and jobs that run from user-writable places | -| Code signatures (`--system`, macOS) | the binary each launchd entry runs, read with `codesign`: a vendor label (`com.google.*`, `ru.keepcoder.*`, ...) that is unsigned or signed by a Team ID that vendor does not use (`HIGH`). Expected IDs come from [`bin/vendor-teams.tsv`](bin/vendor-teams.tsv), then from your installed apps with the same bundle-id prefix; a signature that no longer verifies (`MEDIUM`); an unsigned or ad-hoc signed program in Application Support, `/tmp` or another writable place (`MEDIUM`, elsewhere `INFO`). `--verbose` prints each entry's signer, which is the name System Settings shows under Login Items | -| Shell startup files (`--system`) | piping a download to a shell, decoded payloads, `eval` of downloaded code, `DYLD_INSERT_LIBRARIES`, `NODE_OPTIONS --require`, disabled TLS checks, extra CA bundles, proxies, `sudo`/`ssh` aliases, and scripts sourced from writable dirs (one level deep) | -| AI-tool config (folder always; user-level and managed with `--system`) | Claude, Codex, Cursor, Gemini, Kilo and OpenCode settings: a model base URL pointed at loopback (with the command line of whatever listens on that port) or a non-vendor host, hooks that run code from a writable path or observe every prompt, disabled permission prompts, plain-text keys, unpinned MCP servers, TLS-off and preload variables | -| Processes (`--system`) | interpreters running from staging directories, and capture-named scripts that report out | - -Findings are `HIGH`, `MEDIUM` or `INFO`. `HIGH` and `MEDIUM` exit `1`. To accept something you -have reviewed, add its id and a reason to `~/.config/am-i-hacked/host-allow.txt` (the tool -does not create that folder; run `mkdir -p ~/.config/am-i-hacked` first): - -```text -agent:~/.claude/settings.json:base-url:3 | local LiteLLM gateway I run myself -``` - -It honors `CLAUDE_CONFIG_DIR` and `CODEX_HOME`. Toolchain files from nvm, rvm, cargo, conda and -friends are not flagged just for being sourced from a hidden directory. +| Folder | Dynamic code execution, child processes, direct network module access, runtime global mutation; encoded or obfuscated payloads and unusually long lines; suspicious `package.json` scripts; editor config that runs code on folder open; download-and-run commands in editor config; executable payloads disguised as asset files; clipboard, keystroke or screen capture paired with an exfiltration endpoint; `.env` files in the git index; official Yarn releases verified by SHA256; Python virtualenvs (files that no longer match their package `RECORD` hashes, files no package owns, `.pth` or startup hooks that run code). | +| System (`--system`) | Login persistence (launchd on macOS; systemd user units and XDG autostart on Linux) and, on macOS, the code signature of what it launches; crontab; shell startup files; user-level and managed AI-tool config; running processes; and the "dark corners" where tooling installs code outside any project (`~/.venv`, tool and package caches, and the like), each scanned with the folder scan when present and skipped when not. | -## Reading the output +The folder scan also checks its own AI-tool config (`.claude/settings*.json`, `.mcp.json`). It reads dot-directories and tracked files that `.gitignore` matches; `node_modules` and `.git` are always skipped. In bundled code (webpack/esbuild/ncc) only high-signal patterns are reported; routine dynamic-code and network patterns are counted but not flagged. Findings are `HIGH`, `MEDIUM` or `INFO`; `HIGH` and `MEDIUM` exit `1`. -Each unique `file:line` is reported once with every indicator category that -matched it, so one suspicious location is easy to review instead of being -repeated under each category heading. Match snippets are width-capped β€” a -single minified line cannot flood the report. Output is plain (no ANSI) when -piped; colors are used only on a TTY (set `NO_COLOR` to disable). Findings -are listed sorted by path, then line. +## Flags -While it runs, the scan prints progress on stderr: one line per check with the elapsed time and -the findings so far, and `host --system` counts login items as it checks their signatures, which -is the slow part. Progress is on by default in a terminal; set `AIH_PROGRESS=1` to see it in CI -logs, or `AIH_PROGRESS=0` to turn it off. The report on stdout does not change. +| Flag | Applies to | Meaning | +| --- | --- | --- | +| `` | folder | Directory to scan (default: the current directory). | +| `host` | folder | Check a folder's AI-tool config only: `am-i-hacked host [dir]`. | +| `--system` | both | Also audit the whole machine. Alias: `--full-system-scan`. | +| `--max-findings N` | folder | Maximum findings printed (default: 1000). The true total and a per-rule count are always shown. | +| `-v`, `--verbose` | system | Also list informational items and every persistence entry, with its signer. | +| `-h`, `--help` | both | Show usage. | -If a finding is a false positive because the *pattern* is too broad, that's a -scanner bug β€” please [open an issue](https://github.com/IsaacBell/secure-devtools/issues). -If the code itself can reasonably be rewritten to stop matching, **prefer -that** over suppressing. Malicious test fixtures should live outside the -scanned tree (the scanner excludes directories named `__security_gate_fixtures__` -unless `INCLUDE_FIXTURES=1`). For the remaining case β€” the match is accurate -and the code is genuinely fine as written β€” mark it reviewed instead: +`--system` is shorthand for `host --system`. System-wide Linux units and `/etc/cron.*` are not covered yet. ## Suppressing a finding -Some findings are real matches on code that is genuinely safe β€” a giant -hardcoded string literal, a command built from a value that's already been -validated, and so on. For those, mark the line reviewed instead of -rewriting working code to dodge the pattern: +When a match is accurate but the code is genuinely safe, mark the line reviewed instead of rewriting working code: ```js const decoded = atob(header); // am-i-hacked-ignore: decodes a request header, not a payload ``` -The marker is `am-i-hacked-ignore:` followed by a reason, on the -finding's own line or the line immediately before it (handy when the flagged -line is too long to comment on directly, like a huge literal): - -```js -// am-i-hacked-ignore: bee movie script fixture, not obfuscated code -const script = "...49,000 characters..."; -``` +The marker is `am-i-hacked-ignore:` followed by a required reason, on the finding's own line or the line immediately before it. The pre-2.0 spelling, `am-i-compromised-ignore:`, is still honored. Suppressed findings are never dropped silently: they are counted and listed in their own section on every run, including a clean one. The marker is honored by the folder scan only β€” `safe-pull` deliberately does not read it, because it inspects commits nobody has reviewed. -The pre-2.0 spelling, `am-i-compromised-ignore:`, is still honored. +For the system scan, accept a reviewed finding by adding ` | ` to `~/.config/am-i-hacked/host-allow.txt` (create the folder first; override the path with `AIC_HOST_ALLOW`). Allowed findings are listed on every run. -The reason is required β€” a marker with nothing after the colon does not -suppress anything, so an empty "make it go away" comment can't quietly defeat -the gate. The marker is recognized as plain text anywhere on the line; it -does not need to sit inside any particular comment syntax, since the scanner -reads half a dozen languages. +## Exit codes -Suppressed findings are **never dropped silently**. They are counted and -listed in their own section of the report on every run, including a clean -one, so a suppression can't quietly go stale or hide a second, unrelated -issue on the same line: - -``` -am-i-hacked: 1 finding suppressed by inline comment - - src/auth.ts:42 - const decoded = atob(header); // am-i-hacked-ignore: decodes a request header, not a payload - β†’ Encoded payload primitives (suppressed) - reason: decodes a request header, not a payload -``` - -This marker is honored by the project scan only. **`safe-pull` does -not read it.** `safe-pull` inspects commits nobody has reviewed yet β€” that's -the entire point of the guard β€” so a marker written by whoever authored the -incoming diff must never be able to wave off their own payload. - -## Clipboard/keylogger/exfil detection - -A scanner that only knows npm supply-chain patterns misses a whole class of -malware: a hidden script that reads the clipboard β€” or the keyboard, or the -screen β€” and forwards what it captures to a remote service. This detection -models a real, observed stealer class: a LaunchAgent starts a small shell -wrapper, the wrapper launches a Node script in the background, and the script -posts every clipboard change to a Telegram bot or a webhook. - -A capture API on its own is ordinary (clipboard managers, screenshot tools, -test helpers), so these signals are combined **per file**: a capture signal and -an exfiltration signal in the *same* file is reported, and a few decisive shapes -are reported on their own. The check covers `.js`, `.mjs`, `.cjs`, `.ts`, `.py`, -`.sh`, `.zsh`, `.bash`, `.rb`, `.swift`, `.plist`, and extensionless scripts with -a shebang. - -| Signal group | Example indicators | Reported as | -| --- | --- | --- | -| Clipboard read | `pbpaste`, `xclip`, `xsel`, `wl-paste`, `Get-Clipboard`, `clipboardy`, `clipboard-event`, `NSPasteboard`, `navigator.clipboard.readText`, `pyperclip` | context for the rows below | -| Keystroke / screen capture | `CGEventTap`, `pynput`, `iohook`, `node-global-key-listener`, `keylogger`, `screencapture`, `screenshot-desktop`, `pyautogui.screenshot` | context for the rows below | -| Exfiltration | `api.telegram.org`, `/sendMessage`, `/sendDocument`, `node-telegram-bot-api`, `telegraf`, Discord/Slack webhooks, `webhook.site`, `pastebin.com/api`, `transfer.sh`, `ngrok`, `nc`/`ncat` to a host, bot-token shape `[0-9]{8,10}:[A-Za-z0-9_-]{35}` | context for the rows below | -| Capture **and** exfiltration in one file | any capture signal together with any exfiltration signal | Clipboard/keystroke/screen capture with remote exfiltration | -| Hardcoded Telegram bot token | a `123456789:AAA…` token literal in a scanned file | Telegram bot token literal | -| Background launcher wrapper | `nohup node .js >> &` behind a pid-file lock | Background node launcher with a pid-file lock | -| …with a live payload | the named `.js` sits beside it and captures + exfiltrates | Background node launcher wraps a capture-and-exfiltrate payload | -| Persistence beside capture | `launchctl load`, `~/Library/LaunchAgents`, `crontab -`, `~/.config/autostart` in a script that also captures | Persistence installed by a script that captures input | -| Capture-shaped file name | name matching `(clip\|key\|screen)[-_ ]?(logger\|monitor\|spy\|grab)` that reads the clipboard or input | Capture-named script reads the clipboard or input | - -Files under `node_modules` and the fixtures dir are never scanned. Untracked -build output that `.gitignore` lists is skipped too, so a README mention or a -vendor's clipboard-library source with no exfiltration endpoint does not trip -the scan. Reviewed matches can still -be marked safe with `am-i-hacked-ignore:` (see above). - -`am-i-hacked host` audits the machine itself for the persistence side of this -same class β€” launch agents, shell startup files, AI-tool config, and running -processes. +| Code | Meaning | +| --- | --- | +| `0` | Nothing found. | +| `1` | Findings to review, or a missing requirement (`bash` 4.2+, `rg`, or `jq`; the message names it). | +| `2` | Usage error. | ## Guarded pull (`safe-pull`) -Scanning the working tree is too late for one class of attack: a commit that adds -a `.vscode/tasks.json` running on folder open, or an MCP `stdio` server that starts -with the editor, executes as soon as the code is checked out. `safe-pull` closes -that window by inspecting the incoming commits before anything is written: +`safe-pull` inspects incoming commits before anything is written to disk, then fast-forwards. It checks for a rewritten (force-pushed) upstream, an author/committer mismatch, editor auto-run tasks, download-and-run editor commands, executable payloads disguised as assets, committed `.env` files, and a `dotenv` plus `node-fetch`/`axios` dependency pair. ```sh safe-pull # fetch, inspect, then merge --ff-only @@ -304,65 +106,28 @@ safe-pull --force-update # integrate despite a rewritten upstream history safe-pull --remote --branch # inspect a specific remote branch ``` -What it checks in the incoming commits: a force-pushed (rewritten) upstream, an -author/committer mismatch, a commit rewritten after authoring, editor auto-run -tasks, download-and-run editor commands, executable payloads disguised as asset -files, committed `.env` files, and a `dotenv` plus `node-fetch`/`axios` dependency -pair in a changed `package.json`. The inspection uses `git grep` against the fetched -commit, so it reads blobs from the object store and never writes files to disk. -Plain `git fetch` is safe on its own β€” it executes nothing. +Exit codes: `0` clean, `1` findings (nothing merged), `2` usage or setup problem. -Exit codes: `0` clean (and merged, unless `--dry-run`), `1` findings (nothing -merged), `2` usage or setup problem. +## Commands installed -To use it as a git alias: +`am-i-hacked` (the folder scan and `host`), `aih`, `am-i-compromised` and `aic` (the old name and its short form), `security-gate`, `scanner`, and `safe-pull`. -```sh -git config --global alias.safe-pull '!safe-pull' -``` - -## Renamed from am-i-compromised - -Version 2.0.0 renames the package from `am-i-compromised` to `am-i-hacked`. +## Limits -- `am-i-compromised`, `security-gate`, `scanner` and `safe-pull` still work. -- The project scan's report lines start with `am-i-hacked:` instead of `security-gate:`. -- Suppress with `am-i-hacked-ignore:`. The old `am-i-compromised-ignore:` marker is still honored. -- The host audit's allow file is `~/.config/am-i-hacked/host-allow.txt`. - -See [CHANGELOG.md](CHANGELOG.md). +It looks for warning signs. It is not antivirus, and it does not look up known viruses. A clean result means it found no warning signs; it does not prove the code or the machine is safe. Some checks also flag normal code, such as `eval`; when you have checked a line and it is fine, suppress it with a reason as above. The scanner runs entirely on your machine and sends nothing anywhere. ## Development -Requirements: `bash` 4.2+, `rg`, `jq`, `git`, [`pnpm`](https://pnpm.io), and, for `lint` and -`format`, `shellcheck` and `shfmt`. +Requirements: `bash` 4.2+, `rg`, `jq`, `git`, `pnpm`, and, for lint and format, `shellcheck` and `shfmt`. `bin/system-scan.sh` is the system scanner; `bin/host-audit.sh` remains as an alias. ```sh pnpm install -pnpm test # bats suite -pnpm check # shellcheck + shfmt + bats +pnpm test # bats suite +pnpm check # shellcheck + shfmt + bats ``` -The Bats suite lives in `test/` and includes a self-test that scans the quarantined -fixtures in `test/__security_gate_fixtures__/` β€” treat everything in that directory as -malware and never execute or import it. - -## Contributing - -Bug reports, feature ideas, and pull requests are welcome. Open an issue or a pull request at -. - -## Sponsorship - -If this tool keeps your projects safe, consider supporting the work: - -[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/ibell) - -## Security - -Report vulnerabilities via GitHub's private advisory mechanism: -. +The Bats suite scans the quarantined fixtures in `test/__security_gate_fixtures__/`; treat everything there as malware and never execute or import it. -## License +## Contributing and security -MIT β€” see [LICENSE](LICENSE). +Bug reports and pull requests: . Report vulnerabilities privately via . MIT licensed; see [LICENSE](LICENSE). diff --git a/apps/am-i-hacked/bin/host-audit.sh b/apps/am-i-hacked/bin/host-audit.sh index 5bf027b..811560b 100644 --- a/apps/am-i-hacked/bin/host-audit.sh +++ b/apps/am-i-hacked/bin/host-audit.sh @@ -1,1343 +1,3 @@ #!/usr/bin/env bash -# bin/host-audit.sh β€” run as `am-i-hacked host`. -# -# Is something on THIS machine persisting, capturing, or steering your tools? -# -# The repository scanner (scanner.sh) reads source trees, so it cannot see what -# lives outside them. In a real, observed clipboard-stealer class, a user -# LaunchAgent starts a shell wrapper from ~/Library/Application Support, which -# launches a Node script that sends every clipboard change to a Telegram bot. No -# repository contains any of it, so no source scan can find it. -# -# Read-only: it never modifies, deletes, or contacts anything. It checks: -# - login persistence: launchd agents and daemons (macOS), systemd user units -# and XDG autostart (Linux), and the user crontab -# - the scripts those entries launch, and the script files beside them: -# clipboard reads, keystroke or screen capture, and exfiltration endpoints -# - shell startup files: piped remote scripts, injected libraries, hijacked -# sudo/ssh, background launchers, redirected API base URLs -# - AI-tool configuration: redirected API base URLs, permission bypass, -# plain-text keys, MCP servers that run unpinned code, and every hook that -# observes prompts and tool output -# - running processes: interpreters running capture or staging-area scripts -# -# HIGH and MEDIUM findings make the exit status 1. INFO never does. -# -# A finding you reviewed and accept can be allowed with a reason, one per line -# in ~/.config/am-i-hacked/host-allow.txt (override with AIC_HOST_ALLOW): -# -# | -# -# The reason is required. Allowed findings are listed on every run, never hidden. -# -# Test seams: AIC_HOST_HOME, AIC_HOST_PROJECT, AIC_HOST_OS, AIC_HOST_LAUNCH_DIRS -# (colon-separated), AIC_HOST_PS_FILE, AIC_HOST_CRONTAB_FILE, AIC_HOST_MANAGED_DIRS -# (colon-separated; the root-owned agent settings directories). CLAUDE_CONFIG_DIR -# and CODEX_HOME are honored as on a real machine. -# -# Written for bash 3.2 (the macOS system bash): no associative arrays, mapfile, -# or case-conversion expansions. - -set -u - -usage() { - cat <<'EOF' -usage: am-i-hacked host [--system] [--verbose] [dir] - -Read-only audit. By default it checks one folder: the AI-tool config in dir -(.claude/, .mcp.json). --system adds the machine: login persistence and the -code signatures of what it launches, the crontab, shell startup files, -user-level AI-tool configuration, and running processes. Exits 1 if anything -needs review. - - dir folder to check (default: the current directory) - --system also audit the whole machine (alias: --full-system-scan) - -v, --verbose also list informational items and every persistence entry - -h, --help show this help - -No root or sudo is needed. - -Allow a reviewed finding by adding " | " to -~/.config/am-i-hacked/host-allow.txt (or the file named by AIC_HOST_ALLOW). -EOF -} - -VERBOSE=0 -SYSTEM=0 -PROJECT_ARG="" -for arg in "$@"; do - case "$arg" in - -v | --verbose) VERBOSE=1 ;; - --system | --full-system-scan) SYSTEM=1 ;; - -h | --help) - usage - exit 0 - ;; - -*) - echo "host-audit: unknown option '$arg'" >&2 - usage >&2 - exit 2 - ;; - *) - if [[ -n "$PROJECT_ARG" ]]; then - echo "host-audit: give one folder, not '$PROJECT_ARG' and '$arg'" >&2 - exit 2 - fi - PROJECT_ARG="$arg" - ;; - esac -done -if [[ -n "$PROJECT_ARG" && ! -d "$PROJECT_ARG" ]]; then - echo "host-audit: '$PROJECT_ARG' is not a directory" >&2 - exit 2 -fi - -HOME_DIR="${AIC_HOST_HOME:-${HOME:-}}" -[[ -n "$HOME_DIR" ]] || HOME_DIR="$(cd ~ 2>/dev/null && pwd)" || HOME_DIR="" -if [[ -z "$HOME_DIR" ]]; then - echo "host-audit: cannot determine the home directory (HOME is unset)." >&2 - exit 2 -fi -if [[ -n "$PROJECT_ARG" ]]; then - PROJECT_DIR="$(cd "$PROJECT_ARG" && pwd)" -else - PROJECT_DIR="${AIC_HOST_PROJECT:-$PWD}" -fi -OS="${AIC_HOST_OS:-$(uname -s)}" -ALLOW_FILE="${AIC_HOST_ALLOW:-${XDG_CONFIG_HOME:-$HOME_DIR/.config}/am-i-hacked/host-allow.txt}" - -if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then - C_RED=$'\033[31m' C_YELLOW=$'\033[33m' C_GREEN=$'\033[32m' C_DIM=$'\033[2m' C_BOLD=$'\033[1m' C_RESET=$'\033[0m' -else - C_RED='' C_YELLOW='' C_GREEN='' C_DIM='' C_BOLD='' C_RESET='' -fi - -# Progress: each stage, and a running count while login items are checked, on stderr. On by -# default only when stderr is a terminal; AIH_PROGRESS=1 forces it on (CI logs), AIH_PROGRESS=0 -# turns it off. The report on stdout is unchanged either way. -if [[ "${AIH_PROGRESS:-}" == 1 || ("${AIH_PROGRESS:-}" != 0 && -t 2) ]]; then - PROGRESS=1 -else - PROGRESS=0 -fi -AUDIT_START=$SECONDS - -stage() { - [[ "$PROGRESS" == 1 ]] || return 0 - printf '%s[%3ds, %s found so far]%s %s\n' "$C_DIM" "$((SECONDS - AUDIT_START))" \ - "$(printf '%s' "$FINDINGS" | grep -c . || true)" "$C_RESET" "$1" >&2 -} - -# item_progress