328 lines
12 KiB
Bash
Executable File
328 lines
12 KiB
Bash
Executable File
#!/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 <action> <target> [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}" <<EOF
|
|
{
|
|
"_type": "https://in-toto.io/Statement/v1",
|
|
"subject": [
|
|
{
|
|
"name": "${ARTIFACT_NAME}",
|
|
"digest": {
|
|
"sha256": "${DIGEST}"
|
|
}
|
|
}
|
|
],
|
|
"predicateType": "https://slsa.dev/provenance/v1",
|
|
"predicate": {
|
|
"buildDefinition": {
|
|
"buildType": "https://github.com/user/local-build",
|
|
"externalParameters": {
|
|
"source": {
|
|
"uri": "git+https://github.com/user/charon@local",
|
|
"digest": {
|
|
"sha1": "0000000000000000000000000000000000000000"
|
|
}
|
|
}
|
|
},
|
|
"internalParameters": {},
|
|
"resolvedDependencies": []
|
|
},
|
|
"runDetails": {
|
|
"builder": {
|
|
"id": "https://github.com/user/local-builder@v1.0.0"
|
|
},
|
|
"metadata": {
|
|
"invocationId": "local-$(date +%s)",
|
|
"startedOn": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
"finishedOn": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EOF
|
|
|
|
log_success "Generated provenance: ${OUTPUT_FILE}"
|
|
log_warning "This provenance is NOT cryptographically signed"
|
|
log_warning "Use only for local testing, not for production"
|
|
;;
|
|
|
|
verify)
|
|
log_step "VERIFY" "Verifying SLSA provenance for ${TARGET}"
|
|
|
|
if [[ -z "${SOURCE_URI}" ]]; then
|
|
log_error "Source URI is required for verification"
|
|
log_error "Usage: security-slsa-provenance verify <target> <source_uri> [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 <file> <source_uri> <provenance_file>"
|
|
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
|