mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
Merge branch 'main' into auto-claude/004-llm-based-security-analyst-skill
This commit is contained in:
@@ -193,6 +193,27 @@ 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@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
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 ==="
|
||||
|
||||
scripts/ci/enrich_exploitability.sh \
|
||||
--mode single \
|
||||
--input tmp_advisory.json \
|
||||
--output tmp_advisory.json
|
||||
|
||||
echo "=== Exploitability analysis complete ==="
|
||||
echo "Exploitability score: $(jq -r '.exploitability_score // "unknown"' tmp_advisory.json)"
|
||||
|
||||
- name: Update feed
|
||||
if: steps.parse.outputs.already_exists != 'true'
|
||||
run: |
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
force_full_scan:
|
||||
description: 'Ignore last poll date and scan all CVEs'
|
||||
description: 'Ignore feed state and rebuild CVE advisories from full NVD history'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
@@ -86,6 +86,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p tmp
|
||||
FORCE_FULL_SCAN="${{ inputs.force_full_scan }}"
|
||||
|
||||
START_DATE="${{ steps.dates.outputs.start_date }}"
|
||||
END_DATE="${{ steps.dates.outputs.end_date }}"
|
||||
@@ -102,12 +103,69 @@ jobs:
|
||||
for KEYWORD in $KEYWORDS; do
|
||||
echo "Fetching keyword: $KEYWORD"
|
||||
|
||||
keyword_ok=false
|
||||
last_http_code=""
|
||||
|
||||
if [ "$FORCE_FULL_SCAN" = "true" ]; then
|
||||
echo "Full scan mode enabled: paginating complete NVD history for keyword '$KEYWORD'"
|
||||
echo '{"vulnerabilities":[]}' > "tmp/nvd_${KEYWORD}.json"
|
||||
START_INDEX=0
|
||||
RESULTS_PER_PAGE=2000
|
||||
|
||||
while true; do
|
||||
URL="https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=${KEYWORD}&startIndex=${START_INDEX}&resultsPerPage=${RESULTS_PER_PAGE}"
|
||||
PAGE_FILE="tmp/nvd_${KEYWORD}_${START_INDEX}.json"
|
||||
echo "URL: $URL"
|
||||
|
||||
page_ok=false
|
||||
for i in 1 2 3; do
|
||||
HTTP_CODE=$(curl -sS -w "%{http_code}" -o "$PAGE_FILE" "$URL" || true)
|
||||
if [ -z "$HTTP_CODE" ]; then
|
||||
HTTP_CODE="000"
|
||||
fi
|
||||
last_http_code="$HTTP_CODE"
|
||||
if [ "$HTTP_CODE" = "200" ]; then
|
||||
if jq -e . "$PAGE_FILE" >/dev/null 2>&1; then
|
||||
page_ok=true
|
||||
break
|
||||
fi
|
||||
echo "Invalid JSON for $KEYWORD page $START_INDEX, retry $i..."
|
||||
sleep 5
|
||||
elif [ "$HTTP_CODE" = "403" ] || [ "$HTTP_CODE" = "429" ]; then
|
||||
echo "Rate limited, waiting 30s before retry $i..."
|
||||
sleep 30
|
||||
else
|
||||
echo "HTTP $HTTP_CODE for $KEYWORD page $START_INDEX, retry $i..."
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$page_ok" != "true" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
jq -s '.[0].vulnerabilities += .[1].vulnerabilities | .[0]' \
|
||||
"tmp/nvd_${KEYWORD}.json" "$PAGE_FILE" > "tmp/nvd_${KEYWORD}_merged.json"
|
||||
mv "tmp/nvd_${KEYWORD}_merged.json" "tmp/nvd_${KEYWORD}.json"
|
||||
|
||||
PAGE_COUNT=$(jq '.vulnerabilities | length' "$PAGE_FILE")
|
||||
TOTAL_RESULTS=$(jq '.totalResults // 0' "$PAGE_FILE")
|
||||
echo "Fetched $PAGE_COUNT results at startIndex=$START_INDEX (totalResults=$TOTAL_RESULTS)"
|
||||
|
||||
START_INDEX=$((START_INDEX + RESULTS_PER_PAGE))
|
||||
if [ "$START_INDEX" -ge "$TOTAL_RESULTS" ] || [ "$PAGE_COUNT" -eq 0 ]; then
|
||||
keyword_ok=true
|
||||
break
|
||||
fi
|
||||
|
||||
# NVD recommends 6 second delay between requests
|
||||
sleep 6
|
||||
done
|
||||
else
|
||||
URL="https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=${KEYWORD}&lastModStartDate=${START_ENC}&lastModEndDate=${END_ENC}"
|
||||
echo "URL: $URL"
|
||||
|
||||
# Fetch with retry logic
|
||||
keyword_ok=false
|
||||
last_http_code=""
|
||||
for i in 1 2 3; do
|
||||
HTTP_CODE=$(curl -sS -w "%{http_code}" -o "tmp/nvd_${KEYWORD}.json" "$URL" || true)
|
||||
if [ -z "$HTTP_CODE" ]; then
|
||||
@@ -130,6 +188,7 @@ jobs:
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$keyword_ok" != "true" ]; then
|
||||
echo "::error::Failed to fetch valid NVD response for keyword '$KEYWORD' (last HTTP code: ${last_http_code:-unknown})."
|
||||
@@ -212,6 +271,14 @@ jobs:
|
||||
- name: Check for updates to existing advisories
|
||||
id: updates
|
||||
run: |
|
||||
if [ "${{ inputs.force_full_scan }}" = "true" ]; then
|
||||
echo "Full scan mode enabled: skipping delta update detection."
|
||||
echo '[]' > tmp/updated_advisories.json
|
||||
echo "Advisories to update: 0"
|
||||
echo "update_count=0" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Compare existing CVE advisories against NVD data for changes
|
||||
# Only check advisories that start with "CVE-" (NVD-sourced)
|
||||
|
||||
@@ -353,7 +420,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
|
||||
|
||||
@@ -422,7 +491,12 @@ jobs:
|
||||
id: transform
|
||||
run: |
|
||||
# Read existing IDs into a jq-friendly format
|
||||
if [ "${{ inputs.force_full_scan }}" = "true" ]; then
|
||||
echo "Full scan mode enabled: rebuilding CVE advisories from scratch."
|
||||
EXISTING_IDS='[]'
|
||||
else
|
||||
EXISTING_IDS=$(cat tmp/existing_ids.txt | jq -R -s 'split("\n") | map(select(length > 0))')
|
||||
fi
|
||||
|
||||
# Transform NVD CVEs to our advisory format
|
||||
jq --argjson existing "$EXISTING_IDS" '
|
||||
@@ -568,7 +642,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,12 +658,63 @@ 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@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
|
||||
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
|
||||
|
||||
scripts/ci/enrich_exploitability.sh \
|
||||
--mode batch \
|
||||
--input tmp/new_advisories.json \
|
||||
--output tmp/new_advisories.json \
|
||||
--cvss-vectors tmp/cvss_vectors.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: |
|
||||
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
FORCE_FULL_SCAN="${{ inputs.force_full_scan }}"
|
||||
|
||||
if [ -f "$FEED_PATH" ]; then
|
||||
if [ -f "$FEED_PATH" ] && [ "$FORCE_FULL_SCAN" = "true" ]; then
|
||||
# Full scan mode: replace all CVE advisories with rebuilt set and keep non-CVE entries.
|
||||
jq --argjson rebuilt "$(cat tmp/new_advisories.json)" --arg now "$NOW" '
|
||||
.updated = $now |
|
||||
.advisories = (
|
||||
((.advisories // []) | map(select((.id // "") | startswith("CVE-") | not)))
|
||||
+ $rebuilt
|
||||
| sort_by(.published)
|
||||
| reverse
|
||||
)
|
||||
' "$FEED_PATH" > tmp/updated_feed.json
|
||||
elif [ -f "$FEED_PATH" ]; then
|
||||
# Step 1: Apply updates to existing advisories
|
||||
jq --slurpfile updates tmp/updated_advisories.json '
|
||||
.advisories = [
|
||||
@@ -671,6 +798,7 @@ jobs:
|
||||
## Summary
|
||||
Automated update from NVD CVE feed.
|
||||
|
||||
- **Mode:** ${{ inputs.force_full_scan == true && 'full-rebuild (ignore feed state)' || 'delta (incremental)' }}
|
||||
- **New advisories:** ${{ steps.transform.outputs.new_count }}
|
||||
- **Updated advisories:** ${{ steps.updates.outputs.update_count }}
|
||||
- **Poll window:** ${{ steps.dates.outputs.start_date }} → ${{ steps.dates.outputs.end_date }}
|
||||
@@ -730,10 +858,17 @@ jobs:
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
if [ "${{ inputs.force_full_scan }}" = "true" ]; then
|
||||
MODE="full-rebuild (ignore feed state)"
|
||||
else
|
||||
MODE="delta (incremental)"
|
||||
fi
|
||||
|
||||
echo "## NVD CVE Poll Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Mode | $MODE |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Poll Window | ${{ steps.dates.outputs.start_date }} → ${{ steps.dates.outputs.end_date }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Keywords | $KEYWORDS |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| CVEs Found (filtered) | ${{ steps.process.outputs.filtered_count }} |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -540,7 +540,7 @@ jobs:
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
outputs:
|
||||
skill_name: ${{ steps.parse.outputs.skill_name }}
|
||||
version: ${{ steps.parse.outputs.version }}
|
||||
@@ -877,15 +877,6 @@ jobs:
|
||||
} >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Require automation token for release publishing
|
||||
env:
|
||||
AUTOMATION_TOKEN: ${{ secrets.POLL_NVD_CVES_PAT }}
|
||||
run: |
|
||||
if [ -z "$AUTOMATION_TOKEN" ]; then
|
||||
echo "::error::Set POLL_NVD_CVES_PAT with repo write permissions."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
@@ -945,7 +936,7 @@ jobs:
|
||||
draft: false
|
||||
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.POLL_NVD_CVES_PAT }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Delete superseded releases
|
||||
run: |
|
||||
@@ -982,7 +973,7 @@ jobs:
|
||||
|
||||
echo "Superseded release cleanup complete"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.POLL_NVD_CVES_PAT }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish-clawhub:
|
||||
# Separate job for ClawHub publishing - runs after GitHub release
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
+547
-103
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
1W60LzEFSeZouvJEmO2lBGD3yVWtUVzed9yTQrReZoDLr6qYie7PY7O7Gy02SEI5bHKKdHbUDfYQzECiPVeBBw==
|
||||
SJ1weYVVi723M8f6s8es6rg34CSPKxbvlBy1QIXdS0giskd5KTADTDLr2STqUCuWpaV7U+JQa/1eWqNX2oJ+Aw==
|
||||
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"
|
||||
Executable
+263
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/ci/enrich_exploitability.sh --mode single|batch --input <path> --output <path> [--cvss-vectors <path>] [--analyzer <path>]
|
||||
|
||||
Options:
|
||||
--mode Processing mode: single advisory object or batch advisory array
|
||||
--input Input JSON path
|
||||
--output Output JSON path
|
||||
--cvss-vectors Optional JSON object mapping advisory id -> CVSS vector
|
||||
--analyzer Optional analyzer path (default: utils/analyze_exploitability.py)
|
||||
--help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
MODE=""
|
||||
INPUT_PATH=""
|
||||
OUTPUT_PATH=""
|
||||
CVSS_VECTORS_PATH=""
|
||||
ANALYZER_PATH="utils/analyze_exploitability.py"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--mode)
|
||||
MODE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--input)
|
||||
INPUT_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
OUTPUT_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--cvss-vectors)
|
||||
CVSS_VECTORS_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--analyzer)
|
||||
ANALYZER_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$MODE" != "single" && "$MODE" != "batch" ]]; then
|
||||
echo "ERROR: --mode must be one of: single, batch" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$INPUT_PATH" || -z "$OUTPUT_PATH" ]]; then
|
||||
echo "ERROR: --input and --output are required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$INPUT_PATH" ]]; then
|
||||
echo "ERROR: input file not found: $INPUT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$ANALYZER_PATH" ]]; then
|
||||
echo "ERROR: analyzer file not found: $ANALYZER_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$CVSS_VECTORS_PATH" && ! -f "$CVSS_VECTORS_PATH" ]]; then
|
||||
echo "ERROR: --cvss-vectors file not found: $CVSS_VECTORS_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "ERROR: jq is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v python >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python"
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
PYTHON_BIN="python3"
|
||||
else
|
||||
echo "ERROR: python or python3 is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
resolve_cvss_vector() {
|
||||
local advisory_json="$1"
|
||||
local advisory_id
|
||||
advisory_id="$(echo "$advisory_json" | jq -r '.id // ""')"
|
||||
|
||||
if [[ -n "$CVSS_VECTORS_PATH" ]]; then
|
||||
jq -r --arg id "$advisory_id" '.[$id] // ""' "$CVSS_VECTORS_PATH"
|
||||
else
|
||||
echo "$advisory_json" | jq -r '.cvss_vector // ""'
|
||||
fi
|
||||
}
|
||||
|
||||
severity_to_cvss() {
|
||||
case "$1" in
|
||||
critical) echo "9.5" ;;
|
||||
high) echo "7.5" ;;
|
||||
medium) echo "5.5" ;;
|
||||
low) echo "3.0" ;;
|
||||
*) echo "5.0" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
build_analysis_input() {
|
||||
local advisory_json="$1"
|
||||
local mode="$2"
|
||||
local cve_id cvss_score cvss_vector vuln_type description references severity
|
||||
|
||||
cve_id="$(echo "$advisory_json" | jq -r '.id // ""')"
|
||||
vuln_type="$(echo "$advisory_json" | jq -r '.type // ""')"
|
||||
description="$(echo "$advisory_json" | jq -r '.description // ""')"
|
||||
references="$(echo "$advisory_json" | jq -c '.references // []')"
|
||||
cvss_vector="$(resolve_cvss_vector "$advisory_json")"
|
||||
|
||||
if [[ "$mode" == "single" ]]; then
|
||||
severity="$(echo "$advisory_json" | jq -r '.severity // "medium"')"
|
||||
cvss_score="$(severity_to_cvss "$severity")"
|
||||
else
|
||||
cvss_score="$(echo "$advisory_json" | jq -r '.cvss_score // 0')"
|
||||
fi
|
||||
|
||||
jq -n \
|
||||
--arg cve_id "$cve_id" \
|
||||
--argjson cvss_score "$cvss_score" \
|
||||
--arg cvss_vector "$cvss_vector" \
|
||||
--arg type "$vuln_type" \
|
||||
--arg description "$description" \
|
||||
--argjson references "$references" \
|
||||
'{
|
||||
cve_id: $cve_id,
|
||||
cvss_score: $cvss_score,
|
||||
cvss_vector: $cvss_vector,
|
||||
type: $type,
|
||||
description: $description,
|
||||
references: $references
|
||||
}'
|
||||
}
|
||||
|
||||
run_analysis() {
|
||||
local advisory_json="$1"
|
||||
local mode="$2"
|
||||
local output_file="$3"
|
||||
local advisory_id analysis_input analysis
|
||||
|
||||
advisory_id="$(echo "$advisory_json" | jq -r '.id // "unknown"')"
|
||||
analysis_input="$(build_analysis_input "$advisory_json" "$mode")"
|
||||
|
||||
if analysis="$(echo "$analysis_input" | "$PYTHON_BIN" "$ANALYZER_PATH" --json --check-exploits 2>/dev/null)"; then
|
||||
echo "$analysis" > "$output_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "::warning::Failed to analyze exploitability for $advisory_id, continuing without enrichment"
|
||||
return 1
|
||||
}
|
||||
|
||||
enrich_single() {
|
||||
if ! jq -e 'type == "object"' "$INPUT_PATH" >/dev/null; then
|
||||
echo "ERROR: single mode expects JSON object at $INPUT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local advisory analysis_file output_tmp
|
||||
advisory="$(cat "$INPUT_PATH")"
|
||||
analysis_file="$tmpdir/analysis_single.json"
|
||||
output_tmp="$tmpdir/output_single.json"
|
||||
|
||||
if run_analysis "$advisory" "single" "$analysis_file"; then
|
||||
jq --slurpfile analysis "$analysis_file" '
|
||||
. + {
|
||||
exploitability_score: $analysis[0].exploitability_score,
|
||||
exploitability_rationale: $analysis[0].exploitability_rationale,
|
||||
attack_vector_analysis: $analysis[0].attack_vector_analysis,
|
||||
exploit_detection: $analysis[0].exploit_detection
|
||||
}
|
||||
' "$INPUT_PATH" > "$output_tmp"
|
||||
else
|
||||
cp "$INPUT_PATH" "$output_tmp"
|
||||
fi
|
||||
|
||||
mv "$output_tmp" "$OUTPUT_PATH"
|
||||
echo "Exploitability enrichment complete (single): $OUTPUT_PATH"
|
||||
}
|
||||
|
||||
enrich_batch() {
|
||||
if ! jq -e 'type == "array"' "$INPUT_PATH" >/dev/null; then
|
||||
echo "ERROR: batch mode expects JSON array at $INPUT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local analyzed_count failed_count index advisory analysis_file output_tmp analyses_json
|
||||
analyzed_count=0
|
||||
failed_count=0
|
||||
index=0
|
||||
analyses_json="$tmpdir/analyses.json"
|
||||
output_tmp="$tmpdir/output_batch.json"
|
||||
|
||||
while IFS= read -r advisory; do
|
||||
analysis_file="$tmpdir/analysis_${index}.json"
|
||||
if run_analysis "$advisory" "batch" "$analysis_file"; then
|
||||
analyzed_count=$((analyzed_count + 1))
|
||||
else
|
||||
failed_count=$((failed_count + 1))
|
||||
rm -f "$analysis_file"
|
||||
fi
|
||||
index=$((index + 1))
|
||||
done < <(jq -c '.[]' "$INPUT_PATH")
|
||||
|
||||
if ls "$tmpdir"/analysis_*.json >/dev/null 2>&1; then
|
||||
jq -s '.' "$tmpdir"/analysis_*.json > "$analyses_json"
|
||||
else
|
||||
echo '[]' > "$analyses_json"
|
||||
fi
|
||||
|
||||
jq --slurpfile analyses "$analyses_json" '
|
||||
map(
|
||||
. as $advisory |
|
||||
($analyses[0] | map(select(.cve_id == $advisory.id)) | first) as $analysis |
|
||||
if $analysis then
|
||||
$advisory + {
|
||||
exploitability_score: $analysis.exploitability_score,
|
||||
exploitability_rationale: $analysis.exploitability_rationale,
|
||||
attack_vector_analysis: $analysis.attack_vector_analysis,
|
||||
exploit_detection: $analysis.exploit_detection
|
||||
}
|
||||
else
|
||||
$advisory
|
||||
end
|
||||
)
|
||||
' "$INPUT_PATH" > "$output_tmp"
|
||||
|
||||
mv "$output_tmp" "$OUTPUT_PATH"
|
||||
echo "Exploitability enrichment complete (batch): $OUTPUT_PATH"
|
||||
echo "Analyzed: $analyzed_count, failed: $failed_count"
|
||||
}
|
||||
|
||||
if [[ "$MODE" == "single" ]]; then
|
||||
enrich_single
|
||||
else
|
||||
enrich_batch
|
||||
fi
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
ENRICH_SCRIPT="$PROJECT_ROOT/scripts/ci/enrich_exploitability.sh"
|
||||
|
||||
# Parse args
|
||||
DAYS_BACK=120
|
||||
@@ -46,6 +47,12 @@ echo "Days back: $DAYS_BACK"
|
||||
echo "Force mode: $FORCE"
|
||||
echo ""
|
||||
|
||||
# Verify enrichment helper exists (it validates Python/analyzer prerequisites internally).
|
||||
if [ ! -x "$ENRICH_SCRIPT" ]; then
|
||||
echo "Error: Exploitability enrichment helper not found or not executable: $ENRICH_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create temp directory
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||
@@ -62,7 +69,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 +81,8 @@ echo "End date: $END_DATE"
|
||||
echo ""
|
||||
|
||||
# URL encode dates
|
||||
START_ENC=$(echo "$START_DATE" | sed 's/:/%3A/g')
|
||||
END_ENC=$(echo "$END_DATE" | sed 's/:/%3A/g')
|
||||
START_ENC=${START_DATE//:/%3A}
|
||||
END_ENC=${END_DATE//:/%3A}
|
||||
|
||||
echo "=== Fetching CVEs from NVD ==="
|
||||
|
||||
@@ -281,7 +288,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 +305,28 @@ 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"
|
||||
|
||||
"$ENRICH_SCRIPT" \
|
||||
--mode batch \
|
||||
--input "$TEMP_DIR/new_advisories.json" \
|
||||
--output "$TEMP_DIR/new_advisories.json" \
|
||||
--cvss-vectors "$TEMP_DIR/cvss_vectors.json"
|
||||
|
||||
echo ""
|
||||
echo "=== New Advisories ==="
|
||||
jq -r '.[] | " \(.id) [\(.severity)] - \(.title)"' "$TEMP_DIR/new_advisories.json"
|
||||
@@ -339,15 +370,8 @@ if jq empty "$TEMP_DIR/updated_feed.json" 2>/dev/null; then
|
||||
cp "$TEMP_DIR/updated_feed.json" "$FEED_PATH"
|
||||
echo "✓ Updated: $FEED_PATH"
|
||||
|
||||
# Update skill feed
|
||||
mkdir -p "$(dirname "$SKILL_FEED_PATH")"
|
||||
cp "$FEED_PATH" "$SKILL_FEED_PATH"
|
||||
echo "✓ Updated: $SKILL_FEED_PATH"
|
||||
|
||||
# Update public feed for local dev
|
||||
mkdir -p "$(dirname "$PUBLIC_FEED_PATH")"
|
||||
cp "$FEED_PATH" "$PUBLIC_FEED_PATH"
|
||||
echo "✓ Updated: $PUBLIC_FEED_PATH"
|
||||
# Sync feed mirrors for local skill/public consumers.
|
||||
sync_feed_to_mirrors "$FEED_PATH" "create"
|
||||
|
||||
echo ""
|
||||
TOTAL_ADVISORIES=$(jq '.advisories | length' "$FEED_PATH")
|
||||
|
||||
@@ -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:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
1W60LzEFSeZouvJEmO2lBGD3yVWtUVzed9yTQrReZoDLr6qYie7PY7O7Gy02SEI5bHKKdHbUDfYQzECiPVeBBw==
|
||||
SJ1weYVVi723M8f6s8es6rg34CSPKxbvlBy1QIXdS0giskd5KTADTDLr2STqUCuWpaV7U+JQa/1eWqNX2oJ+Aw==
|
||||
@@ -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