- Created a comprehensive QA report detailing the audit of three GitHub Actions workflows: propagate-changes.yml, nightly-build.yml, and supply-chain-verify.yml. - Included sections on pre-commit hooks, YAML syntax validation, security audit findings, logic review, best practices compliance, and specific workflow analysis. - Highlighted strengths, minor improvements, and recommendations for enhancing security and operational efficiency. - Documented compliance with SLSA Level 2 and OWASP security best practices. - Generated report date: 2026-01-13, with a next review scheduled after Phase 3 implementation or 90 days from deployment.
264 lines
11 KiB
Bash
Executable File
264 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Security Scan Docker Image - Execution Script
|
|
#
|
|
# Build Docker image and scan with Grype/Syft matching CI supply chain verification
|
|
# This script replicates the exact process from supply-chain-pr.yml workflow
|
|
|
|
set -euo pipefail
|
|
|
|
# Source helper scripts
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SKILLS_SCRIPTS_DIR="$(cd "${SCRIPT_DIR}/../scripts" && pwd)"
|
|
|
|
# shellcheck source=../scripts/_logging_helpers.sh
|
|
source "${SKILLS_SCRIPTS_DIR}/_logging_helpers.sh"
|
|
# shellcheck source=../scripts/_error_handling_helpers.sh
|
|
source "${SKILLS_SCRIPTS_DIR}/_error_handling_helpers.sh"
|
|
# shellcheck source=../scripts/_environment_helpers.sh
|
|
source "${SKILLS_SCRIPTS_DIR}/_environment_helpers.sh"
|
|
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
|
|
|
# Validate environment
|
|
log_step "ENVIRONMENT" "Validating prerequisites"
|
|
|
|
# Check Docker
|
|
validate_docker_environment || error_exit "Docker is required but not available"
|
|
|
|
# Check Syft
|
|
if ! command -v syft >/dev/null 2>&1; then
|
|
log_error "Syft not found - install from: https://github.com/anchore/syft"
|
|
log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin v1.17.0"
|
|
error_exit "Syft is required for SBOM generation" 2
|
|
fi
|
|
|
|
# Check Grype
|
|
if ! command -v grype >/dev/null 2>&1; then
|
|
log_error "Grype not found - install from: https://github.com/anchore/grype"
|
|
log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.85.0"
|
|
error_exit "Grype is required for vulnerability scanning" 2
|
|
fi
|
|
|
|
# Check jq
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
log_error "jq not found - install from package manager (apt-get install jq, brew install jq, etc.)"
|
|
error_exit "jq is required for JSON processing" 2
|
|
fi
|
|
|
|
# Verify tool versions match CI
|
|
SYFT_INSTALLED_VERSION=$(syft version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
|
GRYPE_INSTALLED_VERSION=$(grype version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
|
|
|
# Set defaults matching CI workflow
|
|
set_default_env "SYFT_VERSION" "v1.17.0"
|
|
set_default_env "GRYPE_VERSION" "v0.85.0"
|
|
set_default_env "IMAGE_TAG" "charon:local"
|
|
set_default_env "FAIL_ON_SEVERITY" "Critical,High"
|
|
|
|
# Version check (informational only)
|
|
log_info "Installed Syft version: ${SYFT_INSTALLED_VERSION}"
|
|
log_info "Expected Syft version: ${SYFT_VERSION}"
|
|
if [[ "${SYFT_INSTALLED_VERSION}" != "${SYFT_VERSION#v}" ]] && [[ "${SYFT_INSTALLED_VERSION}" != "${SYFT_VERSION}" ]]; then
|
|
log_warning "Syft version mismatch - CI uses ${SYFT_VERSION}, you have ${SYFT_INSTALLED_VERSION}"
|
|
log_warning "Results may differ from CI. Reinstall with: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin ${SYFT_VERSION}"
|
|
fi
|
|
|
|
log_info "Installed Grype version: ${GRYPE_INSTALLED_VERSION}"
|
|
log_info "Expected Grype version: ${GRYPE_VERSION}"
|
|
if [[ "${GRYPE_INSTALLED_VERSION}" != "${GRYPE_VERSION#v}" ]] && [[ "${GRYPE_INSTALLED_VERSION}" != "${GRYPE_VERSION}" ]]; then
|
|
log_warning "Grype version mismatch - CI uses ${GRYPE_VERSION}, you have ${GRYPE_INSTALLED_VERSION}"
|
|
log_warning "Results may differ from CI. Reinstall with: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin ${GRYPE_VERSION}"
|
|
fi
|
|
|
|
# Parse arguments
|
|
IMAGE_TAG="${1:-${IMAGE_TAG}}"
|
|
NO_CACHE_FLAG=""
|
|
if [[ "${2:-}" == "no-cache" ]]; then
|
|
NO_CACHE_FLAG="--no-cache"
|
|
log_info "Building without cache (clean build)"
|
|
fi
|
|
|
|
log_info "Image tag: ${IMAGE_TAG}"
|
|
log_info "Fail on severity: ${FAIL_ON_SEVERITY}"
|
|
|
|
cd "${PROJECT_ROOT}"
|
|
|
|
# ==============================================================================
|
|
# Phase 1: Build Docker Image
|
|
# ==============================================================================
|
|
log_step "BUILD" "Building Docker image: ${IMAGE_TAG}"
|
|
|
|
# Get build metadata
|
|
VERSION="${VERSION:-dev}"
|
|
BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
VCS_REF=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
|
|
log_info "Build args: VERSION=${VERSION}, BUILD_DATE=${BUILD_DATE}, VCS_REF=${VCS_REF}"
|
|
|
|
# Build Docker image with same args as CI
|
|
if docker build ${NO_CACHE_FLAG} \
|
|
--build-arg VERSION="${VERSION}" \
|
|
--build-arg BUILD_DATE="${BUILD_DATE}" \
|
|
--build-arg VCS_REF="${VCS_REF}" \
|
|
-t "${IMAGE_TAG}" \
|
|
-f Dockerfile \
|
|
.; then
|
|
log_success "Docker image built successfully: ${IMAGE_TAG}"
|
|
else
|
|
error_exit "Docker build failed" 2
|
|
fi
|
|
|
|
# ==============================================================================
|
|
# Phase 2: Generate SBOM
|
|
# ==============================================================================
|
|
log_step "SBOM" "Generating SBOM using Syft ${SYFT_VERSION}"
|
|
|
|
log_info "Scanning image: ${IMAGE_TAG}"
|
|
log_info "Format: CycloneDX JSON (matches CI)"
|
|
|
|
# Generate SBOM from the Docker IMAGE (not filesystem)
|
|
if syft "${IMAGE_TAG}" \
|
|
--output cyclonedx-json=sbom.cyclonedx.json \
|
|
--output table; then
|
|
log_success "SBOM generation complete"
|
|
else
|
|
error_exit "SBOM generation failed" 2
|
|
fi
|
|
|
|
# Count components in SBOM
|
|
COMPONENT_COUNT=$(jq '.components | length' sbom.cyclonedx.json 2>/dev/null || echo "0")
|
|
log_info "Generated SBOM contains ${COMPONENT_COUNT} packages"
|
|
|
|
# ==============================================================================
|
|
# Phase 3: Scan for Vulnerabilities
|
|
# ==============================================================================
|
|
log_step "SCAN" "Scanning for vulnerabilities using Grype ${GRYPE_VERSION}"
|
|
|
|
log_info "Scanning SBOM against vulnerability database..."
|
|
log_info "This may take 30-60 seconds on first run (database download)"
|
|
|
|
# Run Grype against the SBOM (generated from image, not filesystem)
|
|
# This matches exactly what CI does in supply-chain-pr.yml
|
|
if grype sbom:sbom.cyclonedx.json \
|
|
--output json \
|
|
--file grype-results.json; then
|
|
log_success "Vulnerability scan complete"
|
|
else
|
|
log_warning "Grype scan completed with findings"
|
|
fi
|
|
|
|
# Generate SARIF output for GitHub Security (matches CI)
|
|
grype sbom:sbom.cyclonedx.json \
|
|
--output sarif \
|
|
--file grype-results.sarif 2>/dev/null || true
|
|
|
|
# ==============================================================================
|
|
# Phase 4: Analyze Results
|
|
# ==============================================================================
|
|
log_step "ANALYSIS" "Analyzing vulnerability scan results"
|
|
|
|
# Count vulnerabilities by severity (matches CI logic)
|
|
if [[ -f grype-results.json ]]; then
|
|
CRITICAL_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Critical")] | length' grype-results.json 2>/dev/null || echo "0")
|
|
HIGH_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "High")] | length' grype-results.json 2>/dev/null || echo "0")
|
|
MEDIUM_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Medium")] | length' grype-results.json 2>/dev/null || echo "0")
|
|
LOW_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Low")] | length' grype-results.json 2>/dev/null || echo "0")
|
|
NEGLIGIBLE_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Negligible")] | length' grype-results.json 2>/dev/null || echo "0")
|
|
UNKNOWN_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Unknown")] | length' grype-results.json 2>/dev/null || echo "0")
|
|
TOTAL_COUNT=$(jq '.matches | length' grype-results.json 2>/dev/null || echo "0")
|
|
else
|
|
CRITICAL_COUNT=0
|
|
HIGH_COUNT=0
|
|
MEDIUM_COUNT=0
|
|
LOW_COUNT=0
|
|
NEGLIGIBLE_COUNT=0
|
|
UNKNOWN_COUNT=0
|
|
TOTAL_COUNT=0
|
|
fi
|
|
|
|
# Display vulnerability summary
|
|
echo ""
|
|
log_info "Vulnerability Summary:"
|
|
echo " 🔴 Critical: ${CRITICAL_COUNT}"
|
|
echo " 🟠 High: ${HIGH_COUNT}"
|
|
echo " 🟡 Medium: ${MEDIUM_COUNT}"
|
|
echo " 🟢 Low: ${LOW_COUNT}"
|
|
if [[ ${NEGLIGIBLE_COUNT} -gt 0 ]]; then
|
|
echo " ⚪ Negligible: ${NEGLIGIBLE_COUNT}"
|
|
fi
|
|
if [[ ${UNKNOWN_COUNT} -gt 0 ]]; then
|
|
echo " ❓ Unknown: ${UNKNOWN_COUNT}"
|
|
fi
|
|
echo " 📊 Total: ${TOTAL_COUNT}"
|
|
echo ""
|
|
|
|
# ==============================================================================
|
|
# Phase 5: Detailed Reporting
|
|
# ==============================================================================
|
|
|
|
# Show Critical vulnerabilities if any
|
|
if [[ ${CRITICAL_COUNT} -gt 0 ]]; then
|
|
log_error "Critical Severity Vulnerabilities Found:"
|
|
echo ""
|
|
jq -r '.matches[] | select(.vulnerability.severity == "Critical") |
|
|
" - \(.vulnerability.id) in \(.artifact.name)\n Package: \(.artifact.name)@\(.artifact.version)\n Fixed: \(.vulnerability.fix.versions[0] // "No fix available")\n CVSS: \(.vulnerability.cvss[0].metrics.baseScore // "N/A")\n Description: \(.vulnerability.description[0:100])...\n"' \
|
|
grype-results.json 2>/dev/null || echo " (Unable to parse details)"
|
|
echo ""
|
|
fi
|
|
|
|
# Show High vulnerabilities if any
|
|
if [[ ${HIGH_COUNT} -gt 0 ]]; then
|
|
log_warning "High Severity Vulnerabilities Found:"
|
|
echo ""
|
|
jq -r '.matches[] | select(.vulnerability.severity == "High") |
|
|
" - \(.vulnerability.id) in \(.artifact.name)\n Package: \(.artifact.name)@\(.artifact.version)\n Fixed: \(.vulnerability.fix.versions[0] // "No fix available")\n CVSS: \(.vulnerability.cvss[0].metrics.baseScore // "N/A")\n Description: \(.vulnerability.description[0:100])...\n"' \
|
|
grype-results.json 2>/dev/null || echo " (Unable to parse details)"
|
|
echo ""
|
|
fi
|
|
|
|
# ==============================================================================
|
|
# Phase 6: Exit Code Determination (Matches CI)
|
|
# ==============================================================================
|
|
|
|
# Check if any failing severities were found
|
|
SHOULD_FAIL=false
|
|
|
|
if [[ "${FAIL_ON_SEVERITY}" == *"Critical"* ]] && [[ ${CRITICAL_COUNT} -gt 0 ]]; then
|
|
SHOULD_FAIL=true
|
|
fi
|
|
|
|
if [[ "${FAIL_ON_SEVERITY}" == *"High"* ]] && [[ ${HIGH_COUNT} -gt 0 ]]; then
|
|
SHOULD_FAIL=true
|
|
fi
|
|
|
|
if [[ "${FAIL_ON_SEVERITY}" == *"Medium"* ]] && [[ ${MEDIUM_COUNT} -gt 0 ]]; then
|
|
SHOULD_FAIL=true
|
|
fi
|
|
|
|
if [[ "${FAIL_ON_SEVERITY}" == *"Low"* ]] && [[ ${LOW_COUNT} -gt 0 ]]; then
|
|
SHOULD_FAIL=true
|
|
fi
|
|
|
|
# Final summary and exit
|
|
echo ""
|
|
log_info "Generated artifacts:"
|
|
log_info " - sbom.cyclonedx.json (SBOM)"
|
|
log_info " - grype-results.json (vulnerability details)"
|
|
log_info " - grype-results.sarif (GitHub Security format)"
|
|
echo ""
|
|
|
|
if [[ "${SHOULD_FAIL}" == "true" ]]; then
|
|
log_error "Found ${CRITICAL_COUNT} Critical and ${HIGH_COUNT} High severity vulnerabilities"
|
|
log_error "These issues must be resolved before deployment"
|
|
log_error "Review grype-results.json for detailed remediation guidance"
|
|
exit 1
|
|
else
|
|
if [[ ${TOTAL_COUNT} -gt 0 ]]; then
|
|
log_success "Docker image scan complete - no critical or high vulnerabilities"
|
|
log_info "Found ${MEDIUM_COUNT} Medium and ${LOW_COUNT} Low severity issues (non-blocking)"
|
|
else
|
|
log_success "Docker image scan complete - no vulnerabilities found"
|
|
fi
|
|
exit 0
|
|
fi
|