mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 13:38:03 +03:00
e0eae65586
* refactor(ci): share exploitability enrichment script * refactor(scripts): reuse shared exploitability enricher in local feed
264 lines
6.8 KiB
Bash
Executable File
264 lines
6.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
scripts/ci/enrich_exploitability.sh --mode single|batch --input <path> --output <path> [--cvss-vectors <path>] [--analyzer <path>]
|
|
|
|
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
|