TL;DR
"Vibe coding" — describing in natural language and letting Cursor, Aider, Cline or Claude Code write the code — is now the norm for many teams. The problem: generated code passes through less-careful human review, accumulates vulnerabilities (OWASP top ten), unchecked dependencies, and patterns a human would never write by default. This article assembles a QA pipeline that runs on every commit: Semgrep with rules specific to AI-generated code, Bearer for PII and secret detection, Trivy for container CVEs, SonarQube Community with custom rules, DSPy to auto-generate tests from the diff, Promptfoo to validate prompts before merge and Gitleaks for credentials. Pre-commit, CI and runtime. The team keeps vibing; the system stops the risk.
The concrete problem
In a typical project with intensive vibe coding we observe 1,000 LOC/week generated. Of those, 8-12% have at least one of:
- SQL injectable via string concatenation.
eval()/exec()over unvalidated input.- Secrets hardcoded "temporarily" that ship to prod.
- New libraries without vetting (supply-chain risk).
- Prompts with inadvertent injection (embedded instructions the model honors).
- Silent error handling (
except: pass). - Endpoints without auth because "we'll fix it later."
The human reviewer misses them because the PR is 400 lines and the "made with Cursor" stamp creates false implicit trust.
Pipeline architecture
Developer → Cursor / Aider / Cline / Claude Code
│
▼
┌──────────────────────────────────────────────┐
│ pre-commit │
│ ├─ gitleaks (secrets) │
│ ├─ semgrep (quick rules) │
│ ├─ bearer (PII/sensitive data) │
│ └─ black/ruff/eslint │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ CI (GitHub Actions) │
│ ├─ semgrep full (OWASP + AI-specific) │
│ ├─ sonarqube community │
│ ├─ trivy fs/image │
│ ├─ bearer full scan │
│ ├─ dspy: generate + run tests │
│ ├─ promptfoo: prompt validation │
│ └─ license check (supply chain) │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ runtime │
│ ├─ dependency-track (SBOM monitoring) │
│ ├─ falco / tetragon (syscall anomalies) │
│ └─ traces to Langfuse (if there are LLMs) │
└──────────────────────────────────────────────┘
Pre-commit: fast and local
.pre-commit-config.yaml:
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
- repo: https://github.com/returntocorp/semgrep
rev: v1.95.0
hooks:
- id: semgrep
args:
- --config=p/owasp-top-ten
- --config=p/secrets
- --config=./.semgrep/ai-gen-rules.yml
- --error
- --skip-unknown-extensions
- repo: https://github.com/Bearer/bearer
rev: v1.49.0
hooks:
- id: bearer
args: ["scan", ".", "--severity=critical,high,medium", "--fail-on-severity=critical,high"]
- repo: local
hooks:
- id: forbidden-patterns
name: "AI gen anti-patterns"
entry: scripts/forbidden_patterns.sh
language: script
scripts/forbidden_patterns.sh:
#!/usr/bin/env bash
set -e
# common patterns Cursor/GPT generate that should not reach prod
grep -nR --include=*.py --include=*.ts --include=*.go \
-E '(except:\s*pass|# FIXME|# TODO:|console\.log\("DEBUG|print\("DEBUG)' \
-- "$@" && {
echo "❌ anti-patterns found"
exit 1
} || true
Semgrep: rules specific to AI
File .semgrep/ai-gen-rules.yml:
rules:
- id: hardcoded-api-key
pattern-either:
- pattern: api_key = "sk-..."
- pattern: $KEY = "pk_live_..."
- pattern: ANTHROPIC_API_KEY = "sk-ant-..."
message: "Secret hardcoded. Use env vars."
severity: ERROR
languages: [python, javascript, typescript, go]
- id: llm-without-timeout
pattern-either:
- pattern: openai.ChatCompletion.create(...)
- pattern: anthropic.messages.create(...)
pattern-not-inside: |
$CTX.with_timeout(...)
message: "LLM call without timeout; can hang the process."
severity: WARNING
languages: [python]
- id: prompt-concat-user-input
patterns:
- pattern-either:
- pattern: |
$PROMPT = f"...{$USER_INPUT}..."
...
$LLM.create(messages=[{"role": "system", "content": $PROMPT}, ...])
- pattern: |
$PROMPT = "..." + $USER_INPUT + "..."
...
$LLM.create(...)
message: "Possible prompt injection: user input concatenated to system prompt."
severity: ERROR
languages: [python, typescript]
- id: sql-string-concat
pattern-either:
- pattern: 'cursor.execute(f"SELECT ... {$VAR} ...")'
- pattern: 'conn.Exec("SELECT ... " + $VAR + " ...")'
message: "SQL via concatenation. Use parameters."
severity: ERROR
languages: [python, go]
- id: agent-tool-no-idempotency
patterns:
- pattern: |
@tool
def $FN(...):
...
- pattern-not-inside: |
def $FN(..., operation_id: str, ...):
...
message: "Agent-side tool without operation_id; missing idempotency."
severity: WARNING
languages: [python]
These rules catch what classical SAST doesn't: AI-code-specific patterns (prompt injection, tools without idempotency, LLM without timeout).
SonarQube Community Edition
Generic SonarQube is complemented with specific rules. File .sonar/rules.xml applied by sonar-scanner:
- Cyclomatic complexity >10 per function.
- Functions with >60 lines (will be misunderstood by humans).
- Lack of tests for functions with side effects.
- Code duplication >3%.
For generated code, lower thresholds temporarily (e.g. complexity <8) and ratchet up — the model gets used to writing cleaner code when feedback is clear.
DSPy: tests generated from the diff
The idea: take the PR's diff, feed it to a DSPy model that produces test cases based on evident intent, and run them.
import dspy
class GenerateTestFromDiff(dspy.Signature):
"""Generate pytest tests for modified functions. Use descriptive names, cover happy path and at least 2 edge cases."""
diff: str = dspy.InputField()
file_path: str = dspy.InputField()
tests: str = dspy.OutputField()
lm = dspy.LM("anthropic/claude-sonnet-4-6", api_base="https://api.numoru.com/v1", api_key=os.environ["LITELLM_MASTER_KEY"])
dspy.configure(lm=lm)
generator = dspy.ChainOfThought(GenerateTestFromDiff)
def generate_and_run(diff: str, file_path: str):
out = generator(diff=diff, file_path=file_path)
tests_file = write_to_tmp(out.tests)
result = subprocess.run(["pytest", tests_file, "-v"], capture_output=True)
return result
If the auto-generated tests fail, the PR doesn't pass. This catches regressions the human forgot to cover.
Promptfoo: validate prompts before merge
When the PR modifies a prompts/*.txt or *.md file, Promptfoo runs a suite against the new prompt and compares against the previous one. Details in agent evals in CI/CD.
Trivy: CVEs in containers and dependencies
# .github/workflows/security.yml
- name: Trivy fs scan
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: "1"
- name: Trivy image scan
if: github.event_name == 'pull_request'
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/numoru/app:${{ github.sha }}
severity: CRITICAL,HIGH
For AI-generated code, package.json and requirements.txt tend to grow with model-suggested dependencies — CVEs appear quickly.
Supply chain: blocking new dependencies
Problem: Cursor suggests import superlib without warning. The PR adds a new package nobody vetted.
Solution: pre-commit hook that detects new lines in package.json/requirements.txt and requires justification in the commit message.
#!/usr/bin/env bash
set -e
DIFF=$(git diff --cached --unified=0 -- 'package.json' 'requirements.txt' 'go.mod')
if echo "$DIFF" | grep -q '^+[^+-]'; then
if ! git log --format=%B -n 1 HEAD | grep -q 'new-dep:'; then
echo "❌ new dependencies detected. Add 'new-dep: <reason>' to the commit message"
exit 1
fi
fi
Full GitHub Actions
name: secure-ai-gen
on: [pull_request]
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: >
p/owasp-top-ten
p/secrets
p/ci
./.semgrep/ai-gen-rules.yml
bearer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bearer/bearer-action@v2
with:
scan-command: "scan . --report=security --severity=critical,high"
sonarqube:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: SonarSource/sonarqube-scan-action@v3
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: https://sonar.numoru.com
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@master
with: { scan-type: fs, severity: "CRITICAL,HIGH", exit-code: "1" }
dspy-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install dspy-ai pytest
- name: Generate + run tests from diff
env:
LITELLM_MASTER_KEY: ${{ secrets.LITELLM_MASTER_KEY }}
run: python scripts/gen_tests_from_diff.py
promptfoo:
if: contains(github.event.pull_request.changed_files, 'prompts/')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm i -g promptfoo
- run: promptfoo eval -c promptfoo.yml
Additional rules for Aider/Cline/Cursor
File .cursorrules (also works with Cline and Windsurf):
# Numoru rules — AI-generated code must:
1. Never hardcode API keys; use env vars via pydantic settings / viper / dotenv.
2. Every LLM call must have a timeout (<30s) and retry with backoff.
3. Prompts built with user input must use clear delimiters (<input>...).
4. Functions with external side effects must accept operation_id: str for idempotency.
5. SQL must use parameters; never f-string or concat.
6. Tests must exist for every public function. Minimum 70% coverage.
7. In Python, never except: pass. In Go, never ignore err.
8. Logs must not include plaintext PII; hash email and phone.
This biases the model during generation, not just after.
Runtime: post-deploy detection
Two tools worth their weight:
- Falco / Tetragon — eBPF-based runtime security. Alerts when a container execs to /bin/sh, opens unexpected ports or writes to sensitive paths.
- Dependency-Track — periodically ingests SBOMs and warns when a dependency in use today gets a recent CVE.
Minimum Falco policy for AI applications:
- macro: llm_service
condition: (proc.name in (uvicorn, gunicorn, node, go-binary))
- rule: LLM service spawning shell
desc: "Should never spawn a shell"
condition: spawned_process and proc.pname in (uvicorn, gunicorn) and proc.name in (bash, sh, dash, zsh)
output: "LLM service spawned shell (command=%proc.cmdline)"
priority: CRITICAL
tags: [ai, runtime]
Metrics to track
- Critical findings per 1,000 LOC (target: <0.5 in 90 days).
- Mean time between commit and merge (the pipeline adds ~5-8 min; ok).
- PR rejection rate (if it crosses 30%, the pipeline is mis-tuned or the team needs training).
- CVE-exposure window (days from CVE publication to detection in prod).
Same team and AI tooling; only difference is whether the QA pipeline runs pre-commit + CI. Observed over 90 days across 4 Numoru client teams.
- Before pipeline
- After pipeline (90 days)
Numoru engagement telemetry, 2024-2026.
Business & commercial impact
Why buy this as a service
Teams know they need it but rarely build it on their own because each piece (Semgrep rules, SonarQube tuning, DSPy test-gen, Falco runtime) is a rabbit hole. Numoru delivers it as a 2-3 week install plus an optional hardening audit. The sale is essentially insurance priced in engineering hours against the asymmetric cost of a shipped vulnerability.
Industries and ticket ranges
Pipeline install + audit ticket by buyer (Numoru, USD)
Public benchmarks
GitHub — Copilot & AI productivity research
Snyk — State of open source security 2024
Illustrative case — 40-engineer SaaS adopting pipeline
Growth-stage LATAM fintech with 40 engineers heavy users of Cursor + Claude Code
ROI calculator — securing an AI-heavy engineering org
40-engineer fintech adopting pipeline (12 months)
| Install (one-time) | −$18,500 |
| Retainer (12 mo × $900) | −$10,800 |
| Infra (Sonar + Semgrep Pro trial) | −$720 |
| Additional CI time (144 h × $92) | −$13,248 |
| Probability-weighted incident avoided | +$153,000 |
| Compliance readiness value | +$35,000 |
| Net year-1 contribution | +$144,732 |
Pricing tiers Numoru sells
- Semgrep + Gitleaks + Trivy
- Basic SonarQube Community setup
- Pre-commit hook + GitHub Actions
- Short team walkthrough
- 30-day warranty
- Everything in Starter
- DSPy test-gen + Promptfoo
- Bearer (PII) + supply-chain
- Falco runtime rules
- Rule tuning retainer
- Quarterly audit
- Multi-repo / monorepo scope
- SOC 2 / ISO 27001 alignment
- Custom Semgrep rules catalog
- Airflow + Notify.io integration
- Dedicated hardening engineer
- Annual red-team engagement
Human anti-patterns
- Disabling rules "temporarily". It stays. Better: a PR fixing the false positive in the rules.
- Always accepting
--skip-unfixed. Makes the scan cosmetic. - Not training the team. If the dev doesn't understand why SonarQube fails, they turn off the check. Short monthly training.
- Only running in CI. Pre-commit saves CI cycles and gives instant feedback.
- Trusting that "the model will learn". It doesn't learn between PRs; rules must be explicit.
FAQ
Does it become unbearably slow?Pre-commit stays under 20s with well-filtered rules. Full CI 5-12 min. Better than a prod incident.
Does it work for small teams?Yes. Minimum: gitleaks + semgrep + trivy + pre-commit. ~30 min of setup.
Does it detect runtime prompt injection?Partially. SAST catches dangerous concat; runtime guardrails (NeMo, Rebuff) catch what slips. They are complementary.
What do I do when a PR has 40 Semgrep findings?Prioritize by severity: ERROR blocks, WARNING stays as tagged tech debt. The first week generates noise; it calibrates to 2-5 findings per PR.
Is Sonar Community sufficient, or do I need Enterprise? Community is enough for teams <15 people. Enterprise adds decent branch analysis and cross-repo, useful in monorepo.
Next steps
Complete pipeline published at github.com/numoru-ia/secure-ai-codegen-template. Includes pre-commit, GitHub Actions, .cursorrules, custom Semgrep rules and Falco policies. Fork + make bootstrap. Next piece in the series: how to wire these metrics to the Langfuse + Grafana dashboard for full code-to-runtime visibility.