fix(nvd): add hermes query specs to feed polling (#203)

* fix(nvd): add hermes query specs to feed polling

* fix(nvd): derive platform fallback from matched targets
This commit is contained in:
davida-ps
2026-04-21 16:18:45 +03:00
committed by GitHub
parent 26af277afd
commit c54f09c3a4
3 changed files with 207 additions and 76 deletions
+53
View File
@@ -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
}
+56 -20
View File
@@ -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