LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Secure Software Development Lifecycle: Building Security Into Every Phase

securitydevopssastdastthreat-modelingci-cddevsecops

Security tacked on at the end of a release cycle is security theater. A vulnerability found in production costs 30x more to fix than the same issue caught at design time — and that’s before accounting for breach costs, regulatory penalties, and reputational damage. The Secure Software Development Lifecycle (Secure SDLC or SSDLC) shifts security left: embedding threat modeling, automated scanning, and security gates at every stage so that findings arrive when they’re cheapest to fix.

This guide covers the full Secure SDLC from first principles through production, with practical tooling you can start using today.


Why “Shift Left” Actually Works

The concept sounds like a buzzword but the math is real. IBM’s Systems Sciences Institute measured defect correction costs across phases:

Phase found Relative cost to fix
Requirements / Design
Implementation
Integration/QA 15×
Production 100×

Shifting security to requirements and design isn’t just cheaper — it’s the only scalable approach. You cannot hire enough pen testers to outpace a development team shipping daily. Automated checks embedded in developer workflows catch 80% of common vulnerabilities before they ever reach staging.


Phase 1: Requirements and Planning

Security work starts before a line of code is written.

Security Requirements Checklist

Every feature ticket should answer these questions before development begins:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
## Security Requirements

### Data Classification
- [ ] What data does this feature create, read, update, or delete?
- [ ] Is any of it PII, PHI, PCI, or otherwise regulated?
- [ ] What is the data retention requirement?

### Authentication & Authorization
- [ ] Which users/roles can access this feature?
- [ ] Are there multi-tenancy isolation requirements?
- [ ] Does this feature need audit logging?

### Input/Output
- [ ] Does this feature accept user input? (if yes, validate and sanitize)
- [ ] Does this feature generate output rendered in a browser? (if yes, encoding needed)
- [ ] Does this feature call external services? (if yes, consider SSRF mitigations)

### Cryptography
- [ ] Does this feature store secrets or credentials?
- [ ] Are there encryption-at-rest or in-transit requirements?

### Compliance
- [ ] Which compliance frameworks apply (GDPR, HIPAA, SOC 2, PCI DSS)?
- [ ] Does this require a Data Protection Impact Assessment?

This checklist isn’t bureaucracy — it surfaces constraints that change the design before implementation begins. A feature that requires HIPAA audit logging has a different architecture than one that doesn’t.


Phase 2: Threat Modeling

Threat modeling is structured thinking about how your system can be attacked. Done early, it shapes architecture decisions. Done late, it becomes an expensive retrofit.

STRIDE Framework

STRIDE (Microsoft) categorizes threats by type:

Letter Threat Violated property Example
S Spoofing Authentication Forging another user’s session token
T Tampering Integrity Modifying a payment amount in transit
R Repudiation Non-repudiation Deleting logs after a malicious action
I Information Disclosure Confidentiality Leaking PII via verbose error messages
D Denial of Service Availability Flooding an endpoint with requests
E Elevation of Privilege Authorization A user accessing admin functionality

Building a Threat Model

The simplest effective threat model is a Data Flow Diagram + STRIDE analysis:

Step 1 — Draw the DFD

[User Browser] ---(HTTPS)---> [Load Balancer] ---(HTTP)---> [API Server]
                                                                 |
                                                        [PostgreSQL DB]
                                                                 |
                                                        [S3 (exports)]

Mark trust boundaries (the dashed line between internet and internal network is a trust boundary). Every crossing of a trust boundary is a potential attack surface.

Step 2 — Apply STRIDE to each component and data flow

For the API Server:

  • Spoofing: Can an attacker forge requests as another user? → JWT validation, session fixation checks
  • Tampering: Can request payloads be modified after authentication? → Input validation, request signing for critical operations
  • Repudiation: Can users deny actions? → Structured audit logging with immutable writes
  • Information Disclosure: Do error responses leak stack traces or DB schema? → Generic error messages in production
  • Denial of Service: Can a single user exhaust resources? → Rate limiting, request size limits
  • Elevation of Privilege: Can a regular user access admin endpoints? → Role-based access control checks on every handler

Step 3 — Rate and prioritize

Use DREAD scoring (Damage, Reproducibility, Exploitability, Affected users, Discoverability) or simply High/Medium/Low to prioritize which threats to mitigate in the current sprint vs. accept as technical debt.

Threat Modeling Tools

  • OWASP Threat Dragon — Free, web-based DFD tool with STRIDE support
  • Microsoft Threat Modeling Tool — Desktop tool, good for Windows-centric stacks
  • IriusRisk — Enterprise platform with automated threat generation from architecture diagrams
  • Threagile — Code-as-model approach; define your architecture in YAML, get a threat model back

For teams just starting, Threat Dragon with a weekly 30-minute session per major feature is enough to catch architectural-level issues without being burdensome.


