← Blog

"AppSec Series #10: IaC Security, SAST, DAST, SCA and Secrets Management"

From Terraform scanning to SAST in CI/CD pipelines, DAST with OWASP ZAP, dependency scanning, and zero hardcoded secrets — the complete automated security testing toolkit.

reading now
views
comments

Series Navigation

Part 9: Container Security

Part 11: DevSecOps — Security in CI/CD Pipelines


Infrastructure as Code Security

IaC files (Terraform, CloudFormation, Pulumi) define your cloud infrastructure. A vulnerability in an IaC file becomes a vulnerability in production.

# ❌ Insecure Terraform — multiple issues
resource "aws_s3_bucket" "data" {
  bucket = "company-customer-data"
  acl    = "public-read"           # PUBLIC!
}

resource "aws_db_instance" "postgres" {
  identifier        = "prod-db"
  engine            = "postgres"
  publicly_accessible = true      # exposed to internet
  storage_encrypted = false       # no encryption at rest
  deletion_protection = false     # can be deleted accidentally
  password          = "Password1!"  # hardcoded credential
}

resource "aws_security_group" "app" {
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]   # SSH from anywhere!
  }
}

# ✅ Secure Terraform
resource "aws_s3_bucket" "data" {
  bucket = "company-customer-data"
}

resource "aws_s3_bucket_public_access_block" "data" {
  bucket                  = aws_s3_bucket.data.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
  bucket = aws_s3_bucket.data.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3_key.arn
    }
  }
}

resource "aws_db_instance" "postgres" {
  identifier          = "prod-db"
  engine              = "postgres"
  publicly_accessible = false
  storage_encrypted   = true
  kms_key_id          = aws_kms_key.rds_key.arn
  deletion_protection = true
  password            = data.aws_secretsmanager_secret_version.db_password.secret_string
  # password from Secrets Manager, never hardcoded
}

IaC Security Scanning

# Checkov — comprehensive IaC scanner
pip install checkov

# Scan all Terraform files
checkov -d ./infrastructure/terraform/ --framework terraform

# Scan CloudFormation
checkov -f template.yaml --framework cloudformation

# Only check specific rules
checkov -d . --check CKV_AWS_18,CKV_AWS_19,CKV_AWS_20

# Generate SARIF output (for GitHub Code Scanning)
checkov -d . --output sarif --output-file checkov-results.sarif

# tfsec — Terraform-specific scanner
brew install tfsec
tfsec ./terraform/ --format json

# terrascan
terrascan scan -t aws -i terraform

# KICS (Keeps Infrastructure as Code Secure) — multi-platform
docker run -v $(pwd):/path checkmarx/kics scan -p /path -o /path/results
# GitHub Actions — IaC security in PR checks
name: IaC Security Scan

on: [pull_request]

jobs:
  checkov:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: infrastructure/
          framework: terraform
          output_format: sarif
          output_file_path: results.sarif

      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: results.sarif

SAST — Static Application Security Testing

SAST analyses source code without running it, finding vulnerabilities during development.

# Semgrep — rule-based SAST (free, fast, accurate)
pip install semgrep

# Run OWASP Top 10 rules
semgrep --config "p/owasp-top-ten" ./src/

# Run Python security rules
semgrep --config "p/python" ./src/

# Run JavaScript/Node rules
semgrep --config "p/nodejs" ./src/

# Custom rules
cat > sql-injection.yaml << 'EOF'
rules:
  - id: sql-injection
    pattern: |
      db.execute($QUERY.format(...))
    message: "Possible SQL injection — use parameterised queries"
    languages: [python]
    severity: ERROR
EOF
semgrep --config sql-injection.yaml ./src/

# Bandit — Python security scanner
pip install bandit
bandit -r ./src/ -ll  # only medium and high severity
bandit -r ./src/ -f json -o bandit-report.json

# ESLint security plugin — JavaScript
npm install eslint-plugin-security
# .eslintrc.json
{
  "plugins": ["security"],
  "extends": ["plugin:security/recommended"]
}
npx eslint ./src/ --ext .js,.ts

# SpotBugs + FindSecBugs — Java
mvn spotbugs:spotbugs

Writing Custom Semgrep Rules

# custom-rules/no-hardcoded-secrets.yaml
rules:
  - id: hardcoded-api-key
    patterns:
      - pattern: $KEY = "..."
      - metavariable-regex:
          metavariable: $KEY
          regex: '.*(api_key|secret|password|token|credential).*'
      - metavariable-regex:
          metavariable: "..."
          regex: '[A-Za-z0-9]{20,}'  # long random string
    message: "Potential hardcoded secret in variable $KEY"
    severity: ERROR
    languages: [python, javascript, java]

  - id: sql-string-format
    pattern: |
      cursor.execute("..." % ...)
    message: "SQL injection: use parameterised queries, not string formatting"
    severity: ERROR
    languages: [python]

  - id: unsafe-yaml-load
    pattern: yaml.load(...)
    fix: yaml.safe_load(...)
    message: "yaml.load() is unsafe — use yaml.safe_load()"
    severity: WARNING
    languages: [python]

DAST — Dynamic Application Security Testing

DAST tests the running application, finding vulnerabilities that only appear at runtime.

# OWASP ZAP — most popular open-source DAST tool
docker pull owasp/zap2docker-stable

# Baseline scan (passive — doesn't attack)
docker run -v $(pwd):/zap/wrk owasp/zap2docker-stable \
  zap-baseline.py \
  -t https://staging.your-app.com \
  -r baseline-report.html \
  -I  # don't fail on warn

