Human review does not scale to AI-generated volume, and it misses things people are bad at spotting. Automate the checks that can be automated, in layers, so each one catches what the others do not.
Watch
A security vendor's explanation of static application security testing (SAST) and how it finds problems in code without running it.
The layered pipeline
| Layer | Catches | Examples |
|---|---|---|
| Formatters and linters | Style, obvious bugs, unused code, risky constructs | Ruff and ESLint |
| Type checkers | Wrong types, missing attributes, invented members | mypy, TypeScript |
| Static application security testing (SAST) | Injection, unsafe functions, insecure patterns | CodeQL, Semgrep, Bandit |
| Secret scanning | Keys, tokens and passwords in code and history | Gitleaks |
| Dependency and license scanning | Known vulnerabilities and license issues | OSV-Scanner, Dependabot |
| Tests | Behavior, regressions | Unit, integration, property tests (lesson 6) |
| Human review | Design, intent, authorization logic, anything subtle | You |
Many hallucinated function calls and wrong parameter types are caught for free by a type checker, so turn one on, even in an existing untyped codebase, starting with the files tools touch most.
SAST: how it works and how to use it
Static analysis reads code without running it, following how data flows from sources (user input) to sinks (database queries, shell commands) to find unsafe paths.
- CodeQL treats code as a database you query, with a large library of security queries. It integrates with GitHub code scanning.
- Semgrep matches code patterns with rules that are easy to write, so you can encode your own team's rules.
- Bandit is a Python-specific security linter.
A custom Semgrep rule that bans shell=True:
rules:
- id: no-shell-true
languages: [python]
severity: ERROR
message: Do not pass shell=True to subprocess. Pass an argument list instead.
pattern: subprocess.$FUNC(..., shell=True, ...)
Turn every recurring review comment into a rule like this. That way each lesson learned from AI-generated mistakes protects the whole codebase permanently.
Secret scanning
Run a scanner such as Gitleaks in pre-commit and in CI. If a secret is committed, assume it is compromised: revoke and rotate it, then clean up. Deleting the line is not enough because git history keeps it.
Handling noise
Scanners produce false positives, and developers learn to ignore noisy tools.
- Start with a small, high-confidence rule set, and expand gradually.
- Triage and suppress with a written reason and an expiry date, never silently.
- Block merges only on high-severity, high-confidence findings. Report the rest.
- Measure time to fix and recurring finding types, and feed these back into training and rules.
Using an LLM as a reviewer
Language models can review a diff and catch issues static tools miss, or explain a finding and propose a fix. Research on AI-generated code has also shown that a chat assistant, given static analysis warnings, could fix a good share of them. Use models this way with care:
- Treat their output as suggestions to verify, not verdicts. They produce false positives and misses.
- Give them context: the threat model, the project rules and the surrounding code.
- Do not make a model's approval your only gate. A model that wrote the code and the model that reviews it may share the same blind spots.
- Re-run the real tools and tests after any model-suggested fix.
Governance for AI-assisted development
- A written policy on which tools are approved, what data may be sent to them and what must be reviewed.
- Pull request templates that ask: was AI used, which parts, what was verified.
- Training for developers on the failure patterns in these lessons.
- Standards alignment. NIST's Secure Software Development Framework (SSDF) describes secure development practices, and NIST publishes a community profile of it for generative AI. CISA's Secure by Design materials describe how vendors can build security into products. Use them to structure your program and show auditors what you do.
Put these checks in a pre-commit hook and a required CI job, with fast feedback. Developers and agents then find out about a problem in seconds, not at release time.
Resources
How CodeQL-based scanning runs in pull requests.
Query language and library reference.
Writing custom rules and running scans in CI.
A secret scanner for repositories and history.
A fast Python linter and formatter.
The standard JavaScript and TypeScript linter.
A static type checker for Python.
A framework of secure development practices.
The profile covering secure development practices for generative AI and foundation models.
Principles for building security into software products.
Check yourself
- Add a linter, a type checker, one SAST tool and a secret scanner to a project, running in CI.
- Introduce three deliberate flaws (SQL injection,
shell=True, a fake API key). Confirm each layer catches what you expect. - Write two custom Semgrep rules from mistakes you found in lessons 2 and 3.