Loading...
Loading...
Expert DevSecOps engineer specializing in secure CI/CD pipelines, shift-left security, security automation, and compliance as code. Use when implementing security gates, container security, infrastructure scanning, secrets management, or building secure supply chains.
npx skill4agent add martinholovsky/claude-skills-generator devsecops-expert# tests/security/test-pipeline-gates.yml
name: Test Security Gates
on: [push]
jobs:
test-sast-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Test 1: SAST should catch SQL injection
- name: Create vulnerable test file
run: |
mkdir -p test-vulnerable
cat > test-vulnerable/vuln.py << 'EOF'
def query(user_input):
return f"SELECT * FROM users WHERE id = {user_input}" # SQL injection
EOF
- name: Run SAST - should fail
id: sast
continue-on-error: true
run: |
semgrep --config p/security-audit test-vulnerable/ --error
- name: Verify SAST caught vulnerability
run: |
if [ "${{ steps.sast.outcome }}" == "success" ]; then
echo "ERROR: SAST should have caught SQL injection!"
exit 1
fi
echo "SAST correctly identified vulnerability"
test-secret-detection:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Test 2: Secret scanner should catch hardcoded secrets
- name: Create file with test secret
run: |
mkdir -p test-secrets
echo 'API_KEY = "AKIAIOSFODNN7EXAMPLE"' > test-secrets/config.py
- name: Run secret scanner - should fail
id: secrets
continue-on-error: true
run: |
trufflehog filesystem test-secrets/ --fail --json
- name: Verify secret was detected
run: |
if [ "${{ steps.secrets.outcome }}" == "success" ]; then
echo "ERROR: Secret scanner should have caught hardcoded key!"
exit 1
fi
echo "Secret scanner correctly identified hardcoded credential"# .github/workflows/security-gates.yml
name: Security Gates
on:
pull_request:
branches: [main]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep SAST
uses: semgrep/semgrep-action@v1
with:
config: p/security-audit
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scan for secrets
uses: trufflesecurity/trufflehog@v3.63.0
with:
extra_args: --fail# Add container scanning after basic gates work
container-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t app:test .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@0.16.1
with:
image-ref: app:test
severity: 'CRITICAL,HIGH'
exit-code: '1'# Verify all security gates
echo "Running security verification..."
# 1. Test SAST detection
semgrep --test tests/security/rules/
# 2. Verify container scan catches CVEs
trivy image --severity HIGH,CRITICAL --exit-code 1 app:test
# 3. Check IaC policies
conftest test terraform/ --policy policies/
# 4. Verify secret scanner
trufflehog filesystem . --fail
# 5. Run integration tests
pytest tests/security/ -v
echo "All security gates verified!"# ❌ Scans entire codebase every time (slow)
sast:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history
- run: semgrep --config auto . # Scans everything# ✅ Incremental scan of changed files only
sast:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Current + parent only
- name: Get changed files
id: changed
run: |
echo "files=$(git diff --name-only HEAD~1 | grep -E '\.(py|js|ts)$' | tr '\n' ' ')" >> $GITHUB_OUTPUT
- name: Scan changed files only
if: steps.changed.outputs.files != ''
run: semgrep --config auto ${{ steps.changed.outputs.files }}# ❌ Each job waits for previous (slow)
jobs:
sast:
runs-on: ubuntu-latest
sca:
needs: sast # Waits for SAST
container:
needs: sca # Waits for SCA# ✅ All scans run simultaneously
jobs:
sast:
runs-on: ubuntu-latest
steps:
- run: semgrep --config auto
sca:
runs-on: ubuntu-latest # No dependency - runs in parallel
steps:
- run: npm audit
container:
runs-on: ubuntu-latest # No dependency - runs in parallel
steps:
- run: trivy image app:test
# Only deploy needs all gates
deploy:
needs: [sast, sca, container]# ❌ Downloads vulnerability DB on every run
container-scan:
steps:
- name: Scan image
run: trivy image app:test # Downloads DB each time# ✅ Cache Trivy DB between runs
container-scan:
steps:
- name: Cache Trivy DB
uses: actions/cache@v4
with:
path: ~/.cache/trivy
key: trivy-db-${{ github.run_id }}
restore-keys: trivy-db-
- name: Scan image
run: trivy image --cache-dir ~/.cache/trivy app:test# ❌ Full IaC scan even for non-IaC changes
iac-scan:
steps:
- run: checkov --directory terraform/ # Always runs full scan# ✅ Only scan when relevant files change
iac-scan:
if: |
contains(github.event.pull_request.changed_files, 'terraform/') ||
contains(github.event.pull_request.changed_files, 'k8s/')
steps:
- name: Get changed IaC files
id: iac-changes
run: |
CHANGED=$(git diff --name-only origin/main | grep -E '^(terraform|k8s)/')
echo "files=$CHANGED" >> $GITHUB_OUTPUT
- name: Scan changed IaC only
run: checkov --file ${{ steps.iac-changes.outputs.files }}# ❌ No layer caching
build:
steps:
- run: docker build -t app .# ✅ Cache layers for faster builds
build:
steps:
- uses: docker/setup-buildx-action@v3
- name: Build with cache
uses: docker/build-push-action@v5
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
tags: app:${{ github.sha }}# .github/workflows/security-pipeline.yml
name: Security Pipeline
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
contents: read
security-events: write
jobs:
# Gate 1: Secret Scanning
secret-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Scan for secrets
uses: trufflesecurity/trufflehog@v3.63.0
with:
path: ./
extra_args: --fail --json
# Gate 2: SAST
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: semgrep/semgrep-action@v1
with:
config: p/security-audit
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
# Gate 3: SCA
sca:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Dependency Review
uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
# Gate 4: Container Scanning
container-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t app:${{ github.sha }} .
- name: Scan with Trivy
uses: aquasecurity/trivy-action@0.16.1
with:
image-ref: app:${{ github.sha }}
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Generate SBOM
uses: anchore/sbom-action@v0.15.0
with:
image: app:${{ github.sha }}
format: spdx-json
# Gate 5: Sign and Attest
sign-attest:
needs: [secret-scan, sast, sca, container-scan]
if: github.ref == 'refs/heads/main'
permissions:
id-token: write
packages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- name: Sign image
run: cosign sign --yes ghcr.io/${{ github.repository }}:${{ github.sha }}# policies/kubernetes/pod-security.rego
package kubernetes.admission
# Deny privileged containers
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
container.securityContext.privileged
msg := sprintf("Privileged container not allowed: %v", [container.name])
}
# Require non-root user
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := sprintf("Container must run as non-root: %v", [container.name])
}
# Require read-only root filesystem
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.securityContext.readOnlyRootFilesystem
msg := sprintf("Read-only filesystem required: %v", [container.name])
}
# Deny host namespaces
deny[msg] {
input.request.kind.kind == "Pod"
input.request.object.spec.hostNetwork
msg := "Host network not allowed"
}
# Require resource limits
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Memory limit required: %v", [container.name])
}# Test policies in CI
conftest test k8s-manifests/ --policy policies/# k8s/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: production
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "app-role"
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
target:
name: app-secrets
template:
data:
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@db:5432/app"
data:
- secretKey: username
remoteRef:
key: app/database
property: username
- secretKey: password
remoteRef:
key: app/database
property: password# Dockerfile - Multi-stage with security hardening
FROM node:20-alpine AS builder
RUN apk update && apk upgrade && apk add --no-cache dumb-init
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
# Distroless runtime
FROM gcr.io/distroless/nodejs20-debian12:nonroot
COPY /usr/bin/dumb-init /usr/bin/dumb-init
COPY /app /app
WORKDIR /app
USER nonroot
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["node", "server.js"]# k8s/pod-security.yaml
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65534
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
serviceAccountName: app-sa
automountServiceAccountToken: false
containers:
- name: app
image: ghcr.io/example/app:v1.0.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop: [ALL]
resources:
limits:
memory: "256Mi"
cpu: "500m"
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir:
sizeLimit: 100Mi# .gitlab-ci.yml
stages:
- validate
- security-scan
terraform-validate:
stage: validate
image: hashicorp/terraform:1.6.6
script:
- terraform init -backend=false
- terraform validate
- terraform fmt -check
checkov-scan:
stage: security-scan
image: bridgecrew/checkov:latest
script:
- checkov --directory terraform/ \
--framework terraform \
--output cli \
--hard-fail-on HIGH,CRITICAL
- checkov --directory k8s/ \
--framework kubernetes \
--hard-fail-on HIGH,CRITICAL
tfsec-scan:
stage: security-scan
image: aquasec/tfsec:latest
script:
- tfsec terraform/ \
--minimum-severity HIGH \
--soft-fail false# .github/workflows/slsa-provenance.yml
name: SLSA3 Build
on:
push:
tags: ['v*']
permissions: read-all
jobs:
build:
permissions:
id-token: write
packages: write
outputs:
digest: ${{ steps.build.outputs.digest }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate SBOM
uses: anchore/sbom-action@v0.15.0
with:
format: spdx-json
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
provenance: true
sbom: true
provenance:
needs: [build]
permissions:
id-token: write
actions: read
packages: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
with:
image: ghcr.io/${{ github.repository }}
digest: ${{ needs.build.outputs.digest }}# kyverno/verify-images.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
annotations:
policies.kyverno.io/category: Supply Chain Security
policies.kyverno.io/severity: critical
spec:
validationFailureAction: Enforce
background: false
rules:
- name: verify-signature
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "ghcr.io/example/*"
attestors:
- count: 1
entries:
- keyless:
subject: "https://github.com/example/*"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-security-context
spec:
validationFailureAction: Enforce
rules:
- name: non-root-required
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Containers must run as non-root"
pattern:
spec:
securityContext:
runAsNonRoot: true
containers:
- securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]| Level | Requirements | Implementation |
|---|---|---|
| L1 | Document build process | Generate provenance, make available |
| L2 | Tamper resistance | Version control, hosted build, authenticated provenance |
| L3 | Extra resistance | Non-falsifiable provenance, no secrets in build |
| L4 | Highest assurance | Two-person review, hermetic builds, recursive SLSA |
# ❌ DANGER
apiVersion: v1
kind: Secret
stringData:
password: SuperSecret123!# ✅ External secret store
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
secretStoreRef:
name: vault-backend
data:
- secretKey: password
remoteRef:
key: app/database# ❌ DANGER
FROM node:20
COPY . .
CMD ["node", "server.js"]# ✅ Non-root user
FROM node:20-alpine
RUN adduser -S nodejs -u 1001
USER nodejs
CMD ["node", "server.js"]# ❌ DANGER: Deploy without scanning
jobs:
deploy:
steps:
- run: docker build -t app .
- run: docker push app# ✅ Security gates block insecure code
jobs:
security:
steps:
- run: semgrep --error
- run: trivy image --severity HIGH,CRITICAL --exit-code 1
deploy:
needs: security# ❌ No verification
kubectl run app --image=ghcr.io/example/app:latest# ✅ Kyverno verifies signatures
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-images
spec:
validationFailureAction: Enforce
rules:
- name: verify-signature
verifyImages:
- imageReferences: ["ghcr.io/example/*"]
attestors:
- entries:
- keyless:
issuer: "https://token.actions.githubusercontent.com"# ❌ Cluster admin for app
kind: ClusterRoleBinding
roleRef:
name: cluster-admin
subjects:
- kind: ServiceAccount
name: app-sa# ✅ Minimal namespace-scoped permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
---
kind: RoleBinding
roleRef:
name: app-role
subjects:
- kind: ServiceAccount
name: app-sa# tests/security/test_gates.yml
name: Security Gate Tests
on: [push]
jobs:
test-gates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Test that SAST catches known vulnerabilities
- name: Test SAST detection
run: |
# Create test vulnerable file
echo 'eval(user_input)' > test.py
semgrep --config p/security-audit test.py --error && exit 1 || echo "SAST working"
rm test.py
# Test that secret scanner catches secrets
- name: Test secret detection
run: |
echo 'AWS_KEY=AKIAIOSFODNN7EXAMPLE' > test.env
trufflehog filesystem . --fail && exit 1 || echo "Secret scanner working"
rm test.env# Test OPA policies
conftest verify policies/
# Test specific policy
conftest test k8s-manifests/pod.yaml --policy policies/pod-security.rego
# Generate test cases
conftest fmt policies/# Test container builds correctly
docker build -t app:test .
# Test non-root user
docker run --rm app:test id | grep -v "uid=0" || exit 1
# Test read-only filesystem (should fail to write)
docker run --rm app:test touch /test 2>&1 | grep -i "read-only" || exit 1
# Test image scanning catches CVEs
trivy image --severity CRITICAL --exit-code 1 app:test# tests/security/test_pipeline_integration.py
import pytest
import subprocess
def test_sast_blocks_vulnerable_code():
"""SAST gate should block code with SQL injection"""
result = subprocess.run(
["semgrep", "--config", "p/security-audit", "tests/fixtures/vulnerable/"],
capture_output=True
)
assert result.returncode != 0, "SAST should detect vulnerabilities"
def test_secret_scanner_detects_hardcoded_secrets():
"""Secret scanner should detect hardcoded credentials"""
result = subprocess.run(
["trufflehog", "filesystem", "tests/fixtures/secrets/", "--fail"],
capture_output=True
)
assert result.returncode != 0, "Secret scanner should detect secrets"
def test_container_scan_detects_cves():
"""Container scanner should detect high/critical CVEs"""
result = subprocess.run(
["trivy", "image", "--severity", "HIGH,CRITICAL", "--exit-code", "1", "vulnerable-image:test"],
capture_output=True
)
assert result.returncode != 0, "Trivy should detect CVEs"