DEV Community

Cover image for CI-Embedded Security
nicolas.vbgh
nicolas.vbgh

Posted on

CI-Embedded Security

Check before you wreck

Part of The Coercion Saga — making AI write quality code.

Linters catch your mistakes. Type checkers catch your assumptions. But what about security? That's a different beast entirely.

Three attack surfaces. Three different problems.

Dependencies — Other people's code. You install a package. It works. Six months later, someone discovers a critical vulnerability. It's been in your production code the whole time. You had no idea.

Your code — The stuff you write. SQL injection. Hardcoded secrets. Unsafe regex. The classics. AI generates these patterns constantly. So do humans. Nobody's immune.

Secrets — API keys. Passwords. Tokens. One accidental commit and it's in your git history forever. Scrubbing it is a nightmare.

You can't manually track thousands of CVEs. You can't manually review every line for security anti-patterns. You can't grep every commit for leaked credentials.

So I let robots do all three.


Trivy: The Customs Officer

Every merge request gets scanned. Every dependency gets checked. HIGH and CRITICAL vulnerabilities block the merge. No exceptions.

trivy:backend:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy fs --severity HIGH,CRITICAL --ignore-unfixed --scanners vuln backend/
  allow_failure: false

trivy:frontend:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy fs --severity HIGH,CRITICAL --ignore-unfixed --scanners vuln frontend/
  allow_failure: false
Enter fullscreen mode Exit fullscreen mode

Two jobs. Backend and frontend. Both must pass.

The flags that matter:

--severity HIGH,CRITICAL — LOW and MEDIUM vulnerabilities create noise. Hundreds of alerts, most theoretical. Focus on what's actually exploitable.

--ignore-unfixed — Some vulnerabilities have no patch yet. You can't fix what doesn't have a fix. Don't block on the impossible.

--scanners vuln — Trivy can scan for misconfigurations, secrets, licenses. Too much at once is overwhelming. Start with vulnerabilities.

When it catches something:

backend/requirements.txt
========================
Total: 1 (HIGH: 1, CRITICAL: 0)

┌─────────────┬────────────────┬──────────┬─────────────────────┐
│   Library   │ Vulnerability  │ Severity │   Fixed Version     │
├─────────────┼────────────────┼──────────┼─────────────────────┤
│ cryptography│ CVE-2023-XXXXX │ HIGH     │ 41.0.0              │
└─────────────┴────────────────┴──────────┴─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

CI fails. You see exactly which package, which CVE, which version fixes it. Update the package. CI passes. Done.


Bandit: The Python Inspector

Trivy watches the door. Bandit watches the Python code.

Static analysis for security issues. SQL injection. Hardcoded passwords. Dangerous function calls. Shell injection. The stuff that ends careers.

bandit:backend:
  stage: security
  image: python:3.11-slim
  before_script:
    - pip install bandit
  script:
    - bandit -r backend/app -ll -ii
  allow_failure: false
Enter fullscreen mode Exit fullscreen mode

The flags that matter:

-r backend/app — Recursive scan of the source directory.

-ll — Only medium and high severity. Low severity is too noisy.

-ii — Only medium and high confidence. Bandit guesses sometimes. Filter the guesses.

Run it locally first:

bandit -r backend/app -ll -ii
Enter fullscreen mode Exit fullscreen mode

What it catches:

>> Issue: [B608:hardcoded_sql_expressions] Possible SQL injection
   Severity: Medium   Confidence: Medium
   Location: backend/app/api/routes/users.py:45
   More Info: https://bandit.readthedocs.io/en/latest/...

44      query = f"SELECT * FROM users WHERE id = {user_id}"
45      cursor.execute(query)
Enter fullscreen mode Exit fullscreen mode

Classic string interpolation in SQL. Use parameterized queries instead.


eslint-plugin-security: The Frontend Inspector

Same idea for TypeScript. Security-focused ESLint rules.

npm install -D eslint-plugin-security
Enter fullscreen mode Exit fullscreen mode

Add to your eslint.config.js:

import security from 'eslint-plugin-security';