Phase 3: Secure Coding Standards

Documented standards give developers clear guidance and provide a baseline for code review.

Core Principles

Input validation — validate at every trust boundary, reject by default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from pydantic import BaseModel, validator, constr
import re

class CreateUserRequest(BaseModel):
    username: constr(min_length=3, max_length=50, pattern=r'^[a-zA-Z0-9_-]+$')
    email: str
    role: str

    @validator('email')
    def validate_email(cls, v):
        # Use a proper email validation library, not regex
        import email_validator
        email_validator.validate_email(v)
        return v.lower()

    @validator('role')
    def validate_role(cls, v):
        allowed_roles = {'viewer', 'editor', 'admin'}
        if v not in allowed_roles:
            raise ValueError(f'role must be one of {allowed_roles}')
        return v

Parameterized queries — never interpolate user input into SQL:

1
2
3
4
5
6
7
8
// BAD — SQL injection
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", userInput)

// GOOD — parameterized
row := db.QueryRowContext(ctx,
    "SELECT id, email, role FROM users WHERE email = $1",
    userInput,
)

Output encoding — context-aware encoding prevents XSS:

1
2
3
4
5
6
7
8
import "html/template"

// BAD — raw string interpolation in HTML context
fmt.Fprintf(w, "<p>Hello %s</p>", username)

// GOOD — html/template handles encoding automatically
tmpl := template.Must(template.New("").Parse(`<p>Hello {{.Username}}</p>`))
tmpl.Execute(w, data)

Secret handling — never log secrets, never hardcode them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import os
import logging

# BAD
api_key = "sk-prod-abc123..."
logging.info(f"Calling API with key {api_key}")

# GOOD
api_key = os.environ.get("API_KEY")
if not api_key:
    raise RuntimeError("API_KEY environment variable not set")
logging.info("Calling external API")  # no key in logs

Language-Specific Security Guides

  • Go: Use html/template not text/template for HTML, crypto/rand not math/rand, golang.org/x/crypto/bcrypt for passwords
  • Python: Use parameterized queries with psycopg2, secrets module for token generation, bleach for HTML sanitization
  • Node.js: Use helmet for HTTP headers, express-validator for input validation, avoid eval() and Function() constructors
  • Java: Use PreparedStatement always, Spring Security for auth, never deserialize untrusted data with Java native serialization

Phase 4: Static Application Security Testing (SAST)

SAST analyzes source code without running it, catching vulnerabilities at the code level.

SAST Tool Comparison

Tool Languages Open Source Best for
Semgrep 30+ Yes (community rules) Custom rules, CI integration
CodeQL 10+ Yes (via GitHub) Deep semantic analysis, GitHub repos
SonarQube 30+ Community edition Dashboard, tech debt tracking
Bandit Python Yes Python-specific, simple to run
gosec Go Yes Go-specific security checks
ESLint security plugins JS/TS Yes Node.js, React applications
Checkmarx / Veracode Broad No Enterprise, compliance reporting

Semgrep in Practice

Semgrep is the most flexible SAST tool — it’s fast, runs locally and in CI, and supports custom rules written in YAML.

Installing and running:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pip install semgrep

# Run against current directory with OWASP ruleset
semgrep --config=p/owasp-top-ten .

# Run with multiple rulesets
semgrep --config=p/python --config=p/secrets --config=p/sql-injection .

# Output SARIF for GitHub Code Scanning
semgrep --config=p/owasp-top-ten --sarif > results.sarif

Writing a custom rule — detect hardcoded AWS credentials:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# .semgrep/rules/no-hardcoded-aws-keys.yaml
rules:
  - id: hardcoded-aws-access-key
    patterns:
      - pattern: |
          $VAR = "AKIA..."
    message: >
      Hardcoded AWS access key detected in $VAR.
      Use environment variables or IAM roles instead.
    languages: [python, go, javascript, typescript]
    severity: ERROR
    metadata:
      cwe: "CWE-798: Use of Hard-coded Credentials"
      owasp: "A07:2021 - Identification and Authentication Failures"

  - id: hardcoded-password-assignment
    patterns:
      - pattern: password = "..."
      - pattern: passwd = "..."
      - pattern: pwd = "..."
    pattern-not:
      - pattern: password = ""
      - pattern: password = os.environ[...]
      - pattern: password = os.getenv(...)
    message: Possible hardcoded password in $VAR
    languages: [python]
    severity: WARNING
1
semgrep --config=.semgrep/rules/ .

Detecting SQL injection patterns:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
rules:
  - id: sql-injection-string-format
    patterns:
      - pattern: |
          $DB.execute("..." % ...)
      - pattern: |
          $DB.execute("..." + ...)
      - pattern: |
          cursor.execute(f"...")
    message: >
      Possible SQL injection via string formatting.
      Use parameterized queries: cursor.execute("SELECT * FROM t WHERE id = %s", (user_id,))
    languages: [python]
    severity: ERROR

