Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 37 additions & 15 deletions .github/workflows/poll-nvd-cves.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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.",
Expand All @@ -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 ==="
Expand Down Expand Up @@ -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
)
Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions components/AdvisoryCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { ExternalLink, Github } from 'lucide-react';
import { Advisory } from '../types';
import { AdvisoryPlatformBadge } from './AdvisoryPlatformBadge';

interface AdvisoryCardProps {
advisory: Advisory;
Expand Down Expand Up @@ -65,6 +66,18 @@ export const AdvisoryCard: React.FC<AdvisoryCardProps> = ({ advisory, formatDate
{advisory.id}
</h3>
<p className="text-sm text-gray-400 line-clamp-3 mb-3">{advisory.title}</p>

{advisory.platforms && advisory.platforms.length > 0 && (
<div className="mb-3 flex flex-wrap gap-1.5">
{advisory.platforms.map((platform) => (
<AdvisoryPlatformBadge
key={`${advisory.id}-${platform}`}
platform={platform}
className="text-[11px] px-2 py-0.5 rounded"
/>
))}
</div>
)}

{/* External link - stop propagation to allow clicking without navigating to detail */}
{isCommunityReport && advisory.github_issue_url ? (
Expand Down
19 changes: 19 additions & 0 deletions components/AdvisoryPlatformBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { getPlatformDescriptor } from '../utils/advisoryPlatforms';

interface AdvisoryPlatformBadgeProps {
platform: string;
className?: string;
}

export const AdvisoryPlatformBadge: React.FC<AdvisoryPlatformBadgeProps> = ({
platform,
className,
}) => {
const { label, classes } = getPlatformDescriptor(platform);
const badgeClasses = ['uppercase tracking-wide', classes, className]
.filter(Boolean)
.join(' ');

return <span className={badgeClasses}>{label}</span>;
};
15 changes: 15 additions & 0 deletions pages/AdvisoryDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +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 } 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,
Expand Down Expand Up @@ -154,6 +156,13 @@ export const AdvisoryDetail: React.FC = () => {
<span className="text-sm text-gray-500">
Published {formatDate(advisory.published)}
</span>
{advisory.platforms?.map((platform) => (
<AdvisoryPlatformBadge
key={`${advisory.id}-platform-${platform}`}
platform={platform}
className="text-xs px-2 py-1 rounded"
/>
))}
</div>

<h1 className="text-3xl font-bold text-white">{advisory.id}</h1>
Expand Down Expand Up @@ -259,6 +268,12 @@ export const AdvisoryDetail: React.FC = () => {
<dt className="text-gray-500 mb-1">Published</dt>
<dd className="text-white">{formatDate(advisory.published)}</dd>
</div>
{advisory.platforms && advisory.platforms.length > 0 && (
<div className="flex justify-between md:block">
<dt className="text-gray-500 mb-1">Platforms</dt>
<dd className="text-white">{advisory.platforms.map((platform) => getPlatformDescriptor(platform).label).join(', ')}</dd>
</div>
)}
{/* Reporter info - subtle display for community reports */}
{advisory.reporter && (
<>
Expand Down
67 changes: 50 additions & 17 deletions pages/FeedSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } from '../types';
import { Advisory, AdvisoryFeed, AdvisoryPlatformFilter } from '../types';
import { isCorePlatformSlug, normalizePlatformSlug } from '../utils/advisoryPlatforms';
import {
ADVISORY_FEED_URL,
LEGACY_ADVISORY_FEED_URL,
Expand All @@ -12,25 +13,34 @@ import {

const ITEMS_PER_PAGE = 9;

type SeverityFilter = 'all' | Advisory['severity'];
type FilterTabOption<T extends string> = { value: T; label: string; active: string; inactive: string };

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<FilterTabOption<SeverityFilter>>;

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' },
] as const;
{ 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' },
Comment on lines 28 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

community-advisory.yml still maps arbitrary Other: slugs into platforms even though platform tabs only cover openclaw, nanoclaw, and hermes, so approved nonstandard slugs won’t show in filtered views—should we constrain the contract to that set or add an other/catch-all path?

Finding type: Type Inconsistency | Severity: 🟢 Low


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

Before applying, verify this suggestion against the current code. In pages/FeedSetup.tsx
around lines 24-27 (the PLATFORM_TABS definition, and related
`selectedPlatform`/`filteredAdvisories` filtering), the UI platform tabs are hard-coded
to only `openclaw`, `nanoclaw`, and `hermes`, but the feed data can include arbitrary
platform slugs (from `Other:`). Refactor so the feed contract and the UI stay
consistent: either constrain/normalize incoming `a.platforms` to only the allowed
literal set before filtering, or add an “Other”/catch-all path (including a tab and
filtering behavior) that surfaces advisories with nonstandard platform slugs when
`selectedPlatform` is set accordingly. Update the relevant TypeScript types so
`selectedPlatform` and the tab values reflect the real set of selectable options, and
ensure nonstandard platforms don’t silently disappear when not on “All Platforms”.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 30f99eb addressed this comment by introducing a typed platform filter that includes an “Other” tab, normalizing incoming platform slugs, and filtering any advisory whose slug isn’t in the CORE_PLATFORM_SLUGS set under that catch-all option so nonstandard slugs remain discoverable.

{ 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<FilterTabOption<AdvisoryPlatformFilter>>;

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 = <T extends string,>({
tabs,
selected,
onSelect,
}: {
tabs: ReadonlyArray<FilterTabOption<T>>;
selected: T;
onSelect: (value: T) => void;
}) => (
<div className="flex flex-wrap justify-center gap-3 mb-8">
{tabs.map(({ value, label, active, inactive }) => (
<button
Expand All @@ -52,8 +62,8 @@ export const FeedSetup: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [selectedSeverity, setSelectedSeverity] = useState<string>('all');
const [selectedPlatform, setSelectedPlatform] = useState<string>('all');
const [selectedSeverity, setSelectedSeverity] = useState<SeverityFilter>('all');
const [selectedPlatform, setSelectedPlatform] = useState<AdvisoryPlatformFilter>('all');

useEffect(() => {
const fetchAdvisories = async () => {
Expand Down Expand Up @@ -92,10 +102,25 @@ export const FeedSetup: React.FC = () => {
}, []);

const filteredAdvisories = useMemo(
() => advisories.filter((a) =>
(selectedSeverity === 'all' || a.severity === selectedSeverity) &&
(selectedPlatform === 'all' || !a.platforms?.length || a.platforms.includes(selectedPlatform))
),
() => advisories.filter((a) => {
if (selectedSeverity !== 'all' && a.severity !== selectedSeverity) {
return false;
}

if (selectedPlatform === 'all') {
return true;
}

const advisoryPlatforms = (a.platforms ?? [])
.map(normalizePlatformSlug)
.filter(Boolean);

if (selectedPlatform === 'other') {
return advisoryPlatforms.some((platform) => !isCorePlatformSlug(platform));
}

return advisoryPlatforms.length === 0 || advisoryPlatforms.includes(selectedPlatform);
}),
[advisories, selectedSeverity, selectedPlatform],
);

Expand Down Expand Up @@ -132,7 +157,7 @@ export const FeedSetup: React.FC = () => {
<h1 className="text-3xl md:text-4xl text-white">Security Hardening Feed</h1>
<p className="text-gray-400 max-w-2xl mx-auto">
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.
</p>
{lastUpdated && (
<p className="text-xs text-gray-500">
Expand All @@ -142,8 +167,16 @@ export const FeedSetup: React.FC = () => {
</section>

<section>
<FilterTabs tabs={SEVERITY_TABS} selected={selectedSeverity} onSelect={setSelectedSeverity} />
<FilterTabs tabs={PLATFORM_TABS} selected={selectedPlatform} onSelect={setSelectedPlatform} />
<FilterTabs
tabs={SEVERITY_TABS}
selected={selectedSeverity}
onSelect={(value) => setSelectedSeverity(value as SeverityFilter)}
/>
<FilterTabs
tabs={PLATFORM_TABS}
selected={selectedPlatform}
onSelect={(value) => setSelectedPlatform(value as AdvisoryPlatformFilter)}
/>

{loading ? (
<div className="flex items-center justify-center py-12">
Expand Down
Loading
Loading