#!/usr/bin/env bash # Risk Bundle Verification Script # RISK-BUNDLE-69-002: CI/offline kit pipeline integration # # Usage: verify-bundle.sh [--signature ] [--strict] [--json] # # This script verifies a risk bundle for integrity and correctness. # Exit codes: # 0 - Bundle is valid # 1 - Bundle is invalid or verification failed # 2 - Input error (missing file, bad arguments) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Defaults BUNDLE_PATH="" SIGNATURE_PATH="" STRICT_MODE=false JSON_OUTPUT=false # Parse arguments while [[ $# -gt 0 ]]; do case $1 in --signature) SIGNATURE_PATH="$2" shift 2 ;; --strict) STRICT_MODE=true shift ;; --json) JSON_OUTPUT=true shift ;; -h|--help) echo "Usage: verify-bundle.sh [--signature ] [--strict] [--json]" echo "" echo "Arguments:" echo " Path to risk-bundle.tar.gz (required)" echo "" echo "Options:" echo " --signature Path to detached signature file" echo " --strict Fail on any warning (e.g., missing optional providers)" echo " --json Output results as JSON" echo "" echo "Exit codes:" echo " 0 - Bundle is valid" echo " 1 - Bundle is invalid" echo " 2 - Input error" exit 0 ;; -*) echo "Unknown option: $1" exit 2 ;; *) if [[ -z "$BUNDLE_PATH" ]]; then BUNDLE_PATH="$1" else echo "Unexpected argument: $1" exit 2 fi shift ;; esac done # Validate required arguments if [[ -z "$BUNDLE_PATH" ]]; then echo "Error: bundle path is required" exit 2 fi if [[ ! -f "$BUNDLE_PATH" ]]; then echo "Error: bundle not found: $BUNDLE_PATH" exit 2 fi # Create temporary extraction directory WORK_DIR=$(mktemp -d) trap "rm -rf $WORK_DIR" EXIT # Initialize result tracking ERRORS=() WARNINGS=() BUNDLE_ID="" BUNDLE_VERSION="" PROVIDER_COUNT=0 MANDATORY_FOUND=false log_error() { ERRORS+=("$1") if [[ "$JSON_OUTPUT" != "true" ]]; then echo "ERROR: $1" >&2 fi } log_warning() { WARNINGS+=("$1") if [[ "$JSON_OUTPUT" != "true" ]]; then echo "WARNING: $1" >&2 fi } log_info() { if [[ "$JSON_OUTPUT" != "true" ]]; then echo "$1" fi } log_info "=== Risk Bundle Verification ===" log_info "Bundle: $BUNDLE_PATH" log_info "" # Step 1: Verify bundle can be extracted log_info "=== Step 1: Extract bundle ===" if ! tar -tzf "$BUNDLE_PATH" > /dev/null 2>&1; then log_error "Bundle is not a valid tar.gz archive" if [[ "$JSON_OUTPUT" == "true" ]]; then echo "{\"valid\": false, \"errors\": [\"Bundle is not a valid tar.gz archive\"]}" fi exit 1 fi tar -xzf "$BUNDLE_PATH" -C "$WORK_DIR" log_info "Bundle extracted successfully" # Step 2: Check required structure log_info "" log_info "=== Step 2: Verify structure ===" REQUIRED_FILES=( "manifests/provider-manifest.json" ) for file in "${REQUIRED_FILES[@]}"; do if [[ ! -f "$WORK_DIR/$file" ]]; then log_error "Missing required file: $file" else log_info "Found: $file" fi done # Step 3: Parse and validate manifest log_info "" log_info "=== Step 3: Validate manifest ===" MANIFEST_FILE="$WORK_DIR/manifests/provider-manifest.json" if [[ -f "$MANIFEST_FILE" ]]; then # Extract manifest fields using basic parsing (portable) if command -v jq &> /dev/null; then BUNDLE_ID=$(jq -r '.bundleId // empty' "$MANIFEST_FILE") BUNDLE_VERSION=$(jq -r '.version // empty' "$MANIFEST_FILE") INPUTS_HASH=$(jq -r '.inputsHash // empty' "$MANIFEST_FILE") PROVIDER_COUNT=$(jq '.providers | length' "$MANIFEST_FILE") log_info "Bundle ID: $BUNDLE_ID" log_info "Version: $BUNDLE_VERSION" log_info "Inputs Hash: $INPUTS_HASH" log_info "Provider count: $PROVIDER_COUNT" else # Fallback to grep-based parsing BUNDLE_ID=$(grep -o '"bundleId"[[:space:]]*:[[:space:]]*"[^"]*"' "$MANIFEST_FILE" | cut -d'"' -f4 || echo "") log_info "Bundle ID: $BUNDLE_ID (jq not available - limited parsing)" fi # Validate required fields if [[ -z "$BUNDLE_ID" ]]; then log_error "Manifest missing bundleId" fi else log_error "Manifest file not found" fi # Step 4: Verify provider files log_info "" log_info "=== Step 4: Verify provider files ===" # Check for mandatory provider (cisa-kev) CISA_KEV_FILE="$WORK_DIR/providers/cisa-kev/snapshot" if [[ -f "$CISA_KEV_FILE" ]]; then log_info "Found mandatory provider: cisa-kev" MANDATORY_FOUND=true # Verify hash if jq is available if command -v jq &> /dev/null && [[ -f "$MANIFEST_FILE" ]]; then EXPECTED_HASH=$(jq -r '.providers[] | select(.providerId == "cisa-kev") | .digest' "$MANIFEST_FILE" | sed 's/sha256://') ACTUAL_HASH=$(sha256sum "$CISA_KEV_FILE" | cut -d' ' -f1) if [[ "$EXPECTED_HASH" == "$ACTUAL_HASH" ]]; then log_info " Hash verified: $ACTUAL_HASH" else log_error "cisa-kev hash mismatch: expected $EXPECTED_HASH, got $ACTUAL_HASH" fi fi else log_error "Missing mandatory provider: cisa-kev" fi # Check optional providers EPSS_FILE="$WORK_DIR/providers/first-epss/snapshot" if [[ -f "$EPSS_FILE" ]]; then log_info "Found optional provider: first-epss" if command -v jq &> /dev/null && [[ -f "$MANIFEST_FILE" ]]; then EXPECTED_HASH=$(jq -r '.providers[] | select(.providerId == "first-epss") | .digest' "$MANIFEST_FILE" | sed 's/sha256://') ACTUAL_HASH=$(sha256sum "$EPSS_FILE" | cut -d' ' -f1) if [[ "$EXPECTED_HASH" == "$ACTUAL_HASH" ]]; then log_info " Hash verified: $ACTUAL_HASH" else log_error "first-epss hash mismatch: expected $EXPECTED_HASH, got $ACTUAL_HASH" fi fi else log_warning "Optional provider not found: first-epss" fi OSV_FILE="$WORK_DIR/providers/osv/snapshot" if [[ -f "$OSV_FILE" ]]; then log_info "Found optional provider: osv" else log_warning "Optional provider not found: osv (this is OK unless --include-osv was specified)" fi # Step 5: Verify DSSE signature (if present) log_info "" log_info "=== Step 5: Check signatures ===" DSSE_FILE="$WORK_DIR/signatures/provider-manifest.dsse" if [[ -f "$DSSE_FILE" ]]; then log_info "Found manifest DSSE signature" # Basic DSSE structure check if command -v jq &> /dev/null; then PAYLOAD_TYPE=$(jq -r '.payloadType // empty' "$DSSE_FILE") SIG_COUNT=$(jq '.signatures | length' "$DSSE_FILE") if [[ "$PAYLOAD_TYPE" == "application/vnd.stellaops.risk-bundle.manifest+json" ]]; then log_info " Payload type: $PAYLOAD_TYPE (valid)" else log_warning "Unexpected payload type: $PAYLOAD_TYPE" fi log_info " Signature count: $SIG_COUNT" fi else log_warning "No DSSE signature found" fi # Check detached bundle signature if [[ -n "$SIGNATURE_PATH" ]]; then if [[ -f "$SIGNATURE_PATH" ]]; then log_info "Found detached bundle signature: $SIGNATURE_PATH" # TODO: Implement actual signature verification else log_error "Specified signature file not found: $SIGNATURE_PATH" fi fi # Step 6: Summarize results log_info "" log_info "=== Verification Summary ===" ERROR_COUNT=${#ERRORS[@]} WARNING_COUNT=${#WARNINGS[@]} if [[ "$JSON_OUTPUT" == "true" ]]; then # Output JSON result ERRORS_JSON=$(printf '%s\n' "${ERRORS[@]}" | jq -R . | jq -s . 2>/dev/null || echo "[]") WARNINGS_JSON=$(printf '%s\n' "${WARNINGS[@]}" | jq -R . | jq -s . 2>/dev/null || echo "[]") cat <