CodeQL for Deep Analysis

CodeQL treats code as data — you query it like a database. It finds vulnerabilities spanning multiple files and function calls.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# .github/workflows/codeql.yml
name: CodeQL Analysis

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * 1'  # Weekly full scan

jobs:
  analyze:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      actions: read
      contents: read

    strategy:
      matrix:
        language: [python, javascript, go]

    steps:
      - uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          queries: security-and-quality  # includes OWASP, CWE

      - name: Autobuild
        uses: github/codeql-action/autobuild@v3

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:${{ matrix.language }}"
          upload: true  # uploads to GitHub Security tab

gosec for Go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
go install github.com/securego/gosec/v2/cmd/gosec@latest

# Scan current module
gosec ./...

# Output as JSON for CI processing
gosec -fmt json -out gosec-results.json ./...

# Exclude specific rules (use sparingly)
gosec -exclude G104 ./...  # G104 = errors unhandled (noisy in some codebases)

Common gosec findings:

  • G101 — Hardcoded credentials
  • G201/G202 — SQL injection via string formatting
  • G304 — File inclusion via user input
  • G401/G402 — Weak crypto (MD5, SHA1, InsecureSkipVerify)
  • G501 — Import of insecure crypto/md5

Bandit for Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pip install bandit

# Full scan with medium+ severity
bandit -r . -ll -ii

# CI-friendly with exit code
bandit -r src/ -f json -o bandit-report.json
echo $? # non-zero if findings above threshold

# Generate HTML report
bandit -r . -f html -o bandit-report.html

Key Bandit checks:

  • B106 — Hardcoded password in function argument
  • B201 — Flask debug mode enabled
  • B301/B302 — Pickle usage (arbitrary code execution risk)
  • B608 — SQL injection
  • B703 — Django SQL injection via .extra()

Phase 5: Dynamic Application Security Testing (DAST)

DAST runs against a live application, probing for vulnerabilities that static analysis misses: runtime injection points, authentication bypasses, business logic flaws.

OWASP ZAP (Zed Attack Proxy)

ZAP is the gold standard open-source DAST tool. It can run in headless/API mode for CI integration.

Docker-based CI scan:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# .github/workflows/dast.yml
name: DAST Scan

on:
  push:
    branches: [main]

jobs:
  dast:
    runs-on: ubuntu-latest
    services:
      app:
        image: your-org/your-app:${{ github.sha }}
        ports:
          - 8080:8080
        env:
          DATABASE_URL: sqlite:///test.db
          SECRET_KEY: ci-test-secret-not-for-production

    steps:
      - uses: actions/checkout@v4

      - name: Wait for app to be ready
        run: |
          timeout 60 bash -c 'until curl -sf http://localhost:8080/health; do sleep 2; done'

      - name: ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: 'http://localhost:8080'
          rules_file_name: '.zap/rules.tsv'
          cmd_options: '-a'  # include ajax spider
          fail_action: true
          artifact_name: zap-report

      - name: Upload ZAP Report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: zap-report
          path: report_html.html

ZAP rules configuration (.zap/rules.tsv) — suppress false positives:

# Rule ID	IGNORE/WARN/FAIL	Description
10096	IGNORE	Timestamp Disclosure - Unix
10027	IGNORE	Information Disclosure - Suspicious Comments (dev comments)
90022	WARN	Application Error Disclosure
40012	FAIL	Cross Site Scripting (Reflected)
40014	FAIL	Cross Site Scripting (Persistent)
40018	FAIL	SQL Injection

ZAP API scan for REST APIs:

1
2
3
4
5
6
7
8
9
docker run --rm \
  -v $(pwd):/zap/wrk/:rw \
  ghcr.io/zaproxy/zaproxy:stable \
  zap-api-scan.py \
    -t http://host.docker.internal:8080/openapi.json \
    -f openapi \
    -r zap-api-report.html \
    -J zap-api-report.json \
    -z "-config scanner.attackStrength=HIGH"

Nuclei for Template-Based Scanning

Nuclei runs a library of 9000+ templates against your application, checking for known CVEs, misconfigurations, and exposures:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Install
go install github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest

# Update templates
nuclei -update-templates

# Scan with specific tags
nuclei -u http://localhost:8080 \
  -tags owasp,injection,auth \
  -severity medium,high,critical \
  -o nuclei-results.txt \
  -json > nuclei-results.jsonl

# Exclude false-positive-heavy templates
nuclei -u http://localhost:8080 \
  -exclude-tags dos,fuzzing \
  -rate-limit 50

Custom Nuclei template — check for debug endpoints:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# templates/debug-endpoints.yaml
id: debug-endpoints-exposed

info:
  name: Debug Endpoints Exposed
  author: your-security-team
  severity: high
  description: Checks for accidentally exposed debug/diagnostic endpoints

