mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
feat: enhance support for NanoClaw in CVE processing and UI components (#67)
This commit is contained in:
@@ -175,7 +175,7 @@ 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"
|
||||
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" '
|
||||
@@ -297,6 +297,36 @@ jobs:
|
||||
end
|
||||
);
|
||||
|
||||
def cpe_criteria:
|
||||
(
|
||||
[.cve.configurations[]? | .. | objects | .criteria? | strings | select(startswith("cpe:2.3:"))]
|
||||
| unique
|
||||
);
|
||||
|
||||
def inferred_targets:
|
||||
(
|
||||
(
|
||||
[
|
||||
(.cve.descriptions[]? | select(.lang == "en") | .value),
|
||||
(.cve.references[]?.url // empty)
|
||||
]
|
||||
| map(strings | ascii_downcase)
|
||||
| join(" ")
|
||||
) 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)
|
||||
)
|
||||
);
|
||||
|
||||
def normalized_affected:
|
||||
(
|
||||
(cpe_criteria + inferred_targets)
|
||||
| unique
|
||||
| .[0:5]
|
||||
| if length == 0 then ["openclaw@*", "nanoclaw@*"] else . end
|
||||
);
|
||||
|
||||
[.[] | {
|
||||
id: .cve.id,
|
||||
severity: (get_cvss_score | map_severity),
|
||||
@@ -305,6 +335,7 @@ jobs:
|
||||
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)),
|
||||
affected: normalized_affected,
|
||||
references: [.cve.references[]?.url // empty] | unique | .[0:3]
|
||||
}]
|
||||
' tmp/filtered_cves.json > tmp/nvd_current_state.json
|
||||
@@ -325,6 +356,7 @@ jobs:
|
||||
($existing_entry.type != $nvd_entry.type) or
|
||||
($existing_entry.nvd_category_id != $nvd_entry.nvd_category_id) or
|
||||
($existing_entry.cvss_score != $nvd_entry.cvss_score) or
|
||||
($existing_entry.affected != $nvd_entry.affected) or
|
||||
($existing_entry.description != $nvd_entry.description) then
|
||||
{
|
||||
id: $nvd_entry.id,
|
||||
@@ -334,6 +366,7 @@ jobs:
|
||||
+ (if $existing_entry.type != $nvd_entry.type then ["type: \($existing_entry.type // "null") → \($nvd_entry.type // "null")"] else [] end)
|
||||
+ (if $existing_entry.nvd_category_id != $nvd_entry.nvd_category_id then ["nvd_category_id: \($existing_entry.nvd_category_id // "null") → \($nvd_entry.nvd_category_id // "null")"] else [] end)
|
||||
+ (if $existing_entry.cvss_score != $nvd_entry.cvss_score then ["cvss_score: \($existing_entry.cvss_score // "null") → \($nvd_entry.cvss_score // "null")"] else [] end)
|
||||
+ (if $existing_entry.affected != $nvd_entry.affected then ["affected targets updated"] else [] end)
|
||||
+ (if $existing_entry.description != $nvd_entry.description then ["description updated"] else [] end)
|
||||
),
|
||||
updated_fields: {
|
||||
@@ -341,6 +374,7 @@ jobs:
|
||||
type: $nvd_entry.type,
|
||||
nvd_category_id: $nvd_entry.nvd_category_id,
|
||||
cvss_score: $nvd_entry.cvss_score,
|
||||
affected: $nvd_entry.affected,
|
||||
description: $nvd_entry.description,
|
||||
title: $nvd_entry.title,
|
||||
references: $nvd_entry.references
|
||||
@@ -453,6 +487,36 @@ jobs:
|
||||
else (cwe_name_map($id) // ("unknown_cwe_" + $id))
|
||||
end
|
||||
);
|
||||
|
||||
def cpe_criteria:
|
||||
(
|
||||
[.cve.configurations[]? | .. | objects | .criteria? | strings | select(startswith("cpe:2.3:"))]
|
||||
| unique
|
||||
);
|
||||
|
||||
def inferred_targets:
|
||||
(
|
||||
(
|
||||
[
|
||||
(.cve.descriptions[]? | select(.lang == "en") | .value),
|
||||
(.cve.references[]?.url // empty)
|
||||
]
|
||||
| map(strings | ascii_downcase)
|
||||
| join(" ")
|
||||
) 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)
|
||||
)
|
||||
);
|
||||
|
||||
def normalized_affected:
|
||||
(
|
||||
(cpe_criteria + inferred_targets)
|
||||
| unique
|
||||
| .[0:5]
|
||||
| if length == 0 then ["openclaw@*", "nanoclaw@*"] else . end
|
||||
);
|
||||
|
||||
[.[] |
|
||||
select(.cve.id as $id | $existing | index($id) | not) |
|
||||
@@ -463,7 +527,7 @@ jobs:
|
||||
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),
|
||||
affected: [.cve.configurations[]?.nodes[]?.cpeMatch[]?.criteria // empty] | unique | .[0:5],
|
||||
affected: normalized_affected,
|
||||
action: "Review and update affected components. See NVD for remediation details.",
|
||||
published: .cve.published,
|
||||
references: [.cve.references[]?.url // empty] | unique | .[0:3],
|
||||
|
||||
@@ -39,3 +39,4 @@ __pycache__/
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
clawsec-signing-private.pem
|
||||
|
||||
@@ -4,7 +4,7 @@ export const Footer: React.FC = () => {
|
||||
return (
|
||||
<footer className="text-center py-6 mt-auto">
|
||||
<p className="text-gray-300 text-sm italic">
|
||||
ClawSec is a project by Prompt Security, a SentinelOne company. It's not affiliated with OpenClaw. Designed for security research and agentic workflow hardening.
|
||||
ClawSec is a project by Prompt Security, a SentinelOne company. It's not affiliated with OpenClaw or NanoClaw. Designed for security research and agentic workflow hardening.
|
||||
</p>
|
||||
<div className="flex justify-center gap-4 mt-4">
|
||||
<span className="text-2xl animate-pulse">🦞</span>
|
||||
|
||||
+1
-1
@@ -76,7 +76,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-related vulnerabilities and verified security incidents.
|
||||
This feed is automatically updated with OpenClaw and NanoClaw-related vulnerabilities and verified security incidents.
|
||||
</p>
|
||||
{lastUpdated && (
|
||||
<p className="text-xs text-gray-500">
|
||||
|
||||
+80
-11
@@ -1,14 +1,17 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { User, Bot, Copy, Check } from 'lucide-react';
|
||||
import { User, Bot, Copy, Check, Lock } from 'lucide-react';
|
||||
import { Footer } from '../components/Footer';
|
||||
|
||||
const FILE_NAMES = ['SOUL.md', 'AGENTS.md', 'USER.md', 'TOOLS.md', 'IDENTITY.md', 'HEARTBEAT.md', 'MEMORY.md'];
|
||||
const PLATFORM_NAMES = ['OpenClaw', 'NanoClaw'];
|
||||
const FILE_LOCK_REVEAL_DELAY_MS = 1600;
|
||||
|
||||
export const Home: React.FC = () => {
|
||||
const [isAgent, setIsAgent] = useState(true);
|
||||
const [copiedCurl, setCopiedCurl] = useState(false);
|
||||
const [copiedHuman, setCopiedHuman] = useState(false);
|
||||
const [currentFileIndex, setCurrentFileIndex] = useState(0);
|
||||
const [currentPlatformIndex, setCurrentPlatformIndex] = useState(0);
|
||||
|
||||
const curlCommand = `npx clawhub@latest install clawsec-suite`;
|
||||
|
||||
@@ -20,6 +23,27 @@ export const Home: React.FC = () => {
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Rotate platform names every 4-6 seconds
|
||||
useEffect(() => {
|
||||
let timeoutId: number | undefined;
|
||||
|
||||
const scheduleNextRotation = () => {
|
||||
const delay = 4000 + Math.floor(Math.random() * 2001);
|
||||
timeoutId = window.setTimeout(() => {
|
||||
setCurrentPlatformIndex((prev) => (prev + 1) % PLATFORM_NAMES.length);
|
||||
scheduleNextRotation();
|
||||
}, delay);
|
||||
};
|
||||
|
||||
scheduleNextRotation();
|
||||
|
||||
return () => {
|
||||
if (timeoutId !== undefined) {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const humanInstruction = `Please install clawsec-suite from clawhubnpx clawhub@latest install clawsec-suite`;
|
||||
|
||||
const handleCopyCurl = () => {
|
||||
@@ -44,24 +68,20 @@ export const Home: React.FC = () => {
|
||||
{/* Hero Section */}
|
||||
<section className="text-center space-y-6 max-w-3xl mx-auto mb-12 md:mb-16">
|
||||
<h2 className="text-3xl md:text-4xl tracking-tight text-white">
|
||||
Secure your <span className="text-clawd-accent">OpenClaw</span> agents
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl text-gray-400 leading-relaxed">
|
||||
A complete security skill suite for OpenClaw's family of agents. Protect your{' '}
|
||||
Secure your{' '}
|
||||
<code
|
||||
key={currentFileIndex}
|
||||
className="px-2 py-1 rounded text-clawd-accent inline-block align-baseline relative text-base"
|
||||
key={currentPlatformIndex}
|
||||
className="px-2 py-1 rounded text-clawd-accent inline-block align-baseline relative"
|
||||
style={{
|
||||
width: '165px',
|
||||
minWidth: '9ch',
|
||||
textAlign: 'center',
|
||||
verticalAlign: 'baseline',
|
||||
backgroundColor: 'rgb(30 27 75 / 1)',
|
||||
animation: 'bgFade 0.4s ease-out 1.2s 1 forwards'
|
||||
}}
|
||||
>
|
||||
{FILE_NAMES[currentFileIndex].split('').map((char, index) => (
|
||||
{PLATFORM_NAMES[currentPlatformIndex].split('').map((char, index) => (
|
||||
<span
|
||||
key={`${currentFileIndex}-${index}`}
|
||||
key={`platform-${currentPlatformIndex}-${index}`}
|
||||
className="inline-block"
|
||||
style={{
|
||||
animation: `flipChar 0.3s ease-in-out ${index * 0.05}s 1 forwards`,
|
||||
@@ -73,6 +93,47 @@ export const Home: React.FC = () => {
|
||||
{char}
|
||||
</span>
|
||||
))}
|
||||
</code>{' '}
|
||||
agents
|
||||
</h2>
|
||||
<p className="text-lg md:text-xl text-gray-400 leading-relaxed">
|
||||
A complete security skill suite for OpenClaw and NanoClaw agents. Protect your{' '}
|
||||
<code
|
||||
key={currentFileIndex}
|
||||
className="px-2 py-1 rounded text-clawd-accent inline-block align-baseline relative text-base"
|
||||
style={{
|
||||
width: '188px',
|
||||
textAlign: 'center',
|
||||
verticalAlign: 'baseline',
|
||||
backgroundColor: 'rgb(30 27 75 / 1)',
|
||||
animation: 'bgFade 0.4s ease-out 1.2s 1 forwards'
|
||||
}}
|
||||
>
|
||||
<span className="inline-block w-full pr-5">
|
||||
{FILE_NAMES[currentFileIndex].split('').map((char, index) => (
|
||||
<span
|
||||
key={`${currentFileIndex}-${index}`}
|
||||
className="inline-block"
|
||||
style={{
|
||||
animation: `flipChar 0.3s ease-in-out ${index * 0.05}s 1 forwards`,
|
||||
transformStyle: 'preserve-3d',
|
||||
perspective: '400px',
|
||||
opacity: 0
|
||||
}}
|
||||
>
|
||||
{char}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<Lock
|
||||
size={14}
|
||||
className="text-clawd-accent absolute right-2 top-1/2 -translate-y-1/2"
|
||||
style={{
|
||||
opacity: 0,
|
||||
animation: `lockReveal ${FILE_LOCK_REVEAL_DELAY_MS}ms steps(1, end) 1 forwards`
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</code>
|
||||
{' '}with drift detection, live security recommendations, automated audits, and skill integrity verification. All from one installable suite.
|
||||
</p>
|
||||
@@ -102,6 +163,14 @@ export const Home: React.FC = () => {
|
||||
background-color: rgb(191 107 42 / 0.15);
|
||||
}
|
||||
}
|
||||
@keyframes lockReveal {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
@keyframes mascotHover {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-12px); }
|
||||
|
||||
+37
-17
@@ -12,6 +12,11 @@ const stripFrontmatter = (content: string): string => {
|
||||
return content.replace(frontmatterRegex, '');
|
||||
};
|
||||
|
||||
const isProbablyHtmlDocument = (text: string): boolean => {
|
||||
const start = text.trimStart().slice(0, 200).toLowerCase();
|
||||
return start.startsWith('<!doctype html') || start.startsWith('<html');
|
||||
};
|
||||
|
||||
export const SkillDetail: React.FC = () => {
|
||||
const { skillId } = useParams<{ skillId: string }>();
|
||||
const [skillData, setSkillData] = useState<SkillJson | null>(null);
|
||||
@@ -29,19 +34,44 @@ export const SkillDetail: React.FC = () => {
|
||||
setDoc(null);
|
||||
|
||||
// Fetch skill.json
|
||||
const skillResponse = await fetch(`./skills/${skillId}/skill.json`);
|
||||
const skillResponse = await fetch(`/skills/${skillId}/skill.json`, {
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
if (!skillResponse.ok) {
|
||||
throw new Error('Skill not found');
|
||||
}
|
||||
const skill = await skillResponse.json();
|
||||
|
||||
const skillContentType = skillResponse.headers.get('content-type') ?? '';
|
||||
const skillRaw = await skillResponse.text();
|
||||
if (skillContentType.includes('text/html') || isProbablyHtmlDocument(skillRaw)) {
|
||||
throw new Error('Skill not found');
|
||||
}
|
||||
|
||||
let skill: SkillJson;
|
||||
try {
|
||||
skill = JSON.parse(skillRaw) as SkillJson;
|
||||
} catch {
|
||||
throw new Error('Invalid skill metadata');
|
||||
}
|
||||
|
||||
setSkillData(skill);
|
||||
|
||||
// Fetch checksums.json
|
||||
try {
|
||||
const checksumsResponse = await fetch(`./skills/${skillId}/checksums.json`);
|
||||
const checksumsResponse = await fetch(`/skills/${skillId}/checksums.json`, {
|
||||
headers: { Accept: 'application/json' }
|
||||
});
|
||||
if (checksumsResponse.ok) {
|
||||
const checksumsData = await checksumsResponse.json();
|
||||
setChecksums(checksumsData);
|
||||
const checksumsContentType = checksumsResponse.headers.get('content-type') ?? '';
|
||||
const checksumsRaw = await checksumsResponse.text();
|
||||
if (!checksumsContentType.includes('text/html') && !isProbablyHtmlDocument(checksumsRaw)) {
|
||||
try {
|
||||
const checksumsData = JSON.parse(checksumsRaw) as SkillChecksums;
|
||||
setChecksums(checksumsData);
|
||||
} catch {
|
||||
// Checksums malformed, ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Checksums not available
|
||||
@@ -51,18 +81,8 @@ export const SkillDetail: React.FC = () => {
|
||||
// Note: Dev servers may fall back to serving index.html with 200 for missing files;
|
||||
// guard against accidentally rendering HTML as docs.
|
||||
try {
|
||||
const isProbablyHtmlDocument = (text: string) => {
|
||||
const start = text.trimStart().slice(0, 200).toLowerCase();
|
||||
return start.startsWith('<!doctype html') || start.startsWith('<html');
|
||||
};
|
||||
|
||||
const stripYamlFrontmatter = (text: string) => {
|
||||
const match = text.match(/^---\\s*\\n[\\s\\S]*?\\n---\\s*\\n/);
|
||||
return match ? text.slice(match[0].length) : text;
|
||||
};
|
||||
|
||||
const fetchDocFile = async (filename: string) => {
|
||||
const response = await fetch(`./skills/${skillId}/${filename}`, {
|
||||
const response = await fetch(`/skills/${skillId}/${filename}`, {
|
||||
headers: { Accept: 'text/plain' }
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
@@ -73,7 +93,7 @@ export const SkillDetail: React.FC = () => {
|
||||
if (contentType.includes('text/html') || isProbablyHtmlDocument(rawText)) return null;
|
||||
|
||||
const text =
|
||||
filename === 'SKILL.md' ? stripYamlFrontmatter(rawText).trim() : rawText.trim();
|
||||
filename === 'SKILL.md' ? stripFrontmatter(rawText).trim() : rawText.trim();
|
||||
|
||||
return text.length > 0 ? text : null;
|
||||
};
|
||||
|
||||
+52
-5
@@ -4,6 +4,27 @@ import { SkillCard } from '../components/SkillCard';
|
||||
import { Footer } from '../components/Footer';
|
||||
import type { SkillMetadata, SkillsIndex } from '../types';
|
||||
|
||||
const SKILLS_INDEX_PATH = '/skills/index.json';
|
||||
|
||||
const isProbablyHtmlDocument = (text: string): boolean => {
|
||||
const start = text.trimStart().slice(0, 200).toLowerCase();
|
||||
return start.startsWith('<!doctype html') || start.startsWith('<html');
|
||||
};
|
||||
|
||||
const parseSkillsIndex = (raw: string): SkillsIndex | null => {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as Partial<SkillsIndex> | null;
|
||||
if (!parsed || !Array.isArray(parsed.skills)) return null;
|
||||
return {
|
||||
version: typeof parsed.version === 'string' ? parsed.version : '1.0.0',
|
||||
updated: typeof parsed.updated === 'string' ? parsed.updated : '',
|
||||
skills: parsed.skills as SkillMetadata[],
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const SkillsCatalog: React.FC = () => {
|
||||
const [skills, setSkills] = useState<SkillMetadata[]>([]);
|
||||
const [filteredSkills, setFilteredSkills] = useState<SkillMetadata[]>([]);
|
||||
@@ -15,15 +36,41 @@ export const SkillsCatalog: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const fetchSkills = async () => {
|
||||
try {
|
||||
const response = await fetch('./skills/index.json');
|
||||
const response = await fetch(SKILLS_INDEX_PATH, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
|
||||
// Missing index file is a valid "empty catalog" state.
|
||||
if (response.status === 404) {
|
||||
setSkills([]);
|
||||
setFilteredSkills([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch skills index');
|
||||
}
|
||||
const data: SkillsIndex = await response.json();
|
||||
setSkills(data.skills || []);
|
||||
setFilteredSkills(data.skills || []);
|
||||
|
||||
const contentType = response.headers.get('content-type') ?? '';
|
||||
const raw = await response.text();
|
||||
|
||||
// Some SPA setups return index.html with 200 for missing JSON files.
|
||||
if (!raw.trim() || contentType.includes('text/html') || isProbablyHtmlDocument(raw)) {
|
||||
setSkills([]);
|
||||
setFilteredSkills([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = parseSkillsIndex(raw);
|
||||
if (!data) {
|
||||
throw new Error('Invalid skills index format');
|
||||
}
|
||||
|
||||
setSkills(data.skills);
|
||||
setFilteredSkills(data.skills);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load skills');
|
||||
console.error('Failed to load skills index:', err);
|
||||
setError('Failed to load skills catalog');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
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"
|
||||
KEYWORDS="OpenClaw clawdbot Moltbot"
|
||||
GITHUB_REF_PATTERN="github.com/openclaw/openclaw"
|
||||
KEYWORDS="OpenClaw clawdbot Moltbot NanoClaw WhatsApp-bot baileys"
|
||||
GITHUB_REF_PATTERN="github.com/openclaw/openclaw github.com/qwibitai/NanoClaw"
|
||||
|
||||
# Parse args
|
||||
DAYS_BACK=120
|
||||
@@ -128,7 +128,7 @@ 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"
|
||||
KEYWORDS_PATTERN="OpenClaw|clawdbot|Moltbot|openclaw|NanoClaw|nanoclaw|WhatsApp-bot|baileys"
|
||||
|
||||
jq --arg kw "$KEYWORDS_PATTERN" --arg gh "$GITHUB_REF_PATTERN" '
|
||||
[.[] | select(
|
||||
@@ -236,6 +236,36 @@ jq --argjson existing "$EXISTING_JSON" '
|
||||
else (cwe_name_map($id) // ("unknown_cwe_" + $id))
|
||||
end
|
||||
);
|
||||
|
||||
def cpe_criteria:
|
||||
(
|
||||
[.cve.configurations[]? | .. | objects | .criteria? | strings | select(startswith("cpe:2.3:"))]
|
||||
| unique
|
||||
);
|
||||
|
||||
def inferred_targets:
|
||||
(
|
||||
(
|
||||
[
|
||||
(.cve.descriptions[]? | select(.lang == "en") | .value),
|
||||
(.cve.references[]?.url // empty)
|
||||
]
|
||||
| map(strings | ascii_downcase)
|
||||
| join(" ")
|
||||
) 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)
|
||||
)
|
||||
);
|
||||
|
||||
def normalized_affected:
|
||||
(
|
||||
(cpe_criteria + inferred_targets)
|
||||
| unique
|
||||
| .[0:5]
|
||||
| if length == 0 then ["openclaw@*", "nanoclaw@*"] else . end
|
||||
);
|
||||
|
||||
[.[] |
|
||||
select(.cve.id as $id | $existing | index($id) | not) |
|
||||
@@ -246,7 +276,7 @@ jq --argjson existing "$EXISTING_JSON" '
|
||||
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),
|
||||
affected: [.cve.configurations[]?.nodes[]?.cpeMatch[]?.criteria // empty] | unique | .[0:5],
|
||||
affected: normalized_affected,
|
||||
action: "Review and update affected components. See NVD for remediation details.",
|
||||
published: .cve.published,
|
||||
references: [.cve.references[]?.url // empty] | unique | .[0:3],
|
||||
@@ -298,7 +328,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-related CVEs from NVD.",
|
||||
description: "Community-driven security advisory feed for ClawSec. Automatically updated with OpenClaw and NanoClaw-related CVEs from NVD.",
|
||||
advisories: ($advisories | sort_by(.published) | reverse)
|
||||
}' > "$TEMP_DIR/updated_feed.json"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user