diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 614c0f4..e024e11 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -4,7 +4,6 @@ on: workflow_run: workflows: ["CI", "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 +18,25 @@ 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 or trusted workflow_run sources. + # PR validation runs in .github/workflows/pages-verify.yml. + if: | + github.event_name == 'workflow_dispatch' || + ( + github.event_name == 'workflow_run' && + github.event.workflow_run.conclusion == 'success' && + ( + ( + github.event.workflow_run.name == 'CI' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'main' + ) || + ( + 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 +417,24 @@ 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 == 'workflow_run' && + github.event.workflow_run.conclusion == 'success' && + ( + ( + github.event.workflow_run.name == 'CI' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'main' + ) || + ( + github.event.workflow_run.name == 'Skill Release' && + github.event.workflow_run.event != 'pull_request' + ) + ) + ) environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.github/workflows/pages-verify.yml b/.github/workflows/pages-verify.yml new file mode 100644 index 0000000..70f38e4 --- /dev/null +++ b/.github/workflows/pages-verify.yml @@ -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<> "$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 diff --git a/.gitignore b/.gitignore index 30934d4..8faf088 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ dist-ssr # Derived public assets (copied during build) public/advisories public/skills +public/wiki/ # Python bytecode __pycache__/ diff --git a/App.tsx b/App.tsx index aad20db..327140d 100644 --- a/App.tsx +++ b/App.tsx @@ -6,6 +6,7 @@ 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'; const App: React.FC = () => { return ( @@ -17,10 +18,11 @@ const App: React.FC = () => { } /> } /> } /> + } /> ); }; -export default App; \ No newline at end of file +export default App; diff --git a/README.md b/README.md index 91ef21b..28b5341 100644 --- a/README.md +++ b/README.md @@ -313,8 +313,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 --- @@ -375,6 +375,9 @@ 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 ``` ### Build @@ -395,6 +398,7 @@ npm run build ├── scripts/ │ ├── 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) diff --git a/components/Header.tsx b/components/Header.tsx index 11b589e..fe130d3 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -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 } from 'lucide-react'; export const Header: React.FC = () => { const [isOpen, setIsOpen] = useState(false); @@ -9,6 +9,7 @@ export const Header: React.FC = () => { { label: 'Home', path: '/', icon: Home }, { label: 'Skills', path: '/skills', icon: Layers }, { label: 'Security Feed', path: '/feed', icon: Rss }, + { label: 'Wiki', path: '/wiki', icon: BookOpenText }, ]; const baseLink = diff --git a/package-lock.json b/package-lock.json index 161aec6..dae8d7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -953,154 +953,329 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "android" + ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "darwin" + ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "darwin" + ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "freebsd" + ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "freebsd" + ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "linux" + ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "openbsd" + ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "openharmony" + ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], "dev": true, - "optional": true + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5049,8 +5224,9 @@ } }, "node_modules/rollup": { - "version": "4.57.1", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "dependencies": { "@types/estree": "1.0.8" @@ -5063,31 +5239,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index 8e39ca1..8f06c9e 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,11 @@ "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" }, diff --git a/pages/SkillDetail.tsx b/pages/SkillDetail.tsx index 103000c..18b4fcd 100644 --- a/pages/SkillDetail.tsx +++ b/pages/SkillDetail.tsx @@ -5,12 +5,8 @@ import Markdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Footer } from '../components/Footer'; import type { SkillJson, SkillChecksums } from '../types'; - -// Strip YAML frontmatter from markdown content -const stripFrontmatter = (content: string): string => { - const frontmatterRegex = /^---\s*\n[\s\S]*?\n---\s*\n/; - return content.replace(frontmatterRegex, ''); -}; +import { defaultMarkdownComponents } from '../utils/markdownComponents'; +import { stripFrontmatter } from '../utils/markdownHelpers.mjs'; const isProbablyHtmlDocument = (text: string): boolean => { const start = text.trimStart().slice(0, 200).toLowerCase(); @@ -320,102 +316,7 @@ export const SkillDetail: React.FC = () => {
( -

- {children} -

- ), - h2: ({ children }) => ( -

{children}

- ), - h3: ({ children }) => ( -

{children}

- ), - h4: ({ children }) => ( -

{children}

- ), - p: ({ children }) => ( -

{children}

- ), - a: ({ href, children }) => ( - - {children} - - ), - ul: ({ children }) => ( -
    - {children} -
- ), - ol: ({ children }) => ( -
    - {children} -
- ), - li: ({ children }) => ( -
  • {children}
  • - ), - blockquote: ({ children }) => ( -
    - {children} -
    - ), - code: ({ className, children }) => { - const isInline = !className; - if (isInline) { - return ( - - {children} - - ); - } - return ( - {children} - ); - }, - pre: ({ children }) => ( -
    -                    {children}
    -                  
    - ), - table: ({ children }) => ( -
    - - {children} -
    -
    - ), - thead: ({ children }) => ( - - {children} - - ), - tbody: ({ children }) => {children}, - tr: ({ children }) => ( - {children} - ), - th: ({ children }) => ( - - {children} - - ), - td: ({ children }) => ( - {children} - ), - hr: () =>
    , - strong: ({ children }) => ( - {children} - ), - em: ({ children }) => ( - {children} - ), - }} + components={defaultMarkdownComponents} > {stripFrontmatter(doc.content)}
    diff --git a/pages/WikiBrowser.tsx b/pages/WikiBrowser.tsx new file mode 100644 index 0000000..c410ab1 --- /dev/null +++ b/pages/WikiBrowser.tsx @@ -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; + +const assetModules = import.meta.glob('../wiki/**/*.{png,jpg,jpeg,gif,svg,webp,avif}', { + eager: true, + import: 'default', +}) as Record; + +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( + wikiDocs.map((doc) => [doc.slug.toLowerCase(), doc]), +); + +const wikiDocByFilePath = new Map( + wikiDocs.map((doc) => [doc.filePath.toLowerCase(), doc]), +); + +const wikiAssetByPath = new Map( + 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(); + 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 ( +
    + +

    Wiki unavailable

    +

    No markdown files were found in the wiki source.

    +
    + ); + } + + 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 {children}; + + const wikiRoute = resolveWikiRouteFromHref(href); + if (wikiRoute) { + return ( + + {children} + + ); + } + + const assetHref = resolveAssetUrl(href); + const finalHref = assetHref ?? href; + const safeHref = sanitizeHref(finalHref); + if (!safeHref) { + return {children}; + } + const external = isExternalHref(safeHref); + + return ( + + {children} + + ); + }, + img: ({ src, alt }) => { + const resolvedSrc = src ? resolveAssetUrl(src) : null; + const finalSrc = resolvedSrc ?? (src ? sanitizeImageSrc(src) : null); + if (!finalSrc) { + return [image blocked]; + } + return ( + {alt + ); + }, + }; + + return ( +
    +
    +

    + + Wiki +

    +

    + Full repository wiki rendered from markdown in wiki/. + This is the same source synced to GitHub Wiki. +

    + +
    + +
    + + +
    + {notFound && ( +
    + Wiki page not found for {requested}. Showing {selectedDoc.title} instead. +
    + )} + + + {selectedDoc.content} + +
    +
    + +
    +
    + ); +}; diff --git a/scripts/generate-wiki-llms.mjs b/scripts/generate-wiki-llms.mjs new file mode 100644 index 0000000..913c4f6 --- /dev/null +++ b/scripts/generate-wiki-llms.mjs @@ -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(); diff --git a/scripts/populate-local-wiki.sh b/scripts/populate-local-wiki.sh new file mode 100755 index 0000000..be02b72 --- /dev/null +++ b/scripts/populate-local-wiki.sh @@ -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//llms.txt" diff --git a/skills/clawsec-nanoclaw/README.md b/skills/clawsec-nanoclaw/README.md index abd4080..bd1b2e2 100644 --- a/skills/clawsec-nanoclaw/README.md +++ b/skills/clawsec-nanoclaw/README.md @@ -142,7 +142,7 @@ 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 diff --git a/utils/markdownComponents.tsx b/utils/markdownComponents.tsx new file mode 100644 index 0000000..a0b4618 --- /dev/null +++ b/utils/markdownComponents.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import type { Components } from 'react-markdown'; + +export const defaultMarkdownComponents: Components = { + h1: ({ children }) => ( +

    + {children} +

    + ), + h2: ({ children }) => ( +

    {children}

    + ), + h3: ({ children }) => ( +

    {children}

    + ), + h4: ({ children }) => ( +

    {children}

    + ), + p: ({ children }) => ( +

    {children}

    + ), + a: ({ href, children }) => ( + + {children} + + ), + ul: ({ children }) => ( +
      + {children} +
    + ), + ol: ({ children }) => ( +
      + {children} +
    + ), + li: ({ children }) => ( +
  • {children}
  • + ), + blockquote: ({ children }) => ( +
    + {children} +
    + ), + code: ({ className, children }) => { + const isInline = !className; + if (isInline) { + return ( + + {children} + + ); + } + return ( + {children} + ); + }, + pre: ({ children }) => ( +
    +      {children}
    +    
    + ), + table: ({ children }) => ( +
    + + {children} +
    +
    + ), + thead: ({ children }) => ( + + {children} + + ), + tbody: ({ children }) => {children}, + tr: ({ children }) => ( + {children} + ), + th: ({ children }) => ( + + {children} + + ), + td: ({ children }) => ( + {children} + ), + hr: () =>
    , + strong: ({ children }) => ( + {children} + ), + em: ({ children }) => ( + {children} + ), +}; diff --git a/utils/markdownHelpers.mjs b/utils/markdownHelpers.mjs new file mode 100644 index 0000000..ef9c785 --- /dev/null +++ b/utils/markdownHelpers.mjs @@ -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); +}; diff --git a/utils/wikiPathHelpers.mjs b/utils/wikiPathHelpers.mjs new file mode 100644 index 0000000..ec87350 --- /dev/null +++ b/utils/wikiPathHelpers.mjs @@ -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`; +}; diff --git a/wiki/GENERATION.md b/wiki/GENERATION.md index b164c52..fb37ba5 100644 --- a/wiki/GENERATION.md +++ b/wiki/GENERATION.md @@ -1,9 +1,9 @@ # Wiki Generation Metadata -- Commit hash: `448aed326192d38812cb508820f967cb74e77ae9` -- Branch name: `main` -- Generation timestamp (local): `2026-02-25T20:59:57+0200` -- Generation mode: `initial` +- 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`) @@ -11,8 +11,8 @@ - `architecture_img_01_prompt-line.svg` (from `public/img/prompt_line.svg`) ## Notes -- This is a first-time generation (`wiki/` did not exist before this run). -- Index sections were generated from repository structure and created wiki pages. +- 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 @@ -24,3 +24,8 @@ - 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 diff --git a/wiki/INDEX.md b/wiki/INDEX.md index 7a59ed7..78be0fa 100644 --- a/wiki/INDEX.md +++ b/wiki/INDEX.md @@ -5,7 +5,7 @@ - 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, Modules for implementation boundaries, and Source References at the end of each page to jump into code. +- 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) @@ -19,6 +19,13 @@ - [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) @@ -32,6 +39,9 @@ ## 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 diff --git a/docs/COMPATIBILITY_REPORT.md b/wiki/compatibility-report.md similarity index 86% rename from docs/COMPATIBILITY_REPORT.md rename to wiki/compatibility-report.md index e35cd6b..458ee85 100644 --- a/docs/COMPATIBILITY_REPORT.md +++ b/wiki/compatibility-report.md @@ -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 diff --git a/docs/MIGRATION-SIGNED-FEED.md b/wiki/migration-signed-feed.md similarity index 93% rename from docs/MIGRATION-SIGNED-FEED.md rename to wiki/migration-signed-feed.md index feeb108..26e1455 100644 --- a/docs/MIGRATION-SIGNED-FEED.md +++ b/wiki/migration-signed-feed.md @@ -37,7 +37,7 @@ 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 @@ -165,3 +165,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 diff --git a/wiki/overview.md b/wiki/overview.md index 72fbdf3..5393fe2 100644 --- a/wiki/overview.md +++ b/wiki/overview.md @@ -18,7 +18,7 @@ | `.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. | -| `docs/` | Operational docs | Signing runbooks, migration plans, compatibility and verification guides. | +| `wiki/` | Documentation hub | Architecture, operations runbooks, compatibility, and verification guides. | ## Entry Points | Entry | Type | Purpose | @@ -84,6 +84,9 @@ npm run build - 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 diff --git a/docs/PLATFORM_VERIFICATION.md b/wiki/platform-verification.md similarity index 87% rename from docs/PLATFORM_VERIFICATION.md rename to wiki/platform-verification.md index 748906c..6e8d14d 100644 --- a/docs/PLATFORM_VERIFICATION.md +++ b/wiki/platform-verification.md @@ -85,3 +85,14 @@ Use this checklist to validate portability and path-handling behavior after chan 4. Confirm no `$HOME` segment directory was created under working directories. Expected outcome: **no directories containing literal `$HOME` are created by supported setup scripts.** + +## Source References +- .gitattributes +- scripts/populate-local-feed.sh +- scripts/populate-local-skills.sh +- skills/clawsec-suite/test/path_resolution.test.mjs +- skills/clawsec-suite/test/guarded_install.test.mjs +- skills/clawsec-suite/test/advisory_suppression.test.mjs +- skills/clawsec-suite/scripts/guarded_skill_install.mjs +- skills/openclaw-audit-watchdog/scripts/load_suppression_config.mjs +- skills/openclaw-audit-watchdog/test/suppression_config.test.mjs diff --git a/docs/REMEDIATION_PLAN.md b/wiki/remediation-plan.md similarity index 86% rename from docs/REMEDIATION_PLAN.md rename to wiki/remediation-plan.md index e2af965..8d59b40 100644 --- a/docs/REMEDIATION_PLAN.md +++ b/wiki/remediation-plan.md @@ -71,3 +71,15 @@ - path token validation now enforced - how to correct invalid quoted env values - where PowerShell examples live + +## Source References +- .gitattributes +- .github/workflows/ci.yml +- scripts/populate-local-feed.sh +- scripts/populate-local-skills.sh +- scripts/release-skill.sh +- skills/clawsec-suite/hooks/clawsec-advisory-guardian/handler.ts +- skills/clawsec-suite/scripts/guarded_skill_install.mjs +- skills/openclaw-audit-watchdog/scripts/load_suppression_config.mjs +- wiki/platform-verification.md +- wiki/compatibility-report.md diff --git a/docs/SECURITY-SIGNING.md b/wiki/security-signing-runbook.md similarity index 94% rename from docs/SECURITY-SIGNING.md rename to wiki/security-signing-runbook.md index a775e17..4ad8f47 100644 --- a/docs/SECURITY-SIGNING.md +++ b/wiki/security-signing-runbook.md @@ -213,3 +213,16 @@ Before requiring signatures in all clients: - deploy pipeline mirrors signature companions - one rollback drill and one key rotation drill completed successfully - incident response on-call owner identified and documented + +## Source References +- advisories/feed.json +- advisories/feed.json.sig +- advisories/feed-signing-public.pem +- clawsec-signing-public.pem +- .github/actions/sign-and-verify/action.yml +- .github/workflows/poll-nvd-cves.yml +- .github/workflows/community-advisory.yml +- .github/workflows/deploy-pages.yml +- .github/workflows/skill-release.yml +- scripts/ci/verify_signing_key_consistency.sh +- wiki/migration-signed-feed.md diff --git a/wiki/security.md b/wiki/security.md index 5d788ed..36d0f39 100644 --- a/wiki/security.md +++ b/wiki/security.md @@ -29,8 +29,8 @@ - Release docs include manual verification commands for downstream consumers. ## Incident and Rotation Playbooks -- `docs/SECURITY-SIGNING.md` defines key generation, custody, rotation, and incident phases. -- `docs/MIGRATION-SIGNED-FEED.md` defines staged enforcement and rollback levels. +- `wiki/security-signing-runbook.md` defines key generation, custody, rotation, and incident phases. +- `wiki/migration-signed-feed.md` defines staged enforcement and rollback levels. - Rollback paths prioritize preserving signed publishing where possible and time-boxing any bypass. ## Example Snippets @@ -56,10 +56,13 @@ openssl pkey -pubin -in clawsec-signing-public.pem -outform DER | shasum -a 256 - Add explicit tests for workflow-level signature failure scenarios. - Increase runtime telemetry for advisory fetch/verification failures to simplify incident triage. +## Update Notes +- 2026-02-26: Repointed signing and migration references from root `docs/` files to dedicated `wiki/` operations pages. + ## Source References - SECURITY.md -- docs/SECURITY-SIGNING.md -- docs/MIGRATION-SIGNED-FEED.md +- wiki/security-signing-runbook.md +- wiki/migration-signed-feed.md - scripts/ci/verify_signing_key_consistency.sh - .github/actions/sign-and-verify/action.yml - .github/workflows/poll-nvd-cves.yml diff --git a/wiki/testing.md b/wiki/testing.md index a326463..7833040 100644 --- a/wiki/testing.md +++ b/wiki/testing.md @@ -57,6 +57,9 @@ node skills/openclaw-audit-watchdog/test/suppression_config.test.mjs 3. For feed/signing changes, run suite verification tests first (`feed_verification`, `guarded_install`). 4. For workflow or release changes, also run `scripts/validate-release-links.sh` and key consistency script. +## Update Notes +- 2026-02-26: Updated source references to the migrated `wiki/platform-verification.md` checklist. + ## Source References - AGENTS.md - scripts/prepare-to-push.sh @@ -70,4 +73,4 @@ node skills/openclaw-audit-watchdog/test/suppression_config.test.mjs - skills/clawsec-suite/test/path_resolution.test.mjs - skills/openclaw-audit-watchdog/test/suppression_config.test.mjs - skills/clawsec-clawhub-checker/test/reputation_check.test.mjs -- docs/PLATFORM_VERIFICATION.md +- wiki/platform-verification.md