requests:
  - method: GET
    path:
      - "{{BaseURL}}/debug"
      - "{{BaseURL}}/debug/pprof"
      - "{{BaseURL}}/__debug__"
      - "{{BaseURL}}/actuator"
      - "{{BaseURL}}/actuator/env"
      - "{{BaseURL}}/actuator/heapdump"
      - "{{BaseURL}}/.env"
      - "{{BaseURL}}/config.json"

    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200

      - type: word
        condition: or
        words:
          - "goroutine"
          - "DATABASE_URL"
          - "SECRET_KEY"
          - "spring.datasource"
          - "heap dump"

Phase 6: Software Composition Analysis (SCA) and Dependency Auditing

Modern applications are 80-90% open source code. Your dependencies are your attack surface.

Understanding the Risk

The Log4Shell vulnerability (CVE-2021-44228) affected hundreds of thousands of applications because they all depended on log4j. Organizations that had dependency inventories could assess exposure in hours; those that didn’t spent weeks doing manual audits.

Tools by Ecosystem

npm (Node.js):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Built-in audit
npm audit

# Fix automatically where possible
npm audit fix

# JSON output for CI
npm audit --json | jq '.vulnerabilities | to_entries[] | select(.value.severity == "critical")'

# More detailed with SARIF output
npx better-npm-audit audit --level critical

pip (Python):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pip install pip-audit safety

# pip-audit — checks against PyPI advisory database
pip-audit
pip-audit --requirement requirements.txt
pip-audit --format json > pip-audit.json

# safety — checks against Safety DB
safety check
safety check --json > safety-report.json
safety check --full-report

Go modules:

1
2
3
4
5
6
# Built-in govulncheck (official Go tool)
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

# Output JSON
govulncheck -json ./... > govulncheck.json

Rust:

1
2
3
4
5
cargo install cargo-audit
cargo audit

# With SARIF output
cargo audit --json > cargo-audit.json

Java (Maven):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!-- pom.xml -->
<plugin>
  <groupId>org.owasp</groupId>
  <artifactId>dependency-check-maven</artifactId>
  <version>9.0.9</version>
  <configuration>
    <failBuildOnCVSS>7</failBuildOnCVSS>
    <formats>HTML,JSON,SARIF</formats>
  </configuration>
  <executions>
    <execution>
      <goals><goal>check</goal></goals>
    </execution>
  </executions>
</plugin>

Trivy — Universal Scanner

Trivy scans container images, file systems, Git repos, and Kubernetes manifests for vulnerabilities, misconfigurations, and secrets:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Install
brew install aquasecurity/trivy/trivy  # macOS
# or
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

# Scan a container image
trivy image nginx:latest
trivy image --severity HIGH,CRITICAL nginx:latest

# Scan local filesystem (dependencies)
trivy fs .
trivy fs --scanners vuln,secret,misconfig .

# Scan a Git repo
trivy repo https://github.com/your-org/your-repo

# Output for CI
trivy image \
  --exit-code 1 \
  --severity CRITICAL \
  --format sarif \
  --output trivy-results.sarif \
  your-org/your-app:latest

Trivy in CI/CD:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# .github/workflows/trivy.yml
name: Trivy Security Scan

on:
  push:
    branches: [main]
  pull_request:

jobs:
  trivy-fs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Trivy filesystem scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          scanners: 'vuln,secret,misconfig'
          severity: 'CRITICAL,HIGH'
          format: 'sarif'
          output: 'trivy-fs.sarif'
          exit-code: '1'

      - name: Upload SARIF to GitHub Security
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-fs.sarif

  trivy-image:
    runs-on: ubuntu-latest
    needs: trivy-fs
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t ${{ github.repository }}:${{ github.sha }} .

      - name: Trivy image scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'image'
          image-ref: '${{ github.repository }}:${{ github.sha }}'
          severity: 'CRITICAL,HIGH'
          format: 'sarif'
          output: 'trivy-image.sarif'
          exit-code: '1'

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-image.sarif

Dependabot and Renovate

Automated dependency updates are the most important thing you can do for long-term dependency hygiene.

Dependabot (GitHub-native):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
    open-pull-requests-limit: 10
    labels:
      - "dependencies"
      - "security"
    # Group non-breaking updates to reduce PR noise
    groups:
      non-major:
        update-types:
          - "minor"
          - "patch"

  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
    labels:
      - "dependencies"
      - "docker"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      actions:
        patterns:
          - "*"

Renovate (more configurable):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// renovate.json
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "schedule": ["before 9am on monday"],
  "packageRules": [
    {
      "matchUpdateTypes": ["minor", "patch"],
      "matchCurrentVersion": "!/^0/",
      "automerge": true,
      "automergeType": "pr",
      "platformAutomerge": true
    },
    {
      "matchPackagePatterns": [".*"],
      "matchUpdateTypes": ["major"],
      "dependencyDashboardApproval": true
    }
  ],
  "vulnerabilityAlerts": {
    "labels": ["security"],
    "assignees": ["security-team"]
  }
}

