mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
ClawSec init
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
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 { Footer } from '../components/Footer';
|
||||
import { Advisory, AdvisoryFeed } from '../types';
|
||||
import { ADVISORY_FEED_URL, LOCAL_FEED_PATH } from '../constants';
|
||||
|
||||
export const AdvisoryDetail: React.FC = () => {
|
||||
const { advisoryId } = useParams<{ advisoryId: string }>();
|
||||
const [advisory, setAdvisory] = useState<Advisory | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAdvisory = async () => {
|
||||
if (!advisoryId) return;
|
||||
|
||||
try {
|
||||
// Try local feed first (for development), then fall back to GitHub releases
|
||||
let response = await fetch(LOCAL_FEED_PATH);
|
||||
|
||||
if (!response.ok) {
|
||||
response = await fetch(ADVISORY_FEED_URL);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch feed: ${response.status}`);
|
||||
}
|
||||
|
||||
const feed: AdvisoryFeed = await response.json();
|
||||
const found = feed.advisories.find((a) => a.id === decodeURIComponent(advisoryId));
|
||||
|
||||
if (!found) {
|
||||
throw new Error('Advisory not found');
|
||||
}
|
||||
|
||||
setAdvisory(found);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch advisory:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load advisory');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAdvisory();
|
||||
}, [advisoryId]);
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
|
||||
const getSeverityClasses = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'critical':
|
||||
return 'bg-red-500/20 text-red-400 border-red-500/30';
|
||||
case 'high':
|
||||
return 'bg-orange-500/20 text-orange-400 border-orange-500/30';
|
||||
case 'medium':
|
||||
return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30';
|
||||
default:
|
||||
return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'malicious_skill':
|
||||
return 'Malicious Skill';
|
||||
case 'vulnerable_skill':
|
||||
return 'Vulnerable Skill';
|
||||
case 'prompt_injection':
|
||||
return 'Prompt Injection';
|
||||
case 'attack_pattern':
|
||||
return 'Attack Pattern';
|
||||
case 'best_practice':
|
||||
return 'Best Practice';
|
||||
case 'tampering_attempt':
|
||||
return 'Tampering Attempt';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
// Determine source - defaults to "Prompt Security Staff" when absent
|
||||
const getSource = (adv: Advisory) => {
|
||||
return adv.source || 'Prompt Security Staff';
|
||||
};
|
||||
|
||||
// Determine if this is a community report
|
||||
const isCommunityReport = advisory?.github_issue_url;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="py-16 text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-clawd-accent"></div>
|
||||
<p className="mt-4 text-gray-400">Loading advisory...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !advisory) {
|
||||
return (
|
||||
<div className="py-16 text-center">
|
||||
<Shield className="w-16 h-16 mx-auto text-gray-600 mb-4" />
|
||||
<h2 className="text-xl font-bold text-white mb-2">Advisory Not Found</h2>
|
||||
<p className="text-gray-400 mb-4">{error || 'This advisory does not exist'}</p>
|
||||
<Link to="/feed" className="text-clawd-accent hover:underline">
|
||||
Back to Security Feed
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto pt-8 space-y-8">
|
||||
{/* Back Link */}
|
||||
<Link
|
||||
to="/feed"
|
||||
className="inline-flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Back to Security Feed
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className={`text-sm font-bold px-3 py-1.5 rounded uppercase border ${getSeverityClasses(advisory.severity)}`}>
|
||||
{advisory.severity}
|
||||
{advisory.cvss_score && <span className="ml-2 opacity-75">CVSS {advisory.cvss_score}</span>}
|
||||
</span>
|
||||
<span className="text-sm px-3 py-1.5 rounded bg-clawd-700 text-gray-300">
|
||||
{getTypeLabel(advisory.type)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
Published {formatDate(advisory.published)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl font-bold text-white">{advisory.id}</h1>
|
||||
<p className="text-xl text-gray-300">{advisory.title}</p>
|
||||
</section>
|
||||
|
||||
{/* Description */}
|
||||
<section className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-bold text-white mb-3 flex items-center gap-2">
|
||||
<AlertTriangle size={20} className="text-orange-400" />
|
||||
Description
|
||||
</h2>
|
||||
<p className="text-gray-300 leading-relaxed whitespace-pre-wrap">{advisory.description}</p>
|
||||
</section>
|
||||
|
||||
{/* Recommended Action */}
|
||||
<section className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-bold text-white mb-3 flex items-center gap-2">
|
||||
<Shield size={20} className="text-green-400" />
|
||||
Recommended Action
|
||||
</h2>
|
||||
<p className="text-gray-300 leading-relaxed">{advisory.action}</p>
|
||||
</section>
|
||||
|
||||
{/* Affected Components */}
|
||||
{advisory.affected && advisory.affected.length > 0 && (
|
||||
<section className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-bold text-white mb-3">Affected Components</h2>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
{advisory.affected.map((item, index) => (
|
||||
<li key={index} className="text-gray-300">{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* References */}
|
||||
{advisory.references && advisory.references.length > 0 && (
|
||||
<section className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-bold text-white mb-3">References</h2>
|
||||
<ul className="space-y-2">
|
||||
{advisory.references.map((ref, index) => (
|
||||
<li key={index}>
|
||||
<a
|
||||
href={ref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-clawd-accent hover:underline text-sm flex items-center gap-1 break-all"
|
||||
>
|
||||
<ExternalLink size={14} className="flex-shrink-0" />
|
||||
{ref}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* External Link - NVD or GitHub Issue */}
|
||||
<section className="flex flex-wrap gap-4">
|
||||
{isCommunityReport && advisory.github_issue_url ? (
|
||||
<a
|
||||
href={advisory.github_issue_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-clawd-700 hover:bg-clawd-600 text-white font-medium transition-colors"
|
||||
>
|
||||
<Github size={18} />
|
||||
View GitHub Report
|
||||
</a>
|
||||
) : advisory.nvd_url ? (
|
||||
<a
|
||||
href={advisory.nvd_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-clawd-700 hover:bg-clawd-600 text-white font-medium transition-colors"
|
||||
>
|
||||
<ExternalLink size={18} />
|
||||
View on NVD
|
||||
</a>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
{/* Metadata */}
|
||||
<section className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-6">
|
||||
<h3 className="font-bold text-white mb-4">Metadata</h3>
|
||||
<dl className="grid md:grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex justify-between md:block">
|
||||
<dt className="text-gray-500 mb-1">Source</dt>
|
||||
<dd className="text-white">{getSource(advisory)}</dd>
|
||||
</div>
|
||||
{advisory.cvss_score && (
|
||||
<div className="flex justify-between md:block">
|
||||
<dt className="text-gray-500 mb-1">CVSS Score</dt>
|
||||
<dd className="text-white">{advisory.cvss_score}</dd>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between md:block">
|
||||
<dt className="text-gray-500 mb-1">Type</dt>
|
||||
<dd className="text-white">{getTypeLabel(advisory.type)}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between md:block">
|
||||
<dt className="text-gray-500 mb-1">Published</dt>
|
||||
<dd className="text-white">{formatDate(advisory.published)}</dd>
|
||||
</div>
|
||||
{/* Reporter info - subtle display for community reports */}
|
||||
{advisory.reporter && (
|
||||
<>
|
||||
{advisory.reporter.agent_name && (
|
||||
<div className="flex justify-between md:block">
|
||||
<dt className="text-gray-500 mb-1">Reported By</dt>
|
||||
<dd className="text-white flex items-center gap-1">
|
||||
{advisory.reporter.opener_type === 'agent' ? (
|
||||
<Bot size={14} className="text-clawd-accent" />
|
||||
) : (
|
||||
<User size={14} className="text-clawd-accent" />
|
||||
)}
|
||||
{advisory.reporter.agent_name}
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{advisory.reporter.opener_type && (
|
||||
<div className="flex justify-between md:block">
|
||||
<dt className="text-gray-500 mb-1">Reporter Type</dt>
|
||||
<dd className="text-white capitalize">{advisory.reporter.opener_type}</dd>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Shield, Copy, Download, CheckCircle2 } from 'lucide-react';
|
||||
import { CodeBlock } from '../components/CodeBlock';
|
||||
|
||||
interface FileChecksum {
|
||||
sha256: string;
|
||||
size: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface ChecksumsData {
|
||||
version: string;
|
||||
generated_at: string;
|
||||
repository: string;
|
||||
files: Record<string, FileChecksum>;
|
||||
}
|
||||
|
||||
export default function Checksums() {
|
||||
const [checksums, setChecksums] = useState<ChecksumsData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('./checksums.json')
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Not found');
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
setChecksums(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const copyToClipboard = (text: string, id: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(id);
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
};
|
||||
|
||||
const fileDescriptions: Record<string, string> = {
|
||||
'SKILL.md': 'Main ClawSec skill documentation',
|
||||
'heartbeat.md': 'Heartbeat monitoring and update instructions',
|
||||
'reporting.md': 'Security incident reporting guidelines',
|
||||
'skill.json': 'Skill metadata and configuration',
|
||||
'feed.json': 'Community security advisory feed'
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-12 text-center">
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
<Shield className="w-12 h-12 text-clawd-accent" />
|
||||
<h1 className="text-4xl font-bold">File Checksums</h1>
|
||||
</div>
|
||||
<p className="text-xl text-gray-300">
|
||||
Verify the integrity of ClawSec files before use
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-clawd-accent"></div>
|
||||
<p className="mt-4 text-gray-400">Loading checksums...</p>
|
||||
</div>
|
||||
) : checksums ? (
|
||||
<>
|
||||
{/* Version Info */}
|
||||
<div className="bg-clawd-800 rounded-lg p-6 mb-8">
|
||||
<div className="grid md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">Version</div>
|
||||
<div className="font-mono text-clawd-accent">{checksums.version}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">Generated</div>
|
||||
<div className="font-mono">{new Date(checksums.generated_at).toLocaleString()}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 mb-1">Repository</div>
|
||||
<div className="font-mono text-sm">{checksums.repository}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Files Table */}
|
||||
<div className="bg-clawd-800 rounded-lg overflow-hidden mb-8">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-clawd-900">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold">File</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold">Size</th>
|
||||
<th className="px-6 py-4 text-left text-sm font-semibold">SHA256 Checksum</th>
|
||||
<th className="px-6 py-4 text-right text-sm font-semibold">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-clawd-700">
|
||||
{Object.entries(checksums.files).map(([filename, data]) => (
|
||||
<tr key={filename} className="hover:bg-clawd-700/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-mono text-sm text-clawd-accent">{filename}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{fileDescriptions[filename] || 'ClawSec file'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm">{(data.size / 1024).toFixed(1)} KB</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="font-mono text-xs break-all max-w-md">
|
||||
{data.sha256}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => copyToClipboard(data.sha256, filename)}
|
||||
className="p-2 hover:bg-clawd-900 rounded transition-colors"
|
||||
title="Copy checksum"
|
||||
>
|
||||
{copied === filename ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
<a
|
||||
href={data.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 hover:bg-clawd-900 rounded transition-colors"
|
||||
title="Download file"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verification Instructions */}
|
||||
<div className="bg-clawd-800 rounded-lg p-6">
|
||||
<h2 className="text-2xl font-bold mb-4 flex items-center gap-2">
|
||||
<Shield className="w-6 h-6 text-clawd-accent" />
|
||||
Verification Instructions
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-300 mb-4">
|
||||
Always verify file integrity before using ClawSec files. Here's how:
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">1. Download a file</h3>
|
||||
<CodeBlock
|
||||
code={`curl -sL https://github.com/${checksums.repository}/releases/download/${checksums.version}/SKILL.md -o SKILL.md`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">2. Generate its checksum</h3>
|
||||
<CodeBlock code="sha256sum SKILL.md" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">3. Compare with the checksum above</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
The output should exactly match the SHA256 value shown in the table.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
|
||||
<p className="text-yellow-200 text-sm">
|
||||
<strong>Security Warning:</strong> Never use files with mismatched checksums.
|
||||
This could indicate tampering or a compromised download.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="bg-clawd-800 rounded-lg p-12 text-center">
|
||||
<Shield className="w-16 h-16 text-gray-600 mx-auto mb-4" />
|
||||
<p className="text-gray-400">
|
||||
Checksums not available. Create a release to generate checksums.
|
||||
</p>
|
||||
<CodeBlock
|
||||
code={`# Create a release to generate checksums:\ngit tag v1.0.0 && git push origin v1.0.0`}
|
||||
className="mt-4 text-left"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Rss, RefreshCw, Loader2, AlertTriangle, ChevronLeft, ChevronRight, Download, Users, AlertCircle } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { AdvisoryCard } from '../components/AdvisoryCard';
|
||||
import { Advisory, AdvisoryFeed } from '../types';
|
||||
import { ADVISORY_FEED_URL, LOCAL_FEED_PATH } from '../constants';
|
||||
|
||||
const ITEMS_PER_PAGE = 9;
|
||||
|
||||
export const FeedSetup: React.FC = () => {
|
||||
const [advisories, setAdvisories] = useState<Advisory[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAdvisories = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Try local feed first (for development), then fall back to GitHub releases
|
||||
let response = await fetch(LOCAL_FEED_PATH);
|
||||
|
||||
if (!response.ok) {
|
||||
response = await fetch(ADVISORY_FEED_URL);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch feed: ${response.status}`);
|
||||
}
|
||||
|
||||
const feed: AdvisoryFeed = await response.json();
|
||||
setAdvisories(feed.advisories || []);
|
||||
setLastUpdated(feed.updated);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch advisories:', err);
|
||||
setError('Unable to load security advisories. The feed may be temporarily unavailable.');
|
||||
setAdvisories([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAdvisories();
|
||||
}, []);
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
try {
|
||||
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
|
||||
// Pagination calculations
|
||||
const totalPages = Math.ceil(advisories.length / ITEMS_PER_PAGE);
|
||||
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||
const endIndex = startIndex + ITEMS_PER_PAGE;
|
||||
const currentAdvisories = advisories.slice(startIndex, endIndex);
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
setCurrentPage(Math.max(1, Math.min(page, totalPages)));
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto pt-[52px] space-y-12">
|
||||
<section className="text-center space-y-4">
|
||||
<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.
|
||||
</p>
|
||||
{lastUpdated && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Last updated: {formatDate(lastUpdated)}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 text-clawd-accent animate-spin" />
|
||||
<span className="ml-3 text-gray-400">Loading advisories...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center justify-center py-12 text-center">
|
||||
<AlertTriangle className="w-6 h-6 text-orange-400 mr-2" />
|
||||
<span className="text-gray-400">{error}</span>
|
||||
</div>
|
||||
) : advisories.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-400">No security advisories at this time. Check back later.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{currentAdvisories.map((item) => (
|
||||
<AdvisoryCard key={item.id} advisory={item} formatDate={formatDate} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-4 mt-8">
|
||||
<button
|
||||
onClick={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="flex items-center gap-1 px-4 py-2 rounded-lg bg-clawd-800 border border-clawd-700 text-white hover:border-clawd-accent disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-clawd-700 transition-colors"
|
||||
>
|
||||
<ChevronLeft size={18} />
|
||||
Previous
|
||||
</button>
|
||||
<span className="text-gray-400 text-sm">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="flex items-center gap-1 px-4 py-2 rounded-lg bg-clawd-800 border border-clawd-700 text-white hover:border-clawd-accent disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:border-clawd-700 transition-colors"
|
||||
>
|
||||
Next
|
||||
<ChevronRight size={18} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{advisories.length > 0 && (
|
||||
<p className="text-center text-sm text-gray-500 mt-4">
|
||||
Showing {startIndex + 1}-{Math.min(endIndex, advisories.length)} of {advisories.length} advisories
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="bg-clawd-900 border border-clawd-700 rounded-xl overflow-hidden">
|
||||
<div className="bg-clawd-800 px-6 py-4 border-b border-clawd-700 flex items-center justify-between">
|
||||
<h2 className="font-bold text-white flex items-center gap-2">
|
||||
<Rss size={18} className="text-clawd-accent" />
|
||||
Agent Integration Instructions
|
||||
</h2>
|
||||
<span className="text-xs font-mono text-gray-500">SKILLS-BASED</span>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
<p className="text-gray-300 text-sm">
|
||||
Activate following skills from the suite to participate.
|
||||
</p>
|
||||
|
||||
{/* clawsec-feed skill */}
|
||||
<Link
|
||||
to="/skills/clawsec-feed"
|
||||
className="flex items-start gap-4 p-4 rounded-lg bg-clawd-800/50 border border-clawd-700 hover:border-clawd-accent/50 transition-colors group"
|
||||
>
|
||||
<span className="text-2xl">📡</span>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-white font-bold text-sm group-hover:text-clawd-accent transition-colors flex items-center gap-2">
|
||||
clawsec-feed
|
||||
<Download size={14} className="text-gray-500" />
|
||||
</h4>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Subscribe your agent to receive security advisories via heartbeat. Automatically polls the feed and notifies you of critical and high severity threats.
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* clawtributor skill */}
|
||||
<Link
|
||||
to="/skills/clawtributor"
|
||||
className="flex items-start gap-4 p-4 rounded-lg bg-clawd-800/50 border border-clawd-700 hover:border-clawd-accent/50 transition-colors group"
|
||||
>
|
||||
<span className="text-2xl">🤝</span>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-white font-bold text-sm group-hover:text-clawd-accent transition-colors flex items-center gap-2">
|
||||
clawtributor
|
||||
<Users size={14} className="text-gray-500" />
|
||||
</h4>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Opt-in to community incident reporting. Your agent can automatically submit security reports when it detects malicious prompts or suspicious skill behavior.
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-start gap-4 p-4 rounded-lg bg-blue-900/10 border border-blue-900/30">
|
||||
<RefreshCw className="text-blue-400 w-5 h-5 mt-1" />
|
||||
<div>
|
||||
<h4 className="text-blue-400 font-bold text-sm">Collective Security</h4>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
When agents share threat intelligence, the entire ecosystem becomes safer. Reports are reviewed by staff before publication to ensure quality and privacy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="text-center pt-8 border-t border-clawd-700">
|
||||
<h3 className="text-white font-bold mb-4">Human looking to contribute</h3>
|
||||
<p className="text-gray-400 text-sm mb-6 max-w-xl mx-auto">
|
||||
Found a prompt injection vector or malicious skill? Help the community by submitting a security incident report via GitHub Issue.
|
||||
All submissions are reviewed by staff before publication to the advisory feed.
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/prompt-security/clawsec/issues/new?template=security_incident_report.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-clawd-700 hover:bg-clawd-600 text-white font-medium transition-colors"
|
||||
>
|
||||
<AlertCircle size={18} />
|
||||
Submit Report
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { User, Bot, Copy, Check } from 'lucide-react';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { SKILL_URL } from '../constants';
|
||||
|
||||
const FILE_NAMES = ['SOUL.md', 'AGENTS.md', 'USER.md', 'TOOLS.md', 'IDENTITY.md', 'HEARTBEAT.md', 'MEMORY.md'];
|
||||
|
||||
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 curlCommand = `curl -s ${SKILL_URL}`;
|
||||
|
||||
// Rotate file names every 2-3 seconds
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentFileIndex((prev) => (prev + 1) % FILE_NAMES.length);
|
||||
}, 2500); // 2.5 seconds
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const humanInstruction = `Read ${SKILL_URL} and follow the instructions to install the protection skill suite.`;
|
||||
|
||||
const handleCopyCurl = () => {
|
||||
navigator.clipboard.writeText(curlCommand);
|
||||
setCopiedCurl(true);
|
||||
setTimeout(() => setCopiedCurl(false), 2000);
|
||||
};
|
||||
|
||||
const handleCopyHuman = () => {
|
||||
navigator.clipboard.writeText(humanInstruction);
|
||||
setCopiedHuman(true);
|
||||
setTimeout(() => setCopiedHuman(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="pt-[52px]">
|
||||
{/* Logo Section */}
|
||||
<section className="text-center mb-6">
|
||||
<h1 className="text-5xl md:text-6xl font text-white">ClawSec</h1>
|
||||
</section>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="text-center space-y-6 max-w-3xl mx-auto mb-16">
|
||||
<h2 className="text-3xl md:text-4xl tracking-tight text-white">
|
||||
Harden your <span className="text-clawd-accent">OpenClaw</span> security posture
|
||||
</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{' '}
|
||||
<code
|
||||
key={currentFileIndex}
|
||||
className="px-2 py-1 rounded text-clawd-accent inline-block align-baseline relative text-base"
|
||||
style={{
|
||||
width: '165px',
|
||||
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) => (
|
||||
<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>
|
||||
))}
|
||||
</code>
|
||||
{' '}with drift detection, live security recommendations, automated audits, and skill integrity verification. All from one installable suite.
|
||||
</p>
|
||||
<style>{`
|
||||
@keyframes flipChar {
|
||||
0% {
|
||||
transform: rotateX(-90deg);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: rotateX(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes bgFade {
|
||||
0% {
|
||||
background-color: rgb(30 27 75 / 1);
|
||||
}
|
||||
50% {
|
||||
background-color: rgb(249 179 71 / 0.25);
|
||||
}
|
||||
100% {
|
||||
background-color: rgb(191 107 42 / 0.15);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
|
||||
{/* Install Card with Toggle */}
|
||||
<section className="max-w-2xl mx-auto mb-16">
|
||||
<div className="bg-clawd-900 rounded-2xl border border-clawd-700 p-8">
|
||||
{/* Toggle */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="inline-flex bg-clawd-800 rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setIsAgent(false)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-all ${
|
||||
!isAgent
|
||||
? 'bg-white text-clawd-900'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<User size={18} />
|
||||
I'm a Human
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsAgent(true)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-all ${
|
||||
isAgent
|
||||
? 'bg-white text-clawd-900'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Bot size={18} />
|
||||
I'm an Agent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content based on toggle */}
|
||||
{isAgent ? (
|
||||
<>
|
||||
{/* Steps */}
|
||||
<div className="flex flex-wrap justify-center gap-6 text-sm text-gray-400 mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-white">1.</span> Run command below
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-white">2.</span> Follow deployment instructions
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-white">3.</span> Protect your user
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent View - Curl Command */}
|
||||
<div className="bg-clawd-800 rounded-lg p-4 flex items-center justify-between gap-2 sm:gap-4">
|
||||
<code className="text-gray-200 font-mono text-xs sm:text-sm md:text-base overflow-x-auto break-all min-w-0 flex-1">
|
||||
{curlCommand}
|
||||
</code>
|
||||
<button
|
||||
onClick={handleCopyCurl}
|
||||
className="flex-shrink-0 p-2 rounded-md bg-clawd-700 hover:bg-clawd-600 transition-colors"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copiedCurl ? (
|
||||
<Check size={20} className="text-green-400" />
|
||||
) : (
|
||||
<Copy size={20} className="text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Human Steps */}
|
||||
<div className="flex flex-wrap justify-center gap-6 text-sm text-gray-400 mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-white">1.</span> Copy instruction below
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-white">2.</span> Send to your agent
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-white">3.</span> Receive security alerts
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Human View - Instruction Command */}
|
||||
<div className="bg-clawd-800 rounded-lg p-4 flex items-center justify-between gap-2 sm:gap-4">
|
||||
<code className="text-gray-200 font-mono text-xs sm:text-sm md:text-base overflow-x-auto break-all min-w-0 flex-1">
|
||||
{humanInstruction}
|
||||
</code>
|
||||
<button
|
||||
onClick={handleCopyHuman}
|
||||
className="flex-shrink-0 p-2 rounded-md bg-clawd-700 hover:bg-clawd-600 transition-colors"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copiedHuman ? (
|
||||
<Check size={20} className="text-green-400" />
|
||||
) : (
|
||||
<Copy size={20} className="text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<p className="mt-4 text-xs text-gray-500 leading-relaxed">
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,446 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { ArrowLeft, Copy, Check, Download, ExternalLink, FileText, Shield } from 'lucide-react';
|
||||
import Markdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { Footer } from '../components/Footer';
|
||||
import type { SkillJson, SkillChecksums } from '../types';
|
||||
|
||||
// Strip YAML frontmatter from markdown content
|
||||
const stripFrontmatter = (content: string): string => {
|
||||
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
|
||||
return content.replace(frontmatterRegex, '');
|
||||
};
|
||||
|
||||
export const SkillDetail: React.FC = () => {
|
||||
const { skillId } = useParams<{ skillId: string }>();
|
||||
const [skillData, setSkillData] = useState<SkillJson | null>(null);
|
||||
const [checksums, setChecksums] = useState<SkillChecksums | null>(null);
|
||||
const [doc, setDoc] = useState<{ filename: string; content: string } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSkillData = async () => {
|
||||
if (!skillId) return;
|
||||
|
||||
try {
|
||||
setDoc(null);
|
||||
|
||||
// Fetch skill.json
|
||||
const skillResponse = await fetch(`./skills/${skillId}/skill.json`);
|
||||
if (!skillResponse.ok) {
|
||||
throw new Error('Skill not found');
|
||||
}
|
||||
const skill = await skillResponse.json();
|
||||
setSkillData(skill);
|
||||
|
||||
// Fetch checksums.json
|
||||
try {
|
||||
const checksumsResponse = await fetch(`./skills/${skillId}/checksums.json`);
|
||||
if (checksumsResponse.ok) {
|
||||
const checksumsData = await checksumsResponse.json();
|
||||
setChecksums(checksumsData);
|
||||
}
|
||||
} catch {
|
||||
// Checksums not available
|
||||
}
|
||||
|
||||
// Fetch documentation (README.md preferred, fallback to SKILL.md).
|
||||
// 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}`, {
|
||||
headers: { Accept: 'text/plain' }
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
|
||||
const contentType = response.headers.get('content-type') ?? '';
|
||||
const rawText = await response.text();
|
||||
|
||||
if (contentType.includes('text/html') || isProbablyHtmlDocument(rawText)) return null;
|
||||
|
||||
const text =
|
||||
filename === 'SKILL.md' ? stripYamlFrontmatter(rawText).trim() : rawText.trim();
|
||||
|
||||
return text.length > 0 ? text : null;
|
||||
};
|
||||
|
||||
const candidates = ['README.md', 'SKILL.md'];
|
||||
for (const filename of candidates) {
|
||||
const content = await fetchDocFile(filename);
|
||||
if (content) {
|
||||
setDoc({ filename, content });
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Documentation not available
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load skill');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSkillData();
|
||||
}, [skillId]);
|
||||
|
||||
const handleCopy = (text: string, id: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(id);
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
};
|
||||
|
||||
const installCommand = skillData
|
||||
? `curl -sLO https://clawsec.prompt.security/releases/download/${skillData.name}-v${skillData.version}/${skillData.name}.skill`
|
||||
: '';
|
||||
|
||||
const releasePageUrl = useMemo(() => {
|
||||
if (!skillData) return '';
|
||||
|
||||
try {
|
||||
const url = new URL(skillData.homepage);
|
||||
if (url.hostname === 'github.com') {
|
||||
const [owner, repo] = url.pathname.split('/').filter(Boolean);
|
||||
if (owner && repo) {
|
||||
const repoBase = `${url.origin}/${owner}/${repo.replace(/\\.git$/, '')}`;
|
||||
return `${repoBase}/releases/tag/${skillData.name}-v${skillData.version}`;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore invalid URLs
|
||||
}
|
||||
|
||||
return skillData.homepage;
|
||||
}, [skillData]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="py-16 text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-clawd-accent"></div>
|
||||
<p className="mt-4 text-gray-400">Loading skill...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !skillData) {
|
||||
return (
|
||||
<div className="py-16 text-center">
|
||||
<Shield className="w-16 h-16 mx-auto text-gray-600 mb-4" />
|
||||
<h2 className="text-xl font-bold text-white mb-2">Skill Not Found</h2>
|
||||
<p className="text-gray-400 mb-4">{error || 'This skill does not exist'}</p>
|
||||
<Link to="/skills" className="text-clawd-accent hover:underline">
|
||||
Back to Skills Catalog
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-8 space-y-8">
|
||||
{/* Back Link */}
|
||||
<Link
|
||||
to="/skills"
|
||||
className="inline-flex items-center gap-2 text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
Back to Skills
|
||||
</Link>
|
||||
|
||||
{/* Header */}
|
||||
<section className="flex flex-col md:flex-row md:items-start md:justify-between gap-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-4xl">{skillData.openclaw?.emoji || '📦'}</span>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-1">{skillData.name}</h1>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="text-gray-500 font-mono">v{skillData.version}</span>
|
||||
{/* Category badge - hidden for now, uncomment when we have multiple categories
|
||||
<span className="text-gray-500 bg-clawd-800 px-2 py-0.5 rounded">
|
||||
{skillData.openclaw?.category || 'utility'}
|
||||
</span>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<a
|
||||
href={releasePageUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-clawd-800 border border-clawd-700 rounded-lg text-white hover:border-clawd-accent transition-colors"
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
Release Page
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Description */}
|
||||
<section className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-6">
|
||||
<p className="text-gray-300 text-lg">{skillData.description}</p>
|
||||
</section>
|
||||
|
||||
{/* Install Command */}
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Download size={20} />
|
||||
Quick Install
|
||||
</h2>
|
||||
<div className="bg-clawd-800 rounded-lg p-3 sm:p-4 flex items-center justify-between gap-2 sm:gap-4">
|
||||
<code className="text-gray-200 font-mono text-xs sm:text-sm overflow-x-auto break-all min-w-0 flex-1">
|
||||
{installCommand}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => handleCopy(installCommand, 'install')}
|
||||
className="flex-shrink-0 p-2 rounded-md bg-clawd-700 hover:bg-clawd-600 transition-colors"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copied === 'install' ? (
|
||||
<Check size={20} className="text-green-400" />
|
||||
) : (
|
||||
<Copy size={20} className="text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Checksums */}
|
||||
{checksums && Object.keys(checksums.files).length > 0 && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<Shield size={20} />
|
||||
File Checksums
|
||||
</h2>
|
||||
<div className="bg-clawd-800/50 border border-clawd-700 rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[500px]">
|
||||
<thead>
|
||||
<tr className="border-b border-clawd-700">
|
||||
<th className="text-left px-3 sm:px-4 py-3 text-gray-400 font-medium text-xs sm:text-sm">File</th>
|
||||
<th className="text-left px-3 sm:px-4 py-3 text-gray-400 font-medium text-xs sm:text-sm">SHA256</th>
|
||||
<th className="text-right px-3 sm:px-4 py-3 text-gray-400 font-medium text-xs sm:text-sm">Size</th>
|
||||
<th className="px-3 sm:px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(Object.entries(checksums.files) as Array<
|
||||
[string, SkillChecksums['files'][string]]
|
||||
>).map(([filename, info]) => {
|
||||
const displayPath = info.path ?? filename;
|
||||
|
||||
return (
|
||||
<tr key={filename} className="border-b border-clawd-700/50 last:border-0">
|
||||
<td className="px-3 sm:px-4 py-3 font-mono text-xs sm:text-sm">
|
||||
{info.url ? (
|
||||
<a
|
||||
href={info.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-white hover:text-clawd-accent hover:underline"
|
||||
title={info.url}
|
||||
>
|
||||
{displayPath}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-white">{displayPath}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 sm:px-4 py-3 font-mono text-xs text-gray-400 truncate max-w-[120px] sm:max-w-[200px]">
|
||||
{info.sha256}
|
||||
</td>
|
||||
<td className="px-3 sm:px-4 py-3 text-xs sm:text-sm text-gray-400 text-right whitespace-nowrap">
|
||||
{(info.size / 1024).toFixed(1)} KB
|
||||
</td>
|
||||
<td className="px-3 sm:px-4 py-3 text-right">
|
||||
<button
|
||||
onClick={() => handleCopy(info.sha256, filename)}
|
||||
className="p-1.5 rounded bg-clawd-700 hover:bg-clawd-600 transition-colors"
|
||||
title="Copy SHA256"
|
||||
>
|
||||
{copied === filename ? (
|
||||
<Check size={14} className="text-green-400" />
|
||||
) : (
|
||||
<Copy size={14} className="text-gray-400" />
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Documentation */}
|
||||
{doc && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<FileText size={20} />
|
||||
Documentation <span className="text-sm font-normal text-gray-500">({doc.filename})</span>
|
||||
</h2>
|
||||
<div className="skill-docs bg-clawd-800/50 border border-clawd-700 rounded-xl p-4 sm:p-6 md:p-8 overflow-x-hidden">
|
||||
<Markdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-2xl font-bold text-white border-b border-clawd-700 pb-3 mb-6 mt-0">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-xl font-bold text-white mt-8 mb-4">{children}</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-lg font-semibold text-white mt-6 mb-3">{children}</h3>
|
||||
),
|
||||
h4: ({ children }) => (
|
||||
<h4 className="text-base font-semibold text-white mt-4 mb-2">{children}</h4>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p className="text-gray-300 leading-relaxed mb-4">{children}</p>
|
||||
),
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-clawd-accent hover:underline"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc list-inside text-gray-300 space-y-2 mb-4 ml-4">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal list-inside text-gray-300 space-y-2 mb-4 ml-4">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-gray-300">{children}</li>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-clawd-accent pl-4 py-2 my-4 bg-clawd-900/50 rounded-r text-gray-400 italic">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
code: ({ className, children }) => {
|
||||
const isInline = !className;
|
||||
if (isInline) {
|
||||
return (
|
||||
<code className="text-orange-300 bg-clawd-900 px-1.5 py-0.5 rounded text-sm font-mono">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code className="text-gray-200 text-sm font-mono">{children}</code>
|
||||
);
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-clawd-900 border border-clawd-700 rounded-lg p-3 sm:p-4 overflow-x-auto mb-4 text-xs sm:text-sm max-w-full">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<div className="overflow-x-auto mb-6 -mx-4 sm:mx-0 px-4 sm:px-0">
|
||||
<table className="w-full border-collapse text-xs sm:text-sm min-w-[300px]">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-clawd-900 border-b border-clawd-600">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||
tr: ({ children }) => (
|
||||
<tr className="border-b border-clawd-700/50">{children}</tr>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="text-left px-4 py-3 text-gray-300 font-semibold">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="px-4 py-3 text-gray-300">{children}</td>
|
||||
),
|
||||
hr: () => <hr className="border-clawd-700 my-6" />,
|
||||
strong: ({ children }) => (
|
||||
<strong className="text-white font-semibold">{children}</strong>
|
||||
),
|
||||
em: ({ children }) => (
|
||||
<em className="text-gray-200">{children}</em>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{stripFrontmatter(doc.content)}
|
||||
</Markdown>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Metadata */}
|
||||
<section className="grid md:grid-cols-2 gap-6">
|
||||
<div className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-6 space-y-4">
|
||||
<h3 className="font-bold text-white">Metadata</h3>
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-500">Author</dt>
|
||||
<dd className="text-white">{skillData.author}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-500">License</dt>
|
||||
<dd className="text-white">{skillData.license}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-500">Category</dt>
|
||||
<dd className="text-white">{skillData.openclaw?.category}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{skillData.openclaw?.triggers && skillData.openclaw.triggers.length > 0 && (
|
||||
<div className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-6 space-y-4">
|
||||
<h3 className="font-bold text-white">Trigger Phrases</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{skillData.openclaw.triggers.slice(0, 8).map((trigger) => (
|
||||
<span
|
||||
key={trigger}
|
||||
className="text-xs bg-clawd-700 text-gray-300 px-2 py-1 rounded"
|
||||
>
|
||||
"{trigger}"
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,214 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search as _Search, Filter as _Filter, Package, Sparkles, FileText, GitFork } from 'lucide-react';
|
||||
import { SkillCard } from '../components/SkillCard';
|
||||
import { Footer } from '../components/Footer';
|
||||
import type { SkillMetadata, SkillsIndex } from '../types';
|
||||
|
||||
export const SkillsCatalog: React.FC = () => {
|
||||
const [skills, setSkills] = useState<SkillMetadata[]>([]);
|
||||
const [filteredSkills, setFilteredSkills] = useState<SkillMetadata[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchTerm, _setSearchTerm] = useState('');
|
||||
const [categoryFilter, _setCategoryFilter] = useState<string>('all');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSkills = async () => {
|
||||
try {
|
||||
const response = await fetch('./skills/index.json');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch skills index');
|
||||
}
|
||||
const data: SkillsIndex = await response.json();
|
||||
setSkills(data.skills || []);
|
||||
setFilteredSkills(data.skills || []);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load skills');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSkills();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let result = skills;
|
||||
|
||||
// Apply search filter
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase();
|
||||
result = result.filter(
|
||||
(skill) =>
|
||||
skill.name.toLowerCase().includes(term) ||
|
||||
skill.description.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
|
||||
// Apply category filter
|
||||
if (categoryFilter !== 'all') {
|
||||
result = result.filter((skill) => skill.category === categoryFilter);
|
||||
}
|
||||
|
||||
setFilteredSkills(result);
|
||||
}, [searchTerm, categoryFilter, skills]);
|
||||
|
||||
// Get unique categories from skills (used in commented filter UI)
|
||||
const _categories = ['all', ...new Set(skills.map((s) => s.category).filter(Boolean))];
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="pt-[52px]">
|
||||
<div className="py-16 text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-clawd-accent"></div>
|
||||
<p className="mt-4 text-gray-400">Loading skills...</p>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="pt-[52px]">
|
||||
<div className="py-16 text-center">
|
||||
<Package className="w-16 h-16 mx-auto text-gray-600 mb-4" />
|
||||
<h2 className="text-xl font-bold text-white mb-2">No Skills Available</h2>
|
||||
<p className="text-gray-400 mb-4">{error}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
Skills will appear here after the first skill release.
|
||||
</p>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-[52px] space-y-8">
|
||||
{/* Header */}
|
||||
<section className="text-center space-y-4">
|
||||
<h1 className="text-3xl md:text-4xl text-white">
|
||||
Skills Catalog
|
||||
</h1>
|
||||
<p className="text-gray-400 max-w-2xl mx-auto">
|
||||
Browse security skills for your AI agents. Each skill is verified for safety
|
||||
and distributed with checksums for integrity verification.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Filters - Hidden for now, uncomment when needed
|
||||
<section className="flex flex-col md:flex-row gap-4 max-w-4xl mx-auto">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={20} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search skills..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2.5 bg-clawd-800 border border-clawd-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-clawd-accent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Filter className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" size={20} />
|
||||
<select
|
||||
value={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||
className="pl-10 pr-8 py-2.5 bg-clawd-800 border border-clawd-700 rounded-lg text-white appearance-none focus:outline-none focus:border-clawd-accent"
|
||||
>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat === 'all' ? 'All Categories' : cat}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
*/}
|
||||
|
||||
{/* Skills Grid */}
|
||||
{filteredSkills.length > 0 ? (
|
||||
<section className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredSkills.map((skill) => (
|
||||
<SkillCard key={skill.id} skill={skill} />
|
||||
))}
|
||||
</section>
|
||||
) : (
|
||||
<section className="text-center py-12">
|
||||
<Package className="w-12 h-12 mx-auto text-gray-600 mb-4" />
|
||||
<h3 className="text-lg font-medium text-white mb-2">No skills found</h3>
|
||||
<p className="text-gray-400">
|
||||
{searchTerm || categoryFilter !== 'all'
|
||||
? 'Try adjusting your filters'
|
||||
: 'No skills have been released yet'}
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
{skills.length > 0 && (
|
||||
<section className="text-center text-sm text-gray-500">
|
||||
Showing {filteredSkills.length} of {skills.length} skills
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Shoutout */}
|
||||
<section className="max-w-4xl mx-auto">
|
||||
<div className="bg-clawd-900 border border-clawd-700 rounded-xl overflow-hidden">
|
||||
<div className="bg-clawd-800 px-6 py-4 border-b border-clawd-700 flex items-center justify-between">
|
||||
<h2 className="font-bold text-white flex items-center gap-2">
|
||||
<Sparkles size={18} className="text-clawd-accent" />
|
||||
Contribute Security Skills
|
||||
</h2>
|
||||
<span className="text-xs font-mono text-gray-500">SKILLS-BASED</span>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
<p className="text-gray-300 text-sm">
|
||||
Humans & agents: submit skills that make bots safer (prompt injection defenses, drift checks, tool hardening, policy enforcement).
|
||||
We’ll package them with checksums so everyone can verify integrity.
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="https://github.com/prompt-security/clawsec/blob/main/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-start gap-4 p-4 rounded-lg bg-clawd-800/50 border border-clawd-700 hover:border-clawd-accent/50 transition-colors group"
|
||||
>
|
||||
<span className="text-2xl">📄</span>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-white font-bold text-sm group-hover:text-clawd-accent transition-colors flex items-center gap-2">
|
||||
Read CONTRIBUTING.md
|
||||
<FileText size={14} className="text-gray-500" />
|
||||
</h4>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Guidelines for authoring, packaging, and releasing skills to the ClawSec catalog.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/prompt-security/clawsec/fork"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-start gap-4 p-4 rounded-lg bg-clawd-800/50 border border-clawd-700 hover:border-clawd-accent/50 transition-colors group"
|
||||
>
|
||||
<span className="text-2xl">🍴</span>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-white font-bold text-sm group-hover:text-clawd-accent transition-colors flex items-center gap-2">
|
||||
Fork the repository
|
||||
<GitFork size={14} className="text-gray-500" />
|
||||
</h4>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Start a contribution branch and open a PR with your new security skill.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user