Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ NEXT_PUBLIC_API_URL=http://localhost:3000/api
DATABASE_URL=postgresql://localhost:5432/test_db

# .env.test.local (gitignored)
NEXTAUTH_SECRET=test-secret-local
NEXTAUTH_SECRET=test-secret-local # security.sh:allow (doc example placeholder)
```

## App Router Patterns
Expand Down
82 changes: 82 additions & 0 deletions .github/skills/security-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
name: security-review
description: >
Security review rules for any RozoAI pull request. Use when reviewing a PR,
before merging, or auditing a diff — especially for changes that touch build
configs, dependencies, .env files, CI, or payment/treasury code. Encodes the
lessons from the 2026-06 supply-chain incidents (tailwind dead-drop loader +
the atob/eval C2 loader). Iterate these rules here in rozo-security; repos
copy what they need.
---

# Security Review — RozoAI

Run `security.sh scan` first (it's mechanical). Then a human/agent applies the
judgment rules below. The scanner catches known shapes; review catches intent.

## 1. Build configs are executable code — review them like code

Any file that runs at build/dev/CI time can exfiltrate the build env. Treat
these as high-scrutiny on every diff:

`*tailwind.config.*` · `metro.config.*` · `app.config.*` · `babel.config.*` ·
`next.config.*` · `vite.config.*` · `postcss.config.*` · `react-router.config.*` ·
`vercel.json` · `.npmrc` · `package.json` (scripts) · any `*.config.{js,ts,mjs,cjs}`

**Red flags in these files (block the PR):**
- `eval(...)`, `new Function(...)`, `child_process` / `spawn` / `exec`
- `atob(...)` / `Buffer.from(x,'base64')` followed by execution or a network call
- `fetch` / `node-fetch` / `import(...)` to any non-obvious domain
- **Code that runs at module load** — an IIFE `(async () => {...})()`, or any
statement *before* or *after* the legitimate `export default` / `module.exports`.
A clean config only *exports* an object; it doesn't *do* anything.
- A config that suddenly `import 'dotenv/config'` to read an env var it never needed.

## 2. Both incidents hid the same way — know the patterns

| Campaign | Where | Tell |
|---|---|---|
| tailwind dead-drop | `tailwind.config.js` tail | code AFTER `module.exports`; beacon `9-0037-2`; reads Tron/BSC chains for stage-2 |
| atob/eval C2 | `react-router.config.ts` / `database.types.ts` head | IIFE BEFORE `export default`; `atob(process.env.X)` → `node-fetch` → `eval`; C2 URL base64'd in a tracked `.env` |

Both: authored by a **compromised developer account**, **back-dated commits** to
look old, and **woven into an existing commit** (no obvious "new suspicious
commit" on top). So: **don't trust commit dates**, and **diff the actual file
content** against a known-clean baseline — not just "what changed recently."

## 3. .env / secrets

- A **tracked `.env`** is a red flag by itself — it should be gitignored. In the
2026-06 incident a tracked `.env` was the *carrier* for the C2 URL (base64).
- Never approve a hard-coded private key, mnemonic, or `service_role` key.
A public Supabase **anon** key or a Firebase client key is OK (RLS/public by
design) but should be annotated `// security.sh:allow`.
- Secret values must never be pasted into the PR description, review comments,
or CI logs.

## 4. Dependencies & supply chain

- New dependency? Check it's the real package (typosquat?), pinned, and that its
`postinstall`/`prepare` scripts don't fetch+run remote code.
- A lockfile change with no corresponding `package.json` change is suspicious.

## 5. Permissions & merge hygiene (the structural lesson)

The 2026-06 malware survived ~6 months because an **admin self-merged without
review**, bypassing branch protection. So:
- PRs to `main` need a **different** person's approval. Authors don't self-approve.
- Keep the "require PR + approval" ruleset; don't put broad admin bypass on it.
- Payment/treasury/migration changes are **high risk**: require owner approval +
an independent review pass before merge.

## 6. The review checklist (paste into the PR)

```
[ ] security.sh scan passes (or every finding is justified)
[ ] no executable code in build configs (no eval/spawn/atob/fetch/IIFE; only exports)
[ ] no code before/after the config's export statement
[ ] no tracked .env; no hard-coded private key / mnemonic / service_role
[ ] new deps are real, pinned, no remote-fetching install scripts
[ ] diff reviewed against content, not trusting commit dates
[ ] not self-approved; high-risk (money/migration) has owner sign-off
```
39 changes: 39 additions & 0 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Security scan

