up the blokcing tasks
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
332
ops/devops/risk-bundle/verify-bundle.sh
Normal file
332
ops/devops/risk-bundle/verify-bundle.sh
Normal file
@@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env bash
|
||||
# Risk Bundle Verification Script
|
||||
# RISK-BUNDLE-69-002: CI/offline kit pipeline integration
|
||||
#
|
||||
# Usage: verify-bundle.sh <bundle-path> [--signature <sig-path>] [--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 <bundle-path> [--signature <sig-path>] [--strict] [--json]"
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " <bundle-path> Path to risk-bundle.tar.gz (required)"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --signature <sig-path> 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 <<EOF
|
||||
{
|
||||
"valid": $([[ $ERROR_COUNT -eq 0 ]] && echo "true" || echo "false"),
|
||||
"bundleId": "$BUNDLE_ID",
|
||||
"version": "$BUNDLE_VERSION",
|
||||
"providerCount": $PROVIDER_COUNT,
|
||||
"mandatoryProviderFound": $MANDATORY_FOUND,
|
||||
"errorCount": $ERROR_COUNT,
|
||||
"warningCount": $WARNING_COUNT,
|
||||
"errors": $ERRORS_JSON,
|
||||
"warnings": $WARNINGS_JSON
|
||||
}
|
||||
EOF
|
||||
else
|
||||
log_info "Bundle ID: $BUNDLE_ID"
|
||||
log_info "Errors: $ERROR_COUNT"
|
||||
log_info "Warnings: $WARNING_COUNT"
|
||||
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Errors:"
|
||||
for err in "${ERRORS[@]}"; do
|
||||
echo " - $err"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ $WARNING_COUNT -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Warnings:"
|
||||
for warn in "${WARNINGS[@]}"; do
|
||||
echo " - $warn"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine exit code
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
if [[ "$JSON_OUTPUT" != "true" ]]; then
|
||||
echo ""
|
||||
echo "RESULT: INVALID"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$STRICT_MODE" == "true" && $WARNING_COUNT -gt 0 ]]; then
|
||||
if [[ "$JSON_OUTPUT" != "true" ]]; then
|
||||
echo ""
|
||||
echo "RESULT: INVALID (strict mode - warnings treated as errors)"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$JSON_OUTPUT" != "true" ]]; then
|
||||
echo ""
|
||||
echo "RESULT: VALID"
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user