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:" echo "Created advisory JSON:"
cat tmp_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 - name: Update feed
if: steps.parse.outputs.already_exists != 'true' if: steps.parse.outputs.already_exists != 'true'
run: | run: |
+103 -3
View File
@@ -353,7 +353,9 @@ jobs:
title: (.cve.descriptions[] | select(.lang == "en") | .value | .[0:100] + (if length > 100 then "..." else "" end)), title: (.cve.descriptions[] | select(.lang == "en") | .value | .[0:100] + (if length > 100 then "..." else "" end)),
affected: normalized_affected, affected: normalized_affected,
platforms: normalized_platforms, 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 ' tmp/filtered_cves.json > tmp/nvd_current_state.json
@@ -553,7 +555,7 @@ jobs:
end end
); );
[.[] | [.[] |
select(.cve.id as $id | $existing | index($id) | not) | select(.cve.id as $id | $existing | index($id) | not) |
{ {
id: .cve.id, id: .cve.id,
@@ -568,7 +570,9 @@ jobs:
published: .cve.published, published: .cve.published,
references: [.cve.references[]?.url // empty] | unique | .[0:3], references: [.cve.references[]?.url // empty] | unique | .[0:3],
cvss_score: get_cvss_score, 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 ' tmp/filtered_cves.json > tmp/new_advisories.json
@@ -582,6 +586,102 @@ jobs:
jq '.[].id' tmp/new_advisories.json jq '.[].id' tmp/new_advisories.json
fi 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 - name: Update feed.json
if: steps.transform.outputs.new_count != '0' || steps.updates.outputs.update_count != '0' if: steps.transform.outputs.new_count != '0' || steps.updates.outputs.update_count != '0'
run: | run: |
+13
View File
@@ -203,6 +203,17 @@ The feed polls CVEs related to:
- Prompt injection patterns - Prompt injection patterns
- Agent security vulnerabilities - 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 ### Advisory Schema
**NVD CVE Advisory:** **NVD CVE Advisory:**
@@ -217,6 +228,8 @@ The feed polls CVEs related to:
"published": "2026-02-01T00:00:00Z", "published": "2026-02-01T00:00:00Z",
"cvss_score": 8.8, "cvss_score": 8.8,
"nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-XXXXX", "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": ["..."], "references": ["..."],
"action": "Recommended remediation" "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
}
+111 -18
View File
@@ -11,13 +11,14 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# shellcheck source=./feed-utils.sh
source "$SCRIPT_DIR/feed-utils.sh"
# Configuration - same as pipeline # Configuration - same as pipeline
FEED_PATH="$PROJECT_ROOT/advisories/feed.json" init_feed_paths "$PROJECT_ROOT"
SKILL_FEED_PATH="$PROJECT_ROOT/skills/clawsec-feed/advisories/feed.json"
PUBLIC_FEED_PATH="$PROJECT_ROOT/public/advisories/feed.json"
KEYWORDS="OpenClaw clawdbot Moltbot NanoClaw WhatsApp-bot baileys" KEYWORDS="OpenClaw clawdbot Moltbot NanoClaw WhatsApp-bot baileys"
GITHUB_REF_PATTERN="github.com/openclaw/openclaw github.com/qwibitai/NanoClaw" GITHUB_REF_PATTERN="github.com/openclaw/openclaw github.com/qwibitai/NanoClaw"
ANALYZER="$PROJECT_ROOT/utils/analyze_exploitability.py"
# Parse args # Parse args
DAYS_BACK=120 DAYS_BACK=120
@@ -46,6 +47,22 @@ echo "Days back: $DAYS_BACK"
echo "Force mode: $FORCE" echo "Force mode: $FORCE"
echo "" 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 # Create temp directory
TEMP_DIR=$(mktemp -d) TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT trap 'rm -rf "$TEMP_DIR"' EXIT
@@ -62,7 +79,7 @@ fi
if [ -z "${START_DATE:-}" ]; then if [ -z "${START_DATE:-}" ]; then
# macOS vs Linux date compatibility # macOS vs Linux date compatibility
if date -v-1d > /dev/null 2>&1; then 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 else
START_DATE=$(date -u -d "${DAYS_BACK} days ago" +%Y-%m-%dT%H:%M:%S.000Z) START_DATE=$(date -u -d "${DAYS_BACK} days ago" +%Y-%m-%dT%H:%M:%S.000Z)
fi fi
@@ -74,8 +91,8 @@ echo "End date: $END_DATE"
echo "" echo ""
# URL encode dates # URL encode dates
START_ENC=$(echo "$START_DATE" | sed 's/:/%3A/g') START_ENC=${START_DATE//:/%3A}
END_ENC=$(echo "$END_DATE" | sed 's/:/%3A/g') END_ENC=${END_DATE//:/%3A}
echo "=== Fetching CVEs from NVD ===" echo "=== Fetching CVEs from NVD ==="
@@ -267,7 +284,7 @@ jq --argjson existing "$EXISTING_JSON" '
| if length == 0 then ["openclaw@*", "nanoclaw@*"] else . end | if length == 0 then ["openclaw@*", "nanoclaw@*"] else . end
); );
[.[] | [.[] |
select(.cve.id as $id | $existing | index($id) | not) | select(.cve.id as $id | $existing | index($id) | not) |
{ {
id: .cve.id, id: .cve.id,
@@ -281,7 +298,9 @@ jq --argjson existing "$EXISTING_JSON" '
published: .cve.published, published: .cve.published,
references: [.cve.references[]?.url // empty] | unique | .[0:3], references: [.cve.references[]?.url // empty] | unique | .[0:3],
cvss_score: get_cvss_score, 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" ' "$TEMP_DIR/filtered_cves.json" > "$TEMP_DIR/new_advisories.json"
@@ -296,6 +315,87 @@ if [ "$NEW_COUNT" -eq 0 ]; then
exit 0 exit 0
fi 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 ""
echo "=== New Advisories ===" echo "=== New Advisories ==="
jq -r '.[] | " \(.id) [\(.severity)] - \(.title)"' "$TEMP_DIR/new_advisories.json" jq -r '.[] | " \(.id) [\(.severity)] - \(.title)"' "$TEMP_DIR/new_advisories.json"
@@ -338,16 +438,9 @@ if jq empty "$TEMP_DIR/updated_feed.json" 2>/dev/null; then
# Update main feed # Update main feed
cp "$TEMP_DIR/updated_feed.json" "$FEED_PATH" cp "$TEMP_DIR/updated_feed.json" "$FEED_PATH"
echo "✓ Updated: $FEED_PATH" echo "✓ Updated: $FEED_PATH"
# Update skill feed # Sync feed mirrors for local skill/public consumers.
mkdir -p "$(dirname "$SKILL_FEED_PATH")" sync_feed_to_mirrors "$FEED_PATH" "create"
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"
echo "" echo ""
TOTAL_ADVISORIES=$(jq '.advisories | length' "$FEED_PATH") TOTAL_ADVISORIES=$(jq '.advisories | length' "$FEED_PATH")
+3 -3
View File
@@ -76,13 +76,13 @@ fi
# ESLint # ESLint
echo -e "\n${YELLOW}Running ESLint...${NC}" echo -e "\n${YELLOW}Running ESLint...${NC}"
if $FIX_MODE; then 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)" check_pass "ESLint (with auto-fix)"
else else
check_fail "ESLint found unfixable issues" check_fail "ESLint found unfixable issues"
fi fi
else 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" check_pass "ESLint"
else else
check_fail "ESLint found issues (run with --fix to auto-fix)" check_fail "ESLint found issues (run with --fix to auto-fix)"
@@ -190,7 +190,7 @@ print_header "Security"
# Trivy FS Scan # Trivy FS Scan
if command -v trivy &> /dev/null; then if command -v trivy &> /dev/null; then
echo -e "\n${YELLOW}Running Trivy filesystem scan...${NC}" 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" check_pass "Trivy filesystem scan"
else else
check_fail "Trivy found CRITICAL/HIGH vulnerabilities" 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 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. description: Security advisory feed with automated NVD CVE polling for OpenClaw-related vulnerabilities. Updated daily.
homepage: https://clawsec.prompt.security homepage: https://clawsec.prompt.security
metadata: {"openclaw":{"emoji":"📡","category":"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", "description": "Skill sends user data to external server",
"affected": ["helper-plus@1.0.0", "helper-plus@1.0.1"], "affected": ["helper-plus@1.0.0", "helper-plus@1.0.1"],
"action": "Remove immediately", "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" 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 ## 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 ## When to Notify Your User
**Notify Immediately (Critical):** **Notify Immediately (Critical):**
- New critical advisory affecting an installed skill - New critical advisory affecting an installed skill
- Active exploitation detected - Active exploitation detected
- **High exploitability score** (regardless of severity)
**Notify Soon (High):** **Notify Soon (High):**
- New high-severity advisory affecting installed skills - New high-severity advisory affecting installed skills
- Failed to fetch advisory feed (network issue?) - Failed to fetch advisory feed (network issue?)
- Medium exploitability with high severity
**Notify at Next Interaction (Medium):** **Notify at Next Interaction (Medium):**
- New medium-severity advisories - New medium-severity advisories
- General security updates - General security updates
- Low exploitability advisories
**Log Only (Low/Info):** **Log Only (Low/Info):**
- Low-severity advisories (mention if user asks) - Low-severity advisories (mention if user asks)
- Feed checked, no new advisories - Feed checked, no new advisories
- Theoretical vulnerabilities (low exploitability, low severity)
--- ---
@@ -503,11 +593,13 @@ done
``` ```
📡 ClawSec Feed: 2 new advisories since last check 📡 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. → 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. → You have this installed! Recommended action: Update to v1.2.1 or remove.
→ Exploitability: Requires specific configuration; not trivially exploitable.
``` ```
### If nothing new: ### If nothing new:
+6 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "clawsec-feed", "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.", "description": "Security advisory feed monitoring for AI agents. Subscribe to community-driven threat intelligence.",
"author": "prompt-security", "author": "prompt-security",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
@@ -21,6 +21,11 @@
"required": true, "required": true,
"description": "Advisory feed skill documentation" "description": "Advisory feed skill documentation"
}, },
{
"path": "CHANGELOG.md",
"required": true,
"description": "Version history for advisory feed updates"
},
{ {
"path": "advisories/feed.json", "path": "advisories/feed.json",
"required": true, "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.
+45 -31
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` - **MCP Tools**: Agents can check for vulnerabilities via `clawsec_check_advisories`
- **Advisory Feed**: Automatic monitoring of https://clawsec.prompt.security/advisories/feed.json - **Advisory Feed**: Automatic monitoring of https://clawsec.prompt.security/advisories/feed.json
- **Signature Verification**: Ed25519-signed feeds ensure integrity - **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 ## Prerequisites
@@ -57,18 +57,30 @@ in each tool file).
Add the host-side IPC handlers for ClawSec operations. Add the host-side IPC handlers for ClawSec operations.
**File**: `host/ipc-handler.ts` **File**: `src/ipc.ts`
```typescript ```typescript
// Add this import at the top // Add these imports at the top
import { registerClawSecHandlers } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js'; 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 // Initialize these once in host startup and pass through deps
export function setupIpcHandlers() { const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger);
// ... your existing handlers ... const signatureVerifier = new SkillSignatureVerifier();
// Register ClawSec handlers // In processTaskIpc switch:
registerClawSecHandlers(); 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. 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 ```typescript
// Add this import import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
import { startAdvisoryCache } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
// Start the service when your host process starts // Start the service when your host process starts
async function main() { async function main() {
// ... your existing initialization ... // ... your existing initialization ...
// Start ClawSec advisory cache (fetches feed every 6 hours) // Initialize cache manager and prime it at startup
startAdvisoryCache({ const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger);
cacheFile: '/workspace/project/data/clawsec-advisory-cache.json', await advisoryCacheManager.initialize();
feedUrl: 'https://clawsec.prompt.security/advisories/feed.json',
publicKeyPath: '/workspace/project/skills/clawsec-nanoclaw/advisories/feed-signing-public.pem', // Recommended refresh cadence (6h)
refreshInterval: 6 * 60 * 60 * 1000, // 6 hours setInterval(() => {
}); advisoryCacheManager.refresh().catch((error) => {
logger.error({ error }, 'Periodic advisory cache refresh failed');
});
}, 6 * 60 * 60 * 1000);
// ... rest of your startup ... // ... rest of your startup ...
} }
@@ -151,9 +165,9 @@ cat /workspace/project/data/clawsec-advisory-cache.json
You should see: You should see:
- `feed`: Array of advisories - `feed`: Array of advisories
- `signature`: Ed25519 signature - `fetchedAt`: Timestamp of last update
- `lastFetch`: Timestamp of last update
- `verified`: Should be `true` - `verified`: Should be `true`
- `publicKeyFingerprint`: SHA-256 fingerprint of the pinned signing key
## Usage Examples ## Usage Examples
@@ -183,13 +197,13 @@ You can also call the MCP tools directly from agent code:
```typescript ```typescript
// Check all installed skills // Check all installed skills
const result = await tools.clawsec_check_advisories({ const result = await tools.clawsec_check_advisories({
skillsRoot: '/workspace/project/skills' installRoot: '/home/node/.claude/skills'
}); });
// Check specific skill before installation // Check specific skill before installation
const safetyCheck = await tools.clawsec_check_skill_safety({ const safetyCheck = await tools.clawsec_check_skill_safety({
skillName: 'risky-skill', 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` 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 ### Refresh Interval
Default: 6 hours Default: 6 hours
To change, update the `refreshInterval` parameter (in milliseconds). To change, update the `setInterval(...)` duration (in milliseconds) in host startup.
### Feed URL ### Feed URL
Default: `https://clawsec.prompt.security/advisories/feed.json` 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 ## Platform-Specific Advisories
@@ -222,7 +236,7 @@ ClawSec advisories can target specific platforms:
- **`platforms: ["openclaw", "nanoclaw"]`**: Affects both - **`platforms: ["openclaw", "nanoclaw"]`**: Affects both
- **No `platforms` field**: Applies to all platforms - **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 ## Security
@@ -260,7 +274,7 @@ Never manually edit the cache file - it will break signature verification.
**Problem**: Advisory cache is empty or stale **Problem**: Advisory cache is empty or stale
**Solution**: **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` 2. Verify network access to `clawsec.prompt.security`
3. Check host logs for fetch errors 3. Check host logs for fetch errors
4. Manually trigger: `curl https://clawsec.prompt.security/advisories/feed.json` 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 **Problem**: Tools return errors about IPC
**Solution**: **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 2. Check that IPC directory exists and is writable
3. Ensure host process is running 3. Ensure host process is running
4. Check host logs for handler errors 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: To remove ClawSec from NanoClaw:
1. Remove MCP tool registration from `ipc-mcp-stdio.ts` 1. Remove MCP tool registration from `ipc-mcp-stdio.ts`
2. Remove IPC handler registration from `host/ipc-handler.ts` 2. Remove IPC handler registration from `src/ipc.ts`
3. Remove `startAdvisoryCache()` call from host entry point 3. Remove `AdvisoryCacheManager` initialization from host entry point
4. Delete the skill directory: `rm -rf skills/clawsec-nanoclaw` 4. Delete the skill directory: `rm -rf skills/clawsec-nanoclaw`
5. Delete the cache file: `rm /workspace/project/data/clawsec-advisory-cache.json` 5. Delete the cache file: `rm /workspace/project/data/clawsec-advisory-cache.json`
6. Restart NanoClaw 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_integrity_status` - View file baseline status
- `clawsec_verify_audit` - Verify audit log hash chain - `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 - **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 - **IPC Communication**: Container-safe host communication
### Installation ### Installation
@@ -77,19 +77,19 @@ The skill integrates into three places:
**1. MCP Tools** (container): **1. MCP Tools** (container):
```typescript ```typescript
// container/agent-runner/src/ipc-mcp-stdio.ts // 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): **2. IPC Handlers** (host):
```typescript ```typescript
// host/ipc-handler.ts // src/ipc.ts
import { registerClawSecHandlers } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js'; import { handleAdvisoryIpc } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
``` ```
**3. Cache Service** (host): **3. Cache Service** (host):
```typescript ```typescript
// host/index.ts // src/index.ts
import { startAdvisoryCache } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js'; import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
``` ```
### Advisory Feed ### Advisory Feed
@@ -148,4 +148,4 @@ Planned features for future releases:
- **Issues**: https://github.com/prompt-security/clawsec/issues - **Issues**: https://github.com/prompt-security/clawsec/issues
- **Security**: security@prompt.security - **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 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 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 ## 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. **Core principle:** Check before you install. Monitor what's running.
@@ -36,7 +36,7 @@ Do NOT use for:
// Before installing any skill // Before installing any skill
const safety = await tools.clawsec_check_skill_safety({ const safety = await tools.clawsec_check_skill_safety({
skillName: 'new-skill', skillName: 'new-skill',
version: '1.0.0' // optional skillVersion: '1.0.0' // optional
}); });
if (!safety.safe) { if (!safety.safe) {
@@ -48,14 +48,16 @@ if (!safety.safe) {
### Security Audit ### Security Audit
```typescript ```typescript
// Check all installed skills // Check all installed skills (defaults to ~/.claude/skills in the container)
const result = await tools.clawsec_check_advisories({ 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 // Alert user immediately
console.error('CRITICAL vulnerabilities found!'); console.error('Urgent advisories found!');
} }
``` ```
@@ -64,8 +66,8 @@ if (result.criticalCount > 0) {
```typescript ```typescript
// List advisories with filters // List advisories with filters
const advisories = await tools.clawsec_list_advisories({ const advisories = await tools.clawsec_list_advisories({
platform: 'nanoclaw', // optional: nanoclaw, openclaw, or both severity: 'high', // optional
severity: 'critical' // optional: critical, high, medium, low exploitabilityScore: 'high' // optional
}); });
``` ```
@@ -75,7 +77,7 @@ const advisories = await tools.clawsec_list_advisories({
|------|------|---------------| |------|------|---------------|
| Pre-install check | `clawsec_check_skill_safety` | `skillName` | | Pre-install check | `clawsec_check_skill_safety` | `skillName` |
| Audit all skills | `clawsec_check_advisories` | `installRoot` (optional) | | 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` | | Verify package signature | `clawsec_verify_skill_package` | `packagePath` |
| Refresh advisory cache | `clawsec_refresh_cache` | (none) | | Refresh advisory cache | `clawsec_refresh_cache` | (none) |
| Check file integrity | `clawsec_check_integrity` | `mode`, `autoRestore` (optional) | | Check file integrity | `clawsec_check_integrity` | `mode`, `autoRestore` (optional) |
@@ -110,7 +112,7 @@ if (safety.safe) {
```typescript ```typescript
// Add to scheduled tasks // Add to scheduled tasks
schedule_task({ 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_type: "cron",
schedule_value: "0 9 * * *" // Daily at 9am schedule_value: "0 9 * * *" // Daily at 9am
}); });
@@ -125,8 +127,8 @@ You: I'll check installed skills for known vulnerabilities.
[Use clawsec_check_advisories] [Use clawsec_check_advisories]
Response: Response:
✅ No critical issues found. ✅ No urgent issues found.
- 2 low-severity advisories (not urgent) - 2 low-severity/low-exploitability advisories
- All skills up to date - All skills up to date
``` ```
@@ -146,30 +148,33 @@ const safety = await tools.clawsec_check_skill_safety({
if (safety.safe) await installSkill('untrusted-skill'); if (safety.safe) await installSkill('untrusted-skill');
``` ```
### ❌ Ignoring platform filters ### ❌ Ignoring exploitability context
```typescript ```typescript
// DON'T: Check OpenClaw advisories on NanoClaw // DON'T: Use severity only
const advisories = await tools.clawsec_list_advisories({ if (advisory.severity === 'high') {
platform: 'openclaw' // Wrong platform! notifyNow(advisory);
}); }
``` ```
```typescript ```typescript
// DO: Use correct platform or let it auto-filter // DO: Use exploitability + severity
const advisories = await tools.clawsec_list_advisories({ if (
platform: 'nanoclaw' // Correct advisory.exploitability_score === 'high' ||
}); advisory.severity === 'critical'
) {
notifyNow(advisory);
}
``` ```
### ❌ Skipping critical severity ### ❌ Skipping critical severity
```typescript ```typescript
// DON'T: Only check low severity // DON'T: Ignore high exploitability in medium severity advisories
if (result.lowCount > 0) alert(); if (advisory.severity === 'critical') alert();
``` ```
```typescript ```typescript
// DO: Prioritize critical and high // DO: Prioritize exploitability and severity together
if (result.criticalCount > 0 || result.highCount > 0) { if (advisory.exploitability_score === 'high' || advisory.severity === 'critical') {
// Alert immediately // Alert immediately
} }
``` ```
@@ -182,7 +187,7 @@ if (result.criticalCount > 0 || result.highCount > 0) {
**Signature Verification**: Ed25519 signed feeds **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. 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 fs from 'node:fs/promises';
import https from 'node:https'; import https from 'node:https';
import path from 'node:path'; import path from 'node:path';
import { evaluateAdvisoryRisk } from '../lib/risk.js';
// ClawSec public key (from clawsec-signing-public.pem) // ClawSec public key (from clawsec-signing-public.pem)
const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY----- const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
@@ -35,6 +36,8 @@ export interface Advisory {
action?: string; action?: string;
published?: string; published?: string;
updated?: string; updated?: string;
exploitability_score?: 'high' | 'medium' | 'low' | 'unknown' | string;
exploitability_rationale?: string;
affected: string[]; affected: string[];
} }
@@ -376,42 +379,5 @@ export function evaluateSkillSafety(advisories: Advisory[]): {
recommendation: 'install' | 'block' | 'review'; recommendation: 'install' | 'block' | 'review';
reason: string; reason: string;
} { } {
if (advisories.length === 0) { return evaluateAdvisoryRisk(advisories);
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',
};
} }
+32 -10
View File
@@ -121,6 +121,34 @@ export function versionMatches(version: string, versionSpec: string): boolean {
return false; 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. * Loads advisory feed from a remote URL with signature verification.
*/ */
@@ -269,10 +297,12 @@ export async function loadFeed(
export function advisoryLooksHighRisk(advisory: Advisory): boolean { export function advisoryLooksHighRisk(advisory: Advisory): boolean {
const type = advisory.type.toLowerCase(); const type = advisory.type.toLowerCase();
const severity = advisory.severity.toLowerCase(); const severity = advisory.severity.toLowerCase();
const exploitability = (advisory.exploitability_score || 'unknown').toLowerCase();
const combined = `${advisory.title} ${advisory.description} ${advisory.action}`.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 (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(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; 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; if (affected.length === 0) continue;
for (const specifier of affected) { for (const specifier of affected) {
const parsed = parseAffectedSpecifier(specifier); if (!matchesAffectedSpecifier(specifier, skillName, version)) {
if (!parsed) continue;
if (normalizeSkillName(parsed.name) !== normalizeSkillName(skillName)) {
continue;
}
// If version specified, check if it matches
if (version && !versionMatches(version, parsed.versionSpec)) {
continue; 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[]; references: string[];
cvss_score?: number; cvss_score?: number;
nvd_url?: string; nvd_url?: string;
exploitability_score?: 'high' | 'medium' | 'low' | 'unknown';
exploitability_rationale?: string;
source?: string; source?: string;
github_issue_url?: string; github_issue_url?: string;
reporter?: { reporter?: {
@@ -11,6 +11,8 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { z } from 'zod'; 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) // These variables are provided by the host environment (ipc-mcp-stdio.ts)
// when this code is integrated into the NanoClaw container agent. // 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 function writeIpcFile(dir: string, data: any): void;
declare const TASKS_DIR: string; declare const TASKS_DIR: string;
declare const groupFolder: 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 * Discover installed skills in a directory
@@ -84,10 +88,7 @@ function findAdvisoryMatches(
const matchedAffected: string[] = []; const matchedAffected: string[] = [];
for (const affected of advisory.affected || []) { for (const affected of advisory.affected || []) {
const atIndex = affected.lastIndexOf('@'); if (matchesAffectedSpecifier(affected, skill.name, skill.version, skill.dirName)) {
const affectedName = atIndex > 0 ? affected.slice(0, atIndex) : affected;
if (affectedName === skill.name || affectedName === skill.dirName) {
matchedAffected.push(affected); matchedAffected.push(affected);
} }
} }
@@ -123,10 +124,8 @@ server.tool(
} }
// Read cache from shared mount // Read cache from shared mount
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
try { 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'); const installRoot = args.installRoot || path.join(process.env.HOME || '~', '.claude', 'skills');
// Discover installed skills // Discover installed skills
@@ -153,6 +152,8 @@ server.tool(
description: m.advisory.description, description: m.advisory.description,
action: m.advisory.action, action: m.advisory.action,
published: m.advisory.published, published: m.advisory.published,
exploitability_score: normalizeExploitabilityScore(m.advisory.exploitability_score),
exploitability_rationale: m.advisory.exploitability_rationale || null,
}, },
skill: m.skill, skill: m.skill,
matchedAffected: m.matchedAffected, matchedAffected: m.matchedAffected,
@@ -187,17 +188,13 @@ server.tool(
skillVersion: z.string().optional().describe('Version of skill (optional, for version-specific checks)'), skillVersion: z.string().optional().describe('Version of skill (optional, for version-specific checks)'),
}, },
async (args) => { async (args) => {
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
try { try {
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
// Find matching advisories for this skill // Find matching advisories for this skill
const matchingAdvisories = cacheData.feed.advisories.filter((advisory: any) => const matchingAdvisories = cacheData.feed.advisories.filter((advisory: any) =>
advisory.affected.some((affected: string) => { advisory.affected.some((affected: string) => {
const atIndex = affected.lastIndexOf('@'); return matchesAffectedSpecifier(affected, args.skillName, args.skillVersion || null);
const affectedName = atIndex > 0 ? affected.slice(0, atIndex) : affected;
return affectedName === args.skillName;
}) })
); );
@@ -215,34 +212,13 @@ server.tool(
}; };
} }
// Evaluate severity const risk = evaluateAdvisoryRisk(matchingAdvisories);
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';
}
return { return {
content: [{ content: [{
type: 'text' as const, type: 'text' as const,
text: JSON.stringify({ text: JSON.stringify({
safe: false, // Always false when advisories exist safe: risk.safe,
advisories: matchingAdvisories.map((a: any) => ({ advisories: matchingAdvisories.map((a: any) => ({
id: a.id, id: a.id,
severity: a.severity, severity: a.severity,
@@ -252,10 +228,13 @@ server.tool(
action: a.action, action: a.action,
published: a.published, published: a.published,
affected: a.affected, affected: a.affected,
exploitability_score: normalizeExploitabilityScore(a.exploitability_score),
exploitability_rationale: a.exploitability_rationale || null,
})), })),
recommendation, recommendation: risk.recommendation,
reason, reason: risk.reason,
skillName: args.skillName, skillName: args.skillName,
skillVersion: args.skillVersion || null,
advisoryCount: matchingAdvisories.length, advisoryCount: matchingAdvisories.length,
}, null, 2), }, null, 2),
}], }],
@@ -280,18 +259,18 @@ server.tool(
server.tool( server.tool(
'clawsec_list_advisories', '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'), 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)'), affectedSkill: z.string().optional().describe('Filter by affected skill name (partial match supported)'),
limit: z.number().optional().describe('Maximum number of results (default: unlimited)'), limit: z.number().optional().describe('Maximum number of results (default: unlimited)'),
}, },
async (args) => { async (args) => {
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
try { try {
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
let advisories = [...cacheData.feed.advisories]; let advisories = [...cacheData.feed.advisories];
// Apply filters // Apply filters
@@ -299,7 +278,13 @@ server.tool(
advisories = advisories.filter((a: any) => a.severity === args.severity); advisories = advisories.filter((a: any) => a.severity === args.severity);
} }
if (args.type) { 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) { if (args.affectedSkill) {
advisories = advisories.filter((a: any) => advisories = advisories.filter((a: any) =>
@@ -307,9 +292,13 @@ server.tool(
); );
} }
// Sort by severity (critical first) and published date (newest first) // Sort by exploitability first, then severity, then publish date (newest first).
const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
advisories.sort((a: any, b: any) => { 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); const severityDiff = (severityOrder[a.severity] || 999) - (severityOrder[b.severity] || 999);
if (severityDiff !== 0) return severityDiff; if (severityDiff !== 0) return severityDiff;
return (b.published || '').localeCompare(a.published || ''); return (b.published || '').localeCompare(a.published || '');
@@ -336,6 +325,8 @@ server.tool(
action: a.action, action: a.action,
published: a.published, published: a.published,
affected: a.affected, affected: a.affected,
exploitability_score: normalizeExploitabilityScore(a.exploitability_score),
exploitability_rationale: a.exploitability_rationale || null,
})), })),
total: cacheData.feed.advisories.length, total: cacheData.feed.advisories.length,
filtered: originalCount, filtered: originalCount,
@@ -343,6 +334,7 @@ server.tool(
filters: { filters: {
severity: args.severity || null, severity: args.severity || null,
type: args.type || null, type: args.type || null,
exploitabilityScore: args.exploitabilityScore || null,
affectedSkill: args.affectedSkill || null, affectedSkill: args.affectedSkill || null,
limit: args.limit || null, limit: args.limit || null,
}, },
+14 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "clawsec-nanoclaw", "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", "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", "author": "prompt-security",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
@@ -27,6 +27,11 @@
"required": true, "required": true,
"description": "NanoClaw skill documentation" "description": "NanoClaw skill documentation"
}, },
{
"path": "CHANGELOG.md",
"required": true,
"description": "Version history and release notes"
},
{ {
"path": "INSTALL.md", "path": "INSTALL.md",
"required": true, "required": true,
@@ -62,6 +67,11 @@
"required": true, "required": true,
"description": "TypeScript type definitions" "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", "path": "advisories/feed-signing-public.pem",
"required": true, "required": true,
@@ -112,9 +122,10 @@
"capabilities": [ "capabilities": [
"Advisory feed monitoring from clawsec.prompt.security", "Advisory feed monitoring from clawsec.prompt.security",
"MCP tools for agent-initiated vulnerability scans", "MCP tools for agent-initiated vulnerability scans",
"Exploitability-aware advisory prioritization for agent environments",
"Pre-installation skill safety checks", "Pre-installation skill safety checks",
"Ed25519 signature verification for advisory feeds", "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" "Containerized agent support with IPC communication"
], ],
"nanoclaw": { "nanoclaw": {
@@ -135,7 +146,7 @@
}, },
"integration": { "integration": {
"mcp_tools_file": "container/agent-runner/src/ipc-mcp-stdio.ts", "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" "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/), 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). 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] ## [0.1.3]
### Added ### Added
+13 -2
View File
@@ -121,6 +121,7 @@ else
while IFS= read -r id; do while IFS= read -r id; do
[ -z "$id" ] && continue [ -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) | "- [\(.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" jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Action: \(.action // "Review advisory details")"' "$FEED_TMP"
done < "$NEW_IDS_FILE" done < "$NEW_IDS_FILE"
else else
@@ -194,8 +195,18 @@ fi
Heartbeat output should include: Heartbeat output should include:
- suite version status, - suite version status,
- advisory feed 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, - installed skills that appear in advisory `affected` lists,
- and a double-confirmation reminder before risky install/remove actions. - 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 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. 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 homepage: https://clawsec.prompt.security
clawdis: clawdis:
@@ -234,12 +234,25 @@ if [ -s "$NEW_IDS_FILE" ]; then
while IFS= read -r id; do while IFS= read -r id; do
[ -z "$id" ] && continue [ -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) | "- [\(.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" done < "$NEW_IDS_FILE"
else else
echo "FEED_OK - no new advisories" echo "FEED_OK - no new advisories"
fi 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 ## Heartbeat Integration
Use the suite heartbeat script as the single periodic security check entrypoint: Use the suite heartbeat script as the single periodic security check entrypoint:
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "clawsec-suite", "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.", "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", "author": "prompt-security",
"license": "AGPL-3.0-or-later", "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