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 |
1× |
| Implementation |
6× |
| 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.
- 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.
| 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.
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.
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()
|
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:
- Governance — Policy, strategy, education
- Design — Threat assessment, security requirements
- Implementation — Secure build, defect management
- Verification — Architecture assessment, requirements testing, security testing
- 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
Week 2 — SAST
Week 3 — Process
Month 2 — Coverage and culture
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