#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' Usage: scripts/ci/enrich_exploitability.sh --mode single|batch --input --output [--cvss-vectors ] [--analyzer ] Options: --mode Processing mode: single advisory object or batch advisory array --input Input JSON path --output Output JSON path --cvss-vectors Optional JSON object mapping advisory id -> CVSS vector --analyzer Optional analyzer path (default: utils/analyze_exploitability.py) --help Show this help EOF } REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$REPO_ROOT" MODE="" INPUT_PATH="" OUTPUT_PATH="" CVSS_VECTORS_PATH="" ANALYZER_PATH="utils/analyze_exploitability.py" while [[ $# -gt 0 ]]; do case "$1" in --mode) MODE="${2:-}" shift 2 ;; --input) INPUT_PATH="${2:-}" shift 2 ;; --output) OUTPUT_PATH="${2:-}" shift 2 ;; --cvss-vectors) CVSS_VECTORS_PATH="${2:-}" shift 2 ;; --analyzer) ANALYZER_PATH="${2:-}" shift 2 ;; --help|-h) usage exit 0 ;; *) echo "ERROR: Unknown argument: $1" >&2 usage >&2 exit 1 ;; esac done if [[ "$MODE" != "single" && "$MODE" != "batch" ]]; then echo "ERROR: --mode must be one of: single, batch" >&2 exit 1 fi if [[ -z "$INPUT_PATH" || -z "$OUTPUT_PATH" ]]; then echo "ERROR: --input and --output are required" >&2 exit 1 fi if [[ ! -f "$INPUT_PATH" ]]; then echo "ERROR: input file not found: $INPUT_PATH" >&2 exit 1 fi if [[ ! -f "$ANALYZER_PATH" ]]; then echo "ERROR: analyzer file not found: $ANALYZER_PATH" >&2 exit 1 fi if [[ -n "$CVSS_VECTORS_PATH" && ! -f "$CVSS_VECTORS_PATH" ]]; then echo "ERROR: --cvss-vectors file not found: $CVSS_VECTORS_PATH" >&2 exit 1 fi if ! command -v jq >/dev/null 2>&1; then echo "ERROR: jq is required" >&2 exit 1 fi if command -v python >/dev/null 2>&1; then PYTHON_BIN="python" elif command -v python3 >/dev/null 2>&1; then PYTHON_BIN="python3" else echo "ERROR: python or python3 is required" >&2 exit 1 fi tmpdir="$(mktemp -d)" trap 'rm -rf "$tmpdir"' EXIT resolve_cvss_vector() { local advisory_json="$1" local advisory_id advisory_id="$(echo "$advisory_json" | jq -r '.id // ""')" if [[ -n "$CVSS_VECTORS_PATH" ]]; then jq -r --arg id "$advisory_id" '.[$id] // ""' "$CVSS_VECTORS_PATH" else echo "$advisory_json" | jq -r '.cvss_vector // ""' fi } severity_to_cvss() { case "$1" in critical) echo "9.5" ;; high) echo "7.5" ;; medium) echo "5.5" ;; low) echo "3.0" ;; *) echo "5.0" ;; esac } build_analysis_input() { local advisory_json="$1" local mode="$2" local cve_id cvss_score cvss_vector vuln_type description references severity cve_id="$(echo "$advisory_json" | jq -r '.id // ""')" vuln_type="$(echo "$advisory_json" | jq -r '.type // ""')" description="$(echo "$advisory_json" | jq -r '.description // ""')" references="$(echo "$advisory_json" | jq -c '.references // []')" cvss_vector="$(resolve_cvss_vector "$advisory_json")" if [[ "$mode" == "single" ]]; then severity="$(echo "$advisory_json" | jq -r '.severity // "medium"')" cvss_score="$(severity_to_cvss "$severity")" else cvss_score="$(echo "$advisory_json" | jq -r '.cvss_score // 0')" fi jq -n \ --arg cve_id "$cve_id" \ --argjson cvss_score "$cvss_score" \ --arg cvss_vector "$cvss_vector" \ --arg type "$vuln_type" \ --arg description "$description" \ --argjson references "$references" \ '{ cve_id: $cve_id, cvss_score: $cvss_score, cvss_vector: $cvss_vector, type: $type, description: $description, references: $references }' } run_analysis() { local advisory_json="$1" local mode="$2" local output_file="$3" local advisory_id analysis_input analysis advisory_id="$(echo "$advisory_json" | jq -r '.id // "unknown"')" analysis_input="$(build_analysis_input "$advisory_json" "$mode")" if analysis="$(echo "$analysis_input" | "$PYTHON_BIN" "$ANALYZER_PATH" --json --check-exploits 2>/dev/null)"; then echo "$analysis" > "$output_file" return 0 fi echo "::warning::Failed to analyze exploitability for $advisory_id, continuing without enrichment" return 1 } enrich_single() { if ! jq -e 'type == "object"' "$INPUT_PATH" >/dev/null; then echo "ERROR: single mode expects JSON object at $INPUT_PATH" >&2 exit 1 fi local advisory analysis_file output_tmp advisory="$(cat "$INPUT_PATH")" analysis_file="$tmpdir/analysis_single.json" output_tmp="$tmpdir/output_single.json" if run_analysis "$advisory" "single" "$analysis_file"; then jq --slurpfile analysis "$analysis_file" ' . + { exploitability_score: $analysis[0].exploitability_score, exploitability_rationale: $analysis[0].exploitability_rationale, attack_vector_analysis: $analysis[0].attack_vector_analysis, exploit_detection: $analysis[0].exploit_detection } ' "$INPUT_PATH" > "$output_tmp" else cp "$INPUT_PATH" "$output_tmp" fi mv "$output_tmp" "$OUTPUT_PATH" echo "Exploitability enrichment complete (single): $OUTPUT_PATH" } enrich_batch() { if ! jq -e 'type == "array"' "$INPUT_PATH" >/dev/null; then echo "ERROR: batch mode expects JSON array at $INPUT_PATH" >&2 exit 1 fi local analyzed_count failed_count index advisory analysis_file output_tmp analyses_json analyzed_count=0 failed_count=0 index=0 analyses_json="$tmpdir/analyses.json" output_tmp="$tmpdir/output_batch.json" while IFS= read -r advisory; do analysis_file="$tmpdir/analysis_${index}.json" if run_analysis "$advisory" "batch" "$analysis_file"; then analyzed_count=$((analyzed_count + 1)) else failed_count=$((failed_count + 1)) rm -f "$analysis_file" fi index=$((index + 1)) done < <(jq -c '.[]' "$INPUT_PATH") if ls "$tmpdir"/analysis_*.json >/dev/null 2>&1; then jq -s '.' "$tmpdir"/analysis_*.json > "$analyses_json" else echo '[]' > "$analyses_json" fi jq --slurpfile analyses "$analyses_json" ' map( . as $advisory | ($analyses[0] | map(select(.cve_id == $advisory.id)) | first) as $analysis | if $analysis then $advisory + { exploitability_score: $analysis.exploitability_score, exploitability_rationale: $analysis.exploitability_rationale, attack_vector_analysis: $analysis.attack_vector_analysis, exploit_detection: $analysis.exploit_detection } else $advisory end ) ' "$INPUT_PATH" > "$output_tmp" mv "$output_tmp" "$OUTPUT_PATH" echo "Exploitability enrichment complete (batch): $OUTPUT_PATH" echo "Analyzed: $analyzed_count, failed: $failed_count" } if [[ "$MODE" == "single" ]]; then enrich_single else enrich_batch fi