export default [
  // ... other configs
  security.configs.recommended,
  {
    rules: {
      // Too noisy for frontend
      'security/detect-object-injection': 'off',
      'security/detect-possible-timing-attacks': 'off',
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

Why disable those rules?

detect-object-injection — Flags every arr[i] and obj[key]. Thousands of false positives.

detect-possible-timing-attacks — Flags password comparisons. Frontend form validation isn't a timing attack vector. That's a server-side concern.

What it keeps:

  • detect-unsafe-regex — ReDoS protection
  • detect-eval-with-expression — eval() abuse
  • detect-child-process — Shell injection
  • detect-non-literal-require — Dynamic requires

Now npm run lint catches security issues. CI already runs lint. No extra job needed.


Gitleaks: The Secret Hunter

The other scanners check code quality. Gitleaks checks for accidents.

API keys. Database passwords. JWT secrets. One commit and they're in your history forever.

gitleaks:
  stage: security
  image:
    name: zricethezav/gitleaks:latest
    entrypoint: [""]
  script:
    - gitleaks detect --source . --verbose
  allow_failure: false
Enter fullscreen mode Exit fullscreen mode

It scans the entire git history. Every commit. Every branch. Every file that ever existed.

When it catches something:

Finding:     POSTGRES_PASSWORD: SuperSecretPassword123
Secret:      SuperSecretPassword123
RuleID:      generic-api-key
File:        docker-compose.yml
Line:        9
Commit:      abc123...
Enter fullscreen mode Exit fullscreen mode

Game over. You committed a secret.

The noise problem:

Gitleaks is paranoid. It will flag example passwords in docs. Test JWT tokens. Template files with placeholder values.

Solution: .gitleaks.toml at the root:

[extend]
useDefault = true

[allowlist]
  description = "Allowlisted files and patterns"

  paths = [
    '''infra/\.env\..*\.template''',
    '''docs/.*\.md''',
    '''docker-compose\.yml''',
  ]
Enter fullscreen mode Exit fullscreen mode

Template files. Documentation. Local dev config. Not production secrets.


The Gate

Five jobs. One stage. All must pass.

stages:
  - security

trivy:backend:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy fs --severity HIGH,CRITICAL --ignore-unfixed --scanners vuln backend/
  allow_failure: false

trivy:frontend:
  stage: security
  image: aquasec/trivy:latest
  script:
    - trivy fs --severity HIGH,CRITICAL --ignore-unfixed --scanners vuln frontend/
  allow_failure: false

bandit:backend:
  stage: security
  image: python:3.11-slim
  before_script:
    - pip install bandit
  script:
    - bandit -r backend/app -ll -ii
  allow_failure: false

gitleaks:
  stage: security
  image:
    name: zricethezav/gitleaks:latest
    entrypoint: [""]
  script:
    - gitleaks detect --source . --verbose
  allow_failure: false
Enter fullscreen mode Exit fullscreen mode

Plus eslint-plugin-security runs with the lint job. No extra CI config.

Copy, paste, adapt. It works.


SCA vs SAST vs Secrets

Three acronyms. Three different things.

SCA (Software Composition Analysis) — Trivy. Scans dependencies. "Is this library vulnerable?"

SAST (Static Application Security Testing) — Bandit, eslint-plugin-security. Scans your code. "Did you write something dangerous?"

Secrets Detection — Gitleaks. Scans git history. "Did you commit something you shouldn't have?"

You need all three. A secure library won't save you from SQL injection. Clean code won't save you from a leaked API key. Secret scanning won't save you from a vulnerable dependency.

Belt, suspenders, and a backup belt.


The Point

I don't track CVEs manually. I don't read security newsletters. I don't grep commits for passwords.

The scanners run on every merge request. Vulnerable dependencies can't reach main. Dangerous code patterns can't reach main. Leaked secrets can't reach main.

Security I don't have to think about.

That's the deal.


Next up: [Backend Tests] -coming soon- — Dependencies are secure. Code is clean. Now prove the backend actually works.

Top comments (0)