audit-security
audit-security
A security audit that the operator will actually read.
Enterprise scanners emit hundreds of warnings. Non-developers silence them within a week and then a real vulnerability slips through because nobody's reading anymore. The opposite strategy works better: a short, hand-curated list of patterns that are almost always actionable, each one triaged individually. Every hit gets classified. Every classification has a reason.
The rule
Never report more than ~20 findings. If the automated sweep returns 100 hits, triage them into categories and report the categories, not the raw list. The operator's attention is the scarcest resource and the wrong optimization is "comprehensive."
When to trigger
Any of:
- "is this safe"
- "security check" / "security audit"
- "are there any vulnerabilities"
- "any secrets leaked"
- "is my
.envexposed" - "OWASP"
- before a public release or open-sourcing
- after a major refactor (run together with
refactor-verify) - when the operator mentions a data breach, incident, or scare
If the operator asks for something outside this skill's scope (compliance audits, penetration testing, crypto review), say so plainly and suggest a specialist tool.
Upfront constraint lock-in
Before running, establish three things with the operator. Do not start the sweep until these are clear:
- Scope — the whole repo, one directory, one file, or one PR diff?
- Severity threshold — report everything found, or only HIGH/CRITICAL?
- Runtime context — is this code behind an authenticated endpoint, public, internal tool, or CLI? The same pattern is a different severity depending on context.
If the operator doesn't know the answers, pick the most conservative defaults: whole repo, report all, assume public-facing. Tell them that's what you picked.
State assumptions — before acting
Before starting the procedure, write an explicit Assumptions block. Don't pick silently between interpretations; surface the choice. If any assumption is wrong or ambiguous, pause and ask — do not proceed on a guess.
Required block:
Assumptions:
- Exposure surface: <public-web | internal | CLI | library>
- Language/framework: <detected set — e.g., Next.js + Python API>
- Existing scanners: <none | Dependabot | Semgrep | GHAS — affects which patterns this sweep adds vs duplicates>
- Scope: <whole repo | subdirectory path | single file>
Typical items for this skill:
- The repo's exposure surface (public-web / internal / CLI / library)
- Language and framework set (affects which pattern families apply)
- Whether CI already runs scanners (affects what the manual sweep should add versus duplicate)
Stop-and-ask triggers:
- Scope is "the whole repo" but LOC > 50k — offer to scope by directory first
- "Audit" without a layer hint — ask server-side / client-side / dependencies / all
Silent picks are the most common failure mode: the skill runs, produces plausible output, and the operator doesn't notice the wrong interpretation was chosen. The Assumptions block is cheap insurance.
The patterns (what to sweep)
These are the ten categories. Each category has language-specific grep/AST patterns in references/patterns.md. Run them all; don't skip any for time.
1. Hardcoded secrets in tracked files
- API keys, tokens, passwords, private keys
- Regex for typical prefix patterns (
sk-,ghp_,xoxb-,-----BEGIN, etc.) - High-entropy strings in config/source files
git log --all -p -S<suspect>to see if ever committed historically
Also check whether secret files themselves are tracked:
git ls-files | grep -iE '\.env$|\.env\.|\.pem$|id_rsa|id_ed25519|credentials|\.ppk$'
Anything returned here is an immediate HIGH regardless of content.
2. SQL injection
SQL built by pasting strings. Language-agnostic pattern:
- Query text that contains an interpolation marker inside a function call named
execute,query,raw, orfetch - F-strings / template literals /
.format()/%inside a query call - String concatenation (
+,.,..) inside a query call
Prepared-statement placeholders (?, $1, :name) are the safe form. Flag anything else.
3. Shell / command injection
subprocess.run(..., shell=True)child_process.exec(userInput)(as opposed toexecFile/spawnwith args)- Backticks or
$()with user input in shell scripts os.systemanywhereeval/exec/Function(userInput)/setTimeout(userInput, ...)(in JS)pickle.loadson anything that might come from a networkyaml.loadwithoutSafeLoader
4. Path traversal
open(request.something),fs.readFile(req.query.x),File.new(params[:path])- Any file-reading call whose path argument traces back to user input without a whitelist regex
FileResponse/sendFilewith user-controlled path segments
5. XSS sinks
innerHTML =,dangerouslySetInnerHTML,v-html,{@html ...}(Svelte)document.write(- Jinja2
|safe, Djangomark_safe - Markdown renderers whose HTML output is injected into the DOM without an explicit sanitizer step:
markeddoes not sanitize HTML output. The historicalsanitize: trueoption was deprecated and removed. The renderer will faithfully reproduce any<script>it finds in the input. Pair it with DOMPurify on the rendered HTML before injecting it into the DOM.markdown-itdisables HTML input by default (html: false), which is safe as long as the option is not overridden. Ifhtml: trueis set anywhere, the output must be sanitized downstream (DOMPurify again).showdownsimilarly does not sanitize. Use a sanitizer on the output.- Any other Markdown library — check its docs. "Sanitize" in a Markdown renderer's API almost always means "disable raw HTML input," not "clean the HTML output." They are different guarantees, and non-developers conflate them.
- Server-side rendering of user-authored Markdown into HTML — same rules. Use
bleach(Python),sanitize-html(Node), or language-equivalent after rendering. Never trust Markdown input to be safe just because the renderer "supports" sanitization.
6. Dangerous deserialization
pickle.loads/cloudpickle.loadsyaml.load/yaml.Loader(useyaml.safe_load/yaml.SafeLoader)Marshal.load(Ruby) on untrusted inputObjectInputStream(Java) on untrusted streamsunserialize(PHP) on user input
7. Missing cookie safety flags
Every set_cookie / Set-Cookie call should include httpOnly, Secure, and SameSite. Flag any that don't.
8. CORS wildcard
Any response header Access-Control-Allow-Origin: * on an endpoint that isn't a public CDN. Especially dangerous if combined with Access-Control-Allow-Credentials: true (actually forbidden by the spec but some code tries).
9. Dependency / lockfile hygiene
- Unpinned dependencies in production (
requirements.txtwith no==,package.jsonwith^or*,Cargo.tomlwith no lockfile) - Known-bad packages (typosquats, abandoned packages) — name match against a small blocklist
- Lockfile missing for a language that should have one (
package-lock.json,Cargo.lock,Gemfile.lock)
10. Auth / session pitfalls
- Session cookies without rotation on login
- Hardcoded admin bypass paths (
if user_id == 1:style) - JWT signing with
nonealgorithm - Password comparison with
==instead of constant-time compare Math.random()used for session tokens / salts / OTPs (not cryptographically secure)
Triage — the critical step
For every hit, classify it as one of:
| Classification | Meaning | Action |
|---|---|---|
| REAL — CRITICAL | Exploitable remotely, data exposure, or secret leak | Fix now, then commit |
| REAL — HIGH | Exploitable but requires auth or specific context | Fix this sprint |
| REAL — MEDIUM | Defense-in-depth gap, not directly exploitable | Queue for cleanup |
| FALSE POSITIVE | The pattern matched but the code is actually safe | Explain why it's safe and move on — do not "fix" it |
| NEEDS REVIEW | You can't tell without more context (e.g., is this input trusted?) | Ask the operator one specific question |
Never just list findings without classification. A raw list of grep matches is worse than no audit, because the operator can't tell signal from noise.
When explaining a false positive, be specific: "This f-string inside a query is safe because days is an integer from int(request.query.get(...)) clamped to 0..365 on line 391." The specificity proves you actually looked.
Per-pattern triage rules: references/false-positive-triage.md
Output format
Always structured. Never a prose wall.
# Security sweep — <scope>
## Summary
- Scope: <files/dirs swept>
- Runtime context: <public / authenticated / internal>
- Total findings: <N>
- Triage: <X critical, Y high, Z medium, W false-positive>
## Critical (<N>)
### 1. <Category> — <file:line>
**What was found:** <quoted line>
**Why it's real:** <one-sentence reason>
**Fix:** <concrete code change or command>
### 2. ...
## High (<N>)
...
## Medium (<N>)
...
## False positives (<N>)
Listed only to show they were checked. No action needed.
### 1. <Category> — <file:line>
**Why not a vulnerability:** <specific reason>
## Needs review (<N>)
One specific question per item so the operator can answer and close it.
### 1. <Category> — <file:line>
**Question:** <precise question>
Deliberately out of scope
This skill is a minimal hand-curated sweep, not a full application security audit. If the operator needs any of the following, say so plainly and recommend a dedicated tool or human reviewer.
| Class of issue | Why this skill doesn't cover it | What to use instead |
|---|---|---|
| SSRF (Server-Side Request Forgery) | Requires dataflow tracing from user input to outbound HTTP clients. Language-agnostic grep is too coarse to avoid false positives. | Semgrep with SSRF rulepacks; language-specific SAST |
| CSRF (Cross-Site Request Forgery) | Framework-specific. Django, Rails, Next.js each have their own CSRF story. A generic checker gives false confidence. | Verify the framework's CSRF middleware is enabled and tokens are required on state-changing routes |
| IDOR (Insecure Direct Object Reference) | Requires understanding the application's authorization model. "Does this user have permission to read /orders/42?" cannot be answered by grep. |
Manual review of every route that takes an ID from the URL; pen test for critical flows |
| Unsafe file upload | Requires runtime behavior (magic bytes, content-type validation, storage isolation). A pattern match catches obvious cases but misses most. | Dedicated upload validation libraries; storage in a separate origin |
| Open redirect | Partially covered (hardcoded redirects), but dynamic redirect targets built from query parameters need dataflow analysis. | Whitelist allowed redirect domains; Semgrep open-redirect rulepack |
| Business logic flaws | "The coupon code can be used twice" is a logic bug that no scanner can find. | Pen test; exploratory testing; code review |
| Crypto primitive choice | "You're using AES-CBC without HMAC" is a crypto-design issue. Not a pattern match. | Cryptography review by someone qualified |
| Supply chain compromises in transitive deps | Requires a full SBOM + CVE database join. Out of scope for a single grep sweep. | pip-audit / npm audit / cargo audit / Dependabot |
| Compliance frameworks (SOC 2, HIPAA, PCI-DSS, GDPR) | Legal / procedural, not technical. | Compliance consultant |
When the operator asks for one of these, respond with: "That's outside what audit-security does well. Here's what it would take to actually cover it: [link or tool]. Do you want me to run the standard audit-security sweep in the meantime?"
Incident runbook — when something leaked
If the sweep finds a live credential leak, or the operator says "I accidentally pushed my .env", switch immediately to incident mode. Do not run the full triage. Do the following in order:
Step 1 — Rotate every exposed credential now
Before anything else. Cleanup comes after rotation — as long as the old credentials are still valid, the attacker's window is open.
- API tokens: revoke + regenerate in the provider's dashboard
- OAuth client secrets: regenerate
- Database passwords: change + update every deployment that uses them
- SSH keys: revoke the compromised public key from
~/.ssh/authorized_keyson every server, generate a new keypair - Signing keys (JWT secret, cookie secret): rotate + invalidate all existing sessions
- Cloud provider IAM keys: delete + generate new ones
Do not skip this step because "the repo is private." Private repos have been exfiltrated by compromised collaborator accounts, leaked CI logs, cloned forks, and accidental public-setting toggles. Treat every exposure as public.
Step 2 — Remove the secret from git history
The secret is still in every past commit until you rewrite history.
# Modern tool (recommended)
git filter-repo --path <path/to/leaked/file> --invert-paths
# or for a specific string:
git filter-repo --replace-text <file-with-patterns>.txt
# Legacy alternative (BFG Repo-Cleaner):
bfg --delete-files <leaked-file>
bfg --replace-text <file-with-secrets>.txt
Then force-push:
git push --force-with-lease origin --all
git push --force-with-lease origin --tags
Warn the operator before force-pushing. All collaborators need to re-clone; their existing clones will diverge.
Step 3 — Notify and re-authenticate
- Every collaborator with access to the repo needs to:
- Pull the rewritten history (effectively re-clone)
- Delete their local copy of the old credentials
- Pull new credentials if they had the old ones in a local
.env
- If the repo is part of a CI/CD pipeline, rotate any cached copies of the secret on build runners
Step 4 — Audit for reuse
Ask the operator: "Was this secret also used in any other project, service, or environment?" Credentials are often reused — the leak of one may mean several places are compromised.
Step 5 — Document the incident
Write a short post-mortem in docs/security/incidents/<date>-<what-happened>.md:
- What leaked
- When (commit SHA, push timestamp)
- How (
.envcommitted directly? secret in.github/workflows/*.yml? hardcoded in source?) - How it was detected
- Rotation status for each affected credential
- What changed to prevent recurrence (pre-commit hook,
.gitignoreupdate,.env.exampleaudit)
Hand the file off to write-for-ai to format. This file is for future AI sessions so the same mistake doesn't recur.
Step 6 — Prevent recurrence
Add these guards before closing the incident:
.gitignoreentries for every secret file type (hand off tomanage-secrets-env)- Pre-commit hook that runs
audit-security(or at minimum a secrets-scanning step likegitleaks) on every commit - Branch protection on
mainso no one can force-push again - Secret-scanning enabled on the hosting provider (GitHub Secret Scanning, GitLab push rules, etc.)
Things not to do
- Don't run a full SAST scanner and dump its output. This skill is the opposite of that.
- Don't copy-paste OWASP descriptions. If you quote OWASP, do it in one sentence, in the operator's words.
- Don't be decorative. No banners, no ASCII art, no emojis beyond what the operator uses. The report is a tool, not a trophy.
- Don't pretend to be a compliance audit. This skill finds technical issues. SOC 2 / HIPAA / PCI-DSS require a different kind of review.
- Don't fix things on your own beyond CRITICAL. Report, let the operator decide. Especially for "fixes" that touch business logic — what looks like a simple param sanitization might break a workflow.
- Don't expand the audit scope beyond what was asked. If the sweep scope was "auth code only" and you notice a SQL injection in the payments module, report it as a hand-off finding — do not quietly expand the sweep to cover it. Scope expansion corrupts the triage ratio and trains the operator to distrust bounded requests.
Common AI failure modes around security
Things to watch for in your own output:
- Over-scoping — the sweep asked about one file and you scanned the whole repo. Don't. Respect the scope the operator set.
- False-positive fatigue — reporting 100 findings without triage, knowing the operator will ignore most. Always triage. If you can't triage, say so and ask one specific question.
- Lecturing — rehashing OWASP Top 10 theory when the operator asked "is my login page secure." Answer the specific question.
- Fabricated severity — inventing "CVSS 9.8" scores you didn't actually compute. Use plain labels (critical/high/medium) and explain the reasoning.
- Missing the obvious — running grep for
eval(and missing a.envfile sitting ingit ls-files. Always check tracked secrets first; it's the highest signal-per-second category.
Sweep mode — read-only audit
This skill is already diagnosis-only by default — it never edits regardless of how it's invoked. When the umbrella runs it with sweep=read-only, the same sweep still runs, but the output shape is trimmed: only CRITICAL and HIGH findings are surfaced, the per-finding triage dialog is deferred to the umbrella's synthesis step, and the report leads with an aggregate stoplight + finding counts instead of a narrative section-by-section walkthrough.
Sweep-mode report includes:
- Stats line (scope, runtime context, total findings) and an aggregate stoplight ( / )
- CRITICAL findings with file:line + one-sentence blast-radius
- HIGH findings with file:line + one-sentence blast-radius
- Hand-off pointers (e.g., tracked
.env→manage-secrets-env; CI secret issues →setup-ci)
Sweep-mode report omits (vs the full direct-call report): MEDIUM findings, the false-positive list, the needs-review questions, the full incident runbook prose, and the proposed code-change snippets per finding.
No behavior change from default pure-diagnosis — the sweep still classifies every hit, still respects scope, still never edits. Only the output shape changes so the umbrella can synthesize across workers without reading a thousand-line report.
Marker-less invocation (direct call /audit-security) keeps the full report — MEDIUM, false-positive triage, needs-review questions, and per-finding fix snippets all included.
Harsh mode — no hedging
When the task context contains the tone=harsh marker (usually set by the /vibesubin harsh umbrella invocation, but can also come from direct requests like "don't sugarcoat" / "brutal review" / "매운 맛"), switch output rules:
- Lead with the worst finding, not the summary. First line of the report is the single most dangerous issue, in one sentence, with file and line.
- No softening words. Drop "potential", "could be", "might allow", "consider", "you may want to". Replace with blast-radius framing: "a stranger can read every user's record via
src/api/users.py:47", not "potential information disclosure in the users endpoint". - Severity labels stay literal. CRITICAL stays CRITICAL. HIGH stays HIGH. Do not inflate — harsh mode is about framing, not severity inflation.
- Triage still applies. Every finding is still real / false-positive / needs-human-review. Harsh mode removes hedge words, not the triage discipline — a false positive is still a false positive, but labeled "false positive, ignore" rather than "probably not exploitable in this codebase, but worth reviewing".
- No "looks fine" closures. If any finding is CRITICAL or HIGH, the verdict line does not end with reassurance. "Don't ship until items 1–3 are fixed and secrets are rotated", not "mostly clean, two things to look at".
- Incident findings get urgency language. If a secret is in git history, the first line of the report is "Stop what you're doing. Rotate the credential now. Here's the incident runbook." — no preamble.
- Plain-language impact still required. "CWE-89" is never the headline; "a user can run arbitrary SQL against your database" is. Harsh mode uses the same plain language, just without the softening connectives around it.
Harsh mode does not invent findings, fabricate CVSS scores, or become rude. Every harsh statement must be backed by the same evidence the balanced version would cite. The change is framing, not substance.
Layperson mode — plain-language translation
When the task context contains explain=layperson (from /vibesubin explain, /vibesubin easy, "쉽게 설명해줘", "일반인도 이해되게", "explain like I'm non-technical", "非開発者でも分かるように", "用通俗的话解释"), add a plain-language layer to every finding this skill emits. Combines freely with tone=harsh. Full rules at /plugins/vibesubin/skills/vibesubin/references/layperson-translation.md.
Three dimensions per finding
Every finding gets three questions answered in plain language, in the operator's language (Korean / English / Japanese / Chinese):
- 왜 이것을 해야 하나요? / Why should you do this? — "보안 구멍은 조용해요. 지금 돌아는 가는데, 악의적인 사용자 한 명이 SQL 쿼리 한 줄로 전체 사용자 데이터를 가져갈 수 있어요."
- 왜 중요한 작업인가요? / Why is it an important task? — "보안 취약점은 배포 직후부터 공격 대상입니다. 몇 시간 안에 첫 시도가 들어오는 게 흔해요."
- 그래서 무엇을 하나요? / So what gets done? — "비밀번호·API 키가 깃 히스토리에 남았는지, 사용자 입력이 검증 없이 DB에 들어가는지, 쿠키 보안 속성이 빠졌는지 등을 짧게 10가지로 체크하고 실제 위험만 추립니다."
Severity translation
- CRITICAL → "지금 당장 — 이 상태로 배포하면 데이터 유출"
- HIGH → "이번 주 안에 — 배포 전 막아야 함"
- MEDIUM → "다음 릴리즈 전까지"
- False positive → "안심해도 됨 — 체크해 봤는데 문제 아니었음"
Box format
Wrap each finding in the box format from the shared reference. Header uses urgency phrase ("지금 당장" / "이번 주 안에" / "다음 릴리즈 전까지" / "시간 날 때") and the finding number. Footer names the hand-off skill (e.g., "어떤 스킬이 고치나요? — refactor-verify").
What does NOT change
Findings, counts, file:line references, evidence, confidence tags, and severity are identical to balanced/harsh output. Only the wrapping and dimension annotations are added. Layperson mode is presentation-only.
Hand-offs
- Critical findings involving refactoring sensitive code → hand off to
refactor-verifyfor the fix - Tracked
.envfiles → hand off tomanage-secrets-envfor the remediation pattern (rotate, remove from history, add to gitignore, re-examine collaborators) - Issues in CI/CD pipeline secrets → hand off to
setup-ci - Repo-rot-adjacent findings (stale dependencies, unused libraries with CVEs) → hand off to
fight-repo-rot
Details
references/patterns.md— concrete grep / AST patterns per categoryreferences/false-positive-triage.md— how to classify borderline hits
Optional helper tools (the pack does not require them, but uses them when available): Semgrep, Bandit (Python), ESLint-security (JS), gosec (Go), cargo-audit (Rust), pip-audit (Python), npm audit (Node), gitleaks / trufflehog for secret scanning.
More from subinium/vibesubin
manage-secrets-env
Opinionated defaults and full lifecycle playbook for secrets and environment variables. Decides where a secret or env-specific value lives (constant, .env, CI secret, env var), scaffolds .env.example and .gitignore, and manages the lifecycle end to end — add, update, rotate, remove, migrate between buckets, audit cross-environment drift, provision new environments. High-stakes companion to project-conventions. Language-agnostic.
4setup-ci
Teaches CI/CD from first principles to a non-developer, then scaffolds a working test + deploy pipeline. Handles the common hosts (GitHub Actions, GitLab CI, CircleCI, Travis, Jenkins) and common deploy targets (SSH to VM, Vercel, Netlify, Fly.io, Cloud Run, Docker registries). Asks what the operator has before generating anything — never assumes.
3refactor-verify
Proves a behavior-preserving code change (refactor, rename, split, merge, extract, inline, or delete of confirmed-dead code) is actually complete. Plans the change as a dependency tree, executes it from the leaves up, and after each step proves 1:1 semantic equivalence through four independent checks — exported symbol-set diff, per-node AST diff, full behavioral test suite, and call-site closure via find-references. Runs before claiming any such change is done. Works for any language with a test runner and a way to grep for symbols.
3ship-cycle
Issue-driven development orchestrator. Turns improvement intent into a well-specified, bilingual issue set; clusters issues into milestones that map 1:1 to semver versions; enforces branch, commit, and PR conventions (GitHub Flow — `<type>/<issue-N>-<slug>`, Conventional Commits, mandatory PR template, rebase-first merge); generates changelog entries and release notes deterministically from closed issues; leaves a durable audit trail for the next AI session. Direct-call only — not part of the /vibesubin parallel sweep. Two tracks — **GitHub track** (default) on GitHub with authenticated `gh` CLI; **PRD track** on any other host, using local markdown files under `docs/release-cycle/vX.Y.Z/` as the durable audit trail. Operator picks at Step 1.5.
2