mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
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:
@@ -193,6 +193,74 @@ jobs:
|
||||
echo "Created advisory JSON:"
|
||||
cat tmp_advisory.json
|
||||
|
||||
- name: Set up Python for exploitability analysis
|
||||
if: steps.parse.outputs.already_exists != 'true'
|
||||
uses: actions/setup-python@8d2f52b6169cf4c0f64d2e9f6f8f6d6a6e1c90f7 # v5.4.1
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Analyze exploitability for community advisory
|
||||
if: steps.parse.outputs.already_exists != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== Analyzing exploitability for community advisory ==="
|
||||
|
||||
# Extract fields from advisory for analysis
|
||||
CVE_ID=$(jq -r '.id' tmp_advisory.json)
|
||||
SEVERITY=$(jq -r '.severity // "medium"' tmp_advisory.json)
|
||||
VULN_TYPE=$(jq -r '.type // "unknown"' tmp_advisory.json)
|
||||
DESCRIPTION=$(jq -r '.description // ""' tmp_advisory.json)
|
||||
REFERENCES=$(jq -c '.references // []' tmp_advisory.json)
|
||||
|
||||
# Map severity to approximate CVSS score for analysis
|
||||
case "$SEVERITY" in
|
||||
critical) CVSS_SCORE=9.5 ;;
|
||||
high) CVSS_SCORE=7.5 ;;
|
||||
medium) CVSS_SCORE=5.5 ;;
|
||||
low) CVSS_SCORE=3.0 ;;
|
||||
*) CVSS_SCORE=5.0 ;;
|
||||
esac
|
||||
|
||||
# Build input JSON for analyzer
|
||||
INPUT_JSON=$(jq -n \
|
||||
--arg cve_id "$CVE_ID" \
|
||||
--argjson cvss_score "$CVSS_SCORE" \
|
||||
--arg type "$VULN_TYPE" \
|
||||
--arg description "$DESCRIPTION" \
|
||||
--argjson references "$REFERENCES" \
|
||||
'{
|
||||
cve_id: $cve_id,
|
||||
cvss_score: $cvss_score,
|
||||
type: $type,
|
||||
description: $description,
|
||||
references: $references
|
||||
}')
|
||||
|
||||
# Run exploitability analysis with exploit detection.
|
||||
# Continue without enrichment if analysis fails.
|
||||
if ANALYSIS=$(echo "$INPUT_JSON" | python utils/analyze_exploitability.py --json --check-exploits 2>/dev/null); then
|
||||
echo "$ANALYSIS" > tmp_exploitability.json
|
||||
echo "✓ Analyzed $CVE_ID"
|
||||
|
||||
# Merge exploitability analysis into advisory
|
||||
jq --slurpfile analysis tmp_exploitability.json '
|
||||
. + {
|
||||
exploitability_score: $analysis[0].exploitability_score,
|
||||
exploitability_rationale: $analysis[0].exploitability_rationale,
|
||||
attack_vector_analysis: $analysis[0].attack_vector_analysis,
|
||||
exploit_detection: $analysis[0].exploit_detection
|
||||
}
|
||||
' tmp_advisory.json > tmp_advisory_enriched.json
|
||||
|
||||
mv tmp_advisory_enriched.json tmp_advisory.json
|
||||
|
||||
echo "=== Exploitability analysis complete ==="
|
||||
echo "Exploitability score: $(jq -r '.exploitability_score // "unknown"' tmp_advisory.json)"
|
||||
else
|
||||
echo "::warning::Failed to analyze exploitability for $CVE_ID, continuing without enrichment"
|
||||
fi
|
||||
|
||||
- name: Update feed
|
||||
if: steps.parse.outputs.already_exists != 'true'
|
||||
run: |
|
||||
|
||||
@@ -353,7 +353,9 @@ jobs:
|
||||
title: (.cve.descriptions[] | select(.lang == "en") | .value | .[0:100] + (if length > 100 then "..." else "" end)),
|
||||
affected: normalized_affected,
|
||||
platforms: normalized_platforms,
|
||||
references: [.cve.references[]?.url // empty] | unique | .[0:3]
|
||||
references: [.cve.references[]?.url // empty] | unique | .[0:3],
|
||||
exploitability_score: null,
|
||||
exploitability_rationale: null
|
||||
}]
|
||||
' tmp/filtered_cves.json > tmp/nvd_current_state.json
|
||||
|
||||
@@ -553,7 +555,7 @@ jobs:
|
||||
end
|
||||
);
|
||||
|
||||
[.[] |
|
||||
[.[] |
|
||||
select(.cve.id as $id | $existing | index($id) | not) |
|
||||
{
|
||||
id: .cve.id,
|
||||
@@ -568,7 +570,9 @@ jobs:
|
||||
published: .cve.published,
|
||||
references: [.cve.references[]?.url // empty] | unique | .[0:3],
|
||||
cvss_score: get_cvss_score,
|
||||
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id)
|
||||
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id),
|
||||
exploitability_score: null,
|
||||
exploitability_rationale: null
|
||||
}
|
||||
]
|
||||
' tmp/filtered_cves.json > tmp/new_advisories.json
|
||||
@@ -582,6 +586,102 @@ jobs:
|
||||
jq '.[].id' tmp/new_advisories.json
|
||||
fi
|
||||
|
||||
- name: Set up Python for exploitability analysis
|
||||
if: steps.transform.outputs.new_count != '0'
|
||||
uses: actions/setup-python@8d2f52b6169cf4c0f64d2e9f6f8f6d6a6e1c90f7 # v5.4.1
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Analyze exploitability for new advisories
|
||||
if: steps.transform.outputs.new_count != '0'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== Analyzing exploitability for new advisories ==="
|
||||
|
||||
# Extract CVSS vectors from filtered CVEs to merge with advisories
|
||||
jq '
|
||||
[.[] | {
|
||||
id: .cve.id,
|
||||
cvss_vector: (
|
||||
.cve.metrics.cvssMetricV31[0]?.cvssData.vectorString //
|
||||
.cve.metrics.cvssMetricV30[0]?.cvssData.vectorString //
|
||||
.cve.metrics.cvssMetricV2[0]?.vectorString //
|
||||
""
|
||||
)
|
||||
}] | map({(.id): .cvss_vector}) | add
|
||||
' tmp/filtered_cves.json > tmp/cvss_vectors.json
|
||||
|
||||
# Process each advisory through exploitability analyzer
|
||||
jq -c '.[]' tmp/new_advisories.json | while IFS= read -r advisory; do
|
||||
CVE_ID=$(echo "$advisory" | jq -r '.id')
|
||||
CVSS_SCORE=$(echo "$advisory" | jq -r '.cvss_score // 0')
|
||||
CVSS_VECTOR=$(jq -r --arg id "$CVE_ID" '.[$id] // ""' tmp/cvss_vectors.json)
|
||||
VULN_TYPE=$(echo "$advisory" | jq -r '.type // ""')
|
||||
DESCRIPTION=$(echo "$advisory" | jq -r '.description // ""')
|
||||
REFERENCES=$(echo "$advisory" | jq -c '.references // []')
|
||||
|
||||
# Build input JSON for analyzer
|
||||
INPUT_JSON=$(jq -n \
|
||||
--arg cve_id "$CVE_ID" \
|
||||
--argjson cvss_score "$CVSS_SCORE" \
|
||||
--arg cvss_vector "$CVSS_VECTOR" \
|
||||
--arg type "$VULN_TYPE" \
|
||||
--arg description "$DESCRIPTION" \
|
||||
--argjson references "$REFERENCES" \
|
||||
'{
|
||||
cve_id: $cve_id,
|
||||
cvss_score: $cvss_score,
|
||||
cvss_vector: $cvss_vector,
|
||||
type: $type,
|
||||
description: $description,
|
||||
references: $references
|
||||
}')
|
||||
|
||||
# Run exploitability analysis with exploit detection.
|
||||
# Keep processing if any single advisory analysis fails.
|
||||
if ANALYSIS=$(echo "$INPUT_JSON" | python utils/analyze_exploitability.py --json --check-exploits 2>/dev/null); then
|
||||
echo "$ANALYSIS" > "tmp/exploitability_${CVE_ID}.json"
|
||||
echo "✓ Analyzed $CVE_ID"
|
||||
else
|
||||
echo "::warning::Failed to analyze exploitability for $CVE_ID, skipping enrichment"
|
||||
fi
|
||||
done
|
||||
|
||||
# Merge exploitability analysis back into advisories.
|
||||
if ls tmp/exploitability_*.json >/dev/null 2>&1; then
|
||||
jq -s '.' tmp/exploitability_*.json > tmp/exploitability_analyses.json
|
||||
else
|
||||
echo '[]' > tmp/exploitability_analyses.json
|
||||
fi
|
||||
|
||||
jq --slurpfile analyses tmp/exploitability_analyses.json '
|
||||
map(
|
||||
. as $advisory |
|
||||
($analyses[0] | map(select(.cve_id == $advisory.id)) | first) as $analysis |
|
||||
if $analysis then
|
||||
$advisory + {
|
||||
exploitability_score: $analysis.exploitability_score,
|
||||
exploitability_rationale: $analysis.exploitability_rationale,
|
||||
attack_vector_analysis: $analysis.attack_vector_analysis,
|
||||
exploit_detection: $analysis.exploit_detection
|
||||
}
|
||||
else
|
||||
$advisory
|
||||
end
|
||||
)
|
||||
' tmp/new_advisories.json > tmp/new_advisories_enriched.json
|
||||
|
||||
# Replace original with enriched version
|
||||
mv tmp/new_advisories_enriched.json tmp/new_advisories.json
|
||||
|
||||
echo "=== Exploitability analysis complete ==="
|
||||
|
||||
# Show summary of exploitability scores
|
||||
echo "Exploitability score distribution:"
|
||||
jq -r '.[] | "\(.id): \(.exploitability_score // "unknown")"' tmp/new_advisories.json | \
|
||||
awk -F': ' '{scores[$2]++} END {for (s in scores) print " " s ": " scores[s]}'
|
||||
|
||||
- name: Update feed.json
|
||||
if: steps.transform.outputs.new_count != '0' || steps.updates.outputs.update_count != '0'
|
||||
run: |
|
||||
|
||||
@@ -203,6 +203,17 @@ The feed polls CVEs related to:
|
||||
- Prompt injection patterns
|
||||
- Agent security vulnerabilities
|
||||
|
||||
### Exploitability Context
|
||||
|
||||
ClawSec enriches CVE advisories with **exploitability context** to help agents assess real-world risk beyond raw CVSS scores. Newly analyzed advisories can include:
|
||||
|
||||
- **Exploit Evidence**: Whether public exploits exist in the wild
|
||||
- **Weaponization Status**: If exploits are integrated into common attack frameworks
|
||||
- **Attack Requirements**: Prerequisites needed for successful exploitation (network access, authentication, user interaction)
|
||||
- **Risk Assessment**: Contextualized risk level combining technical severity with exploitability
|
||||
|
||||
This feature helps agents prioritize vulnerabilities that pose immediate threats versus theoretical risks, enabling smarter security decisions.
|
||||
|
||||
### Advisory Schema
|
||||
|
||||
**NVD CVE Advisory:**
|
||||
@@ -217,6 +228,8 @@ The feed polls CVEs related to:
|
||||
"published": "2026-02-01T00:00:00Z",
|
||||
"cvss_score": 8.8,
|
||||
"nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-XXXXX",
|
||||
"exploitability_score": "high|medium|low|unknown",
|
||||
"exploitability_rationale": "Why this CVE is or is not likely exploitable in agent deployments",
|
||||
"references": ["..."],
|
||||
"action": "Recommended remediation"
|
||||
}
|
||||
|
||||
Executable
+281
@@ -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"
|
||||
@@ -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
@@ -11,13 +11,14 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
# shellcheck source=./feed-utils.sh
|
||||
source "$SCRIPT_DIR/feed-utils.sh"
|
||||
|
||||
# Configuration - same as pipeline
|
||||
FEED_PATH="$PROJECT_ROOT/advisories/feed.json"
|
||||
SKILL_FEED_PATH="$PROJECT_ROOT/skills/clawsec-feed/advisories/feed.json"
|
||||
PUBLIC_FEED_PATH="$PROJECT_ROOT/public/advisories/feed.json"
|
||||
init_feed_paths "$PROJECT_ROOT"
|
||||
KEYWORDS="OpenClaw clawdbot Moltbot NanoClaw WhatsApp-bot baileys"
|
||||
GITHUB_REF_PATTERN="github.com/openclaw/openclaw github.com/qwibitai/NanoClaw"
|
||||
ANALYZER="$PROJECT_ROOT/utils/analyze_exploitability.py"
|
||||
|
||||
# Parse args
|
||||
DAYS_BACK=120
|
||||
@@ -46,6 +47,22 @@ echo "Days back: $DAYS_BACK"
|
||||
echo "Force mode: $FORCE"
|
||||
echo ""
|
||||
|
||||
# Verify exploitability analyzer prerequisites
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "Error: python3 is required but not found in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$ANALYZER" ]; then
|
||||
echo "Error: Exploitability analyzer not found: $ANALYZER"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! python3 "$ANALYZER" --help &> /dev/null; then
|
||||
echo "Error: Exploitability analyzer failed to run. Check Python environment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create temp directory
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
@@ -62,7 +79,7 @@ fi
|
||||
if [ -z "${START_DATE:-}" ]; then
|
||||
# macOS vs Linux date compatibility
|
||||
if date -v-1d > /dev/null 2>&1; then
|
||||
START_DATE=$(date -u -v-${DAYS_BACK}d +%Y-%m-%dT%H:%M:%S.000Z)
|
||||
START_DATE=$(date -u -v-"${DAYS_BACK}"d +%Y-%m-%dT%H:%M:%S.000Z)
|
||||
else
|
||||
START_DATE=$(date -u -d "${DAYS_BACK} days ago" +%Y-%m-%dT%H:%M:%S.000Z)
|
||||
fi
|
||||
@@ -74,8 +91,8 @@ echo "End date: $END_DATE"
|
||||
echo ""
|
||||
|
||||
# URL encode dates
|
||||
START_ENC=$(echo "$START_DATE" | sed 's/:/%3A/g')
|
||||
END_ENC=$(echo "$END_DATE" | sed 's/:/%3A/g')
|
||||
START_ENC=${START_DATE//:/%3A}
|
||||
END_ENC=${END_DATE//:/%3A}
|
||||
|
||||
echo "=== Fetching CVEs from NVD ==="
|
||||
|
||||
@@ -267,7 +284,7 @@ jq --argjson existing "$EXISTING_JSON" '
|
||||
| if length == 0 then ["openclaw@*", "nanoclaw@*"] else . end
|
||||
);
|
||||
|
||||
[.[] |
|
||||
[.[] |
|
||||
select(.cve.id as $id | $existing | index($id) | not) |
|
||||
{
|
||||
id: .cve.id,
|
||||
@@ -281,7 +298,9 @@ jq --argjson existing "$EXISTING_JSON" '
|
||||
published: .cve.published,
|
||||
references: [.cve.references[]?.url // empty] | unique | .[0:3],
|
||||
cvss_score: get_cvss_score,
|
||||
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id)
|
||||
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id),
|
||||
exploitability_score: null,
|
||||
exploitability_rationale: null
|
||||
}
|
||||
]
|
||||
' "$TEMP_DIR/filtered_cves.json" > "$TEMP_DIR/new_advisories.json"
|
||||
@@ -296,6 +315,87 @@ if [ "$NEW_COUNT" -eq 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Analyzing Exploitability ==="
|
||||
|
||||
# Build CVSS vector lookup for enriched analysis inputs.
|
||||
jq '
|
||||
[.[] | {
|
||||
id: .cve.id,
|
||||
cvss_vector: (
|
||||
.cve.metrics.cvssMetricV31[0]?.cvssData.vectorString //
|
||||
.cve.metrics.cvssMetricV30[0]?.cvssData.vectorString //
|
||||
.cve.metrics.cvssMetricV2[0]?.vectorString //
|
||||
""
|
||||
)
|
||||
}] | map({(.id): .cvss_vector}) | add
|
||||
' "$TEMP_DIR/filtered_cves.json" > "$TEMP_DIR/cvss_vectors.json"
|
||||
|
||||
ANALYZED_COUNT=0
|
||||
FAILED_ANALYSIS=0
|
||||
|
||||
while IFS= read -r advisory; do
|
||||
CVE_ID=$(echo "$advisory" | jq -r '.id')
|
||||
CVSS_SCORE=$(echo "$advisory" | jq -r '.cvss_score // 0')
|
||||
CVSS_VECTOR=$(jq -r --arg id "$CVE_ID" '.[$id] // ""' "$TEMP_DIR/cvss_vectors.json")
|
||||
VULN_TYPE=$(echo "$advisory" | jq -r '.type // ""')
|
||||
DESCRIPTION=$(echo "$advisory" | jq -r '.description // ""')
|
||||
REFERENCES=$(echo "$advisory" | jq -c '.references // []')
|
||||
|
||||
INPUT_JSON=$(jq -n \
|
||||
--arg cve_id "$CVE_ID" \
|
||||
--argjson cvss_score "$CVSS_SCORE" \
|
||||
--arg cvss_vector "$CVSS_VECTOR" \
|
||||
--arg type "$VULN_TYPE" \
|
||||
--arg description "$DESCRIPTION" \
|
||||
--argjson references "$REFERENCES" \
|
||||
'{
|
||||
cve_id: $cve_id,
|
||||
cvss_score: $cvss_score,
|
||||
cvss_vector: $cvss_vector,
|
||||
type: $type,
|
||||
description: $description,
|
||||
references: $references
|
||||
}')
|
||||
|
||||
if ANALYSIS=$(echo "$INPUT_JSON" | python3 "$ANALYZER" --json --check-exploits 2>/dev/null); then
|
||||
echo "$ANALYSIS" > "$TEMP_DIR/exploitability_${CVE_ID}.json"
|
||||
SCORE=$(echo "$ANALYSIS" | jq -r '.exploitability_score // "unknown"')
|
||||
echo " ✓ $CVE_ID -> $SCORE"
|
||||
ANALYZED_COUNT=$((ANALYZED_COUNT + 1))
|
||||
else
|
||||
echo " ⚠ $CVE_ID analysis failed; keeping null exploitability fields"
|
||||
FAILED_ANALYSIS=$((FAILED_ANALYSIS + 1))
|
||||
fi
|
||||
done < <(jq -c '.[]' "$TEMP_DIR/new_advisories.json")
|
||||
|
||||
if ls "$TEMP_DIR"/exploitability_*.json >/dev/null 2>&1; then
|
||||
jq -s '.' "$TEMP_DIR"/exploitability_*.json > "$TEMP_DIR/exploitability_analyses.json"
|
||||
else
|
||||
echo '[]' > "$TEMP_DIR/exploitability_analyses.json"
|
||||
fi
|
||||
|
||||
jq --slurpfile analyses "$TEMP_DIR/exploitability_analyses.json" '
|
||||
map(
|
||||
. as $advisory |
|
||||
($analyses[0] | map(select(.cve_id == $advisory.id)) | first) as $analysis |
|
||||
if $analysis then
|
||||
$advisory + {
|
||||
exploitability_score: $analysis.exploitability_score,
|
||||
exploitability_rationale: $analysis.exploitability_rationale,
|
||||
attack_vector_analysis: $analysis.attack_vector_analysis,
|
||||
exploit_detection: $analysis.exploit_detection
|
||||
}
|
||||
else
|
||||
$advisory
|
||||
end
|
||||
)
|
||||
' "$TEMP_DIR/new_advisories.json" > "$TEMP_DIR/new_advisories_enriched.json"
|
||||
|
||||
mv "$TEMP_DIR/new_advisories_enriched.json" "$TEMP_DIR/new_advisories.json"
|
||||
|
||||
echo "Exploitability analysis complete: $ANALYZED_COUNT analyzed, $FAILED_ANALYSIS failed"
|
||||
|
||||
echo ""
|
||||
echo "=== New Advisories ==="
|
||||
jq -r '.[] | " \(.id) [\(.severity)] - \(.title)"' "$TEMP_DIR/new_advisories.json"
|
||||
@@ -338,16 +438,9 @@ if jq empty "$TEMP_DIR/updated_feed.json" 2>/dev/null; then
|
||||
# Update main feed
|
||||
cp "$TEMP_DIR/updated_feed.json" "$FEED_PATH"
|
||||
echo "✓ Updated: $FEED_PATH"
|
||||
|
||||
# Update skill feed
|
||||
mkdir -p "$(dirname "$SKILL_FEED_PATH")"
|
||||
cp "$FEED_PATH" "$SKILL_FEED_PATH"
|
||||
echo "✓ Updated: $SKILL_FEED_PATH"
|
||||
|
||||
# Update public feed for local dev
|
||||
mkdir -p "$(dirname "$PUBLIC_FEED_PATH")"
|
||||
cp "$FEED_PATH" "$PUBLIC_FEED_PATH"
|
||||
echo "✓ Updated: $PUBLIC_FEED_PATH"
|
||||
|
||||
# Sync feed mirrors for local skill/public consumers.
|
||||
sync_feed_to_mirrors "$FEED_PATH" "create"
|
||||
|
||||
echo ""
|
||||
TOTAL_ADVISORIES=$(jq '.advisories | length' "$FEED_PATH")
|
||||
|
||||
@@ -76,13 +76,13 @@ fi
|
||||
# ESLint
|
||||
echo -e "\n${YELLOW}Running ESLint...${NC}"
|
||||
if $FIX_MODE; then
|
||||
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --fix; then
|
||||
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --ignore-pattern '.auto-claude/**' --fix; then
|
||||
check_pass "ESLint (with auto-fix)"
|
||||
else
|
||||
check_fail "ESLint found unfixable issues"
|
||||
fi
|
||||
else
|
||||
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --max-warnings 0; then
|
||||
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --ignore-pattern '.auto-claude/**' --max-warnings 0; then
|
||||
check_pass "ESLint"
|
||||
else
|
||||
check_fail "ESLint found issues (run with --fix to auto-fix)"
|
||||
@@ -190,7 +190,7 @@ print_header "Security"
|
||||
# Trivy FS Scan
|
||||
if command -v trivy &> /dev/null; then
|
||||
echo -e "\n${YELLOW}Running Trivy filesystem scan...${NC}"
|
||||
if trivy fs . --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed; then
|
||||
if trivy fs . --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed --skip-dirs .auto-claude --skip-files clawsec-signing-private.pem; then
|
||||
check_pass "Trivy filesystem scan"
|
||||
else
|
||||
check_fail "Trivy found CRITICAL/HIGH vulnerabilities"
|
||||
|
||||
@@ -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`.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: clawsec-feed
|
||||
version: 0.0.4
|
||||
version: 0.0.5
|
||||
description: Security advisory feed with automated NVD CVE polling for OpenClaw-related vulnerabilities. Updated daily.
|
||||
homepage: https://clawsec.prompt.security
|
||||
metadata: {"openclaw":{"emoji":"📡","category":"security"}}
|
||||
@@ -318,7 +318,9 @@ curl -sSL --fail --show-error --retry 3 --retry-delay 1 "$FEED_URL"
|
||||
"description": "Skill sends user data to external server",
|
||||
"affected": ["helper-plus@1.0.0", "helper-plus@1.0.1"],
|
||||
"action": "Remove immediately",
|
||||
"published": "2026-02-01T10:00:00Z"
|
||||
"published": "2026-02-01T10:00:00Z",
|
||||
"exploitability_score": "critical",
|
||||
"exploitability_rationale": "Trivially exploitable through normal skill usage; no special conditions required. Active exploitation observed in the wild."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -385,6 +387,42 @@ fi
|
||||
echo "$RECENT"
|
||||
```
|
||||
|
||||
### Filter by exploitability score
|
||||
|
||||
Shared exploitability prioritization guidance is maintained in:
|
||||
|
||||
- [`wiki/exploitability-scoring.md`](../../wiki/exploitability-scoring.md)
|
||||
- [`skills/clawsec-suite/SKILL.md`](../clawsec-suite/SKILL.md) ("Quick feed check")
|
||||
|
||||
### Get exploitability context for an advisory
|
||||
|
||||
```bash
|
||||
# Show exploitability details for a specific CVE
|
||||
CVE_ID="CVE-2026-27488"
|
||||
echo "$FEED" | jq --arg cve "$CVE_ID" '.advisories[] | select(.id == $cve) | {
|
||||
id: .id,
|
||||
severity: .severity,
|
||||
exploitability_score: .exploitability_score,
|
||||
exploitability_rationale: .exploitability_rationale,
|
||||
title: .title
|
||||
}'
|
||||
```
|
||||
|
||||
### Prioritize advisories by exploitability
|
||||
|
||||
```bash
|
||||
# Sort advisories by exploitability (critical → high → medium → low)
|
||||
# This helps agents focus on the most immediately actionable threats
|
||||
echo "$FEED" | jq '[.advisories[] | select(.exploitability_score != null)] |
|
||||
sort_by(
|
||||
if .exploitability_score == "critical" then 0
|
||||
elif .exploitability_score == "high" then 1
|
||||
elif .exploitability_score == "medium" then 2
|
||||
elif .exploitability_score == "low" then 3
|
||||
else 4 end
|
||||
)'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cross-Reference Installed Skills
|
||||
@@ -476,23 +514,75 @@ done
|
||||
|
||||
---
|
||||
|
||||
## Prioritizing High-Exploitability Threats
|
||||
|
||||
**IMPORTANT:** When reviewing advisories, always prioritize by **exploitability score** in addition to severity. The exploitability score indicates how easily a vulnerability can be exploited in practice, helping you focus on the most actionable threats.
|
||||
|
||||
### Exploitability Priority Levels
|
||||
|
||||
| Exploitability | Meaning | Action Priority |
|
||||
|----------------|---------|-----------------|
|
||||
| `high` | Trivially or easily exploitable with public tooling | **Immediate notification** |
|
||||
| `medium` | Exploitable but requires specific conditions | **Standard notification** |
|
||||
| `low` | Difficult to exploit or theoretical | **Low priority notification** |
|
||||
|
||||
### How to Use Exploitability in Notifications
|
||||
|
||||
1. **Filter for high-exploitability first:**
|
||||
```bash
|
||||
# Get high exploitability advisories
|
||||
echo "$FEED" | jq '.advisories[] | select(.exploitability_score == "high")'
|
||||
```
|
||||
|
||||
2. **Include exploitability in notifications:**
|
||||
```
|
||||
📡 ClawSec Feed: High-exploitability alert
|
||||
|
||||
CRITICAL - CVE-2026-27488 (Exploitability: HIGH)
|
||||
→ Trivially exploitable RCE in skill-loader v2.1.0
|
||||
→ Public exploit code available
|
||||
→ Recommended action: Immediate removal or upgrade to v2.1.1
|
||||
```
|
||||
|
||||
3. **Prioritize by both severity AND exploitability:**
|
||||
- A HIGH severity + HIGH exploitability CVE is more urgent than a CRITICAL severity + LOW exploitability CVE
|
||||
- Focus user attention on threats that are both severe and easily exploitable
|
||||
- Include the exploitability rationale to help users understand the risk context
|
||||
|
||||
### Example Notification Priority Order
|
||||
|
||||
When multiple advisories exist, present them in this order:
|
||||
1. **Critical severity + High exploitability** - most urgent
|
||||
2. **High severity + High exploitability**
|
||||
3. **Critical severity + Medium/Low exploitability**
|
||||
4. **High severity + Medium/Low exploitability**
|
||||
5. **Medium/Low severity** (any exploitability)
|
||||
|
||||
This ensures you alert users to the most actionable, immediately dangerous threats first.
|
||||
|
||||
---
|
||||
|
||||
## When to Notify Your User
|
||||
|
||||
**Notify Immediately (Critical):**
|
||||
- New critical advisory affecting an installed skill
|
||||
- Active exploitation detected
|
||||
- **High exploitability score** (regardless of severity)
|
||||
|
||||
**Notify Soon (High):**
|
||||
- New high-severity advisory affecting installed skills
|
||||
- Failed to fetch advisory feed (network issue?)
|
||||
- Medium exploitability with high severity
|
||||
|
||||
**Notify at Next Interaction (Medium):**
|
||||
- New medium-severity advisories
|
||||
- General security updates
|
||||
- Low exploitability advisories
|
||||
|
||||
**Log Only (Low/Info):**
|
||||
- Low-severity advisories (mention if user asks)
|
||||
- Feed checked, no new advisories
|
||||
- Theoretical vulnerabilities (low exploitability, low severity)
|
||||
|
||||
---
|
||||
|
||||
@@ -503,11 +593,13 @@ done
|
||||
```
|
||||
📡 ClawSec Feed: 2 new advisories since last check
|
||||
|
||||
CRITICAL - GA-2026-015: Malicious prompt pattern "ignore-all"
|
||||
CRITICAL - GA-2026-015: Malicious prompt pattern "ignore-all" (Exploitability: HIGH)
|
||||
→ Detected prompt injection technique. Update your system prompt defenses.
|
||||
→ Exploitability: Easily exploitable with publicly documented techniques.
|
||||
|
||||
HIGH - GA-2026-016: Vulnerable skill "data-helper" v1.2.0
|
||||
HIGH - GA-2026-016: Vulnerable skill "data-helper" v1.2.0 (Exploitability: MEDIUM)
|
||||
→ You have this installed! Recommended action: Update to v1.2.1 or remove.
|
||||
→ Exploitability: Requires specific configuration; not trivially exploitable.
|
||||
```
|
||||
|
||||
### If nothing new:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawsec-feed",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"description": "Security advisory feed monitoring for AI agents. Subscribe to community-driven threat intelligence.",
|
||||
"author": "prompt-security",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
@@ -21,6 +21,11 @@
|
||||
"required": true,
|
||||
"description": "Advisory feed skill documentation"
|
||||
},
|
||||
{
|
||||
"path": "CHANGELOG.md",
|
||||
"required": true,
|
||||
"description": "Version history for advisory feed updates"
|
||||
},
|
||||
{
|
||||
"path": "advisories/feed.json",
|
||||
"required": true,
|
||||
|
||||
@@ -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.
|
||||
@@ -8,7 +8,7 @@ ClawSec provides security advisory monitoring for NanoClaw through:
|
||||
- **MCP Tools**: Agents can check for vulnerabilities via `clawsec_check_advisories`
|
||||
- **Advisory Feed**: Automatic monitoring of https://clawsec.prompt.security/advisories/feed.json
|
||||
- **Signature Verification**: Ed25519-signed feeds ensure integrity
|
||||
- **Platform Targeting**: Advisories can be NanoClaw-specific or cross-platform
|
||||
- **Exploitability Context**: Advisories include exploitability score and rationale for triage
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -57,18 +57,30 @@ in each tool file).
|
||||
|
||||
Add the host-side IPC handlers for ClawSec operations.
|
||||
|
||||
**File**: `host/ipc-handler.ts`
|
||||
**File**: `src/ipc.ts`
|
||||
|
||||
```typescript
|
||||
// Add this import at the top
|
||||
import { registerClawSecHandlers } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
|
||||
// Add these imports at the top
|
||||
import { handleAdvisoryIpc } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
|
||||
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
|
||||
import { SkillSignatureVerifier } from '../skills/clawsec-nanoclaw/host-services/skill-signature-handler.js';
|
||||
|
||||
// In your IPC handler setup function
|
||||
export function setupIpcHandlers() {
|
||||
// ... your existing handlers ...
|
||||
// Initialize these once in host startup and pass through deps
|
||||
const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger);
|
||||
const signatureVerifier = new SkillSignatureVerifier();
|
||||
|
||||
// Register ClawSec handlers
|
||||
registerClawSecHandlers();
|
||||
// In processTaskIpc switch:
|
||||
case 'refresh_advisory_cache':
|
||||
case 'verify_skill_signature':
|
||||
await handleAdvisoryIpc(
|
||||
data,
|
||||
{ advisoryCacheManager, signatureVerifier },
|
||||
logger,
|
||||
sourceGroup
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// existing task handling
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,23 +88,25 @@ export function setupIpcHandlers() {
|
||||
|
||||
Add the advisory cache manager to your host services.
|
||||
|
||||
**File**: `host/index.ts` (or your main entry point)
|
||||
**File**: `src/index.ts` (or your main entry point)
|
||||
|
||||
```typescript
|
||||
// Add this import
|
||||
import { startAdvisoryCache } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
|
||||
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
|
||||
|
||||
// Start the service when your host process starts
|
||||
async function main() {
|
||||
// ... your existing initialization ...
|
||||
|
||||
// Start ClawSec advisory cache (fetches feed every 6 hours)
|
||||
startAdvisoryCache({
|
||||
cacheFile: '/workspace/project/data/clawsec-advisory-cache.json',
|
||||
feedUrl: 'https://clawsec.prompt.security/advisories/feed.json',
|
||||
publicKeyPath: '/workspace/project/skills/clawsec-nanoclaw/advisories/feed-signing-public.pem',
|
||||
refreshInterval: 6 * 60 * 60 * 1000, // 6 hours
|
||||
});
|
||||
// Initialize cache manager and prime it at startup
|
||||
const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger);
|
||||
await advisoryCacheManager.initialize();
|
||||
|
||||
// Recommended refresh cadence (6h)
|
||||
setInterval(() => {
|
||||
advisoryCacheManager.refresh().catch((error) => {
|
||||
logger.error({ error }, 'Periodic advisory cache refresh failed');
|
||||
});
|
||||
}, 6 * 60 * 60 * 1000);
|
||||
|
||||
// ... rest of your startup ...
|
||||
}
|
||||
@@ -151,9 +165,9 @@ cat /workspace/project/data/clawsec-advisory-cache.json
|
||||
|
||||
You should see:
|
||||
- `feed`: Array of advisories
|
||||
- `signature`: Ed25519 signature
|
||||
- `lastFetch`: Timestamp of last update
|
||||
- `fetchedAt`: Timestamp of last update
|
||||
- `verified`: Should be `true`
|
||||
- `publicKeyFingerprint`: SHA-256 fingerprint of the pinned signing key
|
||||
|
||||
## Usage Examples
|
||||
|
||||
@@ -183,13 +197,13 @@ You can also call the MCP tools directly from agent code:
|
||||
```typescript
|
||||
// Check all installed skills
|
||||
const result = await tools.clawsec_check_advisories({
|
||||
skillsRoot: '/workspace/project/skills'
|
||||
installRoot: '/home/node/.claude/skills'
|
||||
});
|
||||
|
||||
// Check specific skill before installation
|
||||
const safetyCheck = await tools.clawsec_check_skill_safety({
|
||||
skillName: 'risky-skill',
|
||||
version: '1.0.0'
|
||||
skillVersion: '1.0.0'
|
||||
});
|
||||
```
|
||||
|
||||
@@ -199,19 +213,19 @@ const safetyCheck = await tools.clawsec_check_skill_safety({
|
||||
|
||||
Default: `/workspace/project/data/clawsec-advisory-cache.json`
|
||||
|
||||
To change, update the `cacheFile` parameter in `startAdvisoryCache()`.
|
||||
To change, pass a different data directory path to `new AdvisoryCacheManager(dataDir, logger)`.
|
||||
|
||||
### Refresh Interval
|
||||
|
||||
Default: 6 hours
|
||||
|
||||
To change, update the `refreshInterval` parameter (in milliseconds).
|
||||
To change, update the `setInterval(...)` duration (in milliseconds) in host startup.
|
||||
|
||||
### Feed URL
|
||||
|
||||
Default: `https://clawsec.prompt.security/advisories/feed.json`
|
||||
|
||||
To use a mirror or custom feed, update the `feedUrl` parameter.
|
||||
To use a mirror or custom feed, update `FEED_URL` in `skills/clawsec-nanoclaw/host-services/advisory-cache.ts`.
|
||||
|
||||
## Platform-Specific Advisories
|
||||
|
||||
@@ -222,7 +236,7 @@ ClawSec advisories can target specific platforms:
|
||||
- **`platforms: ["openclaw", "nanoclaw"]`**: Affects both
|
||||
- **No `platforms` field**: Applies to all platforms
|
||||
|
||||
The MCP tools automatically filter advisories based on your platform.
|
||||
Platform metadata is preserved in advisory records and can be filtered by your policy layer.
|
||||
|
||||
## Security
|
||||
|
||||
@@ -260,7 +274,7 @@ Never manually edit the cache file - it will break signature verification.
|
||||
**Problem**: Advisory cache is empty or stale
|
||||
|
||||
**Solution**:
|
||||
1. Check that `startAdvisoryCache()` is called in your host entry point
|
||||
1. Check that `AdvisoryCacheManager.initialize()` is called in your host entry point
|
||||
2. Verify network access to `clawsec.prompt.security`
|
||||
3. Check host logs for fetch errors
|
||||
4. Manually trigger: `curl https://clawsec.prompt.security/advisories/feed.json`
|
||||
@@ -280,7 +294,7 @@ Never manually edit the cache file - it will break signature verification.
|
||||
**Problem**: Tools return errors about IPC
|
||||
|
||||
**Solution**:
|
||||
1. Verify IPC handlers are registered in `host/ipc-handler.ts`
|
||||
1. Verify IPC handlers are registered in `src/ipc.ts`
|
||||
2. Check that IPC directory exists and is writable
|
||||
3. Ensure host process is running
|
||||
4. Check host logs for handler errors
|
||||
@@ -290,8 +304,8 @@ Never manually edit the cache file - it will break signature verification.
|
||||
To remove ClawSec from NanoClaw:
|
||||
|
||||
1. Remove MCP tool registration from `ipc-mcp-stdio.ts`
|
||||
2. Remove IPC handler registration from `host/ipc-handler.ts`
|
||||
3. Remove `startAdvisoryCache()` call from host entry point
|
||||
2. Remove IPC handler registration from `src/ipc.ts`
|
||||
3. Remove `AdvisoryCacheManager` initialization from host entry point
|
||||
4. Delete the skill directory: `rm -rf skills/clawsec-nanoclaw`
|
||||
5. Delete the cache file: `rm /workspace/project/data/clawsec-advisory-cache.json`
|
||||
6. Restart NanoClaw
|
||||
|
||||
@@ -56,9 +56,9 @@ ClawSec provides a complete security skill for NanoClaw deployments:
|
||||
- `clawsec_integrity_status` - View file baseline status
|
||||
- `clawsec_verify_audit` - Verify audit log hash chain
|
||||
|
||||
- **Advisory Cache Service**: Automatic feed fetching every 6 hours
|
||||
- **Advisory Cache Service**: Host-managed feed fetching with signature validation
|
||||
- **Signature Verification**: Ed25519-signed feeds ensure integrity
|
||||
- **Platform Filtering**: Shows only relevant advisories for NanoClaw
|
||||
- **Exploitability Context**: Surfaces `exploitability_score` and rationale to reduce alert fatigue
|
||||
- **IPC Communication**: Container-safe host communication
|
||||
|
||||
### Installation
|
||||
@@ -77,19 +77,19 @@ The skill integrates into three places:
|
||||
**1. MCP Tools** (container):
|
||||
```typescript
|
||||
// container/agent-runner/src/ipc-mcp-stdio.ts
|
||||
import { clawsecTools } from '../../../skills/clawsec-nanoclaw/mcp-tools/advisory-tools.js';
|
||||
import '../../../skills/clawsec-nanoclaw/mcp-tools/advisory-tools.js';
|
||||
```
|
||||
|
||||
**2. IPC Handlers** (host):
|
||||
```typescript
|
||||
// host/ipc-handler.ts
|
||||
import { registerClawSecHandlers } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
|
||||
// src/ipc.ts
|
||||
import { handleAdvisoryIpc } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
|
||||
```
|
||||
|
||||
**3. Cache Service** (host):
|
||||
```typescript
|
||||
// host/index.ts
|
||||
import { startAdvisoryCache } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
|
||||
// src/index.ts
|
||||
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
|
||||
```
|
||||
|
||||
### Advisory Feed
|
||||
@@ -148,4 +148,4 @@ Planned features for future releases:
|
||||
|
||||
- **Issues**: https://github.com/prompt-security/clawsec/issues
|
||||
- **Security**: security@prompt.security
|
||||
- NanoClaw Repository: (link TBD)
|
||||
- NanoClaw Repository: https://github.com/qwibitai/nanoclaw
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: clawsec-nanoclaw
|
||||
version: 0.0.1
|
||||
version: 0.0.2
|
||||
description: Use when checking for security vulnerabilities in NanoClaw skills, before installing new skills, or when asked about security advisories affecting the bot
|
||||
---
|
||||
|
||||
@@ -10,7 +10,7 @@ Security advisory monitoring that protects your WhatsApp bot from known vulnerab
|
||||
|
||||
## Overview
|
||||
|
||||
ClawSec provides MCP tools that check installed skills against a curated feed of security advisories. It prevents installation of vulnerable skills and alerts you to issues in existing ones.
|
||||
ClawSec provides MCP tools that check installed skills against a curated feed of security advisories. It prevents installation of vulnerable skills, includes exploitability context for triage, and alerts you to issues in existing ones.
|
||||
|
||||
**Core principle:** Check before you install. Monitor what's running.
|
||||
|
||||
@@ -36,7 +36,7 @@ Do NOT use for:
|
||||
// Before installing any skill
|
||||
const safety = await tools.clawsec_check_skill_safety({
|
||||
skillName: 'new-skill',
|
||||
version: '1.0.0' // optional
|
||||
skillVersion: '1.0.0' // optional
|
||||
});
|
||||
|
||||
if (!safety.safe) {
|
||||
@@ -48,14 +48,16 @@ if (!safety.safe) {
|
||||
### Security Audit
|
||||
|
||||
```typescript
|
||||
// Check all installed skills
|
||||
// Check all installed skills (defaults to ~/.claude/skills in the container)
|
||||
const result = await tools.clawsec_check_advisories({
|
||||
skillsRoot: '/workspace/project/skills' // optional
|
||||
installRoot: '/home/node/.claude/skills' // optional
|
||||
});
|
||||
|
||||
if (result.criticalCount > 0) {
|
||||
if (result.matches.some((m) =>
|
||||
m.advisory.severity === 'critical' || m.advisory.exploitability_score === 'high'
|
||||
)) {
|
||||
// Alert user immediately
|
||||
console.error('CRITICAL vulnerabilities found!');
|
||||
console.error('Urgent advisories found!');
|
||||
}
|
||||
```
|
||||
|
||||
@@ -64,8 +66,8 @@ if (result.criticalCount > 0) {
|
||||
```typescript
|
||||
// List advisories with filters
|
||||
const advisories = await tools.clawsec_list_advisories({
|
||||
platform: 'nanoclaw', // optional: nanoclaw, openclaw, or both
|
||||
severity: 'critical' // optional: critical, high, medium, low
|
||||
severity: 'high', // optional
|
||||
exploitabilityScore: 'high' // optional
|
||||
});
|
||||
```
|
||||
|
||||
@@ -75,7 +77,7 @@ const advisories = await tools.clawsec_list_advisories({
|
||||
|------|------|---------------|
|
||||
| Pre-install check | `clawsec_check_skill_safety` | `skillName` |
|
||||
| Audit all skills | `clawsec_check_advisories` | `installRoot` (optional) |
|
||||
| Browse feed | `clawsec_list_advisories` | `severity`, `type` (optional) |
|
||||
| Browse feed | `clawsec_list_advisories` | `severity`, `type`, `exploitabilityScore` (optional) |
|
||||
| Verify package signature | `clawsec_verify_skill_package` | `packagePath` |
|
||||
| Refresh advisory cache | `clawsec_refresh_cache` | (none) |
|
||||
| Check file integrity | `clawsec_check_integrity` | `mode`, `autoRestore` (optional) |
|
||||
@@ -110,7 +112,7 @@ if (safety.safe) {
|
||||
```typescript
|
||||
// Add to scheduled tasks
|
||||
schedule_task({
|
||||
prompt: "Check for security advisories using clawsec_check_advisories and alert if any critical issues found",
|
||||
prompt: "Check advisories using clawsec_check_advisories and alert when critical or high-exploitability matches appear",
|
||||
schedule_type: "cron",
|
||||
schedule_value: "0 9 * * *" // Daily at 9am
|
||||
});
|
||||
@@ -125,8 +127,8 @@ You: I'll check installed skills for known vulnerabilities.
|
||||
[Use clawsec_check_advisories]
|
||||
|
||||
Response:
|
||||
✅ No critical issues found.
|
||||
- 2 low-severity advisories (not urgent)
|
||||
✅ No urgent issues found.
|
||||
- 2 low-severity/low-exploitability advisories
|
||||
- All skills up to date
|
||||
```
|
||||
|
||||
@@ -146,30 +148,33 @@ const safety = await tools.clawsec_check_skill_safety({
|
||||
if (safety.safe) await installSkill('untrusted-skill');
|
||||
```
|
||||
|
||||
### ❌ Ignoring platform filters
|
||||
### ❌ Ignoring exploitability context
|
||||
```typescript
|
||||
// DON'T: Check OpenClaw advisories on NanoClaw
|
||||
const advisories = await tools.clawsec_list_advisories({
|
||||
platform: 'openclaw' // Wrong platform!
|
||||
});
|
||||
// DON'T: Use severity only
|
||||
if (advisory.severity === 'high') {
|
||||
notifyNow(advisory);
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// DO: Use correct platform or let it auto-filter
|
||||
const advisories = await tools.clawsec_list_advisories({
|
||||
platform: 'nanoclaw' // Correct
|
||||
});
|
||||
// DO: Use exploitability + severity
|
||||
if (
|
||||
advisory.exploitability_score === 'high' ||
|
||||
advisory.severity === 'critical'
|
||||
) {
|
||||
notifyNow(advisory);
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Skipping critical severity
|
||||
```typescript
|
||||
// DON'T: Only check low severity
|
||||
if (result.lowCount > 0) alert();
|
||||
// DON'T: Ignore high exploitability in medium severity advisories
|
||||
if (advisory.severity === 'critical') alert();
|
||||
```
|
||||
|
||||
```typescript
|
||||
// DO: Prioritize critical and high
|
||||
if (result.criticalCount > 0 || result.highCount > 0) {
|
||||
// DO: Prioritize exploitability and severity together
|
||||
if (advisory.exploitability_score === 'high' || advisory.severity === 'critical') {
|
||||
// Alert immediately
|
||||
}
|
||||
```
|
||||
@@ -182,7 +187,7 @@ if (result.criticalCount > 0 || result.highCount > 0) {
|
||||
|
||||
**Signature Verification**: Ed25519 signed feeds
|
||||
|
||||
**Cache Location**: `/workspace/project/data/clawsec-cache.json`
|
||||
**Cache Location**: `/workspace/project/data/clawsec-advisory-cache.json`
|
||||
|
||||
See [INSTALL.md](./INSTALL.md) for setup and [docs/](./docs/) for advanced usage.
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import crypto from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import https from 'node:https';
|
||||
import path from 'node:path';
|
||||
import { evaluateAdvisoryRisk } from '../lib/risk.js';
|
||||
|
||||
// ClawSec public key (from clawsec-signing-public.pem)
|
||||
const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
||||
@@ -35,6 +36,8 @@ export interface Advisory {
|
||||
action?: string;
|
||||
published?: string;
|
||||
updated?: string;
|
||||
exploitability_score?: 'high' | 'medium' | 'low' | 'unknown' | string;
|
||||
exploitability_rationale?: string;
|
||||
affected: string[];
|
||||
}
|
||||
|
||||
@@ -376,42 +379,5 @@ export function evaluateSkillSafety(advisories: Advisory[]): {
|
||||
recommendation: 'install' | 'block' | 'review';
|
||||
reason: string;
|
||||
} {
|
||||
if (advisories.length === 0) {
|
||||
return { safe: true, recommendation: 'install', reason: 'No advisories found' };
|
||||
}
|
||||
|
||||
const hasMalicious = advisories.some((a) => a.type === 'malicious');
|
||||
const hasRemoveAction = advisories.some((a) => a.action === 'remove');
|
||||
const hasCritical = advisories.some((a) => a.severity === 'critical');
|
||||
const hasHigh = advisories.some((a) => a.severity === 'high');
|
||||
|
||||
if (hasMalicious || hasRemoveAction) {
|
||||
return {
|
||||
safe: false,
|
||||
recommendation: 'block',
|
||||
reason: 'Malicious skill or removal recommended',
|
||||
};
|
||||
}
|
||||
|
||||
if (hasCritical) {
|
||||
return {
|
||||
safe: false,
|
||||
recommendation: 'block',
|
||||
reason: 'Critical security advisory',
|
||||
};
|
||||
}
|
||||
|
||||
if (hasHigh) {
|
||||
return {
|
||||
safe: false,
|
||||
recommendation: 'review',
|
||||
reason: 'High severity advisory - user review recommended',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
safe: false,
|
||||
recommendation: 'review',
|
||||
reason: 'Advisory found - review before installing',
|
||||
};
|
||||
return evaluateAdvisoryRisk(advisories);
|
||||
}
|
||||
|
||||
@@ -121,6 +121,34 @@ export function versionMatches(version: string, versionSpec: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an affected specifier matches a skill name/version.
|
||||
* Optionally matches against a skill directory name as alias.
|
||||
*/
|
||||
export function matchesAffectedSpecifier(
|
||||
affected: string,
|
||||
skillName: string,
|
||||
skillVersion: string | null,
|
||||
skillDirName?: string
|
||||
): boolean {
|
||||
const parsed = parseAffectedSpecifier(affected);
|
||||
if (!parsed) return false;
|
||||
|
||||
const normalizedTarget = normalizeSkillName(parsed.name);
|
||||
const normalizedSkillName = normalizeSkillName(skillName);
|
||||
const normalizedDirName = skillDirName ? normalizeSkillName(skillDirName) : null;
|
||||
|
||||
if (normalizedTarget !== normalizedSkillName && normalizedTarget !== normalizedDirName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!skillVersion) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return versionMatches(skillVersion, parsed.versionSpec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads advisory feed from a remote URL with signature verification.
|
||||
*/
|
||||
@@ -269,10 +297,12 @@ export async function loadFeed(
|
||||
export function advisoryLooksHighRisk(advisory: Advisory): boolean {
|
||||
const type = advisory.type.toLowerCase();
|
||||
const severity = advisory.severity.toLowerCase();
|
||||
const exploitability = (advisory.exploitability_score || 'unknown').toLowerCase();
|
||||
const combined = `${advisory.title} ${advisory.description} ${advisory.action}`.toLowerCase();
|
||||
|
||||
if (type === 'malicious_skill' || type === 'malicious_plugin') return true;
|
||||
if (type.includes('malicious')) return true;
|
||||
if (severity === 'critical') return true;
|
||||
if (exploitability === 'high') return true;
|
||||
if (/\b(malicious|exfiltrate|exfiltration|backdoor|trojan|stealer|credential theft)\b/.test(combined)) return true;
|
||||
if (/\b(remove|uninstall|disable|do not use|quarantine)\b/.test(combined)) return true;
|
||||
|
||||
@@ -294,15 +324,7 @@ export function findAdvisoryMatches(
|
||||
if (affected.length === 0) continue;
|
||||
|
||||
for (const specifier of affected) {
|
||||
const parsed = parseAffectedSpecifier(specifier);
|
||||
if (!parsed) continue;
|
||||
|
||||
if (normalizeSkillName(parsed.name) !== normalizeSkillName(skillName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If version specified, check if it matches
|
||||
if (version && !versionMatches(version, parsed.versionSpec)) {
|
||||
if (!matchesAffectedSpecifier(specifier, skillName, version)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,8 @@ export interface Advisory {
|
||||
references: string[];
|
||||
cvss_score?: number;
|
||||
nvd_url?: string;
|
||||
exploitability_score?: 'high' | 'medium' | 'low' | 'unknown';
|
||||
exploitability_rationale?: string;
|
||||
source?: string;
|
||||
github_issue_url?: string;
|
||||
reporter?: {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
import { evaluateAdvisoryRisk, normalizeExploitabilityScore } from '../lib/risk.js';
|
||||
import { matchesAffectedSpecifier } from '../lib/advisories.js';
|
||||
|
||||
// These variables are provided by the host environment (ipc-mcp-stdio.ts)
|
||||
// when this code is integrated into the NanoClaw container agent.
|
||||
@@ -18,8 +20,10 @@ declare const server: { tool: (...args: any[]) => void };
|
||||
declare function writeIpcFile(dir: string, data: any): void;
|
||||
declare const TASKS_DIR: string;
|
||||
declare const groupFolder: string;
|
||||
const CACHE_FILE = '/workspace/project/data/clawsec-advisory-cache.json';
|
||||
|
||||
// Add these helper functions to the file:
|
||||
const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||
const exploitabilityOrder: Record<string, number> = { high: 0, medium: 1, low: 2, unknown: 3 };
|
||||
|
||||
/**
|
||||
* Discover installed skills in a directory
|
||||
@@ -84,10 +88,7 @@ function findAdvisoryMatches(
|
||||
const matchedAffected: string[] = [];
|
||||
|
||||
for (const affected of advisory.affected || []) {
|
||||
const atIndex = affected.lastIndexOf('@');
|
||||
const affectedName = atIndex > 0 ? affected.slice(0, atIndex) : affected;
|
||||
|
||||
if (affectedName === skill.name || affectedName === skill.dirName) {
|
||||
if (matchesAffectedSpecifier(affected, skill.name, skill.version, skill.dirName)) {
|
||||
matchedAffected.push(affected);
|
||||
}
|
||||
}
|
||||
@@ -123,10 +124,8 @@ server.tool(
|
||||
}
|
||||
|
||||
// Read cache from shared mount
|
||||
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
|
||||
|
||||
try {
|
||||
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
||||
const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
||||
const installRoot = args.installRoot || path.join(process.env.HOME || '~', '.claude', 'skills');
|
||||
|
||||
// Discover installed skills
|
||||
@@ -153,6 +152,8 @@ server.tool(
|
||||
description: m.advisory.description,
|
||||
action: m.advisory.action,
|
||||
published: m.advisory.published,
|
||||
exploitability_score: normalizeExploitabilityScore(m.advisory.exploitability_score),
|
||||
exploitability_rationale: m.advisory.exploitability_rationale || null,
|
||||
},
|
||||
skill: m.skill,
|
||||
matchedAffected: m.matchedAffected,
|
||||
@@ -187,17 +188,13 @@ server.tool(
|
||||
skillVersion: z.string().optional().describe('Version of skill (optional, for version-specific checks)'),
|
||||
},
|
||||
async (args) => {
|
||||
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
|
||||
|
||||
try {
|
||||
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
||||
const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
||||
|
||||
// Find matching advisories for this skill
|
||||
const matchingAdvisories = cacheData.feed.advisories.filter((advisory: any) =>
|
||||
advisory.affected.some((affected: string) => {
|
||||
const atIndex = affected.lastIndexOf('@');
|
||||
const affectedName = atIndex > 0 ? affected.slice(0, atIndex) : affected;
|
||||
return affectedName === args.skillName;
|
||||
return matchesAffectedSpecifier(affected, args.skillName, args.skillVersion || null);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -215,34 +212,13 @@ server.tool(
|
||||
};
|
||||
}
|
||||
|
||||
// Evaluate severity
|
||||
const hasMalicious = matchingAdvisories.some((a: any) => a.type === 'malicious');
|
||||
const hasRemoveAction = matchingAdvisories.some((a: any) => a.action === 'remove');
|
||||
const hasCritical = matchingAdvisories.some((a: any) => a.severity === 'critical');
|
||||
const hasHigh = matchingAdvisories.some((a: any) => a.severity === 'high');
|
||||
|
||||
let recommendation: 'install' | 'block' | 'review';
|
||||
let reason: string;
|
||||
|
||||
if (hasMalicious || hasRemoveAction) {
|
||||
recommendation = 'block';
|
||||
reason = 'Malicious skill or removal recommended by ClawSec';
|
||||
} else if (hasCritical) {
|
||||
recommendation = 'block';
|
||||
reason = 'Critical security advisory - do not install';
|
||||
} else if (hasHigh) {
|
||||
recommendation = 'review';
|
||||
reason = 'High severity advisory - user review strongly recommended';
|
||||
} else {
|
||||
recommendation = 'review';
|
||||
reason = 'Advisory found - review details before installing';
|
||||
}
|
||||
const risk = evaluateAdvisoryRisk(matchingAdvisories);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
safe: false, // Always false when advisories exist
|
||||
safe: risk.safe,
|
||||
advisories: matchingAdvisories.map((a: any) => ({
|
||||
id: a.id,
|
||||
severity: a.severity,
|
||||
@@ -252,10 +228,13 @@ server.tool(
|
||||
action: a.action,
|
||||
published: a.published,
|
||||
affected: a.affected,
|
||||
exploitability_score: normalizeExploitabilityScore(a.exploitability_score),
|
||||
exploitability_rationale: a.exploitability_rationale || null,
|
||||
})),
|
||||
recommendation,
|
||||
reason,
|
||||
recommendation: risk.recommendation,
|
||||
reason: risk.reason,
|
||||
skillName: args.skillName,
|
||||
skillVersion: args.skillVersion || null,
|
||||
advisoryCount: matchingAdvisories.length,
|
||||
}, null, 2),
|
||||
}],
|
||||
@@ -280,18 +259,18 @@ server.tool(
|
||||
|
||||
server.tool(
|
||||
'clawsec_list_advisories',
|
||||
'List ClawSec advisories with optional filtering. Use this to browse security advisories, filter by severity/type, or search for specific affected skills.',
|
||||
'List ClawSec advisories with optional filtering. Use this to browse security advisories, filter by severity/type/exploitability, or search for specific affected skills.',
|
||||
{
|
||||
severity: z.enum(['critical', 'high', 'medium', 'low']).optional().describe('Filter by severity level'),
|
||||
type: z.enum(['vulnerability', 'malicious', 'deprecated']).optional().describe('Filter by advisory type'),
|
||||
type: z.string().optional().describe('Filter by advisory type (for example: vulnerable_skill, malicious_skill, prompt_injection)'),
|
||||
exploitabilityScore: z.enum(['high', 'medium', 'low', 'unknown']).optional()
|
||||
.describe('Filter by exploitability score'),
|
||||
affectedSkill: z.string().optional().describe('Filter by affected skill name (partial match supported)'),
|
||||
limit: z.number().optional().describe('Maximum number of results (default: unlimited)'),
|
||||
},
|
||||
async (args) => {
|
||||
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
|
||||
|
||||
try {
|
||||
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
||||
const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
||||
let advisories = [...cacheData.feed.advisories];
|
||||
|
||||
// Apply filters
|
||||
@@ -299,7 +278,13 @@ server.tool(
|
||||
advisories = advisories.filter((a: any) => a.severity === args.severity);
|
||||
}
|
||||
if (args.type) {
|
||||
advisories = advisories.filter((a: any) => a.type === args.type);
|
||||
const typeFilter = String(args.type).toLowerCase().trim();
|
||||
advisories = advisories.filter((a: any) => String(a.type || '').toLowerCase().trim() === typeFilter);
|
||||
}
|
||||
if (args.exploitabilityScore) {
|
||||
advisories = advisories.filter(
|
||||
(a: any) => normalizeExploitabilityScore(a.exploitability_score) === args.exploitabilityScore
|
||||
);
|
||||
}
|
||||
if (args.affectedSkill) {
|
||||
advisories = advisories.filter((a: any) =>
|
||||
@@ -307,9 +292,13 @@ server.tool(
|
||||
);
|
||||
}
|
||||
|
||||
// Sort by severity (critical first) and published date (newest first)
|
||||
const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||
// Sort by exploitability first, then severity, then publish date (newest first).
|
||||
advisories.sort((a: any, b: any) => {
|
||||
const exploitabilityDiff =
|
||||
(exploitabilityOrder[normalizeExploitabilityScore(a.exploitability_score)] ?? 999) -
|
||||
(exploitabilityOrder[normalizeExploitabilityScore(b.exploitability_score)] ?? 999);
|
||||
if (exploitabilityDiff !== 0) return exploitabilityDiff;
|
||||
|
||||
const severityDiff = (severityOrder[a.severity] || 999) - (severityOrder[b.severity] || 999);
|
||||
if (severityDiff !== 0) return severityDiff;
|
||||
return (b.published || '').localeCompare(a.published || '');
|
||||
@@ -336,6 +325,8 @@ server.tool(
|
||||
action: a.action,
|
||||
published: a.published,
|
||||
affected: a.affected,
|
||||
exploitability_score: normalizeExploitabilityScore(a.exploitability_score),
|
||||
exploitability_rationale: a.exploitability_rationale || null,
|
||||
})),
|
||||
total: cacheData.feed.advisories.length,
|
||||
filtered: originalCount,
|
||||
@@ -343,6 +334,7 @@ server.tool(
|
||||
filters: {
|
||||
severity: args.severity || null,
|
||||
type: args.type || null,
|
||||
exploitabilityScore: args.exploitabilityScore || null,
|
||||
affectedSkill: args.affectedSkill || null,
|
||||
limit: args.limit || null,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawsec-nanoclaw",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "ClawSec security suite for NanoClaw - Advisory feed monitoring, MCP tools for vulnerability checking, and Ed25519 signature verification for containerized WhatsApp bot agents",
|
||||
"author": "prompt-security",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
@@ -27,6 +27,11 @@
|
||||
"required": true,
|
||||
"description": "NanoClaw skill documentation"
|
||||
},
|
||||
{
|
||||
"path": "CHANGELOG.md",
|
||||
"required": true,
|
||||
"description": "Version history and release notes"
|
||||
},
|
||||
{
|
||||
"path": "INSTALL.md",
|
||||
"required": true,
|
||||
@@ -62,6 +67,11 @@
|
||||
"required": true,
|
||||
"description": "TypeScript type definitions"
|
||||
},
|
||||
{
|
||||
"path": "lib/risk.ts",
|
||||
"required": true,
|
||||
"description": "Shared advisory risk evaluation logic for host and MCP tools"
|
||||
},
|
||||
{
|
||||
"path": "advisories/feed-signing-public.pem",
|
||||
"required": true,
|
||||
@@ -112,9 +122,10 @@
|
||||
"capabilities": [
|
||||
"Advisory feed monitoring from clawsec.prompt.security",
|
||||
"MCP tools for agent-initiated vulnerability scans",
|
||||
"Exploitability-aware advisory prioritization for agent environments",
|
||||
"Pre-installation skill safety checks",
|
||||
"Ed25519 signature verification for advisory feeds",
|
||||
"Platform-specific advisory filtering (nanoclaw vs openclaw)",
|
||||
"Platform metadata preserved in advisory records for downstream filtering",
|
||||
"Containerized agent support with IPC communication"
|
||||
],
|
||||
"nanoclaw": {
|
||||
@@ -135,7 +146,7 @@
|
||||
},
|
||||
"integration": {
|
||||
"mcp_tools_file": "container/agent-runner/src/ipc-mcp-stdio.ts",
|
||||
"ipc_handlers_file": "host/ipc-handler.ts",
|
||||
"ipc_handlers_file": "src/ipc.ts",
|
||||
"cache_location": "/workspace/project/data/clawsec-advisory-cache.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,21 @@ All notable changes to the ClawSec Suite will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.1.4] - 2026-02-28
|
||||
|
||||
### Added
|
||||
|
||||
- Advisory output snippets now include exploitability context in suite quick-check and heartbeat examples.
|
||||
|
||||
### Changed
|
||||
|
||||
- Clarified exploitability guidance to match runtime score values (`high|medium|low|unknown`).
|
||||
- Prioritization guidance now emphasizes high-exploitability advisories for immediate handling.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Kept exploitability enrichment in advisory workflows non-fatal per item so a single analysis failure does not abort feed updates.
|
||||
|
||||
## [0.1.3]
|
||||
|
||||
### Added
|
||||
|
||||
@@ -121,6 +121,7 @@ else
|
||||
while IFS= read -r id; do
|
||||
[ -z "$id" ] && continue
|
||||
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | "- [\(.severity | ascii_upcase)] \(.id): \(.title)"' "$FEED_TMP"
|
||||
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Exploitability: \(.exploitability_score // "unknown" | ascii_upcase)"' "$FEED_TMP"
|
||||
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Action: \(.action // "Review advisory details")"' "$FEED_TMP"
|
||||
done < "$NEW_IDS_FILE"
|
||||
else
|
||||
@@ -194,8 +195,18 @@ fi
|
||||
Heartbeat output should include:
|
||||
- suite version status,
|
||||
- advisory feed status,
|
||||
- new advisory list (if any),
|
||||
- new advisory list (if any) with exploitability scores,
|
||||
- installed skills that appear in advisory `affected` lists,
|
||||
- and a double-confirmation reminder before risky install/remove actions.
|
||||
|
||||
If your runtime sends alerts, treat `critical` and `high` advisories affecting installed skills as immediate notifications.
|
||||
### Exploitability-Based Prioritization
|
||||
|
||||
When alerting on advisories, prioritize by **exploitability score** in addition to severity:
|
||||
|
||||
- `high` exploitability: Trivially or easily exploitable with public tooling, immediate action required
|
||||
- `medium` exploitability: Exploitable with specific conditions, standard priority
|
||||
- `low` exploitability: Difficult to exploit or theoretical, low priority
|
||||
|
||||
**Priority Rule**: A HIGH severity + HIGH exploitability CVE should be treated more urgently than a CRITICAL severity + LOW exploitability CVE.
|
||||
|
||||
If your runtime sends alerts, treat `high` exploitability advisories affecting installed skills as immediate notifications, regardless of severity rating.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: clawsec-suite
|
||||
version: 0.1.3
|
||||
version: 0.1.4
|
||||
description: ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.
|
||||
homepage: https://clawsec.prompt.security
|
||||
clawdis:
|
||||
@@ -234,12 +234,25 @@ if [ -s "$NEW_IDS_FILE" ]; then
|
||||
while IFS= read -r id; do
|
||||
[ -z "$id" ] && continue
|
||||
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | "- [\(.severity | ascii_upcase)] \(.id): \(.title)"' "$TMP/feed.json"
|
||||
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Exploitability: \(.exploitability_score // "unknown" | ascii_upcase)"' "$TMP/feed.json"
|
||||
done < "$NEW_IDS_FILE"
|
||||
else
|
||||
echo "FEED_OK - no new advisories"
|
||||
fi
|
||||
```
|
||||
|
||||
## Exploitability Context
|
||||
|
||||
Advisories in the feed can include `exploitability_score` and `exploitability_rationale` fields to help agents prioritize real-world threats:
|
||||
|
||||
- **Exploitability scores**: `high`, `medium`, `low`, or `unknown`
|
||||
- **Context-aware assessment**: Considers attack vector, authentication requirements, and AI agent deployment patterns
|
||||
- **Exploit availability**: Detects public exploits and weaponization status
|
||||
|
||||
When processing advisories, prioritize by exploitability in addition to severity. A HIGH severity + HIGH exploitability CVE is more urgent than a CRITICAL severity + LOW exploitability CVE.
|
||||
|
||||
For detailed methodology, see the [exploitability scoring documentation](../../wiki/exploitability-scoring.md).
|
||||
|
||||
## Heartbeat Integration
|
||||
|
||||
Use the suite heartbeat script as the single periodic security check entrypoint:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawsec-suite",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"description": "ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.",
|
||||
"author": "prompt-security",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
Executable
+531
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user