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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The Worker reads from Cloudflare KV (binding `secid_REGISTRY`) and serves resolu
|------|---------|
| [SecID](https://github.com/CloudSecurityAlliance/SecID) | Specification + registry data (source of truth) |
| **SecID-Service** (this repo) | Cloudflare Worker REST API + MCP server (production) |
| [SecID-Server-API](https://github.com/CloudSecurityAlliance/SecID-Server-API) | Self-hosted resolver (Python, TypeScript, Docker) |
| [SecID-Server-API](https://github.com/CloudSecurityAlliance/SecID-Server-API) | Self-hosted resolver (Python; TypeScript and Docker planned) |
| [SecID-Client-SDK](https://github.com/CloudSecurityAlliance/SecID-Client-SDK) | Client libraries (Python, TypeScript, Go) |

## Repository Structure
Expand All @@ -30,18 +30,18 @@ SecID-Service/
│ ├── mcp.ts # MCP tool implementations
│ ├── parser.ts # SecID string parsing (registry-aware)
│ ├── resolver.ts # Resolution logic (pattern tree traversal)
│ ├── registry.ts # Compiled-in fallback registry (build-registry.ts output)
│ ├── registry.ts # Test-only registry snapshot (build-registry.ts output, gitignored)
│ ├── kv-registry.ts # KV reads for registry data
│ ├── kv-resolve.ts # KV-backed resolution path
│ ├── observability.ts # Error recording to KV (UUIDv7 keys)
│ └── types.ts # Shared types
├── scripts/
│ ├── build-registry.ts # Compiles SecID JSON → src/registry.ts (fallback)
│ ├── build-registry.ts # Compiles SecID JSON → src/registry.ts (test snapshot)
│ ├── upload-registry-kv.ts # Uploads registry to KV (--sync deletes orphans)
│ └── setup-dns.sh
├── test/ # vitest tests (auto-generated fixtures from registry)
├── website/ # Astro static site (served from same Worker)
├── wrangler.jsonc # Cloudflare Worker config (account, KV, routes)
├── wrangler.toml # Cloudflare Worker config (account, KV, routes)
└── .github/workflows/
└── registry-kv-upload.yml # Triggered by repository_dispatch from SecID
```
Expand All @@ -65,16 +65,16 @@ npx tsx scripts/upload-registry-kv.ts --sync /path/to/SecID # apply
- **Account:** `f3898058ae0b4c20c692bbfa5b9b44b0` (Kseifried@cloudsecurityalliance.org's Account)
- **Worker route:** `secid.cloudsecurityalliance.org/*` (zone `cloudsecurityalliance.org`)
- **KV namespaces:**
- `secid_REGISTRY` (id `cfbc271787614516a39fa43d9ca4f95a`) — registry data, ~1,150 keys
- `secid_REGISTRY` (id `cfbc271787614516a39fa43d9ca4f95a`) — registry data: one key per namespace plus the type, global and meta index keys
- `secid_OBSERVABILITY` (id `c5cbc52b9a724433b3043efdf31857f4`) — error logging

## Deploy Chain

The full deploy chain is described in [SecID/CLAUDE.md](https://github.com/CloudSecurityAlliance/SecID/blob/main/CLAUDE.md#cicd). Briefly:

1. Registry change pushed to `CloudSecurityAlliance/SecID`
2. SecID's notify workflow fires `repository_dispatch` (event-type `registry-updated`) using PAT `SECID_TO_SERVICE_DISPATCH`
3. This repo's `registry-kv-upload.yml` workflow runs:
2. SecID's `registry-ci.yml` runs the registry validation gates; only if they all pass does its `notify-service` job fire `repository_dispatch` (event-type `registry-updated`, `client_payload.ref` = the validated commit SHA) using PAT `SECID_TO_SERVICE_DISPATCH`
3. This repo's `registry-kv-upload.yml` workflow runs against that SecID commit:
- Builds + tests
- Runs `upload-registry-kv.ts --sync` using `SECID_SERVICE_DEPLOY` Cloudflare token
- Deploys Worker
Expand All @@ -101,7 +101,7 @@ CI runs `--sync` by default, so KV stays continuously synchronized.

## Key Design Decisions

- **Worker is stateless.** All state lives in KV. The Worker's compiled-in registry (`src/registry.ts`) is a fallback only; production reads from KV via `kv-registry.ts`.
- **Worker is stateless.** All state lives in KV; production reads it via `kv-registry.ts`. `src/registry.ts` is **not** a fallback and is not used at runtime: it is a gitignored snapshot that `build-registry.ts` generates so tests (and the seeded test KV) run against a known registry. A stale local snapshot makes tests disagree with production — rebuild it from the SecID checkout you care about.
- **Registry is the source of truth.** This repo doesn't store registry data — it loads from the SecID repo at build time and uploads to KV at deploy time.
- **Tests gate the deploy.** If `npx vitest run` fails, the upload + deploy steps don't run. Test failures from registry-derived fixtures often indicate registry misconfiguration in the SecID repo, not bugs here.
- **Stateless MCP must 405 GET/DELETE** on the Streamable HTTP transport — without this, SSE clients hang forever. (Known community issue across SDKs.)
Expand Down
16 changes: 8 additions & 8 deletions OPERATIONAL-RESOURCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Index of recurring operational work for SecID-Service. Pairs with [BACKUP-RESOUR
- **Inputs:** HTTP requests, KV reads from `secid_REGISTRY`
- **Outputs:** JSON resolver responses, MCP JSON-RPC responses, static HTML, error records into `secid_OBSERVABILITY`
- **Status:** production
- **Last touched:** 2026-04-30
- **Last touched:** 2026-09-25
- **Next review:** 2026-08-01
- **Cadence:** request-driven (no schedule)
- **Health check:** `curl https://secid.cloudsecurityalliance.org/api/v1/resolve?secid=secid:advisory/mitre.org/cve%23CVE-2021-44228` — should return JSON envelope with a URL
Expand All @@ -32,7 +32,7 @@ Index of recurring operational work for SecID-Service. Pairs with [BACKUP-RESOUR
- **Inputs:** registry JSON files in [CloudSecurityAlliance/SecID](https://github.com/CloudSecurityAlliance/SecID) `registry/**/*.json` (724 namespaces as of 2026-05-07)
- **Outputs:** KV keys consumed by the Worker resolver
- **Status:** production
- **Last touched:** 2026-04-30 (last successful sync — chain has been broken since)
- **Last touched:** 2026-09-25 (last successful sync — chain has been broken since)
- **Next review:** unblock as part of FRICTION-001 resolution; then quarterly thereafter
- **Cadence:** auto-synced on push to `registry/**/*.json` in the SecID repo
- **Health check:** `wrangler kv key list --namespace-id=cfbc271787614516a39fa43d9ca4f95a | wc -l` should match the namespace count produced by `build-registry.ts`
Expand Down Expand Up @@ -61,18 +61,18 @@ Index of recurring operational work for SecID-Service. Pairs with [BACKUP-RESOUR

**What it does.** Two-stage auto-deploy from registry JSON changes to live KV + Worker:

1. **Stage 1 (SecID repo):** [`update-registry.yml`](https://github.com/CloudSecurityAlliance/SecID/blob/main/.github/workflows/update-registry.yml) — fires on push to `main` touching `registry/**/*.json`; sends `repository_dispatch` to SecID-Service using `SECID_TO_SERVICE_DISPATCH` PAT
1. **Stage 1 (SecID repo):** [`registry-ci.yml`](https://github.com/CloudSecurityAlliance/SecID/blob/main/.github/workflows/registry-ci.yml) (replaced `update-registry.yml`) — on push to `main`, runs the registry validation gates; only if all pass does its `notify-service` job send `repository_dispatch` to SecID-Service using the `SECID_TO_SERVICE_DISPATCH` PAT, with `client_payload.ref` set to the validated commit SHA
2. **Stage 2 (this repo):** [`.github/workflows/registry-kv-upload.yml`](.github/workflows/registry-kv-upload.yml) — receives dispatch, checks out both repos, runs `build-registry.ts`, builds website, runs `vitest`, uploads to KV in `--sync` mode using `SECID_SERVICE_DEPLOY` Cloudflare token, deploys Worker

- **Runtime:** GitHub Actions runners (`ubuntu-latest`, Node 22)
- **Inputs:** push events on registry JSON; `repository_dispatch` events of type `registry-updated`
- **Outputs:** updated `secid_REGISTRY` KV; redeployed Worker
- **Status:** **broken** since 2026-04-30 — see [FRICTION-001](FRICTION/FRICTION-001.md) and [WAITING-FOR-001](WAITING-FOR/WAITING-FOR-001.md). Auto-trigger fails (`SECID_TO_SERVICE_DISPATCH` token unauthorized); manual `workflow_dispatch` of Stage 2 fails on `cve-schema` Vitest test failure
- **Last touched:** 2026-04-30
- **Next review:** weekly until FRICTION-001 is resolved; then 2026-09-01
- **Status:** working — `repository_dispatch` runs succeed as of 2026-09-25. The 2026-04-30 breakage is recorded in [FRICTION-001](FRICTION/FRICTION-001.md)
- **Last touched:** 2026-09-25
- **Next review:** 2026-12-01
- **Cadence:** event-driven (every push to `registry/**/*.json`)
- **Health check:** `gh run list --workflow=registry-kv-upload.yml --limit 3 -R CloudSecurityAlliance/SecID-Service` — most recent run should be `success`. Currently shows `failure`/no-recent-runs
- **Runbook:** See FRICTION-001 for current breakage and partial workarounds. Local audit (no mutations): `npx tsx scripts/upload-registry-kv.ts --sync --dry-run /path/to/SecID` from this repo with a working `CLOUDFLARE_API_TOKEN`
- **Health check:** `gh run list --workflow=registry-kv-upload.yml --limit 3 -R CloudSecurityAlliance/SecID-Service` — most recent run should be `success`
- **Runbook:** See FRICTION-001 for past failure modes and workarounds. Local audit (no mutations): `npx tsx scripts/upload-registry-kv.ts --sync --dry-run /path/to/SecID` from this repo with a working `CLOUDFLARE_API_TOKEN`
- **Owner:** Kurt Seifried

## DNS — `secid.cloudsecurityalliance.org`
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Add SecID to your AI assistant as a remote MCP server:
https://secid.cloudsecurityalliance.org/mcp
```

That's it. No API keys, no local install, no configuration. Works with Claude Desktop, Claude Code, Cursor, Windsurf, and any MCP client that supports remote servers. Your AI assistant gets three tools (`resolve`, `lookup`, `describe`) and can immediately look up CVEs, CWEs, ATT&CK techniques, NIST controls, and 1,150+ other security knowledge sources.
That's it. No API keys, no local install, no configuration. Works with Claude Desktop, Claude Code, Cursor, Windsurf, and any MCP client that supports remote servers. Your AI assistant gets three tools (`resolve`, `lookup`, `describe`) and can immediately look up CVEs, CWEs, ATT&CK techniques, NIST controls, and more than 2,100 other security knowledge sources.

**Other ways to use SecID:** [Claude Code plugin](https://github.com/CloudSecurityAlliance/SecID/tree/main/plugins/secid) (local MCP server, supports internal resolvers) | [Client SDKs](https://github.com/CloudSecurityAlliance/SecID-Client-SDK) (Python, TypeScript, Go) | REST API (below)

Expand Down Expand Up @@ -56,17 +56,17 @@ No authentication. CORS enabled.
- `secid` input limit: **1024 characters** on REST and MCP tool inputs.
- REST returns `status="error"` with: `SecID query exceeds 1024 characters. Limit: 1024 characters.`
- MCP returns tool error content with the same explicit limit message.
- MCP HTTP request body limit (via `Content-Length`): **64 KiB** (`413` when exceeded).
- MCP HTTP request body limit: **64 KiB** (`413` when exceeded).
- Cloudflare KV value limit: **25 MiB** per key.
- Registry upload script enforces this limit before upload.
- Service also checks `full:registry` payload size before serving `/api/v1/registry.json`.
- Abuse throttling/rate limiting: handled primarily at the **Cloudflare edge** (WAF/rate-limit controls), with Worker input-size guards as defense in depth.
- Abuse throttling: the Worker enforces the input-size limits above. Edge rate limiting (Cloudflare WAF) is the intended primary layer ([ADR-012](DECISIONS.md)), but that configuration lives in the Cloudflare dashboard, not in this repository — check the zone's rules rather than assuming they are active.

## Architecture

- **Runtime:** Cloudflare Workers
- **Framework:** Hono + @modelcontextprotocol/sdk
- **Registry:** Compiled from [SecID](https://github.com/CloudSecurityAlliance/SecID) registry JSON files (1,150+ namespaces, 10 types)
- **Registry:** Compiled from [SecID](https://github.com/CloudSecurityAlliance/SecID) registry JSON files (more than 2,100 namespaces across 10 types)
- **Website:** Astro static site served from the same Worker

## Planned: also runs under the CSA MCP Server front door
Expand Down
2 changes: 1 addition & 1 deletion scripts/upload-registry-kv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Reads all JSON registry files and writes:
* - secid:{type}/{namespace} — raw namespace JSON (×121)
* - secid:{type} — TypeIndex with child_index (×7)
* - secid:{type} — TypeIndex with child_index (one per type)
* - secid:* — GlobalIndex: combined child_index across all types (×1)
* - secid:registry — complete compiled Registry (×1)
* - secid:meta — version/counts metadata (×1)
Expand Down
4 changes: 2 additions & 2 deletions src/kv-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function resolveFromKV(
): Promise<ResolveResponse> {
const ctx = new RegistryContext(kv);

// 0. Root query: "secid" or "secid:" → list all 8 types
// 0. Root query: "secid" or "secid:" → list every type
const trimmed = input.trim();
if (/^secid:?$/i.test(trimmed)) {
const types: Array<{ type: string; description: string; namespace_count: number }> = [];
Expand Down Expand Up @@ -272,7 +272,7 @@ function buildPartialRegistry(
*
* Fetches the "secid:*" KV key — a single combined child_index across all types.
* Pattern-matches the input to find which type(s) and namespace(s) contain it,
* then resolves across all matches. One KV read instead of seven.
* then resolves across all matches. One KV read instead of one per type.
*/
async function searchBareIdentifier(
ctx: RegistryContext,
Expand Down
2 changes: 1 addition & 1 deletion src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,7 @@ function createMcpServer(
server.resource(
"registry",
"secid://registry",
{ description: `Full listing of all SecID types and their namespace counts. SecID covers ${TYPE_REGISTRY.length} types: ${TYPES_INLINE}. 700+ namespaces total.` },
{ description: `Full listing of all SecID types and their namespace counts. SecID covers ${TYPE_REGISTRY.length} types: ${TYPES_INLINE}. Read this resource for the current per-type namespace counts.` },
async () => {
const listing: Record<string, number> = {};
const ctx = new RegistryContext(registryKv);
Expand Down
4 changes: 2 additions & 2 deletions website/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const apiExample = `GET /api/v1/resolve?secid=secid:advisory/mitre.org/cve%23CVE
That's it. No API keys, no local install, no configuration. Works with Claude Desktop, Claude Code, Cursor, Windsurf, and any MCP client that supports remote servers.
</p>
<p>
Your AI assistant gets three tools (<code>resolve</code>, <code>lookup</code>, <code>describe</code>) and can immediately look up CVEs, CWEs, ATT&CK techniques, NIST controls, and 700+ other security knowledge sources.
Your AI assistant gets three tools (<code>resolve</code>, <code>lookup</code>, <code>describe</code>) and can immediately look up CVEs, CWEs, ATT&CK techniques, NIST controls, and more than 2,100 other security knowledge sources.
</p>
</section>

Expand Down Expand Up @@ -155,7 +155,7 @@ secid:control/nist.gov/800-53@r5#AC-1
<section>
<h2>The Registry</h2>
<p>
SecID is backed by a <a href="https://github.com/CloudSecurityAlliance/SecID/tree/main/registry" target="_blank" rel="noopener">curated registry</a> of 700+ namespaces across 10 types — covering CVE, CWE, ATT&CK, NIST 800-53, ISO 27001, OWASP, and dozens more.
SecID is backed by a <a href="https://github.com/CloudSecurityAlliance/SecID/tree/main/registry" target="_blank" rel="noopener">curated registry</a> of more than 2,100 namespaces across 10 types — covering CVE, CWE, ATT&CK, NIST 800-53, ISO 27001, OWASP, and dozens more.
</p>
<p>
The registry is open source. <a href="https://github.com/CloudSecurityAlliance/SecID" target="_blank" rel="noopener">Contributions welcome</a> — adding a new source is a single JSON file.
Expand Down
Loading