Phase 7: Secrets Detection

Secrets committed to git are one of the most common and embarrassing security incidents. The fix has two parts: prevention and detection.

Pre-commit Prevention with detect-secrets

1
2
3
4
5
6
pip install detect-secrets

# Initialize baseline (acknowledge existing "secrets" that are actually OK)
detect-secrets scan > .secrets.baseline

# Add to pre-commit config
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']
        exclude: package.lock.json

  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4
    hooks:
      - id: gitleaks

Gitleaks for Git History Scanning

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Install
brew install gitleaks

# Scan current repo history
gitleaks detect --source . --verbose

# Scan a specific commit range
gitleaks detect --source . --log-opts "HEAD~50..HEAD"

# Protect mode (scan staged changes, use in pre-commit)
gitleaks protect --staged

# Custom rules
gitleaks detect --config .gitleaks.toml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# .gitleaks.toml
title = "Custom Gitleaks Config"

[[rules]]
id = "internal-api-key"
description = "Internal API Key"
regex = '''mycompany_[a-zA-Z0-9]{32}'''
tags = ["key", "internal"]

[[rules]]
id = "database-connection-string"
description = "Database connection string with password"
regex = '''(postgres|mysql|mongodb):\/\/[^:]+:[^@]+@'''
tags = ["database", "connection"]

[allowlist]
description = "Global allowlist"
paths = [
    '''(.*?)(jpg|png|gif|doc|pdf|bin)$''',
    '''tests/fixtures/.*''',
]
regexTarget = "line"
regexes = [
    '''EXAMPLE_KEY''',
    '''<YOUR_API_KEY_HERE>''',
]

Scanning Git History for Leaked Secrets

If you suspect your repo’s history contains leaked secrets, run a full audit:

1
2
3
4
5
6
# Full history scan
gitleaks detect --source . --log-opts "--all" --report-path gitleaks-report.json

# If secrets are found, rotate them FIRST, then consider history cleanup
# History rewriting (use with extreme caution on shared repos)
# git filter-repo --path sensitive-file.txt --invert-paths

Phase 8: Security in CI/CD

The security gate model: different checks at different stages, with escalating strictness.

The Security Pipeline

┌─────────────────────────────────────────────────────────────────┐
│ Developer Workstation                                           │
│  ├── pre-commit: detect-secrets, gitleaks, linting             │
│  └── IDE plugins: Snyk, SonarLint, CodeWhisperer security      │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ Pull Request CI                                                 │
│  ├── SAST: Semgrep, CodeQL (on changed files)                  │
│  ├── SCA: Trivy fs, govulncheck, pip-audit (CRITICAL blocks PR)│
│  ├── Secrets: Gitleaks detect                                   │
│  └── IaC: Checkov, tfsec (if infra changes)                    │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ Merge to Main                                                   │
│  ├── Full SAST scan (all files, not just changed)              │
│  ├── Container image build + Trivy image scan                  │
│  └── SBOM generation (Syft)                                    │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ Staging Deployment                                              │
│  ├── DAST: ZAP baseline scan                                    │
│  ├── Nuclei: CVE and misconfiguration checks                   │
│  └── Integration security tests (auth bypass, injection)       │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ Production                                                      │
│  ├── Runtime security monitoring (Falco)                       │
│  ├── Dependency vulnerability alerts (Dependabot)              │
│  └── Scheduled pen tests / bug bounty program                  │
└─────────────────────────────────────────────────────────────────┘

Complete CI Security Workflow

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# .github/workflows/security.yml
name: Security Gates

on:
  pull_request:
  push:
    branches: [main]

jobs:
  secrets-scan:
    name: Secrets Detection
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # full history for gitleaks

      - name: Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  sast:
    name: Static Analysis
    runs-on: ubuntu-latest
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v4

      - name: Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/python
            p/secrets
          generateSarif: "1"

      - name: Upload Semgrep SARIF
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: semgrep.sarif

  sca:
    name: Dependency Scanning
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: pip-audit
        run: |
          pip install pip-audit
          pip-audit --requirement requirements.txt \
            --format json \
            --output pip-audit.json \
            --vulnerability-service osv \
            --exit-code

      - name: Trivy filesystem
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          scanners: vuln,secret
          severity: CRITICAL,HIGH
          exit-code: '1'

  iac-scan:
    name: IaC Security
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: infrastructure/
          framework: terraform,dockerfile,kubernetes
          output_format: sarif
          output_file_path: checkov.sarif
          soft_fail: false
          check: CKV_DOCKER_2,CKV_K8S_8,CKV_TF_1

      - name: Upload Checkov results
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: checkov.sarif

  container-scan:
    name: Container Image Scan
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t app:${{ github.sha }} .

      - name: Trivy image scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: image
          image-ref: app:${{ github.sha }}
          severity: CRITICAL,HIGH
          format: sarif
          output: trivy-image.sarif
          exit-code: '1'

      - name: Generate SBOM
        run: |
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh
          ./bin/syft app:${{ github.sha }} -o spdx-json=sbom.spdx.json

      - name: Upload SBOM artifact
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.spdx.json

