Exploitability Context for CVE Advisories (#89)

* feat(advisories): add exploitability context for CVE advisories

* fix(ci): align exploitability workflow with signing model

* docs(skills): add patch release changelog entries

* chore(clawsec-feed): bump version to 0.0.5

* chore(clawsec-suite): bump version to 0.1.4

* fix(clawsec-nanoclaw): align exploitability handling and nanoclaw integration

* chore(clawsec-nanoclaw): bump version to 0.0.2

* refactor(scripts): share feed path and mirror sync helpers

* refactor(utils): unify cvss vector parsing flow

* refactor(clawsec-nanoclaw): centralize advisory risk evaluation

* docs(exploitability): refresh release metadata dates

* fix(review): align feed signing and advisory dedupe

* chore(clawsec-feed): bump version to 0.0.6

* chore(clawsec-nanoclaw): bump version to 0.0.3

* fix(backfill): limit signing to target feed only

* fix(review): keep skill runtime verify-only and dedupe matching

* chore(clawsec-nanoclaw): bump version to 0.0.4

* chore(skills): align versions with published tags

* feat(feed): enrich local population with exploitability analysis

* docs(exploitability): mark backfill as historical flow
This commit is contained in:
davida-ps
2026-03-01 18:43:24 +02:00
committed by GitHub
parent 382db82483
commit 073e771b73
26 changed files with 2015 additions and 197 deletions
+68
View File
@@ -193,6 +193,74 @@ jobs:
echo "Created advisory JSON:"
cat tmp_advisory.json
- name: Set up Python for exploitability analysis
if: steps.parse.outputs.already_exists != 'true'
uses: actions/setup-python@8d2f52b6169cf4c0f64d2e9f6f8f6d6a6e1c90f7 # v5.4.1
with:
python-version: '3.10'
- name: Analyze exploitability for community advisory
if: steps.parse.outputs.already_exists != 'true'
run: |
set -euo pipefail
echo "=== Analyzing exploitability for community advisory ==="
# Extract fields from advisory for analysis
CVE_ID=$(jq -r '.id' tmp_advisory.json)
SEVERITY=$(jq -r '.severity // "medium"' tmp_advisory.json)
VULN_TYPE=$(jq -r '.type // "unknown"' tmp_advisory.json)
DESCRIPTION=$(jq -r '.description // ""' tmp_advisory.json)
REFERENCES=$(jq -c '.references // []' tmp_advisory.json)
# Map severity to approximate CVSS score for analysis
case "$SEVERITY" in
critical) CVSS_SCORE=9.5 ;;
high) CVSS_SCORE=7.5 ;;
medium) CVSS_SCORE=5.5 ;;
low) CVSS_SCORE=3.0 ;;
*) CVSS_SCORE=5.0 ;;
esac
# Build input JSON for analyzer
INPUT_JSON=$(jq -n \
--arg cve_id "$CVE_ID" \
--argjson cvss_score "$CVSS_SCORE" \
--arg type "$VULN_TYPE" \
--arg description "$DESCRIPTION" \
--argjson references "$REFERENCES" \
'{
cve_id: $cve_id,
cvss_score: $cvss_score,
type: $type,
description: $description,
references: $references
}')
# Run exploitability analysis with exploit detection.
# Continue without enrichment if analysis fails.
if ANALYSIS=$(echo "$INPUT_JSON" | python utils/analyze_exploitability.py --json --check-exploits 2>/dev/null); then
echo "$ANALYSIS" > tmp_exploitability.json
echo "✓ Analyzed $CVE_ID"
# Merge exploitability analysis into advisory
jq --slurpfile analysis tmp_exploitability.json '
. + {
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
}
' tmp_advisory.json > tmp_advisory_enriched.json
mv tmp_advisory_enriched.json tmp_advisory.json
echo "=== Exploitability analysis complete ==="
echo "Exploitability score: $(jq -r '.exploitability_score // "unknown"' tmp_advisory.json)"
else
echo "::warning::Failed to analyze exploitability for $CVE_ID, continuing without enrichment"
fi
- name: Update feed
if: steps.parse.outputs.already_exists != 'true'
run: |
+102 -2
View File
@@ -353,7 +353,9 @@ jobs:
title: (.cve.descriptions[] | select(.lang == "en") | .value | .[0:100] + (if length > 100 then "..." else "" end)),
affected: normalized_affected,
platforms: normalized_platforms,
references: [.cve.references[]?.url // empty] | unique | .[0:3]
references: [.cve.references[]?.url // empty] | unique | .[0:3],
exploitability_score: null,
exploitability_rationale: null
}]
' tmp/filtered_cves.json > tmp/nvd_current_state.json
@@ -568,7 +570,9 @@ jobs:
published: .cve.published,
references: [.cve.references[]?.url // empty] | unique | .[0:3],
cvss_score: get_cvss_score,
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id)
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id),
exploitability_score: null,
exploitability_rationale: null
}
]
' tmp/filtered_cves.json > tmp/new_advisories.json
@@ -582,6 +586,102 @@ jobs:
jq '.[].id' tmp/new_advisories.json
fi
- name: Set up Python for exploitability analysis
if: steps.transform.outputs.new_count != '0'
uses: actions/setup-python@8d2f52b6169cf4c0f64d2e9f6f8f6d6a6e1c90f7 # v5.4.1
with:
python-version: '3.10'
- name: Analyze exploitability for new advisories
if: steps.transform.outputs.new_count != '0'
run: |
set -euo pipefail
echo "=== Analyzing exploitability for new advisories ==="
# Extract CVSS vectors from filtered CVEs to merge with advisories
jq '
[.[] | {
id: .cve.id,
cvss_vector: (
.cve.metrics.cvssMetricV31[0]?.cvssData.vectorString //
.cve.metrics.cvssMetricV30[0]?.cvssData.vectorString //
.cve.metrics.cvssMetricV2[0]?.vectorString //
""
)
}] | map({(.id): .cvss_vector}) | add
' tmp/filtered_cves.json > tmp/cvss_vectors.json
# Process each advisory through exploitability analyzer
jq -c '.[]' tmp/new_advisories.json | while IFS= read -r advisory; do
CVE_ID=$(echo "$advisory" | jq -r '.id')
CVSS_SCORE=$(echo "$advisory" | jq -r '.cvss_score // 0')
CVSS_VECTOR=$(jq -r --arg id "$CVE_ID" '.[$id] // ""' tmp/cvss_vectors.json)
VULN_TYPE=$(echo "$advisory" | jq -r '.type // ""')
DESCRIPTION=$(echo "$advisory" | jq -r '.description // ""')
REFERENCES=$(echo "$advisory" | jq -c '.references // []')
# Build input JSON for analyzer
INPUT_JSON=$(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 exploitability analysis with exploit detection.
# Keep processing if any single advisory analysis fails.
if ANALYSIS=$(echo "$INPUT_JSON" | python utils/analyze_exploitability.py --json --check-exploits 2>/dev/null); then
echo "$ANALYSIS" > "tmp/exploitability_${CVE_ID}.json"
echo "✓ Analyzed $CVE_ID"
else
echo "::warning::Failed to analyze exploitability for $CVE_ID, skipping enrichment"
fi
done
# Merge exploitability analysis back into advisories.
if ls tmp/exploitability_*.json >/dev/null 2>&1; then
jq -s '.' tmp/exploitability_*.json > tmp/exploitability_analyses.json
else
echo '[]' > tmp/exploitability_analyses.json
fi
jq --slurpfile analyses tmp/exploitability_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
)
' tmp/new_advisories.json > tmp/new_advisories_enriched.json
# Replace original with enriched version
mv tmp/new_advisories_enriched.json tmp/new_advisories.json
echo "=== Exploitability analysis complete ==="
# Show summary of exploitability scores
echo "Exploitability score distribution:"
jq -r '.[] | "\(.id): \(.exploitability_score // "unknown")"' tmp/new_advisories.json | \
awk -F': ' '{scores[$2]++} END {for (s in scores) print " " s ": " scores[s]}'
- name: Update feed.json
if: steps.transform.outputs.new_count != '0' || steps.updates.outputs.update_count != '0'
run: |
+13
View File
@@ -203,6 +203,17 @@ The feed polls CVEs related to:
- Prompt injection patterns
- Agent security vulnerabilities
### Exploitability Context
ClawSec enriches CVE advisories with **exploitability context** to help agents assess real-world risk beyond raw CVSS scores. Newly analyzed advisories can include:
- **Exploit Evidence**: Whether public exploits exist in the wild
- **Weaponization Status**: If exploits are integrated into common attack frameworks
- **Attack Requirements**: Prerequisites needed for successful exploitation (network access, authentication, user interaction)
- **Risk Assessment**: Contextualized risk level combining technical severity with exploitability
This feature helps agents prioritize vulnerabilities that pose immediate threats versus theoretical risks, enabling smarter security decisions.
### Advisory Schema
**NVD CVE Advisory:**
@@ -217,6 +228,8 @@ The feed polls CVEs related to:
"published": "2026-02-01T00:00:00Z",
"cvss_score": 8.8,
"nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-XXXXX",
"exploitability_score": "high|medium|low|unknown",
"exploitability_rationale": "Why this CVE is or is not likely exploitable in agent deployments",
"references": ["..."],
"action": "Recommended remediation"
}
+281
View File
@@ -0,0 +1,281 @@
#!/bin/bash
# backfill-exploitability.sh
# Adds exploitability scoring to existing advisories in feed.json that don't have it yet.
# Historical maintenance utility: normal advisory generation should use
# poll-nvd workflow (init/reset when rebuilding) or populate-local-feed.sh.
#
# Usage: ./scripts/backfill-exploitability.sh [--dry-run] [--feed PATH]
# --dry-run Show what would be updated without making changes
# --feed PATH Use specified feed file (default: advisories/feed.json)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# shellcheck source=./feed-utils.sh
source "$SCRIPT_DIR/feed-utils.sh"
# Configuration
init_feed_paths "$PROJECT_ROOT"
ANALYZER="$PROJECT_ROOT/utils/analyze_exploitability.py"
SIGNING_PRIVATE_KEY="${CLAWSEC_FEED_SIGNING_PRIVATE_KEY_PATH:-${CLAWSEC_SIGNING_PRIVATE_KEY_PATH:-$PROJECT_ROOT/clawsec-signing-private.pem}}"
SIGNING_PUBLIC_KEY="${CLAWSEC_FEED_SIGNING_PUBLIC_KEY_PATH:-${CLAWSEC_SIGNING_PUBLIC_KEY_PATH:-$PROJECT_ROOT/clawsec-signing-public.pem}}"
SIGNING_PASSPHRASE="${CLAWSEC_FEED_SIGNING_PRIVATE_KEY_PASSPHRASE:-${CLAWSEC_SIGNING_PRIVATE_KEY_PASSPHRASE:-}}"
sign_and_verify_feed_signature() {
local feed_file="$1"
local signature_file="$2"
local tmp_dir
local tmp_signature
local signature_bin
local passin_file
tmp_dir=$(mktemp -d)
tmp_signature="${signature_file}.tmp.$$"
signature_bin="$tmp_dir/signature.bin"
passin_file="$tmp_dir/passin.txt"
if [ -n "$SIGNING_PASSPHRASE" ]; then
printf '%s' "$SIGNING_PASSPHRASE" > "$passin_file"
chmod 600 "$passin_file"
if ! openssl pkeyutl -sign -rawin -inkey "$SIGNING_PRIVATE_KEY" -passin "file:$passin_file" -in "$feed_file" \
| openssl base64 -A > "$tmp_signature"; then
rm -rf "$tmp_dir"
rm -f "$tmp_signature"
echo "Error: Failed to sign $feed_file" >&2
return 1
fi
elif ! openssl pkeyutl -sign -rawin -inkey "$SIGNING_PRIVATE_KEY" -in "$feed_file" \
| openssl base64 -A > "$tmp_signature"; then
rm -rf "$tmp_dir"
rm -f "$tmp_signature"
echo "Error: Failed to sign $feed_file" >&2
return 1
fi
if ! openssl base64 -d -A -in "$tmp_signature" -out "$signature_bin"; then
rm -rf "$tmp_dir"
rm -f "$tmp_signature"
echo "Error: Failed to decode generated signature for $feed_file" >&2
return 1
fi
if ! openssl pkeyutl -verify -rawin -pubin -inkey "$SIGNING_PUBLIC_KEY" -sigfile "$signature_bin" -in "$feed_file" >/dev/null; then
rm -rf "$tmp_dir"
rm -f "$tmp_signature"
echo "Error: Signature verification failed after signing $feed_file" >&2
return 1
fi
mv "$tmp_signature" "$signature_file"
rm -rf "$tmp_dir"
echo "✓ Re-signed and verified: $signature_file"
}
# Parse args
DRY_RUN=false
REQUIRE_SIGNING=false
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run)
DRY_RUN=true
shift
;;
--feed)
FEED_PATH="$2"
shift 2
;;
*)
echo "Unknown option: $1"
echo "Usage: $0 [--dry-run] [--feed PATH]"
exit 1
;;
esac
done
echo "=== ClawSec Exploitability Backfill ==="
echo "Feed path: $FEED_PATH"
echo "Dry run: $DRY_RUN"
echo ""
# Verify prerequisites
if [ ! -f "$FEED_PATH" ]; then
echo "Error: Feed file not found: $FEED_PATH"
exit 1
fi
if [ ! -f "$ANALYZER" ]; then
echo "Error: Analyzer script not found: $ANALYZER"
exit 1
fi
# Check Python availability
if ! command -v python3 &> /dev/null; then
echo "Error: python3 is required but not found in PATH"
exit 1
fi
# Verify analyzer works
if ! python3 "$ANALYZER" --help &> /dev/null; then
echo "Error: Analyzer script failed to run. Check Python environment."
exit 1
fi
# Determine whether detached signatures must be regenerated.
# Runtime agents that only have public keys should run in dry-run mode.
if [ "$DRY_RUN" = "false" ]; then
if [ -f "${FEED_PATH}.sig" ]; then
REQUIRE_SIGNING=true
fi
fi
if [ "$REQUIRE_SIGNING" = "true" ]; then
if ! command -v openssl &> /dev/null; then
echo "Error: openssl is required for detached signature signing/verification"
exit 1
fi
if [ ! -f "$SIGNING_PRIVATE_KEY" ]; then
echo "Error: Signing private key not found: $SIGNING_PRIVATE_KEY"
echo "This backfill updates signed feed artifacts. Use --dry-run in public-key-only environments."
exit 1
fi
if [ ! -f "$SIGNING_PUBLIC_KEY" ]; then
echo "Error: Signing public key not found: $SIGNING_PUBLIC_KEY"
exit 1
fi
fi
# Create temp directory
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
echo "=== Analyzing Feed ==="
# Extract advisories without exploitability_score
jq '.advisories | map(select(.exploitability_score == null or .exploitability_score == ""))' \
"$FEED_PATH" > "$TEMP_DIR/missing_exploitability.json"
MISSING_COUNT=$(jq 'length' "$TEMP_DIR/missing_exploitability.json")
TOTAL_COUNT=$(jq '.advisories | length' "$FEED_PATH")
ALREADY_DONE=$((TOTAL_COUNT - MISSING_COUNT))
echo "Total advisories: $TOTAL_COUNT"
echo "Already have exploitability: $ALREADY_DONE"
echo "Missing exploitability: $MISSING_COUNT"
echo ""
if [ "$MISSING_COUNT" -eq 0 ]; then
echo "✓ All advisories already have exploitability scores!"
exit 0
fi
if [ "$DRY_RUN" = "true" ]; then
echo "=== Dry Run - Would Update These Advisories ==="
jq -r '.[] | .id' "$TEMP_DIR/missing_exploitability.json"
echo ""
echo "Total advisories to update: $MISSING_COUNT"
exit 0
fi
echo "=== Processing Advisories ==="
# Process each advisory
PROCESSED=0
FAILED=0
# Read original feed to preserve all metadata
cp "$FEED_PATH" "$TEMP_DIR/feed_working.json"
while IFS= read -r advisory; do
CVE_ID=$(echo "$advisory" | jq -r '.id')
echo -n "Processing $CVE_ID... "
# Prepare input for analyzer
ANALYZER_INPUT=$(echo "$advisory" | jq '{
cve_id: .id,
cvss_score: (.cvss_score // 0.0),
type: .type,
description: .description,
references: (.references // [])
}')
# Run analyzer
if ANALYSIS=$(echo "$ANALYZER_INPUT" | python3 "$ANALYZER" --json --check-exploits 2>/dev/null); then
# Extract exploitability fields
EXPL_SCORE=$(echo "$ANALYSIS" | jq -r '.exploitability_score // "unknown"')
EXPL_RATIONALE=$(echo "$ANALYSIS" | jq -r '.exploitability_rationale // "No rationale available"')
# Update advisory in working feed
jq --arg id "$CVE_ID" \
--arg score "$EXPL_SCORE" \
--arg rationale "$EXPL_RATIONALE" \
'(.advisories[] | select(.id == $id)) |= (. + {
exploitability_score: $score,
exploitability_rationale: $rationale
})' "$TEMP_DIR/feed_working.json" > "$TEMP_DIR/feed_updated.json"
mv "$TEMP_DIR/feed_updated.json" "$TEMP_DIR/feed_working.json"
echo "$EXPL_SCORE"
PROCESSED=$((PROCESSED + 1))
else
echo "✗ Failed"
FAILED=$((FAILED + 1))
fi
done < <(jq -c '.[]' "$TEMP_DIR/missing_exploitability.json")
# Check if loop executed successfully
if [ ! -f "$TEMP_DIR/feed_working.json" ]; then
echo "Error: Feed processing failed"
exit 1
fi
echo ""
echo "=== Processing Complete ==="
echo "Processed: $PROCESSED"
echo "Failed: $FAILED"
echo ""
# Write updated feed
echo "Writing updated feed to: $FEED_PATH"
cp "$TEMP_DIR/feed_working.json" "$FEED_PATH"
# Update feed version and timestamp
CURRENT_VERSION=$(jq -r '.version' "$FEED_PATH")
UPDATED_TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
jq --arg ts "$UPDATED_TS" '.updated = $ts' "$FEED_PATH" > "$TEMP_DIR/feed_final.json"
mv "$TEMP_DIR/feed_final.json" "$FEED_PATH"
echo "✓ Updated feed version: $CURRENT_VERSION"
echo "✓ Updated timestamp: $UPDATED_TS"
echo ""
if [ "$REQUIRE_SIGNING" = "true" ]; then
echo ""
echo "=== Re-signing Advisory Feed ==="
if [ -f "${FEED_PATH}.sig" ]; then
if ! sign_and_verify_feed_signature "$FEED_PATH" "${FEED_PATH}.sig"; then
exit 1
fi
fi
fi
echo ""
echo "=== Summary ==="
echo "✓ Backfill complete!"
echo "$PROCESSED advisories updated with exploitability scores"
if [ "$FAILED" -gt 0 ]; then
echo "$FAILED advisories failed analysis (kept original data)"
fi
# Verify final state
FINAL_MISSING=$(jq '.advisories | map(select(.exploitability_score == null or .exploitability_score == "")) | length' "$FEED_PATH")
echo "✓ Advisories still missing exploitability: $FINAL_MISSING"
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
# feed-utils.sh
# Shared advisory feed path and sync helpers for local/maintenance scripts.
init_feed_paths() {
local project_root="$1"
: "${FEED_PATH:=$project_root/advisories/feed.json}"
: "${SKILL_FEED_PATH:=$project_root/skills/clawsec-feed/advisories/feed.json}"
: "${PUBLIC_FEED_PATH:=$project_root/public/advisories/feed.json}"
}
sync_feed_to_mirrors() {
local source_feed="$1"
local mode="${2:-create}"
local target
for target in "$SKILL_FEED_PATH" "$PUBLIC_FEED_PATH"; do
case "$mode" in
create)
mkdir -p "$(dirname "$target")"
cp "$source_feed" "$target"
echo "✓ Updated: $target"
;;
existing-only)
if [ -f "$target" ]; then
cp "$source_feed" "$target"
echo "✓ Updated: $target"
fi
;;
*)
echo "Error: unsupported mirror sync mode: $mode" >&2
return 1
;;
esac
done
}
+109 -16
View File
@@ -11,13 +11,14 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# shellcheck source=./feed-utils.sh
source "$SCRIPT_DIR/feed-utils.sh"
# Configuration - same as pipeline
FEED_PATH="$PROJECT_ROOT/advisories/feed.json"
SKILL_FEED_PATH="$PROJECT_ROOT/skills/clawsec-feed/advisories/feed.json"
PUBLIC_FEED_PATH="$PROJECT_ROOT/public/advisories/feed.json"
init_feed_paths "$PROJECT_ROOT"
KEYWORDS="OpenClaw clawdbot Moltbot NanoClaw WhatsApp-bot baileys"
GITHUB_REF_PATTERN="github.com/openclaw/openclaw github.com/qwibitai/NanoClaw"
ANALYZER="$PROJECT_ROOT/utils/analyze_exploitability.py"
# Parse args
DAYS_BACK=120
@@ -46,6 +47,22 @@ echo "Days back: $DAYS_BACK"
echo "Force mode: $FORCE"
echo ""
# Verify exploitability analyzer prerequisites
if ! command -v python3 &> /dev/null; then
echo "Error: python3 is required but not found in PATH"
exit 1
fi
if [ ! -f "$ANALYZER" ]; then
echo "Error: Exploitability analyzer not found: $ANALYZER"
exit 1
fi
if ! python3 "$ANALYZER" --help &> /dev/null; then
echo "Error: Exploitability analyzer failed to run. Check Python environment."
exit 1
fi
# Create temp directory
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
@@ -62,7 +79,7 @@ fi
if [ -z "${START_DATE:-}" ]; then
# macOS vs Linux date compatibility
if date -v-1d > /dev/null 2>&1; then
START_DATE=$(date -u -v-${DAYS_BACK}d +%Y-%m-%dT%H:%M:%S.000Z)
START_DATE=$(date -u -v-"${DAYS_BACK}"d +%Y-%m-%dT%H:%M:%S.000Z)
else
START_DATE=$(date -u -d "${DAYS_BACK} days ago" +%Y-%m-%dT%H:%M:%S.000Z)
fi
@@ -74,8 +91,8 @@ echo "End date: $END_DATE"
echo ""
# URL encode dates
START_ENC=$(echo "$START_DATE" | sed 's/:/%3A/g')
END_ENC=$(echo "$END_DATE" | sed 's/:/%3A/g')
START_ENC=${START_DATE//:/%3A}
END_ENC=${END_DATE//:/%3A}
echo "=== Fetching CVEs from NVD ==="
@@ -281,7 +298,9 @@ jq --argjson existing "$EXISTING_JSON" '
published: .cve.published,
references: [.cve.references[]?.url // empty] | unique | .[0:3],
cvss_score: get_cvss_score,
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id)
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id),
exploitability_score: null,
exploitability_rationale: null
}
]
' "$TEMP_DIR/filtered_cves.json" > "$TEMP_DIR/new_advisories.json"
@@ -296,6 +315,87 @@ if [ "$NEW_COUNT" -eq 0 ]; then
exit 0
fi
echo ""
echo "=== Analyzing Exploitability ==="
# Build CVSS vector lookup for enriched analysis inputs.
jq '
[.[] | {
id: .cve.id,
cvss_vector: (
.cve.metrics.cvssMetricV31[0]?.cvssData.vectorString //
.cve.metrics.cvssMetricV30[0]?.cvssData.vectorString //
.cve.metrics.cvssMetricV2[0]?.vectorString //
""
)
}] | map({(.id): .cvss_vector}) | add
' "$TEMP_DIR/filtered_cves.json" > "$TEMP_DIR/cvss_vectors.json"
ANALYZED_COUNT=0
FAILED_ANALYSIS=0
while IFS= read -r advisory; do
CVE_ID=$(echo "$advisory" | jq -r '.id')
CVSS_SCORE=$(echo "$advisory" | jq -r '.cvss_score // 0')
CVSS_VECTOR=$(jq -r --arg id "$CVE_ID" '.[$id] // ""' "$TEMP_DIR/cvss_vectors.json")
VULN_TYPE=$(echo "$advisory" | jq -r '.type // ""')
DESCRIPTION=$(echo "$advisory" | jq -r '.description // ""')
REFERENCES=$(echo "$advisory" | jq -c '.references // []')
INPUT_JSON=$(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
}')
if ANALYSIS=$(echo "$INPUT_JSON" | python3 "$ANALYZER" --json --check-exploits 2>/dev/null); then
echo "$ANALYSIS" > "$TEMP_DIR/exploitability_${CVE_ID}.json"
SCORE=$(echo "$ANALYSIS" | jq -r '.exploitability_score // "unknown"')
echo "$CVE_ID -> $SCORE"
ANALYZED_COUNT=$((ANALYZED_COUNT + 1))
else
echo "$CVE_ID analysis failed; keeping null exploitability fields"
FAILED_ANALYSIS=$((FAILED_ANALYSIS + 1))
fi
done < <(jq -c '.[]' "$TEMP_DIR/new_advisories.json")
if ls "$TEMP_DIR"/exploitability_*.json >/dev/null 2>&1; then
jq -s '.' "$TEMP_DIR"/exploitability_*.json > "$TEMP_DIR/exploitability_analyses.json"
else
echo '[]' > "$TEMP_DIR/exploitability_analyses.json"
fi
jq --slurpfile analyses "$TEMP_DIR/exploitability_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
)
' "$TEMP_DIR/new_advisories.json" > "$TEMP_DIR/new_advisories_enriched.json"
mv "$TEMP_DIR/new_advisories_enriched.json" "$TEMP_DIR/new_advisories.json"
echo "Exploitability analysis complete: $ANALYZED_COUNT analyzed, $FAILED_ANALYSIS failed"
echo ""
echo "=== New Advisories ==="
jq -r '.[] | " \(.id) [\(.severity)] - \(.title)"' "$TEMP_DIR/new_advisories.json"
@@ -339,15 +439,8 @@ if jq empty "$TEMP_DIR/updated_feed.json" 2>/dev/null; then
cp "$TEMP_DIR/updated_feed.json" "$FEED_PATH"
echo "✓ Updated: $FEED_PATH"
# Update skill feed
mkdir -p "$(dirname "$SKILL_FEED_PATH")"
cp "$FEED_PATH" "$SKILL_FEED_PATH"
echo "✓ Updated: $SKILL_FEED_PATH"
# Update public feed for local dev
mkdir -p "$(dirname "$PUBLIC_FEED_PATH")"
cp "$FEED_PATH" "$PUBLIC_FEED_PATH"
echo "✓ Updated: $PUBLIC_FEED_PATH"
# Sync feed mirrors for local skill/public consumers.
sync_feed_to_mirrors "$FEED_PATH" "create"
echo ""
TOTAL_ADVISORIES=$(jq '.advisories | length' "$FEED_PATH")
+3 -3
View File
@@ -76,13 +76,13 @@ fi
# ESLint
echo -e "\n${YELLOW}Running ESLint...${NC}"
if $FIX_MODE; then
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --fix; then
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --ignore-pattern '.auto-claude/**' --fix; then
check_pass "ESLint (with auto-fix)"
else
check_fail "ESLint found unfixable issues"
fi
else
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --max-warnings 0; then
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --ignore-pattern '.auto-claude/**' --max-warnings 0; then
check_pass "ESLint"
else
check_fail "ESLint found issues (run with --fix to auto-fix)"
@@ -190,7 +190,7 @@ print_header "Security"
# Trivy FS Scan
if command -v trivy &> /dev/null; then
echo -e "\n${YELLOW}Running Trivy filesystem scan...${NC}"
if trivy fs . --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed; then
if trivy fs . --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed --skip-dirs .auto-claude --skip-files clawsec-signing-private.pem; then
check_pass "Trivy filesystem scan"
else
check_fail "Trivy found CRITICAL/HIGH vulnerabilities"
+19
View File
@@ -0,0 +1,19 @@
# Changelog
All notable changes to the ClawSec Feed skill will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.0.5] - 2026-02-28
### Added
- Exploitability-focused advisory guidance, including filtering and prioritization examples.
- Notification examples that include exploitability context and rationale.
### Changed
- Clarified exploitability scoring guidance to match runtime values (`high|medium|low|unknown`).
- Updated response-priority guidance to align with exploitability-first triage.
- De-duplicated exploitability filtering guidance in `SKILL.md` by pointing to canonical docs in `wiki/exploitability-scoring.md` and `clawsec-suite`.
+96 -4
View File
@@ -1,6 +1,6 @@
---
name: clawsec-feed
version: 0.0.4
version: 0.0.5
description: Security advisory feed with automated NVD CVE polling for OpenClaw-related vulnerabilities. Updated daily.
homepage: https://clawsec.prompt.security
metadata: {"openclaw":{"emoji":"📡","category":"security"}}
@@ -318,7 +318,9 @@ curl -sSL --fail --show-error --retry 3 --retry-delay 1 "$FEED_URL"
"description": "Skill sends user data to external server",
"affected": ["helper-plus@1.0.0", "helper-plus@1.0.1"],
"action": "Remove immediately",
"published": "2026-02-01T10:00:00Z"
"published": "2026-02-01T10:00:00Z",
"exploitability_score": "critical",
"exploitability_rationale": "Trivially exploitable through normal skill usage; no special conditions required. Active exploitation observed in the wild."
}
]
}
@@ -385,6 +387,42 @@ fi
echo "$RECENT"
```
### Filter by exploitability score
Shared exploitability prioritization guidance is maintained in:
- [`wiki/exploitability-scoring.md`](../../wiki/exploitability-scoring.md)
- [`skills/clawsec-suite/SKILL.md`](../clawsec-suite/SKILL.md) ("Quick feed check")
### Get exploitability context for an advisory
```bash
# Show exploitability details for a specific CVE
CVE_ID="CVE-2026-27488"
echo "$FEED" | jq --arg cve "$CVE_ID" '.advisories[] | select(.id == $cve) | {
id: .id,
severity: .severity,
exploitability_score: .exploitability_score,
exploitability_rationale: .exploitability_rationale,
title: .title
}'
```
### Prioritize advisories by exploitability
```bash
# Sort advisories by exploitability (critical → high → medium → low)
# This helps agents focus on the most immediately actionable threats
echo "$FEED" | jq '[.advisories[] | select(.exploitability_score != null)] |
sort_by(
if .exploitability_score == "critical" then 0
elif .exploitability_score == "high" then 1
elif .exploitability_score == "medium" then 2
elif .exploitability_score == "low" then 3
else 4 end
)'
```
---
## Cross-Reference Installed Skills
@@ -476,23 +514,75 @@ done
---
## Prioritizing High-Exploitability Threats
**IMPORTANT:** When reviewing advisories, always prioritize by **exploitability score** in addition to severity. The exploitability score indicates how easily a vulnerability can be exploited in practice, helping you focus on the most actionable threats.
### Exploitability Priority Levels
| Exploitability | Meaning | Action Priority |
|----------------|---------|-----------------|
| `high` | Trivially or easily exploitable with public tooling | **Immediate notification** |
| `medium` | Exploitable but requires specific conditions | **Standard notification** |
| `low` | Difficult to exploit or theoretical | **Low priority notification** |
### How to Use Exploitability in Notifications
1. **Filter for high-exploitability first:**
```bash
# Get high exploitability advisories
echo "$FEED" | jq '.advisories[] | select(.exploitability_score == "high")'
```
2. **Include exploitability in notifications:**
```
📡 ClawSec Feed: High-exploitability alert
CRITICAL - CVE-2026-27488 (Exploitability: HIGH)
→ Trivially exploitable RCE in skill-loader v2.1.0
→ Public exploit code available
→ Recommended action: Immediate removal or upgrade to v2.1.1
```
3. **Prioritize by both severity AND exploitability:**
- A HIGH severity + HIGH exploitability CVE is more urgent than a CRITICAL severity + LOW exploitability CVE
- Focus user attention on threats that are both severe and easily exploitable
- Include the exploitability rationale to help users understand the risk context
### Example Notification Priority Order
When multiple advisories exist, present them in this order:
1. **Critical severity + High exploitability** - most urgent
2. **High severity + High exploitability**
3. **Critical severity + Medium/Low exploitability**
4. **High severity + Medium/Low exploitability**
5. **Medium/Low severity** (any exploitability)
This ensures you alert users to the most actionable, immediately dangerous threats first.
---
## When to Notify Your User
**Notify Immediately (Critical):**
- New critical advisory affecting an installed skill
- Active exploitation detected
- **High exploitability score** (regardless of severity)
**Notify Soon (High):**
- New high-severity advisory affecting installed skills
- Failed to fetch advisory feed (network issue?)
- Medium exploitability with high severity
**Notify at Next Interaction (Medium):**
- New medium-severity advisories
- General security updates
- Low exploitability advisories
**Log Only (Low/Info):**
- Low-severity advisories (mention if user asks)
- Feed checked, no new advisories
- Theoretical vulnerabilities (low exploitability, low severity)
---
@@ -503,11 +593,13 @@ done
```
📡 ClawSec Feed: 2 new advisories since last check
CRITICAL - GA-2026-015: Malicious prompt pattern "ignore-all"
CRITICAL - GA-2026-015: Malicious prompt pattern "ignore-all" (Exploitability: HIGH)
→ Detected prompt injection technique. Update your system prompt defenses.
→ Exploitability: Easily exploitable with publicly documented techniques.
HIGH - GA-2026-016: Vulnerable skill "data-helper" v1.2.0
HIGH - GA-2026-016: Vulnerable skill "data-helper" v1.2.0 (Exploitability: MEDIUM)
→ You have this installed! Recommended action: Update to v1.2.1 or remove.
→ Exploitability: Requires specific configuration; not trivially exploitable.
```
### If nothing new:
+6 -1
View File
@@ -1,6 +1,6 @@
{
"name": "clawsec-feed",
"version": "0.0.4",
"version": "0.0.5",
"description": "Security advisory feed monitoring for AI agents. Subscribe to community-driven threat intelligence.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -21,6 +21,11 @@
"required": true,
"description": "Advisory feed skill documentation"
},
{
"path": "CHANGELOG.md",
"required": true,
"description": "Version history for advisory feed updates"
},
{
"path": "advisories/feed.json",
"required": true,
+20
View File
@@ -0,0 +1,20 @@
# Changelog
All notable changes to the ClawSec NanoClaw compatibility skill will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.0.2] - 2026-02-28
### Added
- Exploitability-aware advisory output in NanoClaw MCP tools (`exploitability_score`, `exploitability_rationale`).
- Exploitability filtering (`exploitabilityScore`) for `clawsec_list_advisories`.
### Changed
- Updated NanoClaw advisory sorting and pre-install safety recommendation logic to prioritize exploitability context.
- Updated NanoClaw integration docs to match current host/container integration points (`src/ipc.ts`, `src/index.ts`) and current cache schema.
- Removed duplicate exploitability normalization logic from MCP advisory tools and now reuse `normalizeExploitabilityScore` from `lib/risk.ts`.
- Reused `matchesAffectedSpecifier` from `lib/advisories.ts` in MCP advisory tools to keep skill/version matching logic centralized and consistent.
+44 -30
View File
@@ -8,7 +8,7 @@ ClawSec provides security advisory monitoring for NanoClaw through:
- **MCP Tools**: Agents can check for vulnerabilities via `clawsec_check_advisories`
- **Advisory Feed**: Automatic monitoring of https://clawsec.prompt.security/advisories/feed.json
- **Signature Verification**: Ed25519-signed feeds ensure integrity
- **Platform Targeting**: Advisories can be NanoClaw-specific or cross-platform
- **Exploitability Context**: Advisories include exploitability score and rationale for triage
## Prerequisites
@@ -57,18 +57,30 @@ in each tool file).
Add the host-side IPC handlers for ClawSec operations.
**File**: `host/ipc-handler.ts`
**File**: `src/ipc.ts`
```typescript
// Add this import at the top
import { registerClawSecHandlers } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
// Add these imports at the top
import { handleAdvisoryIpc } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
import { SkillSignatureVerifier } from '../skills/clawsec-nanoclaw/host-services/skill-signature-handler.js';
// In your IPC handler setup function
export function setupIpcHandlers() {
// ... your existing handlers ...
// Initialize these once in host startup and pass through deps
const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger);
const signatureVerifier = new SkillSignatureVerifier();
// Register ClawSec handlers
registerClawSecHandlers();
// In processTaskIpc switch:
case 'refresh_advisory_cache':
case 'verify_skill_signature':
await handleAdvisoryIpc(
data,
{ advisoryCacheManager, signatureVerifier },
logger,
sourceGroup
);
break;
default:
// existing task handling
}
```
@@ -76,23 +88,25 @@ export function setupIpcHandlers() {
Add the advisory cache manager to your host services.
**File**: `host/index.ts` (or your main entry point)
**File**: `src/index.ts` (or your main entry point)
```typescript
// Add this import
import { startAdvisoryCache } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
// Start the service when your host process starts
async function main() {
// ... your existing initialization ...
// Start ClawSec advisory cache (fetches feed every 6 hours)
startAdvisoryCache({
cacheFile: '/workspace/project/data/clawsec-advisory-cache.json',
feedUrl: 'https://clawsec.prompt.security/advisories/feed.json',
publicKeyPath: '/workspace/project/skills/clawsec-nanoclaw/advisories/feed-signing-public.pem',
refreshInterval: 6 * 60 * 60 * 1000, // 6 hours
// Initialize cache manager and prime it at startup
const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger);
await advisoryCacheManager.initialize();
// Recommended refresh cadence (6h)
setInterval(() => {
advisoryCacheManager.refresh().catch((error) => {
logger.error({ error }, 'Periodic advisory cache refresh failed');
});
}, 6 * 60 * 60 * 1000);
// ... rest of your startup ...
}
@@ -151,9 +165,9 @@ cat /workspace/project/data/clawsec-advisory-cache.json
You should see:
- `feed`: Array of advisories
- `signature`: Ed25519 signature
- `lastFetch`: Timestamp of last update
- `fetchedAt`: Timestamp of last update
- `verified`: Should be `true`
- `publicKeyFingerprint`: SHA-256 fingerprint of the pinned signing key
## Usage Examples
@@ -183,13 +197,13 @@ You can also call the MCP tools directly from agent code:
```typescript
// Check all installed skills
const result = await tools.clawsec_check_advisories({
skillsRoot: '/workspace/project/skills'
installRoot: '/home/node/.claude/skills'
});
// Check specific skill before installation
const safetyCheck = await tools.clawsec_check_skill_safety({
skillName: 'risky-skill',
version: '1.0.0'
skillVersion: '1.0.0'
});
```
@@ -199,19 +213,19 @@ const safetyCheck = await tools.clawsec_check_skill_safety({
Default: `/workspace/project/data/clawsec-advisory-cache.json`
To change, update the `cacheFile` parameter in `startAdvisoryCache()`.
To change, pass a different data directory path to `new AdvisoryCacheManager(dataDir, logger)`.
### Refresh Interval
Default: 6 hours
To change, update the `refreshInterval` parameter (in milliseconds).
To change, update the `setInterval(...)` duration (in milliseconds) in host startup.
### Feed URL
Default: `https://clawsec.prompt.security/advisories/feed.json`
To use a mirror or custom feed, update the `feedUrl` parameter.
To use a mirror or custom feed, update `FEED_URL` in `skills/clawsec-nanoclaw/host-services/advisory-cache.ts`.
## Platform-Specific Advisories
@@ -222,7 +236,7 @@ ClawSec advisories can target specific platforms:
- **`platforms: ["openclaw", "nanoclaw"]`**: Affects both
- **No `platforms` field**: Applies to all platforms
The MCP tools automatically filter advisories based on your platform.
Platform metadata is preserved in advisory records and can be filtered by your policy layer.
## Security
@@ -260,7 +274,7 @@ Never manually edit the cache file - it will break signature verification.
**Problem**: Advisory cache is empty or stale
**Solution**:
1. Check that `startAdvisoryCache()` is called in your host entry point
1. Check that `AdvisoryCacheManager.initialize()` is called in your host entry point
2. Verify network access to `clawsec.prompt.security`
3. Check host logs for fetch errors
4. Manually trigger: `curl https://clawsec.prompt.security/advisories/feed.json`
@@ -280,7 +294,7 @@ Never manually edit the cache file - it will break signature verification.
**Problem**: Tools return errors about IPC
**Solution**:
1. Verify IPC handlers are registered in `host/ipc-handler.ts`
1. Verify IPC handlers are registered in `src/ipc.ts`
2. Check that IPC directory exists and is writable
3. Ensure host process is running
4. Check host logs for handler errors
@@ -290,8 +304,8 @@ Never manually edit the cache file - it will break signature verification.
To remove ClawSec from NanoClaw:
1. Remove MCP tool registration from `ipc-mcp-stdio.ts`
2. Remove IPC handler registration from `host/ipc-handler.ts`
3. Remove `startAdvisoryCache()` call from host entry point
2. Remove IPC handler registration from `src/ipc.ts`
3. Remove `AdvisoryCacheManager` initialization from host entry point
4. Delete the skill directory: `rm -rf skills/clawsec-nanoclaw`
5. Delete the cache file: `rm /workspace/project/data/clawsec-advisory-cache.json`
6. Restart NanoClaw
+8 -8
View File
@@ -56,9 +56,9 @@ ClawSec provides a complete security skill for NanoClaw deployments:
- `clawsec_integrity_status` - View file baseline status
- `clawsec_verify_audit` - Verify audit log hash chain
- **Advisory Cache Service**: Automatic feed fetching every 6 hours
- **Advisory Cache Service**: Host-managed feed fetching with signature validation
- **Signature Verification**: Ed25519-signed feeds ensure integrity
- **Platform Filtering**: Shows only relevant advisories for NanoClaw
- **Exploitability Context**: Surfaces `exploitability_score` and rationale to reduce alert fatigue
- **IPC Communication**: Container-safe host communication
### Installation
@@ -77,19 +77,19 @@ The skill integrates into three places:
**1. MCP Tools** (container):
```typescript
// container/agent-runner/src/ipc-mcp-stdio.ts
import { clawsecTools } from '../../../skills/clawsec-nanoclaw/mcp-tools/advisory-tools.js';
import '../../../skills/clawsec-nanoclaw/mcp-tools/advisory-tools.js';
```
**2. IPC Handlers** (host):
```typescript
// host/ipc-handler.ts
import { registerClawSecHandlers } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
// src/ipc.ts
import { handleAdvisoryIpc } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
```
**3. Cache Service** (host):
```typescript
// host/index.ts
import { startAdvisoryCache } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
// src/index.ts
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
```
### Advisory Feed
@@ -148,4 +148,4 @@ Planned features for future releases:
- **Issues**: https://github.com/prompt-security/clawsec/issues
- **Security**: security@prompt.security
- NanoClaw Repository: (link TBD)
- NanoClaw Repository: https://github.com/qwibitai/nanoclaw
+32 -27
View File
@@ -1,6 +1,6 @@
---
name: clawsec-nanoclaw
version: 0.0.1
version: 0.0.2
description: Use when checking for security vulnerabilities in NanoClaw skills, before installing new skills, or when asked about security advisories affecting the bot
---
@@ -10,7 +10,7 @@ Security advisory monitoring that protects your WhatsApp bot from known vulnerab
## Overview
ClawSec provides MCP tools that check installed skills against a curated feed of security advisories. It prevents installation of vulnerable skills and alerts you to issues in existing ones.
ClawSec provides MCP tools that check installed skills against a curated feed of security advisories. It prevents installation of vulnerable skills, includes exploitability context for triage, and alerts you to issues in existing ones.
**Core principle:** Check before you install. Monitor what's running.
@@ -36,7 +36,7 @@ Do NOT use for:
// Before installing any skill
const safety = await tools.clawsec_check_skill_safety({
skillName: 'new-skill',
version: '1.0.0' // optional
skillVersion: '1.0.0' // optional
});
if (!safety.safe) {
@@ -48,14 +48,16 @@ if (!safety.safe) {
### Security Audit
```typescript
// Check all installed skills
// Check all installed skills (defaults to ~/.claude/skills in the container)
const result = await tools.clawsec_check_advisories({
skillsRoot: '/workspace/project/skills' // optional
installRoot: '/home/node/.claude/skills' // optional
});
if (result.criticalCount > 0) {
if (result.matches.some((m) =>
m.advisory.severity === 'critical' || m.advisory.exploitability_score === 'high'
)) {
// Alert user immediately
console.error('CRITICAL vulnerabilities found!');
console.error('Urgent advisories found!');
}
```
@@ -64,8 +66,8 @@ if (result.criticalCount > 0) {
```typescript
// List advisories with filters
const advisories = await tools.clawsec_list_advisories({
platform: 'nanoclaw', // optional: nanoclaw, openclaw, or both
severity: 'critical' // optional: critical, high, medium, low
severity: 'high', // optional
exploitabilityScore: 'high' // optional
});
```
@@ -75,7 +77,7 @@ const advisories = await tools.clawsec_list_advisories({
|------|------|---------------|
| Pre-install check | `clawsec_check_skill_safety` | `skillName` |
| Audit all skills | `clawsec_check_advisories` | `installRoot` (optional) |
| Browse feed | `clawsec_list_advisories` | `severity`, `type` (optional) |
| Browse feed | `clawsec_list_advisories` | `severity`, `type`, `exploitabilityScore` (optional) |
| Verify package signature | `clawsec_verify_skill_package` | `packagePath` |
| Refresh advisory cache | `clawsec_refresh_cache` | (none) |
| Check file integrity | `clawsec_check_integrity` | `mode`, `autoRestore` (optional) |
@@ -110,7 +112,7 @@ if (safety.safe) {
```typescript
// Add to scheduled tasks
schedule_task({
prompt: "Check for security advisories using clawsec_check_advisories and alert if any critical issues found",
prompt: "Check advisories using clawsec_check_advisories and alert when critical or high-exploitability matches appear",
schedule_type: "cron",
schedule_value: "0 9 * * *" // Daily at 9am
});
@@ -125,8 +127,8 @@ You: I'll check installed skills for known vulnerabilities.
[Use clawsec_check_advisories]
Response:
✅ No critical issues found.
- 2 low-severity advisories (not urgent)
✅ No urgent issues found.
- 2 low-severity/low-exploitability advisories
- All skills up to date
```
@@ -146,30 +148,33 @@ const safety = await tools.clawsec_check_skill_safety({
if (safety.safe) await installSkill('untrusted-skill');
```
### ❌ Ignoring platform filters
### ❌ Ignoring exploitability context
```typescript
// DON'T: Check OpenClaw advisories on NanoClaw
const advisories = await tools.clawsec_list_advisories({
platform: 'openclaw' // Wrong platform!
});
// DON'T: Use severity only
if (advisory.severity === 'high') {
notifyNow(advisory);
}
```
```typescript
// DO: Use correct platform or let it auto-filter
const advisories = await tools.clawsec_list_advisories({
platform: 'nanoclaw' // Correct
});
// DO: Use exploitability + severity
if (
advisory.exploitability_score === 'high' ||
advisory.severity === 'critical'
) {
notifyNow(advisory);
}
```
### ❌ Skipping critical severity
```typescript
// DON'T: Only check low severity
if (result.lowCount > 0) alert();
// DON'T: Ignore high exploitability in medium severity advisories
if (advisory.severity === 'critical') alert();
```
```typescript
// DO: Prioritize critical and high
if (result.criticalCount > 0 || result.highCount > 0) {
// DO: Prioritize exploitability and severity together
if (advisory.exploitability_score === 'high' || advisory.severity === 'critical') {
// Alert immediately
}
```
@@ -182,7 +187,7 @@ if (result.criticalCount > 0 || result.highCount > 0) {
**Signature Verification**: Ed25519 signed feeds
**Cache Location**: `/workspace/project/data/clawsec-cache.json`
**Cache Location**: `/workspace/project/data/clawsec-advisory-cache.json`
See [INSTALL.md](./INSTALL.md) for setup and [docs/](./docs/) for advanced usage.
@@ -16,6 +16,7 @@ import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import https from 'node:https';
import path from 'node:path';
import { evaluateAdvisoryRisk } from '../lib/risk.js';
// ClawSec public key (from clawsec-signing-public.pem)
const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
@@ -35,6 +36,8 @@ export interface Advisory {
action?: string;
published?: string;
updated?: string;
exploitability_score?: 'high' | 'medium' | 'low' | 'unknown' | string;
exploitability_rationale?: string;
affected: string[];
}
@@ -376,42 +379,5 @@ export function evaluateSkillSafety(advisories: Advisory[]): {
recommendation: 'install' | 'block' | 'review';
reason: string;
} {
if (advisories.length === 0) {
return { safe: true, recommendation: 'install', reason: 'No advisories found' };
}
const hasMalicious = advisories.some((a) => a.type === 'malicious');
const hasRemoveAction = advisories.some((a) => a.action === 'remove');
const hasCritical = advisories.some((a) => a.severity === 'critical');
const hasHigh = advisories.some((a) => a.severity === 'high');
if (hasMalicious || hasRemoveAction) {
return {
safe: false,
recommendation: 'block',
reason: 'Malicious skill or removal recommended',
};
}
if (hasCritical) {
return {
safe: false,
recommendation: 'block',
reason: 'Critical security advisory',
};
}
if (hasHigh) {
return {
safe: false,
recommendation: 'review',
reason: 'High severity advisory - user review recommended',
};
}
return {
safe: false,
recommendation: 'review',
reason: 'Advisory found - review before installing',
};
return evaluateAdvisoryRisk(advisories);
}
+32 -10
View File
@@ -121,6 +121,34 @@ export function versionMatches(version: string, versionSpec: string): boolean {
return false;
}
/**
* Checks whether an affected specifier matches a skill name/version.
* Optionally matches against a skill directory name as alias.
*/
export function matchesAffectedSpecifier(
affected: string,
skillName: string,
skillVersion: string | null,
skillDirName?: string
): boolean {
const parsed = parseAffectedSpecifier(affected);
if (!parsed) return false;
const normalizedTarget = normalizeSkillName(parsed.name);
const normalizedSkillName = normalizeSkillName(skillName);
const normalizedDirName = skillDirName ? normalizeSkillName(skillDirName) : null;
if (normalizedTarget !== normalizedSkillName && normalizedTarget !== normalizedDirName) {
return false;
}
if (!skillVersion) {
return true;
}
return versionMatches(skillVersion, parsed.versionSpec);
}
/**
* Loads advisory feed from a remote URL with signature verification.
*/
@@ -269,10 +297,12 @@ export async function loadFeed(
export function advisoryLooksHighRisk(advisory: Advisory): boolean {
const type = advisory.type.toLowerCase();
const severity = advisory.severity.toLowerCase();
const exploitability = (advisory.exploitability_score || 'unknown').toLowerCase();
const combined = `${advisory.title} ${advisory.description} ${advisory.action}`.toLowerCase();
if (type === 'malicious_skill' || type === 'malicious_plugin') return true;
if (type.includes('malicious')) return true;
if (severity === 'critical') return true;
if (exploitability === 'high') return true;
if (/\b(malicious|exfiltrate|exfiltration|backdoor|trojan|stealer|credential theft)\b/.test(combined)) return true;
if (/\b(remove|uninstall|disable|do not use|quarantine)\b/.test(combined)) return true;
@@ -294,15 +324,7 @@ export function findAdvisoryMatches(
if (affected.length === 0) continue;
for (const specifier of affected) {
const parsed = parseAffectedSpecifier(specifier);
if (!parsed) continue;
if (normalizeSkillName(parsed.name) !== normalizeSkillName(skillName)) {
continue;
}
// If version specified, check if it matches
if (version && !versionMatches(version, parsed.versionSpec)) {
if (!matchesAffectedSpecifier(specifier, skillName, version)) {
continue;
}
+88
View File
@@ -0,0 +1,88 @@
/**
* Shared advisory risk evaluation for NanoClaw host + MCP layers.
*/
export type SkillSafetyRecommendation = 'install' | 'block' | 'review';
export interface AdvisoryRiskInput {
severity?: string;
type?: string;
action?: string;
exploitability_score?: string;
}
export interface AdvisoryRiskEvaluation {
safe: boolean;
recommendation: SkillSafetyRecommendation;
reason: string;
}
export function normalizeExploitabilityScore(score: unknown): 'high' | 'medium' | 'low' | 'unknown' {
const value = String(score || '').toLowerCase().trim();
if (value === 'high' || value === 'medium' || value === 'low') {
return value;
}
return 'unknown';
}
export function evaluateAdvisoryRisk(advisories: AdvisoryRiskInput[]): AdvisoryRiskEvaluation {
if (advisories.length === 0) {
return { safe: true, recommendation: 'install', reason: 'No advisories found' };
}
const hasMalicious = advisories.some((a) => String(a.type || '').toLowerCase().includes('malicious'));
const hasRemoveAction = advisories.some((a) =>
/\b(remove|uninstall|disable|quarantine|block)\b/i.test(String(a.action || ''))
);
const hasCritical = advisories.some((a) => String(a.severity || '').toLowerCase() === 'critical');
const hasHigh = advisories.some((a) => String(a.severity || '').toLowerCase() === 'high');
const hasHighExploitability = advisories.some(
(a) => normalizeExploitabilityScore(a.exploitability_score) === 'high'
);
if (hasMalicious || hasRemoveAction) {
return {
safe: false,
recommendation: 'block',
reason: 'Malicious skill or removal recommended by ClawSec',
};
}
if (hasCritical && hasHighExploitability) {
return {
safe: false,
recommendation: 'block',
reason: 'Critical advisory with high exploitability context - do not install',
};
}
if (hasCritical) {
return {
safe: false,
recommendation: 'block',
reason: 'Critical security advisory - do not install',
};
}
if (hasHighExploitability) {
return {
safe: false,
recommendation: 'review',
reason: 'High exploitability advisory - urgent user review strongly recommended',
};
}
if (hasHigh) {
return {
safe: false,
recommendation: 'review',
reason: 'High severity advisory - user review strongly recommended',
};
}
return {
safe: false,
recommendation: 'review',
reason: 'Advisory found - review details before installing',
};
}
+2
View File
@@ -15,6 +15,8 @@ export interface Advisory {
references: string[];
cvss_score?: number;
nvd_url?: string;
exploitability_score?: 'high' | 'medium' | 'low' | 'unknown';
exploitability_rationale?: string;
source?: string;
github_issue_url?: string;
reporter?: {
@@ -11,6 +11,8 @@
import fs from 'fs';
import path from 'path';
import { z } from 'zod';
import { evaluateAdvisoryRisk, normalizeExploitabilityScore } from '../lib/risk.js';
import { matchesAffectedSpecifier } from '../lib/advisories.js';
// These variables are provided by the host environment (ipc-mcp-stdio.ts)
// when this code is integrated into the NanoClaw container agent.
@@ -18,8 +20,10 @@ declare const server: { tool: (...args: any[]) => void };
declare function writeIpcFile(dir: string, data: any): void;
declare const TASKS_DIR: string;
declare const groupFolder: string;
const CACHE_FILE = '/workspace/project/data/clawsec-advisory-cache.json';
// Add these helper functions to the file:
const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
const exploitabilityOrder: Record<string, number> = { high: 0, medium: 1, low: 2, unknown: 3 };
/**
* Discover installed skills in a directory
@@ -84,10 +88,7 @@ function findAdvisoryMatches(
const matchedAffected: string[] = [];
for (const affected of advisory.affected || []) {
const atIndex = affected.lastIndexOf('@');
const affectedName = atIndex > 0 ? affected.slice(0, atIndex) : affected;
if (affectedName === skill.name || affectedName === skill.dirName) {
if (matchesAffectedSpecifier(affected, skill.name, skill.version, skill.dirName)) {
matchedAffected.push(affected);
}
}
@@ -123,10 +124,8 @@ server.tool(
}
// Read cache from shared mount
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
try {
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
const installRoot = args.installRoot || path.join(process.env.HOME || '~', '.claude', 'skills');
// Discover installed skills
@@ -153,6 +152,8 @@ server.tool(
description: m.advisory.description,
action: m.advisory.action,
published: m.advisory.published,
exploitability_score: normalizeExploitabilityScore(m.advisory.exploitability_score),
exploitability_rationale: m.advisory.exploitability_rationale || null,
},
skill: m.skill,
matchedAffected: m.matchedAffected,
@@ -187,17 +188,13 @@ server.tool(
skillVersion: z.string().optional().describe('Version of skill (optional, for version-specific checks)'),
},
async (args) => {
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
try {
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
// Find matching advisories for this skill
const matchingAdvisories = cacheData.feed.advisories.filter((advisory: any) =>
advisory.affected.some((affected: string) => {
const atIndex = affected.lastIndexOf('@');
const affectedName = atIndex > 0 ? affected.slice(0, atIndex) : affected;
return affectedName === args.skillName;
return matchesAffectedSpecifier(affected, args.skillName, args.skillVersion || null);
})
);
@@ -215,34 +212,13 @@ server.tool(
};
}
// Evaluate severity
const hasMalicious = matchingAdvisories.some((a: any) => a.type === 'malicious');
const hasRemoveAction = matchingAdvisories.some((a: any) => a.action === 'remove');
const hasCritical = matchingAdvisories.some((a: any) => a.severity === 'critical');
const hasHigh = matchingAdvisories.some((a: any) => a.severity === 'high');
let recommendation: 'install' | 'block' | 'review';
let reason: string;
if (hasMalicious || hasRemoveAction) {
recommendation = 'block';
reason = 'Malicious skill or removal recommended by ClawSec';
} else if (hasCritical) {
recommendation = 'block';
reason = 'Critical security advisory - do not install';
} else if (hasHigh) {
recommendation = 'review';
reason = 'High severity advisory - user review strongly recommended';
} else {
recommendation = 'review';
reason = 'Advisory found - review details before installing';
}
const risk = evaluateAdvisoryRisk(matchingAdvisories);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
safe: false, // Always false when advisories exist
safe: risk.safe,
advisories: matchingAdvisories.map((a: any) => ({
id: a.id,
severity: a.severity,
@@ -252,10 +228,13 @@ server.tool(
action: a.action,
published: a.published,
affected: a.affected,
exploitability_score: normalizeExploitabilityScore(a.exploitability_score),
exploitability_rationale: a.exploitability_rationale || null,
})),
recommendation,
reason,
recommendation: risk.recommendation,
reason: risk.reason,
skillName: args.skillName,
skillVersion: args.skillVersion || null,
advisoryCount: matchingAdvisories.length,
}, null, 2),
}],
@@ -280,18 +259,18 @@ server.tool(
server.tool(
'clawsec_list_advisories',
'List ClawSec advisories with optional filtering. Use this to browse security advisories, filter by severity/type, or search for specific affected skills.',
'List ClawSec advisories with optional filtering. Use this to browse security advisories, filter by severity/type/exploitability, or search for specific affected skills.',
{
severity: z.enum(['critical', 'high', 'medium', 'low']).optional().describe('Filter by severity level'),
type: z.enum(['vulnerability', 'malicious', 'deprecated']).optional().describe('Filter by advisory type'),
type: z.string().optional().describe('Filter by advisory type (for example: vulnerable_skill, malicious_skill, prompt_injection)'),
exploitabilityScore: z.enum(['high', 'medium', 'low', 'unknown']).optional()
.describe('Filter by exploitability score'),
affectedSkill: z.string().optional().describe('Filter by affected skill name (partial match supported)'),
limit: z.number().optional().describe('Maximum number of results (default: unlimited)'),
},
async (args) => {
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
try {
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
let advisories = [...cacheData.feed.advisories];
// Apply filters
@@ -299,7 +278,13 @@ server.tool(
advisories = advisories.filter((a: any) => a.severity === args.severity);
}
if (args.type) {
advisories = advisories.filter((a: any) => a.type === args.type);
const typeFilter = String(args.type).toLowerCase().trim();
advisories = advisories.filter((a: any) => String(a.type || '').toLowerCase().trim() === typeFilter);
}
if (args.exploitabilityScore) {
advisories = advisories.filter(
(a: any) => normalizeExploitabilityScore(a.exploitability_score) === args.exploitabilityScore
);
}
if (args.affectedSkill) {
advisories = advisories.filter((a: any) =>
@@ -307,9 +292,13 @@ server.tool(
);
}
// Sort by severity (critical first) and published date (newest first)
const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
// Sort by exploitability first, then severity, then publish date (newest first).
advisories.sort((a: any, b: any) => {
const exploitabilityDiff =
(exploitabilityOrder[normalizeExploitabilityScore(a.exploitability_score)] ?? 999) -
(exploitabilityOrder[normalizeExploitabilityScore(b.exploitability_score)] ?? 999);
if (exploitabilityDiff !== 0) return exploitabilityDiff;
const severityDiff = (severityOrder[a.severity] || 999) - (severityOrder[b.severity] || 999);
if (severityDiff !== 0) return severityDiff;
return (b.published || '').localeCompare(a.published || '');
@@ -336,6 +325,8 @@ server.tool(
action: a.action,
published: a.published,
affected: a.affected,
exploitability_score: normalizeExploitabilityScore(a.exploitability_score),
exploitability_rationale: a.exploitability_rationale || null,
})),
total: cacheData.feed.advisories.length,
filtered: originalCount,
@@ -343,6 +334,7 @@ server.tool(
filters: {
severity: args.severity || null,
type: args.type || null,
exploitabilityScore: args.exploitabilityScore || null,
affectedSkill: args.affectedSkill || null,
limit: args.limit || null,
},
+14 -3
View File
@@ -1,6 +1,6 @@
{
"name": "clawsec-nanoclaw",
"version": "0.0.1",
"version": "0.0.2",
"description": "ClawSec security suite for NanoClaw - Advisory feed monitoring, MCP tools for vulnerability checking, and Ed25519 signature verification for containerized WhatsApp bot agents",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -27,6 +27,11 @@
"required": true,
"description": "NanoClaw skill documentation"
},
{
"path": "CHANGELOG.md",
"required": true,
"description": "Version history and release notes"
},
{
"path": "INSTALL.md",
"required": true,
@@ -62,6 +67,11 @@
"required": true,
"description": "TypeScript type definitions"
},
{
"path": "lib/risk.ts",
"required": true,
"description": "Shared advisory risk evaluation logic for host and MCP tools"
},
{
"path": "advisories/feed-signing-public.pem",
"required": true,
@@ -112,9 +122,10 @@
"capabilities": [
"Advisory feed monitoring from clawsec.prompt.security",
"MCP tools for agent-initiated vulnerability scans",
"Exploitability-aware advisory prioritization for agent environments",
"Pre-installation skill safety checks",
"Ed25519 signature verification for advisory feeds",
"Platform-specific advisory filtering (nanoclaw vs openclaw)",
"Platform metadata preserved in advisory records for downstream filtering",
"Containerized agent support with IPC communication"
],
"nanoclaw": {
@@ -135,7 +146,7 @@
},
"integration": {
"mcp_tools_file": "container/agent-runner/src/ipc-mcp-stdio.ts",
"ipc_handlers_file": "host/ipc-handler.ts",
"ipc_handlers_file": "src/ipc.ts",
"cache_location": "/workspace/project/data/clawsec-advisory-cache.json"
}
}
+15
View File
@@ -5,6 +5,21 @@ All notable changes to the ClawSec Suite will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.4] - 2026-02-28
### Added
- Advisory output snippets now include exploitability context in suite quick-check and heartbeat examples.
### Changed
- Clarified exploitability guidance to match runtime score values (`high|medium|low|unknown`).
- Prioritization guidance now emphasizes high-exploitability advisories for immediate handling.
### Fixed
- Kept exploitability enrichment in advisory workflows non-fatal per item so a single analysis failure does not abort feed updates.
## [0.1.3]
### Added
+13 -2
View File
@@ -121,6 +121,7 @@ else
while IFS= read -r id; do
[ -z "$id" ] && continue
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | "- [\(.severity | ascii_upcase)] \(.id): \(.title)"' "$FEED_TMP"
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Exploitability: \(.exploitability_score // "unknown" | ascii_upcase)"' "$FEED_TMP"
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Action: \(.action // "Review advisory details")"' "$FEED_TMP"
done < "$NEW_IDS_FILE"
else
@@ -194,8 +195,18 @@ fi
Heartbeat output should include:
- suite version status,
- advisory feed status,
- new advisory list (if any),
- new advisory list (if any) with exploitability scores,
- installed skills that appear in advisory `affected` lists,
- and a double-confirmation reminder before risky install/remove actions.
If your runtime sends alerts, treat `critical` and `high` advisories affecting installed skills as immediate notifications.
### Exploitability-Based Prioritization
When alerting on advisories, prioritize by **exploitability score** in addition to severity:
- `high` exploitability: Trivially or easily exploitable with public tooling, immediate action required
- `medium` exploitability: Exploitable with specific conditions, standard priority
- `low` exploitability: Difficult to exploit or theoretical, low priority
**Priority Rule**: A HIGH severity + HIGH exploitability CVE should be treated more urgently than a CRITICAL severity + LOW exploitability CVE.
If your runtime sends alerts, treat `high` exploitability advisories affecting installed skills as immediate notifications, regardless of severity rating.
+14 -1
View File
@@ -1,6 +1,6 @@
---
name: clawsec-suite
version: 0.1.3
version: 0.1.4
description: ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.
homepage: https://clawsec.prompt.security
clawdis:
@@ -234,12 +234,25 @@ if [ -s "$NEW_IDS_FILE" ]; then
while IFS= read -r id; do
[ -z "$id" ] && continue
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | "- [\(.severity | ascii_upcase)] \(.id): \(.title)"' "$TMP/feed.json"
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Exploitability: \(.exploitability_score // "unknown" | ascii_upcase)"' "$TMP/feed.json"
done < "$NEW_IDS_FILE"
else
echo "FEED_OK - no new advisories"
fi
```
## Exploitability Context
Advisories in the feed can include `exploitability_score` and `exploitability_rationale` fields to help agents prioritize real-world threats:
- **Exploitability scores**: `high`, `medium`, `low`, or `unknown`
- **Context-aware assessment**: Considers attack vector, authentication requirements, and AI agent deployment patterns
- **Exploit availability**: Detects public exploits and weaponization status
When processing advisories, prioritize by exploitability in addition to severity. A HIGH severity + HIGH exploitability CVE is more urgent than a CRITICAL severity + LOW exploitability CVE.
For detailed methodology, see the [exploitability scoring documentation](../../wiki/exploitability-scoring.md).
## Heartbeat Integration
Use the suite heartbeat script as the single periodic security check entrypoint:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "clawsec-suite",
"version": "0.1.3",
"version": "0.1.4",
"description": "ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
+531
View File
@@ -0,0 +1,531 @@
#!/usr/bin/env python3
"""
Exploitability Analyzer - Analyzes CVE exploitability in OpenClaw/NanoClaw deployments
Usage:
python utils/analyze_exploitability.py --help
echo '{"cve_id":"CVE-2026-27488","cvss_score":7.3}' | python utils/analyze_exploitability.py --json
python utils/analyze_exploitability.py --test-cases
Example:
cat cve-data.json | python utils/analyze_exploitability.py --json
"""
import argparse
import json
import sys
from typing import Any
def parse_cvss_vector(vector_string: str) -> dict[str, str]:
"""
Parse CVSS v2, v3.0, or v3.1 vector string into components.
Args:
vector_string: CVSS vector (e.g., "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")
Returns:
Dictionary of CVSS metrics and values
"""
if not vector_string:
return {}
metrics = {}
normalized = vector_string.strip()
# Remove leading CVSS v3.x prefix if present (e.g., "CVSS:3.1/")
if normalized.startswith("CVSS:3"):
_, separator, remainder = normalized.partition("/")
normalized = remainder if separator else ""
# Remove surrounding parentheses/whitespace used by some CVSS v2 strings.
normalized = normalized.strip().strip("()").strip()
if not normalized:
return metrics
# Parse all vector formats with shared key/value extraction logic.
for part in normalized.split("/"):
if ":" in part:
key, value = part.split(":", 1)
metrics[key] = value
return metrics
def analyze_attack_vector(cvss_metrics: dict[str, str]) -> dict[str, Any]:
"""
Analyze attack vector from CVSS metrics.
Args:
cvss_metrics: Parsed CVSS metrics dictionary
Returns:
Dictionary with attack vector analysis
"""
analysis = {
"is_network_accessible": False,
"requires_authentication": True,
"requires_user_interaction": True,
"complexity": "unknown"
}
# Attack Vector (AV)
av = cvss_metrics.get("AV", "")
if av == "N": # Network
analysis["is_network_accessible"] = True
elif av == "A": # Adjacent Network
analysis["is_network_accessible"] = True
elif av in ["L", "P"]: # Local or Physical
analysis["is_network_accessible"] = False
# Privileges Required (PR) / Authentication (AU for v2)
pr = cvss_metrics.get("PR", cvss_metrics.get("Au", ""))
if pr in ["N", "NONE"]:
analysis["requires_authentication"] = False
elif pr in ["L", "H", "SINGLE", "MULTIPLE"]:
analysis["requires_authentication"] = True
# User Interaction (UI)
ui = cvss_metrics.get("UI", "")
if ui == "N": # None
analysis["requires_user_interaction"] = False
elif ui == "R": # Required
analysis["requires_user_interaction"] = True
# Attack Complexity (AC)
ac = cvss_metrics.get("AC", "")
if ac == "L":
analysis["complexity"] = "low"
elif ac in ["M", "H"]:
analysis["complexity"] = "high"
return analysis
def detect_exploit_availability(references: list[str]) -> dict[str, Any]:
"""
Detect if exploits are publicly available based on reference URLs.
Args:
references: List of reference URLs
Returns:
Dictionary with exploit_available (bool) and exploit_sources (list)
"""
exploit_indicators = [
"exploit-db.com",
"exploit-database",
"exploitdb",
"packetstormsecurity.com",
"packetstorm",
"github.com/exploit",
"github.com/poc",
"github.com/proof-of-concept",
"metasploit",
"exploit/",
"/exploit",
"/poc",
"/proof-of-concept",
"exploitability",
"exploit-code",
]
exploit_sources = []
for ref in references:
ref_lower = ref.lower()
for indicator in exploit_indicators:
if indicator in ref_lower:
exploit_sources.append(ref)
break
return {
"exploit_available": len(exploit_sources) > 0,
"exploit_sources": exploit_sources
}
def analyze_exploitability(cve_data: dict[str, Any], check_exploits: bool = False) -> dict[str, Any]:
"""
Analyze CVE exploitability for OpenClaw/NanoClaw deployments.
Args:
cve_data: Dictionary containing CVE information with keys:
- cve_id: CVE identifier
- cvss_score: CVSS base score (float)
- cvss_vector: CVSS vector string (optional)
- type: Vulnerability type
- description: CVE description text
- references: List of reference URLs (optional)
check_exploits: Whether to check references for exploit availability
Returns:
Dictionary with exploitability_score (high/medium/low/unknown) and rationale
"""
cve_id = cve_data.get("cve_id", "unknown")
cvss_score = cve_data.get("cvss_score", 0.0)
cvss_vector = cve_data.get("cvss_vector", "")
vuln_type = cve_data.get("type", "")
description = cve_data.get("description", "")
references = cve_data.get("references", [])
# Parse CVSS vector if available
cvss_metrics = parse_cvss_vector(cvss_vector)
attack_analysis = analyze_attack_vector(cvss_metrics)
# Initial scoring based on CVSS
score = "unknown"
rationale_parts = []
# CVSS-based baseline
if cvss_score >= 9.0:
score = "high"
rationale_parts.append(f"Critical CVSS score ({cvss_score})")
elif cvss_score >= 7.0:
score = "high"
rationale_parts.append(f"High CVSS score ({cvss_score})")
elif cvss_score >= 4.0:
score = "medium"
rationale_parts.append(f"Medium CVSS score ({cvss_score})")
elif cvss_score > 0:
score = "low"
rationale_parts.append(f"Low CVSS score ({cvss_score})")
else:
score = "unknown"
rationale_parts.append("No CVSS score available")
# Adjust based on attack vector analysis
if attack_analysis["is_network_accessible"]:
if not attack_analysis["requires_authentication"] and not attack_analysis["requires_user_interaction"]:
# Network accessible, no auth, no user interaction = highly exploitable
if score == "medium":
score = "high"
rationale_parts.append("remotely exploitable without authentication")
else:
rationale_parts.append("network accessible")
else:
# Local-only vulnerabilities are less critical in agent deployments
if score == "high":
score = "medium"
rationale_parts.append("requires local access")
# OpenClaw/NanoClaw deployment context - adjust based on vulnerability type
vuln_type_lower = vuln_type.lower()
description_lower = description.lower()
# High-risk vulnerability types in AI agent deployments
if any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"ssrf", "server_side_request_forgery", "server-side request forgery"
]):
# SSRF is critical for agents that make external API calls
if score != "high" and cvss_score >= 6.0:
score = "high"
rationale_parts.append("SSRF affects agents making external requests")
elif any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"path_traversal", "path traversal", "directory traversal", "file_inclusion"
]):
# Path traversal is critical for agents with file system access
if score != "high" and cvss_score >= 6.0:
score = "high"
rationale_parts.append("path traversal affects agents with file access")
elif any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"rce", "remote_code_execution", "remote code execution", "code_injection",
"command_injection", "command injection", "arbitrary code"
]):
# RCE is always critical regardless of other factors
score = "high"
rationale_parts.append("RCE is critical in agent deployments")
elif any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"prototype_pollution", "prototype pollution"
]):
# Prototype pollution in Node.js agents can lead to RCE
if score == "low":
score = "medium"
rationale_parts.append("prototype pollution can escalate in Node.js agents")
elif any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"xss", "cross_site_scripting", "cross-site scripting", "reflected xss", "stored xss"
]):
# XSS is lower risk in headless agent deployments (no browser rendering)
if score == "high" and not attack_analysis["is_network_accessible"]:
score = "medium"
rationale_parts.append("XSS has limited impact in headless agents")
elif any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"sql_injection", "sql injection", "nosql injection"
]):
# SQL injection depends on whether agent uses databases
if attack_analysis["is_network_accessible"] and not attack_analysis["requires_authentication"]:
if score == "medium":
score = "high"
rationale_parts.append("injection affects agents with database access")
# Check for exploit availability if requested
exploit_info = {"exploit_available": False, "exploit_sources": []}
if check_exploits and references:
exploit_info = detect_exploit_availability(references)
if exploit_info["exploit_available"]:
# Elevate score if public exploits exist
if score == "low":
score = "medium"
elif score == "medium":
score = "high"
elif score == "unknown" and cvss_score > 0:
# If we have some CVSS score but it was unknown, upgrade to at least medium
score = "medium"
exploit_count = len(exploit_info["exploit_sources"])
source_suffix = "s" if exploit_count > 1 else ""
rationale_parts.append(
f"public exploit available ({exploit_count} source{source_suffix})"
)
# Build rationale string
rationale = "; ".join(rationale_parts[:5]) # Limit to first 5 parts for context
result = {
"cve_id": cve_id,
"exploitability_score": score,
"exploitability_rationale": rationale,
"attack_vector_analysis": attack_analysis
}
# Include exploit info if check_exploits was enabled
if check_exploits:
result["exploit_detection"] = exploit_info
return result
def run_test_cases():
"""
Run comprehensive test cases for attack vector analysis.
Tests CVSS vector parsing and attack vector analysis logic.
"""
test_cases = [
{
"name": "CVSS 3.1 - Network accessible, no auth, no UI (critical)",
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"expected": {
"is_network_accessible": True,
"requires_authentication": False,
"requires_user_interaction": False,
"complexity": "low"
}
},
{
"name": "CVSS 3.1 - Network accessible, requires auth",
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"expected": {
"is_network_accessible": True,
"requires_authentication": True,
"requires_user_interaction": False,
"complexity": "low"
}
},
{
"name": "CVSS 3.1 - Network accessible, requires UI",
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"expected": {
"is_network_accessible": True,
"requires_authentication": False,
"requires_user_interaction": True,
"complexity": "low"
}
},
{
"name": "CVSS 3.1 - Local access required",
"cvss_vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"expected": {
"is_network_accessible": False,
"requires_authentication": False,
"requires_user_interaction": False,
"complexity": "low"
}
},
{
"name": "CVSS 3.1 - Adjacent network, high auth",
"cvss_vector": "CVSS:3.1/AV:A/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:L",
"expected": {
"is_network_accessible": True,
"requires_authentication": True,
"requires_user_interaction": True,
"complexity": "high"
}
},
{
"name": "CVSS 3.0 - Physical access required",
"cvss_vector": "CVSS:3.0/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"expected": {
"is_network_accessible": False,
"requires_authentication": False,
"requires_user_interaction": False,
"complexity": "low"
}
},
{
"name": "CVSS v2 - Network, no auth required",
"cvss_vector": "(AV:N/AC:L/Au:N/C:C/I:C/A:C)",
"expected": {
"is_network_accessible": True,
"requires_authentication": False,
"requires_user_interaction": True, # v2 doesn't have UI, defaults to True
"complexity": "low"
}
},
{
"name": "CVSS v2 - Network, single auth",
"cvss_vector": "AV:N/AC:M/Au:SINGLE/C:P/I:P/A:P",
"expected": {
"is_network_accessible": True,
"requires_authentication": True,
"requires_user_interaction": True, # v2 doesn't have UI, defaults to True
"complexity": "high"
}
},
{
"name": "CVSS v2 - Local access, multiple auth",
"cvss_vector": "(AV:L/AC:L/Au:MULTIPLE/C:C/I:C/A:C)",
"expected": {
"is_network_accessible": False,
"requires_authentication": True,
"requires_user_interaction": True, # v2 doesn't have UI, defaults to True
"complexity": "low"
}
},
{
"name": "Empty CVSS vector",
"cvss_vector": "",
"expected": {
"is_network_accessible": False,
"requires_authentication": True,
"requires_user_interaction": True,
"complexity": "unknown"
}
}
]
print("Running attack vector analysis test cases...")
print("=" * 70)
passed = 0
failed = 0
for i, test in enumerate(test_cases, 1):
print(f"\nTest {i}/{len(test_cases)}: {test['name']}")
print(f" CVSS Vector: {test['cvss_vector']}")
# Parse CVSS vector and analyze attack vector
cvss_metrics = parse_cvss_vector(test['cvss_vector'])
result = analyze_attack_vector(cvss_metrics)
# Compare with expected results
test_passed = True
for key, expected_value in test['expected'].items():
actual_value = result.get(key)
if actual_value != expected_value:
print(f" ❌ FAILED: {key}")
print(f" Expected: {expected_value}")
print(f" Got: {actual_value}")
test_passed = False
failed += 1
break
if test_passed:
print(" ✓ PASSED")
passed += 1
else:
# Show full result for debugging
print(f" Full result: {json.dumps(result, indent=6)}")
print("\n" + "=" * 70)
print(f"Test Results: {passed} passed, {failed} failed out of {len(test_cases)} total")
if failed > 0:
print("\n❌ Some tests failed!")
sys.exit(1)
else:
print("\n✅ All tests passed!")
sys.exit(0)
def main():
parser = argparse.ArgumentParser(
description="Analyze CVE exploitability for OpenClaw/NanoClaw deployments",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Analyze from JSON stdin
echo '{"cve_id":"CVE-2026-27488","cvss_score":7.3,"type":"ssrf"}' | python utils/analyze_exploitability.py --json
# Analyze with CVSS vector
echo '{"cve_id":"CVE-2026-1234","cvss_score":9.8,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"}' \
| python utils/analyze_exploitability.py --json
# Run test cases
python utils/analyze_exploitability.py --test-cases
# Parse CVSS vector only
python utils/analyze_exploitability.py --parse-vector "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
"""
)
parser.add_argument(
"--json",
action="store_true",
help="Read CVE data from stdin as JSON and output analysis"
)
parser.add_argument(
"--parse-vector",
type=str,
metavar="VECTOR",
help="Parse and display CVSS vector string"
)
parser.add_argument(
"--test-cases",
action="store_true",
help="Run built-in test cases to verify analyzer logic"
)
parser.add_argument(
"--check-exploits",
action="store_true",
help="Check references for publicly available exploits and adjust score accordingly"
)
args = parser.parse_args()
# Handle --parse-vector
if args.parse_vector:
metrics = parse_cvss_vector(args.parse_vector)
print(json.dumps(metrics, indent=2))
sys.exit(0)
# Handle --test-cases
if args.test_cases:
run_test_cases()
sys.exit(0)
# Handle --json (stdin)
if args.json:
try:
cve_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
sys.exit(1)
result = analyze_exploitability(cve_data, check_exploits=args.check_exploits)
print(json.dumps(result, indent=2))
sys.exit(0)
# No action specified - show help
parser.print_help()
sys.exit(0)
if __name__ == "__main__":
main()
+420
View File
@@ -0,0 +1,420 @@
# Exploitability Scoring Methodology
## Overview
ClawSec's exploitability scoring system provides context-aware vulnerability assessment specifically designed for AI agent deployments (OpenClaw/NanoClaw). Unlike generic CVSS scores that treat all environments equally, our scoring considers the unique attack surface and usage patterns of AI agents to reduce alert fatigue and prioritize actionable threats.
## Scoring Levels
| Level | Severity | Meaning |
|---|---|---|
| `high` | Critical/High | Exploitable in typical agent deployments, immediate attention required |
| `medium` | Medium | May be exploitable depending on configuration, warrants investigation |
| `low` | Low | Limited exploitability in agent context, low priority |
| `unknown` | Unknown | Insufficient data to assess exploitability |
## Scoring Factors
### 1. CVSS Base Score (Baseline)
The analysis starts with the CVSS base score as a foundation:
- **CVSS ≥ 9.0**: Critical severity → initial score `high`
- **CVSS 7.0-8.9**: High severity → initial score `high`
- **CVSS 4.0-6.9**: Medium severity → initial score `medium`
- **CVSS 1.0-3.9**: Low severity → initial score `low`
- **No CVSS**: → initial score `unknown`
### 2. Attack Vector Analysis (CVSS Metrics)
The analyzer parses CVSS v2, v3.0, and v3.1 vectors to assess:
#### Network Accessibility
- **AV:N** (Network): Remotely exploitable over network
- **AV:A** (Adjacent): Requires local network access
- **AV:L** (Local): Requires local system access
- **AV:P** (Physical): Requires physical access
**Impact on agents**: Network-accessible vulnerabilities are elevated because agents typically run as network services or make external API calls.
#### Authentication Requirements
- **PR:N / Au:NONE**: No authentication required → elevates score
- **PR:L / Au:SINGLE**: Low privileges required
- **PR:H / Au:MULTIPLE**: High privileges required → reduces score
**Impact on agents**: Unauthenticated exploits are critical for publicly exposed agent APIs.
#### User Interaction
- **UI:N**: No user interaction required → elevates score
- **UI:R**: Requires user interaction → reduces score
**Impact on agents**: Agents often operate autonomously, so vulnerabilities requiring user interaction are less critical.
#### Attack Complexity
- **AC:L**: Low complexity → elevates score
- **AC:M / AC:H**: Medium/High complexity → neutral or reduces score
**Impact on agents**: Low-complexity exploits are more likely to be automated and used in mass attacks.
### 3. Vulnerability Type (Deployment Context)
ClawSec adjusts scores based on how vulnerability types affect AI agent deployments:
#### High-Risk Types in Agent Context
**Remote Code Execution (RCE)**
```
Score: Always HIGH
Rationale: RCE is critical in agent deployments
```
AI agents execute arbitrary code as part of their function. RCE vulnerabilities allow attackers to hijack agent execution flow, exfiltrate credentials, or pivot to other systems.
**Server-Side Request Forgery (SSRF)**
```
Score: Elevated to HIGH if CVSS ≥ 6.0
Rationale: SSRF affects agents making external requests
```
Agents frequently call external APIs, access internal services, and fetch remote resources. SSRF allows attackers to:
- Access internal cloud metadata services (AWS IMDSv1, GCP metadata)
- Pivot to internal networks
- Exfiltrate data through DNS tunneling
**Path Traversal / Directory Traversal**
```
Score: Elevated to HIGH if CVSS ≥ 6.0
Rationale: Path traversal affects agents with file access
```
Agents read files, execute scripts, and manage codebases. Path traversal enables:
- Reading sensitive configuration files (.env, credentials)
- Accessing SSH keys, API tokens
- Overwriting critical system files
**Command Injection**
```
Score: Always HIGH
Rationale: Command injection is critical in agent deployments
```
Similar to RCE, agents often execute shell commands to interact with systems. Command injection allows full system compromise.
#### Medium-Risk Types
**Prototype Pollution (Node.js)**
```
Score: Elevated from LOW to MEDIUM
Rationale: Prototype pollution can escalate in Node.js agents
```
Many agent frameworks run on Node.js. Prototype pollution can lead to:
- Bypass of authentication checks
- Privilege escalation
- Denial of service
**SQL Injection / NoSQL Injection**
```
Score: Elevated to HIGH if network-accessible and unauthenticated
Rationale: Injection affects agents with database access
```
Agents that store conversation history, user data, or tool results in databases are vulnerable to injection attacks.
#### Lower-Risk Types
**Cross-Site Scripting (XSS)**
```
Score: Reduced to MEDIUM if not network-accessible
Rationale: XSS has limited impact in headless agents
```
Agents typically don't render HTML in browsers, reducing XSS impact. However, XSS in agent management UIs or chat interfaces remains a concern.
### 4. Exploit Availability
When `--check-exploits` is enabled, the analyzer checks reference URLs for public exploits:
**Exploit Indicators:**
- exploit-db.com / exploit-database.com
- packetstormsecurity.com
- github.com/exploit, github.com/poc
- metasploit framework modules
- URLs containing "/exploit", "/poc", "/proof-of-concept"
**Score Elevation:**
- `low``medium` (exploit available)
- `medium``high` (exploit available)
- `unknown``medium` (exploit available + CVSS > 0)
**Rationale**: Public exploits lower the skill barrier for attackers and increase the likelihood of automated exploitation.
## Scoring Algorithm
The analyzer follows this decision tree:
```
1. Parse CVSS score → set baseline (high/medium/low/unknown)
2. Parse CVSS vector → analyze attack characteristics
3. Adjust for attack vector:
- Network-accessible + no auth + no UI → elevate to HIGH
- Local-only access → reduce HIGH to MEDIUM
4. Adjust for vulnerability type:
- Check against agent-specific risk categories
- Elevate or reduce score based on deployment context
5. Check for public exploits (if enabled):
- Elevate score if exploits detected
6. Generate rationale explaining the final score
```
## Examples
### Example 1: Critical RCE (High Exploitability)
```json
{
"cve_id": "CVE-2024-12345",
"cvss_score": 9.8,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "remote_code_execution",
"description": "Unauthenticated RCE in Express.js framework"
}
```
**Analysis Output:**
```json
{
"exploitability_score": "high",
"exploitability_rationale": "Critical CVSS score (9.8); remotely exploitable without authentication; RCE is critical in agent deployments"
}
```
**Why HIGH**: Critical CVSS + network accessible + no auth + RCE type.
### Example 2: SSRF in Agent API (High Exploitability)
```json
{
"cve_id": "CVE-2024-23456",
"cvss_score": 7.3,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L",
"type": "server_side_request_forgery",
"description": "SSRF in webhook handler allows internal network access"
}
```
**Analysis Output:**
```json
{
"exploitability_score": "high",
"exploitability_rationale": "High CVSS score (7.3); remotely exploitable without authentication; SSRF affects agents making external requests"
}
```
**Why HIGH**: SSRF is critical for agents that make API calls (most do). Network-accessible without authentication elevates risk.
### Example 3: Path Traversal with Public Exploit (High Exploitability)
```json
{
"cve_id": "CVE-2024-34567",
"cvss_score": 6.5,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "path_traversal",
"references": [
"https://exploit-db.com/exploits/51234",
"https://nvd.nist.gov/vuln/detail/CVE-2024-34567"
]
}
```
**Analysis Output (with --check-exploits):**
```json
{
"exploitability_score": "high",
"exploitability_rationale": "Medium CVSS score (6.5); network accessible; path traversal affects agents with file access; public exploit available (1 source)"
}
```
**Why HIGH**: Path traversal + agent file access + public exploit elevates medium CVSS to high exploitability.
### Example 4: XSS in Agent UI (Medium Exploitability)
```json
{
"cve_id": "CVE-2024-45678",
"cvss_score": 7.1,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L",
"type": "cross_site_scripting",
"description": "Stored XSS in agent management dashboard"
}
```
**Analysis Output:**
```json
{
"exploitability_score": "medium",
"exploitability_rationale": "High CVSS score (7.1); network accessible; XSS has limited impact in headless agents"
}
```
**Why MEDIUM**: Despite high CVSS, XSS is less critical in agent deployments (headless operation). Requires user interaction.
### Example 5: Local Privilege Escalation (Medium Exploitability)
```json
{
"cve_id": "CVE-2024-56789",
"cvss_score": 8.8,
"cvss_vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
"type": "privilege_escalation",
"description": "Local privilege escalation via symbolic link attack"
}
```
**Analysis Output:**
```json
{
"exploitability_score": "medium",
"exploitability_rationale": "High CVSS score (8.8); requires local access"
}
```
**Why MEDIUM**: Despite high CVSS, requires local access. Agents typically run in containerized/sandboxed environments where local escalation has limited impact.
### Example 6: Prototype Pollution with Exploit (High Exploitability)
```json
{
"cve_id": "CVE-2024-67890",
"cvss_score": 5.3,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
"type": "prototype_pollution",
"description": "Prototype pollution in lodash merge function",
"references": [
"https://github.com/exploit/prototype-pollution-poc",
"https://snyk.io/vuln/SNYK-JS-LODASH-1234567"
]
}
```
**Analysis Output (with --check-exploits):**
```json
{
"exploitability_score": "high",
"exploitability_rationale": "Medium CVSS score (5.3); remotely exploitable without authentication; prototype pollution can escalate in Node.js agents; public exploit available (1 source)"
}
```
**Why HIGH**: Prototype pollution in Node.js agents + public exploit + network-accessible without auth = high risk despite moderate CVSS.
## Usage in ClawSec Workflows
### Automated Scoring (NVD Feed)
The `poll-nvd-cves.yml` workflow automatically scores new CVEs:
```bash
# Workflow step
python utils/analyze_exploitability.py --json --check-exploits < cve-data.json
```
Advisories in `advisories/feed.json` can include:
```json
{
"id": "CVE-2024-12345",
"severity": "high",
"exploitability_score": "high",
"exploitability_rationale": "Critical CVSS score (9.8); remotely exploitable without authentication; RCE is critical in agent deployments",
"attack_vector_analysis": {
"is_network_accessible": true,
"requires_authentication": false,
"requires_user_interaction": false,
"complexity": "low"
}
}
```
### Manual Analysis
Security researchers can analyze CVEs manually:
```bash
# Basic analysis
echo '{"cve_id":"CVE-2024-12345","cvss_score":7.3,"type":"ssrf"}' | \
python utils/analyze_exploitability.py --json
# With exploit detection
echo '{"cve_id":"CVE-2024-12345","cvss_score":7.3,"references":["https://exploit-db.com/exploits/51234"]}' | \
python utils/analyze_exploitability.py --json --check-exploits
```
### Filtering by Exploitability
Users can filter advisories by exploitability score:
```bash
# Get only high-exploitability advisories
curl -s https://clawsec.prompt.security/feed.json | \
jq '.advisories[] | select(.exploitability_score == "high")'
# Prioritize by exploitability and severity
curl -s https://clawsec.prompt.security/feed.json | \
jq '[.advisories[] | select(.exploitability_score == "high" and .severity == "critical")] | sort_by(.cvss_score) | reverse'
```
## Backfilling Existing Advisories (Historical Maintenance)
`scripts/backfill-exploitability.sh` is retained as a historical maintainer utility for one-off repository maintenance.
It is not the primary path for normal advisory generation.
Preferred paths:
1. CI canonical path: run the NVD workflow with init/reset to rebuild advisories from NVD and sign artifacts in pipeline.
2. Local developer path: run `./scripts/populate-local-feed.sh --force` to repopulate local feeds with exploitability context.
Use backfill only when explicitly repairing legacy feed content that already exists in-repo.
## Community Contributions
Community members can submit exploitability assessments:
1. **Report via GitHub Issue**: Use the advisory template to report CVEs with exploitability context
2. **Automated Analysis**: The `community-advisory.yml` workflow automatically scores community-reported CVEs
3. **Manual Review**: Maintainers review and approve exploitability assessments
4. **Feed Update**: Approved advisories are added to the feed with exploitability scores
## Limitations and Future Work
### Current Limitations
1. **Static Analysis**: Scoring is based on CVE metadata, not dynamic runtime analysis
2. **No Version Detection**: Doesn't check if specific versions are vulnerable
3. **Binary Classification**: Doesn't consider partial mitigations or defense-in-depth
4. **Limited Context**: Doesn't know exact agent configuration or deployed tools
### Future Enhancements
1. **EPSS Integration**: Incorporate EPSS (Exploit Prediction Scoring System) probability scores
2. **KEV Matching**: Cross-reference with CISA KEV (Known Exploited Vulnerabilities) catalog
3. **Agent Profiling**: Consider deployed agent capabilities and exposed APIs
4. **Mitigation Detection**: Check for WAF rules, sandboxing, or other compensating controls
5. **ML-Based Scoring**: Use machine learning to predict exploitability based on historical data
## References
- **CVSS v3.1 Specification**: [https://www.first.org/cvss/v3.1/specification-document](https://www.first.org/cvss/v3.1/specification-document)
- **CVSS v2 Guide**: [https://www.first.org/cvss/v2/guide](https://www.first.org/cvss/v2/guide)
- **EPSS**: [https://www.first.org/epss/](https://www.first.org/epss/)
- **CISA KEV**: [https://www.cisa.gov/known-exploited-vulnerabilities-catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
- **NVD API**: [https://nvd.nist.gov/developers/vulnerabilities](https://nvd.nist.gov/developers/vulnerabilities)
## Contributing
To improve the exploitability scoring methodology:
1. **Submit Test Cases**: Add test cases to `utils/analyze_exploitability.py`
2. **Report False Positives/Negatives**: Open GitHub issues with CVE examples
3. **Propose Scoring Adjustments**: Submit PRs with rationale and examples
4. **Share Agent Context**: Contribute agent-specific vulnerability patterns
See [CONTRIBUTING.md](../CONTRIBUTING.md) for detailed contribution guidelines.
---
**Maintained by**: [Prompt Security](https://prompt.security)
**License**: AGPL-3.0-or-later
**Last Updated**: 2026-03-01