#!/usr/bin/env bash # Security SLSA Provenance - Execution Script # # This script generates and verifies SLSA provenance attestations. 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)" # Set defaults set_default_env "SLSA_LEVEL" "2" # Parse arguments ACTION="${1:-}" TARGET="${2:-}" SOURCE_URI="${3:-}" PROVENANCE_FILE="${4:-}" if [[ -z "${ACTION}" ]] || [[ -z "${TARGET}" ]]; then log_error "Usage: security-slsa-provenance [source_uri] [provenance_file]" log_error " action: generate, verify, inspect" log_error " target: Docker image, file path, or provenance file" log_error " source_uri: Source repository URI (for verify)" log_error " provenance_file: Path to provenance file (for verify with file)" log_error "" log_error "Examples:" log_error " security-slsa-provenance verify ghcr.io/user/charon:latest github.com/user/charon" log_error " security-slsa-provenance verify ./dist/binary github.com/user/repo provenance.json" log_error " security-slsa-provenance inspect provenance.json" exit 2 fi # Validate action case "${ACTION}" in generate|verify|inspect) ;; *) log_error "Invalid action: ${ACTION}" log_error "Action must be one of: generate, verify, inspect" exit 2 ;; esac # Check required tools log_step "ENVIRONMENT" "Validating prerequisites" if ! command -v jq >/dev/null 2>&1; then log_error "jq is not installed" log_error "Install from: https://stedolan.github.io/jq/download/" exit 2 fi if [[ "${ACTION}" == "verify" ]] && ! command -v slsa-verifier >/dev/null 2>&1; then log_error "slsa-verifier is not installed" log_error "Install from: https://github.com/slsa-framework/slsa-verifier" log_error "Quick install:" log_error " go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest" log_error "Or:" log_error " curl -sLO https://github.com/slsa-framework/slsa-verifier/releases/download/v2.6.0/slsa-verifier-linux-amd64" log_error " sudo install slsa-verifier-linux-amd64 /usr/local/bin/slsa-verifier" exit 2 fi if [[ "${ACTION}" == "verify" ]] && [[ "${TARGET}" =~ ^ghcr\.|^docker\.|: ]]; then # Docker image verification requires gh CLI if ! command -v gh >/dev/null 2>&1; then log_error "gh (GitHub CLI) is not installed (required for Docker image verification)" log_error "Install from: https://cli.github.com/" exit 2 fi fi cd "${PROJECT_ROOT}" # Execute action case "${ACTION}" in generate) log_step "GENERATE" "Generating SLSA provenance for ${TARGET}" log_warning "This generates a basic provenance for testing only" log_warning "Production provenance must be generated by CI/CD build platform" if [[ ! -f "${TARGET}" ]]; then log_error "File not found: ${TARGET}" exit 1 fi # Calculate digest DIGEST=$(sha256sum "${TARGET}" | awk '{print $1}') ARTIFACT_NAME=$(basename "${TARGET}") OUTPUT_FILE="provenance-${ARTIFACT_NAME}.json" # Generate basic provenance structure cat > "${OUTPUT_FILE}" < [provenance_file]" exit 2 fi # Determine if target is Docker image or file # Match: ghcr.io/user/repo:tag, docker.io/user/repo:tag, user/repo:tag, simple:tag, registry.io:5000/app:v1 # Avoid: ./file, /path/to/file, file.ext, http://url # Strategy: Images have "name:tag" format and don't start with ./ or / and aren't files if [[ ! -f "${TARGET}" ]] && \ [[ ! "${TARGET}" =~ ^\./ ]] && \ [[ ! "${TARGET}" =~ ^/ ]] && \ [[ ! "${TARGET}" =~ ^https?:// ]] && \ [[ "${TARGET}" =~ : ]]; then # Looks like a Docker image log_info "Target appears to be a Docker image" if [[ -n "${PROVENANCE_FILE}" ]]; then log_warning "Provenance file parameter ignored for Docker images" log_warning "Provenance will be downloaded from registry" fi # Verify image with slsa-verifier log_info "Verifying image with slsa-verifier..." if slsa-verifier verify-image "${TARGET}" \ --source-uri "github.com/${SOURCE_URI}" \ --print-provenance 2>&1 | tee slsa-verify.log; then log_success "Provenance verification passed" # Parse SLSA level from output if grep -q "SLSA" slsa-verify.log; then LEVEL=$(grep -oP 'SLSA Level: \K\d+' slsa-verify.log || echo "unknown") log_info "SLSA Level: ${LEVEL}" if [[ "${LEVEL}" =~ ^[0-9]+$ ]] && [[ "${LEVEL}" -lt "${SLSA_LEVEL}" ]]; then log_warning "SLSA level ${LEVEL} is below minimum required level ${SLSA_LEVEL}" fi fi rm -f slsa-verify.log exit 0 else log_error "Provenance verification failed" cat slsa-verify.log >&2 || true rm -f slsa-verify.log exit 1 fi else # File artifact log_info "Target appears to be a file artifact" if [[ ! -f "${TARGET}" ]]; then log_error "File not found: ${TARGET}" exit 1 fi if [[ -z "${PROVENANCE_FILE}" ]]; then log_error "Provenance file is required for file verification" log_error "Usage: security-slsa-provenance verify " exit 2 fi if [[ ! -f "${PROVENANCE_FILE}" ]]; then log_error "Provenance file not found: ${PROVENANCE_FILE}" exit 1 fi log_info "Verifying artifact with slsa-verifier..." if slsa-verifier verify-artifact "${TARGET}" \ --provenance-path "${PROVENANCE_FILE}" \ --source-uri "github.com/${SOURCE_URI}" \ --print-provenance 2>&1 | tee slsa-verify.log; then log_success "Provenance verification passed" # Parse SLSA level from output if grep -q "SLSA" slsa-verify.log; then LEVEL=$(grep -oP 'SLSA Level: \K\d+' slsa-verify.log || echo "unknown") log_info "SLSA Level: ${LEVEL}" if [[ "${LEVEL}" =~ ^[0-9]+$ ]] && [[ "${LEVEL}" -lt "${SLSA_LEVEL}" ]]; then log_warning "SLSA level ${LEVEL} is below minimum required level ${SLSA_LEVEL}" fi fi rm -f slsa-verify.log exit 0 else log_error "Provenance verification failed" cat slsa-verify.log >&2 || true rm -f slsa-verify.log exit 1 fi fi ;; inspect) log_step "INSPECT" "Inspecting SLSA provenance" if [[ ! -f "${TARGET}" ]]; then log_error "Provenance file not found: ${TARGET}" exit 1 fi # Validate JSON if ! jq empty "${TARGET}" 2>/dev/null; then log_error "Invalid JSON in provenance file" exit 1 fi echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " SLSA PROVENANCE DETAILS" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # Extract and display key fields PREDICATE_TYPE=$(jq -r '.predicateType // "unknown"' "${TARGET}") echo "Predicate Type: ${PREDICATE_TYPE}" # Builder BUILDER_ID=$(jq -r '.predicate.runDetails.builder.id // .predicate.builder.id // "unknown"' "${TARGET}") echo "" echo "Builder:" echo " ID: ${BUILDER_ID}" # Source SOURCE_URI_FOUND=$(jq -r '.predicate.buildDefinition.externalParameters.source.uri // .predicate.materials[0].uri // "unknown"' "${TARGET}") SOURCE_DIGEST=$(jq -r '.predicate.buildDefinition.externalParameters.source.digest.sha1 // "unknown"' "${TARGET}") echo "" echo "Source Repository:" echo " URI: ${SOURCE_URI_FOUND}" if [[ "${SOURCE_DIGEST}" != "unknown" ]]; then echo " Digest: ${SOURCE_DIGEST}" fi # Subject SUBJECT_NAME=$(jq -r '.subject[0].name // "unknown"' "${TARGET}") SUBJECT_DIGEST=$(jq -r '.subject[0].digest.sha256 // "unknown"' "${TARGET}") echo "" echo "Subject:" echo " Name: ${SUBJECT_NAME}" echo " Digest: sha256:${SUBJECT_DIGEST:0:12}..." # Build metadata STARTED=$(jq -r '.predicate.runDetails.metadata.startedOn // .predicate.metadata.buildStartedOn // "unknown"' "${TARGET}") FINISHED=$(jq -r '.predicate.runDetails.metadata.finishedOn // .predicate.metadata.buildFinishedOn // "unknown"' "${TARGET}") echo "" echo "Build Metadata:" if [[ "${STARTED}" != "unknown" ]]; then echo " Started: ${STARTED}" fi if [[ "${FINISHED}" != "unknown" ]]; then echo " Finished: ${FINISHED}" fi # Materials/Dependencies MATERIALS_COUNT=$(jq '.predicate.buildDefinition.resolvedDependencies // .predicate.materials // [] | length' "${TARGET}") if [[ "${MATERIALS_COUNT}" -gt 0 ]]; then echo "" echo "Materials (Dependencies): ${MATERIALS_COUNT}" jq -r '.predicate.buildDefinition.resolvedDependencies // .predicate.materials // [] | .[] | " - \(.uri // .name // "unknown")"' "${TARGET}" | head -n 5 if [[ "${MATERIALS_COUNT}" -gt 5 ]]; then echo " ... and $((MATERIALS_COUNT - 5)) more" fi fi echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" log_success "Provenance inspection complete" ;; esac exit 0