Phase 9: Infrastructure as Code Security

Terraform, Kubernetes manifests, and Dockerfiles are code — they need security scanning too.

Checkov for Terraform

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
pip install checkov

# Scan Terraform
checkov -d infrastructure/terraform/ --framework terraform

# Scan Kubernetes manifests
checkov -d k8s/ --framework kubernetes

# Scan Dockerfile
checkov -f Dockerfile --framework dockerfile

# Custom policy in Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# checks/custom/ensure_no_latest_tag.py
from checkov.common.models.enums import CheckResult, CheckCategories
from checkov.kubernetes.checks.resource.base_container_check import BaseK8ContainerCheck

class EnsureNoLatestTag(BaseK8ContainerCheck):
    def __init__(self):
        name = "Ensure container image does not use 'latest' tag"
        id = "CKV_CUSTOM_K8S_1"
        supported_entities = ['containers', 'initContainers']
        categories = [CheckCategories.SUPPLY_CHAIN]
        super().__init__(name=name, id=id, categories=categories,
                         supported_entities=supported_entities)

    def scan_container_conf(self, metadata, conf):
        image = conf.get("image", "")
        if isinstance(image, str) and (image.endswith(":latest") or ":" not in image):
            return CheckResult.FAILED
        return CheckResult.PASSED

scanner = EnsureNoLatestTag()

tfsec for Terraform

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
brew install tfsec  # macOS
# or
go install github.com/aquasecurity/tfsec/cmd/tfsec@latest

# Scan with severity threshold
tfsec . --minimum-severity HIGH

# Output for CI
tfsec . --format sarif > tfsec.sarif
tfsec . --format json > tfsec.json

# Custom check
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# tfsec-custom/no-public-s3.yaml
checks:
  - code: CUS001
    description: S3 bucket should not be public
    requiredTypes:
      - resource
    requiredLabels:
      - aws_s3_bucket
    base_block: []
    filters:
      - ||
        - and:
          - name: acl
            type: equals
            value: private

Secure Dockerfile Checklist

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# BAD Dockerfile
FROM ubuntu:latest
RUN apt-get install -y everything
COPY . /app
CMD ["python", "app.py"]

# GOOD Dockerfile
# Use specific digest for immutability
FROM python:3.12.3-slim@sha256:abc123...

# Create non-root user
RUN groupadd --gid 1000 appuser && \
    useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser

WORKDIR /app

# Copy requirements first for layer caching
COPY requirements.txt .

# Install as root, then switch user
RUN pip install --no-cache-dir --require-hashes -r requirements.txt

# Copy application code
COPY --chown=appuser:appuser src/ ./src/

# Drop privileges
USER appuser

# Declare read-only filesystem where possible
# (set in kubernetes securityContext or docker run --read-only)

# Use exec form (no shell injection)
ENTRYPOINT ["python", "-m", "src.main"]

# Add health check
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"

Phase 10: Security Testing

Beyond automated tooling, some manual and semi-automated testing is irreplaceable.

Security-Focused Integration Tests

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
# tests/security/test_auth.py
import pytest
import httpx

BASE_URL = "http://localhost:8080"

class TestAuthorizationBypass:
    """Tests for BOLA (Broken Object Level Authorization) — OWASP API #1"""

    def test_user_cannot_access_other_users_data(self, client, user_a_token, user_b_id):
        """User A should not be able to read User B's profile"""
        response = client.get(
            f"/api/users/{user_b_id}",
            headers={"Authorization": f"Bearer {user_a_token}"}
        )
        assert response.status_code in (403, 404), \
            f"Expected 403/404 but got {response.status_code}"

    def test_user_cannot_modify_other_users_data(self, client, user_a_token, user_b_id):
        response = client.put(
            f"/api/users/{user_b_id}",
            headers={"Authorization": f"Bearer {user_a_token}"},
            json={"email": "hacked@evil.com"}
        )
        assert response.status_code in (403, 404)

    def test_unauthenticated_request_rejected(self, client):
        response = client.get("/api/users/1")
        assert response.status_code == 401

    def test_expired_token_rejected(self, client, expired_token):
        response = client.get("/api/users/1",
            headers={"Authorization": f"Bearer {expired_token}"})
        assert response.status_code == 401


