ADR-012: Security Scanning Strategy
Status
Implemented - Reviewed by LLM Council (2026-01-18), Implementation completed (2026-01-19)
Implementation Status
| Component | Status | Implementation |
|---|---|---|
| Pre-commit Hooks | ✅ Implemented | .pre-commit-config.yaml, .gitleaks.toml |
| SAST (Semgrep) | ✅ Implemented | .github/workflows/ci-build-test.yml |
| Container Scanning | ✅ Implemented | .trivy.yaml, .trivyignore |
| DAST (ZAP) | ✅ Implemented | .github/workflows/dast-scan.yml, .zap/ |
| IDOR Tests | ✅ Implemented | e2e/security/idor.spec.ts |
| Auth Bypass Tests | ✅ Implemented | e2e/security/auth-bypass.spec.ts |
| Gitleaks Blocking | ✅ Implemented | Issue #37 - CI fails on secret detection |
| SCA Blocking (HIGH+) | ✅ Implemented | Issue #39 - CI fails on HIGH/CRITICAL vulns |
| Trivy Exception Mgmt | ✅ Implemented | Issue #40 - Expiration dates, audit script |
Note: DAST scanning starts in warn-only mode per council recommendation. After 2-4 weeks of tuning, it can be upgraded to blocking mode for CRITICAL findings.
Context
Habit Hub handles sensitive user data including authentication credentials, habit tracking information, and gamification data. A comprehensive security scanning strategy is essential to identify vulnerabilities before they reach production. This ADR covers Static Application Security Testing (SAST), Dynamic Application Security Testing (DAST), Software Composition Analysis (SCA), container security, secrets scanning, and Infrastructure as Code (IaC) security.
Current State Assessment
Security Scanning Inventory
| Category | Tool | Implementation | Location | Status | Council Notes |
|---|---|---|---|---|---|
| SAST | Semgrep | Multi-ruleset scanning | security-audit.yml | ✅ Implemented | Primary SAST |
| SAST | Python-specific analysis | security-audit.yml | ⏸️ Deprecated | Remove (Semgrep sufficient) | |
| SCA | Safety | Python dependencies | security-audit.yml | ✅ Implemented | |
| SCA | npm audit | JS dependencies | security-audit.yml | ✅ Implemented | |
| Container | Trivy | Image vulnerability scan | security-audit.yml | ✅ Implemented | |
| Secrets | Gitleaks | Secret detection | security-audit.yml | ✅ Implemented | Primary for CI |
| Secrets | Verified secrets | security-audit.yml | ⏸️ Deprecated | Remove (Gitleaks sufficient) | |
| IaC | Checkov | K8s/Terraform/Docker | security-audit.yml | ✅ Implemented | |
| DAST | OWASP ZAP | Dynamic scanning | security-audit.yml | ⚠️ Partial | Start with warn mode |
Tool Consolidation (council recommendation): Removed redundant tools to reduce maintenance burden and conflicting findings. Bandit and TruffleHog deprecated in favor of Semgrep and Gitleaks respectively.
Current Workflow Configuration
# From .github/workflows/security-audit.yml
name: Security Audit
on:
schedule:
- cron: "0 2 * * 0" # Weekly Sunday 2 AM
workflow_dispatch:
Security Scan Results Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ SECURITY SCANNING FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ SAST │ │ SCA │ │Container │ │ Secrets │ │
│ │ Semgrep │ │ Safety │ │ Trivy │ │ Gitleaks │ │
│ │ Bandit │ │npm audit │ │ │ │TruffleHog│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ SARIF Format Results │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ GitHub Security Tab (Code Scanning Alerts) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Gaps Identified
DAST Gaps
| Issue | Impact | Priority |
|---|---|---|
No .zap/ directory | ZAP rules not configured | High |
| Placeholder target URL | Scans don't run against real environment | Critical |
| Only scheduled runs | No DAST on deployment | High |
| No authenticated scanning | Can't test protected endpoints | Medium |
| No API-specific scanning | REST/GraphQL untested | Medium |
SAST Gaps
| Issue | Impact | Priority |
|---|---|---|
| Not in UAT pipeline | Only runs weekly | High |
| No custom rules | Missing business logic checks | Medium |
| No incremental scanning | Full scan on every run | Low |
SCA Gaps
| Issue | Impact | Priority |
|---|---|---|
| No license compliance | Legal risk untracked | Medium |
| No SBOM generation | Supply chain visibility | Medium |
| No vulnerability prioritization | All CVEs treated equally | Low |
Container Security Gaps
| Issue | Impact | Priority |
|---|---|---|
| No base image pinning verification | Drift risk | Medium |
| No runtime security | Only build-time scanning | Medium |
| No signed image verification | Supply chain risk | Low |
Secrets Scanning Gaps
| Issue | Impact | Priority |
|---|---|---|
| No pre-commit hooks | Secrets can be committed | High |
| No rotation verification | Stale secrets undetected | Medium |
IaC Security Gaps
| Issue | Impact | Priority |
|---|---|---|
| Terraform modules incomplete | Not all IaC scanned | Medium |
| No policy-as-code enforcement | Compliance drift | Medium |
Decision
1. Comprehensive Security Scanning Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ SECURITY SCANNING ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ PRE-COMMIT CI PIPELINE DEPLOYMENT RUNTIME │
│ ────────── ─────────── ────────── ─────── │
│ │
│ ┌─────────┐ ┌─────────────┐ ┌──────────┐ ┌─────────┐ │
│ │Gitleaks │ │ SAST │ │ DAST │ │ Runtime │ │
│ │pre-hook │ │ (Semgrep, │ │ (ZAP) │ │ Scan │ │
│ └────┬────┘ │ Bandit) │ └────┬─────┘ └────┬────┘ │
│ │ └──────┬──────┘ │ │ │
│ │ │ │ │ │
│ │ ┌──────┴──────┐ │ │ │
│ │ │ SCA │ │ │ │
│ │ │ (Safety, │ │ │ │
│ │ │ npm audit) │ │ │ │
│ │ └──────┬──────┘ │ │ │
│ │ │ │ │ │
│ │ ┌──────┴──────┐ │ │ │
│ │ │ Container │ │ │ │
│ │ │ (Trivy) │ │ │ │
│ │ └──────┬──────┘ │ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Unified Security Dashboard (GitHub Security) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2. SAST Configuration
Semgrep Rules
# .semgrep/custom-rules.yml
rules:
- id: habit-hub-sql-injection
pattern: |
$QUERY = f"... {$USER_INPUT} ..."
...
session.execute($QUERY)
message: "Potential SQL injection via f-string"
severity: ERROR
languages: [python]
- id: habit-hub-auth-bypass
pattern: |
if $CONDITION:
return $RESPONSE
# Missing else clause with auth check
message: "Potential authentication bypass"
severity: WARNING
languages: [python]
Integration Points
| Stage | Trigger | Blocking | Tools |
|---|---|---|---|
| PR | On open/update | Yes (HIGH/CRITICAL) | Semgrep only (Bandit deprecated) |
| UAT Deploy | Pre-deploy | Yes (CRITICAL only) | Semgrep |
| Post-Deploy | After UAT deploy | Warn only (initially) | OWASP ZAP (baseline) |
| Weekly | Scheduled | No (report only) | Full suite |
Note: DAST starts in warn-only mode per council recommendation - blocking enabled after false positive tuning period (~2-4 weeks).
3. DAST Configuration
OWASP ZAP Setup
.zap/
├── rules.tsv # Custom scan rules
├── context/
│ └── habit-hub.context # Authentication context
├── scripts/
│ └── auth.js # Authentication script
└── policies/
└── api-scan.policy # API scanning policy
ZAP Rules Configuration
# .zap/rules.tsv
# Rule ID Action Parameter
10010 IGNORE # Cookie without Secure flag (handled by ingress)
10011 IGNORE # Cookie without HttpOnly (frontend cookies)
40012 WARN # XSS (Reflected)
40014 FAIL # XSS (Persistent)
90019 FAIL # Server Side Code Injection
90020 FAIL # Remote OS Command Injection
Authenticated Scanning
Service Account Requirements (council recommendation):
- Dedicated service account for DAST scanning (
zap-scanner@habit-hub.iam) - Minimal permissions: read-only for most resources, write for test data only
- Separate from admin/user accounts to prevent privilege escalation in tests
- Credentials stored in AWS Secrets Manager, rotated monthly
// .zap/scripts/auth.js
function authenticate(helper, paramsValues, credentials) {
var loginUrl = paramsValues.get("loginUrl");
var username = credentials.getParam("username"); // zap-scanner service account
var password = credentials.getParam("password");
var msg = helper.prepareMessage();
msg.setRequestHeader(
new HttpRequestHeader(
HttpRequestHeader.POST,
new URI(loginUrl, false),
HttpHeader.HTTP11,
),
);
msg.setRequestBody(
JSON.stringify({
email: username,
password: password,
}),
);
msg.getRequestHeader().setContentType("application/json");
helper.sendAndReceive(msg);
return msg;
}
DAST in Pipeline
dast:
triggers:
- post_uat_deployment
- weekly_schedule
target: https://uat.habitclan.com
authenticated: true
service_account: zap-scanner@habit-hub.iam
scan_types:
- baseline # Quick scan (5 min)
- api # OpenAPI-based (15 min)
- full # Comprehensive (60 min, weekly only)
# Start in warn mode per council - switch to fail after tuning period
fail_on: [] # Initially empty - warn only
warn_on:
- severity: HIGH
- severity: CRITICAL
# After 2-4 weeks of tuning, change to:
# fail_on:
# - severity: CRITICAL
# warn_on:
# - severity: HIGH
Business Logic Testing (IDOR)
Council Note: Automated scanners miss business logic vulnerabilities like IDOR (Insecure Direct Object Reference). These require manual or semi-automated testing.
# e2e/security/idor-tests.spec.ts patterns
idor_test_scenarios:
- name: cross_user_habit_access
description: "User A cannot view/modify User B's habits"
test_file: "e2e/security/idor-habits.spec.ts"
- name: cross_user_family_member_access
description: "User A cannot access User B's family members"
test_file: "e2e/security/idor-family-members.spec.ts"
- name: cross_user_achievement_manipulation
description: "User A cannot unlock User B's achievements"
test_file: "e2e/security/idor-achievements.spec.ts"
Implementation: IDOR tests are implemented as Playwright E2E tests (see e2e/security/) and run as part of the T1 regression suite.
4. SCA Configuration
Dependency Scanning
sca:
python:
tool: safety
ignore:
- CVE-2021-XXXX # False positive, not applicable
sbom: true
license_check: true
allowed_licenses:
- MIT
- Apache-2.0
- BSD-3-Clause
- ISC
javascript:
tool: npm-audit
severity_threshold: moderate
ignore_dev_dependencies: false
SBOM Generation
sbom:
format: CycloneDX
output: sbom.json
include:
- backend/requirements.txt
- frontend/package.json
- Dockerfile.prod
upload_to: dependency-track
5. Container Security
Trivy Configuration
# .trivy.yaml
severity:
- CRITICAL
- HIGH
- MEDIUM
ignore:
- CVE-2023-XXXXX # Documented exception
vuln-type:
- os
- library
scanners:
- vuln
- secret
- config
Image Signing (Cosign)
container_security:
sign_images: true
verify_base_images: true
allowed_registries:
- ghcr.io/amiable-dev
- docker.io/library
sbom_attestation: true
6. Secrets Scanning
Pre-Commit Hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
args: ["--config", ".gitleaks.toml"]
Gitleaks Configuration
# .gitleaks.toml
[allowlist]
description = "Habit Hub Allowlist"
paths = [
'''\.env\.example$''',
'''test/fixtures/''',
]
[[rules]]
id = "supabase-key"
description = "Supabase API Key"
regex = '''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+'''
tags = ["key", "supabase"]
[[rules]]
id = "aws-secret"
description = "AWS Secret Access Key"
regex = '''(?i)aws(.{0,20})?['\"][0-9a-zA-Z\/+]{40}['\"]'''
tags = ["key", "aws"]
7. IaC Security
Checkov Configuration
# .checkov.yaml
framework:
- kubernetes
- dockerfile
- terraform
- helm
skip-check:
- CKV_K8S_21 # Default namespace (managed by ArgoCD)
- CKV_K8S_40 # Resource limits (set in Helm values)
soft-fail-on:
- CKV_K8S_43 # Image digest (using tags for now)
Policy-as-Code (OPA/Gatekeeper)
# policies/require-resource-limits.rego
package kubernetes.admission
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container %v must have memory limits", [container.name])
}
8. Security Scanning Schedule
| Scan Type | Trigger | Duration | Blocking |
|---|---|---|---|
| Pre-commit (Gitleaks) | Every commit | < 5s | Yes |
| SAST (PR) | PR open/update | ~3 min | Yes (HIGH+) |
| SCA (PR) | PR open/update | ~2 min | Yes (HIGH+) |
| Container (Build) | Image build | ~5 min | Yes (CRITICAL) |
| SAST (UAT) | Pre-deploy | ~3 min | Yes (CRITICAL) |
| DAST Baseline | Post-deploy | ~5 min | Warn only (initially) |
| DAST API | Post-deploy | ~15 min | Warn only |
| IDOR Tests | T1 regression | ~5 min | Yes |
| Full Scan | Weekly | ~60 min | Report only |
Note: DAST scans start in warn-only mode. After 2-4 weeks of tuning to reduce false positives, DAST Baseline can be upgraded to blocking for CRITICAL findings.
9. Blocking Security Scans (Issues #37, #39)
Gitleaks Blocking (Issue #37):
Gitleaks now runs as a blocking gate in CI. Secret detection failures prevent merge:
# .github/workflows/ci-build-test.yml
gitleaks:
name: Secrets Scan (Gitleaks)
# No continue-on-error - secrets detection blocks merge
SCA Blocking for HIGH/CRITICAL (Issue #39):
SCA scans (pip-audit, npm audit) now fail on HIGH/CRITICAL vulnerabilities:
# pip-audit - fail on HIGH/CRITICAL
pip-audit --desc --require-hashes -r backend/requirements.txt 2>&1 || {
if echo "$output" | grep -qE "(HIGH|CRITICAL)"; then
exit 1 # Fail on HIGH/CRITICAL
fi
}
# npm audit - fail on high severity
npm audit --audit-level=high
10. Trivy Exception Management (Issue #40)
Exception Expiration Policy:
All Trivy exceptions in .trivyignore require expiration metadata:
# .trivyignore format
# CVE-YYYY-NNNNN
# Reviewed: YYYY-MM-DD
# Expires: YYYY-MM-DD (90 days from review)
# Owner: Team Name
# Reason: Brief justification
Expiration Checker Script:
A script validates exception expiration dates:
# scripts/check-trivy-exceptions.sh
# Runs in CI to warn on expired/near-expiry exceptions
# Exit codes:
# 0 - All exceptions valid
# 1 - Expired exceptions found (blocks CI)
# 2 - Exceptions expiring within 14 days (warning)
Exception Review Cadence:
- Weekly review of expiring exceptions
- 90-day maximum exception duration
- Expired exceptions must be renewed with updated justification or CVE must be remediated
Implementation Plan
Phase 1: Tool Consolidation & DAST Setup (Week 1)
- Remove Bandit from security-audit.yml (Semgrep sufficient)
- Remove TruffleHog from security-audit.yml (Gitleaks sufficient)
- Create
.zap/directory with configuration - Create dedicated service account for authenticated DAST
- Configure DAST in warn-only mode
Phase 2: Pipeline Integration (Week 2)
- Add SAST (Semgrep only) to UAT pipeline
- Configure Gitleaks pre-commit hooks
- Set up SBOM generation
- Create IDOR test suite in
e2e/security/
Phase 3: Advanced Features (Week 3-4)
- Implement image signing
- Configure OPA/Gatekeeper policies
- Set up unified security dashboard
- Review DAST findings and tune rules
- Evaluate upgrading DAST to blocking mode
Consequences
Positive
- Comprehensive vulnerability coverage
- Shift-left security (catch issues early)
- Automated compliance verification
- Clear audit trail
Negative
- Increased CI/CD duration (~10-15 min added)
- False positive management overhead
- Tool maintenance burden
Risks
- Alert fatigue from false positives
- Tool updates may break pipelines
- Authenticated DAST requires credential management
Metrics
| Metric | Target | Current |
|---|---|---|
| SAST Coverage | 100% of code | ~90% |
| DAST Coverage | All endpoints | 0% (not running) |
| Time to Remediate (Critical) | < 24 hours | Unknown |
| Time to Remediate (High) | < 7 days | Unknown |
| False Positive Rate | < 10% | Unknown |
| Secret Leak Prevention | 100% | ~80% (no pre-commit) |
LLM Council Review
Review Date: 2026-01-18 Consensus: 0.88 (Strong Agreement)
Key Changes Based on Council Feedback
-
Tool Consolidation:
- Bandit deprecated → Semgrep sufficient for Python SAST
- TruffleHog deprecated → Gitleaks sufficient for secrets scanning
- Reduces maintenance burden and conflicting findings
- Single source of truth for each vulnerability category
-
DAST Blocking Mode Changed to Warn:
- Initial rollout in warn-only mode
- High false positive rate in early stages causes alert fatigue
- 2-4 week tuning period before considering blocking mode
- Service account isolation prevents accidental privilege escalation
-
Business Logic Testing (IDOR) Added:
- Automated scanners miss authorization bypass vulnerabilities
- Dedicated E2E test suite in
e2e/security/ - Tests cross-user access for habits, family members, achievements
- Runs as part of T1 regression suite (blocking)
-
Service Account for Authenticated DAST:
- Dedicated least-privilege account (
zap-scanner@habit-hub.iam) - Read-only for most resources
- Credentials in AWS Secrets Manager with monthly rotation
- Prevents tests from having excessive permissions
- Dedicated least-privilege account (
-
Security Logging Verification (future consideration):
- Council noted need for security event logging verification
- Ensure auth failures, permission denials are logged
- Deferred to ADR-013 (Observability Strategy)
Council Dissent (Minority Opinion)
- One model suggested keeping TruffleHog for verified secret detection
- Majority view: Gitleaks regex patterns sufficient, TruffleHog API verification adds latency
References
- ADR-010: Regression Testing Strategy
- ADR-011: DevOps Automation Pipeline
- OWASP Testing Guide
- NIST Secure Software Development Framework
- CIS Kubernetes Benchmark
- Security Audit Workflow
- CI Build Test Workflow
- Gitleaks Configuration
- Trivy Exceptions
- Trivy Exception Checker
- Trivy Documentation
- OWASP ZAP Documentation