Skip to main content

ADR-012: Security Scanning Strategy

Status

Implemented - Reviewed by LLM Council (2026-01-18), Implementation completed (2026-01-19)

Implementation Status

ComponentStatusImplementation
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✅ Implementede2e/security/idor.spec.ts
Auth Bypass Tests✅ Implementede2e/security/auth-bypass.spec.ts
Gitleaks Blocking✅ ImplementedIssue #37 - CI fails on secret detection
SCA Blocking (HIGH+)✅ ImplementedIssue #39 - CI fails on HIGH/CRITICAL vulns
Trivy Exception Mgmt✅ ImplementedIssue #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

CategoryToolImplementationLocationStatusCouncil Notes
SASTSemgrepMulti-ruleset scanningsecurity-audit.yml✅ ImplementedPrimary SAST
SASTBanditPython-specific analysissecurity-audit.yml⏸️ DeprecatedRemove (Semgrep sufficient)
SCASafetyPython dependenciessecurity-audit.yml✅ Implemented
SCAnpm auditJS dependenciessecurity-audit.yml✅ Implemented
ContainerTrivyImage vulnerability scansecurity-audit.yml✅ Implemented
SecretsGitleaksSecret detectionsecurity-audit.yml✅ ImplementedPrimary for CI
SecretsTruffleHogVerified secretssecurity-audit.yml⏸️ DeprecatedRemove (Gitleaks sufficient)
IaCCheckovK8s/Terraform/Dockersecurity-audit.yml✅ Implemented
DASTOWASP ZAPDynamic scanningsecurity-audit.yml⚠️ PartialStart 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

IssueImpactPriority
No .zap/ directoryZAP rules not configuredHigh
Placeholder target URLScans don't run against real environmentCritical
Only scheduled runsNo DAST on deploymentHigh
No authenticated scanningCan't test protected endpointsMedium
No API-specific scanningREST/GraphQL untestedMedium

SAST Gaps

IssueImpactPriority
Not in UAT pipelineOnly runs weeklyHigh
No custom rulesMissing business logic checksMedium
No incremental scanningFull scan on every runLow

SCA Gaps

IssueImpactPriority
No license complianceLegal risk untrackedMedium
No SBOM generationSupply chain visibilityMedium
No vulnerability prioritizationAll CVEs treated equallyLow

Container Security Gaps

IssueImpactPriority
No base image pinning verificationDrift riskMedium
No runtime securityOnly build-time scanningMedium
No signed image verificationSupply chain riskLow

Secrets Scanning Gaps

IssueImpactPriority
No pre-commit hooksSecrets can be committedHigh
No rotation verificationStale secrets undetectedMedium

IaC Security Gaps

IssueImpactPriority
Terraform modules incompleteNot all IaC scannedMedium
No policy-as-code enforcementCompliance driftMedium

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

StageTriggerBlockingTools
PROn open/updateYes (HIGH/CRITICAL)Semgrep only (Bandit deprecated)
UAT DeployPre-deployYes (CRITICAL only)Semgrep
Post-DeployAfter UAT deployWarn only (initially)OWASP ZAP (baseline)
WeeklyScheduledNo (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 TypeTriggerDurationBlocking
Pre-commit (Gitleaks)Every commit< 5sYes
SAST (PR)PR open/update~3 minYes (HIGH+)
SCA (PR)PR open/update~2 minYes (HIGH+)
Container (Build)Image build~5 minYes (CRITICAL)
SAST (UAT)Pre-deploy~3 minYes (CRITICAL)
DAST BaselinePost-deploy~5 minWarn only (initially)
DAST APIPost-deploy~15 minWarn only
IDOR TestsT1 regression~5 minYes
Full ScanWeekly~60 minReport 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)

  1. Remove Bandit from security-audit.yml (Semgrep sufficient)
  2. Remove TruffleHog from security-audit.yml (Gitleaks sufficient)
  3. Create .zap/ directory with configuration
  4. Create dedicated service account for authenticated DAST
  5. Configure DAST in warn-only mode

Phase 2: Pipeline Integration (Week 2)

  1. Add SAST (Semgrep only) to UAT pipeline
  2. Configure Gitleaks pre-commit hooks
  3. Set up SBOM generation
  4. Create IDOR test suite in e2e/security/

Phase 3: Advanced Features (Week 3-4)

  1. Implement image signing
  2. Configure OPA/Gatekeeper policies
  3. Set up unified security dashboard
  4. Review DAST findings and tune rules
  5. 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

MetricTargetCurrent
SAST Coverage100% of code~90%
DAST CoverageAll endpoints0% (not running)
Time to Remediate (Critical)< 24 hoursUnknown
Time to Remediate (High)< 7 daysUnknown
False Positive Rate< 10%Unknown
Secret Leak Prevention100%~80% (no pre-commit)

LLM Council Review

Review Date: 2026-01-18 Consensus: 0.88 (Strong Agreement)

Key Changes Based on Council Feedback

  1. 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
  2. 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
  3. 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)
  4. 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
  5. 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