# How to Scan AI-Generated Code for Vulnerabilities

> AI Release · @ai_release1 · https://ai-release.org/guides/kak-proverit-kod-ot-cursor-i-claude-code-na-uyazvimosti_en.html

## TL;DR

- **Time to complete:** ~45 minutes
- **Difficulty:** Intermediate
- **You will need:** a Git repository with code produced by Cursor or Claude Code; Python 3 with `pipx`; Node.js with `npm`; Gitleaks; Semgrep; Trivy (optional, for Docker/IaC files); a CI runner such as GitHub Actions. Tool versions change often — run `--version` on each scanner and check its documentation for the flag set that matches your installed release.

## Requirements and setup
![Illustration: Requirements and setup](https://ai-release.org/guides/img/kak-proverit-kod-ot-cursor-i-claude-code-na-uyazvimosti-en-1.jpg?v=1792778451)

**What to do:** install three independent scanner layers and confirm each one runs on your machine before you point it at the AI-written code.

**Why:** AI assistants generate a lot of code fast, and the typical failure modes are different from human-written code: a live API key pasted into a config file, a dependency name the model invented, and a plausible-looking function with an injection bug. No single tool covers all three, so you stack them.

| Layer | Question it answers | Tool | What it reads |
|---|---|---|---|
| Secrets | Did the assistant paste a real key or token into a file? | Gitleaks | the whole repo, including Git history |
| SCA (software composition analysis — checks third-party libraries for known CVEs) | Does a library version I depend on have a published vulnerability? | `pip-audit` (Python), `npm audit` (Node.js) | lockfiles and manifests |
| SAST (static application security testing — reads source code without running it) | Does the code contain injection, weak randomness, unsafe deserialization, etc.? | Semgrep | source files |
| Container / IaC | Does a Dockerfile or YAML manifest run as root or expose a port? | Trivy | `Dockerfile`, `*.yaml` |

- [ ] `git` installed and the project is a Git repository (`git rev-parse --is-inside-work-tree` prints `true`)
- [ ] Python 3 available: `python3 --version`
- [ ] Node.js and npm available: `node --version` and `npm --version`
- [ ] At least 1 GB of free disk space for scanner caches and reports
- [ ] Write permission to the repository root (you will create `reports/`)

Install the scanners:

```bash
pipx install pip-audit
brew install gitleaks semgrep trivy
```

On Windows use `winget` or `scoop` instead of `brew` — check each tool's documentation for the current package name.

**Example:** after installation, `gitleaks version` prints a single version string and `semgrep --version` prints a version plus a Python path. If a command prints `command not found`, the binary is not on your `PATH` — reopen the terminal or add `~/.local/bin` to `PATH`.

Before the first run, exclude directories that produce noise and false positives:

```bash
cat > .semgrepignore <<'EOF'
node_modules/
.venv/
dist/
build/
.cursor/
EOF
```

**Why this works:** Semgrep and Gitleaks both walk the filesystem; excluding vendored and tool-cache directories stops them from reporting the same issue hundreds of times.

## Step 1. Freeze the AI-generated diff and verify every dependency name
![Illustration: Step 1. Freeze the AI-generated diff and verify every dependency name](https://ai-release.org/guides/img/kak-proverit-kod-ot-cursor-i-claude-code-na-uyazvimosti-en-2.jpg?v=1792778456)

**What to do:** commit everything the assistant wrote onto a dedicated branch, then list every dependency the project declares and check that each package actually exists in its public registry.

**Why:** scanners only see committed files — a half-saved file in an editor is invisible to them. And a model occasionally references a package that does not exist; an attacker can register that name later and your next `npm install` pulls their code. That class of risk is invisible to CVE scanners because there is no CVE yet.

```bash
git switch -c sec/ai-code-review
git add -A
git commit -m "chore: freeze AI-generated code before security review"
git status --short
git diff --stat main...HEAD
```

**Example output (your numbers will differ):**

```
42 files changed, 3120 insertions(+), 118 deletions(-)
```

`git status --short` must print **nothing** — a clean tree means the scan covers exactly what is committed. If your agents create many small commits and you need a cleaner history for review, see [Jujutsu or Git: What to Choose for AI Agent Workflows](https://ai-release.org/guides/jujutsu-ili-git-chto-vybrat-dlya-raboty-s-ii-agentami-i.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).

Now inventory the dependencies:

```bash
pip freeze > requirements.lock.txt
npm ls --depth=0
```

Check each direct dependency name against its registry:

```bash
npm view <package-name> version
pip index versions <package-name>
```

**Example:** if the assistant wrote `import { parse } from "safe-json-parser-pro"`, run `npm view safe-json-parser-pro version`. A real package prints a version number. A hallucinated one prints:

```
npm error code E404
npm error 404 Not Found - GET https://registry.npmjs.org/safe-json-parser-pro - Not found
```

**How to check:** the commit exists (`git log --oneline -1`) and every package name in `requirements.lock.txt` and in `package.json` returns a version from the registry, not a 404.

## Step 2. Run the three scanner layers into report files
![Illustration: Step 2. Run the three scanner layers into report files](https://ai-release.org/guides/img/kak-proverit-kod-ot-cursor-i-claude-code-na-uyazvimosti-en-3.jpg?v=1792778463)

**What to do:** run secrets, dependency, and static analysis in that order, and write each result to a machine-readable file inside `reports/`.

**Why:** order matters. A leaked key must be rotated within minutes, while a style-level SAST finding can wait. Writing reports to files gives you a diffable artifact you can attach to a pull request and compare between runs.

```bash
mkdir -p reports
gitleaks detect --source . --report-format json --report-path reports/gitleaks.json -v
pip-audit -r requirements.txt -f json -o reports/pip-audit.json
npm audit --audit-level=high
semgrep --config auto --sarif --output reports/semgrep.sarif .
trivy fs .
```

**Why this works:** Gitleaks matches entropy and known key patterns in text, `pip-audit` and `npm audit` compare your resolved versions against public CVE feeds, and Semgrep parses the code into a syntax tree so a finding like "user input reaches a SQL string" survives reformatting and renaming. SARIF is a JSON report format that code-scanning dashboards can read directly.

**Example:** a Semgrep finding looks like this on screen — `python.lang.security.audit.formatted-sql-query`, the file path, the line number, and a code snippet. A Gitleaks hit prints `RuleID`, `File`, `Line`, and a redacted `Secret`.

Wire the same commands into CI so no one has to remember them:

```yaml
name: ai-code-security
on: [pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pipx install pip-audit
      - run: pip-audit -r requirements.txt
      - run: npm audit --audit-level=high
      - run: gitleaks detect --source . --redact -v
      - run: semgrep --config auto --error .
```

If your team already runs Claude Code with subagents, put these commands in a subagent checklist so the reviewer agent always executes the same three layers — see [Claude Code for Teams: Setting Up Subagents and a Shared AI Superagent](https://ai-release.org/guides/claude-code-dlya-komandy-kak-nastroit-subagentov-i-obsc.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).

**How to check:** `ls -la reports/` lists `gitleaks.json` and `semgrep.sarif`, and every command above finished without a stack trace.

## Step 3. Triage the reports and gate the merge

**What to do:** read the reports, fix the blocking findings, then re-run the scanners in "fail on finding" mode so the check can be used as a merge gate.

```bash
jq 'length' reports/gitleaks.json
jq -r '.[] | "\(.File):\(.StartLine) \(.RuleID)"' reports/gitleaks.json
```

**Example:** `jq 'length'` printing `0` means no secrets were found anywhere in history. Any other number is a list you must work through line by line.

Fix each item in this order: rotate the leaked credential first (deleting the file does not invalidate the key), then upgrade or replace the vulnerable dependency, then patch the SAST findings. After fixing, re-run with hard failure enabled:

```bash
gitleaks detect --source . --exit-code 1 --redact
semgrep --config auto --error .
echo $?
```

**How to check:** `echo $?` prints `0`, the CI job named `ai-code-security` is green on the pull request, and `git log -1 --stat` shows the fix commit. If the number is not `0`, a finding is still open — go back to the report.

## Troubleshooting

**Problem. The install step fails on a package the assistant recommended**

- Error: `ERROR: No matching distribution found for pdf-tools-pro`
- Cause: the model invented the package name, or it exists only in a private index your machine cannot reach. This is the supply-chain risk described in Step 1, not a typo you should "fix" by guessing a similar name.
- Solution: search the public registry manually, then replace the import with a maintained library the team already uses, or add the private index URL explicitly: `pip install --index-url https://<your-internal-index>/simple pdf-tools-pro`. Re-run `pip index versions <package-name>` until it resolves.

**Problem. Gitleaks blocks the build on a test fixture**

- Error: `RuleID: generic-api-key` reported against `tests/fixtures/config.yaml`
- Cause: sample credentials committed as fixtures are indistinguishable from real ones by pattern.
- Solution: add an allowlist rather than disabling the rule, then re-run with the config:

```bash
cat > .gitleaks.toml <<'EOF'
[allowlist]
  paths = ['''tests/fixtures/.*''']
EOF
gitleaks detect --source . --config .gitleaks.toml --exit-code 1 --redact
```

**Problem. The CI step fails even though the local run passed**

- Error: `Error: Process completed with exit code 1.`
- Cause: `npm audit --audit-level=high` exits with code 1 whenever a high-severity advisory exists, and CI resolves fresh transitive versions that your local `node_modules` does not have.
- Solution: reproduce with the same resolution locally — `rm -rf node_modules package-lock.json && npm install && npm audit --audit-level=high` — then commit the updated lockfile together with the upgrade. Never use `npm audit fix --force` on AI-generated code without reviewing the resulting diff.

## Summary and next steps

You now have a repeatable gate: freeze the AI diff, verify dependency names, run secrets/SCA/SAST into `reports/`, and merge only when `echo $?` returns `0`. Next, add the same job to every repository where assistants write code, and if your code cannot leave your network, route the review through a self-hosted model as described in [How to connect a local LLM to Claude Code](https://ai-release.org/guides/kak-podklyuchit-lokalnuyu-model-k-claude-code-llama-ser.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).

## FAQ

**Can Cursor or Claude Code introduce security vulnerabilities?**
Yes — the assistant writes code faster than a human reviews it, and the recurring issues are leaked credentials, invented package names, and plausible-but-unsafe logic such as string-concatenated SQL. The scanners in this guide target exactly those three categories.

**Does Cursor have a built-in security scanner?**
This guide does not depend on any editor feature; it uses external scanners so the result is the same regardless of which assistant wrote the code. Check the vendor's documentation for the current built-in feature set.

**Is it safe to send proprietary code to a cloud AI assistant?**
That depends on your contract and data policy, which this guide cannot verify for you. If policy forbids sending code to a third party, keep the scanners local and use a self-hosted model for the review step.

**How often should I run these scanners?**
On every pull request via CI, plus one full-history Gitleaks run whenever a new contributor or repository is onboarded, because a secret committed months ago stays in history until it is rotated.
