Vulnerability Scanning
Continuous vulnerability scanning identifies security weaknesses in code, containers, and infrastructure before they can be exploited. A defense-in-depth scanning strategy combines SAST, DAST, SCA, and container scanning at every stage of the software lifecycle.
Vulnerability Scanning Types
| Type | Full Name | Scan Target | When | Depth |
|---|---|---|---|---|
| SAST | Static Application Security Testing | Source code | Build | Code-level vulnerabilities, injection flaws, insecure patterns |
| DAST | Dynamic Application Security Testing | Running application | Test/Stage | Runtime vulnerabilities, auth bypass, misconfigurations |
| SCA | Software Composition Analysis | Dependencies, libraries | Build | Known CVEs, license compliance, outdated packages |
| Container | Container Image Scanning | Container images | Build/Registry | OS CVEs, application CVEs, misconfigurations |
| IaC | Infrastructure as Code | Terraform, CloudFormation | Build | Cloud misconfigurations, policy violations |
| Secrets | Secrets Detection | All files | Pre-commit + CI | Hardcoded credentials, tokens, keys |
SonarQube Setup and Integration
SonarQube provides comprehensive SAST capabilities with quality gates that enforce security standards across all code changes.
Docker Compose Deployment
# docker-compose.sonarqube.yml
version: "3.8"
services:
sonarqube:
image: sonarqube:10-community
container_name: sonarqube
depends_on:
- postgres
environment:
SONAR_JDBC_URL: jdbc:postgresql://postgres:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: ${SONAR_DB_PASSWORD}
SONAR_WEB_JAVAOPTS: "-Xmx2g -Xms1g"
SONAR_CE_JAVAOPTS: "-Xmx2g -Xms512m"
SONAR_SEARCH_JAVAOPTS: "-Xmx2g -Xms2g"
# Security hardening
SONAR_SECURITY_REALM: LDAP
SONAR_LDAP_URL: ldaps://ldap.internal:636
SONAR_AUTHENTICATION_FORCEAUTHENTICATION: "true"
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
ports:
- "9000:9000"
ulimits:
nofile:
soft: 65536
hard: 65536
networks:
- sonarqube
postgres:
image: postgres:16-alpine
container_name: sonarqube-db
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: ${SONAR_DB_PASSWORD}
POSTGRES_DB: sonar
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- sonarqube
# Security settings
command:
- "postgres"
- "-c"
- "ssl=on"
- "-c"
- "ssl_cert_file=/etc/ssl/certs/server.crt"
- "-c"
- "ssl_key_file=/etc/ssl/private/server.key"
# SonarQube scanner for CI/CD
scanner-cli:
image: sonarsource/sonar-scanner-cli:latest
profiles: ["scanner"]
environment:
SONAR_HOST_URL: "http://sonarqube:9000"
SONAR_TOKEN: ${SONAR_TOKEN}
volumes:
- ./src:/usr/src:ro
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
postgres_data:
networks:
sonarqube:
driver: bridge
Quality Gates Configuration
#!/bin/bash
# configure-quality-gate.sh โ SonarQube quality gate setup
SONAR_URL="http://sonarqube:9000"
SONAR_TOKEN="${SONAR_ADMIN_TOKEN}"
# Create production security-focused quality gate
curl -u "$SONAR_TOKEN:" -X POST "$SONAR_URL/api/qualitygates/create" \
-d "name=Production-Security-Gate"
# Set conditions
curl -u "$SONAR_TOKEN:" -X POST "$SONAR_URL/api/qualitygates/create_condition" \
-d "gateName=Production-Security-Gate" \
-d "metric=new_security_rating" \
-d "op=GT" \
-d "error=1" # A rating only
curl -u "$SONAR_TOKEN:" -X POST "$SONAR_URL/api/qualitygates/create_condition" \
-d "gateName=Production-Security-Gate" \
-d "metric=new_vulnerabilities" \
-d "op=GT" \
-d "error=0" # Zero new vulnerabilities
curl -u "$SONAR_TOKEN:" -X POST "$SONAR_URL/api/qualitygates/create_condition" \
-d "gateName=Production-Security-Gate" \
-d "metric=new_hotspots_reviewed" \
-d "op=LT" \
-d "error=100" # 100% of hotspots reviewed
curl -u "$SONAR_TOKEN:" -X POST "$SONAR_URL/api/qualitygates/create_condition" \
-d "gateName=Production-Security-Gate" \
-d "metric=new_coverage" \
-d "op=LT" \
-d "error=80" # 80% minimum coverage
curl -u "$SONAR_TOKEN:" -X POST "$SONAR_URL/api/qualitygates/create_condition" \
-d "gateName=Production-Security-Gate" \
-d "metric=new_duplicated_lines_density" \
-d "op=GT" \
-d "error=3" # Max 3% duplication
# Set as default
curl -u "$SONAR_TOKEN:" -X POST "$SONAR_URL/api/qualitygates/set_as_default" \
-d "name=Production-Security-Gate"
echo "Production security quality gate configured"
CI/CD Integration with GitHub Actions
# .github/workflows/sonarqube-scan.yml
name: SonarQube Security Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
jobs:
sonarqube:
name: SonarQube Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for blame information
# Java/Node/Python setup as needed
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install Dependencies
run: npm ci
# Run tests with coverage
- name: Run Tests with Coverage
run: npm run test:coverage
# SonarQube scan
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
with:
args: >
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.javascript.lcov.reportPaths=./coverage/lcov.info
-Dsonar.coverage.exclusions=**/*.test.js,**/*.spec.js,**/tests/**
-Dsonar.exclusions=node_modules/**,coverage/**,dist/**
-Dsonar.qualitygate.wait=true
-Dsonar.qualitygate.timeout=300
# Check quality gate result
- name: Check Quality Gate
if: always()
run: |
if [ "${{ steps.sonarqube.outcome }}" == "failure" ]; then
echo "::error::SonarQube quality gate failed!"
echo "Check $SONAR_HOST_URL/dashboard?id=${{ github.repository_owner }}_${{ github.event.repository.name }}"
exit 1
fi
# sonar-project.properties
sonar.projectKey=company_api-gateway
sonar.organization=company
sonar.projectName=API Gateway
sonar.projectVersion=1.0
sonar.sources=src
sonar.tests=tests
sonar.language=js
# Security-specific settings
sonar.security.hotspots.perFile.max=10
sonar.javascript.node.maxspace=4096
# Coverage
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.coverage.minimum=80.0
# Exclusions
sonar.exclusions=node_modules/**,coverage/**,dist/**,**/*.test.js
sonar.test.inclusions=**/*.test.js,**/*.spec.js
AWS Inspector
AWS Inspector provides automated vulnerability scanning for EC2 instances and container images with native AWS integration.
# terraform/aws-inspector.tf
# Enable Inspector V2
resource "aws_inspector2_enabler" "main" {
account_ids = [data.aws_caller_identity.current.account_id]
resource_types = ["EC2", "ECR"]
}
# Inspector finding suppression rules (after risk assessment)
resource "aws_inspector2_suppression_rule" "false_positives" {
name = "approved-base-image-findings"
description = "Suppress findings from approved hardened base images"
filter {
ecr_image_repository {
comparison = "EQUALS"
value = "company/hardened-base"
}
}
filter {
severity {
comparison = "EQUALS"
value = "INFORMATIONAL"
}
}
}
# EventBridge rule for critical findings
resource "aws_cloudwatch_event_rule" "inspector_critical" {
name = "inspector-critical-findings"
description = "Trigger on critical Inspector findings"
event_pattern = jsonencode({
source = ["aws.inspector2"]
detail-type = ["Inspector2 Finding"]
detail = {
severity = ["CRITICAL", "HIGH"]
status = ["ACTIVE"]
}
})
}
resource "aws_cloudwatch_event_target" "sns_critical" {
rule = aws_cloudwatch_event_rule.inspector_critical.name
target_id = "sns"
arn = aws_sns_topic.security_alerts.arn
}
# SNS topic for security alerts
resource "aws_sns_topic" "security_alerts" {
name = "security-critical-findings"
}
resource "aws_sns_topic_subscription" "security_team" {
topic_arn = aws_sns_topic.security_alerts.arn
protocol = "email"
endpoint = "security@company.com"
}
Trivy for Container and Filesystem Scanning
Trivy by Aqua Security is the fastest and most comprehensive open-source scanner for containers, filesystems, and Git repositories.
# Dockerfile โ Multi-stage with Trivy scan
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine AS production
RUN apk add --no-cache dumb-init ca-certificates
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER node
EXPOSE 3000
CMD ["dumb-init", "node", "server.js"]
# .github/workflows/trivy-scan.yml
name: Trivy Security Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6 AM
jobs:
# Filesystem scan โ check source code dependencies
fs-scan:
name: Filesystem Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy filesystem scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-fs-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
ignore-unfixed: true
skip-dirs: 'node_modules,vendor'
- name: Upload scan results
if: always()
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-fs-results.sarif'
category: 'trivy-fs'
# Container image scan
image-scan:
name: Container Image Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t app:${{ github.sha }} .
- name: Run Trivy image scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'app:${{ github.sha }}'
format: 'sarif'
output: 'trivy-image-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
ignore-unfixed: true
vuln-type: 'os,library'
- name: Upload scan results
if: always()
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-image-results.sarif'
category: 'trivy-image'
# Configuration scan (Terraform, Kubernetes)
config-scan:
name: Configuration Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy config scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'config'
scan-ref: '.'
format: 'sarif'
output: 'trivy-config-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Upload scan results
if: always()
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-config-results.sarif'
category: 'trivy-config'
Trivy Ignore File
# .trivyignore โ Suppress findings after risk assessment
# Format: CVE-ID # comment with justification
# CVE-2023-XXXX โ False positive, not exploitable in our configuration
# We don't use the affected feature (HTTP/2 server push)
CVE-2023-XXXX
# CVE-2023-YYYY โ Accepted risk, mitigation in place
# Network segmentation prevents external access; scheduled for patching in Q2
CVE-2023-YYYY exp:2024-06-30
# Ignore all CVEs in a specific package with severity below HIGH
# After security team review
git:* CVE-* < HIGH
OpenVAS/Greenbone Setup Guide
# docker-compose.openvas.yml
version: "3.8"
services:
openvas:
image: immauss/openvas:latest
container_name: openvas
restart: always
environment:
PASSWORD: "${OPENVAS_ADMIN_PASSWORD}"
USERNAME: "admin"
RELAYHOST: "smtp.internal:25"
SMTPPORT: "25"
REDISDBS: "512"
# Automatic update schedule
AUTO_SYNC: "true"
NVT_SYNC_CRON: "0 2 * * *" # Daily at 2 AM
SCAP_SYNC_CRON: "0 3 * * 0" # Weekly Sunday 3 AM
volumes:
- openvas_data:/data
ports:
- "9392:9392" # Web UI
- "8443:8443" # HTTPS API
networks:
- openvas
# Greenbone Security Assistant
gsad:
image: immauss/gsa:latest
container_name: gsad
restart: always
environment:
GSAD_LISTEN: "0.0.0.0"
GSAD_PORT: "443"
GVMD_HOST: "openvas"
GVMD_PORT: "9390"
ports:
- "443:443"
depends_on:
- openvas
networks:
- openvas
volumes:
openvas_data:
networks:
openvas:
driver: bridge
#!/bin/bash
# openvas-automated-scan.sh โ Automated network vulnerability scanning
OPENVAS_URL="https://openvas.internal:9392"
USERNAME="admin"
PASSWORD="${OPENVAS_ADMIN_PASSWORD}"
# Authenticate and get token
TOKEN=$(curl -k -s -X POST "$OPENVAS_URL/gmp" \
-d "cmd=authenticate&login=$USERNAME&password=$PASSWORD" \
| grep -oP 'token="\K[^"]+')
# Create target
curl -k -s -X POST "$OPENVAS_URL/gmp" \
-d "cmd=create_target&token=$TOKEN" \
-d "name=production-network-scan" \
-d "hosts=10.0.1.0/24,10.0.2.0/24" \
-d "port_list_id=33d0cd82-57c6-11e1-8ed1-406186ea4fc5"
# Create task with Full and Fast scan config
curl -k -s -X POST "$OPENVAS_URL/gmp" \
-d "cmd=create_task&token=$TOKEN" \
-d "name=daily-production-scan" \
-d "target_id=TARGET_ID" \
-d "config_id=daba56c8-73ec-11df-a475-002264764cea" \
-d "scanner_id=08b69003-5fc2-4037-a479-93b440211c73"
# Start task
curl -k -s -X POST "$OPENVAS_URL/gmp" \
-d "cmd=start_task&token=$TOKEN" \
-d "task_id=TASK_ID"
# Export results (PDF + XML)
curl -k -s -X POST "$OPENVAS_URL/gmp" \
-d "cmd=get_reports&token=$TOKEN" \
-d "report_id=REPORT_ID" \
-d "report_format_id=c402cc3e-b531-11e1-9163-406186ea4fc5" \
-o "/reports/openvas-$(date +%Y%m%d).pdf"
Vulnerability Severity Classification (CVSS v3.1)
| Severity | CVSS Score | Description | Action Required |
|---|---|---|---|
| CRITICAL | 9.0 โ 10.0 | Remote code execution, full system compromise, wormable vulnerability | Immediate โ within 24 hours |
| HIGH | 7.0 โ 8.9 | Significant data exposure, privilege escalation, lateral movement possible | Urgent โ within 7 days |
| MEDIUM | 4.0 โ 6.9 | Limited impact exploitation, requires additional conditions | Planned โ within 30 days |
| LOW | 0.1 โ 3.9 | Minimal impact, difficult to exploit | Scheduled โ within 90 days |
| INFO | 0.0 | Informational finding, no direct security impact | Best effort โ track in backlog |
Remediation SLA Matrix
| Severity | Internet-Facing | Internal | Isolated/Dev | Exception Process |
|---|---|---|---|---|
| CRITICAL | 4 hours | 24 hours | 48 hours | CISO approval required |
| HIGH | 24 hours | 7 days | 14 days | Security lead approval |
| MEDIUM | 7 days | 30 days | 60 days | Team lead approval |
| LOW | 30 days | 90 days | 120 days | Self-approved with ticket |
SLA Clock Starts at Detection
The remediation SLA begins when the vulnerability is first detected, not when a ticket is created. Automated vulnerability scanners must integrate directly with ticketing systems to ensure immediate tracking.
False Positive Management
# vulnerability-management-process.yaml
false_positive_workflow:
identification:
- Scanner flags potential vulnerability
- Initial assessment by security champion
investigation:
steps:
- Verify vulnerability exists in deployed version
- Check if exploit conditions are present
- Review vendor security advisories
- Assess compensating controls
documentation_required:
- CVE/CWE identifier
- Detailed technical justification
- Compensating controls list
- Risk acceptance rationale
- Reviewer signatures
approval_hierarchy:
critical: "CISO + Legal"
high: "Security Lead + Engineering Director"
medium: "Security Champion + Team Lead"
low: "Self-documented with review"
expiration:
default: "90 days"
critical: "30 days"
review_trigger: "Vendor advisory update, architecture change"
audit_trail:
- All false positives logged in vulnerability tracker
- Quarterly review by external assessors
- Reported in compliance evidence packages
Vulnerability Dashboard and Reporting
# vulnerability_report.py โ Automated vulnerability reporting
import json
import boto3
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import List, Dict
import csv
@dataclass
class Vulnerability:
cve_id: str
severity: str
cvss_score: float
affected_hosts: List[str]
first_detected: datetime
remediated: bool
sla_hours: int
class VulnerabilityReporter:
"""Generate vulnerability reports from multiple scanners."""
def __init__(self):
self.inspector = boto3.client('inspector2')
self.security_hub = boto3.client('securityhub')
def get_open_findings(self) -> List[Vulnerability]:
"""Retrieve all active vulnerability findings."""
findings = []
paginator = self.inspector.get_paginator('list_findings')
for page in paginator.paginate(
filterCriteria={
'findingStatus': [{'comparison': 'EQUALS', 'value': 'ACTIVE'}]
}
):
for finding in page['findings']:
findings.append(Vulnerability(
cve_id=finding.get('cveId', 'N/A'),
severity=finding['severity'],
cvss_score=finding.get('cvssScore', 0.0),
affected_hosts=[r['id'] for r in finding.get('resources', [])],
first_detected=finding['firstObservedAt'],
remediated=False,
sla_hours=self._get_sla_hours(finding['severity'])
))
return findings
def generate_executive_summary(self, findings: List[Vulnerability]) -> Dict:
"""Generate executive dashboard metrics."""
total = len(findings)
by_severity = {}
overdue = 0
for f in findings:
by_severity[f.severity] = by_severity.get(f.severity, 0) + 1
if datetime.now() - f.first_detected > timedelta(hours=f.sla_hours):
overdue += 1
return {
'generated_at': datetime.utcnow().isoformat(),
'total_open_findings': total,
'by_severity': by_severity,
'overdue_findings': overdue,
'overdue_percentage': round(overdue / total * 100, 2) if total else 0,
'mean_time_to_remediate_hours': self._calculate_mttr(findings),
'sla_compliance_rate': round((total - overdue) / total * 100, 2) if total else 100
}
def export_csv(self, findings: List[Vulnerability], path: str):
"""Export findings to CSV for compliance evidence."""
with open(path, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
'CVE_ID', 'Severity', 'CVSS', 'Hosts',
'First Detected', 'SLA Hours', 'Overdue'
])
for finding in findings:
writer.writerow([
finding.cve_id,
finding.severity,
finding.cvss_score,
';'.join(finding.affected_hosts),
finding.first_detected.isoformat(),
finding.sla_hours,
'Yes' if datetime.now() - finding.first_detected >
timedelta(hours=finding.sla_hours) else 'No'
])
def _get_sla_hours(self, severity: str) -> int:
sla_map = {'CRITICAL': 24, 'HIGH': 168, 'MEDIUM': 720, 'LOW': 2160}
return sla_map.get(severity, 2160)
def _calculate_mttr(self, findings: List[Vulnerability]) -> float:
# Implementation would track actual remediation times
return 0.0
# Usage
reporter = VulnerabilityReporter()
findings = reporter.get_open_findings()
summary = reporter.generate_executive_summary(findings)
print(json.dumps(summary, indent=2))
reporter.export_csv(findings, f"vuln-report-{datetime.now():%Y%m%d}.csv")
Complete CI/CD Security Gate Example
# .github/workflows/security-gate.yml
name: Security Gate โ Block on Critical Findings
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
BLOCK_CRITICAL: true
BLOCK_HIGH_CONTAINER: true
MAX_HIGH_DEPENDENCIES: 5
jobs:
sast:
name: SAST โ SonarQube
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
with:
args: >
-Dsonar.qualitygate.wait=true
-Dsonar.qualitygate.timeout=300
sca:
name: SCA โ Dependency Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Trivy FS Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
format: 'json'
output: 'sca-results.json'
severity: 'CRITICAL,HIGH,MEDIUM'
exit-code: '0' # We parse results manually
- name: Check SCA Results
run: |
CRITICAL=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' sca-results.json || echo "0")
HIGH=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length' sca-results.json || echo "0")
echo "Critical vulnerabilities: $CRITICAL"
echo "High vulnerabilities: $HIGH"
if [ "$CRITICAL" -gt 0 ] && [ "${{ env.BLOCK_CRITICAL }}" == "true" ]; then
echo "::error::Found $CRITICAL critical vulnerabilities in dependencies"
exit 1
fi
if [ "$HIGH" -gt "${{ env.MAX_HIGH_DEPENDENCIES }}" ]; then
echo "::error::Found $HIGH high vulnerabilities (max: ${{ env.MAX_HIGH_DEPENDENCIES }})"
exit 1
fi
container-scan:
name: Container Scan โ Trivy
runs-on: ubuntu-latest
needs: [sast, sca]
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:
image-ref: 'app:${{ github.sha }}'
format: 'table'
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL,HIGH'
- name: Generate SBOM
uses: aquasecurity/trivy-action@master
with:
image-ref: 'app:${{ github.sha }}'
format: 'spdx-json'
output: 'sbom.spdx.json'
scan-type: 'image'
secrets-scan:
name: Secrets Detection
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: GitLeaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: TruffleHog
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: main
head: HEAD
extra_args: --only-verified
# Final security gate โ all jobs must pass
security-gate:
name: Security Gate
needs: [sast, sca, container-scan, secrets-scan]
runs-on: ubuntu-latest
if: always()
steps:
- name: Evaluate Security Gate
run: |
FAILED=false
for job in sast sca container-scan secrets-scan; do
result=$(echo '${{ toJson(needs) }}' | jq -r ".$job.result")
if [ "$result" != "success" ]; then
echo "::error::Job $job failed with result: $result"
FAILED=true
fi
done
if [ "$FAILED" = true ]; then
echo "::error::Security gate FAILED โ merge blocked"
exit 1
fi
echo "Security gate PASSED โ all checks successful"
SBOM Generation
The pipeline above generates a Software Bill of Materials (SBOM) in SPDX format on every build. SBOMs are required for compliance with EO 14028 and enable rapid vulnerability impact assessment when new CVEs are announced.
Related Topics
- Code Security โ Supply chain security and pre-commit scanning
- SecOps Overview โ Security metrics and vulnerability management lifecycle
- Secrets Management โ Secrets scanning in CI/CD