From 7b103d6981749a837d00505fd0d969bc362e3fde Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Tue, 21 Apr 2026 15:17:39 +0300 Subject: [PATCH 1/7] fix(nvd): add hermes query specs to feed polling --- .github/workflows/poll-nvd-cves.yml | 62 ++++++++--------------------- scripts/populate-local-feed.sh | 31 ++++----------- 2 files changed, 24 insertions(+), 69 deletions(-) diff --git a/.github/workflows/poll-nvd-cves.yml b/.github/workflows/poll-nvd-cves.yml index ea8a0a3..3c0a8a6 100644 --- a/.github/workflows/poll-nvd-cves.yml +++ b/.github/workflows/poll-nvd-cves.yml @@ -402,38 +402,23 @@ jobs: ) ); - def matched_targets: + def normalized_affected: ( (cpe_criteria + inferred_targets) | unique | .[0:5] - ); - - 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 + inferred_targets as $targets + | [ + (if ($targets | map(select(startswith("openclaw@"))) | length > 0) then "openclaw" else empty end), + (if ($targets | map(select(startswith("nanoclaw@"))) | length > 0) then "nanoclaw" else empty end), + (if ($targets | map(select(startswith("hermes@"))) | length > 0) then "hermes" else empty end) + ] + | if length == 0 then ["openclaw", "nanoclaw", "hermes"] else . end ); [.[] | { @@ -635,38 +620,23 @@ jobs: ) ); - def matched_targets: + def normalized_affected: ( (cpe_criteria + inferred_targets) | unique | .[0:5] - ); - - 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 + inferred_targets as $targets + | [ + (if ($targets | map(select(startswith("openclaw@"))) | length > 0) then "openclaw" else empty end), + (if ($targets | map(select(startswith("nanoclaw@"))) | length > 0) then "nanoclaw" else empty end), + (if ($targets | map(select(startswith("hermes@"))) | length > 0) then "hermes" else empty end) + ] + | if length == 0 then ["openclaw", "nanoclaw", "hermes"] else . end ); [.[] | diff --git a/scripts/populate-local-feed.sh b/scripts/populate-local-feed.sh index c0d4039..13abebb 100755 --- a/scripts/populate-local-feed.sh +++ b/scripts/populate-local-feed.sh @@ -275,38 +275,23 @@ jq --argjson existing "$EXISTING_JSON" ' ) ); - def matched_targets: + def normalized_affected: ( (cpe_criteria + inferred_targets) | unique | .[0:5] - ); - - 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 + inferred_targets as $targets + | [ + (if ($targets | map(select(startswith("openclaw@"))) | length > 0) then "openclaw" else empty end), + (if ($targets | map(select(startswith("nanoclaw@"))) | length > 0) then "nanoclaw" else empty end), + (if ($targets | map(select(startswith("hermes@"))) | length > 0) then "hermes" else empty end) + ] + | if length == 0 then ["openclaw", "nanoclaw", "hermes"] else . end ); [.[] | From dd5e2f87b41598cbec9f4c1a41a08039a734677b Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Tue, 21 Apr 2026 16:13:13 +0300 Subject: [PATCH 2/7] fix(nvd): derive platform fallback from matched targets --- .github/workflows/poll-nvd-cves.yml | 62 +++++++++++++++++++++-------- scripts/populate-local-feed.sh | 31 +++++++++++---- 2 files changed, 69 insertions(+), 24 deletions(-) diff --git a/.github/workflows/poll-nvd-cves.yml b/.github/workflows/poll-nvd-cves.yml index 3c0a8a6..ea8a0a3 100644 --- a/.github/workflows/poll-nvd-cves.yml +++ b/.github/workflows/poll-nvd-cves.yml @@ -402,23 +402,38 @@ jobs: ) ); - def normalized_affected: + def matched_targets: ( (cpe_criteria + inferred_targets) | unique | .[0:5] + ); + + 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 - | [ - (if ($targets | map(select(startswith("openclaw@"))) | length > 0) then "openclaw" else empty end), - (if ($targets | map(select(startswith("nanoclaw@"))) | length > 0) then "nanoclaw" else empty end), - (if ($targets | map(select(startswith("hermes@"))) | length > 0) then "hermes" else empty end) - ] - | if length == 0 then ["openclaw", "nanoclaw", "hermes"] else . end + 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 ); [.[] | { @@ -620,23 +635,38 @@ jobs: ) ); - def normalized_affected: + def matched_targets: ( (cpe_criteria + inferred_targets) | unique | .[0:5] + ); + + 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 - | [ - (if ($targets | map(select(startswith("openclaw@"))) | length > 0) then "openclaw" else empty end), - (if ($targets | map(select(startswith("nanoclaw@"))) | length > 0) then "nanoclaw" else empty end), - (if ($targets | map(select(startswith("hermes@"))) | length > 0) then "hermes" else empty end) - ] - | if length == 0 then ["openclaw", "nanoclaw", "hermes"] else . end + 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/populate-local-feed.sh b/scripts/populate-local-feed.sh index 13abebb..c0d4039 100755 --- a/scripts/populate-local-feed.sh +++ b/scripts/populate-local-feed.sh @@ -275,23 +275,38 @@ jq --argjson existing "$EXISTING_JSON" ' ) ); - def normalized_affected: + def matched_targets: ( (cpe_criteria + inferred_targets) | unique | .[0:5] + ); + + 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 - | [ - (if ($targets | map(select(startswith("openclaw@"))) | length > 0) then "openclaw" else empty end), - (if ($targets | map(select(startswith("nanoclaw@"))) | length > 0) then "nanoclaw" else empty end), - (if ($targets | map(select(startswith("hermes@"))) | length > 0) then "hermes" else empty end) - ] - | if length == 0 then ["openclaw", "nanoclaw", "hermes"] else . end + 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 ); [.[] | From 2638481f88a04d9b347a4391da92896e7228831c Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Tue, 21 Apr 2026 16:31:35 +0300 Subject: [PATCH 3/7] fix(nvd): avoid arg overflow on full cve rescan --- .github/workflows/poll-nvd-cves.yml | 52 ++++++++++++++++++++--------- components/AdvisoryCard.tsx | 39 ++++++++++++++++++++++ pages/AdvisoryDetail.tsx | 40 ++++++++++++++++++++++ pages/FeedSetup.tsx | 3 +- scripts/populate-local-feed.sh | 37 ++++++++++++-------- 5 files changed, 142 insertions(+), 29 deletions(-) diff --git a/.github/workflows/poll-nvd-cves.yml b/.github/workflows/poll-nvd-cves.yml index ea8a0a3..a0b9bf8 100644 --- a/.github/workflows/poll-nvd-cves.yml +++ b/.github/workflows/poll-nvd-cves.yml @@ -436,14 +436,21 @@ jobs: end ); + def preferred_description: + ( + (.cve.descriptions[]? | select(.lang == "en") | .value) + // .cve.descriptions[0]?.value + // "No description provided by NVD." + ); + [.[] | { id: .cve.id, severity: (get_cvss_score | map_severity), type: nvd_category_name, nvd_category_id: nvd_category_raw, cvss_score: get_cvss_score, - description: (.cve.descriptions[] | select(.lang == "en") | .value), - title: (.cve.descriptions[] | select(.lang == "en") | .value | .[0:100] + (if length > 100 then "..." else "" end)), + description: preferred_description, + title: (preferred_description | .[0:100] + (if length > 100 then "..." else "" end)), affected: normalized_affected, platforms: normalized_platforms, references: [.cve.references[]?.url // empty] | unique | .[0:3], @@ -516,16 +523,16 @@ jobs: - name: Transform CVEs to advisories id: transform run: | - # Read existing IDs into a jq-friendly format + # Read existing IDs into a jq-friendly file for jq (avoids huge CLI args on full scans). if [ "${{ inputs.force_full_scan }}" = "true" ]; then echo "Full scan mode enabled: rebuilding CVE advisories from scratch." - EXISTING_IDS='[]' + echo '[]' > tmp/existing_ids.json else - EXISTING_IDS=$(cat tmp/existing_ids.txt | jq -R -s 'split("\n") | map(select(length > 0))') + jq -R -s 'split("\n") | map(select(length > 0))' < tmp/existing_ids.txt > tmp/existing_ids.json fi # Transform NVD CVEs to our advisory format - jq --argjson existing "$EXISTING_IDS" ' + jq --slurpfile existing tmp/existing_ids.json ' def map_severity: if . == null then "medium" elif . >= 9.0 then "critical" @@ -668,16 +675,23 @@ jobs: | if ($from_targets | length) > 0 then $from_targets else ["openclaw", "nanoclaw", "hermes"] end end ); + + def preferred_description: + ( + (.cve.descriptions[]? | select(.lang == "en") | .value) + // .cve.descriptions[0]?.value + // "No description provided by NVD." + ); [.[] | - select(.cve.id as $id | $existing | index($id) | not) | + select(.cve.id as $id | (($existing[0] // []) | index($id) | not)) | { id: .cve.id, severity: (get_cvss_score | map_severity), type: nvd_category_name, nvd_category_id: nvd_category_raw, - title: (.cve.descriptions[] | select(.lang == "en") | .value | .[0:100] + (if length > 100 then "..." else "" end)), - description: (.cve.descriptions[] | select(.lang == "en") | .value), + title: (preferred_description | .[0:100] + (if length > 100 then "..." else "" end)), + description: preferred_description, affected: normalized_affected, platforms: normalized_platforms, action: "Review and update affected components. See NVD for remediation details.", @@ -694,6 +708,14 @@ jobs: NEW_COUNT=$(jq 'length' tmp/new_advisories.json) echo "New advisories to add: $NEW_COUNT" echo "new_count=$NEW_COUNT" >> $GITHUB_OUTPUT + + if [ "${{ inputs.force_full_scan }}" = "true" ]; then + FILTERED_COUNT="${{ steps.process.outputs.filtered_count }}" + if [ "$NEW_COUNT" -ne "$FILTERED_COUNT" ]; then + echo "::error::Full scan transform mismatch: filtered CVEs=$FILTERED_COUNT transformed advisories=$NEW_COUNT" + exit 1 + fi + fi if [ "$NEW_COUNT" -gt 0 ]; then echo "=== New advisories ===" @@ -747,11 +769,11 @@ jobs: 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" ' + jq --slurpfile rebuilt tmp/new_advisories.json --arg now "$NOW" ' .updated = $now | .advisories = ( ((.advisories // []) | map(select((.id // "") | startswith("CVE-") | not))) - + $rebuilt + + ($rebuilt[0] // []) | sort_by(.published) | reverse ) @@ -773,16 +795,16 @@ jobs: ' "$FEED_PATH" > tmp/feed_with_updates.json # Step 2: Add new advisories - jq --argjson new "$(cat tmp/new_advisories.json)" --arg now "$NOW" ' + jq --slurpfile new tmp/new_advisories.json --arg now "$NOW" ' .updated = $now | - .advisories = (.advisories + $new | sort_by(.published) | reverse) + .advisories = (.advisories + ($new[0] // []) | sort_by(.published) | reverse) ' tmp/feed_with_updates.json > tmp/updated_feed.json else - jq -n --argjson advisories "$(cat tmp/new_advisories.json)" --arg now "$NOW" '{ + jq -n --slurpfile advisories tmp/new_advisories.json --arg now "$NOW" '{ version: "1.0.0", updated: $now, description: "Community-driven security advisory feed for ClawSec", - advisories: ($advisories | sort_by(.published) | reverse) + advisories: (($advisories[0] // []) | sort_by(.published) | reverse) }' > tmp/updated_feed.json fi diff --git a/components/AdvisoryCard.tsx b/components/AdvisoryCard.tsx index e1cb6b0..2e0d4e5 100644 --- a/components/AdvisoryCard.tsx +++ b/components/AdvisoryCard.tsx @@ -41,6 +41,32 @@ export const AdvisoryCard: React.FC = ({ advisory, formatDate } }; + const getPlatformLabel = (platform: string) => { + switch (platform) { + case 'openclaw': + return 'OpenClaw'; + case 'nanoclaw': + return 'NanoClaw'; + case 'hermes': + return 'Hermes'; + default: + return platform; + } + }; + + const getPlatformClasses = (platform: string) => { + switch (platform) { + case 'openclaw': + return 'bg-clawd-accent/20 text-clawd-accent border border-clawd-accent/40'; + case 'nanoclaw': + return 'bg-clawd-secondary/20 text-clawd-secondary border border-clawd-secondary/40'; + case 'hermes': + return 'bg-emerald-500/20 text-emerald-300 border border-emerald-400/40'; + default: + return 'bg-clawd-700 text-gray-300 border border-clawd-600'; + } + }; + // Determine if this is a community report (has github_issue_url) or NVD/staff advisory const isCommunityReport = !!advisory.github_issue_url; @@ -65,6 +91,19 @@ export const AdvisoryCard: React.FC = ({ advisory, formatDate {advisory.id}

{advisory.title}

+ + {advisory.platforms && advisory.platforms.length > 0 && ( +
+ {advisory.platforms.map((platform) => ( + + {getPlatformLabel(platform)} + + ))} +
+ )} {/* External link - stop propagation to allow clicking without navigating to detail */} {isCommunityReport && advisory.github_issue_url ? ( diff --git a/pages/AdvisoryDetail.tsx b/pages/AdvisoryDetail.tsx index 1c2cfa6..66e42ee 100644 --- a/pages/AdvisoryDetail.tsx +++ b/pages/AdvisoryDetail.tsx @@ -100,6 +100,32 @@ export const AdvisoryDetail: React.FC = () => { } }; + const getPlatformLabel = (platform: string) => { + switch (platform) { + case 'openclaw': + return 'OpenClaw'; + case 'nanoclaw': + return 'NanoClaw'; + case 'hermes': + return 'Hermes'; + default: + return platform; + } + }; + + const getPlatformClasses = (platform: string) => { + switch (platform) { + case 'openclaw': + return 'bg-clawd-accent/20 text-clawd-accent border border-clawd-accent/40'; + case 'nanoclaw': + return 'bg-clawd-secondary/20 text-clawd-secondary border border-clawd-secondary/40'; + case 'hermes': + return 'bg-emerald-500/20 text-emerald-300 border border-emerald-400/40'; + default: + return 'bg-clawd-700 text-gray-300 border border-clawd-600'; + } + }; + // Determine source - defaults to "Prompt Security Staff" when absent const getSource = (adv: Advisory) => { return adv.source || 'Prompt Security Staff'; @@ -154,6 +180,14 @@ export const AdvisoryDetail: React.FC = () => { Published {formatDate(advisory.published)} + {advisory.platforms?.map((platform) => ( + + {getPlatformLabel(platform)} + + ))}

{advisory.id}

@@ -259,6 +293,12 @@ export const AdvisoryDetail: React.FC = () => {
Published
{formatDate(advisory.published)}
+ {advisory.platforms && advisory.platforms.length > 0 && ( +
+
Platforms
+
{advisory.platforms.map(getPlatformLabel).join(', ')}
+
+ )} {/* Reporter info - subtle display for community reports */} {advisory.reporter && ( <> diff --git a/pages/FeedSetup.tsx b/pages/FeedSetup.tsx index 224d1de..1bc740e 100644 --- a/pages/FeedSetup.tsx +++ b/pages/FeedSetup.tsx @@ -24,6 +24,7 @@ const PLATFORM_TABS = [ { value: 'all', label: 'All Platforms', active: 'bg-clawd-accent text-white', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-accent/50' }, { value: 'openclaw', label: 'OpenClaw', active: 'bg-clawd-accent/20 text-clawd-accent border-2 border-clawd-accent', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-accent/50' }, { value: 'nanoclaw', label: 'NanoClaw', active: 'bg-clawd-secondary/20 text-clawd-secondary border-2 border-clawd-secondary', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-secondary/50' }, + { value: 'hermes', label: 'Hermes', active: 'bg-emerald-500/20 text-emerald-300 border-2 border-emerald-400', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-emerald-400/50' }, ] as const; const FilterTabs: React.FC<{ @@ -132,7 +133,7 @@ export const FeedSetup: React.FC = () => {

Security Hardening Feed

A continuous stream of security advisories from NVD CVE data and staff-approved community reports. - This feed is automatically updated with OpenClaw and NanoClaw-related vulnerabilities and verified security incidents. + This feed is automatically updated with OpenClaw, NanoClaw, and Hermes-related vulnerabilities and verified security incidents.

{lastUpdated && (

diff --git a/scripts/populate-local-feed.sh b/scripts/populate-local-feed.sh index c0d4039..4b25616 100755 --- a/scripts/populate-local-feed.sh +++ b/scripts/populate-local-feed.sh @@ -158,17 +158,16 @@ echo "Filtered CVEs (matching criteria): $FILTERED" # Get existing advisory IDs (unless force mode) if [ "$FORCE" = "true" ]; then echo "Force mode: ignoring existing advisory IDs during transform" - EXISTING_IDS="" + echo '[]' > "$TEMP_DIR/existing_ids.json" elif [ -f "$FEED_PATH" ]; then - EXISTING_IDS=$(jq -r '.advisories[]?.id // empty' "$FEED_PATH" | sort -u) + jq -r '.advisories[]?.id // empty' "$FEED_PATH" | sort -u | \ + jq -R -s 'split("\n") | map(select(length > 0))' > "$TEMP_DIR/existing_ids.json" else - EXISTING_IDS="" + echo '[]' > "$TEMP_DIR/existing_ids.json" fi # Transform CVEs to our advisory format (same logic as pipeline) -EXISTING_JSON=$(echo "$EXISTING_IDS" | jq -R -s 'split("\n") | map(select(length > 0))') - -jq --argjson existing "$EXISTING_JSON" ' +jq --slurpfile existing "$TEMP_DIR/existing_ids.json" ' def map_severity: if . == null then "medium" elif . >= 9.0 then "critical" @@ -308,16 +307,23 @@ jq --argjson existing "$EXISTING_JSON" ' | if ($from_targets | length) > 0 then $from_targets else ["openclaw", "nanoclaw", "hermes"] end end ); + + def preferred_description: + ( + (.cve.descriptions[]? | select(.lang == "en") | .value) + // .cve.descriptions[0]?.value + // "No description provided by NVD." + ); [.[] | - select(.cve.id as $id | $existing | index($id) | not) | + select(.cve.id as $id | (($existing[0] // []) | index($id) | not)) | { id: .cve.id, severity: (get_cvss_score | map_severity), type: nvd_category_name, nvd_category_id: nvd_category_raw, - title: (.cve.descriptions[] | select(.lang == "en") | .value | .[0:100] + (if length > 100 then "..." else "" end)), - description: (.cve.descriptions[] | select(.lang == "en") | .value), + title: (preferred_description | .[0:100] + (if length > 100 then "..." else "" end)), + description: preferred_description, affected: normalized_affected, platforms: normalized_platforms, action: "Review and update affected components. See NVD for remediation details.", @@ -334,6 +340,11 @@ jq --argjson existing "$EXISTING_JSON" ' NEW_COUNT=$(jq 'length' "$TEMP_DIR/new_advisories.json") echo "New advisories to add: $NEW_COUNT" +if [ "$FORCE" = "true" ] && [ "$NEW_COUNT" -ne "$FILTERED" ]; then + echo "Error: full rebuild transform mismatch (filtered=$FILTERED, transformed=$NEW_COUNT)" + exit 1 +fi + if [ "$NEW_COUNT" -eq 0 ]; then echo "" echo "No new CVEs found. Feed is up to date." @@ -374,11 +385,11 @@ NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ) # Merge new advisories into existing feed if [ -f "$FEED_PATH" ]; then - jq --argjson new "$(cat "$TEMP_DIR/new_advisories.json")" --arg now "$NOW" ' + jq --slurpfile new "$TEMP_DIR/new_advisories.json" --arg now "$NOW" ' .updated = $now | # Merge by advisory ID so force mode can refresh existing CVEs without duplicates .advisories = ( - reduce (.advisories + $new)[] as $adv + reduce ((.advisories // []) + ($new[0] // []))[] as $adv ({}; if ($adv.id // "") == "" then . @@ -392,11 +403,11 @@ if [ -f "$FEED_PATH" ]; then ) ' "$FEED_PATH" > "$TEMP_DIR/updated_feed.json" else - jq -n --argjson advisories "$(cat "$TEMP_DIR/new_advisories.json")" --arg now "$NOW" '{ + jq -n --slurpfile advisories "$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, NanoClaw, and Hermes-related CVEs from NVD.", - advisories: ($advisories | sort_by(.published) | reverse) + advisories: (($advisories[0] // []) | sort_by(.published) | reverse) }' > "$TEMP_DIR/updated_feed.json" fi From 30f99eb17818e0c77e15ca102def28ef3546a382 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 22 Apr 2026 08:26:49 +0300 Subject: [PATCH 4/7] fix(feed): add other platform filter for nonstandard slugs --- pages/FeedSetup.tsx | 54 +++++++++++++++++++++++++++++++++------------ types.ts | 7 +++++- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/pages/FeedSetup.tsx b/pages/FeedSetup.tsx index 1bc740e..7bb2bb8 100644 --- a/pages/FeedSetup.tsx +++ b/pages/FeedSetup.tsx @@ -3,7 +3,7 @@ import { Rss, RefreshCw, Loader2, AlertTriangle, ChevronLeft, ChevronRight, Down import { Link } from 'react-router-dom'; import { Footer } from '../components/Footer'; import { AdvisoryCard } from '../components/AdvisoryCard'; -import { Advisory, AdvisoryFeed } from '../types'; +import { Advisory, AdvisoryFeed, AdvisoryPlatformFilter, CORE_PLATFORM_SLUGS } from '../types'; import { ADVISORY_FEED_URL, LEGACY_ADVISORY_FEED_URL, @@ -12,26 +12,37 @@ import { const ITEMS_PER_PAGE = 9; +type SeverityFilter = 'all' | Advisory['severity']; +type FilterTabOption = { value: T; label: string; active: string; inactive: string }; + +const KNOWN_PLATFORM_SET = new Set(CORE_PLATFORM_SLUGS); +const normalizePlatformSlug = (platform: string) => platform.trim().toLowerCase(); + const SEVERITY_TABS = [ { value: 'all', label: 'All', active: 'bg-clawd-accent text-white', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-accent/50' }, { value: 'critical', label: 'Critical', active: 'bg-red-500/20 text-red-400 border-2 border-red-400', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-red-400/50' }, { value: 'high', label: 'High', active: 'bg-orange-500/20 text-orange-400 border-2 border-orange-400', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-orange-400/50' }, { value: 'medium', label: 'Medium', active: 'bg-yellow-500/20 text-yellow-400 border-2 border-yellow-400', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-yellow-400/50' }, { value: 'low', label: 'Low', active: 'bg-blue-500/20 text-blue-400 border-2 border-blue-400', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-blue-400/50' }, -] as const; +] as const satisfies ReadonlyArray>; const PLATFORM_TABS = [ { value: 'all', label: 'All Platforms', active: 'bg-clawd-accent text-white', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-accent/50' }, { value: 'openclaw', label: 'OpenClaw', active: 'bg-clawd-accent/20 text-clawd-accent border-2 border-clawd-accent', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-accent/50' }, { value: 'nanoclaw', label: 'NanoClaw', active: 'bg-clawd-secondary/20 text-clawd-secondary border-2 border-clawd-secondary', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-secondary/50' }, { value: 'hermes', label: 'Hermes', active: 'bg-emerald-500/20 text-emerald-300 border-2 border-emerald-400', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-emerald-400/50' }, -] as const; + { value: 'other', label: 'Other', active: 'bg-clawd-600/40 text-gray-100 border-2 border-clawd-500', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-500/50' }, +] as const satisfies ReadonlyArray>; -const FilterTabs: React.FC<{ - tabs: ReadonlyArray<{ value: string; label: string; active: string; inactive: string }>; - selected: string; - onSelect: (value: string) => void; -}> = ({ tabs, selected, onSelect }) => ( +const FilterTabs = ({ + tabs, + selected, + onSelect, +}: { + tabs: ReadonlyArray>; + selected: T; + onSelect: (value: T) => void; +}) => (

{tabs.map(({ value, label, active, inactive }) => (
@@ -296,7 +270,7 @@ export const AdvisoryDetail: React.FC = () => { {advisory.platforms && advisory.platforms.length > 0 && (
Platforms
-
{advisory.platforms.map(getPlatformLabel).join(', ')}
+
{advisory.platforms.map((platform) => getPlatformDescriptor(platform).label).join(', ')}
)} {/* Reporter info - subtle display for community reports */} From 9677b69e16e3b91e215b14dac522c27bd9b8fcc4 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 22 Apr 2026 10:25:12 +0300 Subject: [PATCH 6/7] fix(feed): share platform normalization and fix tab callback typing --- components/AdvisoryPlatformBadge.tsx | 35 +---------------------- pages/AdvisoryDetail.tsx | 3 +- pages/FeedSetup.tsx | 20 ++++++++----- utils/advisoryPlatforms.ts | 42 ++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 utils/advisoryPlatforms.ts diff --git a/components/AdvisoryPlatformBadge.tsx b/components/AdvisoryPlatformBadge.tsx index 1d48c95..b76ae24 100644 --- a/components/AdvisoryPlatformBadge.tsx +++ b/components/AdvisoryPlatformBadge.tsx @@ -1,38 +1,5 @@ import React from 'react'; - -interface PlatformDescriptor { - label: string; - classes: string; -} - -const normalizePlatformSlug = (platform: string) => platform.trim().toLowerCase(); - -export const getPlatformDescriptor = (platform: string): PlatformDescriptor => { - const normalized = normalizePlatformSlug(platform); - - switch (normalized) { - case 'openclaw': - return { - label: 'OpenClaw', - classes: 'bg-clawd-accent/20 text-clawd-accent border border-clawd-accent/40', - }; - case 'nanoclaw': - return { - label: 'NanoClaw', - classes: 'bg-clawd-secondary/20 text-clawd-secondary border border-clawd-secondary/40', - }; - case 'hermes': - return { - label: 'Hermes', - classes: 'bg-emerald-500/20 text-emerald-300 border border-emerald-400/40', - }; - default: - return { - label: platform.trim() || platform, - classes: 'bg-clawd-700 text-gray-300 border border-clawd-600', - }; - } -}; +import { getPlatformDescriptor } from '../utils/advisoryPlatforms'; interface AdvisoryPlatformBadgeProps { platform: string; diff --git a/pages/AdvisoryDetail.tsx b/pages/AdvisoryDetail.tsx index bd2d5b8..88fe269 100644 --- a/pages/AdvisoryDetail.tsx +++ b/pages/AdvisoryDetail.tsx @@ -1,9 +1,10 @@ import React, { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import { ArrowLeft, ExternalLink, Shield, AlertTriangle, Github, User, Bot } from 'lucide-react'; -import { AdvisoryPlatformBadge, getPlatformDescriptor } from '../components/AdvisoryPlatformBadge'; +import { AdvisoryPlatformBadge } from '../components/AdvisoryPlatformBadge'; import { Footer } from '../components/Footer'; import { Advisory, AdvisoryFeed } from '../types'; +import { getPlatformDescriptor } from '../utils/advisoryPlatforms'; import { ADVISORY_FEED_URL, LEGACY_ADVISORY_FEED_URL, diff --git a/pages/FeedSetup.tsx b/pages/FeedSetup.tsx index 7bb2bb8..65942c1 100644 --- a/pages/FeedSetup.tsx +++ b/pages/FeedSetup.tsx @@ -3,7 +3,8 @@ import { Rss, RefreshCw, Loader2, AlertTriangle, ChevronLeft, ChevronRight, Down import { Link } from 'react-router-dom'; import { Footer } from '../components/Footer'; import { AdvisoryCard } from '../components/AdvisoryCard'; -import { Advisory, AdvisoryFeed, AdvisoryPlatformFilter, CORE_PLATFORM_SLUGS } from '../types'; +import { Advisory, AdvisoryFeed, AdvisoryPlatformFilter } from '../types'; +import { isCorePlatformSlug, normalizePlatformSlug } from '../utils/advisoryPlatforms'; import { ADVISORY_FEED_URL, LEGACY_ADVISORY_FEED_URL, @@ -15,9 +16,6 @@ const ITEMS_PER_PAGE = 9; type SeverityFilter = 'all' | Advisory['severity']; type FilterTabOption = { value: T; label: string; active: string; inactive: string }; -const KNOWN_PLATFORM_SET = new Set(CORE_PLATFORM_SLUGS); -const normalizePlatformSlug = (platform: string) => platform.trim().toLowerCase(); - const SEVERITY_TABS = [ { value: 'all', label: 'All', active: 'bg-clawd-accent text-white', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-clawd-accent/50' }, { value: 'critical', label: 'Critical', active: 'bg-red-500/20 text-red-400 border-2 border-red-400', inactive: 'bg-clawd-800 text-gray-400 border border-clawd-700 hover:border-red-400/50' }, @@ -118,7 +116,7 @@ export const FeedSetup: React.FC = () => { .filter(Boolean); if (selectedPlatform === 'other') { - return advisoryPlatforms.some((platform) => !KNOWN_PLATFORM_SET.has(platform)); + return advisoryPlatforms.some((platform) => !isCorePlatformSlug(platform)); } return advisoryPlatforms.length === 0 || advisoryPlatforms.includes(selectedPlatform); @@ -169,8 +167,16 @@ export const FeedSetup: React.FC = () => {
- - + setSelectedSeverity(value as SeverityFilter)} + /> + setSelectedPlatform(value as AdvisoryPlatformFilter)} + /> {loading ? (
diff --git a/utils/advisoryPlatforms.ts b/utils/advisoryPlatforms.ts new file mode 100644 index 0000000..23868d6 --- /dev/null +++ b/utils/advisoryPlatforms.ts @@ -0,0 +1,42 @@ +import { CORE_PLATFORM_SLUGS } from '../types'; + +export interface PlatformDescriptor { + label: string; + classes: string; +} + +export const normalizePlatformSlug = (platform: string) => platform.trim().toLowerCase(); + +const PLATFORM_DESCRIPTOR_BY_SLUG: Record = { + openclaw: { + label: 'OpenClaw', + classes: 'bg-clawd-accent/20 text-clawd-accent border border-clawd-accent/40', + }, + nanoclaw: { + label: 'NanoClaw', + classes: 'bg-clawd-secondary/20 text-clawd-secondary border border-clawd-secondary/40', + }, + hermes: { + label: 'Hermes', + classes: 'bg-emerald-500/20 text-emerald-300 border border-emerald-400/40', + }, +}; + +const CORE_PLATFORM_SET = new Set(CORE_PLATFORM_SLUGS); + +export const isCorePlatformSlug = (platform: string) => + CORE_PLATFORM_SET.has(normalizePlatformSlug(platform)); + +export const getPlatformDescriptor = (platform: string): PlatformDescriptor => { + const normalized = normalizePlatformSlug(platform); + const descriptor = PLATFORM_DESCRIPTOR_BY_SLUG[normalized]; + + if (descriptor) { + return descriptor; + } + + return { + label: platform.trim() || platform, + classes: 'bg-clawd-700 text-gray-300 border border-clawd-600', + }; +}; From 899997c1177f46ecbbf55db952ae6c206d4c3839 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Wed, 22 Apr 2026 10:27:53 +0300 Subject: [PATCH 7/7] refactor(feed): simplify platform descriptor fallback --- utils/advisoryPlatforms.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/utils/advisoryPlatforms.ts b/utils/advisoryPlatforms.ts index 23868d6..74829f5 100644 --- a/utils/advisoryPlatforms.ts +++ b/utils/advisoryPlatforms.ts @@ -29,13 +29,7 @@ export const isCorePlatformSlug = (platform: string) => export const getPlatformDescriptor = (platform: string): PlatformDescriptor => { const normalized = normalizePlatformSlug(platform); - const descriptor = PLATFORM_DESCRIPTOR_BY_SLUG[normalized]; - - if (descriptor) { - return descriptor; - } - - return { + return PLATFORM_DESCRIPTOR_BY_SLUG[normalized] ?? { label: platform.trim() || platform, classes: 'bg-clawd-700 text-gray-300 border border-clawd-600', };