Compare commits

...

30 Commits

Author SHA1 Message Date
davida-ps 79c303fa3f fix(ci): restore github token flow for skill release (#99) 2026-03-02 09:47:42 +02:00
davida-ps e0eae65586 refactor(ci): extract shared exploitability enrichment helper (#95)
* refactor(ci): share exploitability enrichment script

* refactor(scripts): reuse shared exploitability enricher in local feed
2026-03-01 21:50:10 +02:00
github-actions[bot] 56a36b7e52 chore: CVE advisories - 35 new, 0 updated (#97)
Automated update from NVD CVE feed.
Keywords: OpenClaw clawdbot Moltbot NanoClaw WhatsApp-bot baileys
Poll window: 2025-11-01T18:07:01.000Z to 2026-03-01T18:07:01.000Z

Co-authored-by: davida-ps <232346510+davida-ps@users.noreply.github.com>
2026-03-01 20:14:58 +02:00
davida-ps 8ad38dfdc6 feat(ci): add full-scan rebuild mode to NVD polling (#96) 2026-03-01 20:00:42 +02:00
davida-ps 3c336021d7 fix(ci): use valid setup-python pin in advisory workflows (#92) 2026-03-01 18:54:32 +02:00
davida-ps 073e771b73 Exploitability Context for CVE Advisories (#89)
* feat(advisories): add exploitability context for CVE advisories

* fix(ci): align exploitability workflow with signing model

* docs(skills): add patch release changelog entries

* chore(clawsec-feed): bump version to 0.0.5

* chore(clawsec-suite): bump version to 0.1.4

* fix(clawsec-nanoclaw): align exploitability handling and nanoclaw integration

* chore(clawsec-nanoclaw): bump version to 0.0.2

* refactor(scripts): share feed path and mirror sync helpers

* refactor(utils): unify cvss vector parsing flow

* refactor(clawsec-nanoclaw): centralize advisory risk evaluation

* docs(exploitability): refresh release metadata dates

* fix(review): align feed signing and advisory dedupe

* chore(clawsec-feed): bump version to 0.0.6

* chore(clawsec-nanoclaw): bump version to 0.0.3

* fix(backfill): limit signing to target feed only

* fix(review): keep skill runtime verify-only and dedupe matching

* chore(clawsec-nanoclaw): bump version to 0.0.4

* chore(skills): align versions with published tags

* feat(feed): enrich local population with exploitability analysis

* docs(exploitability): mark backfill as historical flow
2026-03-01 18:43:24 +02:00
davida-ps 382db82483 Add Severity Filter Tabs to Advisory Feed Page (#87)
* feat: add severity filter tabs to advisory feed page

Add horizontal severity filter tabs (All, Critical, High, Medium, Low)
to the advisory feed page. Advisories are filtered by CVSS score ranges
matching NVD conventions. Tab counts update dynamically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract severity filter tabs into data-driven map

Replace five duplicated button blocks with a SEVERITY_TABS metadata
array and a single .map() loop. Class strings are kept as full literals
for Tailwind purge compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: replace filteredAdvisories state with useMemo

filteredAdvisories is derived from advisories + selectedSeverity and
should not be independent state. Replace useState + filtering useEffect
with a single useMemo. Keep a minimal useEffect that only resets
currentPage on dependency changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add platform filter tabs (OpenClaw / NanoClaw) to advisory feed

Add a second row of filter tabs for platform selection using the clawd
color palette. Add platforms field to Advisory type to match feed data.
Both severity and platform filters compose via useMemo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: extract shared FilterTabs component and treat missing platforms as universal

Extract a reusable FilterTabs component so severity and platform tab
rows share identical markup. Fix platform filter to treat advisories
with missing or empty platforms as matching all platforms, preventing
legacy entries from being silently dropped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 20:14:08 +02:00
davida-ps c9a66d5c99 Extract Shared Test Harness Module from 9 Test Files (#85)
* refactor: extract shared test harness module from 9 test files

Extract duplicated test utilities into a reusable test_harness.mjs module
to eliminate ~200-250 lines of boilerplate code across test files.

Changes:
- Create skills/clawsec-suite/test/lib/test_harness.mjs with:
  - Test reporting: pass(), fail(), report(), exitWithResults()
  - Crypto utilities: generateEd25519KeyPair(), signPayload()
  - Temp directory: createTempDir() with cleanup
  - Environment helpers: withEnv() for isolated env vars
  - Test runner factory: createTestRunner() for isolated counters

- Refactor 9 test files to use shared harness:
  - feed_verification.test.mjs
  - guarded_install.test.mjs
  - skill_catalog_discovery.test.mjs
  - advisory_suppression.test.mjs
  - advisory_application_scope.test.mjs
  - path_resolution.test.mjs
  - fuzz_properties.test.mjs
  - suppression_config.test.mjs
  - render_report_suppression.test.mjs

Benefits:
- Single source of truth for test utilities
- Consistent test reporting across all files
- Easier to add new test files
- Reduced maintenance burden

Verification:
- All 80 tests pass (15+8+3+15+4+6+1+17+11)
- Zero ESLint warnings
- No behavior changes - only code deduplication
- Cross-skill module sharing works (openclaw-audit-watchdog → clawsec-suite)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: update minimatch override to 10.2.4 to resolve ReDoS vulnerabilities

Bump minimatch from 10.2.1 to 10.2.4 in overrides to fix 10 high-severity
ReDoS vulnerabilities (GHSA-7r86-cg39-jmmj, GHSA-23c5-xmqv-rm74).
Also add .venv/ to ESLint ignores to prevent linting Python venv files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 09:20:36 +02:00
davida-ps e4ca378603 Codex/fix poll nvd pr auth (#86)
* chore(gitignore): ignore auto-claude workspace dir

* fix(ci): restore github token auth for poll-nvd workflow
2026-02-27 09:00:17 +02:00
davida-ps 5c5c7f539a feat: add Product Demo page and integrate into routing (#84) 2026-02-26 16:51:06 +02:00
davida-ps 7c0aa37a05 fix pipelines (#83) 2026-02-26 12:25:52 +02:00
davida-ps 86342d2789 fix: update README for video demo clarity and replace demo GIFs (#82) 2026-02-26 11:59:14 +02:00
davida-ps 95c856ad8a docs: refresh README, contributing guide, and wiki accuracy (#81)
* docs(repo): refresh docs and wiki alignment

* fix(feed): align frontend advisory URL with canonical endpoint
2026-02-26 11:28:16 +02:00
davida-ps fefecaa60a feat(wiki): add full in-app wiki browser and llms index (#80)
* feat(wiki): add full in-app wiki browser and llms index

* feat(wiki): auto-generate per-page llms exports

* vuln package

* fix(wiki): guard malformed route decoding

* fix(wiki): preserve markdown anchor fragments across page links

* refactor(markdown): share default render components

* fix(wiki): block unsafe markdown link schemes

* fix(wiki): block unsafe markdown image schemes

* docs(wiki): migrate root docs into wiki pages

* chore(wiki): de-track generated llms exports

* chore(wiki): ignore generated public wiki artifacts

* fix(wiki): align llms urls with per-page endpoint pattern

* fix(wiki): derive llms index from wiki index page

* refactor(markdown): share frontmatter and title helpers

* refactor(wiki): share route and llms path mapping

* ci(pages): add pr verify workflow and tighten deploy triggers
2026-02-26 10:43:36 +02:00
davida-ps 8132c23f41 Codex/wiki sync revert working (#79)
* fix(wiki-sync): restore known-good pat auth flow

* fix(wiki-sync): restore github token write flow
2026-02-26 00:37:50 +02:00
davida-ps 433a9596a6 fix(wiki-sync): use single x-access-token auth path (#78) 2026-02-26 00:17:21 +02:00
davida-ps c17931d38d Codex/main synced wiki readme (#77)
* fix(readme): use github-safe demo previews and links

* fix(wiki): map wiki root to index

* refactor(wiki): generate Home from INDEX during sync
2026-02-25 22:22:56 +02:00
davida-ps 516e8f0428 Codex/fix readme video links (#76)
* fix(readme): use github-safe demo previews and links

* fix(readme): use only github-hosted demo links

* fix(wiki): map wiki root to index

* feat(readme): add lightweight animated gif demo previews

* refactor(wiki): generate Home from INDEX during sync

* fix(ci): remove github token write scopes in workflows

* chore(ci): use existing poll token for write automation
2026-02-25 22:10:52 +02:00
davida-ps cbc484faf3 Add comprehensive documentation for ClawSec modules and workflows (#75)
- Introduced glossary for key terms and definitions related to security advisories, skill packaging, and CI/CD processes.
- Documented the Automation and Release Pipelines module, detailing responsibilities, key files, public interfaces, and configuration.
- Added ClawSec Suite Core module documentation, outlining its responsibilities, key files, public interfaces, and configuration.
- Created Frontend Web App module documentation, covering responsibilities, key files, public interfaces, and configuration.
- Added Local Validation and Packaging Tools module documentation, detailing responsibilities, key files, public interfaces, and configuration.
- Documented NanoClaw Integration module, including responsibilities, key files, public interfaces, and configuration.
- Introduced an overview of ClawSec, including purpose, repo layout, entry points, key artifacts, and workflows.
- Added a Security section outlining the security model, cryptographic controls, runtime enforcement, and incident playbooks.
- Created a Testing section detailing the testing strategy, verification layers, CI workflow coverage, and local testing commands.
- Documented the Workflow section, covering the end-to-end lifecycle, primary workflow map, local operator workflow, and operational risks.
2026-02-25 21:44:51 +02:00
github-actions[bot] 448aed3261 chore: CVE advisories - 0 new, 34 updated (#73)
Automated update from NVD CVE feed.
Keywords: OpenClaw clawdbot Moltbot NanoClaw WhatsApp-bot baileys
Poll window: 2025-10-28T16:48:19.000Z to 2026-02-25T16:48:19.000Z

Co-authored-by: davida-ps <232346510+davida-ps@users.noreply.github.com>
2026-02-25 18:51:57 +02:00
davida-ps 037bd125b9 fix: refine target selection logic for advisory workflows (#72) 2026-02-25 18:47:34 +02:00
davida-ps 5ef122dd91 feat: enhance platform detection and handling in advisory workflows (#70) 2026-02-25 18:07:57 +02:00
davida-ps 938eb929f3 feat: add property-based fuzz tests for advisory parsing, semver matc… (#69)
* feat: add property-based fuzz tests for advisory parsing, semver matching, and suppression config

* fix(ci): install deps before fuzz test jobs
2026-02-25 17:48:48 +02:00
dependabot[bot] 55fb234fc0 chore(deps): bump lucide-react from 0.564.0 to 0.575.0 (#59) 2026-02-25 16:21:21 +02:00
github-actions[bot] ea44aea49e chore: CVE advisories - 0 new, 34 updated (#68)
Automated update from NVD CVE feed.
Keywords: OpenClaw clawdbot Moltbot NanoClaw WhatsApp-bot baileys
Poll window: 2025-10-28T12:39:05.000Z to 2026-02-25T12:39:05.000Z

Co-authored-by: davida-ps <232346510+davida-ps@users.noreply.github.com>
2026-02-25 14:40:50 +02:00
dependabot[bot] 2e64201254 chore(deps): bump react-router-dom from 7.13.0 to 7.13.1 (#56)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 7.13.0 to 7.13.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.13.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-version: 7.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 14:25:42 +02:00
davida-ps 371d792e97 feat: enhance support for NanoClaw in CVE processing and UI components (#67) 2026-02-25 14:18:57 +02:00
dependabot[bot] 0602c0fbe5 chore(deps): bump ruff from 0.15.1 to 0.15.2 in /.github (#55)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.1 to 0.15.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.1...0.15.2)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 13:51:41 +02:00
dependabot[bot] 8908319dd0 chore(deps): bump github/codeql-action from 4.32.3 to 4.32.4 (#54)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.3 to 4.32.4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/9e907b5e64f6b83e7804b09294d44122997950d6...89a39a4e59826350b863aa6b6252a07ad50cf83e)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 13:46:08 +02:00
dependabot[bot] 6f2fe918a2 chore(deps): bump aquasecurity/trivy-action from 0.34.0 to 0.34.1 (#53)
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.34.0 to 0.34.1.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/c1824fd6edce30d7ab345a9989de00bbd46ef284...e368e328979b113139d6f9068e03accaed98a518)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: 0.34.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-25 13:43:22 +02:00
106 changed files with 7866 additions and 1283 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
ruff==0.15.1
ruff==0.15.2
bandit==1.9.3
+14 -4
View File
@@ -3,8 +3,7 @@ name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
permissions: read-all
@@ -31,6 +30,7 @@ jobs:
- name: TypeScript Check
run: npx tsc --noEmit
- name: Build Check
if: matrix.os == 'ubuntu-latest'
run: npm run build
lint-python:
@@ -63,7 +63,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Trivy FS Scan
uses: aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284 # 0.34.0
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1
with:
scan-type: 'fs'
scan-ref: '.'
@@ -71,7 +71,7 @@ jobs:
exit-code: '1'
ignore-unfixed: true
- name: Trivy Config Scan
uses: aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284 # 0.34.0
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1
with:
scan-type: 'config'
scan-ref: '.'
@@ -101,6 +101,8 @@ jobs:
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Feed Verification Tests
run: node skills/clawsec-suite/test/feed_verification.test.mjs
- name: Guarded Install Tests
@@ -109,6 +111,10 @@ jobs:
run: node skills/clawsec-suite/test/advisory_suppression.test.mjs
- name: Path Resolution Tests
run: node skills/clawsec-suite/test/path_resolution.test.mjs
- name: Fuzz Property Tests
run: node skills/clawsec-suite/test/fuzz_properties.test.mjs
- name: Semver/Scope/Suppression Fuzz Tests
run: node skills/clawsec-suite/test/fuzz_semver_scope_suppression.test.mjs
- name: Advisory Application Scope Tests
run: node skills/clawsec-suite/test/advisory_application_scope.test.mjs
@@ -120,7 +126,11 @@ jobs:
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Suppression Config Tests
run: node skills/openclaw-audit-watchdog/test/suppression_config.test.mjs
- name: Suppression Config Fuzz Tests
run: node skills/openclaw-audit-watchdog/test/suppression_config_fuzz.test.mjs
- name: Render Report Suppression Tests
run: node skills/openclaw-audit-watchdog/test/render_report_suppression.test.mjs
+3 -4
View File
@@ -1,10 +1,9 @@
name: CodeQL
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
schedule:
- cron: "17 3 * * 1"
@@ -28,7 +27,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize CodeQL
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
with:
languages: ${{ matrix.language }}
@@ -38,4 +37,4 @@ jobs:
- name: Build project
run: npm run build
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
+61 -5
View File
@@ -20,10 +20,6 @@ jobs:
process-advisory:
if: github.event.label.name == 'advisory-approved'
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -117,6 +113,32 @@ jobs:
fi
echo "Affected: $AFFECTED"
# Build platforms array
OPENCLAW_SELECTED="false"
if echo "$ISSUE_BODY" | grep -qi '^[[:space:]]*-[[:space:]]*\[[xX]\][[:space:]]*OpenClaw'; then
OPENCLAW_SELECTED="true"
fi
OTHER_PLATFORM_RAW=$(echo "$ISSUE_BODY" | sed -n 's/^[[:space:]]*-[[:space:]]*\[[xX]\][[:space:]]*Other:[[:space:]]*\(.*\)$/\1/p' | head -1 | xargs)
OTHER_PLATFORM=""
if [ -n "$OTHER_PLATFORM_RAW" ]; then
OTHER_PLATFORM=$(echo "$OTHER_PLATFORM_RAW" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//')
if echo "$OTHER_PLATFORM" | grep -q 'nanoclaw'; then
OTHER_PLATFORM="nanoclaw"
fi
fi
PLATFORMS=$(jq -n --arg open "$OPENCLAW_SELECTED" --arg other "$OTHER_PLATFORM" '
[
(if $open == "true" then "openclaw" else empty end),
(if ($other | length) > 0 then $other else empty end)
] | unique
')
if [ "$PLATFORMS" = "[]" ]; then
PLATFORMS='["openclaw","nanoclaw"]'
fi
echo "Platforms: $PLATFORMS"
# Parse recommended action
ACTION=$(echo "$ISSUE_BODY" | sed -n '/^## Recommended Action/,/^---/p' | grep -v '^## Recommended Action' | grep -v '^---' | grep -v '^<!--' | sed '/^\s*$/d' | tr '\n' ' ' | xargs)
if [ -z "$ACTION" ]; then
@@ -142,6 +164,7 @@ jobs:
--arg title "$TITLE" \
--arg description "$DESCRIPTION" \
--argjson affected "$AFFECTED" \
--argjson platforms "$PLATFORMS" \
--arg action "$ACTION" \
--arg published "$PUBLISHED" \
--arg source "Community Report" \
@@ -155,6 +178,7 @@ jobs:
title: $title,
description: $description,
affected: $affected,
platforms: $platforms,
action: $action,
published: $published,
references: [],
@@ -169,6 +193,27 @@ jobs:
echo "Created advisory JSON:"
cat tmp_advisory.json
- name: Set up Python for exploitability analysis
if: steps.parse.outputs.already_exists != 'true'
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: '3.10'
- name: Analyze exploitability for community advisory
if: steps.parse.outputs.already_exists != 'true'
run: |
set -euo pipefail
echo "=== Analyzing exploitability for community advisory ==="
scripts/ci/enrich_exploitability.sh \
--mode single \
--input tmp_advisory.json \
--output tmp_advisory.json
echo "=== Exploitability analysis complete ==="
echo "Exploitability score: $(jq -r '.exploitability_score // "unknown"' tmp_advisory.json)"
- name: Update feed
if: steps.parse.outputs.already_exists != 'true'
run: |
@@ -216,12 +261,21 @@ jobs:
if: steps.parse.outputs.already_exists != 'true'
run: cp "$FEED_SIG_PATH" "$SKILL_FEED_SIG_PATH"
- name: Require automation token for write operations
env:
AUTOMATION_TOKEN: ${{ secrets.POLL_NVD_CVES_PAT }}
run: |
if [ -z "$AUTOMATION_TOKEN" ]; then
echo "::error::Set POLL_NVD_CVES_PAT with repo write permissions."
exit 1
fi
- name: Create Pull Request
if: steps.parse.outputs.already_exists != 'true'
id: create-pr
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.POLL_NVD_CVES_PAT }}
branch: automated/community-advisory-${{ github.event.issue.number }}
delete-branch: true
title: "chore: add community advisory ${{ steps.parse.outputs.advisory_id }}"
@@ -250,6 +304,7 @@ jobs:
if: steps.parse.outputs.already_exists != 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.POLL_NVD_CVES_PAT }}
script: |
const advisoryId = '${{ steps.parse.outputs.advisory_id }}';
const pullRequestUrl = '${{ steps.create-pr.outputs.pull-request-url }}';
@@ -275,6 +330,7 @@ jobs:
if: steps.parse.outputs.already_exists == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.POLL_NVD_CVES_PAT }}
script: |
const advisoryId = '${{ steps.parse.outputs.advisory_id }}';
await github.rest.issues.createComment({
+30 -5
View File
@@ -1,10 +1,11 @@
name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_run:
workflows: ["CI", "Skill Release"]
workflows: ["Skill Release"]
types: [completed]
# Note: No branch restriction - must trigger on both main branch CI runs AND tag-based Skill Releases
workflow_dispatch:
permissions:
@@ -19,8 +20,20 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
# Only run if workflow_dispatch OR the triggering workflow succeeded
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
# Production build only: manual dispatch, push to main, or trusted release workflows.
# PR validation runs in .github/workflows/pages-verify.yml.
if: |
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'push' &&
github.ref_name == 'main'
) ||
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.name == 'Skill Release' &&
github.event.workflow_run.event != 'pull_request'
)
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -401,7 +414,19 @@ jobs:
path: ./dist
deploy:
# Deploy after build succeeds (CI or Skill Release must pass first, or manual dispatch)
# Deploy after a production build succeeds.
if: |
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'push' &&
github.ref_name == 'main'
) ||
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.name == 'Skill Release' &&
github.event.workflow_run.event != 'pull_request'
)
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
+111
View File
@@ -0,0 +1,111 @@
name: Pages Verify
on:
pull_request:
branches: [main]
permissions:
contents: read
concurrency:
group: pages-verify-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
verify-pages-build:
name: Verify Pages Build (No Publish)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Verify signing key consistency (repo + docs)
run: ./scripts/ci/verify_signing_key_consistency.sh
- name: Prepare advisory artifacts for pre-deploy checks
run: |
set -euo pipefail
mkdir -p public/advisories
cp advisories/feed.json public/advisories/feed.json
- name: Generate advisory checksums manifest
run: |
set -euo pipefail
FEED_FILE="public/advisories/feed.json"
FEED_SHA=$(sha256sum "$FEED_FILE" | awk '{print $1}')
FEED_SIZE=$(stat -c%s "$FEED_FILE" 2>/dev/null || stat -f%z "$FEED_FILE")
jq -n \
--arg schema_version "1" \
--arg algorithm "sha256" \
--arg version "1.1.0" \
--arg generated "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg repo "${{ github.repository }}" \
--arg sha "$FEED_SHA" \
--argjson size "$FEED_SIZE" \
'{
schema_version: $schema_version,
algorithm: $algorithm,
version: $version,
generated_at: $generated,
repository: $repo,
files: {
"advisories/feed.json": {
sha256: $sha,
size: $size,
path: "advisories/feed.json",
url: "https://clawsec.prompt.security/advisories/feed.json"
}
}
}' > public/checksums.json
- name: Generate ephemeral signing key for PR verification
id: test_key
run: |
set -euo pipefail
KEY_FILE=$(mktemp)
openssl genpkey -algorithm Ed25519 -out "$KEY_FILE"
{
echo "private_key<<EOF"
cat "$KEY_FILE"
echo "EOF"
} >> "$GITHUB_OUTPUT"
rm -f "$KEY_FILE"
- name: Sign advisory feed and verify
uses: ./.github/actions/sign-and-verify
with:
private_key: ${{ steps.test_key.outputs.private_key }}
input_file: public/advisories/feed.json
signature_file: public/advisories/feed.json.sig
public_key_output: public/signing-public.pem
- name: Sign checksums and verify
uses: ./.github/actions/sign-and-verify
with:
private_key: ${{ steps.test_key.outputs.private_key }}
input_file: public/checksums.json
signature_file: public/checksums.sig
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
env:
NODE_ENV: production
- name: Sanity-check generated artifacts
run: |
set -euo pipefail
test -f dist/index.html
test -f public/advisories/feed.json.sig
test -f public/checksums.sig
test -f public/signing-public.pem
+305 -32
View File
@@ -7,7 +7,7 @@ on:
workflow_dispatch:
inputs:
force_full_scan:
description: 'Ignore last poll date and scan all CVEs'
description: 'Ignore feed state and rebuild CVE advisories from full NVD history'
required: false
default: 'false'
type: boolean
@@ -30,6 +30,7 @@ jobs:
poll-and-update:
runs-on: ubuntu-latest
permissions:
actions: write
contents: write
pull-requests: write
steps:
@@ -85,6 +86,7 @@ jobs:
run: |
set -euo pipefail
mkdir -p tmp
FORCE_FULL_SCAN="${{ inputs.force_full_scan }}"
START_DATE="${{ steps.dates.outputs.start_date }}"
END_DATE="${{ steps.dates.outputs.end_date }}"
@@ -100,35 +102,93 @@ jobs:
# Fetch for each keyword
for KEYWORD in $KEYWORDS; do
echo "Fetching keyword: $KEYWORD"
URL="https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=${KEYWORD}&lastModStartDate=${START_ENC}&lastModEndDate=${END_ENC}"
echo "URL: $URL"
# Fetch with retry logic
keyword_ok=false
last_http_code=""
for i in 1 2 3; do
HTTP_CODE=$(curl -sS -w "%{http_code}" -o "tmp/nvd_${KEYWORD}.json" "$URL" || true)
if [ -z "$HTTP_CODE" ]; then
HTTP_CODE="000"
fi
last_http_code="$HTTP_CODE"
if [ "$HTTP_CODE" = "200" ]; then
if jq -e . "tmp/nvd_${KEYWORD}.json" >/dev/null 2>&1; then
echo "Success for $KEYWORD"
if [ "$FORCE_FULL_SCAN" = "true" ]; then
echo "Full scan mode enabled: paginating complete NVD history for keyword '$KEYWORD'"
echo '{"vulnerabilities":[]}' > "tmp/nvd_${KEYWORD}.json"
START_INDEX=0
RESULTS_PER_PAGE=2000
while true; do
URL="https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=${KEYWORD}&startIndex=${START_INDEX}&resultsPerPage=${RESULTS_PER_PAGE}"
PAGE_FILE="tmp/nvd_${KEYWORD}_${START_INDEX}.json"
echo "URL: $URL"
page_ok=false
for i in 1 2 3; do
HTTP_CODE=$(curl -sS -w "%{http_code}" -o "$PAGE_FILE" "$URL" || true)
if [ -z "$HTTP_CODE" ]; then
HTTP_CODE="000"
fi
last_http_code="$HTTP_CODE"
if [ "$HTTP_CODE" = "200" ]; then
if jq -e . "$PAGE_FILE" >/dev/null 2>&1; then
page_ok=true
break
fi
echo "Invalid JSON for $KEYWORD page $START_INDEX, retry $i..."
sleep 5
elif [ "$HTTP_CODE" = "403" ] || [ "$HTTP_CODE" = "429" ]; then
echo "Rate limited, waiting 30s before retry $i..."
sleep 30
else
echo "HTTP $HTTP_CODE for $KEYWORD page $START_INDEX, retry $i..."
sleep 5
fi
done
if [ "$page_ok" != "true" ]; then
break
fi
jq -s '.[0].vulnerabilities += .[1].vulnerabilities | .[0]' \
"tmp/nvd_${KEYWORD}.json" "$PAGE_FILE" > "tmp/nvd_${KEYWORD}_merged.json"
mv "tmp/nvd_${KEYWORD}_merged.json" "tmp/nvd_${KEYWORD}.json"
PAGE_COUNT=$(jq '.vulnerabilities | length' "$PAGE_FILE")
TOTAL_RESULTS=$(jq '.totalResults // 0' "$PAGE_FILE")
echo "Fetched $PAGE_COUNT results at startIndex=$START_INDEX (totalResults=$TOTAL_RESULTS)"
START_INDEX=$((START_INDEX + RESULTS_PER_PAGE))
if [ "$START_INDEX" -ge "$TOTAL_RESULTS" ] || [ "$PAGE_COUNT" -eq 0 ]; then
keyword_ok=true
break
fi
echo "Invalid JSON for $KEYWORD, retry $i..."
sleep 5
elif [ "$HTTP_CODE" = "403" ] || [ "$HTTP_CODE" = "429" ]; then
echo "Rate limited, waiting 30s before retry $i..."
sleep 30
else
echo "HTTP $HTTP_CODE for $KEYWORD, retry $i..."
sleep 5
fi
done
# NVD recommends 6 second delay between requests
sleep 6
done
else
URL="https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=${KEYWORD}&lastModStartDate=${START_ENC}&lastModEndDate=${END_ENC}"
echo "URL: $URL"
# Fetch with retry logic
for i in 1 2 3; do
HTTP_CODE=$(curl -sS -w "%{http_code}" -o "tmp/nvd_${KEYWORD}.json" "$URL" || true)
if [ -z "$HTTP_CODE" ]; then
HTTP_CODE="000"
fi
last_http_code="$HTTP_CODE"
if [ "$HTTP_CODE" = "200" ]; then
if jq -e . "tmp/nvd_${KEYWORD}.json" >/dev/null 2>&1; then
echo "Success for $KEYWORD"
keyword_ok=true
break
fi
echo "Invalid JSON for $KEYWORD, retry $i..."
sleep 5
elif [ "$HTTP_CODE" = "403" ] || [ "$HTTP_CODE" = "429" ]; then
echo "Rate limited, waiting 30s before retry $i..."
sleep 30
else
echo "HTTP $HTTP_CODE for $KEYWORD, retry $i..."
sleep 5
fi
done
fi
if [ "$keyword_ok" != "true" ]; then
echo "::error::Failed to fetch valid NVD response for keyword '$KEYWORD' (last HTTP code: ${last_http_code:-unknown})."
@@ -175,7 +235,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" '
@@ -211,6 +271,14 @@ jobs:
- name: Check for updates to existing advisories
id: updates
run: |
if [ "${{ inputs.force_full_scan }}" = "true" ]; then
echo "Full scan mode enabled: skipping delta update detection."
echo '[]' > tmp/updated_advisories.json
echo "Advisories to update: 0"
echo "update_count=0" >> $GITHUB_OUTPUT
exit 0
fi
# Compare existing CVE advisories against NVD data for changes
# Only check advisories that start with "CVE-" (NVD-sourced)
@@ -297,6 +365,51 @@ jobs:
end
);
def cpe_criteria:
(
[.cve.configurations[]? | .. | objects | .criteria? | strings | select(startswith("cpe:2.3:"))]
| unique
);
def context_blob:
(
[
(.cve.descriptions[]? | select(.lang == "en") | .value),
(.cve.references[]?.url // empty)
]
| map(strings | ascii_downcase)
| join(" ")
);
def inferred_targets:
(
context_blob 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
);
def normalized_platforms:
(
inferred_targets as $targets
| ($targets | map(select(startswith("openclaw@"))) | length > 0) as $has_openclaw
| ($targets | map(select(startswith("nanoclaw@"))) | length > 0) as $has_nanoclaw
| if $has_openclaw and $has_nanoclaw then ["openclaw", "nanoclaw"]
elif $has_openclaw then ["openclaw"]
elif $has_nanoclaw then ["nanoclaw"]
else ["openclaw", "nanoclaw"]
end
);
[.[] | {
id: .cve.id,
severity: (get_cvss_score | map_severity),
@@ -305,7 +418,11 @@ 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)),
references: [.cve.references[]?.url // empty] | unique | .[0:3]
affected: normalized_affected,
platforms: normalized_platforms,
references: [.cve.references[]?.url // empty] | unique | .[0:3],
exploitability_score: null,
exploitability_rationale: null
}]
' tmp/filtered_cves.json > tmp/nvd_current_state.json
@@ -325,6 +442,8 @@ 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.platforms != $nvd_entry.platforms) or
($existing_entry.description != $nvd_entry.description) then
{
id: $nvd_entry.id,
@@ -334,6 +453,8 @@ 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.platforms != $nvd_entry.platforms then ["platforms updated"] else [] end)
+ (if $existing_entry.description != $nvd_entry.description then ["description updated"] else [] end)
),
updated_fields: {
@@ -341,6 +462,8 @@ jobs:
type: $nvd_entry.type,
nvd_category_id: $nvd_entry.nvd_category_id,
cvss_score: $nvd_entry.cvss_score,
affected: $nvd_entry.affected,
platforms: $nvd_entry.platforms,
description: $nvd_entry.description,
title: $nvd_entry.title,
references: $nvd_entry.references
@@ -368,7 +491,12 @@ jobs:
id: transform
run: |
# Read existing IDs into a jq-friendly format
EXISTING_IDS=$(cat tmp/existing_ids.txt | jq -R -s 'split("\n") | map(select(length > 0))')
if [ "${{ inputs.force_full_scan }}" = "true" ]; then
echo "Full scan mode enabled: rebuilding CVE advisories from scratch."
EXISTING_IDS='[]'
else
EXISTING_IDS=$(cat tmp/existing_ids.txt | jq -R -s 'split("\n") | map(select(length > 0))')
fi
# Transform NVD CVEs to our advisory format
jq --argjson existing "$EXISTING_IDS" '
@@ -453,8 +581,53 @@ 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 context_blob:
(
[
(.cve.descriptions[]? | select(.lang == "en") | .value),
(.cve.references[]?.url // empty)
]
| map(strings | ascii_downcase)
| join(" ")
);
def inferred_targets:
(
context_blob 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
);
def normalized_platforms:
(
inferred_targets as $targets
| ($targets | map(select(startswith("openclaw@"))) | length > 0) as $has_openclaw
| ($targets | map(select(startswith("nanoclaw@"))) | length > 0) as $has_nanoclaw
| if $has_openclaw and $has_nanoclaw then ["openclaw", "nanoclaw"]
elif $has_openclaw then ["openclaw"]
elif $has_nanoclaw then ["nanoclaw"]
else ["openclaw", "nanoclaw"]
end
);
[.[] |
[.[] |
select(.cve.id as $id | $existing | index($id) | not) |
{
id: .cve.id,
@@ -463,12 +636,15 @@ 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,
platforms: normalized_platforms,
action: "Review and update affected components. See NVD for remediation details.",
published: .cve.published,
references: [.cve.references[]?.url // empty] | unique | .[0:3],
cvss_score: get_cvss_score,
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id)
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id),
exploitability_score: null,
exploitability_rationale: null
}
]
' tmp/filtered_cves.json > tmp/new_advisories.json
@@ -482,12 +658,63 @@ jobs:
jq '.[].id' tmp/new_advisories.json
fi
- name: Set up Python for exploitability analysis
if: steps.transform.outputs.new_count != '0'
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: '3.10'
- name: Analyze exploitability for new advisories
if: steps.transform.outputs.new_count != '0'
run: |
set -euo pipefail
echo "=== Analyzing exploitability for new advisories ==="
# Extract CVSS vectors from filtered CVEs to merge with advisories
jq '
[.[] | {
id: .cve.id,
cvss_vector: (
.cve.metrics.cvssMetricV31[0]?.cvssData.vectorString //
.cve.metrics.cvssMetricV30[0]?.cvssData.vectorString //
.cve.metrics.cvssMetricV2[0]?.vectorString //
""
)
}] | map({(.id): .cvss_vector}) | add
' tmp/filtered_cves.json > tmp/cvss_vectors.json
scripts/ci/enrich_exploitability.sh \
--mode batch \
--input tmp/new_advisories.json \
--output tmp/new_advisories.json \
--cvss-vectors tmp/cvss_vectors.json
echo "=== Exploitability analysis complete ==="
# Show summary of exploitability scores
echo "Exploitability score distribution:"
jq -r '.[] | "\(.id): \(.exploitability_score // "unknown")"' tmp/new_advisories.json | \
awk -F': ' '{scores[$2]++} END {for (s in scores) print " " s ": " scores[s]}'
- name: Update feed.json
if: steps.transform.outputs.new_count != '0' || steps.updates.outputs.update_count != '0'
run: |
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
FORCE_FULL_SCAN="${{ inputs.force_full_scan }}"
if [ -f "$FEED_PATH" ]; then
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" '
.updated = $now |
.advisories = (
((.advisories // []) | map(select((.id // "") | startswith("CVE-") | not)))
+ $rebuilt
| sort_by(.published)
| reverse
)
' "$FEED_PATH" > tmp/updated_feed.json
elif [ -f "$FEED_PATH" ]; then
# Step 1: Apply updates to existing advisories
jq --slurpfile updates tmp/updated_advisories.json '
.advisories = [
@@ -571,6 +798,7 @@ jobs:
## Summary
Automated update from NVD CVE feed.
- **Mode:** ${{ inputs.force_full_scan == true && 'full-rebuild (ignore feed state)' || 'delta (incremental)' }}
- **New advisories:** ${{ steps.transform.outputs.new_count }}
- **Updated advisories:** ${{ steps.updates.outputs.update_count }}
- **Poll window:** ${{ steps.dates.outputs.start_date }} → ${{ steps.dates.outputs.end_date }}
@@ -590,12 +818,57 @@ jobs:
${{ env.SKILL_FEED_PATH }}
${{ env.SKILL_FEED_SIG_PATH }}
- name: Run CodeQL on generated PR branch
if: steps.create-pr.outputs.pull-request-number != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
BRANCH="${{ steps.create-pr.outputs.pull-request-branch }}"
if [ -z "$BRANCH" ]; then
echo "::error::Missing pull-request-branch output from create-pull-request"
exit 1
fi
echo "Dispatching CodeQL for branch: $BRANCH"
gh workflow run codeql.yml --ref "$BRANCH"
RUN_ID=""
for _ in $(seq 1 30); do
RUN_ID=$(gh run list \
--workflow "CodeQL" \
--branch "$BRANCH" \
--event workflow_dispatch \
--json databaseId,createdAt \
--jq 'sort_by(.createdAt) | last | .databaseId // empty')
if [ -n "$RUN_ID" ]; then
break
fi
sleep 5
done
if [ -z "$RUN_ID" ]; then
echo "::error::Unable to locate dispatched CodeQL run for branch $BRANCH"
exit 1
fi
echo "Waiting for CodeQL run id: $RUN_ID"
gh run watch "$RUN_ID" --exit-status
- name: Summary
run: |
if [ "${{ inputs.force_full_scan }}" = "true" ]; then
MODE="full-rebuild (ignore feed state)"
else
MODE="delta (incremental)"
fi
echo "## NVD CVE Poll Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Mode | $MODE |" >> $GITHUB_STEP_SUMMARY
echo "| Poll Window | ${{ steps.dates.outputs.start_date }} → ${{ steps.dates.outputs.end_date }} |" >> $GITHUB_STEP_SUMMARY
echo "| Keywords | $KEYWORDS |" >> $GITHUB_STEP_SUMMARY
echo "| CVEs Found (filtered) | ${{ steps.process.outputs.filtered_count }} |" >> $GITHUB_STEP_SUMMARY
+1 -3
View File
@@ -11,8 +11,6 @@ on:
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '19 23 * * 0'
push:
branches: [ "main" ]
# Declare default permissions as read only.
permissions: read-all
@@ -73,6 +71,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
sarif_file: results.sarif
+73
View File
@@ -0,0 +1,73 @@
name: Sync Wiki
on:
push:
branches: [main]
paths:
- 'wiki/**'
workflow_dispatch:
permissions:
contents: write
concurrency:
group: wiki-sync
cancel-in-progress: false
jobs:
sync-wiki:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Sync wiki folder to repository wiki
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
if [ ! -d wiki ]; then
echo "::error::wiki/ directory not found"
exit 1
fi
# GitHub Wiki root (/wiki) renders Home.md, not INDEX.md.
# INDEX.md is the canonical source; generate Home.md from it.
if [ ! -f wiki/INDEX.md ]; then
echo "::error::wiki/INDEX.md not found. It is required to generate wiki/Home.md."
exit 1
fi
cp wiki/INDEX.md wiki/Home.md
WIKI_REMOTE="https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.wiki.git"
if ! git ls-remote "$WIKI_REMOTE" >/dev/null 2>&1; then
echo "::warning::Wiki remote unavailable (repository wiki may be disabled). Skipping sync."
exit 0
fi
WIKI_TMP="$(mktemp -d)"
trap 'rm -rf "$WIKI_TMP"' EXIT
git clone --depth 1 "$WIKI_REMOTE" "$WIKI_TMP"
rsync -a --delete --exclude '.git/' wiki/ "$WIKI_TMP/"
cd "$WIKI_TMP"
if [ -z "$(git status --porcelain)" ]; then
echo "No wiki changes to sync."
exit 0
fi
WIKI_HEAD_REF="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || true)"
if [ -n "$WIKI_HEAD_REF" ]; then
WIKI_BRANCH="${WIKI_HEAD_REF#origin/}"
else
WIKI_BRANCH="master"
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "docs(wiki): sync from ${GITHUB_SHA}"
# Clone may sanitize credentials from origin URL; push with explicit auth URL.
git push "$WIKI_REMOTE" HEAD:"$WIKI_BRANCH"
+12
View File
@@ -1,4 +1,5 @@
.claude
.auto-claude/
.codex
_bmad
_bmad-output
@@ -24,6 +25,7 @@ dist-ssr
# Derived public assets (copied during build)
public/advisories
public/skills
public/wiki/
# Python bytecode
__pycache__/
@@ -39,3 +41,13 @@ __pycache__/
*.njsproj
*.sln
*.sw?
clawsec-signing-private.pem
# Auto Claude generated files
.auto-claude/
.auto-claude-security.json
.auto-claude-status
.claude_settings.json
.worktrees/
.security-key
logs/security/
+6
View File
@@ -5,6 +5,8 @@ ClawSec combines a Vite + React frontend with security skill packages and releas
- Frontend entrypoints: `index.tsx`, `App.tsx`
- UI and routes: `components/`, `pages/`
- Shared types/constants: `types.ts`, `constants.ts`
- Wiki source docs: `wiki/` (synced to GitHub Wiki by `.github/workflows/wiki-sync.yml`)
- Generated wiki exports: `public/wiki/` (`llms.txt` outputs; generated locally/CI and gitignored)
- Skills: `skills/<skill-name>/` (`skill.json`, `SKILL.md`, optional `scripts/`, `test/`)
- Advisory feed: `advisories/feed.json`, `advisories/feed.json.sig`
- Automation: `scripts/`, `.github/workflows/`
@@ -15,7 +17,9 @@ ClawSec combines a Vite + React frontend with security skill packages and releas
- `npm run dev`: run local Vite server.
- `npm run build`: create production build (CI gate).
- `npm run preview`: preview built app.
- `npm run gen:wiki-llms`: generate wiki `llms.txt` exports from `wiki/` into `public/wiki/`.
- `./scripts/prepare-to-push.sh [--fix]`: run lint, types, build, and security checks.
- `./scripts/populate-local-wiki.sh`: regenerate local wiki `llms.txt` exports for preview.
- `npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --max-warnings 0`: lint JS/TS.
- `npx tsc --noEmit`: type-check TypeScript.
- `node skills/clawsec-suite/test/feed_verification.test.mjs`: run a skill-local Node test.
@@ -31,6 +35,7 @@ ClawSec combines a Vite + React frontend with security skill packages and releas
There is no root `npm test`; tests are mostly skill-local.
- Run changed tests directly: `node skills/<skill>/test/<name>.test.mjs`.
- For frontend/config changes, run ESLint, `npx tsc --noEmit`, and `npm run build`.
- For wiki rendering/export changes, run `npm run gen:wiki-llms` and `npm run build`.
- For Python utility updates, run `ruff check utils/` and `bandit -r utils/ -ll`.
## Pull Request Guidelines
@@ -39,6 +44,7 @@ There is no root `npm test`; tests are mostly skill-local.
- Keep PRs focused and include summary, security benefit, and testing performed.
- Keep versions aligned between `skills/<skill>/skill.json` and `skills/<skill>/SKILL.md`.
- Do not push release tags from PR branches; releases are tagged from `main`.
- Do not commit generated `public/wiki/` artifacts; edit `wiki/` source files instead.
## Agent Collaboration & Git Safety
- Delete unused or obsolete files only when your changes make them irrelevant; revert files only when the change is yours or explicitly requested. If a git operation creates uncertainty about another agents in-flight work, stop and coordinate instead of deleting.
+5 -1
View File
@@ -6,6 +6,8 @@ import { FeedSetup } from './pages/FeedSetup';
import { SkillsCatalog } from './pages/SkillsCatalog';
import { SkillDetail } from './pages/SkillDetail';
import { AdvisoryDetail } from './pages/AdvisoryDetail';
import { WikiBrowser } from './pages/WikiBrowser';
import { ProductDemo } from './pages/ProductDemo';
const App: React.FC = () => {
return (
@@ -17,10 +19,12 @@ const App: React.FC = () => {
<Route path="/skills/:skillId" element={<SkillDetail />} />
<Route path="/feed" element={<FeedSetup />} />
<Route path="/feed/:advisoryId" element={<AdvisoryDetail />} />
<Route path="/demo" element={<ProductDemo />} />
<Route path="/wiki/*" element={<WikiBrowser />} />
</Routes>
</Layout>
</Router>
);
};
export default App;
export default App;
+6 -1
View File
@@ -2,8 +2,13 @@
Thank you for your interest in contributing security skills to the ClawSec ecosystem! This guide will walk you through creating, testing, and submitting new skills.
## Wiki Documentation Source of Truth
For contributor-facing wiki docs, treat `wiki/` in this repository as the single source of truth. Do not edit the GitHub Wiki directly; `.github/workflows/wiki-sync.yml` publishes `wiki/` to `<repo>.wiki.git` when `wiki/**` changes on `main`.
## Table of Contents
- [Wiki Documentation Source of Truth](#wiki-documentation-source-of-truth)
- [Getting Started](#getting-started)
- [Skill Structure](#skill-structure)
- [Creating a New Skill](#creating-a-new-skill)
@@ -649,7 +654,7 @@ Wait for a verified patched version.
Once your advisory is published:
1. **Agents receive it** - The feed is served from raw GitHub, so agents see it on their next feed check
1. **Agents receive it** - The feed is served at `https://clawsec.prompt.security/advisories/feed.json` (with signature/checksum artifacts), so agents see it on their next feed check
2. **You're credited** - Your issue is linked in the advisory
3. **Community is protected** - Agents using ClawSec Feed will be alerted
+88 -19
View File
@@ -6,7 +6,7 @@
<div align="center">
## Secure Your OpenClaw Bots with a Complete Security Skill Suite
## Secure Your OpenClaw and NanoClaw Agents with a Complete Security Skill Suite
<h4>Brought to you by <a href="https://prompt.security">Prompt Security</a>, the Platform for AI Security</h4>
@@ -37,7 +37,7 @@ ClawSec is a **complete security skill suite for AI agent platforms**. It provid
### Supported Platforms
- **OpenClaw** (Moltbot, Clawdbot, and clones) - Full suite with skill installer, file integrity protection, and security audits
- **OpenClaw** (MoltBot, Clawdbot, and clones) - Full suite with skill installer, file integrity protection, and security audits
- **NanoClaw** - Containerized WhatsApp bot security with MCP tools for advisory monitoring, signature verification, and file integrity
### Core Capabilities
@@ -51,26 +51,48 @@ ClawSec is a **complete security skill suite for AI agent platforms**. It provid
---
## 🎬 Product Demos
Animated previews below are GIFs (no audio). Click any preview to open the full MP4 with audio.
### Install Demo (`clawsec-suite`)
[![Install demo animated preview](public/video/install-demo-preview.gif)](public/video/install-demo.mp4)
Direct link: [install-demo.mp4](public/video/install-demo.mp4)
### Drift Detection Demo (`soul-guardian`)
[![Drift detection animated preview](public/video/soul-guardian-demo-preview.gif)](public/video/soul-guardian-demo.mp4)
Direct link: [soul-guardian-demo.mp4](public/video/soul-guardian-demo.mp4)
---
## 🚀 Quick Start
### For AI Agents
```bash
# Fetch and install the ClawSec security suite
curl -sL https://clawsec.prompt.security/releases/latest/download/SKILL.md
# Install the ClawSec security suite
npx clawhub@latest install clawsec-suite
```
The skill file contains deployment instructions. Your agent will:
1. Detect its agent family (OpenClaw/MoltBot/ClawdBot or other)
2. Install appropriate skills from the catalog
3. Verify integrity using checksums
4. Set up cron update checks
After install, the suite can:
1. Discover installable protections from the published skills catalog
2. Verify release integrity using signed checksums
3. Set up advisory monitoring and hook-based protection flows
4. Add optional scheduled checks
Manual/source-first option:
> Read https://github.com/prompt-security/clawsec/releases/latest/download/SKILL.md and follow the installation instructions.
### For Humans
Copy this instruction to your AI agent:
> Read https://clawsec.prompt.security/releases/latest/download/SKILL.md and follow the instructions to install the protection skill suite.
> Install ClawSec with `npx clawhub@latest install clawsec-suite`, then complete the setup steps from the generated instructions.
### Shell and OS Notes
@@ -142,13 +164,13 @@ The **clawsec-suite** is a skill-of-skills manager that installs, verifies, and
| Skill | Description | Installation | Compatibility |
|-------|-------------|--------------|---------------|
| 📡 **clawsec-feed** | Security advisory feed monitoring with live CVE updates | ✅ Included by default | All agents |
| 🔭 **openclaw-audit-watchdog** | Automated daily audits with email reporting | ⚙️ Optional (install separately) | OpenClaw/MoltBot/ClawdBot |
| 🔭 **openclaw-audit-watchdog** | Automated daily audits with email reporting | ⚙️ Optional (install separately) | OpenClaw/MoltBot/Clawdbot |
| 👻 **soul-guardian** | Drift detection and file integrity guard with auto-restore | ⚙️ Optional | All agents |
| 🤝 **clawtributor** | Community incident reporting | ❌ Optional (Explicit request) | All agents |
> ⚠️ **clawtributor** is not installed by default as it may share anonymized incident data. Install only on explicit user request.
> ⚠️ **openclaw-audit-watchdog** is tailored for the OpenClaw/MoltBot/ClawdBot agent family. Other agents receive the universal skill set.
> ⚠️ **openclaw-audit-watchdog** is tailored for the OpenClaw/MoltBot/Clawdbot agent family. Other agents receive the universal skill set.
### Suite Features
@@ -170,6 +192,9 @@ ClawSec maintains a continuously updated security advisory feed, automatically p
curl -s https://clawsec.prompt.security/advisories/feed.json | jq '.advisories[] | select(.severity == "critical" or .severity == "high")'
```
Canonical endpoint: `https://clawsec.prompt.security/advisories/feed.json`
Compatibility mirror (legacy): `https://clawsec.prompt.security/releases/latest/download/feed.json`
### Monitored Keywords
The feed polls CVEs related to:
@@ -178,6 +203,17 @@ The feed polls CVEs related to:
- Prompt injection patterns
- Agent security vulnerabilities
### Exploitability Context
ClawSec enriches CVE advisories with **exploitability context** to help agents assess real-world risk beyond raw CVSS scores. Newly analyzed advisories can include:
- **Exploit Evidence**: Whether public exploits exist in the wild
- **Weaponization Status**: If exploits are integrated into common attack frameworks
- **Attack Requirements**: Prerequisites needed for successful exploitation (network access, authentication, user interaction)
- **Risk Assessment**: Contextualized risk level combining technical severity with exploitability
This feature helps agents prioritize vulnerabilities that pose immediate threats versus theoretical risks, enabling smarter security decisions.
### Advisory Schema
**NVD CVE Advisory:**
@@ -192,6 +228,8 @@ The feed polls CVEs related to:
"published": "2026-02-01T00:00:00Z",
"cvss_score": 8.8,
"nvd_url": "https://nvd.nist.gov/vuln/detail/CVE-2026-XXXXX",
"exploitability_score": "high|medium|low|unknown",
"exploitability_rationale": "Why this CVE is or is not likely exploitable in agent deployments",
"references": ["..."],
"action": "Recommended remediation"
}
@@ -215,7 +253,7 @@ The feed polls CVEs related to:
```
**Platform values:**
- `"openclaw"` - OpenClaw/ClawdBot/MoltBot only
- `"openclaw"` - OpenClaw/Clawdbot/MoltBot only
- `"nanoclaw"` - NanoClaw only
- `["openclaw", "nanoclaw"]` - Both platforms
- (empty/missing) - All platforms (backward compatible)
@@ -230,10 +268,13 @@ ClawSec uses automated pipelines for continuous security updates and skill distr
| Workflow | Trigger | Description |
|----------|---------|-------------|
| **ci.yml** | PRs to `main`, pushes to `main` | Lint/type/build + skill test suites |
| **pages-verify.yml** | PRs to `main` | Verifies Pages build and signing outputs without publishing |
| **poll-nvd-cves.yml** | Daily cron (06:00 UTC) | Polls NVD for new CVEs, updates feed |
| **community-advisory.yml** | Issue labeled `advisory-approved` | Processes community reports into advisories |
| **skill-release.yml** | `<skill>-v*.*.*` tags | Packages individual skills with checksums to GitHub Releases |
| **deploy-pages.yml** | Push to main | Builds and deploys the web interface to GitHub Pages |
| **skill-release.yml** | Skill tags + metadata PR changes | Validates version parity in PRs and publishes signed skill releases on tags |
| **deploy-pages.yml** | `workflow_run` after successful trusted CI/release or manual dispatch | Builds and deploys the web interface to GitHub Pages |
| **wiki-sync.yml** | Pushes to `main` touching `wiki/**` | Syncs `wiki/` to the GitHub Wiki mirror |
### Skill Release Pipeline
@@ -244,7 +285,7 @@ When a skill is tagged (e.g., `soul-guardian-v1.0.0`), the pipeline:
3. **Generates Checksums** - Creates `checksums.json` with SHA256 hashes for all SBOM files
4. **Signs + verifies** - Signs `checksums.json` and validates the generated `signing-public.pem` fingerprint against canonical repo key material
5. **Releases** - Publishes to GitHub Releases with all artifacts
6. **Supersedes Old Releases** - Marks older versions (same major) as pre-releases
6. **Supersedes Old Releases** - Deletes older versions within the same major line (tags remain)
7. **Triggers Pages Update** - Refreshes the skills catalog on the website
### Signing Key Consistency Guardrails
@@ -295,8 +336,8 @@ Each skill release includes:
### Signing Operations Documentation
For feed/release signing rollout and operations guidance:
- [`docs/SECURITY-SIGNING.md`](docs/SECURITY-SIGNING.md) - key generation, GitHub secrets, rotation/revocation, incident response
- [`docs/MIGRATION-SIGNED-FEED.md`](docs/MIGRATION-SIGNED-FEED.md) - phased migration from unsigned feed, enforcement gates, rollback plan
- [`wiki/security-signing-runbook.md`](wiki/security-signing-runbook.md) - key generation, GitHub secrets, rotation/revocation, incident response
- [`wiki/migration-signed-feed.md`](wiki/migration-signed-feed.md) - phased migration from unsigned feed, enforcement gates, rollback plan
---
@@ -357,8 +398,18 @@ npm run dev
# Populate advisory feed with real NVD CVE data
./scripts/populate-local-feed.sh --days 120
# Generate wiki llms exports from wiki/ (for local preview)
./scripts/populate-local-wiki.sh
# Direct generator entrypoint (used by predev/prebuild)
npm run gen:wiki-llms
```
Notes:
- `npm run dev` and `npm run build` automatically regenerate wiki `llms.txt` exports (`predev`/`prebuild` hooks).
- `public/wiki/` is generated output (local + CI) and is intentionally gitignored.
### Build
```bash
@@ -374,24 +425,34 @@ npm run build
│ └── feed.json # Main advisory feed (auto-updated from NVD)
├── components/ # React components
├── pages/ # Page components
├── wiki/ # Source-of-truth docs (synced to GitHub Wiki)
├── scripts/
│ ├── generate-wiki-llms.mjs # wiki/*.md -> public/wiki/**/llms.txt
│ ├── populate-local-feed.sh # Local CVE feed populator
│ ├── populate-local-skills.sh # Local skills catalog populator
│ ├── populate-local-wiki.sh # Local wiki llms export populator
│ └── release-skill.sh # Manual skill release helper
├── skills/
│ ├── clawsec-suite/ # 📦 Suite installer (skill-of-skills)
│ ├── clawsec-feed/ # 📡 Advisory feed skill
│ ├── clawsec-nanoclaw/ # 📱 NanoClaw platform security suite
│ ├── clawsec-clawhub-checker/ # 🧪 ClawHub reputation checks
│ ├── clawtributor/ # 🤝 Community reporting skill
│ ├── openclaw-audit-watchdog/ # 🔭 Automated audit skill
│ ├── prompt-agent/ # 🧠 Prompt-focused protection workflows
│ └── soul-guardian/ # 👻 File integrity skill
├── utils/
│ ├── package_skill.py # Skill packager utility
│ └── validate_skill.py # Skill validator utility
├── .github/workflows/
│ ├── ci.yml # Cross-platform lint/type/build + tests
│ ├── pages-verify.yml # PR-only pages build verification
│ ├── poll-nvd-cves.yml # CVE polling pipeline
│ ├── community-advisory.yml # Approved issue -> advisory PR
│ ├── skill-release.yml # Skill release pipeline
│ ├── wiki-sync.yml # Sync repo wiki/ to GitHub Wiki
│ └── deploy-pages.yml # Pages deployment
└── public/ # Static assets and published skills
└── public/ # Static assets + generated publish artifacts
```
---
@@ -419,6 +480,14 @@ See [CONTRIBUTING.md](CONTRIBUTING.md#submitting-security-advisories) for detail
4. Validate with `python utils/validate_skill.py skills/your-skill`
5. Submit a PR for review
## 📚 Documentation Source of Truth
For all wiki content, edit files under `wiki/` in this repository. The GitHub Wiki (`<repo>.wiki.git`) is synced from `wiki/` by `.github/workflows/wiki-sync.yml` when `wiki/**` changes on `main`.
LLM exports are generated from `wiki/` into `public/wiki/`:
- `/wiki/llms.txt` is the LLM-ready export for `wiki/INDEX.md` (or a generated fallback index if `INDEX.md` is missing).
- `/wiki/<page>/llms.txt` is the LLM-ready export for that single wiki page.
---
## 📄 License
+715 -70
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
Rs++ntJvBvX4zVTJ/DsrfXOQG3VTUc2x4esSURSMonesmYzSm9U9kd3rBz5d+DemJOVJ/esH21VACpdE+T34AA==
SJ1weYVVi723M8f6s8es6rg34CSPKxbvlBy1QIXdS0giskd5KTADTDLr2STqUCuWpaV7U+JQa/1eWqNX2oJ+Aw==
+1 -1
View File
@@ -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>
+3 -1
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { NavLink } from 'react-router-dom';
import { Menu, X, Terminal, Layers, Rss, Home, Github } from 'lucide-react';
import { Menu, X, Terminal, Layers, Rss, Home, Github, BookOpenText, PlayCircle } from 'lucide-react';
export const Header: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
@@ -9,6 +9,8 @@ export const Header: React.FC = () => {
{ label: 'Home', path: '/', icon: Home },
{ label: 'Skills', path: '/skills', icon: Layers },
{ label: 'Security Feed', path: '/feed', icon: Rss },
{ label: 'Product Demo', path: '/demo', icon: PlayCircle },
{ label: 'Wiki', path: '/wiki', icon: BookOpenText },
];
const baseLink =
+5 -3
View File
@@ -1,7 +1,9 @@
// Feed URL for fetching live advisories
export const ADVISORY_FEED_URL = 'https://clawsec.prompt.security/releases/latest/download/feed.json';
// Canonical hosted feed endpoint for fetching live advisories
export const ADVISORY_FEED_URL = 'https://clawsec.prompt.security/advisories/feed.json';
// Compatibility mirror for legacy clients; keep as last-resort fallback only
export const LEGACY_ADVISORY_FEED_URL = 'https://clawsec.prompt.security/releases/latest/download/feed.json';
// Local feed path for development
export const LOCAL_FEED_PATH = '/advisories/feed.json';
+1 -1
View File
@@ -113,6 +113,6 @@ export default [
}
},
{
ignores: ['dist/', 'node_modules/', '*.config.js', 'public/']
ignores: ['dist/', 'node_modules/', '*.config.js', 'public/', '.venv/']
}
];
+2 -2
View File
@@ -1,4 +1,4 @@
{
"name": "ClawSec",
"description": "A security-first skill distribution platform for OpenClaw agents (and some clones), featuring verified audit skills, hardening feeds, and guardian mode protocols."
}
"description": "A security-first skill distribution platform for OpenClaw and NanoClaw agents, featuring verified audit skills, hardening feeds, and guardian mode protocols."
}
+455 -382
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -5,16 +5,20 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"gen:wiki-llms": "node scripts/generate-wiki-llms.mjs",
"populate-local-wiki": "./scripts/populate-local-wiki.sh",
"predev": "npm run gen:wiki-llms",
"dev": "vite",
"prebuild": "npm run gen:wiki-llms",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.564.0",
"lucide-react": "^0.575.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.13.0",
"react-router-dom": "^7.13.1",
"remark-gfm": "^4.0.1"
},
"devDependencies": {
@@ -26,6 +30,7 @@
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"fast-check": "^4.5.3",
"typescript": "~5.8.2",
"vite": "^7.3.1"
},
@@ -33,6 +38,6 @@
"ajv": "6.14.0",
"balanced-match": "4.0.3",
"brace-expansion": "5.0.2",
"minimatch": "10.2.1"
"minimatch": "10.2.4"
}
}
+10 -2
View File
@@ -3,7 +3,11 @@ 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';
import {
ADVISORY_FEED_URL,
LEGACY_ADVISORY_FEED_URL,
LOCAL_FEED_PATH,
} from '../constants';
export const AdvisoryDetail: React.FC = () => {
const { advisoryId } = useParams<{ advisoryId: string }>();
@@ -16,13 +20,17 @@ export const AdvisoryDetail: React.FC = () => {
if (!advisoryId) return;
try {
// Try local feed first (for development), then fall back to GitHub releases
// Try local feed first (dev), then canonical hosted endpoint, then legacy mirror.
let response = await fetch(LOCAL_FEED_PATH);
if (!response.ok) {
response = await fetch(ADVISORY_FEED_URL);
}
if (!response.ok) {
response = await fetch(LEGACY_ADVISORY_FEED_URL);
}
if (!response.ok) {
throw new Error(`Failed to fetch feed: ${response.status}`);
}
+75 -11
View File
@@ -1,19 +1,59 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } 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';
import {
ADVISORY_FEED_URL,
LEGACY_ADVISORY_FEED_URL,
LOCAL_FEED_PATH,
} from '../constants';
const ITEMS_PER_PAGE = 9;
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;
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;
const FilterTabs: React.FC<{
tabs: ReadonlyArray<{ value: string; label: string; active: string; inactive: string }>;
selected: string;
onSelect: (value: string) => void;
}> = ({ tabs, selected, onSelect }) => (
<div className="flex flex-wrap justify-center gap-3 mb-8">
{tabs.map(({ value, label, active, inactive }) => (
<button
key={value}
onClick={() => onSelect(value)}
className={`px-4 py-2 rounded-lg text-sm font-semibold transition-all ${
selected === value ? active : inactive
}`}
>
{label}
</button>
))}
</div>
);
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);
const [selectedSeverity, setSelectedSeverity] = useState<string>('all');
const [selectedPlatform, setSelectedPlatform] = useState<string>('all');
useEffect(() => {
const fetchAdvisories = async () => {
@@ -21,13 +61,17 @@ export const FeedSetup: React.FC = () => {
setError(null);
try {
// Try local feed first (for development), then fall back to GitHub releases
// Try local feed first (dev), then canonical hosted endpoint, then legacy mirror.
let response = await fetch(LOCAL_FEED_PATH);
if (!response.ok) {
response = await fetch(ADVISORY_FEED_URL);
}
if (!response.ok) {
response = await fetch(LEGACY_ADVISORY_FEED_URL);
}
if (!response.ok) {
throw new Error(`Failed to fetch feed: ${response.status}`);
}
@@ -47,6 +91,18 @@ export const FeedSetup: React.FC = () => {
fetchAdvisories();
}, []);
const filteredAdvisories = useMemo(
() => advisories.filter((a) =>
(selectedSeverity === 'all' || a.severity === selectedSeverity) &&
(selectedPlatform === 'all' || !a.platforms?.length || a.platforms.includes(selectedPlatform))
),
[advisories, selectedSeverity, selectedPlatform],
);
useEffect(() => {
setCurrentPage(1);
}, [advisories, selectedSeverity, selectedPlatform]);
const formatDate = (dateStr: string) => {
try {
return new Date(dateStr).toLocaleDateString('en-US', {
@@ -60,10 +116,10 @@ export const FeedSetup: React.FC = () => {
};
// Pagination calculations
const totalPages = Math.ceil(advisories.length / ITEMS_PER_PAGE);
const totalPages = Math.ceil(filteredAdvisories.length / ITEMS_PER_PAGE);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const currentAdvisories = advisories.slice(startIndex, endIndex);
const currentAdvisories = filteredAdvisories.slice(startIndex, endIndex);
const goToPage = (page: number) => {
setCurrentPage(Math.max(1, Math.min(page, totalPages)));
@@ -76,7 +132,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">
@@ -86,6 +142,9 @@ export const FeedSetup: React.FC = () => {
</section>
<section>
<FilterTabs tabs={SEVERITY_TABS} selected={selectedSeverity} onSelect={setSelectedSeverity} />
<FilterTabs tabs={PLATFORM_TABS} selected={selectedPlatform} onSelect={setSelectedPlatform} />
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 text-clawd-accent animate-spin" />
@@ -96,9 +155,13 @@ export const FeedSetup: React.FC = () => {
<AlertTriangle className="w-6 h-6 text-orange-400 mr-2" />
<span className="text-gray-400">{error}</span>
</div>
) : advisories.length === 0 ? (
) : filteredAdvisories.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-400">No security advisories at this time. Check back later.</p>
<p className="text-gray-400">
{advisories.length === 0
? 'No security advisories at this time. Check back later.'
: 'No advisories found for the selected filters.'}
</p>
</div>
) : (
<>
@@ -133,9 +196,10 @@ export const FeedSetup: React.FC = () => {
</div>
)}
{advisories.length > 0 && (
{filteredAdvisories.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
Showing {startIndex + 1}-{Math.min(endIndex, filteredAdvisories.length)} of {filteredAdvisories.length} advisories
{(selectedSeverity !== 'all' || selectedPlatform !== 'all') && ` (${advisories.length} total)`}
</p>
)}
</>
+80 -11
View File
@@ -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); }
+92
View File
@@ -0,0 +1,92 @@
import React from 'react';
import { ExternalLink, PlayCircle } from 'lucide-react';
import { Footer } from '../components/Footer';
interface DemoVideo {
id: string;
title: string;
description: string;
videoSrc: string;
posterSrc: string;
videoContainerClassName?: string;
}
const demoVideos: DemoVideo[] = [
{
id: 'drift-demo',
title: 'Drift Detection Demo (soul-guardian)',
description:
'Shows integrity monitoring in action: tamper detection, alerting, and restoration-oriented behavior for protected files.',
videoSrc: '/video/soul-guardian-demo.mp4',
posterSrc: '/video/soul-guardian-demo-poster.jpg',
},
{
id: 'install-demo',
title: 'Install Demo (clawsec-suite)',
description:
'Walkthrough of the one-command suite install flow and what gets configured for advisory monitoring and protection.',
videoSrc: '/video/install-demo.mp4',
posterSrc: '/video/install-demo-poster.jpg',
videoContainerClassName: 'md:max-w-[50%]',
},
];
export const ProductDemo: React.FC = () => {
return (
<div className="max-w-5xl mx-auto pt-[52px] space-y-10">
<section className="text-center space-y-4">
<h1 className="text-3xl md:text-4xl text-white flex items-center justify-center gap-3">
<PlayCircle className="text-clawd-accent" />
Watch It in Action
</h1>
<p className="text-gray-400 max-w-3xl mx-auto">
Product demos for ClawSec installation and runtime protection behavior. These are the
same demo assets referenced in the repository README, presented as playable videos.
</p>
</section>
<section className="space-y-8">
{demoVideos.map((demo) => (
<article
key={demo.id}
className="bg-clawd-900 border border-clawd-700 rounded-xl overflow-hidden"
>
<div className="px-6 pt-6 pb-4 space-y-3">
<h2 className="text-xl text-white">{demo.title}</h2>
<p className="text-gray-400">{demo.description}</p>
</div>
<div className="px-6 pb-6 space-y-4">
<div
className={`rounded-lg overflow-hidden border border-clawd-700 bg-black ${
demo.videoContainerClassName ?? ''
}`}
>
<video
className="w-full h-auto"
controls
playsInline
preload="metadata"
poster={demo.posterSrc}
>
<source src={demo.videoSrc} type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
<a
href={demo.videoSrc}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm text-clawd-accent hover:underline"
>
<ExternalLink size={15} />
Open video in new tab
</a>
</div>
</article>
))}
</section>
<Footer />
</div>
);
};
+38 -117
View File
@@ -5,11 +5,12 @@ import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Footer } from '../components/Footer';
import type { SkillJson, SkillChecksums } from '../types';
import { defaultMarkdownComponents } from '../utils/markdownComponents';
import { stripFrontmatter } from '../utils/markdownHelpers.mjs';
// Strip YAML frontmatter from markdown content
const stripFrontmatter = (content: string): string => {
const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/;
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 = () => {
@@ -29,19 +30,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 +77,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 +89,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;
};
@@ -300,102 +316,7 @@ export const SkillDetail: React.FC = () => {
<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>
),
}}
components={defaultMarkdownComponents}
>
{stripFrontmatter(doc.content)}
</Markdown>
+52 -5
View File
@@ -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);
}
+375
View File
@@ -0,0 +1,375 @@
import React, { useMemo } from 'react';
import { BookOpenText, ExternalLink, FileText } from 'lucide-react';
import { Link, useParams } from 'react-router-dom';
import Markdown from 'react-markdown';
import type { Components } from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Footer } from '../components/Footer';
import { defaultMarkdownComponents } from '../utils/markdownComponents';
import {
extractTitleFromMarkdown,
fallbackTitleFromPath,
stripFrontmatter,
} from '../utils/markdownHelpers.mjs';
import {
isWikiIndexSlug,
toWikiLlmsPath,
toWikiRoute,
} from '../utils/wikiPathHelpers.mjs';
interface WikiDoc {
filePath: string;
slug: string;
title: string;
content: string;
}
const normalizePath = (path: string): string => {
const clean = path.replace(/\\/g, '/');
const parts: string[] = [];
for (const part of clean.split('/')) {
if (!part || part === '.') continue;
if (part === '..') {
if (parts.length > 0) parts.pop();
continue;
}
parts.push(part);
}
return parts.join('/');
};
const dirname = (path: string): string => {
const idx = path.lastIndexOf('/');
return idx === -1 ? '' : path.slice(0, idx);
};
const resolveFromFile = (currentFilePath: string, targetPath: string): string => {
if (!targetPath) return currentFilePath;
if (targetPath.startsWith('/')) return normalizePath(targetPath.slice(1));
const baseDir = dirname(currentFilePath);
const joined = baseDir ? `${baseDir}/${targetPath}` : targetPath;
return normalizePath(joined);
};
const splitHash = (href: string): { path: string; hash: string } => {
const idx = href.indexOf('#');
if (idx === -1) return { path: href, hash: '' };
return { path: href.slice(0, idx), hash: href.slice(idx) };
};
const toWikiRelativePath = (globPath: string): string =>
globPath.replace(/^\.\.\/wiki\//, '').replace(/\\/g, '/');
const isExternalHref = (href: string): boolean =>
/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(href) || href.startsWith('//');
const ALLOWED_LINK_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:']);
const ALLOWED_IMAGE_SCHEMES = new Set(['http:', 'https:']);
const sanitizeHref = (href: string): string | null => {
const trimmed = href.trim();
if (!trimmed) return null;
if (trimmed.startsWith('//')) return null;
const schemeMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*:)/);
if (!schemeMatch) return trimmed;
return ALLOWED_LINK_SCHEMES.has(schemeMatch[1].toLowerCase()) ? trimmed : null;
};
const sanitizeImageSrc = (src: string): string | null => {
const trimmed = src.trim();
if (!trimmed) return null;
if (trimmed.startsWith('//')) return null;
const schemeMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9+.-]*:)/);
if (!schemeMatch) return trimmed;
return ALLOWED_IMAGE_SCHEMES.has(schemeMatch[1].toLowerCase()) ? trimmed : null;
};
const markdownModules = import.meta.glob('../wiki/**/*.md', {
eager: true,
query: '?raw',
import: 'default',
}) as Record<string, string>;
const assetModules = import.meta.glob('../wiki/**/*.{png,jpg,jpeg,gif,svg,webp,avif}', {
eager: true,
import: 'default',
}) as Record<string, string>;
const wikiDocs: WikiDoc[] = Object.entries(markdownModules)
.map(([globPath, content]) => {
const filePath = toWikiRelativePath(globPath);
return {
filePath,
slug: filePath.replace(/\.md$/i, ''),
title: extractTitleFromMarkdown(content, filePath),
content: stripFrontmatter(content).trim(),
};
})
.sort((a, b) => {
const aIndex = a.slug.toLowerCase() === 'index';
const bIndex = b.slug.toLowerCase() === 'index';
if (aIndex && !bIndex) return -1;
if (!aIndex && bIndex) return 1;
const aModule = a.filePath.startsWith('modules/');
const bModule = b.filePath.startsWith('modules/');
if (aModule !== bModule) return aModule ? 1 : -1;
return a.title.localeCompare(b.title, 'en', { sensitivity: 'base' });
});
const wikiDocBySlug = new Map<string, WikiDoc>(
wikiDocs.map((doc) => [doc.slug.toLowerCase(), doc]),
);
const wikiDocByFilePath = new Map<string, WikiDoc>(
wikiDocs.map((doc) => [doc.filePath.toLowerCase(), doc]),
);
const wikiAssetByPath = new Map<string, string>(
Object.entries(assetModules).map(([globPath, assetUrl]) => [
toWikiRelativePath(globPath).toLowerCase(),
assetUrl,
]),
);
const defaultDoc = wikiDocBySlug.get('index') ?? wikiDocs[0] ?? null;
const toGroupName = (filePath: string): string => {
if (!filePath.includes('/')) return 'Core';
if (filePath.startsWith('modules/')) return 'Modules';
const [firstSegment] = filePath.split('/');
return fallbackTitleFromPath(firstSegment);
};
export const WikiBrowser: React.FC = () => {
const params = useParams<{ '*': string }>();
const wildcard = params['*'] ?? '';
const normalizedWildcard = wildcard.replace(/^\/+|\/+$/g, '');
let requested = '';
let decodeFailed = false;
try {
requested = decodeURIComponent(normalizedWildcard);
} catch (error) {
decodeFailed = normalizedWildcard.length > 0;
console.warn('Failed to decode wiki route segment', { wildcard, error });
requested = '';
}
const requestedSlug = requested || 'INDEX';
const selectedDoc = wikiDocBySlug.get(requestedSlug.toLowerCase()) ?? defaultDoc;
const notFound =
(decodeFailed && normalizedWildcard.length > 0) ||
(requested.length > 0 && !wikiDocBySlug.has(requestedSlug.toLowerCase()));
const groupedDocs = useMemo(() => {
const map = new Map<string, WikiDoc[]>();
for (const doc of wikiDocs) {
const group = toGroupName(doc.filePath);
const existing = map.get(group) ?? [];
existing.push(doc);
map.set(group, existing);
}
const preferredOrder = ['Core', 'Modules'];
return Array.from(map.entries())
.sort(([a], [b]) => {
const idxA = preferredOrder.indexOf(a);
const idxB = preferredOrder.indexOf(b);
if (idxA !== -1 || idxB !== -1) {
if (idxA === -1) return 1;
if (idxB === -1) return -1;
return idxA - idxB;
}
return a.localeCompare(b, 'en', { sensitivity: 'base' });
})
.map(([name, docs]) => ({
name,
docs: docs.sort((a, b) =>
a.title.localeCompare(b.title, 'en', { sensitivity: 'base' }),
),
}));
}, []);
if (!selectedDoc) {
return (
<div className="pt-[52px] py-20 text-center space-y-4">
<BookOpenText className="w-12 h-12 text-gray-500 mx-auto" />
<h1 className="text-2xl text-white">Wiki unavailable</h1>
<p className="text-gray-400">No markdown files were found in the wiki source.</p>
</div>
);
}
const activeSlug = selectedDoc.slug.toLowerCase();
const pageLlmsPath = toWikiLlmsPath(activeSlug);
const showWikiLlmsIndexLink = !isWikiIndexSlug(activeSlug);
const resolveWikiRouteFromHref = (href: string): string | null => {
if (!href || isExternalHref(href) || href.startsWith('mailto:') || href.startsWith('tel:')) {
return null;
}
const { path, hash } = splitHash(href);
if (!path || !path.toLowerCase().endsWith('.md')) return null;
const resolvedFilePath = resolveFromFile(selectedDoc.filePath, path).toLowerCase();
const targetDoc = wikiDocByFilePath.get(resolvedFilePath);
if (!targetDoc) return null;
return `${toWikiRoute(targetDoc.slug)}${hash}`;
};
const resolveAssetUrl = (srcOrHref: string): string | null => {
if (!srcOrHref || isExternalHref(srcOrHref) || srcOrHref.startsWith('/')) return null;
const { path } = splitHash(srcOrHref);
if (!path) return null;
const resolvedAssetPath = resolveFromFile(selectedDoc.filePath, path).toLowerCase();
return wikiAssetByPath.get(resolvedAssetPath) ?? null;
};
const wikiMarkdownComponents: Components = {
...defaultMarkdownComponents,
a: ({ href, children }) => {
if (!href) return <span className="text-gray-300">{children}</span>;
const wikiRoute = resolveWikiRouteFromHref(href);
if (wikiRoute) {
return (
<Link to={wikiRoute} className="text-clawd-accent hover:underline">
{children}
</Link>
);
}
const assetHref = resolveAssetUrl(href);
const finalHref = assetHref ?? href;
const safeHref = sanitizeHref(finalHref);
if (!safeHref) {
return <span className="text-gray-300">{children}</span>;
}
const external = isExternalHref(safeHref);
return (
<a
href={safeHref}
target={external ? '_blank' : undefined}
rel={external ? 'noopener noreferrer' : undefined}
className="text-clawd-accent hover:underline"
>
{children}
</a>
);
},
img: ({ src, alt }) => {
const resolvedSrc = src ? resolveAssetUrl(src) : null;
const finalSrc = resolvedSrc ?? (src ? sanitizeImageSrc(src) : null);
if (!finalSrc) {
return <span className="text-gray-500 text-sm">[image blocked]</span>;
}
return (
<img
src={finalSrc}
alt={alt ?? ''}
className="max-w-full h-auto rounded-lg border border-clawd-700 bg-clawd-900/40 p-2 my-4"
loading="lazy"
/>
);
},
};
return (
<div className="pt-[52px] space-y-8">
<section className="space-y-3">
<h1 className="text-3xl md:text-4xl text-white flex items-center gap-3">
<BookOpenText className="text-clawd-accent" />
Wiki
</h1>
<p className="text-gray-400 max-w-3xl">
Full repository wiki rendered from markdown in <code className="text-gray-300">wiki/</code>.
This is the same source synced to GitHub Wiki.
</p>
<div className="flex flex-wrap gap-3">
<a
href={pageLlmsPath}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-clawd-700 hover:bg-clawd-600 text-white text-sm transition-colors"
>
<FileText size={15} />
Page llms.txt
</a>
{showWikiLlmsIndexLink && (
<a
href="/wiki/llms.txt"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-clawd-800 border border-clawd-700 hover:border-clawd-accent text-white text-sm transition-colors"
>
<FileText size={15} />
Wiki llms.txt Index
</a>
)}
<a
href="https://github.com/prompt-security/clawsec/wiki"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md border border-clawd-700 hover:border-clawd-accent text-gray-200 text-sm transition-colors"
>
<ExternalLink size={15} />
GitHub Wiki
</a>
</div>
</section>
<div className="grid lg:grid-cols-[280px_minmax(0,1fr)] gap-6 items-start">
<aside className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-4 lg:sticky lg:top-20 max-h-[calc(100vh-7rem)] overflow-auto">
<div className="space-y-5">
{groupedDocs.map((group) => (
<section key={group.name} className="space-y-2">
<h2 className="text-xs uppercase tracking-wide text-gray-400">{group.name}</h2>
<div className="space-y-1">
{group.docs.map((doc) => {
const isActive = activeSlug === doc.slug.toLowerCase();
return (
<Link
key={doc.filePath}
to={toWikiRoute(doc.slug)}
className={`block px-3 py-2 rounded-md text-sm transition-colors ${
isActive
? 'bg-white/10 text-white border border-white/10'
: 'text-gray-300 hover:text-white hover:bg-white/5'
}`}
>
{doc.title}
</Link>
);
})}
</div>
</section>
))}
</div>
</aside>
<section className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-4 sm:p-6 md:p-8 overflow-x-hidden">
{notFound && (
<div className="mb-6 p-3 rounded-md border border-orange-800 bg-orange-900/20 text-orange-200 text-sm">
Wiki page not found for <code>{requested}</code>. Showing <strong>{selectedDoc.title}</strong> instead.
</div>
)}
<Markdown
remarkPlugins={[remarkGfm]}
components={wikiMarkdownComponents}
>
{selectedDoc.content}
</Markdown>
</section>
</div>
<Footer />
</div>
);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.
+281
View File
@@ -0,0 +1,281 @@
#!/bin/bash
# backfill-exploitability.sh
# Adds exploitability scoring to existing advisories in feed.json that don't have it yet.
# Historical maintenance utility: normal advisory generation should use
# poll-nvd workflow (init/reset when rebuilding) or populate-local-feed.sh.
#
# Usage: ./scripts/backfill-exploitability.sh [--dry-run] [--feed PATH]
# --dry-run Show what would be updated without making changes
# --feed PATH Use specified feed file (default: advisories/feed.json)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# shellcheck source=./feed-utils.sh
source "$SCRIPT_DIR/feed-utils.sh"
# Configuration
init_feed_paths "$PROJECT_ROOT"
ANALYZER="$PROJECT_ROOT/utils/analyze_exploitability.py"
SIGNING_PRIVATE_KEY="${CLAWSEC_FEED_SIGNING_PRIVATE_KEY_PATH:-${CLAWSEC_SIGNING_PRIVATE_KEY_PATH:-$PROJECT_ROOT/clawsec-signing-private.pem}}"
SIGNING_PUBLIC_KEY="${CLAWSEC_FEED_SIGNING_PUBLIC_KEY_PATH:-${CLAWSEC_SIGNING_PUBLIC_KEY_PATH:-$PROJECT_ROOT/clawsec-signing-public.pem}}"
SIGNING_PASSPHRASE="${CLAWSEC_FEED_SIGNING_PRIVATE_KEY_PASSPHRASE:-${CLAWSEC_SIGNING_PRIVATE_KEY_PASSPHRASE:-}}"
sign_and_verify_feed_signature() {
local feed_file="$1"
local signature_file="$2"
local tmp_dir
local tmp_signature
local signature_bin
local passin_file
tmp_dir=$(mktemp -d)
tmp_signature="${signature_file}.tmp.$$"
signature_bin="$tmp_dir/signature.bin"
passin_file="$tmp_dir/passin.txt"
if [ -n "$SIGNING_PASSPHRASE" ]; then
printf '%s' "$SIGNING_PASSPHRASE" > "$passin_file"
chmod 600 "$passin_file"
if ! openssl pkeyutl -sign -rawin -inkey "$SIGNING_PRIVATE_KEY" -passin "file:$passin_file" -in "$feed_file" \
| openssl base64 -A > "$tmp_signature"; then
rm -rf "$tmp_dir"
rm -f "$tmp_signature"
echo "Error: Failed to sign $feed_file" >&2
return 1
fi
elif ! openssl pkeyutl -sign -rawin -inkey "$SIGNING_PRIVATE_KEY" -in "$feed_file" \
| openssl base64 -A > "$tmp_signature"; then
rm -rf "$tmp_dir"
rm -f "$tmp_signature"
echo "Error: Failed to sign $feed_file" >&2
return 1
fi
if ! openssl base64 -d -A -in "$tmp_signature" -out "$signature_bin"; then
rm -rf "$tmp_dir"
rm -f "$tmp_signature"
echo "Error: Failed to decode generated signature for $feed_file" >&2
return 1
fi
if ! openssl pkeyutl -verify -rawin -pubin -inkey "$SIGNING_PUBLIC_KEY" -sigfile "$signature_bin" -in "$feed_file" >/dev/null; then
rm -rf "$tmp_dir"
rm -f "$tmp_signature"
echo "Error: Signature verification failed after signing $feed_file" >&2
return 1
fi
mv "$tmp_signature" "$signature_file"
rm -rf "$tmp_dir"
echo "✓ Re-signed and verified: $signature_file"
}
# Parse args
DRY_RUN=false
REQUIRE_SIGNING=false
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run)
DRY_RUN=true
shift
;;
--feed)
FEED_PATH="$2"
shift 2
;;
*)
echo "Unknown option: $1"
echo "Usage: $0 [--dry-run] [--feed PATH]"
exit 1
;;
esac
done
echo "=== ClawSec Exploitability Backfill ==="
echo "Feed path: $FEED_PATH"
echo "Dry run: $DRY_RUN"
echo ""
# Verify prerequisites
if [ ! -f "$FEED_PATH" ]; then
echo "Error: Feed file not found: $FEED_PATH"
exit 1
fi
if [ ! -f "$ANALYZER" ]; then
echo "Error: Analyzer script not found: $ANALYZER"
exit 1
fi
# Check Python availability
if ! command -v python3 &> /dev/null; then
echo "Error: python3 is required but not found in PATH"
exit 1
fi
# Verify analyzer works
if ! python3 "$ANALYZER" --help &> /dev/null; then
echo "Error: Analyzer script failed to run. Check Python environment."
exit 1
fi
# Determine whether detached signatures must be regenerated.
# Runtime agents that only have public keys should run in dry-run mode.
if [ "$DRY_RUN" = "false" ]; then
if [ -f "${FEED_PATH}.sig" ]; then
REQUIRE_SIGNING=true
fi
fi
if [ "$REQUIRE_SIGNING" = "true" ]; then
if ! command -v openssl &> /dev/null; then
echo "Error: openssl is required for detached signature signing/verification"
exit 1
fi
if [ ! -f "$SIGNING_PRIVATE_KEY" ]; then
echo "Error: Signing private key not found: $SIGNING_PRIVATE_KEY"
echo "This backfill updates signed feed artifacts. Use --dry-run in public-key-only environments."
exit 1
fi
if [ ! -f "$SIGNING_PUBLIC_KEY" ]; then
echo "Error: Signing public key not found: $SIGNING_PUBLIC_KEY"
exit 1
fi
fi
# Create temp directory
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
echo "=== Analyzing Feed ==="
# Extract advisories without exploitability_score
jq '.advisories | map(select(.exploitability_score == null or .exploitability_score == ""))' \
"$FEED_PATH" > "$TEMP_DIR/missing_exploitability.json"
MISSING_COUNT=$(jq 'length' "$TEMP_DIR/missing_exploitability.json")
TOTAL_COUNT=$(jq '.advisories | length' "$FEED_PATH")
ALREADY_DONE=$((TOTAL_COUNT - MISSING_COUNT))
echo "Total advisories: $TOTAL_COUNT"
echo "Already have exploitability: $ALREADY_DONE"
echo "Missing exploitability: $MISSING_COUNT"
echo ""
if [ "$MISSING_COUNT" -eq 0 ]; then
echo "✓ All advisories already have exploitability scores!"
exit 0
fi
if [ "$DRY_RUN" = "true" ]; then
echo "=== Dry Run - Would Update These Advisories ==="
jq -r '.[] | .id' "$TEMP_DIR/missing_exploitability.json"
echo ""
echo "Total advisories to update: $MISSING_COUNT"
exit 0
fi
echo "=== Processing Advisories ==="
# Process each advisory
PROCESSED=0
FAILED=0
# Read original feed to preserve all metadata
cp "$FEED_PATH" "$TEMP_DIR/feed_working.json"
while IFS= read -r advisory; do
CVE_ID=$(echo "$advisory" | jq -r '.id')
echo -n "Processing $CVE_ID... "
# Prepare input for analyzer
ANALYZER_INPUT=$(echo "$advisory" | jq '{
cve_id: .id,
cvss_score: (.cvss_score // 0.0),
type: .type,
description: .description,
references: (.references // [])
}')
# Run analyzer
if ANALYSIS=$(echo "$ANALYZER_INPUT" | python3 "$ANALYZER" --json --check-exploits 2>/dev/null); then
# Extract exploitability fields
EXPL_SCORE=$(echo "$ANALYSIS" | jq -r '.exploitability_score // "unknown"')
EXPL_RATIONALE=$(echo "$ANALYSIS" | jq -r '.exploitability_rationale // "No rationale available"')
# Update advisory in working feed
jq --arg id "$CVE_ID" \
--arg score "$EXPL_SCORE" \
--arg rationale "$EXPL_RATIONALE" \
'(.advisories[] | select(.id == $id)) |= (. + {
exploitability_score: $score,
exploitability_rationale: $rationale
})' "$TEMP_DIR/feed_working.json" > "$TEMP_DIR/feed_updated.json"
mv "$TEMP_DIR/feed_updated.json" "$TEMP_DIR/feed_working.json"
echo "$EXPL_SCORE"
PROCESSED=$((PROCESSED + 1))
else
echo "✗ Failed"
FAILED=$((FAILED + 1))
fi
done < <(jq -c '.[]' "$TEMP_DIR/missing_exploitability.json")
# Check if loop executed successfully
if [ ! -f "$TEMP_DIR/feed_working.json" ]; then
echo "Error: Feed processing failed"
exit 1
fi
echo ""
echo "=== Processing Complete ==="
echo "Processed: $PROCESSED"
echo "Failed: $FAILED"
echo ""
# Write updated feed
echo "Writing updated feed to: $FEED_PATH"
cp "$TEMP_DIR/feed_working.json" "$FEED_PATH"
# Update feed version and timestamp
CURRENT_VERSION=$(jq -r '.version' "$FEED_PATH")
UPDATED_TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
jq --arg ts "$UPDATED_TS" '.updated = $ts' "$FEED_PATH" > "$TEMP_DIR/feed_final.json"
mv "$TEMP_DIR/feed_final.json" "$FEED_PATH"
echo "✓ Updated feed version: $CURRENT_VERSION"
echo "✓ Updated timestamp: $UPDATED_TS"
echo ""
if [ "$REQUIRE_SIGNING" = "true" ]; then
echo ""
echo "=== Re-signing Advisory Feed ==="
if [ -f "${FEED_PATH}.sig" ]; then
if ! sign_and_verify_feed_signature "$FEED_PATH" "${FEED_PATH}.sig"; then
exit 1
fi
fi
fi
echo ""
echo "=== Summary ==="
echo "✓ Backfill complete!"
echo "$PROCESSED advisories updated with exploitability scores"
if [ "$FAILED" -gt 0 ]; then
echo "$FAILED advisories failed analysis (kept original data)"
fi
# Verify final state
FINAL_MISSING=$(jq '.advisories | map(select(.exploitability_score == null or .exploitability_score == "")) | length' "$FEED_PATH")
echo "✓ Advisories still missing exploitability: $FINAL_MISSING"
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
scripts/ci/enrich_exploitability.sh --mode single|batch --input <path> --output <path> [--cvss-vectors <path>] [--analyzer <path>]
Options:
--mode Processing mode: single advisory object or batch advisory array
--input Input JSON path
--output Output JSON path
--cvss-vectors Optional JSON object mapping advisory id -> CVSS vector
--analyzer Optional analyzer path (default: utils/analyze_exploitability.py)
--help Show this help
EOF
}
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$REPO_ROOT"
MODE=""
INPUT_PATH=""
OUTPUT_PATH=""
CVSS_VECTORS_PATH=""
ANALYZER_PATH="utils/analyze_exploitability.py"
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
MODE="${2:-}"
shift 2
;;
--input)
INPUT_PATH="${2:-}"
shift 2
;;
--output)
OUTPUT_PATH="${2:-}"
shift 2
;;
--cvss-vectors)
CVSS_VECTORS_PATH="${2:-}"
shift 2
;;
--analyzer)
ANALYZER_PATH="${2:-}"
shift 2
;;
--help|-h)
usage
exit 0
;;
*)
echo "ERROR: Unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ "$MODE" != "single" && "$MODE" != "batch" ]]; then
echo "ERROR: --mode must be one of: single, batch" >&2
exit 1
fi
if [[ -z "$INPUT_PATH" || -z "$OUTPUT_PATH" ]]; then
echo "ERROR: --input and --output are required" >&2
exit 1
fi
if [[ ! -f "$INPUT_PATH" ]]; then
echo "ERROR: input file not found: $INPUT_PATH" >&2
exit 1
fi
if [[ ! -f "$ANALYZER_PATH" ]]; then
echo "ERROR: analyzer file not found: $ANALYZER_PATH" >&2
exit 1
fi
if [[ -n "$CVSS_VECTORS_PATH" && ! -f "$CVSS_VECTORS_PATH" ]]; then
echo "ERROR: --cvss-vectors file not found: $CVSS_VECTORS_PATH" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq is required" >&2
exit 1
fi
if command -v python >/dev/null 2>&1; then
PYTHON_BIN="python"
elif command -v python3 >/dev/null 2>&1; then
PYTHON_BIN="python3"
else
echo "ERROR: python or python3 is required" >&2
exit 1
fi
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
resolve_cvss_vector() {
local advisory_json="$1"
local advisory_id
advisory_id="$(echo "$advisory_json" | jq -r '.id // ""')"
if [[ -n "$CVSS_VECTORS_PATH" ]]; then
jq -r --arg id "$advisory_id" '.[$id] // ""' "$CVSS_VECTORS_PATH"
else
echo "$advisory_json" | jq -r '.cvss_vector // ""'
fi
}
severity_to_cvss() {
case "$1" in
critical) echo "9.5" ;;
high) echo "7.5" ;;
medium) echo "5.5" ;;
low) echo "3.0" ;;
*) echo "5.0" ;;
esac
}
build_analysis_input() {
local advisory_json="$1"
local mode="$2"
local cve_id cvss_score cvss_vector vuln_type description references severity
cve_id="$(echo "$advisory_json" | jq -r '.id // ""')"
vuln_type="$(echo "$advisory_json" | jq -r '.type // ""')"
description="$(echo "$advisory_json" | jq -r '.description // ""')"
references="$(echo "$advisory_json" | jq -c '.references // []')"
cvss_vector="$(resolve_cvss_vector "$advisory_json")"
if [[ "$mode" == "single" ]]; then
severity="$(echo "$advisory_json" | jq -r '.severity // "medium"')"
cvss_score="$(severity_to_cvss "$severity")"
else
cvss_score="$(echo "$advisory_json" | jq -r '.cvss_score // 0')"
fi
jq -n \
--arg cve_id "$cve_id" \
--argjson cvss_score "$cvss_score" \
--arg cvss_vector "$cvss_vector" \
--arg type "$vuln_type" \
--arg description "$description" \
--argjson references "$references" \
'{
cve_id: $cve_id,
cvss_score: $cvss_score,
cvss_vector: $cvss_vector,
type: $type,
description: $description,
references: $references
}'
}
run_analysis() {
local advisory_json="$1"
local mode="$2"
local output_file="$3"
local advisory_id analysis_input analysis
advisory_id="$(echo "$advisory_json" | jq -r '.id // "unknown"')"
analysis_input="$(build_analysis_input "$advisory_json" "$mode")"
if analysis="$(echo "$analysis_input" | "$PYTHON_BIN" "$ANALYZER_PATH" --json --check-exploits 2>/dev/null)"; then
echo "$analysis" > "$output_file"
return 0
fi
echo "::warning::Failed to analyze exploitability for $advisory_id, continuing without enrichment"
return 1
}
enrich_single() {
if ! jq -e 'type == "object"' "$INPUT_PATH" >/dev/null; then
echo "ERROR: single mode expects JSON object at $INPUT_PATH" >&2
exit 1
fi
local advisory analysis_file output_tmp
advisory="$(cat "$INPUT_PATH")"
analysis_file="$tmpdir/analysis_single.json"
output_tmp="$tmpdir/output_single.json"
if run_analysis "$advisory" "single" "$analysis_file"; then
jq --slurpfile analysis "$analysis_file" '
. + {
exploitability_score: $analysis[0].exploitability_score,
exploitability_rationale: $analysis[0].exploitability_rationale,
attack_vector_analysis: $analysis[0].attack_vector_analysis,
exploit_detection: $analysis[0].exploit_detection
}
' "$INPUT_PATH" > "$output_tmp"
else
cp "$INPUT_PATH" "$output_tmp"
fi
mv "$output_tmp" "$OUTPUT_PATH"
echo "Exploitability enrichment complete (single): $OUTPUT_PATH"
}
enrich_batch() {
if ! jq -e 'type == "array"' "$INPUT_PATH" >/dev/null; then
echo "ERROR: batch mode expects JSON array at $INPUT_PATH" >&2
exit 1
fi
local analyzed_count failed_count index advisory analysis_file output_tmp analyses_json
analyzed_count=0
failed_count=0
index=0
analyses_json="$tmpdir/analyses.json"
output_tmp="$tmpdir/output_batch.json"
while IFS= read -r advisory; do
analysis_file="$tmpdir/analysis_${index}.json"
if run_analysis "$advisory" "batch" "$analysis_file"; then
analyzed_count=$((analyzed_count + 1))
else
failed_count=$((failed_count + 1))
rm -f "$analysis_file"
fi
index=$((index + 1))
done < <(jq -c '.[]' "$INPUT_PATH")
if ls "$tmpdir"/analysis_*.json >/dev/null 2>&1; then
jq -s '.' "$tmpdir"/analysis_*.json > "$analyses_json"
else
echo '[]' > "$analyses_json"
fi
jq --slurpfile analyses "$analyses_json" '
map(
. as $advisory |
($analyses[0] | map(select(.cve_id == $advisory.id)) | first) as $analysis |
if $analysis then
$advisory + {
exploitability_score: $analysis.exploitability_score,
exploitability_rationale: $analysis.exploitability_rationale,
attack_vector_analysis: $analysis.attack_vector_analysis,
exploit_detection: $analysis.exploit_detection
}
else
$advisory
end
)
' "$INPUT_PATH" > "$output_tmp"
mv "$output_tmp" "$OUTPUT_PATH"
echo "Exploitability enrichment complete (batch): $OUTPUT_PATH"
echo "Analyzed: $analyzed_count, failed: $failed_count"
}
if [[ "$MODE" == "single" ]]; then
enrich_single
else
enrich_batch
fi
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
# feed-utils.sh
# Shared advisory feed path and sync helpers for local/maintenance scripts.
init_feed_paths() {
local project_root="$1"
: "${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}"
}
sync_feed_to_mirrors() {
local source_feed="$1"
local mode="${2:-create}"
local target
for target in "$SKILL_FEED_PATH" "$PUBLIC_FEED_PATH"; do
case "$mode" in
create)
mkdir -p "$(dirname "$target")"
cp "$source_feed" "$target"
echo "✓ Updated: $target"
;;
existing-only)
if [ -f "$target" ]; then
cp "$source_feed" "$target"
echo "✓ Updated: $target"
fi
;;
*)
echo "Error: unsupported mirror sync mode: $mode" >&2
return 1
;;
esac
done
}
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env node
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
extractTitleFromMarkdown,
stripFrontmatter,
} from '../utils/markdownHelpers.mjs';
import {
isWikiIndexSlug,
toWikiLlmsPath,
toWikiRoute,
} from '../utils/wikiPathHelpers.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '..');
const WIKI_ROOT = path.join(REPO_ROOT, 'wiki');
const PUBLIC_WIKI_ROOT = path.join(REPO_ROOT, 'public', 'wiki');
const LLM_INDEX_FILE = path.join(PUBLIC_WIKI_ROOT, 'llms.txt');
const WEBSITE_BASE = 'https://clawsec.prompt.security';
const REPO_BASE = 'https://github.com/prompt-security/clawsec';
const RAW_BASE = 'https://raw.githubusercontent.com/prompt-security/clawsec/main';
const toPosix = (inputPath) => inputPath.split(path.sep).join('/');
const toLlmsPageUrl = (slug) => `${WEBSITE_BASE}${toWikiLlmsPath(slug)}`;
const walkMarkdownFiles = async (dir) => {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
const nested = await walkMarkdownFiles(fullPath);
files.push(...nested);
continue;
}
if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
files.push(fullPath);
}
}
return files;
};
const sortDocs = (a, b) => {
if (a.slug === 'index' && b.slug !== 'index') return -1;
if (a.slug !== 'index' && b.slug === 'index') return 1;
return a.slug.localeCompare(b.slug, 'en', { sensitivity: 'base' });
};
const buildPageBody = (doc) => {
const pageRoute = toWikiRoute(doc.slug);
const pageUrl = `${WEBSITE_BASE}/#${pageRoute}`;
const sourceUrl = `${RAW_BASE}/wiki/${doc.relativePath}`;
const llmsUrl = toLlmsPageUrl(doc.slug);
return [
`# ClawSec Wiki · ${doc.title}`,
'',
'LLM-ready export for a single wiki page.',
'',
'## Canonical',
`- Wiki page: ${pageUrl}`,
`- LLM export: ${llmsUrl}`,
`- Source markdown: ${sourceUrl}`,
'',
'## Markdown',
'',
doc.content.trim(),
'',
].join('\n');
};
const buildFallbackIndexBody = (docs) => {
const lines = [
'# ClawSec Wiki llms.txt',
'',
'LLM-readable index for wiki pages.',
'',
`Website wiki root: ${WEBSITE_BASE}/#/wiki`,
`GitHub wiki mirror: ${REPO_BASE}/wiki`,
`Canonical source of truth: ${REPO_BASE}/tree/main/wiki`,
'',
'## Generated Page Exports',
];
for (const doc of docs) {
const pageRoute = toWikiRoute(doc.slug);
const pageUrl = `${WEBSITE_BASE}/#${pageRoute}`;
const llmsUrl = toLlmsPageUrl(doc.slug);
lines.push(`- ${doc.title}: ${llmsUrl} (page: ${pageUrl})`);
}
return `${lines.join('\n')}\n`;
};
const main = async () => {
try {
const wikiStat = await fs.stat(WIKI_ROOT).catch(() => null);
if (!wikiStat || !wikiStat.isDirectory()) {
throw new Error('wiki/ directory not found.');
}
const markdownFiles = await walkMarkdownFiles(WIKI_ROOT);
const docs = [];
for (const fullPath of markdownFiles) {
const relativePath = toPosix(path.relative(WIKI_ROOT, fullPath));
const slug = relativePath.replace(/\.md$/i, '').toLowerCase();
const rawContent = await fs.readFile(fullPath, 'utf8');
const content = stripFrontmatter(rawContent);
const title = extractTitleFromMarkdown(rawContent, relativePath);
docs.push({ relativePath, slug, title, content });
}
docs.sort(sortDocs);
const pageDocs = docs.filter((doc) => !isWikiIndexSlug(doc.slug));
const indexDoc = docs.find((doc) => isWikiIndexSlug(doc.slug));
// `public/wiki/` is fully generated; wipe stale output before regenerating.
await fs.rm(PUBLIC_WIKI_ROOT, { recursive: true, force: true });
await fs.mkdir(PUBLIC_WIKI_ROOT, { recursive: true });
for (const doc of pageDocs) {
const outputFile = path.join(PUBLIC_WIKI_ROOT, doc.slug, 'llms.txt');
await fs.mkdir(path.dirname(outputFile), { recursive: true });
await fs.writeFile(outputFile, buildPageBody(doc), 'utf8');
}
const indexBody = indexDoc ? buildPageBody(indexDoc) : buildFallbackIndexBody(pageDocs);
await fs.writeFile(LLM_INDEX_FILE, indexBody, 'utf8');
// Keep logs short for CI readability.
console.log(`Generated ${pageDocs.length} page llms.txt exports and /wiki/llms.txt`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Failed to generate wiki llms exports: ${message}`);
process.exit(1);
}
};
await main();
+77 -23
View File
@@ -11,13 +11,14 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# shellcheck source=./feed-utils.sh
source "$SCRIPT_DIR/feed-utils.sh"
# Configuration - same as pipeline
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"
init_feed_paths "$PROJECT_ROOT"
KEYWORDS="OpenClaw clawdbot Moltbot NanoClaw WhatsApp-bot baileys"
GITHUB_REF_PATTERN="github.com/openclaw/openclaw github.com/qwibitai/NanoClaw"
ENRICH_SCRIPT="$PROJECT_ROOT/scripts/ci/enrich_exploitability.sh"
# Parse args
DAYS_BACK=120
@@ -46,6 +47,12 @@ echo "Days back: $DAYS_BACK"
echo "Force mode: $FORCE"
echo ""
# Verify enrichment helper exists (it validates Python/analyzer prerequisites internally).
if [ ! -x "$ENRICH_SCRIPT" ]; then
echo "Error: Exploitability enrichment helper not found or not executable: $ENRICH_SCRIPT"
exit 1
fi
# Create temp directory
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
@@ -62,7 +69,7 @@ fi
if [ -z "${START_DATE:-}" ]; then
# macOS vs Linux date compatibility
if date -v-1d > /dev/null 2>&1; then
START_DATE=$(date -u -v-${DAYS_BACK}d +%Y-%m-%dT%H:%M:%S.000Z)
START_DATE=$(date -u -v-"${DAYS_BACK}"d +%Y-%m-%dT%H:%M:%S.000Z)
else
START_DATE=$(date -u -d "${DAYS_BACK} days ago" +%Y-%m-%dT%H:%M:%S.000Z)
fi
@@ -74,8 +81,8 @@ echo "End date: $END_DATE"
echo ""
# URL encode dates
START_ENC=$(echo "$START_DATE" | sed 's/:/%3A/g')
END_ENC=$(echo "$END_DATE" | sed 's/:/%3A/g')
START_ENC=${START_DATE//:/%3A}
END_ENC=${END_DATE//:/%3A}
echo "=== Fetching CVEs from NVD ==="
@@ -128,7 +135,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,8 +243,38 @@ 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) |
{
id: .cve.id,
@@ -246,12 +283,14 @@ 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],
cvss_score: get_cvss_score,
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id)
nvd_url: ("https://nvd.nist.gov/vuln/detail/" + .cve.id),
exploitability_score: null,
exploitability_rationale: null
}
]
' "$TEMP_DIR/filtered_cves.json" > "$TEMP_DIR/new_advisories.json"
@@ -266,6 +305,28 @@ if [ "$NEW_COUNT" -eq 0 ]; then
exit 0
fi
echo ""
echo "=== Analyzing Exploitability ==="
# Build CVSS vector lookup for enriched analysis inputs.
jq '
[.[] | {
id: .cve.id,
cvss_vector: (
.cve.metrics.cvssMetricV31[0]?.cvssData.vectorString //
.cve.metrics.cvssMetricV30[0]?.cvssData.vectorString //
.cve.metrics.cvssMetricV2[0]?.vectorString //
""
)
}] | map({(.id): .cvss_vector}) | add
' "$TEMP_DIR/filtered_cves.json" > "$TEMP_DIR/cvss_vectors.json"
"$ENRICH_SCRIPT" \
--mode batch \
--input "$TEMP_DIR/new_advisories.json" \
--output "$TEMP_DIR/new_advisories.json" \
--cvss-vectors "$TEMP_DIR/cvss_vectors.json"
echo ""
echo "=== New Advisories ==="
jq -r '.[] | " \(.id) [\(.severity)] - \(.title)"' "$TEMP_DIR/new_advisories.json"
@@ -298,7 +359,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
@@ -308,16 +369,9 @@ if jq empty "$TEMP_DIR/updated_feed.json" 2>/dev/null; then
# Update main feed
cp "$TEMP_DIR/updated_feed.json" "$FEED_PATH"
echo "✓ Updated: $FEED_PATH"
# Update skill feed
mkdir -p "$(dirname "$SKILL_FEED_PATH")"
cp "$FEED_PATH" "$SKILL_FEED_PATH"
echo "✓ Updated: $SKILL_FEED_PATH"
# Update public feed for local dev
mkdir -p "$(dirname "$PUBLIC_FEED_PATH")"
cp "$FEED_PATH" "$PUBLIC_FEED_PATH"
echo "✓ Updated: $PUBLIC_FEED_PATH"
# Sync feed mirrors for local skill/public consumers.
sync_feed_to_mirrors "$FEED_PATH" "create"
echo ""
TOTAL_ADVISORIES=$(jq '.advisories | length' "$FEED_PATH")
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# populate-local-wiki.sh
# Generates wiki-derived public assets for local preview and CI parity.
#
# Usage: ./scripts/populate-local-wiki.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
WIKI_DIR="$PROJECT_ROOT/wiki"
PUBLIC_WIKI_DIR="$PROJECT_ROOT/public/wiki"
if [ ! -d "$WIKI_DIR" ]; then
echo "Error: wiki directory not found at $WIKI_DIR"
exit 1
fi
echo "=== ClawSec Local Wiki Populator ==="
echo "Project root: $PROJECT_ROOT"
node "$PROJECT_ROOT/scripts/generate-wiki-llms.mjs"
PAGE_COUNT=0
if [ -d "$PUBLIC_WIKI_DIR" ]; then
PAGE_COUNT=$(find "$PUBLIC_WIKI_DIR" -type f -path '*/llms.txt' ! -path "$PUBLIC_WIKI_DIR/llms.txt" | wc -l | tr -d ' ')
fi
echo "Wiki llms index: $PUBLIC_WIKI_DIR/llms.txt"
echo "Wiki llms pages: $PAGE_COUNT files under $PUBLIC_WIKI_DIR/<page>/llms.txt"
+3 -3
View File
@@ -76,13 +76,13 @@ fi
# ESLint
echo -e "\n${YELLOW}Running ESLint...${NC}"
if $FIX_MODE; then
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --fix; then
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --ignore-pattern '.auto-claude/**' --fix; then
check_pass "ESLint (with auto-fix)"
else
check_fail "ESLint found unfixable issues"
fi
else
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --max-warnings 0; then
if npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --ignore-pattern '.auto-claude/**' --max-warnings 0; then
check_pass "ESLint"
else
check_fail "ESLint found issues (run with --fix to auto-fix)"
@@ -190,7 +190,7 @@ print_header "Security"
# Trivy FS Scan
if command -v trivy &> /dev/null; then
echo -e "\n${YELLOW}Running Trivy filesystem scan...${NC}"
if trivy fs . --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed; then
if trivy fs . --severity CRITICAL,HIGH --exit-code 1 --ignore-unfixed --skip-dirs .auto-claude --skip-files clawsec-signing-private.pem; then
check_pass "Trivy filesystem scan"
else
check_fail "Trivy found CRITICAL/HIGH vulnerabilities"
+19
View File
@@ -0,0 +1,19 @@
# Changelog
All notable changes to the ClawSec Feed skill will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.0.5] - 2026-02-28
### Added
- Exploitability-focused advisory guidance, including filtering and prioritization examples.
- Notification examples that include exploitability context and rationale.
### Changed
- Clarified exploitability scoring guidance to match runtime values (`high|medium|low|unknown`).
- Updated response-priority guidance to align with exploitability-first triage.
- De-duplicated exploitability filtering guidance in `SKILL.md` by pointing to canonical docs in `wiki/exploitability-scoring.md` and `clawsec-suite`.
+96 -4
View File
@@ -1,6 +1,6 @@
---
name: clawsec-feed
version: 0.0.4
version: 0.0.5
description: Security advisory feed with automated NVD CVE polling for OpenClaw-related vulnerabilities. Updated daily.
homepage: https://clawsec.prompt.security
metadata: {"openclaw":{"emoji":"📡","category":"security"}}
@@ -318,7 +318,9 @@ curl -sSL --fail --show-error --retry 3 --retry-delay 1 "$FEED_URL"
"description": "Skill sends user data to external server",
"affected": ["helper-plus@1.0.0", "helper-plus@1.0.1"],
"action": "Remove immediately",
"published": "2026-02-01T10:00:00Z"
"published": "2026-02-01T10:00:00Z",
"exploitability_score": "critical",
"exploitability_rationale": "Trivially exploitable through normal skill usage; no special conditions required. Active exploitation observed in the wild."
}
]
}
@@ -385,6 +387,42 @@ fi
echo "$RECENT"
```
### Filter by exploitability score
Shared exploitability prioritization guidance is maintained in:
- [`wiki/exploitability-scoring.md`](../../wiki/exploitability-scoring.md)
- [`skills/clawsec-suite/SKILL.md`](../clawsec-suite/SKILL.md) ("Quick feed check")
### Get exploitability context for an advisory
```bash
# Show exploitability details for a specific CVE
CVE_ID="CVE-2026-27488"
echo "$FEED" | jq --arg cve "$CVE_ID" '.advisories[] | select(.id == $cve) | {
id: .id,
severity: .severity,
exploitability_score: .exploitability_score,
exploitability_rationale: .exploitability_rationale,
title: .title
}'
```
### Prioritize advisories by exploitability
```bash
# Sort advisories by exploitability (critical → high → medium → low)
# This helps agents focus on the most immediately actionable threats
echo "$FEED" | jq '[.advisories[] | select(.exploitability_score != null)] |
sort_by(
if .exploitability_score == "critical" then 0
elif .exploitability_score == "high" then 1
elif .exploitability_score == "medium" then 2
elif .exploitability_score == "low" then 3
else 4 end
)'
```
---
## Cross-Reference Installed Skills
@@ -476,23 +514,75 @@ done
---
## Prioritizing High-Exploitability Threats
**IMPORTANT:** When reviewing advisories, always prioritize by **exploitability score** in addition to severity. The exploitability score indicates how easily a vulnerability can be exploited in practice, helping you focus on the most actionable threats.
### Exploitability Priority Levels
| Exploitability | Meaning | Action Priority |
|----------------|---------|-----------------|
| `high` | Trivially or easily exploitable with public tooling | **Immediate notification** |
| `medium` | Exploitable but requires specific conditions | **Standard notification** |
| `low` | Difficult to exploit or theoretical | **Low priority notification** |
### How to Use Exploitability in Notifications
1. **Filter for high-exploitability first:**
```bash
# Get high exploitability advisories
echo "$FEED" | jq '.advisories[] | select(.exploitability_score == "high")'
```
2. **Include exploitability in notifications:**
```
📡 ClawSec Feed: High-exploitability alert
CRITICAL - CVE-2026-27488 (Exploitability: HIGH)
→ Trivially exploitable RCE in skill-loader v2.1.0
→ Public exploit code available
→ Recommended action: Immediate removal or upgrade to v2.1.1
```
3. **Prioritize by both severity AND exploitability:**
- A HIGH severity + HIGH exploitability CVE is more urgent than a CRITICAL severity + LOW exploitability CVE
- Focus user attention on threats that are both severe and easily exploitable
- Include the exploitability rationale to help users understand the risk context
### Example Notification Priority Order
When multiple advisories exist, present them in this order:
1. **Critical severity + High exploitability** - most urgent
2. **High severity + High exploitability**
3. **Critical severity + Medium/Low exploitability**
4. **High severity + Medium/Low exploitability**
5. **Medium/Low severity** (any exploitability)
This ensures you alert users to the most actionable, immediately dangerous threats first.
---
## When to Notify Your User
**Notify Immediately (Critical):**
- New critical advisory affecting an installed skill
- Active exploitation detected
- **High exploitability score** (regardless of severity)
**Notify Soon (High):**
- New high-severity advisory affecting installed skills
- Failed to fetch advisory feed (network issue?)
- Medium exploitability with high severity
**Notify at Next Interaction (Medium):**
- New medium-severity advisories
- General security updates
- Low exploitability advisories
**Log Only (Low/Info):**
- Low-severity advisories (mention if user asks)
- Feed checked, no new advisories
- Theoretical vulnerabilities (low exploitability, low severity)
---
@@ -503,11 +593,13 @@ done
```
📡 ClawSec Feed: 2 new advisories since last check
CRITICAL - GA-2026-015: Malicious prompt pattern "ignore-all"
CRITICAL - GA-2026-015: Malicious prompt pattern "ignore-all" (Exploitability: HIGH)
→ Detected prompt injection technique. Update your system prompt defenses.
→ Exploitability: Easily exploitable with publicly documented techniques.
HIGH - GA-2026-016: Vulnerable skill "data-helper" v1.2.0
HIGH - GA-2026-016: Vulnerable skill "data-helper" v1.2.0 (Exploitability: MEDIUM)
→ You have this installed! Recommended action: Update to v1.2.1 or remove.
→ Exploitability: Requires specific configuration; not trivially exploitable.
```
### If nothing new:
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
Rs++ntJvBvX4zVTJ/DsrfXOQG3VTUc2x4esSURSMonesmYzSm9U9kd3rBz5d+DemJOVJ/esH21VACpdE+T34AA==
SJ1weYVVi723M8f6s8es6rg34CSPKxbvlBy1QIXdS0giskd5KTADTDLr2STqUCuWpaV7U+JQa/1eWqNX2oJ+Aw==
+6 -1
View File
@@ -1,6 +1,6 @@
{
"name": "clawsec-feed",
"version": "0.0.4",
"version": "0.0.5",
"description": "Security advisory feed monitoring for AI agents. Subscribe to community-driven threat intelligence.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -21,6 +21,11 @@
"required": true,
"description": "Advisory feed skill documentation"
},
{
"path": "CHANGELOG.md",
"required": true,
"description": "Version history for advisory feed updates"
},
{
"path": "advisories/feed.json",
"required": true,
+20
View File
@@ -0,0 +1,20 @@
# Changelog
All notable changes to the ClawSec NanoClaw compatibility skill will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.0.2] - 2026-02-28
### Added
- Exploitability-aware advisory output in NanoClaw MCP tools (`exploitability_score`, `exploitability_rationale`).
- Exploitability filtering (`exploitabilityScore`) for `clawsec_list_advisories`.
### Changed
- Updated NanoClaw advisory sorting and pre-install safety recommendation logic to prioritize exploitability context.
- Updated NanoClaw integration docs to match current host/container integration points (`src/ipc.ts`, `src/index.ts`) and current cache schema.
- Removed duplicate exploitability normalization logic from MCP advisory tools and now reuse `normalizeExploitabilityScore` from `lib/risk.ts`.
- Reused `matchesAffectedSpecifier` from `lib/advisories.ts` in MCP advisory tools to keep skill/version matching logic centralized and consistent.
+45 -31
View File
@@ -8,7 +8,7 @@ ClawSec provides security advisory monitoring for NanoClaw through:
- **MCP Tools**: Agents can check for vulnerabilities via `clawsec_check_advisories`
- **Advisory Feed**: Automatic monitoring of https://clawsec.prompt.security/advisories/feed.json
- **Signature Verification**: Ed25519-signed feeds ensure integrity
- **Platform Targeting**: Advisories can be NanoClaw-specific or cross-platform
- **Exploitability Context**: Advisories include exploitability score and rationale for triage
## Prerequisites
@@ -57,18 +57,30 @@ in each tool file).
Add the host-side IPC handlers for ClawSec operations.
**File**: `host/ipc-handler.ts`
**File**: `src/ipc.ts`
```typescript
// Add this import at the top
import { registerClawSecHandlers } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
// Add these imports at the top
import { handleAdvisoryIpc } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
import { SkillSignatureVerifier } from '../skills/clawsec-nanoclaw/host-services/skill-signature-handler.js';
// In your IPC handler setup function
export function setupIpcHandlers() {
// ... your existing handlers ...
// Initialize these once in host startup and pass through deps
const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger);
const signatureVerifier = new SkillSignatureVerifier();
// Register ClawSec handlers
registerClawSecHandlers();
// In processTaskIpc switch:
case 'refresh_advisory_cache':
case 'verify_skill_signature':
await handleAdvisoryIpc(
data,
{ advisoryCacheManager, signatureVerifier },
logger,
sourceGroup
);
break;
default:
// existing task handling
}
```
@@ -76,23 +88,25 @@ export function setupIpcHandlers() {
Add the advisory cache manager to your host services.
**File**: `host/index.ts` (or your main entry point)
**File**: `src/index.ts` (or your main entry point)
```typescript
// Add this import
import { startAdvisoryCache } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
// Start the service when your host process starts
async function main() {
// ... your existing initialization ...
// Start ClawSec advisory cache (fetches feed every 6 hours)
startAdvisoryCache({
cacheFile: '/workspace/project/data/clawsec-advisory-cache.json',
feedUrl: 'https://clawsec.prompt.security/advisories/feed.json',
publicKeyPath: '/workspace/project/skills/clawsec-nanoclaw/advisories/feed-signing-public.pem',
refreshInterval: 6 * 60 * 60 * 1000, // 6 hours
});
// Initialize cache manager and prime it at startup
const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger);
await advisoryCacheManager.initialize();
// Recommended refresh cadence (6h)
setInterval(() => {
advisoryCacheManager.refresh().catch((error) => {
logger.error({ error }, 'Periodic advisory cache refresh failed');
});
}, 6 * 60 * 60 * 1000);
// ... rest of your startup ...
}
@@ -151,9 +165,9 @@ cat /workspace/project/data/clawsec-advisory-cache.json
You should see:
- `feed`: Array of advisories
- `signature`: Ed25519 signature
- `lastFetch`: Timestamp of last update
- `fetchedAt`: Timestamp of last update
- `verified`: Should be `true`
- `publicKeyFingerprint`: SHA-256 fingerprint of the pinned signing key
## Usage Examples
@@ -183,13 +197,13 @@ You can also call the MCP tools directly from agent code:
```typescript
// Check all installed skills
const result = await tools.clawsec_check_advisories({
skillsRoot: '/workspace/project/skills'
installRoot: '/home/node/.claude/skills'
});
// Check specific skill before installation
const safetyCheck = await tools.clawsec_check_skill_safety({
skillName: 'risky-skill',
version: '1.0.0'
skillVersion: '1.0.0'
});
```
@@ -199,19 +213,19 @@ const safetyCheck = await tools.clawsec_check_skill_safety({
Default: `/workspace/project/data/clawsec-advisory-cache.json`
To change, update the `cacheFile` parameter in `startAdvisoryCache()`.
To change, pass a different data directory path to `new AdvisoryCacheManager(dataDir, logger)`.
### Refresh Interval
Default: 6 hours
To change, update the `refreshInterval` parameter (in milliseconds).
To change, update the `setInterval(...)` duration (in milliseconds) in host startup.
### Feed URL
Default: `https://clawsec.prompt.security/advisories/feed.json`
To use a mirror or custom feed, update the `feedUrl` parameter.
To use a mirror or custom feed, update `FEED_URL` in `skills/clawsec-nanoclaw/host-services/advisory-cache.ts`.
## Platform-Specific Advisories
@@ -222,7 +236,7 @@ ClawSec advisories can target specific platforms:
- **`platforms: ["openclaw", "nanoclaw"]`**: Affects both
- **No `platforms` field**: Applies to all platforms
The MCP tools automatically filter advisories based on your platform.
Platform metadata is preserved in advisory records and can be filtered by your policy layer.
## Security
@@ -260,7 +274,7 @@ Never manually edit the cache file - it will break signature verification.
**Problem**: Advisory cache is empty or stale
**Solution**:
1. Check that `startAdvisoryCache()` is called in your host entry point
1. Check that `AdvisoryCacheManager.initialize()` is called in your host entry point
2. Verify network access to `clawsec.prompt.security`
3. Check host logs for fetch errors
4. Manually trigger: `curl https://clawsec.prompt.security/advisories/feed.json`
@@ -280,7 +294,7 @@ Never manually edit the cache file - it will break signature verification.
**Problem**: Tools return errors about IPC
**Solution**:
1. Verify IPC handlers are registered in `host/ipc-handler.ts`
1. Verify IPC handlers are registered in `src/ipc.ts`
2. Check that IPC directory exists and is writable
3. Ensure host process is running
4. Check host logs for handler errors
@@ -290,8 +304,8 @@ Never manually edit the cache file - it will break signature verification.
To remove ClawSec from NanoClaw:
1. Remove MCP tool registration from `ipc-mcp-stdio.ts`
2. Remove IPC handler registration from `host/ipc-handler.ts`
3. Remove `startAdvisoryCache()` call from host entry point
2. Remove IPC handler registration from `src/ipc.ts`
3. Remove `AdvisoryCacheManager` initialization from host entry point
4. Delete the skill directory: `rm -rf skills/clawsec-nanoclaw`
5. Delete the cache file: `rm /workspace/project/data/clawsec-advisory-cache.json`
6. Restart NanoClaw
+9 -9
View File
@@ -56,9 +56,9 @@ ClawSec provides a complete security skill for NanoClaw deployments:
- `clawsec_integrity_status` - View file baseline status
- `clawsec_verify_audit` - Verify audit log hash chain
- **Advisory Cache Service**: Automatic feed fetching every 6 hours
- **Advisory Cache Service**: Host-managed feed fetching with signature validation
- **Signature Verification**: Ed25519-signed feeds ensure integrity
- **Platform Filtering**: Shows only relevant advisories for NanoClaw
- **Exploitability Context**: Surfaces `exploitability_score` and rationale to reduce alert fatigue
- **IPC Communication**: Container-safe host communication
### Installation
@@ -77,19 +77,19 @@ The skill integrates into three places:
**1. MCP Tools** (container):
```typescript
// container/agent-runner/src/ipc-mcp-stdio.ts
import { clawsecTools } from '../../../skills/clawsec-nanoclaw/mcp-tools/advisory-tools.js';
import '../../../skills/clawsec-nanoclaw/mcp-tools/advisory-tools.js';
```
**2. IPC Handlers** (host):
```typescript
// host/ipc-handler.ts
import { registerClawSecHandlers } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
// src/ipc.ts
import { handleAdvisoryIpc } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
```
**3. Cache Service** (host):
```typescript
// host/index.ts
import { startAdvisoryCache } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
// src/index.ts
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
```
### Advisory Feed
@@ -142,10 +142,10 @@ Planned features for future releases:
- [Skill Documentation](skills/clawsec-nanoclaw/SKILL.md) - Features and architecture
- [Installation Guide](skills/clawsec-nanoclaw/INSTALL.md) - Detailed setup instructions
- [ClawSec Main README](README.md) - Overall ClawSec documentation
- [Security & Signing](../../docs/SECURITY-SIGNING.md) - Signature verification details
- [Security & Signing](../../wiki/security-signing-runbook.md) - Signature verification details
## Support
- **Issues**: https://github.com/prompt-security/clawsec/issues
- **Security**: security@prompt.security
- NanoClaw Repository: (link TBD)
- NanoClaw Repository: https://github.com/qwibitai/nanoclaw
+32 -27
View File
@@ -1,6 +1,6 @@
---
name: clawsec-nanoclaw
version: 0.0.1
version: 0.0.2
description: Use when checking for security vulnerabilities in NanoClaw skills, before installing new skills, or when asked about security advisories affecting the bot
---
@@ -10,7 +10,7 @@ Security advisory monitoring that protects your WhatsApp bot from known vulnerab
## Overview
ClawSec provides MCP tools that check installed skills against a curated feed of security advisories. It prevents installation of vulnerable skills and alerts you to issues in existing ones.
ClawSec provides MCP tools that check installed skills against a curated feed of security advisories. It prevents installation of vulnerable skills, includes exploitability context for triage, and alerts you to issues in existing ones.
**Core principle:** Check before you install. Monitor what's running.
@@ -36,7 +36,7 @@ Do NOT use for:
// Before installing any skill
const safety = await tools.clawsec_check_skill_safety({
skillName: 'new-skill',
version: '1.0.0' // optional
skillVersion: '1.0.0' // optional
});
if (!safety.safe) {
@@ -48,14 +48,16 @@ if (!safety.safe) {
### Security Audit
```typescript
// Check all installed skills
// Check all installed skills (defaults to ~/.claude/skills in the container)
const result = await tools.clawsec_check_advisories({
skillsRoot: '/workspace/project/skills' // optional
installRoot: '/home/node/.claude/skills' // optional
});
if (result.criticalCount > 0) {
if (result.matches.some((m) =>
m.advisory.severity === 'critical' || m.advisory.exploitability_score === 'high'
)) {
// Alert user immediately
console.error('CRITICAL vulnerabilities found!');
console.error('Urgent advisories found!');
}
```
@@ -64,8 +66,8 @@ if (result.criticalCount > 0) {
```typescript
// List advisories with filters
const advisories = await tools.clawsec_list_advisories({
platform: 'nanoclaw', // optional: nanoclaw, openclaw, or both
severity: 'critical' // optional: critical, high, medium, low
severity: 'high', // optional
exploitabilityScore: 'high' // optional
});
```
@@ -75,7 +77,7 @@ const advisories = await tools.clawsec_list_advisories({
|------|------|---------------|
| Pre-install check | `clawsec_check_skill_safety` | `skillName` |
| Audit all skills | `clawsec_check_advisories` | `installRoot` (optional) |
| Browse feed | `clawsec_list_advisories` | `severity`, `type` (optional) |
| Browse feed | `clawsec_list_advisories` | `severity`, `type`, `exploitabilityScore` (optional) |
| Verify package signature | `clawsec_verify_skill_package` | `packagePath` |
| Refresh advisory cache | `clawsec_refresh_cache` | (none) |
| Check file integrity | `clawsec_check_integrity` | `mode`, `autoRestore` (optional) |
@@ -110,7 +112,7 @@ if (safety.safe) {
```typescript
// Add to scheduled tasks
schedule_task({
prompt: "Check for security advisories using clawsec_check_advisories and alert if any critical issues found",
prompt: "Check advisories using clawsec_check_advisories and alert when critical or high-exploitability matches appear",
schedule_type: "cron",
schedule_value: "0 9 * * *" // Daily at 9am
});
@@ -125,8 +127,8 @@ You: I'll check installed skills for known vulnerabilities.
[Use clawsec_check_advisories]
Response:
✅ No critical issues found.
- 2 low-severity advisories (not urgent)
✅ No urgent issues found.
- 2 low-severity/low-exploitability advisories
- All skills up to date
```
@@ -146,30 +148,33 @@ const safety = await tools.clawsec_check_skill_safety({
if (safety.safe) await installSkill('untrusted-skill');
```
### ❌ Ignoring platform filters
### ❌ Ignoring exploitability context
```typescript
// DON'T: Check OpenClaw advisories on NanoClaw
const advisories = await tools.clawsec_list_advisories({
platform: 'openclaw' // Wrong platform!
});
// DON'T: Use severity only
if (advisory.severity === 'high') {
notifyNow(advisory);
}
```
```typescript
// DO: Use correct platform or let it auto-filter
const advisories = await tools.clawsec_list_advisories({
platform: 'nanoclaw' // Correct
});
// DO: Use exploitability + severity
if (
advisory.exploitability_score === 'high' ||
advisory.severity === 'critical'
) {
notifyNow(advisory);
}
```
### ❌ Skipping critical severity
```typescript
// DON'T: Only check low severity
if (result.lowCount > 0) alert();
// DON'T: Ignore high exploitability in medium severity advisories
if (advisory.severity === 'critical') alert();
```
```typescript
// DO: Prioritize critical and high
if (result.criticalCount > 0 || result.highCount > 0) {
// DO: Prioritize exploitability and severity together
if (advisory.exploitability_score === 'high' || advisory.severity === 'critical') {
// Alert immediately
}
```
@@ -182,7 +187,7 @@ if (result.criticalCount > 0 || result.highCount > 0) {
**Signature Verification**: Ed25519 signed feeds
**Cache Location**: `/workspace/project/data/clawsec-cache.json`
**Cache Location**: `/workspace/project/data/clawsec-advisory-cache.json`
See [INSTALL.md](./INSTALL.md) for setup and [docs/](./docs/) for advanced usage.
@@ -16,6 +16,7 @@ import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import https from 'node:https';
import path from 'node:path';
import { evaluateAdvisoryRisk } from '../lib/risk.js';
// ClawSec public key (from clawsec-signing-public.pem)
const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
@@ -35,6 +36,8 @@ export interface Advisory {
action?: string;
published?: string;
updated?: string;
exploitability_score?: 'high' | 'medium' | 'low' | 'unknown' | string;
exploitability_rationale?: string;
affected: string[];
}
@@ -376,42 +379,5 @@ export function evaluateSkillSafety(advisories: Advisory[]): {
recommendation: 'install' | 'block' | 'review';
reason: string;
} {
if (advisories.length === 0) {
return { safe: true, recommendation: 'install', reason: 'No advisories found' };
}
const hasMalicious = advisories.some((a) => a.type === 'malicious');
const hasRemoveAction = advisories.some((a) => a.action === 'remove');
const hasCritical = advisories.some((a) => a.severity === 'critical');
const hasHigh = advisories.some((a) => a.severity === 'high');
if (hasMalicious || hasRemoveAction) {
return {
safe: false,
recommendation: 'block',
reason: 'Malicious skill or removal recommended',
};
}
if (hasCritical) {
return {
safe: false,
recommendation: 'block',
reason: 'Critical security advisory',
};
}
if (hasHigh) {
return {
safe: false,
recommendation: 'review',
reason: 'High severity advisory - user review recommended',
};
}
return {
safe: false,
recommendation: 'review',
reason: 'Advisory found - review before installing',
};
return evaluateAdvisoryRisk(advisories);
}
+32 -10
View File
@@ -121,6 +121,34 @@ export function versionMatches(version: string, versionSpec: string): boolean {
return false;
}
/**
* Checks whether an affected specifier matches a skill name/version.
* Optionally matches against a skill directory name as alias.
*/
export function matchesAffectedSpecifier(
affected: string,
skillName: string,
skillVersion: string | null,
skillDirName?: string
): boolean {
const parsed = parseAffectedSpecifier(affected);
if (!parsed) return false;
const normalizedTarget = normalizeSkillName(parsed.name);
const normalizedSkillName = normalizeSkillName(skillName);
const normalizedDirName = skillDirName ? normalizeSkillName(skillDirName) : null;
if (normalizedTarget !== normalizedSkillName && normalizedTarget !== normalizedDirName) {
return false;
}
if (!skillVersion) {
return true;
}
return versionMatches(skillVersion, parsed.versionSpec);
}
/**
* Loads advisory feed from a remote URL with signature verification.
*/
@@ -269,10 +297,12 @@ export async function loadFeed(
export function advisoryLooksHighRisk(advisory: Advisory): boolean {
const type = advisory.type.toLowerCase();
const severity = advisory.severity.toLowerCase();
const exploitability = (advisory.exploitability_score || 'unknown').toLowerCase();
const combined = `${advisory.title} ${advisory.description} ${advisory.action}`.toLowerCase();
if (type === 'malicious_skill' || type === 'malicious_plugin') return true;
if (type.includes('malicious')) return true;
if (severity === 'critical') return true;
if (exploitability === 'high') return true;
if (/\b(malicious|exfiltrate|exfiltration|backdoor|trojan|stealer|credential theft)\b/.test(combined)) return true;
if (/\b(remove|uninstall|disable|do not use|quarantine)\b/.test(combined)) return true;
@@ -294,15 +324,7 @@ export function findAdvisoryMatches(
if (affected.length === 0) continue;
for (const specifier of affected) {
const parsed = parseAffectedSpecifier(specifier);
if (!parsed) continue;
if (normalizeSkillName(parsed.name) !== normalizeSkillName(skillName)) {
continue;
}
// If version specified, check if it matches
if (version && !versionMatches(version, parsed.versionSpec)) {
if (!matchesAffectedSpecifier(specifier, skillName, version)) {
continue;
}
+88
View File
@@ -0,0 +1,88 @@
/**
* Shared advisory risk evaluation for NanoClaw host + MCP layers.
*/
export type SkillSafetyRecommendation = 'install' | 'block' | 'review';
export interface AdvisoryRiskInput {
severity?: string;
type?: string;
action?: string;
exploitability_score?: string;
}
export interface AdvisoryRiskEvaluation {
safe: boolean;
recommendation: SkillSafetyRecommendation;
reason: string;
}
export function normalizeExploitabilityScore(score: unknown): 'high' | 'medium' | 'low' | 'unknown' {
const value = String(score || '').toLowerCase().trim();
if (value === 'high' || value === 'medium' || value === 'low') {
return value;
}
return 'unknown';
}
export function evaluateAdvisoryRisk(advisories: AdvisoryRiskInput[]): AdvisoryRiskEvaluation {
if (advisories.length === 0) {
return { safe: true, recommendation: 'install', reason: 'No advisories found' };
}
const hasMalicious = advisories.some((a) => String(a.type || '').toLowerCase().includes('malicious'));
const hasRemoveAction = advisories.some((a) =>
/\b(remove|uninstall|disable|quarantine|block)\b/i.test(String(a.action || ''))
);
const hasCritical = advisories.some((a) => String(a.severity || '').toLowerCase() === 'critical');
const hasHigh = advisories.some((a) => String(a.severity || '').toLowerCase() === 'high');
const hasHighExploitability = advisories.some(
(a) => normalizeExploitabilityScore(a.exploitability_score) === 'high'
);
if (hasMalicious || hasRemoveAction) {
return {
safe: false,
recommendation: 'block',
reason: 'Malicious skill or removal recommended by ClawSec',
};
}
if (hasCritical && hasHighExploitability) {
return {
safe: false,
recommendation: 'block',
reason: 'Critical advisory with high exploitability context - do not install',
};
}
if (hasCritical) {
return {
safe: false,
recommendation: 'block',
reason: 'Critical security advisory - do not install',
};
}
if (hasHighExploitability) {
return {
safe: false,
recommendation: 'review',
reason: 'High exploitability advisory - urgent user review strongly recommended',
};
}
if (hasHigh) {
return {
safe: false,
recommendation: 'review',
reason: 'High severity advisory - user review strongly recommended',
};
}
return {
safe: false,
recommendation: 'review',
reason: 'Advisory found - review details before installing',
};
}
+2
View File
@@ -15,6 +15,8 @@ export interface Advisory {
references: string[];
cvss_score?: number;
nvd_url?: string;
exploitability_score?: 'high' | 'medium' | 'low' | 'unknown';
exploitability_rationale?: string;
source?: string;
github_issue_url?: string;
reporter?: {
@@ -11,6 +11,8 @@
import fs from 'fs';
import path from 'path';
import { z } from 'zod';
import { evaluateAdvisoryRisk, normalizeExploitabilityScore } from '../lib/risk.js';
import { matchesAffectedSpecifier } from '../lib/advisories.js';
// These variables are provided by the host environment (ipc-mcp-stdio.ts)
// when this code is integrated into the NanoClaw container agent.
@@ -18,8 +20,10 @@ declare const server: { tool: (...args: any[]) => void };
declare function writeIpcFile(dir: string, data: any): void;
declare const TASKS_DIR: string;
declare const groupFolder: string;
const CACHE_FILE = '/workspace/project/data/clawsec-advisory-cache.json';
// Add these helper functions to the file:
const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
const exploitabilityOrder: Record<string, number> = { high: 0, medium: 1, low: 2, unknown: 3 };
/**
* Discover installed skills in a directory
@@ -84,10 +88,7 @@ function findAdvisoryMatches(
const matchedAffected: string[] = [];
for (const affected of advisory.affected || []) {
const atIndex = affected.lastIndexOf('@');
const affectedName = atIndex > 0 ? affected.slice(0, atIndex) : affected;
if (affectedName === skill.name || affectedName === skill.dirName) {
if (matchesAffectedSpecifier(affected, skill.name, skill.version, skill.dirName)) {
matchedAffected.push(affected);
}
}
@@ -123,10 +124,8 @@ server.tool(
}
// Read cache from shared mount
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
try {
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
const installRoot = args.installRoot || path.join(process.env.HOME || '~', '.claude', 'skills');
// Discover installed skills
@@ -153,6 +152,8 @@ server.tool(
description: m.advisory.description,
action: m.advisory.action,
published: m.advisory.published,
exploitability_score: normalizeExploitabilityScore(m.advisory.exploitability_score),
exploitability_rationale: m.advisory.exploitability_rationale || null,
},
skill: m.skill,
matchedAffected: m.matchedAffected,
@@ -187,17 +188,13 @@ server.tool(
skillVersion: z.string().optional().describe('Version of skill (optional, for version-specific checks)'),
},
async (args) => {
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
try {
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
// Find matching advisories for this skill
const matchingAdvisories = cacheData.feed.advisories.filter((advisory: any) =>
advisory.affected.some((affected: string) => {
const atIndex = affected.lastIndexOf('@');
const affectedName = atIndex > 0 ? affected.slice(0, atIndex) : affected;
return affectedName === args.skillName;
return matchesAffectedSpecifier(affected, args.skillName, args.skillVersion || null);
})
);
@@ -215,34 +212,13 @@ server.tool(
};
}
// Evaluate severity
const hasMalicious = matchingAdvisories.some((a: any) => a.type === 'malicious');
const hasRemoveAction = matchingAdvisories.some((a: any) => a.action === 'remove');
const hasCritical = matchingAdvisories.some((a: any) => a.severity === 'critical');
const hasHigh = matchingAdvisories.some((a: any) => a.severity === 'high');
let recommendation: 'install' | 'block' | 'review';
let reason: string;
if (hasMalicious || hasRemoveAction) {
recommendation = 'block';
reason = 'Malicious skill or removal recommended by ClawSec';
} else if (hasCritical) {
recommendation = 'block';
reason = 'Critical security advisory - do not install';
} else if (hasHigh) {
recommendation = 'review';
reason = 'High severity advisory - user review strongly recommended';
} else {
recommendation = 'review';
reason = 'Advisory found - review details before installing';
}
const risk = evaluateAdvisoryRisk(matchingAdvisories);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
safe: false, // Always false when advisories exist
safe: risk.safe,
advisories: matchingAdvisories.map((a: any) => ({
id: a.id,
severity: a.severity,
@@ -252,10 +228,13 @@ server.tool(
action: a.action,
published: a.published,
affected: a.affected,
exploitability_score: normalizeExploitabilityScore(a.exploitability_score),
exploitability_rationale: a.exploitability_rationale || null,
})),
recommendation,
reason,
recommendation: risk.recommendation,
reason: risk.reason,
skillName: args.skillName,
skillVersion: args.skillVersion || null,
advisoryCount: matchingAdvisories.length,
}, null, 2),
}],
@@ -280,18 +259,18 @@ server.tool(
server.tool(
'clawsec_list_advisories',
'List ClawSec advisories with optional filtering. Use this to browse security advisories, filter by severity/type, or search for specific affected skills.',
'List ClawSec advisories with optional filtering. Use this to browse security advisories, filter by severity/type/exploitability, or search for specific affected skills.',
{
severity: z.enum(['critical', 'high', 'medium', 'low']).optional().describe('Filter by severity level'),
type: z.enum(['vulnerability', 'malicious', 'deprecated']).optional().describe('Filter by advisory type'),
type: z.string().optional().describe('Filter by advisory type (for example: vulnerable_skill, malicious_skill, prompt_injection)'),
exploitabilityScore: z.enum(['high', 'medium', 'low', 'unknown']).optional()
.describe('Filter by exploitability score'),
affectedSkill: z.string().optional().describe('Filter by affected skill name (partial match supported)'),
limit: z.number().optional().describe('Maximum number of results (default: unlimited)'),
},
async (args) => {
const cacheFile = '/workspace/project/data/clawsec-advisory-cache.json';
try {
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
let advisories = [...cacheData.feed.advisories];
// Apply filters
@@ -299,7 +278,13 @@ server.tool(
advisories = advisories.filter((a: any) => a.severity === args.severity);
}
if (args.type) {
advisories = advisories.filter((a: any) => a.type === args.type);
const typeFilter = String(args.type).toLowerCase().trim();
advisories = advisories.filter((a: any) => String(a.type || '').toLowerCase().trim() === typeFilter);
}
if (args.exploitabilityScore) {
advisories = advisories.filter(
(a: any) => normalizeExploitabilityScore(a.exploitability_score) === args.exploitabilityScore
);
}
if (args.affectedSkill) {
advisories = advisories.filter((a: any) =>
@@ -307,9 +292,13 @@ server.tool(
);
}
// Sort by severity (critical first) and published date (newest first)
const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
// Sort by exploitability first, then severity, then publish date (newest first).
advisories.sort((a: any, b: any) => {
const exploitabilityDiff =
(exploitabilityOrder[normalizeExploitabilityScore(a.exploitability_score)] ?? 999) -
(exploitabilityOrder[normalizeExploitabilityScore(b.exploitability_score)] ?? 999);
if (exploitabilityDiff !== 0) return exploitabilityDiff;
const severityDiff = (severityOrder[a.severity] || 999) - (severityOrder[b.severity] || 999);
if (severityDiff !== 0) return severityDiff;
return (b.published || '').localeCompare(a.published || '');
@@ -336,6 +325,8 @@ server.tool(
action: a.action,
published: a.published,
affected: a.affected,
exploitability_score: normalizeExploitabilityScore(a.exploitability_score),
exploitability_rationale: a.exploitability_rationale || null,
})),
total: cacheData.feed.advisories.length,
filtered: originalCount,
@@ -343,6 +334,7 @@ server.tool(
filters: {
severity: args.severity || null,
type: args.type || null,
exploitabilityScore: args.exploitabilityScore || null,
affectedSkill: args.affectedSkill || null,
limit: args.limit || null,
},
+14 -3
View File
@@ -1,6 +1,6 @@
{
"name": "clawsec-nanoclaw",
"version": "0.0.1",
"version": "0.0.2",
"description": "ClawSec security suite for NanoClaw - Advisory feed monitoring, MCP tools for vulnerability checking, and Ed25519 signature verification for containerized WhatsApp bot agents",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -27,6 +27,11 @@
"required": true,
"description": "NanoClaw skill documentation"
},
{
"path": "CHANGELOG.md",
"required": true,
"description": "Version history and release notes"
},
{
"path": "INSTALL.md",
"required": true,
@@ -62,6 +67,11 @@
"required": true,
"description": "TypeScript type definitions"
},
{
"path": "lib/risk.ts",
"required": true,
"description": "Shared advisory risk evaluation logic for host and MCP tools"
},
{
"path": "advisories/feed-signing-public.pem",
"required": true,
@@ -112,9 +122,10 @@
"capabilities": [
"Advisory feed monitoring from clawsec.prompt.security",
"MCP tools for agent-initiated vulnerability scans",
"Exploitability-aware advisory prioritization for agent environments",
"Pre-installation skill safety checks",
"Ed25519 signature verification for advisory feeds",
"Platform-specific advisory filtering (nanoclaw vs openclaw)",
"Platform metadata preserved in advisory records for downstream filtering",
"Containerized agent support with IPC communication"
],
"nanoclaw": {
@@ -135,7 +146,7 @@
},
"integration": {
"mcp_tools_file": "container/agent-runner/src/ipc-mcp-stdio.ts",
"ipc_handlers_file": "host/ipc-handler.ts",
"ipc_handlers_file": "src/ipc.ts",
"cache_location": "/workspace/project/data/clawsec-advisory-cache.json"
}
}
+15
View File
@@ -5,6 +5,21 @@ All notable changes to the ClawSec Suite will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.4] - 2026-02-28
### Added
- Advisory output snippets now include exploitability context in suite quick-check and heartbeat examples.
### Changed
- Clarified exploitability guidance to match runtime score values (`high|medium|low|unknown`).
- Prioritization guidance now emphasizes high-exploitability advisories for immediate handling.
### Fixed
- Kept exploitability enrichment in advisory workflows non-fatal per item so a single analysis failure does not abort feed updates.
## [0.1.3]
### Added
+13 -2
View File
@@ -121,6 +121,7 @@ else
while IFS= read -r id; do
[ -z "$id" ] && continue
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | "- [\(.severity | ascii_upcase)] \(.id): \(.title)"' "$FEED_TMP"
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Exploitability: \(.exploitability_score // "unknown" | ascii_upcase)"' "$FEED_TMP"
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Action: \(.action // "Review advisory details")"' "$FEED_TMP"
done < "$NEW_IDS_FILE"
else
@@ -194,8 +195,18 @@ fi
Heartbeat output should include:
- suite version status,
- advisory feed status,
- new advisory list (if any),
- new advisory list (if any) with exploitability scores,
- installed skills that appear in advisory `affected` lists,
- and a double-confirmation reminder before risky install/remove actions.
If your runtime sends alerts, treat `critical` and `high` advisories affecting installed skills as immediate notifications.
### Exploitability-Based Prioritization
When alerting on advisories, prioritize by **exploitability score** in addition to severity:
- `high` exploitability: Trivially or easily exploitable with public tooling, immediate action required
- `medium` exploitability: Exploitable with specific conditions, standard priority
- `low` exploitability: Difficult to exploit or theoretical, low priority
**Priority Rule**: A HIGH severity + HIGH exploitability CVE should be treated more urgently than a CRITICAL severity + LOW exploitability CVE.
If your runtime sends alerts, treat `high` exploitability advisories affecting installed skills as immediate notifications, regardless of severity rating.
+14 -1
View File
@@ -1,6 +1,6 @@
---
name: clawsec-suite
version: 0.1.3
version: 0.1.4
description: ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.
homepage: https://clawsec.prompt.security
clawdis:
@@ -234,12 +234,25 @@ if [ -s "$NEW_IDS_FILE" ]; then
while IFS= read -r id; do
[ -z "$id" ] && continue
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | "- [\(.severity | ascii_upcase)] \(.id): \(.title)"' "$TMP/feed.json"
jq -r --arg id "$id" '.advisories[] | select(.id == $id) | " Exploitability: \(.exploitability_score // "unknown" | ascii_upcase)"' "$TMP/feed.json"
done < "$NEW_IDS_FILE"
else
echo "FEED_OK - no new advisories"
fi
```
## Exploitability Context
Advisories in the feed can include `exploitability_score` and `exploitability_rationale` fields to help agents prioritize real-world threats:
- **Exploitability scores**: `high`, `medium`, `low`, or `unknown`
- **Context-aware assessment**: Considers attack vector, authentication requirements, and AI agent deployment patterns
- **Exploit availability**: Detects public exploits and weaponization status
When processing advisories, prioritize by exploitability in addition to severity. A HIGH severity + HIGH exploitability CVE is more urgent than a CRITICAL severity + LOW exploitability CVE.
For detailed methodology, see the [exploitability scoring documentation](../../wiki/exploitability-scoring.md).
## Heartbeat Integration
Use the suite heartbeat script as the single periodic security check entrypoint:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "clawsec-suite",
"version": "0.1.3",
"version": "0.1.4",
"description": "ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -11,25 +11,12 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { pass, fail, report, exitWithResults } from "./lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LIB_PATH = path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib");
const { advisoryAppliesToOpenclaw } = await import(`${LIB_PATH}/advisory_scope.mjs`);
let passCount = 0;
let failCount = 0;
function pass(name) {
passCount += 1;
console.log(`\u2713 ${name}`);
}
function fail(name, error) {
failCount += 1;
console.error(`\u2717 ${name}`);
console.error(` ${String(error)}`);
}
function testFindMatchesFiltersByApplicationScope() {
const testName = "advisoryAppliesToOpenclaw: openclaw + legacy advisories are considered";
@@ -89,10 +76,8 @@ function runTests() {
testFindMatchesAcceptsApplicationArray();
testInvalidApplicationValueFallsBackCompat();
console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) {
process.exit(1);
}
report();
exitWithResults();
}
runTests();
@@ -15,9 +15,9 @@
*/
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { pass, fail, report, exitWithResults, createTempDir } from "./lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LIB_PATH = path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib");
@@ -27,29 +27,6 @@ const { isAdvisorySuppressed, loadAdvisorySuppression } = await import(
);
let tempDir;
let passCount = 0;
let failCount = 0;
function pass(name) {
passCount++;
console.log(`\u2713 ${name}`);
}
function fail(name, error) {
failCount++;
console.error(`\u2717 ${name}`);
console.error(` ${String(error)}`);
}
async function setupTestDir() {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "advisory-suppression-test-"));
}
async function cleanupTestDir() {
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
}
function makeMatch(advisoryId, skillName, version = "1.0.0") {
return {
@@ -190,7 +167,7 @@ async function testMissingAdvisoryId() {
async function testLoadWithAdvisorySentinel() {
const testName = "loadAdvisorySuppression: loads config with advisory sentinel";
try {
const configFile = path.join(tempDir, "advisory-config.json");
const configFile = path.join(tempDir.path, "advisory-config.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["advisory"],
suppressions: [{
@@ -215,7 +192,7 @@ async function testLoadWithAdvisorySentinel() {
async function testLoadWithMissingSentinel() {
const testName = "loadAdvisorySuppression: missing sentinel returns empty config";
try {
const configFile = path.join(tempDir, "no-sentinel.json");
const configFile = path.join(tempDir.path, "no-sentinel.json");
await fs.writeFile(configFile, JSON.stringify({
suppressions: [{
checkId: "CVE-2026-25593",
@@ -239,7 +216,7 @@ async function testLoadWithMissingSentinel() {
async function testLoadWithAuditOnlySentinel() {
const testName = "loadAdvisorySuppression: audit-only sentinel returns empty for advisory";
try {
const configFile = path.join(tempDir, "audit-only.json");
const configFile = path.join(tempDir.path, "audit-only.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["audit"],
suppressions: [{
@@ -264,7 +241,7 @@ async function testLoadWithAuditOnlySentinel() {
async function testLoadWithBothSentinels() {
const testName = "loadAdvisorySuppression: both audit+advisory sentinels activates advisory";
try {
const configFile = path.join(tempDir, "both-sentinel.json");
const configFile = path.join(tempDir.path, "both-sentinel.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["audit", "advisory"],
suppressions: [{
@@ -289,7 +266,7 @@ async function testLoadWithBothSentinels() {
async function testLoadNonexistentExplicitPath() {
const testName = "loadAdvisorySuppression: explicit nonexistent path throws";
try {
await loadAdvisorySuppression(path.join(tempDir, "does-not-exist.json"));
await loadAdvisorySuppression(path.join(tempDir.path, "does-not-exist.json"));
fail(testName, "Expected error for nonexistent explicit path");
} catch (error) {
if (String(error).includes("not found")) {
@@ -328,7 +305,7 @@ async function testLoadNoConfigReturnsEmpty() {
async function testEnvPathHomeExpansion() {
const testName = "loadAdvisorySuppression: OPENCLAW_AUDIT_CONFIG expands $HOME";
try {
const configFile = path.join(tempDir, "env-home.json");
const configFile = path.join(tempDir.path, "env-home.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["advisory"],
suppressions: [{
@@ -341,7 +318,7 @@ async function testEnvPathHomeExpansion() {
const savedConfig = process.env.OPENCLAW_AUDIT_CONFIG;
const savedHome = process.env.HOME;
process.env.HOME = tempDir;
process.env.HOME = tempDir.path;
process.env.OPENCLAW_AUDIT_CONFIG = "$HOME/env-home.json";
try {
const config = await loadAdvisorySuppression();
@@ -390,7 +367,7 @@ async function testEscapedHomeTokenRejected() {
async function runAllTests() {
console.log("=== Advisory Suppression Tests ===\n");
await setupTestDir();
tempDir = await createTempDir();
try {
// isAdvisorySuppressed tests
@@ -412,15 +389,11 @@ async function runAllTests() {
await testEnvPathHomeExpansion();
await testEscapedHomeTokenRejected();
} finally {
await cleanupTestDir();
await tempDir.cleanup();
}
console.log("");
console.log(`=== Results: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) {
process.exit(1);
}
report();
exitWithResults();
}
runAllTests().catch((err) => {
@@ -14,9 +14,17 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
pass,
fail,
report,
exitWithResults,
generateEd25519KeyPair,
signPayload,
createTempDir,
} from "./lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LIB_PATH = path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib");
@@ -26,33 +34,7 @@ const { verifySignedPayload, loadLocalFeed, isValidFeedPayload } = await import(
`${LIB_PATH}/feed.mjs`
);
let tempDir;
let passCount = 0;
let failCount = 0;
function pass(name) {
passCount++;
console.log(`${name}`);
}
function fail(name, error) {
failCount++;
console.error(`${name}`);
console.error(` ${String(error)}`);
}
function generateEd25519KeyPair() {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
return { publicKeyPem, privateKeyPem };
}
function signPayload(data, privateKeyPem) {
const privateKey = crypto.createPrivateKey(privateKeyPem);
const signature = crypto.sign(null, Buffer.from(data, "utf8"), privateKey);
return signature.toString("base64");
}
let tempDirCleanup;
function createValidFeed() {
return JSON.stringify(
@@ -88,16 +70,6 @@ function createChecksumManifest(files) {
);
}
async function setupTestDir() {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawsec-test-"));
}
async function cleanupTestDir() {
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - valid signature
// -----------------------------------------------------------------------------
@@ -252,11 +224,11 @@ async function testLoadLocalFeed_ValidSignedFeed() {
const checksumSignature = signPayload(checksumManifest, privateKeyPem);
// Write files
const feedPath = path.join(tempDir, "feed.json");
const sigPath = path.join(tempDir, "feed.json.sig");
const checksumPath = path.join(tempDir, "checksums.json");
const checksumSigPath = path.join(tempDir, "checksums.json.sig");
const keyPath = path.join(tempDir, "feed-signing-public.pem");
const feedPath = path.join(globalThis.__testTempDir, "feed.json");
const sigPath = path.join(globalThis.__testTempDir, "feed.json.sig");
const checksumPath = path.join(globalThis.__testTempDir, "checksums.json");
const checksumSigPath = path.join(globalThis.__testTempDir, "checksums.json.sig");
const keyPath = path.join(globalThis.__testTempDir, "feed-signing-public.pem");
await fs.writeFile(feedPath, feedContent);
await fs.writeFile(sigPath, feedSignature + "\n");
@@ -293,7 +265,7 @@ async function testLoadLocalFeed_AdvisoriesPrefixedChecksumKeys() {
const feedContent = createValidFeed();
const feedSignature = signPayload(feedContent, privateKeyPem);
const advisoriesDir = path.join(tempDir, "advisories");
const advisoriesDir = path.join(globalThis.__testTempDir, "advisories");
await fs.mkdir(advisoriesDir, { recursive: true });
const checksumManifest = createChecksumManifest({
@@ -347,8 +319,8 @@ async function testLoadLocalFeed_TamperedFeedFails() {
// Tamper with feed after signing
const tamperedFeed = feedContent.replace("TEST-001", "TAMPERED-001");
const feedPath = path.join(tempDir, "tampered-feed.json");
const sigPath = path.join(tempDir, "tampered-feed.json.sig");
const feedPath = path.join(globalThis.__testTempDir, "tampered-feed.json");
const sigPath = path.join(globalThis.__testTempDir, "tampered-feed.json.sig");
await fs.writeFile(feedPath, tamperedFeed);
await fs.writeFile(sigPath, feedSignature + "\n");
@@ -383,8 +355,8 @@ async function testLoadLocalFeed_MissingSignatureFails() {
const { publicKeyPem } = generateEd25519KeyPair();
const feedContent = createValidFeed();
const feedPath = path.join(tempDir, "nosig-feed.json");
const sigPath = path.join(tempDir, "nosig-feed.json.sig");
const feedPath = path.join(globalThis.__testTempDir, "nosig-feed.json");
const sigPath = path.join(globalThis.__testTempDir, "nosig-feed.json.sig");
await fs.writeFile(feedPath, feedContent);
// Don't write signature file
@@ -418,7 +390,7 @@ async function testLoadLocalFeed_AllowUnsignedBypasses() {
try {
const feedContent = createValidFeed();
const feedPath = path.join(tempDir, "unsigned-feed.json");
const feedPath = path.join(globalThis.__testTempDir, "unsigned-feed.json");
await fs.writeFile(feedPath, feedContent);
const feed = await loadLocalFeed(feedPath, {
@@ -462,10 +434,10 @@ async function testLoadLocalFeed_ChecksumMismatchFails() {
);
const checksumSignature = signPayload(badChecksumManifest, privateKeyPem);
const feedPath = path.join(tempDir, "badcs-feed.json");
const sigPath = path.join(tempDir, "badcs-feed.json.sig");
const checksumPath = path.join(tempDir, "badcs-checksums.json");
const checksumSigPath = path.join(tempDir, "badcs-checksums.json.sig");
const feedPath = path.join(globalThis.__testTempDir, "badcs-feed.json");
const sigPath = path.join(globalThis.__testTempDir, "badcs-feed.json.sig");
const checksumPath = path.join(globalThis.__testTempDir, "badcs-checksums.json");
const checksumSigPath = path.join(globalThis.__testTempDir, "badcs-checksums.json.sig");
await fs.writeFile(feedPath, feedContent);
await fs.writeFile(sigPath, feedSignature + "\n");
@@ -580,7 +552,11 @@ async function testIsValidFeedPayload_AdvisoryMissingId() {
async function runTests() {
console.log("=== ClawSec Feed Verification Tests ===\n");
await setupTestDir();
const tempDir = await createTempDir();
tempDirCleanup = tempDir.cleanup;
// Store temp dir path in module scope for tests to access
globalThis.__testTempDir = tempDir.path;
try {
// Signature verification tests
@@ -604,14 +580,11 @@ async function runTests() {
await testIsValidFeedPayload_MissingVersion();
await testIsValidFeedPayload_AdvisoryMissingId();
} finally {
await cleanupTestDir();
await tempDirCleanup();
}
console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) {
process.exit(1);
}
report();
exitWithResults();
}
runTests().catch((error) => {
@@ -0,0 +1,62 @@
import assert from "node:assert/strict";
import path from "node:path";
import fc from "fast-check";
import { parseAffectedSpecifier } from "../hooks/clawsec-advisory-guardian/lib/feed.mjs";
import { normalizeSkillName, resolveConfiguredPath, uniqueStrings } from "../hooks/clawsec-advisory-guardian/lib/utils.mjs";
const SAFE_SEGMENT = fc
.array(fc.constantFrom(...("abcdefghijklmnopqrstuvwxyz0123456789-_")), { minLength: 1, maxLength: 24 })
.map((chars) => chars.join(""));
/**
* Runs property-based fuzz checks for advisory parsing and utility behavior.
*/
export function runFuzzProperties() {
fc.assert(
fc.property(fc.string(), (raw) => {
const expected = String(raw ?? "")
.trim()
.toLowerCase();
assert.equal(normalizeSkillName(raw), expected);
}),
{ numRuns: 300 },
);
fc.assert(
fc.property(fc.array(fc.string(), { maxLength: 40 }), (values) => {
const deduped = uniqueStrings(values);
assert.deepEqual(deduped, Array.from(new Set(values)));
}),
{ numRuns: 200 },
);
fc.assert(
fc.property(fc.string(), fc.string(), (left, right) => {
const rawSpecifier = `${left}@${right}`;
const specifier = rawSpecifier.trim();
const parsed = parseAffectedSpecifier(rawSpecifier);
assert.ok(parsed !== null);
const atIndex = specifier.lastIndexOf("@");
if (atIndex <= 0) {
assert.equal(parsed.name, specifier);
assert.equal(parsed.versionSpec, "*");
} else {
assert.equal(parsed.name, specifier.slice(0, atIndex));
assert.equal(parsed.versionSpec, specifier.slice(atIndex + 1));
}
}),
{ numRuns: 300 },
);
fc.assert(
fc.property(SAFE_SEGMENT, (suffix) => {
const fallback = `/tmp/clawsec-suite/${suffix}`;
const resolved = resolveConfiguredPath(`\\$HOME/${suffix}`, fallback, {
label: "FUZZ_PATH",
});
assert.equal(resolved, path.normalize(fallback));
}),
{ numRuns: 200 },
);
}
@@ -0,0 +1,22 @@
#!/usr/bin/env node
/**
* Property-based fuzzing checks for core advisory parsing/path helpers.
*
* Run: node skills/clawsec-suite/test/fuzz_properties.test.mjs
*/
import { pass, fail, report, exitWithResults } from "./lib/test_harness.mjs";
import { runFuzzProperties } from "./fuzz_properties.js";
console.log("=== ClawSec Fast-Check Fuzz Properties ===\n");
try {
runFuzzProperties();
pass("Property-based fuzz tests");
} catch (error) {
fail("Property-based fuzz tests", error);
}
report();
exitWithResults();
@@ -0,0 +1,137 @@
#!/usr/bin/env node
/**
* Property-based fuzz tests for semver matching, advisory scope, and suppression matching.
*
* Run: node skills/clawsec-suite/test/fuzz_semver_scope_suppression.test.mjs
*/
import assert from "node:assert/strict";
import fc from "fast-check";
import { advisoryAppliesToOpenclaw } from "../hooks/clawsec-advisory-guardian/lib/advisory_scope.mjs";
import { isAdvisorySuppressed } from "../hooks/clawsec-advisory-guardian/lib/suppression.mjs";
import { compareSemver, parseSemver, versionMatches } from "../hooks/clawsec-advisory-guardian/lib/version.mjs";
const semverCoreArb = fc.tuple(
fc.integer({ min: 0, max: 999 }),
fc.integer({ min: 0, max: 999 }),
fc.integer({ min: 0, max: 999 }),
);
const semverArb = semverCoreArb.map(([major, minor, patch]) => `${major}.${minor}.${patch}`);
const idArb = fc.string({ minLength: 1, maxLength: 24 });
const skillArb = fc.string({ minLength: 1, maxLength: 24 });
function runSemverProperties() {
fc.assert(
fc.property(semverCoreArb, ([major, minor, patch]) => {
const version = `v${major}.${minor}.${patch}`;
assert.deepEqual(parseSemver(version), [major, minor, patch]);
}),
{ numRuns: 250 },
);
fc.assert(
fc.property(semverArb, semverArb, (left, right) => {
const leftVsRight = compareSemver(left, right);
const rightVsLeft = compareSemver(right, left);
assert.notEqual(leftVsRight, null);
assert.notEqual(rightVsLeft, null);
assert.equal(leftVsRight, -rightVsLeft);
}),
{ numRuns: 250 },
);
fc.assert(
fc.property(semverArb, semverArb, (left, right) => {
const compared = compareSemver(left, right);
assert.notEqual(compared, null);
assert.equal(versionMatches(left, `>=${right}`), compared >= 0);
assert.equal(versionMatches(left, `<=${right}`), compared <= 0);
assert.equal(versionMatches(left, `>${right}`), compared > 0);
assert.equal(versionMatches(left, `<${right}`), compared < 0);
assert.equal(versionMatches(left, `=${right}`), compared === 0);
}),
{ numRuns: 250 },
);
}
function runAdvisoryScopeProperties() {
fc.assert(
fc.property(fc.string(), (application) => {
const normalized = application.trim().toLowerCase();
const expected = normalized === "" || normalized === "openclaw" || normalized === "all";
assert.equal(advisoryAppliesToOpenclaw({ application }), expected);
}),
{ numRuns: 250 },
);
fc.assert(
fc.property(fc.array(fc.string(), { maxLength: 8 }), (applications) => {
const normalized = applications
.map((entry) => entry.trim().toLowerCase())
.filter(Boolean);
const expected =
normalized.length === 0 || normalized.includes("openclaw") || normalized.includes("all");
assert.equal(advisoryAppliesToOpenclaw({ application: applications }), expected);
}),
{ numRuns: 250 },
);
assert.equal(advisoryAppliesToOpenclaw({}), true);
assert.equal(advisoryAppliesToOpenclaw({ application: null }), true);
}
function runSuppressionProperties() {
fc.assert(
fc.property(idArb, skillArb, (id, skill) => {
const match = {
advisory: { id },
skill: { name: skill.toUpperCase() },
};
const suppressions = [
{
checkId: id,
skill: skill.toLowerCase(),
reason: "fuzz",
suppressedAt: "2026-02-25",
},
];
assert.equal(isAdvisorySuppressed(match, suppressions), true);
}),
{ numRuns: 250 },
);
fc.assert(
fc.property(idArb, idArb, skillArb, (targetId, otherId, skill) => {
const differentId = targetId === otherId ? `${otherId}-x` : otherId;
const match = {
advisory: { id: targetId },
skill: { name: skill },
};
const suppressions = [
{
checkId: differentId,
skill,
reason: "fuzz",
suppressedAt: "2026-02-25",
},
];
assert.equal(isAdvisorySuppressed(match, suppressions), false);
}),
{ numRuns: 250 },
);
}
try {
console.log("=== ClawSec Semver/Scope/Suppression Fuzz Properties ===\n");
runSemverProperties();
runAdvisoryScopeProperties();
runSuppressionProperties();
console.log("=== Results: all fuzz properties passed ===");
} catch (error) {
console.error("Fuzz property test failed:");
console.error(error);
process.exit(1);
}
@@ -18,37 +18,19 @@ import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import {
pass,
fail,
report,
exitWithResults,
generateEd25519KeyPair,
signPayload,
} from "./lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT_PATH = path.resolve(__dirname, "..", "scripts", "guarded_skill_install.mjs");
let tempDir;
let passCount = 0;
let failCount = 0;
function pass(name) {
passCount++;
console.log(`${name}`);
}
function fail(name, error) {
failCount++;
console.error(`${name}`);
console.error(` ${String(error)}`);
}
function generateEd25519KeyPair() {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
return { publicKeyPem, privateKeyPem };
}
function signPayload(data, privateKeyPem) {
const privateKey = crypto.createPrivateKey(privateKeyPem);
const signature = crypto.sign(null, Buffer.from(data, "utf8"), privateKey);
return signature.toString("base64");
}
function createFeed(advisories) {
return JSON.stringify(
@@ -416,11 +398,8 @@ async function runTests() {
await cleanupTestDir();
}
console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) {
process.exit(1);
}
report();
exitWithResults();
}
runTests().catch((error) => {
@@ -0,0 +1,182 @@
/**
* Shared test harness for clawsec-suite tests.
* Provides consistent test reporting and runner utilities.
*/
import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
let passCount = 0;
let failCount = 0;
/**
* Records a passing test.
* @param {string} name - Test name
*/
export function pass(name) {
passCount++;
console.log(`${name}`);
}
/**
* Records a failing test.
* @param {string} name - Test name
* @param {Error|string} error - Error details
*/
export function fail(name, error) {
failCount++;
console.error(`${name}`);
console.error(` ${String(error)}`);
}
/**
* Gets current test statistics.
* @returns {{passCount: number, failCount: number}}
*/
export function getStats() {
return { passCount, failCount };
}
/**
* Reports final test results to console.
*/
export function report() {
console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
}
/**
* Exits with appropriate code based on test results.
* Exit code 0 for success, 1 for failures.
*/
export function exitWithResults() {
if (failCount > 0) {
process.exit(1);
}
}
/**
* Creates an isolated test runner with its own pass/fail counters.
* Useful for running independent test suites within the same process.
* @returns {{pass: Function, fail: Function, getStats: Function, report: Function, exitWithResults: Function}}
*/
export function createTestRunner() {
let localPassCount = 0;
let localFailCount = 0;
return {
/**
* Records a passing test.
* @param {string} name - Test name
*/
pass(name) {
localPassCount++;
console.log(`${name}`);
},
/**
* Records a failing test.
* @param {string} name - Test name
* @param {Error|string} error - Error details
*/
fail(name, error) {
localFailCount++;
console.error(`${name}`);
console.error(` ${String(error)}`);
},
/**
* Gets current test statistics.
* @returns {{passCount: number, failCount: number}}
*/
getStats() {
return { passCount: localPassCount, failCount: localFailCount };
},
/**
* Reports final test results to console.
*/
report() {
console.log(`\n=== Results: ${localPassCount} passed, ${localFailCount} failed ===`);
},
/**
* Exits with appropriate code based on test results.
* Exit code 0 for success, 1 for failures.
*/
exitWithResults() {
if (localFailCount > 0) {
process.exit(1);
}
},
};
}
/**
* Generates an Ed25519 keypair for test use.
* @returns {{publicKeyPem: string, privateKeyPem: string}}
*/
export function generateEd25519KeyPair() {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
return { publicKeyPem, privateKeyPem };
}
/**
* Signs a payload with an Ed25519 private key.
* @param {string} data - Data to sign
* @param {string} privateKeyPem - PEM-encoded private key
* @returns {string} Base64-encoded signature
*/
export function signPayload(data, privateKeyPem) {
const privateKey = crypto.createPrivateKey(privateKeyPem);
const signature = crypto.sign(null, Buffer.from(data, "utf8"), privateKey);
return signature.toString("base64");
}
/**
* Creates a temporary directory for test use.
* @returns {Promise<{path: string, cleanup: Function}>} Object with temp dir path and cleanup function
*/
export async function createTempDir() {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawsec-test-"));
return {
path: tmpDir,
cleanup: async () => {
try {
await fs.rm(tmpDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
},
};
}
/**
* Temporarily sets an environment variable for the duration of a function.
* Restores the original value (or deletes the variable) after the function completes.
* @param {string} key - Environment variable name
* @param {string|undefined} value - Value to set (undefined to delete)
* @param {Function} fn - Function to execute with the modified environment
* @returns {Promise<*>} Result of the function
*/
export async function withEnv(key, value, fn) {
const oldValue = process.env[key];
try {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
return await fn();
} finally {
if (oldValue === undefined) {
delete process.env[key];
} else {
process.env[key] = oldValue;
}
}
}
@@ -8,43 +8,12 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { pass, fail, report, exitWithResults, withEnv } from "./lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LIB_PATH = path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib");
const { resolveUserPath, resolveConfiguredPath } = await import(`${LIB_PATH}/utils.mjs`);
let passCount = 0;
let failCount = 0;
function pass(name) {
passCount += 1;
console.log(`\u2713 ${name}`);
}
function fail(name, error) {
failCount += 1;
console.error(`\u2717 ${name}`);
console.error(` ${String(error)}`);
}
async function withEnv(key, value, fn) {
const oldValue = process.env[key];
try {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
return await fn();
} finally {
if (oldValue === undefined) {
delete process.env[key];
} else {
process.env[key] = oldValue;
}
}
}
async function testTildeExpansion() {
const testName = "resolveUserPath: expands leading tilde";
await withEnv("HOME", "/tmp/clawsec-home", async () => {
@@ -157,10 +126,8 @@ async function runTests() {
await testConfiguredPathFallbackOnInvalidExplicit();
await testConfiguredPathUsesValidExplicit();
console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) {
process.exit(1);
}
report();
exitWithResults();
}
runTests().catch((error) => {
@@ -15,24 +15,11 @@ import http from "node:http";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { pass, fail, report, exitWithResults } from "./lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT_PATH = path.resolve(__dirname, "..", "scripts", "discover_skill_catalog.mjs");
let passCount = 0;
let failCount = 0;
function pass(name) {
passCount += 1;
console.log(`${name}`);
}
function fail(name, error) {
failCount += 1;
console.error(`${name}`);
console.error(` ${String(error)}`);
}
function runCatalogScript(args, env = {}) {
return new Promise((resolve) => {
const proc = spawn("node", [SCRIPT_PATH, ...args], {
@@ -237,11 +224,8 @@ async function runTests() {
await testInvalidRemotePayloadFallsBack();
await testUnreachableRemoteFallsBack();
console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) {
process.exit(1);
}
report();
exitWithResults();
}
runTests().catch((error) => {
@@ -16,39 +16,16 @@
*/
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { pass, fail, report, exitWithResults, createTempDir } from "../../clawsec-suite/test/lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT_PATH = path.resolve(__dirname, "..", "scripts", "render_report.mjs");
const NODE_BIN = process.execPath;
let tempDir;
let passCount = 0;
let failCount = 0;
function pass(name) {
passCount++;
console.log(`${name}`);
}
function fail(name, error) {
failCount++;
console.error(`${name}`);
console.error(` ${String(error)}`);
}
async function setupTestDir() {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "render-report-test-"));
}
async function cleanupTestDir() {
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
}
function createAuditJson(findings) {
return JSON.stringify({
@@ -730,7 +707,8 @@ async function testConfigWithoutEnableFlagDoesNotSuppress() {
// Main test runner
// -----------------------------------------------------------------------------
async function runAllTests() {
await setupTestDir();
const tmpDir = await createTempDir();
tempDir = tmpDir.path;
try {
await testSuppressedFindingsDisplayed();
@@ -745,16 +723,11 @@ async function runAllTests() {
await testEmptySuppressions();
await testConfigWithoutEnableFlagDoesNotSuppress();
} finally {
await cleanupTestDir();
await tmpDir.cleanup();
}
console.log("");
console.log(`Passed: ${passCount}`);
console.log(`Failed: ${failCount}`);
if (failCount > 0) {
process.exit(1);
}
report();
exitWithResults();
}
runAllTests().catch((err) => {
@@ -19,57 +19,33 @@
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import {
pass,
fail,
report,
exitWithResults,
createTempDir,
withEnv,
} from "../../clawsec-suite/test/lib/test_harness.mjs";
import { loadSuppressionConfig } from "../scripts/load_suppression_config.mjs";
let passCount = 0;
let failCount = 0;
function pass(name) {
passCount += 1;
console.log(`\u2713 ${name}`);
}
function fail(name, error) {
failCount += 1;
console.error(`\u2717 ${name}`);
console.error(` ${String(error)}`);
}
/**
* Creates a temporary file with the given content.
* Wrapper around createTempDir for test config file creation.
* @param {string} content - File content
* @returns {Promise<{path: string, cleanup: Function}>}
*/
async function withTempFile(content) {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-"));
const tmpFile = path.join(tmpDir, "test-config.json");
const tmpDir = await createTempDir();
const tmpFile = path.join(tmpDir.path, "test-config.json");
await fs.writeFile(tmpFile, content, "utf8");
return {
path: tmpFile,
cleanup: async () => {
try {
await fs.rm(tmpDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
},
cleanup: tmpDir.cleanup,
};
}
async function withEnv(key, value, fn) {
const oldValue = process.env[key];
try {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
return await fn();
} finally {
if (oldValue === undefined) {
delete process.env[key];
} else {
process.env[key] = oldValue;
}
}
}
/** Suppress stderr output during a function call (avoids noisy warnings in test output). */
async function silenceStderr(fn) {
const original = process.stderr.write;
@@ -748,11 +724,8 @@ async function runTests() {
await testMissingSentinel();
await testWrongSentinel();
console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) {
process.exit(1);
}
report();
exitWithResults();
}
runTests().catch((error) => {
@@ -0,0 +1,93 @@
#!/usr/bin/env node
/**
* Property-based fuzz tests for openclaw suppression config gating behavior.
*
* Run: node skills/openclaw-audit-watchdog/test/suppression_config_fuzz.test.mjs
*/
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import fc from "fast-check";
import { loadSuppressionConfig } from "../scripts/load_suppression_config.mjs";
const pipelineArb = fc.constantFrom("audit", "advisory", "watchdog");
function makeValidConfig({ pipeline, includePipeline }) {
const enabledFor = includePipeline ? [pipeline.toUpperCase(), "other"] : ["other"];
return JSON.stringify({
enabledFor,
suppressions: [
{
checkId: "SCAN-001",
skill: "soul-guardian",
reason: "fuzz test",
suppressedAt: "2026-02-25",
},
],
});
}
async function withTempConfig(content, fn) {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "watchdog-fuzz-"));
const configPath = path.join(tmpDir, "suppression.json");
await fs.writeFile(configPath, content, "utf8");
try {
await fn(configPath);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
}
async function withSilencedStderr(fn) {
const originalWrite = process.stderr.write;
process.stderr.write = () => true;
try {
return await fn();
} finally {
process.stderr.write = originalWrite;
}
}
async function runProperties() {
await fc.assert(
fc.asyncProperty(fc.string(), pipelineArb, async (rawPath, pipeline) => {
const result = await loadSuppressionConfig(rawPath, { enabled: false, pipeline });
assert.equal(result.source, "none");
assert.deepEqual(result.suppressions, []);
}),
{ numRuns: 120 },
);
await fc.assert(
fc.asyncProperty(pipelineArb, fc.boolean(), async (pipeline, includePipeline) => {
const content = makeValidConfig({ pipeline, includePipeline });
await withTempConfig(content, async (configPath) => {
const result = await withSilencedStderr(() =>
loadSuppressionConfig(configPath, { enabled: true, pipeline }),
);
if (includePipeline) {
assert.equal(result.source, configPath);
assert.equal(result.suppressions.length, 1);
assert.equal(result.suppressions[0].checkId, "SCAN-001");
} else {
assert.equal(result.source, "none");
assert.deepEqual(result.suppressions, []);
}
});
}),
{ numRuns: 80 },
);
}
try {
console.log("=== OpenClaw Suppression Config Fuzz Properties ===\n");
await runProperties();
console.log("=== Results: all fuzz properties passed ===");
} catch (error) {
console.error("Fuzz property test failed:");
console.error(error);
process.exit(1);
}
+1
View File
@@ -41,6 +41,7 @@ export interface Advisory {
references?: string[];
cvss_score?: number | null;
nvd_url?: string;
platforms?: string[];
// Community report fields (source defaults to "Prompt Security Staff" when absent)
source?: string;
github_issue_url?: string;
+531
View File
@@ -0,0 +1,531 @@
#!/usr/bin/env python3
"""
Exploitability Analyzer - Analyzes CVE exploitability in OpenClaw/NanoClaw deployments
Usage:
python utils/analyze_exploitability.py --help
echo '{"cve_id":"CVE-2026-27488","cvss_score":7.3}' | python utils/analyze_exploitability.py --json
python utils/analyze_exploitability.py --test-cases
Example:
cat cve-data.json | python utils/analyze_exploitability.py --json
"""
import argparse
import json
import sys
from typing import Any
def parse_cvss_vector(vector_string: str) -> dict[str, str]:
"""
Parse CVSS v2, v3.0, or v3.1 vector string into components.
Args:
vector_string: CVSS vector (e.g., "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")
Returns:
Dictionary of CVSS metrics and values
"""
if not vector_string:
return {}
metrics = {}
normalized = vector_string.strip()
# Remove leading CVSS v3.x prefix if present (e.g., "CVSS:3.1/")
if normalized.startswith("CVSS:3"):
_, separator, remainder = normalized.partition("/")
normalized = remainder if separator else ""
# Remove surrounding parentheses/whitespace used by some CVSS v2 strings.
normalized = normalized.strip().strip("()").strip()
if not normalized:
return metrics
# Parse all vector formats with shared key/value extraction logic.
for part in normalized.split("/"):
if ":" in part:
key, value = part.split(":", 1)
metrics[key] = value
return metrics
def analyze_attack_vector(cvss_metrics: dict[str, str]) -> dict[str, Any]:
"""
Analyze attack vector from CVSS metrics.
Args:
cvss_metrics: Parsed CVSS metrics dictionary
Returns:
Dictionary with attack vector analysis
"""
analysis = {
"is_network_accessible": False,
"requires_authentication": True,
"requires_user_interaction": True,
"complexity": "unknown"
}
# Attack Vector (AV)
av = cvss_metrics.get("AV", "")
if av == "N": # Network
analysis["is_network_accessible"] = True
elif av == "A": # Adjacent Network
analysis["is_network_accessible"] = True
elif av in ["L", "P"]: # Local or Physical
analysis["is_network_accessible"] = False
# Privileges Required (PR) / Authentication (AU for v2)
pr = cvss_metrics.get("PR", cvss_metrics.get("Au", ""))
if pr in ["N", "NONE"]:
analysis["requires_authentication"] = False
elif pr in ["L", "H", "SINGLE", "MULTIPLE"]:
analysis["requires_authentication"] = True
# User Interaction (UI)
ui = cvss_metrics.get("UI", "")
if ui == "N": # None
analysis["requires_user_interaction"] = False
elif ui == "R": # Required
analysis["requires_user_interaction"] = True
# Attack Complexity (AC)
ac = cvss_metrics.get("AC", "")
if ac == "L":
analysis["complexity"] = "low"
elif ac in ["M", "H"]:
analysis["complexity"] = "high"
return analysis
def detect_exploit_availability(references: list[str]) -> dict[str, Any]:
"""
Detect if exploits are publicly available based on reference URLs.
Args:
references: List of reference URLs
Returns:
Dictionary with exploit_available (bool) and exploit_sources (list)
"""
exploit_indicators = [
"exploit-db.com",
"exploit-database",
"exploitdb",
"packetstormsecurity.com",
"packetstorm",
"github.com/exploit",
"github.com/poc",
"github.com/proof-of-concept",
"metasploit",
"exploit/",
"/exploit",
"/poc",
"/proof-of-concept",
"exploitability",
"exploit-code",
]
exploit_sources = []
for ref in references:
ref_lower = ref.lower()
for indicator in exploit_indicators:
if indicator in ref_lower:
exploit_sources.append(ref)
break
return {
"exploit_available": len(exploit_sources) > 0,
"exploit_sources": exploit_sources
}
def analyze_exploitability(cve_data: dict[str, Any], check_exploits: bool = False) -> dict[str, Any]:
"""
Analyze CVE exploitability for OpenClaw/NanoClaw deployments.
Args:
cve_data: Dictionary containing CVE information with keys:
- cve_id: CVE identifier
- cvss_score: CVSS base score (float)
- cvss_vector: CVSS vector string (optional)
- type: Vulnerability type
- description: CVE description text
- references: List of reference URLs (optional)
check_exploits: Whether to check references for exploit availability
Returns:
Dictionary with exploitability_score (high/medium/low/unknown) and rationale
"""
cve_id = cve_data.get("cve_id", "unknown")
cvss_score = cve_data.get("cvss_score", 0.0)
cvss_vector = cve_data.get("cvss_vector", "")
vuln_type = cve_data.get("type", "")
description = cve_data.get("description", "")
references = cve_data.get("references", [])
# Parse CVSS vector if available
cvss_metrics = parse_cvss_vector(cvss_vector)
attack_analysis = analyze_attack_vector(cvss_metrics)
# Initial scoring based on CVSS
score = "unknown"
rationale_parts = []
# CVSS-based baseline
if cvss_score >= 9.0:
score = "high"
rationale_parts.append(f"Critical CVSS score ({cvss_score})")
elif cvss_score >= 7.0:
score = "high"
rationale_parts.append(f"High CVSS score ({cvss_score})")
elif cvss_score >= 4.0:
score = "medium"
rationale_parts.append(f"Medium CVSS score ({cvss_score})")
elif cvss_score > 0:
score = "low"
rationale_parts.append(f"Low CVSS score ({cvss_score})")
else:
score = "unknown"
rationale_parts.append("No CVSS score available")
# Adjust based on attack vector analysis
if attack_analysis["is_network_accessible"]:
if not attack_analysis["requires_authentication"] and not attack_analysis["requires_user_interaction"]:
# Network accessible, no auth, no user interaction = highly exploitable
if score == "medium":
score = "high"
rationale_parts.append("remotely exploitable without authentication")
else:
rationale_parts.append("network accessible")
else:
# Local-only vulnerabilities are less critical in agent deployments
if score == "high":
score = "medium"
rationale_parts.append("requires local access")
# OpenClaw/NanoClaw deployment context - adjust based on vulnerability type
vuln_type_lower = vuln_type.lower()
description_lower = description.lower()
# High-risk vulnerability types in AI agent deployments
if any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"ssrf", "server_side_request_forgery", "server-side request forgery"
]):
# SSRF is critical for agents that make external API calls
if score != "high" and cvss_score >= 6.0:
score = "high"
rationale_parts.append("SSRF affects agents making external requests")
elif any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"path_traversal", "path traversal", "directory traversal", "file_inclusion"
]):
# Path traversal is critical for agents with file system access
if score != "high" and cvss_score >= 6.0:
score = "high"
rationale_parts.append("path traversal affects agents with file access")
elif any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"rce", "remote_code_execution", "remote code execution", "code_injection",
"command_injection", "command injection", "arbitrary code"
]):
# RCE is always critical regardless of other factors
score = "high"
rationale_parts.append("RCE is critical in agent deployments")
elif any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"prototype_pollution", "prototype pollution"
]):
# Prototype pollution in Node.js agents can lead to RCE
if score == "low":
score = "medium"
rationale_parts.append("prototype pollution can escalate in Node.js agents")
elif any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"xss", "cross_site_scripting", "cross-site scripting", "reflected xss", "stored xss"
]):
# XSS is lower risk in headless agent deployments (no browser rendering)
if score == "high" and not attack_analysis["is_network_accessible"]:
score = "medium"
rationale_parts.append("XSS has limited impact in headless agents")
elif any(keyword in vuln_type_lower or keyword in description_lower for keyword in [
"sql_injection", "sql injection", "nosql injection"
]):
# SQL injection depends on whether agent uses databases
if attack_analysis["is_network_accessible"] and not attack_analysis["requires_authentication"]:
if score == "medium":
score = "high"
rationale_parts.append("injection affects agents with database access")
# Check for exploit availability if requested
exploit_info = {"exploit_available": False, "exploit_sources": []}
if check_exploits and references:
exploit_info = detect_exploit_availability(references)
if exploit_info["exploit_available"]:
# Elevate score if public exploits exist
if score == "low":
score = "medium"
elif score == "medium":
score = "high"
elif score == "unknown" and cvss_score > 0:
# If we have some CVSS score but it was unknown, upgrade to at least medium
score = "medium"
exploit_count = len(exploit_info["exploit_sources"])
source_suffix = "s" if exploit_count > 1 else ""
rationale_parts.append(
f"public exploit available ({exploit_count} source{source_suffix})"
)
# Build rationale string
rationale = "; ".join(rationale_parts[:5]) # Limit to first 5 parts for context
result = {
"cve_id": cve_id,
"exploitability_score": score,
"exploitability_rationale": rationale,
"attack_vector_analysis": attack_analysis
}
# Include exploit info if check_exploits was enabled
if check_exploits:
result["exploit_detection"] = exploit_info
return result
def run_test_cases():
"""
Run comprehensive test cases for attack vector analysis.
Tests CVSS vector parsing and attack vector analysis logic.
"""
test_cases = [
{
"name": "CVSS 3.1 - Network accessible, no auth, no UI (critical)",
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"expected": {
"is_network_accessible": True,
"requires_authentication": False,
"requires_user_interaction": False,
"complexity": "low"
}
},
{
"name": "CVSS 3.1 - Network accessible, requires auth",
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"expected": {
"is_network_accessible": True,
"requires_authentication": True,
"requires_user_interaction": False,
"complexity": "low"
}
},
{
"name": "CVSS 3.1 - Network accessible, requires UI",
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"expected": {
"is_network_accessible": True,
"requires_authentication": False,
"requires_user_interaction": True,
"complexity": "low"
}
},
{
"name": "CVSS 3.1 - Local access required",
"cvss_vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"expected": {
"is_network_accessible": False,
"requires_authentication": False,
"requires_user_interaction": False,
"complexity": "low"
}
},
{
"name": "CVSS 3.1 - Adjacent network, high auth",
"cvss_vector": "CVSS:3.1/AV:A/AC:H/PR:H/UI:R/S:U/C:L/I:L/A:L",
"expected": {
"is_network_accessible": True,
"requires_authentication": True,
"requires_user_interaction": True,
"complexity": "high"
}
},
{
"name": "CVSS 3.0 - Physical access required",
"cvss_vector": "CVSS:3.0/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"expected": {
"is_network_accessible": False,
"requires_authentication": False,
"requires_user_interaction": False,
"complexity": "low"
}
},
{
"name": "CVSS v2 - Network, no auth required",
"cvss_vector": "(AV:N/AC:L/Au:N/C:C/I:C/A:C)",
"expected": {
"is_network_accessible": True,
"requires_authentication": False,
"requires_user_interaction": True, # v2 doesn't have UI, defaults to True
"complexity": "low"
}
},
{
"name": "CVSS v2 - Network, single auth",
"cvss_vector": "AV:N/AC:M/Au:SINGLE/C:P/I:P/A:P",
"expected": {
"is_network_accessible": True,
"requires_authentication": True,
"requires_user_interaction": True, # v2 doesn't have UI, defaults to True
"complexity": "high"
}
},
{
"name": "CVSS v2 - Local access, multiple auth",
"cvss_vector": "(AV:L/AC:L/Au:MULTIPLE/C:C/I:C/A:C)",
"expected": {
"is_network_accessible": False,
"requires_authentication": True,
"requires_user_interaction": True, # v2 doesn't have UI, defaults to True
"complexity": "low"
}
},
{
"name": "Empty CVSS vector",
"cvss_vector": "",
"expected": {
"is_network_accessible": False,
"requires_authentication": True,
"requires_user_interaction": True,
"complexity": "unknown"
}
}
]
print("Running attack vector analysis test cases...")
print("=" * 70)
passed = 0
failed = 0
for i, test in enumerate(test_cases, 1):
print(f"\nTest {i}/{len(test_cases)}: {test['name']}")
print(f" CVSS Vector: {test['cvss_vector']}")
# Parse CVSS vector and analyze attack vector
cvss_metrics = parse_cvss_vector(test['cvss_vector'])
result = analyze_attack_vector(cvss_metrics)
# Compare with expected results
test_passed = True
for key, expected_value in test['expected'].items():
actual_value = result.get(key)
if actual_value != expected_value:
print(f" ❌ FAILED: {key}")
print(f" Expected: {expected_value}")
print(f" Got: {actual_value}")
test_passed = False
failed += 1
break
if test_passed:
print(" ✓ PASSED")
passed += 1
else:
# Show full result for debugging
print(f" Full result: {json.dumps(result, indent=6)}")
print("\n" + "=" * 70)
print(f"Test Results: {passed} passed, {failed} failed out of {len(test_cases)} total")
if failed > 0:
print("\n❌ Some tests failed!")
sys.exit(1)
else:
print("\n✅ All tests passed!")
sys.exit(0)
def main():
parser = argparse.ArgumentParser(
description="Analyze CVE exploitability for OpenClaw/NanoClaw deployments",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Analyze from JSON stdin
echo '{"cve_id":"CVE-2026-27488","cvss_score":7.3,"type":"ssrf"}' | python utils/analyze_exploitability.py --json
# Analyze with CVSS vector
echo '{"cve_id":"CVE-2026-1234","cvss_score":9.8,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"}' \
| python utils/analyze_exploitability.py --json
# Run test cases
python utils/analyze_exploitability.py --test-cases
# Parse CVSS vector only
python utils/analyze_exploitability.py --parse-vector "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
"""
)
parser.add_argument(
"--json",
action="store_true",
help="Read CVE data from stdin as JSON and output analysis"
)
parser.add_argument(
"--parse-vector",
type=str,
metavar="VECTOR",
help="Parse and display CVSS vector string"
)
parser.add_argument(
"--test-cases",
action="store_true",
help="Run built-in test cases to verify analyzer logic"
)
parser.add_argument(
"--check-exploits",
action="store_true",
help="Check references for publicly available exploits and adjust score accordingly"
)
args = parser.parse_args()
# Handle --parse-vector
if args.parse_vector:
metrics = parse_cvss_vector(args.parse_vector)
print(json.dumps(metrics, indent=2))
sys.exit(0)
# Handle --test-cases
if args.test_cases:
run_test_cases()
sys.exit(0)
# Handle --json (stdin)
if args.json:
try:
cve_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
sys.exit(1)
result = analyze_exploitability(cve_data, check_exploits=args.check_exploits)
print(json.dumps(result, indent=2))
sys.exit(0)
# No action specified - show help
parser.print_help()
sys.exit(0)
if __name__ == "__main__":
main()
+99
View File
@@ -0,0 +1,99 @@
import React from 'react';
import type { Components } from 'react-markdown';
export const defaultMarkdownComponents: 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>
),
};
+40
View File
@@ -0,0 +1,40 @@
const FRONTMATTER_REGEX = /^---\s*\n[\s\S]*?\n---\s*\n/;
/**
* Remove a leading YAML frontmatter block from markdown content.
* @param {string} content
* @returns {string}
*/
export const stripFrontmatter = (content) =>
String(content ?? '').replace(FRONTMATTER_REGEX, '');
/**
* Build a readable fallback title from a markdown file path.
* @param {string} filePath
* @returns {string}
*/
export const fallbackTitleFromPath = (filePath) => {
const normalized = String(filePath ?? '');
const filename = normalized.split('/').pop() ?? normalized;
const stem = filename.replace(/\.md$/i, '');
return stem
.split(/[-_]/)
.filter(Boolean)
.map((part) => {
if (part.toUpperCase() === part && part.length > 1) return part;
return part.charAt(0).toUpperCase() + part.slice(1);
})
.join(' ');
};
/**
* Extract the first H1 title from markdown; fall back to path-derived title.
* @param {string} content
* @param {string} filePath
* @returns {string}
*/
export const extractTitleFromMarkdown = (content, filePath) => {
const cleaned = stripFrontmatter(content).trim();
const match = cleaned.match(/^#\s+(.+)$/m);
return match?.[1]?.trim() || fallbackTitleFromPath(filePath);
};
+38
View File
@@ -0,0 +1,38 @@
/**
* Normalize a wiki slug for route/path construction.
* @param {string} slug
* @returns {string}
*/
const normalizeWikiSlug = (slug) =>
String(slug ?? '')
.replace(/\\/g, '/')
.replace(/^\/+|\/+$/g, '');
/**
* Return whether a slug represents the wiki index page.
* @param {string} slug
* @returns {boolean}
*/
export const isWikiIndexSlug = (slug) => normalizeWikiSlug(slug).toLowerCase() === 'index';
/**
* Convert a wiki slug to app route path.
* @param {string} slug
* @returns {string}
*/
export const toWikiRoute = (slug) => {
const normalized = normalizeWikiSlug(slug);
if (!normalized || isWikiIndexSlug(normalized)) return '/wiki';
return `/wiki/${normalized}`;
};
/**
* Convert a wiki slug to its llms.txt endpoint path.
* @param {string} slug
* @returns {string}
*/
export const toWikiLlmsPath = (slug) => {
const normalized = normalizeWikiSlug(slug);
if (!normalized || isWikiIndexSlug(normalized)) return '/wiki/llms.txt';
return `/wiki/${normalized}/llms.txt`;
};
+31
View File
@@ -0,0 +1,31 @@
# Wiki Generation Metadata
- Commit hash: `d5aadfbee15b48ebb4872dfb838e4df88c611d56`
- Branch name: `codex/wiki-tab-ui`
- Generation timestamp (local): `2026-02-26T09:16:02+0200`
- Generation mode: `update`
- Output language: `English`
- Assets copied into `wiki/assets/`:
- `overview_img_01_prompt-security-logo.png` (from `img/Black+Color.png`)
- `overview_img_02_clawsec-mascot.png` (from `public/img/mascot.png`)
- `architecture_img_01_prompt-line.svg` (from `public/img/prompt_line.svg`)
## Notes
- Migrated root documentation pages from `docs/` into dedicated `wiki/` operation pages.
- Updated index and cross-links to use `wiki/` as the documentation source of truth.
- Future updates should preserve existing headings and append `Update Notes` sections when making deltas.
## Source References
- README.md
- package.json
- AGENTS.md
- wiki/overview.md
- wiki/architecture.md
- wiki/dependencies.md
- wiki/data-flow.md
- wiki/glossary.md
- wiki/security-signing-runbook.md
- wiki/migration-signed-feed.md
- wiki/platform-verification.md
- wiki/remediation-plan.md
- wiki/compatibility-report.md
+53
View File
@@ -0,0 +1,53 @@
# Wiki Index
## Summary
- Purpose: Document ClawSec as a combined web catalog, signed advisory channel, and multi-skill security distribution system.
- Tech stack: React 19 + Vite + TypeScript frontend, Node/ESM scripts, Python utilities, Bash automation, GitHub Actions pipelines.
- Entry points: `index.tsx`, `App.tsx`, `scripts/prepare-to-push.sh`, `scripts/populate-local-feed.sh`, `scripts/populate-local-skills.sh`, workflow files under `.github/workflows/`.
- Where to start: Read [Overview](overview.md), then [Architecture](architecture.md), then module pages for the area you are editing.
- How to navigate: Use Guides for cross-cutting concerns, Operations for runbooks and migration plans, Modules for implementation boundaries, and Source References at the end of each page to jump into code.
## Start Here
- [Overview](overview.md)
- [Architecture](architecture.md)
## Guides
- [Dependencies](dependencies.md)
- [Data Flow](data-flow.md)
- [Configuration](configuration.md)
- [Testing](testing.md)
- [Workflow](workflow.md)
- [Security](security.md)
## Operations
- [Security Signing Runbook](security-signing-runbook.md)
- [Signed Feed Migration Plan](migration-signed-feed.md)
- [Platform Verification Checklist](platform-verification.md)
- [Cross-Platform Remediation Plan](remediation-plan.md)
- [Cross-Platform Compatibility Report](compatibility-report.md)
## Modules
- [Frontend Web App](modules/frontend-web.md)
- [ClawSec Suite Core](modules/clawsec-suite.md)
- [NanoClaw Integration](modules/nanoclaw-integration.md)
- [Automation and Release Pipelines](modules/automation-release.md)
- [Local Validation and Packaging Tools](modules/local-tooling.md)
## Glossary
- [Glossary](glossary.md)
## Generation Metadata
- [Generation Metadata](GENERATION.md)
## Update Notes
- 2026-02-26: Added Operations pages and updated navigation guidance after migrating root docs into wiki pages.
## Source References
- README.md
- App.tsx
- package.json
- scripts/prepare-to-push.sh
- scripts/populate-local-feed.sh
- scripts/populate-local-skills.sh
- skills/clawsec-suite/skill.json
- .github/workflows/ci.yml
+131
View File
@@ -0,0 +1,131 @@
# Architecture
## System Context
- This page appears under the `Start Here` section in `INDEX.md`.
- ClawSec sits between upstream intelligence sources (NVD + community issues), GitHub automation, and runtime agent environments.
- The repository publishes both static site content and signed artifacts that runtime skills verify before using.
- External actor groups:
- GitHub Actions runners executing CI, release, and feed workflows.
- OpenClaw/NanoClaw agents consuming skills, advisories, and verification scripts.
- Repository maintainers approving advisory issues and merging release/tag changes.
## Components
| Component | Location | Responsibility |
| --- | --- | --- |
| Web UI | `App.tsx`, `pages/`, `components/` | Renders skills catalog and advisory detail experiences. |
| Advisory Feed Core | `advisories/feed.json*`, `skills/clawsec-suite/.../feed.mjs` | Stores, verifies, and parses advisories with detached signatures/checksums. |
| Skill Packages | `skills/*/` | Distributes installable security capabilities with SBOM metadata. |
| Local Automation Scripts | `scripts/*.sh` | Build local mirrors, pre-push checks, and manual release helpers. |
| CI/CD Workflows | `.github/workflows/*.yml` | Linting, tests, NVD polling, release packaging, and Pages deploy. |
| Python Utility Layer | `utils/*.py` | Skill metadata validation and checksum generation. |
## Key Flows
- Skill catalog flow:
1. Release/tag workflows publish skill assets.
2. Deploy workflow discovers release assets and builds `public/skills/index.json`.
3. UI fetches `public/skills/index.json` and skill docs for `/skills` pages.
- Advisory feed flow:
1. `poll-nvd-cves.yml` and `community-advisory.yml` update `advisories/feed.json`.
2. Feed is signed and mirrored to public paths.
3. Runtime hooks/scripts load remote feed and fallback to local signed copies.
- Guarded install flow:
1. Installer requests target skill + version.
2. Advisory matcher checks affected specifiers and severity/risk hints.
3. Exit code 42 enforces second confirmation when advisories match.
## Diagrams
```mermaid
flowchart TD
A["NVD + Community Inputs"] --> B["Feed Workflows\n(poll/community)"]
B --> C["advisories/feed.json + signatures"]
C --> D["Deploy Workflow Mirrors to public/"]
D --> E["React UI (catalog/feed pages)"]
C --> F["clawsec-suite hook + installers"]
F --> G["Agent advisory alerts / gated install"]
```
![Prompt Line Motif](assets/architecture_img_01_prompt-line.svg)
## Interfaces and Contracts
| Interface | Contract Form | Validation |
| --- | --- | --- |
| Skill metadata | `skills/*/skill.json` | Validated by Python utility + CI version-parity checks. |
| Advisory feed | JSON + Ed25519 detached signature | Verified by `feed.mjs` and NanoClaw signature utilities. |
| Checksums manifest | `checksums.json` (+ optional `.sig`) | Parsed and hash-matched before trusting payloads. |
| Hook event interface | `HookEvent` (`type`, `action`, `messages`) | Runtime handler only processes selected event names. |
| Workflow release naming | Tag pattern `<skill>-vX.Y.Z` | Parsed in release/deploy workflows to discover skills. |
## Key Parameters
| Parameter | Default | Effect |
| --- | --- | --- |
| `CLAWSEC_FEED_URL` | `https://clawsec.prompt.security/advisories/feed.json` | Remote advisory source for suite scripts/hooks. |
| `CLAWSEC_ALLOW_UNSIGNED_FEED` | `0` | Enables temporary unsigned fallback compatibility. |
| `CLAWSEC_VERIFY_CHECKSUM_MANIFEST` | `1` | Requires checksum manifest verification where available. |
| `CLAWSEC_HOOK_INTERVAL_SECONDS` | `300` | Scan throttling window for advisory hook. |
| `CLAWSEC_SKILLS_INDEX_TIMEOUT_MS` | `5000` | Remote skill index fetch timeout for catalog discovery. |
| `PROMPTSEC_GIT_PULL` | `0` | Optional auto-pull before watchdog audit runs. |
## Error Handling and Reliability
- Feed fetching is fail-closed for invalid signatures and malformed manifests.
- Remote fetch failures gracefully fall back to local signed feeds.
- Hook state uses atomic file writes with strict mode where supported.
- UI pages detect HTML fallbacks served as JSON and avoid rendering corrupted data.
- Workflow steps enforce key-fingerprint consistency to avoid split-key drift.
## Example Snippets
```tsx
// Route topology in the web app
<Routes>
<Route path="/" element={<Home />} />
<Route path="/skills" element={<SkillsCatalog />} />
<Route path="/skills/:skillId" element={<SkillDetail />} />
<Route path="/feed" element={<FeedSetup />} />
<Route path="/feed/:advisoryId" element={<AdvisoryDetail />} />
<Route path="/wiki/*" element={<WikiBrowser />} />
</Routes>
```
```ts
// Guarded feed loading contract in advisory hook
const remoteFeed = await loadRemoteFeed(feedUrl, {
signatureUrl: feedSignatureUrl,
checksumsUrl: feedChecksumsUrl,
checksumsSignatureUrl: feedChecksumsSignatureUrl,
publicKeyPem,
checksumsPublicKeyPem: publicKeyPem,
allowUnsigned,
verifyChecksumManifest,
});
```
## Runtime and Deployment
| Runtime Surface | Execution Model | Output |
| --- | --- | --- |
| Vite app (`npm run dev`) | Local frontend server | Interactive web app for feed/skills. |
| GitHub CI | Multi-OS matrix + dedicated jobs | Lint/type/build/security and test confidence. |
| Skill release workflow | Tag-driven publish + PR dry-run checks | Release assets, signed checksums, optional ClawHub publish. |
| Pages deploy workflow | Triggered by CI/Release success | Static site + mirrored advisories/releases. |
| Runtime hooks | OpenClaw event hooks / NanoClaw IPC | Advisory alerts, gating decisions, integrity checks. |
## Scaling Notes
- Advisory volume scales with keyword set in NVD polling; dedupe and post-filtering control noise.
- Deploy workflow processes release lists and keeps newest skill versions in index output.
- Module boundaries by skill folder allow adding new security capabilities without changing frontend structure.
- Signature verification paths remain lightweight because payload sizes (feed/manifests) are small.
## Source References
- App.tsx
- pages/SkillsCatalog.tsx
- pages/FeedSetup.tsx
- pages/AdvisoryDetail.tsx
- pages/WikiBrowser.tsx
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/handler.ts
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/feed.mjs
- skills/clawsec-suite/scripts/guarded_skill_install.mjs
- skills/clawsec-suite/scripts/discover_skill_catalog.mjs
- skills/clawsec-nanoclaw/lib/advisories.ts
- skills/clawsec-nanoclaw/lib/signatures.ts
- .github/workflows/poll-nvd-cves.yml
- .github/workflows/community-advisory.yml
- .github/workflows/deploy-pages.yml
- .github/workflows/skill-release.yml
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1455.1 1298.3">
<!-- Generator: Adobe Illustrator 29.0.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 186) -->
<defs>
<style>
.st0 {
fill: none;
stroke: #fff;
stroke-width: 4px;
}
.st1 {
opacity: .1;
}
</style>
</defs>
<g class="st1">
<path class="st0" d="M730,505.6l209.7,362.8M730,505.6l-209.6,362.8M730,505.6l-64.5-111.7c-26.1-45.2-91.4-45.2-117.5,0l-405.4,701.7c-23.9,41.4-23.9,92.4,0,133.8M939.7,868.4h-419.3M939.7,868.4h144.1c44.8,0,72.9-48.5,50.5-87.3l-281.7-487.6M520.4,868.4h563.4c44.8,0,72.9-48.5,50.5-87.3l-281.7-487.6M520.4,868.4l-73.2,126.6c-22.4,38.8,5.6,87.3,50.5,87.3h818.1M852.6,293.5l-102.3-177.1M852.6,293.5l-102.3-177.1h0M750.2,116.4l-27.3-47.2C698.8,27.6,654.4,1.9,606.3,1.9M606.4,2h245.3c48.5,0,93.2,25.8,117.5,67.8l465.9,806.4c24.2,41.9,24.2,93.6,0,135.5l-1.7,2.9M606.4,2c-48.1,0-92.6,25.6-116.6,67.3L20.2,882c-24.2,41.9-24.2,93.6,0,135.5l122.4,211.9M1315.8,1082.3c43.9,0,85-21.4,110.1-57.3l7.3-10.5M1315.8,1082.3c48.5,0,93.2-25.9,117.5-67.8M1433.3,1014.6h0l-39.2,67.8h0l-84.5,146.2c-24.2,41.9-69,67.8-117.4,67.8H258.5c-47.8,0-92-25.5-115.9-66.9"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 KiB

@@ -34,7 +34,7 @@ This could produce paths like `~/.openclaw/workspace/$HOME/...`.
| CP-006 | High | Windows | Multiple SKILL docs and shell scripts | Install/maintenance flow is still heavily POSIX-shell based. | Add PowerShell equivalents or Node wrappers for critical flows. | Open |
| CP-007 | Medium | Linux/macOS/Windows | `skills/soul-guardian/scripts/soul_guardian.py` | `Path(...).expanduser()` handles `~` but not `$HOME`/`%USERPROFILE%`. | Add explicit env-token expansion + validation for `--state-dir`. | Open |
| CP-008 | Medium | Windows | `scripts/release-skill.sh`, `scripts/populate-local-*.sh` | GNU/BSD shell toolchain assumptions block native Windows usage. | Provide cross-platform Node/Python replacements or PowerShell equivalents. | Open |
| CP-009 | Low | Windows | docs + scripts using `chmod 600/644` | POSIX permission semantics are partial/non-portable on Windows. | Document best-effort behavior and Windows ACL alternatives. | Open |
| CP-009 | Low | Windows | documentation + scripts using `chmod 600/644` | POSIX permission semantics are partial/non-portable on Windows. | Document best-effort behavior and Windows ACL alternatives. | Open |
| CP-010 | Low | macOS/Windows | CI non-Node jobs | Shell/Python/security scan jobs remain Ubuntu-only. | Add scoped matrix or dedicated non-Linux smoke jobs where practical. | Open |
---
@@ -54,7 +54,7 @@ This could produce paths like `~/.openclaw/workspace/$HOME/...`.
## Permissions / Filesystem Semantics
- Confirmed many scripts rely on POSIX permission commands.
- Existing `state.ts` already handles `chmod` failures on unsupported filesystems.
- Open: docs still mostly assume POSIX permissions.
- Open: documentation still mostly assumes POSIX permissions.
## Line Endings
- Fixed by adding `.gitattributes` with LF rules for scripts and key text/config files.
@@ -62,7 +62,7 @@ This could produce paths like `~/.openclaw/workspace/$HOME/...`.
## Runtime Dependencies
- Node scripts generally portable.
- Python utilities are portable.
- OpenSSL usage in docs/workflows remains shell/toolchain dependent.
- OpenSSL usage in documentation/workflows remains shell/toolchain dependent.
## CI / Automation
- Fixed: TS/lint/build matrix now runs on Linux/macOS/Windows.
@@ -95,3 +95,17 @@ This could produce paths like `~/.openclaw/workspace/$HOME/...`.
- `sh` (where scripts are invoked through Node entrypoints): same path behavior in Node layer.
- Windows PowerShell: `%USERPROFILE%` / `$env:USERPROFILE` expansion and path normalization validated in Node tests.
## Source References
- .gitattributes
- .github/workflows/ci.yml
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/handler.ts
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/suppression.mjs
- skills/clawsec-suite/scripts/guarded_skill_install.mjs
- skills/openclaw-audit-watchdog/scripts/setup_cron.mjs
- skills/openclaw-audit-watchdog/scripts/load_suppression_config.mjs
- skills/soul-guardian/scripts/soul_guardian.py
- scripts/release-skill.sh
- scripts/populate-local-feed.sh
- scripts/populate-local-skills.sh
- wiki/remediation-plan.md
- wiki/platform-verification.md
+88
View File
@@ -0,0 +1,88 @@
# Configuration
## Scope
- Configuration spans frontend build settings, runtime feed paths, workflow triggers, and skill metadata contracts.
- Most runtime-sensitive controls are environment variables prefixed with `CLAWSEC_` or `OPENCLAW_`.
- Path normalization is security-sensitive and intentionally rejects unresolved home-token literals.
## Core Runtime Variables
| Variable | Default | Used By |
| --- | --- | --- |
| `CLAWSEC_FEED_URL` | Hosted advisory URL | Suite hook and guarded installer feed loading. |
| `CLAWSEC_FEED_SIG_URL` | `<feed>.sig` | Detached signature source. |
| `CLAWSEC_FEED_CHECKSUMS_URL` | `checksums.json` near feed URL | Optional checksum-manifest source. |
| `CLAWSEC_FEED_PUBLIC_KEY` | Suite-local PEM file | Feed signature verification. |
| `CLAWSEC_ALLOW_UNSIGNED_FEED` | `0` | Temporary migration bypass flag. |
| `CLAWSEC_VERIFY_CHECKSUM_MANIFEST` | `1` | Enables checksum-manifest verification. |
| `CLAWSEC_HOOK_INTERVAL_SECONDS` | `300` | Advisory hook scan throttle. |
## Path Resolution Rules
| Rule | Behavior | Enforcement Location |
| --- | --- | --- |
| `~` expansion | Resolved to detected home directory | Shared path utility functions in suite/watchdog scripts. |
| `$HOME` / `${HOME}` expansion | Resolved when unescaped | Same utilities. |
| Windows home tokens | `%USERPROFILE%`, `$env:USERPROFILE` normalized | Same utilities. |
| Escaped tokens (`\$HOME`) | Rejected with explicit error | Prevents accidental literal directory creation. |
| Invalid explicit path | Can fallback to default path with warning | `resolveConfiguredPath` helpers. |
## Frontend and Build Configuration
- `vite.config.ts` defines port (`3000`), host (`0.0.0.0`), and path alias (`@`).
- `index.html` provides Tailwind runtime config, custom fonts, and base color tokens.
- `tsconfig.json` uses bundler module resolution, `noEmit`, and JSX runtime configuration.
- `eslint.config.js` applies TS, React, hooks, and script-specific lint rules.
## Skill Metadata Configuration
| Field Group | Location | Function |
| --- | --- | --- |
| Core skill identity | `skills/*/skill.json` | Name/version/author/license/description metadata. |
| SBOM file list | `skill.json -> sbom.files` | Declares release-required artifacts. |
| Platform metadata | `openclaw` or `nanoclaw` blocks | CLI requirements, triggers, platform capability hints. |
| Suite catalog metadata | `skills/clawsec-suite/skill.json -> catalog` | Integrated/default/consent behavior for suite members. |
## Workflow Configuration
- Schedule configuration exists in workflow `cron` entries (`poll-nvd-cves`, `codeql`, `scorecard`).
- Release workflow expects tag naming pattern `<skill>-v<semver>`.
- Deployment workflow is triggered by successful CI/release `workflow_run` events and manual dispatch.
- Composite signing action requires private key inputs and verifies signatures immediately after signing.
## Example Snippets
```bash
# run guarded install with explicit local signed feed paths
CLAWSEC_LOCAL_FEED="$HOME/.openclaw/skills/clawsec-suite/advisories/feed.json" \
CLAWSEC_LOCAL_FEED_SIG="$HOME/.openclaw/skills/clawsec-suite/advisories/feed.json.sig" \
CLAWSEC_FEED_PUBLIC_KEY="$HOME/.openclaw/skills/clawsec-suite/advisories/feed-signing-public.pem" \
node skills/clawsec-suite/scripts/guarded_skill_install.mjs --skill clawtributor --dry-run
```
```json
{
"name": "example-skill",
"version": "1.2.3",
"sbom": {
"files": [
{ "path": "SKILL.md", "required": true, "description": "Install docs" }
]
}
}
```
## Operational Notes
- Keep signing keys outside the repository and inject via GitHub Secrets only.
- Prefer absolute paths or unescaped home expressions in local environment variable overrides.
- Treat unsigned feed mode as temporary migration support, not normal operation.
- Re-run release-link validation when editing `SKILL.md` URLs to avoid broken artifact references.
## Source References
- vite.config.ts
- index.html
- tsconfig.json
- eslint.config.js
- skills/clawsec-suite/skill.json
- skills/clawsec-nanoclaw/skill.json
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/utils.mjs
- skills/openclaw-audit-watchdog/scripts/load_suppression_config.mjs
- skills/clawsec-suite/scripts/guarded_skill_install.mjs
- scripts/validate-release-links.sh
- .github/workflows/poll-nvd-cves.yml
- .github/workflows/skill-release.yml
- .github/actions/sign-and-verify/action.yml
+98
View File
@@ -0,0 +1,98 @@
# Data Flow
## Primary Flows
- `Advisory ingestion`: NVD/community inputs are transformed into a normalized advisory feed, signed, then mirrored for clients.
- `Skill catalog publication`: release assets are discovered and converted into `public/skills/index.json` plus per-skill docs/checksums.
- `Runtime enforcement`: suite and nanoclaw consumers load advisory data, match against skills, and emit alerts or confirmation gates.
- This page appears under the `Guides` section in `INDEX.md`.
## Step-by-Step
1. Feed producer workflow/script fetches source data (`NVD API` or issue payload).
2. JSON transform logic normalizes severity/type/affected fields and deduplicates by advisory ID.
3. Signature/checksum steps generate detached signatures and checksum manifests.
4. Deploy workflow mirrors signed artifacts under `public/` and `public/releases/latest/download/`.
5. UI consumers validate JSON shape/content; runtime consumers additionally verify signatures/checksums before trusting feed data.
6. Matchers compare `affected` specifiers to skill names/versions and emit alerts or enforce confirmation.
## Inputs and Outputs
Inputs/outputs are summarized in the table below.
| Type | Name | Location | Description |
| --- | --- | --- | --- |
| Input | CVE payloads | `services.nvd.nist.gov/rest/json/cves/2.0` | Source vulnerabilities filtered by ClawSec keywords. |
| Input | Community advisory issue | `.github/workflows/community-advisory.yml` event payload | Maintainer-approved issue transformed into advisory record. |
| Input | Skill release assets | GitHub Releases API + assets | Used to build web catalog and mirror downloads. |
| Input | Local config/env | `OPENCLAW_AUDIT_CONFIG`, `CLAWSEC_*` vars | Controls feed pathing, suppression, and verification behavior. |
| Output | Advisory feed | `advisories/feed.json` | Canonical repository feed. |
| Output | Advisory signature | `advisories/feed.json.sig` | Detached signature for feed authenticity. |
| Output | Skill catalog index | `public/skills/index.json` | Runtime web catalog used by `/skills` pages. |
| Output | Release checksums/signatures | `release-assets/checksums.json(.sig)` | Integrity manifest for release consumers. |
| Output | Hook state | `~/.openclaw/clawsec-suite-feed-state.json` | Tracks scan timing and notified matches. |
## Data Structures
| Structure | Key Fields | Purpose |
| --- | --- | --- |
| Advisory feed record | `id`, `severity`, `type`, `affected[]`, `published` | Unit of risk data used by UI and installers. |
| Skill metadata record | `id`, `name`, `version`, `emoji`, `tag` | Catalog row for web browsing and install commands. |
| Checksums manifest | `schema_version`, `algorithm`, `files` | Maps file names to expected digests. |
| Advisory state | `known_advisories`, `last_hook_scan`, `notified_matches` | Prevents repeated alerts and throttles scans. |
| Suppression config | `enabledFor[]`, `suppressions[]` | Targeted skip list by `checkId` + `skill`. |
## Diagrams
```mermaid
flowchart LR
A["NVD + Issue Inputs"] --> B["Transform + Deduplicate"]
B --> C["advisories/feed.json"]
C --> D["Sign + checksums"]
D --> E["public/advisories + releases/latest"]
E --> F["Web UI fetch"]
E --> G["Suite/NanoClaw verification"]
G --> H["Match skills + emit alerts/gates"]
```
## State and Storage
| Store | Path/Scope | Write Path |
| --- | --- | --- |
| Canonical advisories | `advisories/` | NVD + community workflows and local populate script. |
| Embedded advisory copies | `skills/clawsec-feed/advisories/` and `skills/clawsec-suite/advisories/` | Sync/packaging processes and release workflow. |
| Public mirrors | `public/advisories/`, `public/releases/` | Deploy workflow. |
| Runtime state | `~/.openclaw/clawsec-suite-feed-state.json` | Advisory hook state persistence. |
| NanoClaw cache | `/workspace/project/data/clawsec-advisory-cache.json` | Host-side advisory cache manager. |
| Integrity state | `/workspace/project/data/soul-guardian/` (NanoClaw) | Integrity monitor baseline/audit storage. |
## Example Snippets
```bash
# Local feed flow (NVD fetch -> transform -> sync)
./scripts/populate-local-feed.sh --days 120
jq '.updated, (.advisories | length)' advisories/feed.json
```
```bash
# Runtime guarded install uses signed feed paths
CLAWSEC_LOCAL_FEED=~/.openclaw/skills/clawsec-suite/advisories/feed.json \
CLAWSEC_FEED_PUBLIC_KEY=~/.openclaw/skills/clawsec-suite/advisories/feed-signing-public.pem \
node skills/clawsec-suite/scripts/guarded_skill_install.mjs --skill test-skill --dry-run
```
## Failure Modes
- NVD rate limits (`403/429`) can delay feed refresh and require retries/backoff.
- Missing or invalid detached signatures cause feed rejection in fail-closed mode.
- HTML fallback responses for JSON endpoints can produce false positives unless explicitly filtered.
- Path-token misconfiguration (`\$HOME`) can break local fallback path resolution.
- Mismatched public key fingerprints in workflows trigger hard CI failure.
## Source References
- advisories/feed.json
- advisories/feed.json.sig
- scripts/populate-local-feed.sh
- scripts/populate-local-skills.sh
- .github/workflows/poll-nvd-cves.yml
- .github/workflows/community-advisory.yml
- .github/workflows/deploy-pages.yml
- .github/workflows/skill-release.yml
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/feed.mjs
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/state.ts
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/matching.ts
- skills/clawsec-suite/scripts/guarded_skill_install.mjs
- skills/clawsec-nanoclaw/lib/advisories.ts
- skills/clawsec-nanoclaw/host-services/advisory-cache.ts
+101
View File
@@ -0,0 +1,101 @@
# Dependencies
## Build and Runtime
| Layer | Primary Dependencies | Why It Exists |
| --- | --- | --- |
| Frontend runtime | `react`, `react-dom`, `react-router-dom`, `lucide-react` | UI rendering, routing, iconography. |
| Markdown rendering | `react-markdown`, `remark-gfm` | Render skill docs/readmes and in-app wiki markdown pages. |
| Build tooling | `vite`, `@vitejs/plugin-react`, `typescript` | Fast TS/TSX bundling and production builds. |
| Python utilities | stdlib + `ruff`/`bandit` policy from `pyproject.toml` | Validate/package skills and run static checks. |
| Shell automation | `bash`, `jq`, `curl`, `openssl`, `sha256sum`/`shasum` | Feed polling, signing, checksum generation, release checks. |
## Dependency Details
| Package | Version Constraint | Scope |
| --- | --- | --- |
| `react` / `react-dom` | `^19.2.4` | Frontend runtime |
| `react-router-dom` | `^7.13.1` | Frontend routing |
| `lucide-react` | `^0.575.0` | UI icon set |
| `vite` | `^7.3.1` | Dev server + build |
| `typescript` | `~5.8.2` | Type checking |
| `eslint` | `^9.39.2` | JS/TS linting |
| `@typescript-eslint/*` | `^8.55.0` / `^8.56.0` | TS lint parser/rules |
| `fast-check` | `^4.5.3` | Property/fuzz style tests |
| Override | Pinned Version | Rationale |
| --- | --- | --- |
| `ajv` | `6.14.0` | Security and compatibility stabilization. |
| `balanced-match` | `4.0.3` | Transitive vulnerability control. |
| `brace-expansion` | `5.0.2` | Transitive dependency hardening. |
| `minimatch` | `10.2.1` | Deterministic dependency behavior. |
## External Services
| Service | Used By | Function |
| --- | --- | --- |
| NVD API (`services.nvd.nist.gov`) | `poll-nvd-cves` workflow + local feed script | Pull CVEs by keyword/date window. |
| GitHub API | Deploy/release workflows | Discover releases, download assets, publish outputs. |
| GitHub Pages | Deploy workflow | Serve static site and mirrored artifacts. |
| ClawHub CLI/registry | Install scripts + optional publish jobs | Install and publish skills. |
| Optional local SMTP/sendmail | `openclaw-audit-watchdog` scripts | Deliver audit reports by email. |
## Development Tools
| Tool | Invocation | Coverage |
| --- | --- | --- |
| ESLint | `npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --max-warnings 0` | Frontend and script linting. |
| TypeScript | `npx tsc --noEmit` | Compile-time TS contract checks. |
| Ruff | `ruff check utils/` | Python style and bug pattern checks. |
| Bandit | `bandit -r utils/ -ll` | Python security checks. |
| Trivy | Workflow + optional local run | FS/config vulnerability scans. |
| Gitleaks | `scripts/prepare-to-push.sh` optional local run | Secret leak detection before push. |
## Example Snippets
```json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.4",
"react-router-dom": "^7.13.1"
}
}
```
```toml
[tool.ruff]
target-version = "py310"
line-length = 120
[tool.bandit]
exclude_dirs = ["__pycache__", ".venv"]
skips = ["B101"]
```
## Compatibility Notes
- Local scripts account for macOS vs Linux differences in `date` and `stat` usage.
- Some workflows/scripts require OpenSSL features used with Ed25519 and `pkeyutl -rawin`.
- Windows support is strongest for Node-based tooling; POSIX shell paths may require WSL/Git Bash.
- Feed consumers include compatibility bypasses for migration phases, but signed mode is the intended steady state.
## Versioning Notes
- Skill release tags follow `<skill>-v<semver>` and are parsed by CI/deploy automation.
- PR validation enforces version parity between `skill.json` and `SKILL.md` frontmatter for bumped skills.
- The public skills index keeps latest discovered version per skill for UI display.
- Signed artifact manifests (`checksums.json`) are versioned per release and include file hashes and URLs.
## Source References
- package.json
- package-lock.json
- pyproject.toml
- eslint.config.js
- tsconfig.json
- scripts/prepare-to-push.sh
- scripts/populate-local-feed.sh
- scripts/populate-local-skills.sh
- .github/workflows/ci.yml
- .github/workflows/codeql.yml
- .github/workflows/scorecard.yml
- .github/workflows/poll-nvd-cves.yml
- .github/workflows/deploy-pages.yml
- .github/workflows/skill-release.yml
+420
View File
@@ -0,0 +1,420 @@
# Exploitability Scoring Methodology
## Overview
ClawSec's exploitability scoring system provides context-aware vulnerability assessment specifically designed for AI agent deployments (OpenClaw/NanoClaw). Unlike generic CVSS scores that treat all environments equally, our scoring considers the unique attack surface and usage patterns of AI agents to reduce alert fatigue and prioritize actionable threats.
## Scoring Levels
| Level | Severity | Meaning |
|---|---|---|
| `high` | Critical/High | Exploitable in typical agent deployments, immediate attention required |
| `medium` | Medium | May be exploitable depending on configuration, warrants investigation |
| `low` | Low | Limited exploitability in agent context, low priority |
| `unknown` | Unknown | Insufficient data to assess exploitability |
## Scoring Factors
### 1. CVSS Base Score (Baseline)
The analysis starts with the CVSS base score as a foundation:
- **CVSS ≥ 9.0**: Critical severity → initial score `high`
- **CVSS 7.0-8.9**: High severity → initial score `high`
- **CVSS 4.0-6.9**: Medium severity → initial score `medium`
- **CVSS 1.0-3.9**: Low severity → initial score `low`
- **No CVSS**: → initial score `unknown`
### 2. Attack Vector Analysis (CVSS Metrics)
The analyzer parses CVSS v2, v3.0, and v3.1 vectors to assess:
#### Network Accessibility
- **AV:N** (Network): Remotely exploitable over network
- **AV:A** (Adjacent): Requires local network access
- **AV:L** (Local): Requires local system access
- **AV:P** (Physical): Requires physical access
**Impact on agents**: Network-accessible vulnerabilities are elevated because agents typically run as network services or make external API calls.
#### Authentication Requirements
- **PR:N / Au:NONE**: No authentication required → elevates score
- **PR:L / Au:SINGLE**: Low privileges required
- **PR:H / Au:MULTIPLE**: High privileges required → reduces score
**Impact on agents**: Unauthenticated exploits are critical for publicly exposed agent APIs.
#### User Interaction
- **UI:N**: No user interaction required → elevates score
- **UI:R**: Requires user interaction → reduces score
**Impact on agents**: Agents often operate autonomously, so vulnerabilities requiring user interaction are less critical.
#### Attack Complexity
- **AC:L**: Low complexity → elevates score
- **AC:M / AC:H**: Medium/High complexity → neutral or reduces score
**Impact on agents**: Low-complexity exploits are more likely to be automated and used in mass attacks.
### 3. Vulnerability Type (Deployment Context)
ClawSec adjusts scores based on how vulnerability types affect AI agent deployments:
#### High-Risk Types in Agent Context
**Remote Code Execution (RCE)**
```
Score: Always HIGH
Rationale: RCE is critical in agent deployments
```
AI agents execute arbitrary code as part of their function. RCE vulnerabilities allow attackers to hijack agent execution flow, exfiltrate credentials, or pivot to other systems.
**Server-Side Request Forgery (SSRF)**
```
Score: Elevated to HIGH if CVSS ≥ 6.0
Rationale: SSRF affects agents making external requests
```
Agents frequently call external APIs, access internal services, and fetch remote resources. SSRF allows attackers to:
- Access internal cloud metadata services (AWS IMDSv1, GCP metadata)
- Pivot to internal networks
- Exfiltrate data through DNS tunneling
**Path Traversal / Directory Traversal**
```
Score: Elevated to HIGH if CVSS ≥ 6.0
Rationale: Path traversal affects agents with file access
```
Agents read files, execute scripts, and manage codebases. Path traversal enables:
- Reading sensitive configuration files (.env, credentials)
- Accessing SSH keys, API tokens
- Overwriting critical system files
**Command Injection**
```
Score: Always HIGH
Rationale: Command injection is critical in agent deployments
```
Similar to RCE, agents often execute shell commands to interact with systems. Command injection allows full system compromise.
#### Medium-Risk Types
**Prototype Pollution (Node.js)**
```
Score: Elevated from LOW to MEDIUM
Rationale: Prototype pollution can escalate in Node.js agents
```
Many agent frameworks run on Node.js. Prototype pollution can lead to:
- Bypass of authentication checks
- Privilege escalation
- Denial of service
**SQL Injection / NoSQL Injection**
```
Score: Elevated to HIGH if network-accessible and unauthenticated
Rationale: Injection affects agents with database access
```
Agents that store conversation history, user data, or tool results in databases are vulnerable to injection attacks.
#### Lower-Risk Types
**Cross-Site Scripting (XSS)**
```
Score: Reduced to MEDIUM if not network-accessible
Rationale: XSS has limited impact in headless agents
```
Agents typically don't render HTML in browsers, reducing XSS impact. However, XSS in agent management UIs or chat interfaces remains a concern.
### 4. Exploit Availability
When `--check-exploits` is enabled, the analyzer checks reference URLs for public exploits:
**Exploit Indicators:**
- exploit-db.com / exploit-database.com
- packetstormsecurity.com
- github.com/exploit, github.com/poc
- metasploit framework modules
- URLs containing "/exploit", "/poc", "/proof-of-concept"
**Score Elevation:**
- `low``medium` (exploit available)
- `medium``high` (exploit available)
- `unknown``medium` (exploit available + CVSS > 0)
**Rationale**: Public exploits lower the skill barrier for attackers and increase the likelihood of automated exploitation.
## Scoring Algorithm
The analyzer follows this decision tree:
```
1. Parse CVSS score → set baseline (high/medium/low/unknown)
2. Parse CVSS vector → analyze attack characteristics
3. Adjust for attack vector:
- Network-accessible + no auth + no UI → elevate to HIGH
- Local-only access → reduce HIGH to MEDIUM
4. Adjust for vulnerability type:
- Check against agent-specific risk categories
- Elevate or reduce score based on deployment context
5. Check for public exploits (if enabled):
- Elevate score if exploits detected
6. Generate rationale explaining the final score
```
## Examples
### Example 1: Critical RCE (High Exploitability)
```json
{
"cve_id": "CVE-2024-12345",
"cvss_score": 9.8,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"type": "remote_code_execution",
"description": "Unauthenticated RCE in Express.js framework"
}
```
**Analysis Output:**
```json
{
"exploitability_score": "high",
"exploitability_rationale": "Critical CVSS score (9.8); remotely exploitable without authentication; RCE is critical in agent deployments"
}
```
**Why HIGH**: Critical CVSS + network accessible + no auth + RCE type.
### Example 2: SSRF in Agent API (High Exploitability)
```json
{
"cve_id": "CVE-2024-23456",
"cvss_score": 7.3,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L",
"type": "server_side_request_forgery",
"description": "SSRF in webhook handler allows internal network access"
}
```
**Analysis Output:**
```json
{
"exploitability_score": "high",
"exploitability_rationale": "High CVSS score (7.3); remotely exploitable without authentication; SSRF affects agents making external requests"
}
```
**Why HIGH**: SSRF is critical for agents that make API calls (most do). Network-accessible without authentication elevates risk.
### Example 3: Path Traversal with Public Exploit (High Exploitability)
```json
{
"cve_id": "CVE-2024-34567",
"cvss_score": 6.5,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "path_traversal",
"references": [
"https://exploit-db.com/exploits/51234",
"https://nvd.nist.gov/vuln/detail/CVE-2024-34567"
]
}
```
**Analysis Output (with --check-exploits):**
```json
{
"exploitability_score": "high",
"exploitability_rationale": "Medium CVSS score (6.5); network accessible; path traversal affects agents with file access; public exploit available (1 source)"
}
```
**Why HIGH**: Path traversal + agent file access + public exploit elevates medium CVSS to high exploitability.
### Example 4: XSS in Agent UI (Medium Exploitability)
```json
{
"cve_id": "CVE-2024-45678",
"cvss_score": 7.1,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L",
"type": "cross_site_scripting",
"description": "Stored XSS in agent management dashboard"
}
```
**Analysis Output:**
```json
{
"exploitability_score": "medium",
"exploitability_rationale": "High CVSS score (7.1); network accessible; XSS has limited impact in headless agents"
}
```
**Why MEDIUM**: Despite high CVSS, XSS is less critical in agent deployments (headless operation). Requires user interaction.
### Example 5: Local Privilege Escalation (Medium Exploitability)
```json
{
"cve_id": "CVE-2024-56789",
"cvss_score": 8.8,
"cvss_vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
"type": "privilege_escalation",
"description": "Local privilege escalation via symbolic link attack"
}
```
**Analysis Output:**
```json
{
"exploitability_score": "medium",
"exploitability_rationale": "High CVSS score (8.8); requires local access"
}
```
**Why MEDIUM**: Despite high CVSS, requires local access. Agents typically run in containerized/sandboxed environments where local escalation has limited impact.
### Example 6: Prototype Pollution with Exploit (High Exploitability)
```json
{
"cve_id": "CVE-2024-67890",
"cvss_score": 5.3,
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
"type": "prototype_pollution",
"description": "Prototype pollution in lodash merge function",
"references": [
"https://github.com/exploit/prototype-pollution-poc",
"https://snyk.io/vuln/SNYK-JS-LODASH-1234567"
]
}
```
**Analysis Output (with --check-exploits):**
```json
{
"exploitability_score": "high",
"exploitability_rationale": "Medium CVSS score (5.3); remotely exploitable without authentication; prototype pollution can escalate in Node.js agents; public exploit available (1 source)"
}
```
**Why HIGH**: Prototype pollution in Node.js agents + public exploit + network-accessible without auth = high risk despite moderate CVSS.
## Usage in ClawSec Workflows
### Automated Scoring (NVD Feed)
The `poll-nvd-cves.yml` workflow automatically scores new CVEs:
```bash
# Workflow step
python utils/analyze_exploitability.py --json --check-exploits < cve-data.json
```
Advisories in `advisories/feed.json` can include:
```json
{
"id": "CVE-2024-12345",
"severity": "high",
"exploitability_score": "high",
"exploitability_rationale": "Critical CVSS score (9.8); remotely exploitable without authentication; RCE is critical in agent deployments",
"attack_vector_analysis": {
"is_network_accessible": true,
"requires_authentication": false,
"requires_user_interaction": false,
"complexity": "low"
}
}
```
### Manual Analysis
Security researchers can analyze CVEs manually:
```bash
# Basic analysis
echo '{"cve_id":"CVE-2024-12345","cvss_score":7.3,"type":"ssrf"}' | \
python utils/analyze_exploitability.py --json
# With exploit detection
echo '{"cve_id":"CVE-2024-12345","cvss_score":7.3,"references":["https://exploit-db.com/exploits/51234"]}' | \
python utils/analyze_exploitability.py --json --check-exploits
```
### Filtering by Exploitability
Users can filter advisories by exploitability score:
```bash
# Get only high-exploitability advisories
curl -s https://clawsec.prompt.security/feed.json | \
jq '.advisories[] | select(.exploitability_score == "high")'
# Prioritize by exploitability and severity
curl -s https://clawsec.prompt.security/feed.json | \
jq '[.advisories[] | select(.exploitability_score == "high" and .severity == "critical")] | sort_by(.cvss_score) | reverse'
```
## Backfilling Existing Advisories (Historical Maintenance)
`scripts/backfill-exploitability.sh` is retained as a historical maintainer utility for one-off repository maintenance.
It is not the primary path for normal advisory generation.
Preferred paths:
1. CI canonical path: run the NVD workflow with init/reset to rebuild advisories from NVD and sign artifacts in pipeline.
2. Local developer path: run `./scripts/populate-local-feed.sh --force` to repopulate local feeds with exploitability context.
Use backfill only when explicitly repairing legacy feed content that already exists in-repo.
## Community Contributions
Community members can submit exploitability assessments:
1. **Report via GitHub Issue**: Use the advisory template to report CVEs with exploitability context
2. **Automated Analysis**: The `community-advisory.yml` workflow automatically scores community-reported CVEs
3. **Manual Review**: Maintainers review and approve exploitability assessments
4. **Feed Update**: Approved advisories are added to the feed with exploitability scores
## Limitations and Future Work
### Current Limitations
1. **Static Analysis**: Scoring is based on CVE metadata, not dynamic runtime analysis
2. **No Version Detection**: Doesn't check if specific versions are vulnerable
3. **Binary Classification**: Doesn't consider partial mitigations or defense-in-depth
4. **Limited Context**: Doesn't know exact agent configuration or deployed tools
### Future Enhancements
1. **EPSS Integration**: Incorporate EPSS (Exploit Prediction Scoring System) probability scores
2. **KEV Matching**: Cross-reference with CISA KEV (Known Exploited Vulnerabilities) catalog
3. **Agent Profiling**: Consider deployed agent capabilities and exposed APIs
4. **Mitigation Detection**: Check for WAF rules, sandboxing, or other compensating controls
5. **ML-Based Scoring**: Use machine learning to predict exploitability based on historical data
## References
- **CVSS v3.1 Specification**: [https://www.first.org/cvss/v3.1/specification-document](https://www.first.org/cvss/v3.1/specification-document)
- **CVSS v2 Guide**: [https://www.first.org/cvss/v2/guide](https://www.first.org/cvss/v2/guide)
- **EPSS**: [https://www.first.org/epss/](https://www.first.org/epss/)
- **CISA KEV**: [https://www.cisa.gov/known-exploited-vulnerabilities-catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
- **NVD API**: [https://nvd.nist.gov/developers/vulnerabilities](https://nvd.nist.gov/developers/vulnerabilities)
## Contributing
To improve the exploitability scoring methodology:
1. **Submit Test Cases**: Add test cases to `utils/analyze_exploitability.py`
2. **Report False Positives/Negatives**: Open GitHub issues with CVE examples
3. **Propose Scoring Adjustments**: Submit PRs with rationale and examples
4. **Share Agent Context**: Contribute agent-specific vulnerability patterns
See [CONTRIBUTING.md](../CONTRIBUTING.md) for detailed contribution guidelines.
---
**Maintained by**: [Prompt Security](https://prompt.security)
**License**: AGPL-3.0-or-later
**Last Updated**: 2026-03-01
+57
View File
@@ -0,0 +1,57 @@
# Glossary
## Terms
| Term | Definition |
| --- | --- |
| Advisory Feed | JSON document (`feed.json`) containing security advisories for skills/platforms. |
| Affected Specifier | Skill selector such as `skill@1.2.3`, wildcard, or range used in matching logic. |
| Guarded Install | Two-step installer behavior that requires explicit confirmation when advisories match. |
| SBOM Files | Skill-declared artifact list in `skill.json` used for packaging and validation. |
| Detached Signature | Base64 signature file (`.sig`) stored separately from signed payload. |
| Checksum Manifest | File hash map (`checksums.json`) used to verify payload integrity. |
## Skill Packaging Terms
| Term | Definition |
| --- | --- |
| Skill Tag | Git tag formatted as `<skill>-v<semver>` used by release automation. |
| Release Assets | Files attached to GitHub release (zip, `skill.json`, checksums, signatures). |
| Catalog Index | `public/skills/index.json`, generated list consumed by web catalog. |
| Embedded Components | Capability bundle from one skill included in another (for example feed embedded in suite). |
## Advisory and Security Terms
| Term | Definition |
| --- | --- |
| Fail-Closed Verification | Reject payload if signature or checksum validation fails. |
| Unsigned Compatibility Mode | Temporary bypass path enabled via `CLAWSEC_ALLOW_UNSIGNED_FEED=1`. |
| Suppression Rule | Config entry matching `checkId` and `skill` to suppress known/accepted findings. |
| Key Fingerprint | SHA-256 digest of DER-encoded public key used for key consistency checks. |
## Runtime and Platform Terms
| Term | Definition |
| --- | --- |
| OpenClaw Hook | Runtime event handler (`clawsec-advisory-guardian`) that checks advisories. |
| NanoClaw IPC | Host/container task exchange for advisory refresh, signature verification, integrity checks. |
| Integrity Baseline | Stored approved hashes/snapshots for protected files. |
| Hash-Chained Audit Log | Append-only audit log where each entry depends on prior hash. |
## CI/CD Terms
| Term | Definition |
| --- | --- |
| Poll NVD CVEs Workflow | Scheduled workflow that fetches and transforms NVD CVEs into advisories. |
| Community Advisory Workflow | Issue-label-triggered workflow that publishes approved community advisories. |
| Skill Release Workflow | Tag-triggered packaging/signing/publishing pipeline for skills. |
| Deploy Pages Workflow | Workflow that builds site assets and mirrors release/advisory artifacts. |
## Source References
- types.ts
- skills/clawsec-suite/skill.json
- skills/clawsec-nanoclaw/skill.json
- skills/clawsec-suite/scripts/guarded_skill_install.mjs
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/feed.mjs
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/suppression.mjs
- skills/clawsec-nanoclaw/guardian/integrity-monitor.ts
- scripts/populate-local-feed.sh
- .github/workflows/poll-nvd-cves.yml
- .github/workflows/community-advisory.yml
- .github/workflows/skill-release.yml
- .github/workflows/deploy-pages.yml
@@ -1,26 +1,30 @@
# Migration Plan: Unsigned Feed → Signed Feed
# Migration Record: Unsigned Feed → Signed Feed (Completed)
## 1) Objective
## 1) Objective and Status
Move ClawSec advisory distribution from unsigned `feed.json` delivery to detached-signature verification with minimal disruption.
Document how ClawSec advisory distribution moved from unsigned `feed.json` delivery to detached-signature verification, with compatibility preserved for legacy clients.
This plan is written against the current repository behavior:
- feed is produced by `poll-nvd-cves.yml` and `community-advisory.yml`
- feed is published by `deploy-pages.yml`
- suite consumers currently load unsigned JSON from remote/local fallback paths
Current status on `main`:
- Signed feed publishing is active in advisory workflows and deploy workflow.
- Suite and NanoClaw consumers default to signed feed endpoints.
- Unsigned behavior exists only as explicit compatibility bypass (`CLAWSEC_ALLOW_UNSIGNED_FEED=1`).
## 2) Baseline (today)
## 2) Baseline (today, post-migration)
Current feed paths in active use:
- Source of truth: `advisories/feed.json`
- Source signature: `advisories/feed.json.sig`
- Skill copy: `skills/clawsec-feed/advisories/feed.json`
- Skill copy signature: `skills/clawsec-feed/advisories/feed.json.sig`
- Pages copy: `public/advisories/feed.json`
- Latest mirror copy: `public/releases/latest/download/advisories/feed.json`
- Pages signature: `public/advisories/feed.json.sig`
- Latest mirror copy: `public/releases/latest/download/advisories/feed.json` (+ `.sig`)
Current consumer defaults:
- `skills/clawsec-suite/hooks/clawsec-advisory-guardian/handler.ts`
- `skills/clawsec-suite/scripts/guarded_skill_install.mjs`
- default URL: `https://raw.githubusercontent.com/prompt-security/clawsec/main/advisories/feed.json`
- `skills/clawsec-nanoclaw/lib/advisories.ts`
- default URL: `https://clawsec.prompt.security/advisories/feed.json`
## 3) Migration principles
@@ -29,21 +33,21 @@ Current consumer defaults:
- **Measured rollout**: enforce verification after telemetry confirms stable signed publishing.
- **Fast rollback**: preserve a path back to unsigned behavior while root cause is investigated.
## 4) Phased timeline
## 4) Phased timeline (historical)
### Phase 0 — Preparation (Week 0)
### Phase 0 — Preparation (Completed)
Deliverables:
- signing keys generated and fingerprints recorded
- GitHub secrets created
- public key(s) added in repo
- runbooks approved (`SECURITY-SIGNING.md`, this file)
- runbooks approved (`security-signing-runbook.md`, this file)
Exit criteria:
- key fingerprints verified by reviewer
- protected branch/workflow controls enabled
### Phase 1 — CI signing enabled, no client enforcement (Week 1)
### Phase 1 — CI signing enabled, no client enforcement (Completed)
Implement:
- add feed signing step/workflow to produce `advisories/feed.json.sig`
@@ -58,7 +62,7 @@ Exit criteria:
- signatures generated successfully for all feed update paths
- deploy artifacts contain both payload and signature companions
### Phase 2 — Consumer dual-read/dual-verify support (Week 2)
### Phase 2 — Consumer dual-read/dual-verify support (Completed)
Implement in consumers:
- read `feed.json` and `feed.json.sig`
@@ -74,7 +78,7 @@ Exit criteria:
- verification logic released and tested
- no false-positive verification failures in soak period
### Phase 3 — Enforcement (Week 3)
### Phase 3 — Enforcement (Completed)
Actions:
- disable temporary unsigned fallback behavior in default paths
@@ -85,7 +89,7 @@ Exit criteria:
- all production clients verify signatures by default
- no unsigned feed dependency in standard installation flow
### Phase 4 — Stabilization (Week 4)
### Phase 4 — Stabilization (Ongoing)
Actions:
- run first key rotation tabletop drill
@@ -165,3 +169,12 @@ Go only if all are true:
- consumer verification path tested for remote + local fallback
- rollback owner is assigned and reachable
- key rotation procedure has been dry-run at least once
## Source References
- .github/workflows/poll-nvd-cves.yml
- .github/workflows/community-advisory.yml
- .github/workflows/deploy-pages.yml
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/handler.ts
- skills/clawsec-suite/scripts/guarded_skill_install.mjs
- advisories/feed.json
- wiki/security-signing-runbook.md
+98
View File
@@ -0,0 +1,98 @@
# Module: Automation and Release Pipelines
## Responsibilities
- Enforce repository quality/security checks before merge and deployment.
- Generate and maintain advisory feed updates from automated and community sources.
- Package, sign, and publish skill release artifacts from tag events.
- Build and deploy static website outputs and mirrored release/advisory assets.
## Key Files
- `.github/workflows/ci.yml`: lint/type/build/security/test matrix.
- `.github/workflows/pages-verify.yml`: PR-only Pages build/signing verification (no publish).
- `.github/workflows/poll-nvd-cves.yml`: daily NVD advisory ingestion.
- `.github/workflows/community-advisory.yml`: issue-label-driven advisory publishing.
- `.github/workflows/skill-release.yml`: release validation, packaging, signing, and publishing.
- `.github/workflows/deploy-pages.yml`: site build + asset mirroring to GitHub Pages.
- `.github/workflows/wiki-sync.yml`: syncs repository `wiki/` into GitHub Wiki.
- `.github/actions/sign-and-verify/action.yml`: shared Ed25519 sign/verify composite action.
- `scripts/prepare-to-push.sh`: local CI-like quality gate.
- `scripts/release-skill.sh`: manual helper for version bump + tag workflow.
## Public Interfaces
| Interface | Trigger | Outcome |
| --- | --- | --- |
| CI workflow | Push/PR on `main` | Fails fast on lint/type/build/test/security regressions. |
| Pages Verify workflow | PR on `main` | Validates Pages build/signing artifacts without production deploy. |
| NVD poll workflow | Cron + dispatch | Updates advisory feed with deduped, normalized CVEs. |
| Community advisory workflow | Issue labeled `advisory-approved` | Opens PR adding signed advisory records. |
| Skill release workflow | Metadata PR changes + tag `<skill>-v*` | PR dry-run/version checks and tagged release publishing. |
| Deploy pages workflow | Successful CI/release run | Publishes site + mirrored artifacts to Pages. |
| Sync wiki workflow | Push `wiki/**` on `main` | Publishes repository wiki content into GitHub Wiki remote. |
## Inputs and Outputs
Inputs/outputs are summarized in the table below.
| Type | Name | Location | Description |
| --- | --- | --- | --- |
| Input | Git refs/events | GitHub Actions event payloads | Determines which workflow path runs. |
| Input | Skill metadata/SBOM | `skills/*/skill.json` | Drives release asset assembly and validation. |
| Input | NVD API data | External API responses | Source CVEs for advisory feed generation. |
| Input | Signing secrets | GitHub Secrets | Private key material for signing artifacts. |
| Output | Signed advisories | `advisories/feed.json(.sig)` + mirrored public files | Consumable signed feed channel. |
| Output | Skill release assets | `release-assets/*` and GitHub release attachments | Installable and verifiable skill artifacts. |
| Output | Website build | `dist/` deployment artifact | Public web frontend and mirrors. |
## Configuration
| Config Point | Location | Notes |
| --- | --- | --- |
| Workflow schedules | `poll-nvd-cves.yml`, `codeql.yml`, `scorecard.yml` | Daily/weekly security automation cadence. |
| Concurrency groups | Workflow `concurrency` blocks | Prevents destructive overlap in key pipelines. |
| Signing key checks | `scripts/ci/verify_signing_key_consistency.sh` | Ensures docs and canonical PEM files align. |
| Local pre-push gating | `scripts/prepare-to-push.sh` | Mirrors CI checks with optional auto-fix. |
## Example Snippets
```yaml
# skill release trigger pattern
on:
push:
tags:
- '*-v[0-9]*.[0-9]*.[0-9]*'
```
```bash
# local all-in-one pre-push gate
./scripts/prepare-to-push.sh
# optional auto-fix
./scripts/prepare-to-push.sh --fix
```
## Edge Cases
- NVD API rate limiting (`403`/`429`) is handled with retry/backoff and can fail workflow on persistent errors.
- Release pipeline blocks on version mismatch between `skill.json` and `SKILL.md` frontmatter.
- Key fingerprint drift between canonical PEM files and docs hard-fails signing-related workflows.
- Deploy workflow intentionally allows unsigned legacy checksums for backward compatibility in some branches.
- Manual helper script has safety checks but includes destructive rollback logic in error branches; use carefully.
## Tests
| Validation Layer | Location |
| --- | --- |
| Workflow execution tests | CI jobs in `.github/workflows/ci.yml` |
| Skill-level unit/property tests | `skills/*/test/*.test.mjs` invoked by CI |
| Local deterministic checks | `scripts/prepare-to-push.sh` |
| Release link checks | `scripts/validate-release-links.sh` |
## Source References
- .github/workflows/ci.yml
- .github/workflows/poll-nvd-cves.yml
- .github/workflows/community-advisory.yml
- .github/workflows/skill-release.yml
- .github/workflows/deploy-pages.yml
- .github/workflows/pages-verify.yml
- .github/workflows/wiki-sync.yml
- .github/workflows/codeql.yml
- .github/workflows/scorecard.yml
- .github/actions/sign-and-verify/action.yml
- scripts/prepare-to-push.sh
- scripts/release-skill.sh
- scripts/validate-release-links.sh
- scripts/ci/verify_signing_key_consistency.sh
+96
View File
@@ -0,0 +1,96 @@
# Module: ClawSec Suite Core
## Responsibilities
- Act as the main skill-of-skills security bundle for OpenClaw-style agents.
- Verify advisory feed authenticity (Ed25519 signatures and optional checksum manifests).
- Detect advisory matches against installed skills and emit actionable runtime alerts.
- Enforce two-step confirmation for risky skill installations.
## Key Files
- `skills/clawsec-suite/skill.json`: suite metadata, embedded components, catalog defaults.
- `skills/clawsec-suite/hooks/clawsec-advisory-guardian/handler.ts`: runtime event handler.
- `skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/feed.mjs`: signed feed loading/parsing.
- `skills/clawsec-suite/hooks/.../lib/matching.ts`: advisory-to-skill matching logic.
- `skills/clawsec-suite/hooks/.../lib/state.ts`: scan state persistence.
- `skills/clawsec-suite/hooks/.../lib/suppression.mjs`: allowlist-based suppression loader.
- `skills/clawsec-suite/scripts/guarded_skill_install.mjs`: advisory-gated installer wrapper.
- `skills/clawsec-suite/scripts/discover_skill_catalog.mjs`: remote/fallback catalog discovery.
## Public Interfaces
| Interface | Consumer | Behavior |
| --- | --- | --- |
| Hook handler default export | OpenClaw hook runtime | Handles `agent:bootstrap` and `command:new` events. |
| `guarded_skill_install.mjs` CLI | Operators/automation | Blocks on advisory matches unless `--confirm-advisory`. |
| `discover_skill_catalog.mjs` CLI | Suite docs/automation | Lists installable skills with fallback metadata. |
| `feed.mjs` functions | Suite scripts and tests | Feed load, signature verification, checksum manifest checks. |
| Exit code contract | External wrappers | `42` indicates explicit second confirmation required. |
## Inputs and Outputs
Inputs/outputs are summarized in the table below.
| Type | Name | Location | Description |
| --- | --- | --- | --- |
| Input | Advisory feed + signatures | Remote URLs or local `advisories/` files | Risk intelligence source for hook and installer. |
| Input | Installed skill metadata | Skill directories under install root | Matcher compares installed versions to advisory affected specs. |
| Input | Suppression config | `OPENCLAW_AUDIT_CONFIG` or default config paths | Selective suppression by check and skill name. |
| Output | Runtime alert messages | Hook event `messages[]` | Advisories and recommended user actions. |
| Output | Persistent state | `~/.openclaw/clawsec-suite-feed-state.json` | De-dup alerts, track scan windows. |
| Output | CLI gating exit codes | Installer process status | Ensures deliberate user confirmation on risk. |
## Configuration
| Variable | Default | Module Effect |
| --- | --- | --- |
| `CLAWSEC_FEED_URL` | Hosted advisory URL | Chooses primary remote feed endpoint. |
| `CLAWSEC_LOCAL_FEED*` vars | Suite-local advisories directory | Configures local signed fallback artifacts. |
| `CLAWSEC_FEED_PUBLIC_KEY` | `advisories/feed-signing-public.pem` | Verification key path. |
| `CLAWSEC_ALLOW_UNSIGNED_FEED` | `0` | Enables temporary migration bypass mode. |
| `CLAWSEC_VERIFY_CHECKSUM_MANIFEST` | `1` | Enables checksum manifest verification layer. |
| `CLAWSEC_HOOK_INTERVAL_SECONDS` | `300` | Controls event-driven scan throttling. |
## Example Snippets
```ts
// hook only handles selected events
function shouldHandleEvent(event: HookEvent): boolean {
const eventName = toEventName(event);
return eventName === 'agent:bootstrap' || eventName === 'command:new';
}
```
```js
// guarded installer confirmation contract
if (matches.length > 0 && !args.confirmAdvisory) {
process.stdout.write('Re-run with --confirm-advisory to proceed.\n');
process.exit(EXIT_CONFIRM_REQUIRED); // 42
}
```
## Edge Cases
- Missing/malformed feed signatures force remote rejection and local fallback attempts.
- Ambiguous checksum manifest basename collisions are treated as errors.
- Unknown skill versions are treated conservatively in version matching logic.
- Suppression is disabled unless config includes the pipeline sentinel (`enabledFor`).
- Invalid environment path tokens are rejected to avoid accidental literal path usage.
## Tests
| Test File | Focus |
| --- | --- |
| `skills/clawsec-suite/test/feed_verification.test.mjs` | Signature/checksum verification and fail-closed behavior. |
| `skills/clawsec-suite/test/guarded_install.test.mjs` | Confirmation gating and match semantics. |
| `skills/clawsec-suite/test/path_resolution.test.mjs` | Home/path expansion and invalid token handling. |
| `skills/clawsec-suite/test/advisory_suppression.test.mjs` | Suppression config parsing and matching. |
| `skills/clawsec-suite/test/skill_catalog_discovery.test.mjs` | Remote index and fallback merge behavior. |
## Source References
- skills/clawsec-suite/skill.json
- skills/clawsec-suite/SKILL.md
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/handler.ts
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/feed.mjs
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/matching.ts
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/state.ts
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/suppression.mjs
- skills/clawsec-suite/hooks/clawsec-advisory-guardian/lib/version.mjs
- skills/clawsec-suite/scripts/guarded_skill_install.mjs
- skills/clawsec-suite/scripts/discover_skill_catalog.mjs
- skills/clawsec-suite/test/feed_verification.test.mjs
- skills/clawsec-suite/test/guarded_install.test.mjs
- skills/clawsec-suite/test/path_resolution.test.mjs
+107
View File
@@ -0,0 +1,107 @@
# Module: Frontend Web App
## Responsibilities
- Render the ClawSec website for home, skills catalog/detail, and advisory feed/detail experiences.
- Render repository wiki content from `wiki/` markdown and expose per-page `llms.txt` links.
- Provide resilient JSON fetch behavior that handles SPA HTML fallback cases.
- Display install commands, checksums, and advisory metadata in a browser-focused UX.
## Key Files
- `index.tsx`: React bootstrap and root mount.
- `App.tsx`: Router map and page entry wiring.
- `pages/Home.tsx`: Landing page, install card, animated platform/file labels.
- `pages/SkillsCatalog.tsx`: Catalog fetch/filter state machine and empty-state handling.
- `pages/SkillDetail.tsx`: Loads `skill.json`, checksums, README/SKILL docs with markdown renderer.
- `pages/FeedSetup.tsx`: Advisory listing UI with pagination.
- `pages/AdvisoryDetail.tsx`: Advisory deep-dive view and source links.
- `pages/WikiBrowser.tsx`: In-app wiki renderer with wiki-page and `llms.txt` links.
- `components/Layout.tsx` + `components/Header.tsx`: Shared shell and nav behavior.
## Public Interfaces
- Browser routes:
- `/`
- `/skills`
- `/skills/:skillId`
- `/feed`
- `/feed/:advisoryId`
- `/wiki/*`
- Static fetch targets:
- `/skills/index.json`
- `/skills/<skill>/skill.json`
- `/skills/<skill>/checksums.json`
- `/advisories/feed.json`
- `/wiki/llms.txt`
- `/wiki/<page>/llms.txt`
- Display contracts:
- `SkillMetadata`, `SkillJson`, `SkillChecksums`, `AdvisoryFeed`, `Advisory` from `types.ts`.
## Inputs and Outputs
Inputs/outputs are summarized in the table below.
| Type | Name | Location | Description |
| --- | --- | --- | --- |
| Input | Skills index JSON | `/skills/index.json` | List of published skills and metadata. |
| Input | Skill payload files | `/skills/<id>/skill.json`, markdown docs, `checksums.json` | Detail-page content and integrity table. |
| Input | Advisory feed JSON | `/advisories/feed.json`, then `https://clawsec.prompt.security/advisories/feed.json` (legacy mirror fallback to `/releases/latest/download/feed.json`) | Advisory list/detail content. |
| Output | Route-specific UI states | Browser view state | Loading, empty, error, and populated experiences. |
| Output | Copy-to-clipboard commands | Clipboard API | Install and checksum snippets copied for users. |
## Configuration
- Build/runtime config comes from:
- `vite.config.ts` (port, host, path alias)
- `index.html` Tailwind config + custom fonts/colors
- `constants.ts` (`ADVISORY_FEED_URL`, `LOCAL_FEED_PATH`)
- Wiki markdown source lives in `wiki/`; `scripts/generate-wiki-llms.mjs` generates `public/wiki/**/llms.txt` (via `predev`/`prebuild`).
- Runtime behavior assumptions:
- JSON responses may be empty or HTML fallback and must be validated.
- Advisory list pagination uses `ITEMS_PER_PAGE = 9`.
## Example Snippets
```tsx
// Catalog fetch logic guards against HTML fallback responses
const contentType = response.headers.get('content-type') ?? '';
const raw = await response.text();
if (!raw.trim() || contentType.includes('text/html') || isProbablyHtmlDocument(raw)) {
setSkills([]);
setFilteredSkills([]);
return;
}
```
```tsx
// Route map defined in App.tsx
<Route path="/skills/:skillId" element={<SkillDetail />} />
<Route path="/feed/:advisoryId" element={<AdvisoryDetail />} />
<Route path="/wiki/*" element={<WikiBrowser />} />
```
## Edge Cases
- Missing `skills/index.json` returns empty catalog instead of hard failure.
- Some environments return `index.html` for missing JSON paths with status `200`; code defends against this.
- Skill detail tolerates missing/malformed checksums and missing markdown docs.
- Advisory detail handles absent optional fields (`cvss_score`, `reporter`, `references`).
## Tests
| Test Type | Location | Notes |
| --- | --- | --- |
| Type/lint/build checks | `scripts/prepare-to-push.sh` + CI | Frontend confidence comes from static checks and build success. |
| App-wide CI gates | `.github/workflows/ci.yml` | Multi-OS TypeScript/ESLint/build checks. |
| Manual smoke checks | `npm run dev` | Validate route rendering and fetch paths during development. |
## Source References
- index.tsx
- App.tsx
- pages/Home.tsx
- pages/SkillsCatalog.tsx
- pages/SkillDetail.tsx
- pages/FeedSetup.tsx
- pages/AdvisoryDetail.tsx
- pages/WikiBrowser.tsx
- pages/Checksums.tsx
- components/Layout.tsx
- components/Header.tsx
- constants.ts
- types.ts
- vite.config.ts
- index.html
- scripts/generate-wiki-llms.mjs
+84
View File
@@ -0,0 +1,84 @@
# Module: Local Validation and Packaging Tools
## Responsibilities
- Validate skill directory metadata/schema and SBOM file presence before release.
- Generate per-skill checksums manifests for local testing or packaging.
- Provide local data bootstrap scripts that mirror CI behavior for advisories and skills.
- Offer release-link and signing-key consistency checks for maintainers.
## Key Files
- `utils/validate_skill.py`: schema and file existence checks for a skill directory.
- `utils/package_skill.py`: checksum manifest generator with skill pre-validation.
- `scripts/populate-local-skills.sh`: generates local catalog and checksums under `public/skills/`.
- `scripts/populate-local-feed.sh`: pulls NVD data and updates feed copies.
- `scripts/validate-release-links.sh`: verifies docs reference releasable assets.
- `scripts/ci/verify_signing_key_consistency.sh`: verifies key fingerprints across docs/files.
## Public Interfaces
| Tool | Interface | Primary Output |
| --- | --- | --- |
| `validate_skill.py` | `python utils/validate_skill.py <skill-dir>` | Exit code + validation summary with warnings/errors. |
| `package_skill.py` | `python utils/package_skill.py <skill-dir> [out-dir]` | `checksums.json` artifact for skill files. |
| `populate-local-skills.sh` | shell CLI | `public/skills/index.json` and per-skill files/checksums. |
| `populate-local-feed.sh` | shell CLI flags `--days`, `--force` | Updated advisory feeds in repo/skill/public paths. |
| `validate-release-links.sh` | shell CLI optional skill arg | Release-link validation report. |
## Inputs and Outputs
Inputs/outputs are summarized in the table below.
| Type | Name | Location | Description |
| --- | --- | --- | --- |
| Input | Skill metadata and SBOM | `skills/<name>/skill.json` | Enumerates required files and release artifacts. |
| Input | Existing feed state | `advisories/feed.json` | Determines incremental NVD polling start date. |
| Input | Environment tools | `jq`, `curl`, `openssl`, Python runtime | Required execution dependencies. |
| Output | Validation diagnostics | stdout/stderr + exit code | Signals readiness for release/CI. |
| Output | Checksums manifests | `checksums.json` | Integrity data for skill artifacts. |
| Output | Local mirrors | `public/skills/*`, `public/advisories/feed.json` | Makes local web preview match CI outputs. |
## Configuration
| Setting | Location | Purpose |
| --- | --- | --- |
| Ruff/Bandit policy | `pyproject.toml` | Python lint/security baseline. |
| CLI flags (`--days`, `--force`) | `populate-local-feed.sh` | Controls window and overwrite semantics. |
| `OPENCLAW_AUDIT_CONFIG` | suppression loaders in scripts | Chooses suppression config path. |
| `CLAWSEC_*` env vars | installer/hook scripts | Path and verification behavior tuning. |
## Example Snippets
```bash
# validate and package a skill locally
python utils/validate_skill.py skills/clawsec-feed
python utils/package_skill.py skills/clawsec-feed ./dist
```
```bash
# refresh local UI data to mirror CI-generated artifacts
./scripts/populate-local-skills.sh
./scripts/populate-local-feed.sh --days 120
```
## Edge Cases
- Validation allows warnings (for example missing optional files) while still returning success when required fields/files are present.
- NVD poll script handles macOS/Linux differences in `date` and `stat` utilities.
- Release-link validation can detect doc references to files missing from SBOM-derived release assets.
- Path expansion guards reject unexpanded home-token literals to avoid misdirected filesystem writes.
## Tests
| Test/Check | Scope |
| --- | --- |
| `ruff check utils/` | Python style and correctness checks. |
| `bandit -r utils/ -ll` | Python security issue scan. |
| `scripts/prepare-to-push.sh` | Combined local gate across TS/Python/shell/security checks. |
| Skill-local tests | `skills/*/test/*.test.mjs` (targeted invocation) |
## Source References
- utils/validate_skill.py
- utils/package_skill.py
- pyproject.toml
- scripts/populate-local-skills.sh
- scripts/populate-local-feed.sh
- scripts/prepare-to-push.sh
- scripts/validate-release-links.sh
- scripts/ci/verify_signing_key_consistency.sh
- skills/openclaw-audit-watchdog/scripts/load_suppression_config.mjs
- skills/clawsec-suite/scripts/guarded_skill_install.mjs
- skills/clawsec-suite/scripts/discover_skill_catalog.mjs
+97
View File
@@ -0,0 +1,97 @@
# Module: NanoClaw Integration
## Responsibilities
- Port ClawSec advisory/signature logic into NanoClaw host+container architecture.
- Provide MCP tools that expose advisory checks, signature verification, and integrity monitoring.
- Maintain host-side cached advisory state with TLS/signature enforcement and IPC-triggered refresh.
- Protect critical NanoClaw files with baseline drift detection and hash-chained audit trails.
## Key Files
- `skills/clawsec-nanoclaw/skill.json`: NanoClaw package contract and MCP tool registry.
- `skills/clawsec-nanoclaw/lib/signatures.ts`: secure fetch and Ed25519 verification primitives.
- `skills/clawsec-nanoclaw/lib/advisories.ts`: feed load and advisory matching helpers.
- `skills/clawsec-nanoclaw/host-services/advisory-cache.ts`: host cache manager.
- `skills/clawsec-nanoclaw/host-services/ipc-handlers.ts`: IPC request dispatch for advisory/signature tasks.
- `skills/clawsec-nanoclaw/host-services/skill-signature-handler.ts`: package signature verification service.
- `skills/clawsec-nanoclaw/guardian/integrity-monitor.ts`: baseline/diff/restore/audit engine.
- `skills/clawsec-nanoclaw/mcp-tools/*.ts`: container-side tool definitions.
## Public Interfaces
| Interface | Context | Notes |
| --- | --- | --- |
| `clawsec_check_advisories` | MCP tool | Lists advisories affecting installed skills. |
| `clawsec_check_skill_safety` | MCP tool | Returns install recommendation for a specific skill. |
| `clawsec_verify_skill_package` | MCP tool | Verifies detached package signature through host IPC. |
| `clawsec_check_integrity` | MCP tool | Runs integrity check, optional auto-restore for critical targets. |
| IPC task `verify_skill_signature` | Host service | Returns structured verification response with error codes. |
| IPC task `refresh_advisory_cache` | Host service | Refreshes signed advisory cache on demand. |
## Inputs and Outputs
Inputs/outputs are summarized in the table below.
| Type | Name | Location | Description |
| --- | --- | --- | --- |
| Input | Signed advisory feed | `https://clawsec.prompt.security/advisories/feed.json(.sig)` | Threat intelligence source for cache refresh. |
| Input | Package + signature files | Host filesystem paths | Pre-install package authenticity checks. |
| Input | Integrity policy | `guardian/policy.json` | Per-path mode and priority controls. |
| Output | Advisory cache | `/workspace/project/data/clawsec-advisory-cache.json` | Host-managed verified advisory data. |
| Output | Verification results | `/workspace/ipc/clawsec_results/*.json` | IPC response payload for tool calls. |
| Output | Integrity state | `.../soul-guardian/` | Baselines, snapshots, patches, quarantine, audit logs. |
## Configuration
| Setting | Default | Effect |
| --- | --- | --- |
| Feed URL | Hosted ClawSec advisory endpoint | Primary remote source for advisory cache manager. |
| Cache TTL | `5 minutes` | Controls staleness threshold before requiring refresh. |
| Fetch timeout | `10 seconds` | Limits host network wait time. |
| Allowed domains | `clawsec.prompt.security`, `prompt.security`, `raw.githubusercontent.com`, `github.com` | Restricts remote fetch targets. |
| Integrity policy modes | `restore`, `alert`, `ignore` | Controls automatic restoration and alert-only behavior. |
## Example Snippets
```ts
// host-side signature verification dispatch
const result = await deps.signatureVerifier.verify({
packagePath,
signaturePath,
publicKeyPem,
allowUnsigned: allowUnsigned || false,
});
```
```ts
// integrity monitor drift handling
if (baseline.mode === 'restore' && autoRestore) {
// quarantine modified file, restore approved snapshot, append audit event
}
```
## Edge Cases
- Disallowed domains or non-HTTPS URLs are blocked by security policy wrappers.
- Missing signature files can be tolerated only when `allowUnsigned` is explicitly set.
- IPC result waits can timeout, causing conservative block recommendations.
- Integrity engine refuses symlink operations to reduce path-redirection attacks.
- Audit-chain validation can detect tampering or corruption in historical records.
## Tests
| Test Scope | File/Path | Notes |
| --- | --- | --- |
| Type contracts | `skills/clawsec-nanoclaw/lib/types.ts` | Defines tool/IPC DB payload contracts. |
| Operational docs | `skills/clawsec-nanoclaw/docs/SKILL_SIGNING.md`, `skills/clawsec-nanoclaw/docs/INTEGRITY.md` | Describes verification/integrity usage patterns. |
| Cross-module behavior | Reuses suite verification patterns | Signature/checksum primitives ported from suite logic. |
## Source References
- skills/clawsec-nanoclaw/skill.json
- skills/clawsec-nanoclaw/lib/types.ts
- skills/clawsec-nanoclaw/lib/signatures.ts
- skills/clawsec-nanoclaw/lib/advisories.ts
- skills/clawsec-nanoclaw/host-services/advisory-cache.ts
- skills/clawsec-nanoclaw/host-services/ipc-handlers.ts
- skills/clawsec-nanoclaw/host-services/skill-signature-handler.ts
- skills/clawsec-nanoclaw/host-services/integrity-handler.ts
- skills/clawsec-nanoclaw/guardian/integrity-monitor.ts
- skills/clawsec-nanoclaw/guardian/policy.json
- skills/clawsec-nanoclaw/mcp-tools/advisory-tools.ts
- skills/clawsec-nanoclaw/mcp-tools/signature-verification.ts
- skills/clawsec-nanoclaw/mcp-tools/integrity-tools.ts
- skills/clawsec-nanoclaw/docs/SKILL_SIGNING.md
- skills/clawsec-nanoclaw/docs/INTEGRITY.md
+109
View File
@@ -0,0 +1,109 @@
# Overview
## Purpose
- ClawSec is a security-focused repository that combines a public web catalog with installable security skills for OpenClaw and NanoClaw environments.
- The codebase supports three delivery paths at once: static website publishing, signed advisory distribution, and per-skill GitHub release packaging.
- Primary users are agent operators, skill developers, and maintainers running CI-based security automation.
![Prompt Security Logo](assets/overview_img_01_prompt-security-logo.png)
![ClawSec Mascot](assets/overview_img_02_clawsec-mascot.png)
## Repo Layout
| Path | Role | Notes |
| --- | --- | --- |
| `pages/`, `components/`, `App.tsx`, `index.tsx` | Vite + React UI | Skill catalog, advisory feed, and detail pages. |
| `skills/` | Security skill packages | Each skill has `skill.json`, `SKILL.md`, optional scripts/tests/docs. |
| `advisories/` | Repository advisory channel | Signed `feed.json` + `feed.json.sig` and key material. |
| `scripts/` | Local automation | Populate feed/skills, pre-push checks, release helpers. |
| `.github/workflows/` | CI/CD pipelines | CI, releases, NVD polling, community advisory ingestion, pages deploy. |
| `utils/` | Python utilities | Skill validation and checksum packaging helpers. |
| `public/` | Published static assets | Site media, mirrored advisories, and generated skill artifacts. |
| `wiki/` | Documentation hub | Architecture, operations runbooks, compatibility, and verification guides. |
## Entry Points
| Entry | Type | Purpose |
| --- | --- | --- |
| `index.tsx` | Frontend bootstrap | Mounts React app into `#root`. |
| `App.tsx` | Frontend router | Defines route map for home, skills, feed, and wiki pages. |
| `scripts/prepare-to-push.sh` | Dev workflow | Runs lint/type/build/security checks before push. |
| `scripts/populate-local-feed.sh` | Data bootstrap | Pulls CVEs from NVD and updates local advisory feeds. |
| `scripts/populate-local-skills.sh` | Data bootstrap | Builds `public/skills/index.json` and per-skill checksums. |
| `scripts/generate-wiki-llms.mjs` | Docs export | Generates `public/wiki/llms.txt` and per-page wiki exports. |
| `.github/workflows/skill-release.yml` | Release entry | Handles PR version-parity/dry-run checks and tag-based packaging/signing/release. |
| `.github/workflows/poll-nvd-cves.yml` | Scheduled feed updates | Polls NVD and updates advisories. |
## Key Artifacts
| Artifact | Produced By | Consumed By |
| --- | --- | --- |
| `advisories/feed.json` | NVD poll + community advisory workflows | Web UI, clawsec-suite hook, installers. |
| `advisories/feed.json.sig` | Signing workflow steps | Signature verification in suite/nanoclaw tooling. |
| `public/skills/index.json` | Deploy workflow / local populate script | `pages/SkillsCatalog.tsx` and `pages/SkillDetail.tsx`. |
| `public/wiki/llms.txt` + `public/wiki/**/llms.txt` | Wiki generator script + build hooks | LLM-ready wiki exports linked from the wiki UI. |
| `public/checksums.json` + `public/checksums.sig` | Deploy workflow | Published integrity artifacts for operators and runtime clients. |
| `release-assets/checksums.json` | Skill release workflow | Release consumers verifying zip integrity. |
| `skills/*/skill.json` | Skill authors | Site catalog generation, validators, and release pipelines. |
## Key Workflows
- Local web development: `npm install` then `npm run dev`.
- Local security data preview: run `./scripts/populate-local-skills.sh` and `./scripts/populate-local-feed.sh` before loading `/skills` and `/feed` pages.
- Pre-push quality gate: run `./scripts/prepare-to-push.sh` (optionally `--fix`).
- Skill lifecycle: edit `skills/<name>/`, validate with `python utils/validate_skill.py`, then tag `<skill>-vX.Y.Z` to trigger release workflow.
- Advisory lifecycle: scheduled NVD poll and issue-label-based community ingestion both merge into the same signed feed.
## Example Snippets
```bash
# local UI + locally populated data
npm install
./scripts/populate-local-skills.sh
./scripts/populate-local-feed.sh --days 120
npm run dev
```
```bash
# canonical TypeScript quality checks used by CI
npx eslint . --ext .ts,.tsx,.js,.jsx,.mjs --max-warnings 0
npx tsc --noEmit
npm run build
```
## Where to Start
- Read `README.md` for product positioning and install paths.
- Open `App.tsx` and `pages/` to understand user-facing behavior.
- Open `skills/clawsec-suite/skill.json` to understand the suite contract and embedded components.
- Review `.github/workflows/ci.yml`, `.github/workflows/pages-verify.yml`, `.github/workflows/skill-release.yml`, `.github/workflows/deploy-pages.yml`, and `.github/workflows/wiki-sync.yml` for production behavior.
## How to Navigate
- UI behavior is centered in `pages/`; visual wrappers sit in `components/`.
- Skill-specific logic is isolated by folder under `skills/`; each folder includes its own scripts/tests/docs.
- Feed handling appears in three layers: repository feed files, workflow updates, and runtime consumers (`clawsec-suite`/`clawsec-nanoclaw`).
- Operational quality gates live in `scripts/` and workflow YAML files.
- For generation traces and update baselines, start from `wiki/GENERATION.md` and then branch into module pages.
## Common Pitfalls
- Using literal home tokens (for example `\$HOME`) in config path env vars can trigger path validation failures.
- Fetching JSON from SPA routes can return HTML with status 200; pages guard for this and treat it as empty-state.
- Unsigned feed bypass mode (`CLAWSEC_ALLOW_UNSIGNED_FEED=1`) exists for migration compatibility and should not be used in steady state.
- Skill release automation expects version parity between `skill.json` and `SKILL.md` frontmatter.
- Some scripts are POSIX shell oriented; Windows users should prefer PowerShell equivalents or WSL.
## Update Notes
- 2026-02-26: Updated repo layout to point operational documentation at `wiki/` instead of the removed root `docs/` directory.
## Source References
- README.md
- package.json
- App.tsx
- index.tsx
- pages/Home.tsx
- pages/SkillsCatalog.tsx
- pages/SkillDetail.tsx
- pages/FeedSetup.tsx
- scripts/prepare-to-push.sh
- scripts/populate-local-feed.sh
- scripts/populate-local-skills.sh
- skills/clawsec-suite/skill.json
- .github/workflows/ci.yml
- .github/workflows/pages-verify.yml
- .github/workflows/skill-release.yml
- .github/workflows/deploy-pages.yml
- .github/workflows/wiki-sync.yml

Some files were not shown because too many files have changed in this diff Show More