class TestInjection:
    """SQL injection and XSS checks"""

    SQL_PAYLOADS = [
        "' OR '1'='1",
        "'; DROP TABLE users; --",
        "1 UNION SELECT * FROM users--",
        "1; SELECT SLEEP(5)--",
    ]

    XSS_PAYLOADS = [
        "<script>alert(1)</script>",
        "javascript:alert(1)",
        "<img src=x onerror=alert(1)>",
        "';alert(1)//",
    ]

    @pytest.mark.parametrize("payload", SQL_PAYLOADS)
    def test_sql_injection_in_search(self, client, auth_token, payload):
        response = client.get(
            "/api/search",
            params={"q": payload},
            headers={"Authorization": f"Bearer {auth_token}"}
        )
        # Should return 200 with empty results or 400, never 500
        assert response.status_code != 500, \
            f"SQL injection may have caused error: {response.text[:200]}"
        # Should not return all users
        data = response.json()
        assert len(data.get("results", [])) < 100

    @pytest.mark.parametrize("payload", XSS_PAYLOADS)
    def test_xss_in_profile_name(self, client, auth_token, payload):
        response = client.put(
            "/api/profile",
            headers={"Authorization": f"Bearer {auth_token}"},
            json={"display_name": payload}
        )
        if response.status_code == 200:
            # If accepted, ensure it's encoded in output
            profile_response = client.get("/api/profile",
                headers={"Authorization": f"Bearer {auth_token}"})
            body = profile_response.text
            assert "<script>" not in body
            assert "javascript:" not in body


class TestRateLimiting:
    """Verify rate limiting protects authentication endpoints"""

    def test_login_rate_limited(self, client):
        for i in range(20):
            response = client.post("/api/auth/login", json={
                "email": f"test{i}@example.com",
                "password": "wrongpassword"
            })

        # After 20 attempts, should be rate limited
        assert response.status_code == 429, \
            "Authentication endpoint is not rate limited"

    def test_rate_limit_headers_present(self, client, auth_token):
        response = client.get("/api/users",
            headers={"Authorization": f"Bearer {auth_token}"})
        assert "X-RateLimit-Remaining" in response.headers or \
               "RateLimit-Remaining" in response.headers

Automated Pen Testing with OWASP ZAP Python API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# scripts/security_scan.py
import time
from zapv2 import ZAPv2

TARGET = "http://localhost:8080"
ZAP_ADDRESS = "http://localhost:8090"

zap = ZAPv2(proxies={'http': ZAP_ADDRESS, 'https': ZAP_ADDRESS})

print(f"[*] Spidering {TARGET}")
scan_id = zap.spider.scan(TARGET)
while int(zap.spider.status(scan_id)) < 100:
    time.sleep(2)
print(f"[+] Spider complete. Found {len(zap.spider.results(scan_id))} URLs")

# Authenticate if needed
# zap.authentication.set_authentication_method(...)

print("[*] Running active scan")
scan_id = zap.ascan.scan(TARGET, recurse=True, inscopeonly=False)
while int(zap.ascan.status(scan_id)) < 100:
    print(f"  Progress: {zap.ascan.status(scan_id)}%")
    time.sleep(5)

alerts = zap.core.alerts(baseurl=TARGET)
high_alerts = [a for a in alerts if a['risk'] in ('High', 'Critical')]

print(f"\n[+] Scan complete. {len(alerts)} total alerts, {len(high_alerts)} HIGH/CRITICAL")
for alert in high_alerts:
    print(f"\n  [{alert['risk']}] {alert['name']}")
    print(f"    URL: {alert['url']}")
    print(f"    Description: {alert['description'][:200]}")

if high_alerts:
    print("\n[!] HIGH/CRITICAL vulnerabilities found. Failing build.")
    exit(1)

Phase 11: Vulnerability Management

Finding vulnerabilities is only half the job. Managing them to resolution is the other half.

Severity-Based SLAs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
## Vulnerability Remediation SLA

| Severity | CVSS Score | Response SLA | Remediation SLA |
|---|---|---|---|
| Critical | 9.0–10.0 | 4 hours | 24 hours |
| High | 7.0–8.9 | 24 hours | 7 days |
| Medium | 4.0–6.9 | 72 hours | 30 days |
| Low | 0.1–3.9 | 1 week | 90 days |

### Escalation
- Critical: Page on-call security engineer immediately
- High: Slack #security-alerts + ticket within 24h
- Medium/Low: Ticket in next sprint planning

### Exceptions
Exceptions require:
1. Business justification
2. Compensating controls documented
3. Engineering manager + security sign-off
4. Maximum extension: 1 additional cycle at same SLA

Tracking with GitHub Security Advisories

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# .github/workflows/vulnerability-tracking.yml
name: Vulnerability Report

