diff --git a/CHANGELOG.md b/CHANGELOG.md index f51d87b..eb65d1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,3 +101,4 @@ Newest first. One line per change. Default-behavior changes marked **CHANGED**. - Initial release of am-i-compromised. - Scanner reporting improved, test suite hardened, demo added. npm release procedure documented. + diff --git a/README.md b/README.md index e8dc1d2..e899e3e 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,48 @@ # secure-devtools -Plain-shell security tools that check the code you are about to run, and the machine you run it on, for signs of a supply-chain or dev-environment attack. They run locally and in CI, need no account, and have zero npm runtime dependencies. +Plain-shell tools that check the code you are about to run, and the machine you run it on, for signs of a supply-chain or dev-environment attack. They run locally, need no account or API key, and have zero npm runtime dependencies. ```sh -pnpx am-i-hacked . # or: npx am-i-hacked . +npx 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) [![npm am-i-hacked](https://img.shields.io/npm/v/am-i-hacked?label=am-i-hacked)](https://www.npmjs.com/package/am-i-hacked) [![npm am-i-being-recorded](https://img.shields.io/npm/v/am-i-being-recorded?label=am-i-being-recorded)](https://www.npmjs.com/package/am-i-being-recorded) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -[![skills.sh](https://skills.sh/b/IsaacBell/secure-devtools)](https://skills.sh/IsaacBell/secure-devtools) +[![npm secure-semgrep](https://img.shields.io/npm/v/secure-semgrep?label=secure-semgrep)](https://www.npmjs.com/package/secure-semgrep) -**Status:** actively maintained. `am-i-hacked` 2.0.1 and `am-i-being-recorded` 1.0.0 are the current releases. macOS and Linux. +**Status:** actively maintained, macOS and Linux. Current releases: `am-i-hacked` 2.0.1, `am-i-being-recorded` 1.0.0, `secure-semgrep` 1.0.1. The next `am-i-hacked` release adds a dark-corner system scan, a Python virtualenv integrity check and `--max-findings`; each package's changelog has the detail. Website: . ## Tools -| Tool | What it checks | How to run | +| Tool | What it checks | Run | | --- | --- | --- | -| [`am-i-hacked`](apps/am-i-hacked/README.md) | A project folder, for malicious-code indicators: editor tasks that run on folder open, payloads disguised as asset files, obfuscation, capture code paired with an exfiltration endpoint, `.env` files in the git index, risky `package.json` scripts. With `--system`, this machine: login and startup items and their code signatures, crontab, shell startup files, AI-tool config, running processes. | `pnpx am-i-hacked` | -| [`am-i-being-recorded`](apps/am-i-being-recorded/README.md) | Which browser extension is behind a screen-recording indicator, and what else on the machine can capture you. | `pnpx am-i-being-recorded` | -| [`secure-semgrep`](apps/secure-semgrep/README.md) | Semgrep rules and loadout packs for AI-agent, bash and web code. Needs `semgrep`. | From this repository | +| [`am-i-hacked`](apps/am-i-hacked/README.md) | A project folder, before you open, install or run it, for malicious-code indicators: editor tasks that run on folder open, payloads disguised as asset files, obfuscation, capture code paired with an exfiltration endpoint, `.env` files in the git index, risky `package.json` scripts, official Yarn releases by SHA256, and Python virtualenv integrity. With `--system`, this machine: login items and their code signatures, crontab, shell startup files, AI-tool config, running processes, and the caches and bin folders where tooling installs code outside any project. | `npx am-i-hacked` | +| [`am-i-being-recorded`](apps/am-i-being-recorded/README.md) | Which browser extension is behind a screen-recording indicator, and what else on the machine can capture you. | `npx am-i-being-recorded` | +| [`secure-semgrep`](apps/secure-semgrep/README.md) | Bundled Semgrep rules you own β€” AI-agent, bash and SSRF patterns β€” plus maintained registry loadouts (Python, JS, TS, React, Node, Rust), fetched from the Semgrep registry at scan time. Needs `semgrep` on PATH. | `npx secure-semgrep ./src` | ### Agent skills -Install all seven skills for your user account, or pick one: +Install all seven for your user account, or pick one: ```sh npx skills add IsaacBell/secure-devtools -g npx skills add IsaacBell/secure-devtools --skill ssrf-safe-fetch ``` -Drop `-g` to install into the current project, and add `--list` to see the skills without installing. `pnpx skills add` works the same way. The `skills` command reports anonymous install counts to skills.sh; set `DISABLE_TELEMETRY=1` to turn that off. +Drop `-g` to install into the current project, and add `--list` to see the skills without installing. The `skills` command reports anonymous install counts to skills.sh; set `DISABLE_TELEMETRY=1` to turn that off. -The skills, in [`skills/`](skills/): [quarantine-review](skills/quarantine-review/SKILL.md) (inspect an untrusted repository without running any of it), [secure-skill-pull](skills/secure-skill-pull/SKILL.md) (vet a skill or plugin from a URL without running its installer), [create-skill](skills/create-skill/SKILL.md) (write, check and publish an agent skill), [skill-publish-review](skills/skill-publish-review/SKILL.md) (review skills before they go into a public repository), [ssrf-safe-fetch](skills/ssrf-safe-fetch/SKILL.md) (validate the URL and the resolved address before a server fetches it), [login-item-triage](skills/login-item-triage/SKILL.md) and [jujutsu](skills/jujutsu/SKILL.md). +The skills, in [`skills/`](skills/): [quarantine-review](skills/quarantine-review/SKILL.md) (inspect an untrusted repository without running any of it), [secure-skill-pull](skills/secure-skill-pull/SKILL.md) (vet a skill or plugin from a URL without running its installer), [create-skill](skills/create-skill/SKILL.md) (write, check and publish an agent skill), [skill-publish-review](skills/skill-publish-review/SKILL.md) (review skills before they go into a public repository), [ssrf-safe-fetch](skills/ssrf-safe-fetch/SKILL.md) (validate the URL and the resolved address before a server fetches it), [login-item-triage](skills/login-item-triage/SKILL.md) (the manual method behind am-i-hacked's login-item signature checks) and [jujutsu](skills/jujutsu/SKILL.md). ## 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. +- One command from a cold machine: `npx am-i-hacked` needs no signup, account, API key or install step. +- Exit `1` on HIGH or MEDIUM findings, so it works as a pre-commit hook, a `package.json` script or a CI step. The [am-i-hacked README](apps/am-i-hacked/README.md#install-and-run) has the workflow snippet. +- It reads the repository for indicators advisory scanners cannot report: a `.vscode/tasks.json` that runs on folder open, a payload saved as a font file, a launchd or systemd item that starts at login, a capture tool paired with an exfiltration endpoint. `npm audit`, OSV-Scanner and Snyk match your dependencies against published advisories, so an unreported attack, or one committed into the tree, stays invisible to them. Run both. ## What it does not do -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, mark it with a short reason. That line still shows up as reviewed on every run, so nothing gets hidden. +The scanners look for warning signs; they are not antivirus and do not look up known viruses. A clean result means no warning signs were found, not that the code or the machine is safe. Some checks also flag ordinary code, such as `eval`; when a line is fine, mark it reviewed with a short reason (`am-i-hacked-ignore: `) and it still appears as reviewed on every run. ## Develop @@ -57,22 +54,11 @@ mise run setup # dev dependencies and git hooks mise run check # shellcheck, shfmt check and the bats tests: what CI runs ``` -`mise run` lists every task. The git hooks run the same checks before each commit. CI in [`.github/workflows/`](.github/workflows/) runs the checks and tests, this repository's own security gate (`mise run gate`), a gitleaks secret scan, Semgrep, dependency review, and CodeAnt AI scan. Releases: [docs/RELEASING.md](docs/RELEASING.md). Changes: each package's `CHANGELOG.md`. - -## 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. - -Run it over this repository (review mode records findings without breaking the build): - -```bash -$ mise run semgrep -``` - -To turn findings into a hard gate, run `mise run semgrep-strict`. In any other repository, use it the same way as a post-`npm install` script β€” see the [package README](apps/secure-semgrep/README.md) for loadouts (`react`, `ts`, `node`, `py`, `rust`) and CI snippets. +`mise run` lists every task. The git hooks run the same checks before each commit. CI in [`.github/workflows/`](.github/workflows/) runs the checks and tests, this repository's own security gate (`mise run gate`), a gitleaks secret scan, Semgrep, dependency review and a CodeAnt AI scan. Releases: [docs/RELEASING.md](docs/RELEASING.md). Static analysis runs as `mise run semgrep` (review mode) or `mise run semgrep-strict` (gate); [secure-semgrep's README](apps/secure-semgrep/README.md) documents the loadouts and CI snippets. ## Contributing, security and support +- Maintainer: [Isaac Bell](https://isaacbell.io) ([@IsaacBell](https://github.com/IsaacBell)). - Contributing: [CONTRIBUTING.md](CONTRIBUTING.md). Issues and pull requests are welcome. - Security problems: [report them privately](https://github.com/IsaacBell/secure-devtools/security/advisories/new); [SECURITY.md](SECURITY.md) has the details. Please do not open a public issue. Published fixes are listed under [security advisories](https://github.com/IsaacBell/secure-devtools/security/advisories). - Sponsor the work: [ko-fi](https://ko-fi.com/ibell). `npm fund` points to the same page. diff --git a/apps/am-i-being-recorded/README.md b/apps/am-i-being-recorded/README.md index a0cb81f..297f44b 100644 --- a/apps/am-i-being-recorded/README.md +++ b/apps/am-i-being-recorded/README.md @@ -8,7 +8,7 @@ Find out **which browser extension is recording your screen** β€” and what else the machine can capture you. ```sh -pnpx am-i-being-recorded # or: npx am-i-being-recorded +npx am-i-being-recorded ``` The sibling of [`am-i-hacked`](https://www.npmjs.com/package/am-i-hacked): @@ -35,13 +35,13 @@ apt-get install jq Run without installing: ```sh -pnpx am-i-being-recorded +npx am-i-being-recorded ``` Install as a dev dependency: ```sh -pnpm add -D am-i-being-recorded # or: npm install -D am-i-being-recorded +npm install --save-dev am-i-being-recorded ``` ## What it checks diff --git a/apps/am-i-hacked/CHANGELOG.md b/apps/am-i-hacked/CHANGELOG.md index 53a6958..34a9163 100644 --- a/apps/am-i-hacked/CHANGELOG.md +++ b/apps/am-i-hacked/CHANGELOG.md @@ -2,6 +2,29 @@ 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, for every venv under the scanned folder: files that no longer match their package `RECORD` hashes are a `HIGH` finding; `RECORD` paths that leave the venv or pass through a symlinked folder (never opened), files and symlinks no package owns, console scripts and symlinks in `bin/` no package claims (each venv checked on its own claims), a console script name containing `|`, `RECORD` entries with no hash, a venv whose `RECORD` files or whole `dist-info` folders were deleted, a `_virtualenv.pth` that is not the stock one-line file, `.pth` and `sitecustomize` hooks that run code, unreadable files and folders (reported rather than skipped), and file names containing newlines are all reported. +- `--max-findings N` sets the maximum findings printed, from 1 to 999999999, 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. +- The system scan reports a check it could not run instead of passing it: `ps` missing or failing, a `crontab -l` error other than "no crontab", `codesign` missing, an AI-tool config `jq` cannot parse (JSONC), an unreadable dark corner, your own relative-path process whose working directory `lsof` could not resolve, and a login item's file that you own but cannot read (a root-owned one stays informational). + +### Changed +- **CHANGED:** hex and unicode escape findings inside installed dependency folders (`site-packages`, `dist-packages`, `node_modules`) are reported as one summary count instead of one finding per match. `vendor/` is committed with the project, so its findings are still listed. +- **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. + +### Security +- A source file containing a NUL byte was skipped as binary, so one NUL in a comment hid the whole file from the folder scan. Source files are now always read as text. +- A systemd or autostart `Exec` line containing `*` was glob-expanded against the scanner's working directory. It is now split without globbing. +- A hook command containing a tab was cut at the tab, so a dangerous path after it was not checked. +- A persistence script whose file name contains a newline was not read. + +### Fixed +- The `ignored null byte in input` warnings are no longer emitted, including for matched lines and build-output detection. + ## 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..9ededa2 100644 --- a/apps/am-i-hacked/README.md +++ b/apps/am-i-hacked/README.md @@ -1,300 +1,99 @@ -# πŸ•΅οΈ 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 . +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. The next release adds a dark-corner system scan, Python virtualenv integrity and `--max-findings`; the tables below document that next-release state, and [CHANGELOG.md](CHANGELOG.md) marks it Unreleased. Formerly published as `am-i-compromised`; the old command name still works. + +## 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 +npx am-i-hacked # scan the folder you are in +npx am-i-hacked --system # also check 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 +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, jq and npx but no ripgrep): ```yaml -- uses: pnpm/action-setup@v4 - with: - version: 10 - run: sudo apt-get install -y ripgrep -- 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 +- run: npx am-i-hacked@2 ``` -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, 1 to 999999999 (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 +103,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