diff --git a/.github/workflows/poll-nvd-cves.yml b/.github/workflows/poll-nvd-cves.yml index b80bad9..ea8a0a3 100644 --- a/.github/workflows/poll-nvd-cves.yml +++ b/.github/workflows/poll-nvd-cves.yml @@ -23,8 +23,6 @@ env: FEED_SIG_PATH: advisories/feed.json.sig SKILL_FEED_PATH: skills/clawsec-feed/advisories/feed.json SKILL_FEED_SIG_PATH: skills/clawsec-feed/advisories/feed.json.sig - KEYWORDS: "OpenClaw clawdbot Moltbot NanoClaw WhatsApp-bot baileys" - GITHUB_REF_PATTERN: "github.com/openclaw/openclaw github.com/qwibitai/NanoClaw" jobs: poll-and-update: @@ -85,8 +83,10 @@ jobs: id: fetch run: | set -euo pipefail + source scripts/feed-utils.sh mkdir -p tmp FORCE_FULL_SCAN="${{ inputs.force_full_scan }}" + NVD_QUERY_SPECS="$(nvd_query_specs)" START_DATE="${{ steps.dates.outputs.start_date }}" END_DATE="${{ steps.dates.outputs.end_date }}" @@ -97,24 +97,26 @@ jobs: echo "=== Fetching CVEs from NVD ===" - FAILED_KEYWORDS=() + FAILED_QUERIES=() - # Fetch for each keyword - for KEYWORD in $KEYWORDS; do - echo "Fetching keyword: $KEYWORD" + while IFS='|' read -r QUERY_KIND QUERY_VALUE; do + [ -n "$QUERY_KIND" ] || continue + + QUERY_SLUG="$(nvd_query_slug "$QUERY_KIND" "$QUERY_VALUE")" + echo "Fetching $QUERY_KIND query: $QUERY_VALUE" 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" + echo "Full scan mode enabled: paginating complete NVD history for '$QUERY_KIND:$QUERY_VALUE'" + echo '{"vulnerabilities":[]}' > "tmp/nvd_${QUERY_SLUG}.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" + URL="$(nvd_build_url "$QUERY_KIND" "$QUERY_VALUE" "&startIndex=${START_INDEX}&resultsPerPage=${RESULTS_PER_PAGE}")" + PAGE_FILE="tmp/nvd_${QUERY_SLUG}_${START_INDEX}.json" echo "URL: $URL" page_ok=false @@ -129,13 +131,13 @@ jobs: page_ok=true break fi - echo "Invalid JSON for $KEYWORD page $START_INDEX, retry $i..." + echo "Invalid JSON for $QUERY_KIND:$QUERY_VALUE 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..." + echo "HTTP $HTTP_CODE for $QUERY_KIND:$QUERY_VALUE page $START_INDEX, retry $i..." sleep 5 fi done @@ -145,8 +147,8 @@ jobs: 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" + "tmp/nvd_${QUERY_SLUG}.json" "$PAGE_FILE" > "tmp/nvd_${QUERY_SLUG}_merged.json" + mv "tmp/nvd_${QUERY_SLUG}_merged.json" "tmp/nvd_${QUERY_SLUG}.json" PAGE_COUNT=$(jq '.vulnerabilities | length' "$PAGE_FILE") TOTAL_RESULTS=$(jq '.totalResults // 0' "$PAGE_FILE") @@ -162,45 +164,45 @@ jobs: sleep 6 done else - URL="https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=${KEYWORD}&lastModStartDate=${START_ENC}&lastModEndDate=${END_ENC}" + URL="$(nvd_build_url "$QUERY_KIND" "$QUERY_VALUE" "&lastModStartDate=${START_ENC}&lastModEndDate=${END_ENC}")" echo "URL: $URL" # Fetch with retry logic for i in 1 2 3; do - HTTP_CODE=$(curl -sS -w "%{http_code}" -o "tmp/nvd_${KEYWORD}.json" "$URL" || true) + HTTP_CODE=$(curl -sS -w "%{http_code}" -o "tmp/nvd_${QUERY_SLUG}.json" "$URL" || true) if [ -z "$HTTP_CODE" ]; then HTTP_CODE="000" fi last_http_code="$HTTP_CODE" if [ "$HTTP_CODE" = "200" ]; then - if jq -e . "tmp/nvd_${KEYWORD}.json" >/dev/null 2>&1; then - echo "Success for $KEYWORD" + if jq -e . "tmp/nvd_${QUERY_SLUG}.json" >/dev/null 2>&1; then + echo "Success for $QUERY_KIND:$QUERY_VALUE" keyword_ok=true break fi - echo "Invalid JSON for $KEYWORD, retry $i..." + echo "Invalid JSON for $QUERY_KIND:$QUERY_VALUE, 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, retry $i..." + echo "HTTP $HTTP_CODE for $QUERY_KIND:$QUERY_VALUE, retry $i..." 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})." - FAILED_KEYWORDS+=("$KEYWORD") + echo "::error::Failed to fetch valid NVD response for '$QUERY_KIND:$QUERY_VALUE' (last HTTP code: ${last_http_code:-unknown})." + FAILED_QUERIES+=("${QUERY_KIND}:${QUERY_VALUE}") fi # NVD recommends 6 second delay between requests sleep 6 - done + done <<< "$NVD_QUERY_SPECS" - if [ "${#FAILED_KEYWORDS[@]}" -gt 0 ]; then - echo "::error::NVD fetch failed for keyword(s): ${FAILED_KEYWORDS[*]}" + if [ "${#FAILED_QUERIES[@]}" -gt 0 ]; then + echo "::error::NVD fetch failed for query spec(s): ${FAILED_QUERIES[*]}" exit 1 fi @@ -210,11 +212,19 @@ jobs: - name: Merge and filter CVEs id: process run: | + source scripts/feed-utils.sh + NVD_QUERY_SPECS="$(nvd_query_specs)" + KEYWORDS_PATTERN="$(nvd_keyword_pattern)" + GITHUB_PATTERN="$(nvd_github_ref_pattern)" + CPE_PATTERN="$(nvd_cpe_pattern)" + # Combine all fetched CVEs echo '{"vulnerabilities":[]}' > tmp/combined.json - for KEYWORD in $KEYWORDS; do - FILE="tmp/nvd_${KEYWORD}.json" + while IFS='|' read -r QUERY_KIND QUERY_VALUE; do + [ -n "$QUERY_KIND" ] || continue + QUERY_SLUG="$(nvd_query_slug "$QUERY_KIND" "$QUERY_VALUE")" + FILE="tmp/nvd_${QUERY_SLUG}.json" if [ -f "$FILE" ] && [ -s "$FILE" ]; then # Check if file has vulnerabilities array if jq -e '.vulnerabilities' "$FILE" > /dev/null 2>&1; then @@ -227,7 +237,7 @@ jobs: mv tmp/combined_new.json tmp/combined.json fi fi - done + done <<< "$NVD_QUERY_SPECS" # Deduplicate by CVE ID jq '.vulnerabilities | unique_by(.cve.id)' tmp/combined.json > tmp/unique_cves.json @@ -235,16 +245,16 @@ jobs: echo "Total unique CVEs from NVD: $TOTAL" # Post-filter: keep only CVEs where description contains keywords OR references contain github pattern - KEYWORDS_PATTERN="OpenClaw|clawdbot|Moltbot|openclaw|NanoClaw|nanoclaw|WhatsApp-bot|baileys" - GITHUB_PATTERN="${GITHUB_REF_PATTERN}" - - jq --arg kw "$KEYWORDS_PATTERN" --arg gh "$GITHUB_PATTERN" ' + jq --arg kw "$KEYWORDS_PATTERN" --arg gh "$GITHUB_PATTERN" --arg cpe "$CPE_PATTERN" ' [.[] | select( # Check if any description contains keywords (case insensitive) (.cve.descriptions[]? | select(.lang == "en") | .value | test($kw; "i")) or # Check if any reference URL contains the github pattern (.cve.references[]? | .url | test($gh; "i")) + or + # Check if any CPE criteria contain the Hermes product identifier + ([.cve.configurations[]? | .. | objects | .criteria? | strings | test($cpe; "i")] | any) )] ' tmp/unique_cves.json > tmp/filtered_cves.json @@ -371,11 +381,12 @@ jobs: | unique ); - def context_blob: + def detection_blob: ( [ (.cve.descriptions[]? | select(.lang == "en") | .value), - (.cve.references[]?.url // empty) + (.cve.references[]?.url // empty), + (.cve.configurations[]? | .. | objects | .criteria? // empty) ] | map(strings | ascii_downcase) | join(" ") @@ -383,30 +394,45 @@ jobs: def inferred_targets: ( - context_blob as $blob + detection_blob as $blob | ( (if ($blob | test("github\\.com/openclaw/openclaw|\\bopenclaw\\b|\\bclawdbot\\b|\\bmoltbot\\b")) then ["openclaw@*"] else [] end) + (if ($blob | test("github\\.com/qwibitai/nanoclaw|\\bnanoclaw\\b|whatsapp-bot|\\bbaileys\\b")) then ["nanoclaw@*"] else [] end) + + (if ($blob | test("github\\.com/softwarepub/hermes|cpe:2\\.3:a:software-metadata\\.pub:hermes|\\bhermes workflow\\b|software publication with rich metadata")) then ["hermes@*"] else [] end) ) ); - def normalized_affected: + def matched_targets: ( (cpe_criteria + inferred_targets) | unique | .[0:5] - | if length == 0 then ["openclaw@*", "nanoclaw@*"] else . end + ); + + def platforms_from_targets($targets): + ( + [ + (if ($targets | map(strings | ascii_downcase | select(startswith("openclaw@") or test("^cpe:2\\.3:[aho]:openclaw:openclaw(?::|$)"))) | length > 0) then "openclaw" else empty end), + (if ($targets | map(strings | ascii_downcase | select(startswith("nanoclaw@") or test("^cpe:2\\.3:[aho]:[^:]*:nanoclaw(?::|$)"))) | length > 0) then "nanoclaw" else empty end), + (if ($targets | map(strings | ascii_downcase | select(startswith("hermes@") or test("^cpe:2\\.3:[aho]:software-metadata\\.pub:hermes(?::|$)"))) | length > 0) then "hermes" else empty end) + ] + ); + + def normalized_affected: + ( + matched_targets + | if length == 0 then ["openclaw@*", "nanoclaw@*", "hermes@*"] else . end ); def normalized_platforms: ( - inferred_targets as $targets - | ($targets | map(select(startswith("openclaw@"))) | length > 0) as $has_openclaw - | ($targets | map(select(startswith("nanoclaw@"))) | length > 0) as $has_nanoclaw - | if $has_openclaw and $has_nanoclaw then ["openclaw", "nanoclaw"] - elif $has_openclaw then ["openclaw"] - elif $has_nanoclaw then ["nanoclaw"] - else ["openclaw", "nanoclaw"] + inferred_targets as $inferred + | platforms_from_targets($inferred) as $from_inferred + | if ($from_inferred | length) > 0 then $from_inferred + else + matched_targets as $targets + | platforms_from_targets($targets) as $from_targets + | if ($from_targets | length) > 0 then $from_targets else ["openclaw", "nanoclaw", "hermes"] end end ); @@ -588,11 +614,12 @@ jobs: | unique ); - def context_blob: + def detection_blob: ( [ (.cve.descriptions[]? | select(.lang == "en") | .value), - (.cve.references[]?.url // empty) + (.cve.references[]?.url // empty), + (.cve.configurations[]? | .. | objects | .criteria? // empty) ] | map(strings | ascii_downcase) | join(" ") @@ -600,30 +627,45 @@ jobs: def inferred_targets: ( - context_blob as $blob + detection_blob as $blob | ( (if ($blob | test("github\\.com/openclaw/openclaw|\\bopenclaw\\b|\\bclawdbot\\b|\\bmoltbot\\b")) then ["openclaw@*"] else [] end) + (if ($blob | test("github\\.com/qwibitai/nanoclaw|\\bnanoclaw\\b|whatsapp-bot|\\bbaileys\\b")) then ["nanoclaw@*"] else [] end) + + (if ($blob | test("github\\.com/softwarepub/hermes|cpe:2\\.3:a:software-metadata\\.pub:hermes|\\bhermes workflow\\b|software publication with rich metadata")) then ["hermes@*"] else [] end) ) ); - def normalized_affected: + def matched_targets: ( (cpe_criteria + inferred_targets) | unique | .[0:5] - | if length == 0 then ["openclaw@*", "nanoclaw@*"] else . end + ); + + def platforms_from_targets($targets): + ( + [ + (if ($targets | map(strings | ascii_downcase | select(startswith("openclaw@") or test("^cpe:2\\.3:[aho]:openclaw:openclaw(?::|$)"))) | length > 0) then "openclaw" else empty end), + (if ($targets | map(strings | ascii_downcase | select(startswith("nanoclaw@") or test("^cpe:2\\.3:[aho]:[^:]*:nanoclaw(?::|$)"))) | length > 0) then "nanoclaw" else empty end), + (if ($targets | map(strings | ascii_downcase | select(startswith("hermes@") or test("^cpe:2\\.3:[aho]:software-metadata\\.pub:hermes(?::|$)"))) | length > 0) then "hermes" else empty end) + ] + ); + + def normalized_affected: + ( + matched_targets + | if length == 0 then ["openclaw@*", "nanoclaw@*", "hermes@*"] else . end ); def normalized_platforms: ( - inferred_targets as $targets - | ($targets | map(select(startswith("openclaw@"))) | length > 0) as $has_openclaw - | ($targets | map(select(startswith("nanoclaw@"))) | length > 0) as $has_nanoclaw - | if $has_openclaw and $has_nanoclaw then ["openclaw", "nanoclaw"] - elif $has_openclaw then ["openclaw"] - elif $has_nanoclaw then ["nanoclaw"] - else ["openclaw", "nanoclaw"] + inferred_targets as $inferred + | platforms_from_targets($inferred) as $from_inferred + | if ($from_inferred | length) > 0 then $from_inferred + else + matched_targets as $targets + | platforms_from_targets($targets) as $from_targets + | if ($from_targets | length) > 0 then $from_targets else ["openclaw", "nanoclaw", "hermes"] end end ); diff --git a/scripts/feed-utils.sh b/scripts/feed-utils.sh index 5f7b29f..7df2c98 100644 --- a/scripts/feed-utils.sh +++ b/scripts/feed-utils.sh @@ -35,3 +35,56 @@ sync_feed_to_mirrors() { esac done } + +nvd_query_specs() { + cat <<'EOF' +keyword|OpenClaw +keyword|clawdbot +keyword|Moltbot +keyword|NanoClaw +keyword|WhatsApp-bot +keyword|baileys +keyword|hermes workflow +virtualMatchString|cpe:2.3:a:software-metadata.pub:hermes +EOF +} + +nvd_keyword_pattern() { + echo 'OpenClaw|clawdbot|Moltbot|openclaw|NanoClaw|nanoclaw|WhatsApp-bot|baileys|HERMES workflow|software publication with rich metadata' +} + +nvd_github_ref_pattern() { + echo 'github\.com/openclaw/openclaw|github\.com/qwibitai/nanoclaw|github\.com/softwarepub/hermes' +} + +nvd_cpe_pattern() { + echo 'cpe:2\.3:a:software-metadata\.pub:hermes(?::|$)' +} + +nvd_query_slug() { + local kind="$1" + local value="$2" + printf '%s__%s' "$kind" "$value" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9._-]/_/g' +} + +nvd_build_url() { + local kind="$1" + local value="$2" + local suffix="${3:-}" + local encoded + + encoded=$(jq -nr --arg v "$value" '$v|@uri') + + case "$kind" in + keyword) + printf 'https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=%s%s' "$encoded" "$suffix" + ;; + virtualMatchString) + printf 'https://services.nvd.nist.gov/rest/json/cves/2.0?virtualMatchString=%s%s' "$encoded" "$suffix" + ;; + *) + echo "Error: unsupported NVD query kind: $kind" >&2 + return 1 + ;; + esac +} diff --git a/scripts/populate-local-feed.sh b/scripts/populate-local-feed.sh index d02a3c5..c0d4039 100755 --- a/scripts/populate-local-feed.sh +++ b/scripts/populate-local-feed.sh @@ -16,8 +16,10 @@ source "$SCRIPT_DIR/feed-utils.sh" # Configuration - same as pipeline init_feed_paths "$PROJECT_ROOT" -KEYWORDS="OpenClaw clawdbot Moltbot NanoClaw WhatsApp-bot baileys" -GITHUB_REF_PATTERN="github.com/openclaw/openclaw github.com/qwibitai/NanoClaw" +NVD_QUERY_SPECS="$(nvd_query_specs)" +KEYWORDS_PATTERN="$(nvd_keyword_pattern)" +GITHUB_REF_PATTERN="$(nvd_github_ref_pattern)" +CPE_PATTERN="$(nvd_cpe_pattern)" ENRICH_SCRIPT="$PROJECT_ROOT/scripts/ci/enrich_exploitability.sh" # Parse args @@ -86,16 +88,19 @@ END_ENC=${END_DATE//:/%3A} echo "=== Fetching CVEs from NVD ===" -for KEYWORD in $KEYWORDS; do - echo "Fetching keyword: $KEYWORD" - - URL="https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=${KEYWORD}&lastModStartDate=${START_ENC}&lastModEndDate=${END_ENC}" - +while IFS='|' read -r QUERY_KIND QUERY_VALUE; do + [ -n "$QUERY_KIND" ] || continue + + QUERY_SLUG=$(nvd_query_slug "$QUERY_KIND" "$QUERY_VALUE") + echo "Fetching $QUERY_KIND query: $QUERY_VALUE" + + URL=$(nvd_build_url "$QUERY_KIND" "$QUERY_VALUE" "&lastModStartDate=${START_ENC}&lastModEndDate=${END_ENC}") + # Fetch with retry logic for i in 1 2 3; do - HTTP_CODE=$(curl -s -w "%{http_code}" -o "$TEMP_DIR/nvd_${KEYWORD}.json" "$URL") + HTTP_CODE=$(curl -s -w "%{http_code}" -o "$TEMP_DIR/nvd_${QUERY_SLUG}.json" "$URL") if [ "$HTTP_CODE" = "200" ]; then - COUNT=$(jq '.vulnerabilities | length // 0' "$TEMP_DIR/nvd_${KEYWORD}.json" 2>/dev/null || echo 0) + COUNT=$(jq '.vulnerabilities | length // 0' "$TEMP_DIR/nvd_${QUERY_SLUG}.json" 2>/dev/null || echo 0) echo " ✓ Found $COUNT CVEs" break elif [ "$HTTP_CODE" = "403" ] || [ "$HTTP_CODE" = "429" ]; then @@ -110,7 +115,7 @@ for KEYWORD in $KEYWORDS; do # NVD recommends 6 second delay between requests echo " Waiting 6s (NVD rate limit)..." sleep 6 -done +done <<< "$NVD_QUERY_SPECS" echo "" echo "=== Processing CVEs ===" @@ -118,8 +123,10 @@ echo "=== Processing CVEs ===" # Combine all fetched CVEs echo '{"vulnerabilities":[]}' > "$TEMP_DIR/combined.json" -for KEYWORD in $KEYWORDS; do - FILE="$TEMP_DIR/nvd_${KEYWORD}.json" +while IFS='|' read -r QUERY_KIND QUERY_VALUE; do + [ -n "$QUERY_KIND" ] || continue + QUERY_SLUG=$(nvd_query_slug "$QUERY_KIND" "$QUERY_VALUE") + FILE="$TEMP_DIR/nvd_${QUERY_SLUG}.json" if [ -f "$FILE" ] && [ -s "$FILE" ]; then if jq -e '.vulnerabilities' "$FILE" > /dev/null 2>&1; then jq -s '.[0].vulnerabilities += .[1].vulnerabilities | .[0]' \ @@ -127,7 +134,7 @@ for KEYWORD in $KEYWORDS; do mv "$TEMP_DIR/combined_new.json" "$TEMP_DIR/combined.json" fi fi -done +done <<< "$NVD_QUERY_SPECS" # Deduplicate by CVE ID jq '.vulnerabilities | unique_by(.cve.id)' "$TEMP_DIR/combined.json" > "$TEMP_DIR/unique_cves.json" @@ -135,13 +142,13 @@ TOTAL=$(jq 'length' "$TEMP_DIR/unique_cves.json") echo "Total unique CVEs from NVD: $TOTAL" # Post-filter: keep only CVEs matching our criteria -KEYWORDS_PATTERN="OpenClaw|clawdbot|Moltbot|openclaw|NanoClaw|nanoclaw|WhatsApp-bot|baileys" - -jq --arg kw "$KEYWORDS_PATTERN" --arg gh "$GITHUB_REF_PATTERN" ' +jq --arg kw "$KEYWORDS_PATTERN" --arg gh "$GITHUB_REF_PATTERN" --arg cpe "$CPE_PATTERN" ' [.[] | select( (.cve.descriptions[]? | select(.lang == "en") | .value | test($kw; "i")) or (.cve.references[]? | .url | test($gh; "i")) + or + ([.cve.configurations[]? | .. | objects | .criteria? | strings | test($cpe; "i")] | any) )] ' "$TEMP_DIR/unique_cves.json" > "$TEMP_DIR/filtered_cves.json" @@ -255,7 +262,8 @@ jq --argjson existing "$EXISTING_JSON" ' ( [ (.cve.descriptions[]? | select(.lang == "en") | .value), - (.cve.references[]?.url // empty) + (.cve.references[]?.url // empty), + (.cve.configurations[]? | .. | objects | .criteria? // empty) ] | map(strings | ascii_downcase) | join(" ") @@ -263,15 +271,42 @@ jq --argjson existing "$EXISTING_JSON" ' | ( (if ($blob | test("github\\.com/openclaw/openclaw|\\bopenclaw\\b|\\bclawdbot\\b|\\bmoltbot\\b")) then ["openclaw@*"] else [] end) + (if ($blob | test("github\\.com/qwibitai/nanoclaw|\\bnanoclaw\\b|whatsapp-bot|\\bbaileys\\b")) then ["nanoclaw@*"] else [] end) + + (if ($blob | test("github\\.com/softwarepub/hermes|cpe:2\\.3:a:software-metadata\\.pub:hermes|\\bhermes workflow\\b|software publication with rich metadata")) then ["hermes@*"] else [] end) ) ); - def normalized_affected: + def matched_targets: ( (cpe_criteria + inferred_targets) | unique | .[0:5] - | if length == 0 then ["openclaw@*", "nanoclaw@*"] else . end + ); + + def platforms_from_targets($targets): + ( + [ + (if ($targets | map(strings | ascii_downcase | select(startswith("openclaw@") or test("^cpe:2\\.3:[aho]:openclaw:openclaw(?::|$)"))) | length > 0) then "openclaw" else empty end), + (if ($targets | map(strings | ascii_downcase | select(startswith("nanoclaw@") or test("^cpe:2\\.3:[aho]:[^:]*:nanoclaw(?::|$)"))) | length > 0) then "nanoclaw" else empty end), + (if ($targets | map(strings | ascii_downcase | select(startswith("hermes@") or test("^cpe:2\\.3:[aho]:software-metadata\\.pub:hermes(?::|$)"))) | length > 0) then "hermes" else empty end) + ] + ); + + def normalized_affected: + ( + matched_targets + | if length == 0 then ["openclaw@*", "nanoclaw@*", "hermes@*"] else . end + ); + + def normalized_platforms: + ( + inferred_targets as $inferred + | platforms_from_targets($inferred) as $from_inferred + | if ($from_inferred | length) > 0 then $from_inferred + else + matched_targets as $targets + | platforms_from_targets($targets) as $from_targets + | if ($from_targets | length) > 0 then $from_targets else ["openclaw", "nanoclaw", "hermes"] end + end ); [.[] | @@ -284,6 +319,7 @@ jq --argjson existing "$EXISTING_JSON" ' title: (.cve.descriptions[] | select(.lang == "en") | .value | .[0:100] + (if length > 100 then "..." else "" end)), description: (.cve.descriptions[] | select(.lang == "en") | .value), affected: normalized_affected, + platforms: normalized_platforms, action: "Review and update affected components. See NVD for remediation details.", published: .cve.published, references: [.cve.references[]?.url // empty] | unique | .[0:3], @@ -359,7 +395,7 @@ else jq -n --argjson advisories "$(cat "$TEMP_DIR/new_advisories.json")" --arg now "$NOW" '{ version: "1.0.0", updated: $now, - description: "Community-driven security advisory feed for ClawSec. Automatically updated with OpenClaw and NanoClaw-related CVEs from NVD.", + description: "Community-driven security advisory feed for ClawSec. Automatically updated with OpenClaw, NanoClaw, and Hermes-related CVEs from NVD.", advisories: ($advisories | sort_by(.published) | reverse) }' > "$TEMP_DIR/updated_feed.json" fi