on:
  schedule:
    - cron: '0 8 * * 1'  # Monday morning report

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Generate vulnerability report
        run: |
          # Aggregate findings from multiple scanners
          trivy fs . --format json > trivy-report.json
          pip-audit --format json > pip-report.json

          python scripts/aggregate_vulns.py \
            --trivy trivy-report.json \
            --pip pip-report.json \
            --output weekly-report.md

      - name: Create issue for unresolved criticals
        uses: peter-evans/create-issue-from-file@v5
        with:
          title: "Weekly Security Vulnerability Report"
          content-filepath: weekly-report.md
          labels: security, vulnerability-report
          assignees: security-team-lead

Phase 12: Security Champions Program

Tooling alone doesn’t create a security culture. The Security Champions model embeds a security-aware developer in each team.

Security Champion Responsibilities

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
## Security Champion Role

### Time Commitment
~10% of sprint capacity (4 hours/week for a 2-week sprint)

### Responsibilities

**Weekly**
- Review dependency update PRs for security implications
- Check security dashboard for new findings in team's services
- Respond to security questions from teammates

**Per-Sprint**
- Participate in threat modeling for new features (1 hour)
- Review security-sensitive code in PRs (auth, crypto, input handling)
- Ensure security requirements are in tickets

**Quarterly**
- Participate in cross-team security champions sync
- Complete one security training module
- Present one security topic to the team (brown bag)

### Not Responsible For
Champions are not security engineers. They don't:
- Do full penetration testing
- Make final security decisions
- Review all code for security

Security Champions Meeting Agenda

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
## Security Champions Sync — [Date]

**Attendees**: Champions from each team + Security team lead

**Standing Items (30 min)**
1. New CVEs affecting our stack (10 min)
   - Review critical/high from past 2 weeks
   - Assign remediation owners
2. Dashboard review (10 min)
   - SAST finding trends
   - Dependency vulnerability age
3. Incident debrief (10 min)
   - Any security incidents since last meeting?
   - Lessons learned

**Topic of the Month (30 min)**
- Hands-on session: SQL injection lab
- Or: Review a real vulnerability in our codebase
- Or: New tool introduction

**Next Steps**
- [ ] Action item owner — due date

Measuring Your Secure SDLC

You can’t improve what you don’t measure.

Key Security Metrics

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Example metrics to track (Prometheus-style naming)

# Detection velocity
security_vulnerability_mean_time_to_detect_hours{severity="critical"}
security_vulnerability_mean_time_to_detect_hours{severity="high"}

# Remediation velocity
security_vulnerability_mean_time_to_remediate_hours{severity="critical"}
security_vulnerability_mean_time_to_remediate_hours{severity="high"}

# Coverage
security_sast_coverage_percent           # % of repos with SAST
security_sca_coverage_percent            # % with dependency scanning
security_dast_coverage_percent           # % with DAST
security_threat_model_coverage_percent   # % of major features threat-modeled

# Finding rates
security_findings_per_release{scanner="semgrep",severity="high"}
security_findings_introduced_vs_resolved_ratio  # should trend toward <1

# False positive rate
security_sast_false_positive_rate  # high false positives = tool gets ignored

Maturity Assessment (OWASP SAMM)

OWASP’s Software Assurance Maturity Model (SAMM) provides a framework for assessing and improving your Secure SDLC across five business functions:

  1. Governance — Policy, strategy, education
  2. Design — Threat assessment, security requirements
  3. Implementation — Secure build, defect management
  4. Verification — Architecture assessment, requirements testing, security testing
  5. Operations — Incident management, environment management

Each function has three levels (1-3). A practical starting target for most teams is Level 1 across all functions: basic practices in place, ad hoc execution. Level 2 adds consistency and tooling. Level 3 is continuous improvement with advanced techniques.

Use the OWASP SAMM assessment tool at owaspsamm.org for a structured self-assessment.


Quick Start Checklist

To get started without boiling the ocean:

Week 1 — Foundations

  • Add detect-secrets and gitleaks pre-commit hooks
  • Enable Dependabot for all repos
  • Run Trivy against your most critical container image
  • Document one security requirement in your next feature ticket

Week 2 — SAST

  • Add Semgrep with p/owasp-top-ten to CI
  • Enable CodeQL on GitHub repos
  • Fix all CRITICAL findings before moving on

Week 3 — Process

  • Hold your first threat modeling session (30 minutes on an upcoming feature)
  • Write three security integration tests for your authentication flow
  • Define your vulnerability remediation SLAs

Month 2 — Coverage and culture

  • Add DAST (ZAP baseline) to staging CI
  • Designate security champions per team
  • Build vulnerability metrics dashboard

The Fundamental Rule

Security is a process, not a product or a one-time audit. The most dangerous thing you can say is “we did a pen test last year.” Attackers don’t wait for your annual review cycle.

The effective Secure SDLC runs continuously: automated checks on every commit, threat models on every major feature, dependency updates every week, and a culture where every developer considers security as a basic quality attribute alongside correctness and performance.

Start with automation. Fix the criticals. Build the culture. The rest follows.


Related: OWASP Top 10, Container Security, Secrets Management, Runtime Security with Falco

Comments