41 pages ยท 8 sections
Ctrl K
GitHub Portfolio

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

TypeFull NameScan TargetWhenDepth
SASTStatic Application Security TestingSource codeBuildCode-level vulnerabilities, injection flaws, insecure patterns
DASTDynamic Application Security TestingRunning applicationTest/StageRuntime vulnerabilities, auth bypass, misconfigurations
SCASoftware Composition AnalysisDependencies, librariesBuildKnown CVEs, license compliance, outdated packages
ContainerContainer Image ScanningContainer imagesBuild/RegistryOS CVEs, application CVEs, misconfigurations
IaCInfrastructure as CodeTerraform, CloudFormationBuildCloud misconfigurations, policy violations
SecretsSecrets DetectionAll filesPre-commit + CIHardcoded 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)

SeverityCVSS ScoreDescriptionAction Required
CRITICAL9.0 โ€“ 10.0Remote code execution, full system compromise, wormable vulnerabilityImmediate โ€” within 24 hours
HIGH7.0 โ€“ 8.9Significant data exposure, privilege escalation, lateral movement possibleUrgent โ€” within 7 days
MEDIUM4.0 โ€“ 6.9Limited impact exploitation, requires additional conditionsPlanned โ€” within 30 days
LOW0.1 โ€“ 3.9Minimal impact, difficult to exploitScheduled โ€” within 90 days
INFO0.0Informational finding, no direct security impactBest effort โ€” track in backlog

Remediation SLA Matrix

SeverityInternet-FacingInternalIsolated/DevException Process
CRITICAL4 hours24 hours48 hoursCISO approval required
HIGH24 hours7 days14 daysSecurity lead approval
MEDIUM7 days30 days60 daysTeam lead approval
LOW30 days90 days120 daysSelf-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