# Full scan (active — attacks the app)
docker run -v $(pwd):/zap/wrk owasp/zap2docker-stable \
  zap-full-scan.py \
  -t https://staging.your-app.com \
  -r full-scan-report.html \
  -J full-scan-report.json

# API scan (uses OpenAPI/Swagger spec)
docker run -v $(pwd):/zap/wrk owasp/zap2docker-stable \
  zap-api-scan.py \
  -t https://api.staging.your-app.com/swagger.json \
  -f openapi \
  -r api-scan-report.html
# Automated ZAP scan via Python API
from zapv2 import ZAPv2
import time

zap = ZAPv2(proxies={'http': 'http://localhost:8080', 'https': 'http://localhost:8080'})

target = 'https://staging.your-app.com'

# Spider the application
print("Spidering...")
scan_id = zap.spider.scan(target)
while int(zap.spider.status(scan_id)) < 100:
    time.sleep(2)

# Active scan
print("Active scanning...")
scan_id = zap.ascan.scan(target)
while int(zap.ascan.status(scan_id)) < 100:
    print(f"Progress: {zap.ascan.status(scan_id)}%")
    time.sleep(5)

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

if high_alerts:
    print(f"FAILED: {len(high_alerts)} high/critical vulnerabilities found")
    for alert in high_alerts:
        print(f"  [{alert['risk']}] {alert['name']}: {alert['url']}")
    exit(1)

print("PASSED: No high/critical vulnerabilities found")

SCA — Software Composition Analysis

# Python — check all dependencies
pip install pip-audit
pip-audit

# Safety
pip install safety
safety check

# Node.js
npm audit
npm audit --json | python3 -c "
import json,sys
data = json.load(sys.stdin)
critical = [v for v in data.get('vulnerabilities',{}).values() if v['severity']=='critical']
print(f'Critical: {len(critical)}')
for v in critical: print(f'  {v[\"name\"]}: {v[\"title\"]}')
"

# Java
mvn dependency-check:check  # OWASP Dependency Check

# Container images (checks OS packages too)
trivy image your-app:latest --severity HIGH,CRITICAL

# Snyk (multi-language, SaaS)
npm install -g snyk
snyk auth
snyk test --all-projects
snyk monitor  # continuous monitoring
snyk container test your-app:latest

# Create a SBOM (Software Bill of Materials) — required for some compliance frameworks
syft your-app:latest -o spdx-json > sbom.json
grype sbom:sbom.json  # scan SBOM for vulnerabilities

Secrets Management

Never Hardcode Secrets

# Detect secrets in git history
pip install truffleHog
trufflehog git https://github.com/your-org/your-repo

# Or in local repo
trufflehog git file://./

# detect-secrets — pre-commit hook
pip install detect-secrets
detect-secrets scan > .secrets.baseline
# Add to pre-commit:
pre-commit install
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Yelp/detect-secrets
  rev: v1.4.0
  hooks:
  - id: detect-secrets
    args: ['--baseline', '.secrets.baseline']

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

AWS Secrets Manager

import boto3
import json
from functools import lru_cache

secretsmanager = boto3.client('secretsmanager', region_name='us-east-1')

@lru_cache(maxsize=None)
def get_secret(secret_name: str) -> dict:
    """Get secret — cached in memory to reduce API calls"""
    response = secretsmanager.get_secret_value(SecretId=secret_name)
    return json.loads(response['SecretString'])

# Usage — no secrets in code at all
db_config = get_secret('prod/app/database')
DATABASE_URL = f"postgresql://{db_config['username']}:{db_config['password']}@{db_config['host']}/{db_config['dbname']}"

stripe_config = get_secret('prod/app/stripe')
STRIPE_API_KEY = stripe_config['api_key']

HashiCorp Vault

import hvac

vault_client = hvac.Client(
    url='https://vault.your-company.com:8200',
    token=os.environ['VAULT_TOKEN']  # short-lived token from Vault auth
)

# Read secret
secret = vault_client.secrets.kv.read_secret_version(
    path='app/database',
    mount_point='secret'
)
db_password = secret['data']['data']['password']

# Dynamic database credentials — Vault generates short-lived creds
# No static database passwords at all
db_creds = vault_client.read('database/creds/app-role')
# Returns: {'username': 'v-token-XYZ', 'password': 'A1b2C3...', 'ttl': 3600}
# Credentials expire after TTL — no long-lived DB passwords

Interview Questions

Q: What is the difference between SAST, DAST, and IAST?

SAST (Static) analyses source code without running it — fast, runs in CI, finds code-level issues but can't find runtime vulnerabilities. DAST (Dynamic) tests the running application — finds real runtime vulnerabilities, no source code needed, but slower and requires a running environment. IAST (Interactive) instruments the application during testing — combines benefits of both but requires agent installation and active test execution. In practice: SAST early in pipeline, DAST in staging, dependency scanning continuously.

Q: A developer wants to store a database password in a Kubernetes secret. Is this secure enough?

Kubernetes secrets are only base64 encoded by default — not encrypted. Anyone with kubectl get secret permission can read them. To make them secure: enable encryption at rest in etcd (AES-CBC or KMS provider), use external secret operators (External Secrets Operator with AWS Secrets Manager or Vault), restrict RBAC so only the app's service account can read the specific secret, and audit secret access via audit logs. The best approach is using an external secret manager and injecting credentials as files at runtime.


What's Next

In Part 11 we build the DevSecOps pipeline — integrating all these security tools (SAST, DAST, SCA, secret scanning, IaC scanning) into a single CI/CD pipeline that prevents vulnerabilities from reaching production.

Discussion

Loading...

Leave a Comment

All comments are reviewed before appearing. No links please.

0 / 1000