# Pre-merge security gate. Runs scripts/security.sh (one file: code-injection +
# secret-leak checks) on every push and PR. See scripts/SECURITY.md.
#
# A finding fails the job. If/when you want to soften it while cleaning up known
# pre-existing findings, add `continue-on-error: true` to the job.

on:
push:
branches: ['**']
pull_request:
branches: ['**']

permissions:
contents: read

jobs:
security-scan:
name: Security scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Run security gate
run: |
chmod +x scripts/security.sh
bash scripts/security.sh scan

- name: On failure, explain
if: failure()
run: |
echo "::error::scripts/security.sh found a code-injection or secret-leak issue."
echo "Matched secret values are intentionally hidden — inspect the reported file:line."
echo "Real secret → remove + rotate + use a secret manager. Build-config injection → see docs/incident-tailwind-malware-2026-06-18.md."
echo "Confirmed false positive → add a '# security.sh:allow' comment on that line. See scripts/SECURITY.md."
67 changes: 67 additions & 0 deletions scripts/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Pre-commit Security Gate — SOP

One file does everything: **`scripts/security.sh`**. Copy that single file into
any repo to get the same protection — nothing else required.

It runs two checks on the files you're about to commit:

| Check | Catches |
|---|---|
| **Code injection** | Malicious code in build configs — the 2026-06 tailwind dead-drop loader class (`eval` / `child_process` / network calls / code after `module.exports`). Covers tailwind, metro, app.config, babel, vite, next, vercel.json, .npmrc, package.json scripts. |
| **Leaked secrets** | Credentials about to be committed: sensitive filenames (`.env`, `*.pem`, `*.key`, keystores, `.netrc`, service-account json) **and** secret shapes in content (JWT, AWS/GitHub/Stripe/Google/Slack tokens, mnemonics, EVM private keys). |

> **Secret matches never print the value** — only `file:line + rule name` — so
> secrets can't leak into CI logs, terminal scrollback, or AI transcripts.

## Use it

```bash
bash scripts/security.sh scan # scan the whole repo (what CI runs)
bash scripts/security.sh hook # scan only staged files (pre-commit)
bash scripts/security.sh install # install the git pre-commit hook (once per clone)
bash scripts/security.sh help
```

`install` writes a `.git/hooks/pre-commit` that calls `security.sh hook`, so
every commit is checked locally. CI runs `security.sh scan` via
`.github/workflows/ioc-scan.yml`.

## When it fires

- **Code injection** → a build config has suspicious code. Serious — do not
bypass without confirming it's benign. See
`docs/incident-tailwind-malware-2026-06-18.md`.
- **Leaked secret** →
- **Real secret**: remove it, **rotate it**, load it from env / a secret
manager. (git history keeps it alive until rotated.)
- **Placeholder/example**: mark it (`<value>`, `your-key-here`, `***`) or move
it into a `*.env.example` — those are allow-listed.
- **Reviewed & known-benign** (e.g. a public Supabase *anon* key): add a
`# security.sh:allow` comment on that line to silence it.

## Bypass (last resort)

```bash
git commit --no-verify # skips the local hook; use only for a confirmed false positive
```

CI still runs, so a real problem is caught there anyway.

## Copy to another repo

```bash
cp scripts/security.sh <other-repo>/scripts/
cp .github/workflows/ioc-scan.yml <other-repo>/.github/workflows/security-scan.yml
cd <other-repo> && bash scripts/security.sh install
```

No repo-specific values are hard-coded. To tune: edit the `CONTENT_RULES` /
`CONFIG_GLOBS` / `PUBLIC_BY_DESIGN_RE` arrays near the top of `security.sh`.

## Known pre-existing findings

`packages/shared/core/src/rozo-intent/index.ts:777,867` hold hard-coded Supabase
**anon** JWTs. The anon key is public by design (protected by RLS, not secrecy),
so this is a code-tidiness issue, not a leak — but the scanner flags it. Either
add `# security.sh:allow` on those lines, or move them to env, to get a green
scan.
Loading
Loading