Skip to content
Draft
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
40 changes: 40 additions & 0 deletions docs/api-inventory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Wardnet HTTP API inventory

Snapshot: 2026-08-26, `origin/main` at `107117634764c901dff540044585d64088fafedb`.

| Product area | Existing HTTP contracts | Lifecycle gap after this change |
| --- | --- | --- |
| Health and deployment | `GET /healthz`, `/readyz`, `/api/version`, `/metrics`, `/api/support-bundle` | No authenticated runtime configuration view or reload contract. |
| Gateway and routes | `ANY /gateway/{path}`, `GET/POST /api/routes`, `GET/PUT/DELETE /api/routes/{route_id}`, `POST /api/evaluate` | Route collection pagination and an explicit gateway decision trace lookup remain absent. |
| WAF | `POST /api/waf/coraza/audit` | No rule-set activation/version API; this must follow the Coraza/CRS authority contract rather than inventing rules. |
| IDS | `POST /api/ids/suricata/eve` | No sensor registration, sensor health, or EVE cursor/checkpoint API. |
| AI SOC | `GET /api/soc/llm-config`, `POST /api/soc/analyze` | No analysis job/history/feedback lifecycle. |
| Events and KPIs | `GET /api/events`, `/api/events.ndjson`, `/api/kpis`, `/api/audit-logs` | Event and audit cursor pagination, time ranges, acknowledgement/case state, and retention controls remain absent. |
| DNSBL | `GET/POST /api/dnsbl`, `GET/PUT/DELETE /api/dnsbl/{address}`, `GET /dnsbl/zone` | Serial/conditional zone transfer contracts remain absent. |
| Threat intelligence | `GET /api/threats`, `GET /api/threat-feeds`, `/freshness`; import endpoints for generic feeds, phishing-database, STIX, MISP, TAXII, and OpenCTI | Individual indicator/feed lifecycle and import idempotency keys remain absent. |
| APIM and load balancing | Route CRUD and prefix-based upstream proxying | No upstream pool/member, health-check, retry/circuit-breaker, API consumer, quota, or API-key lifecycle. These need persisted models before endpoints. |
| Credentials and config | Admin token RBAC; integration config status views | No credential registry CRUD/rotation metadata API. Secret values must never be returned. Operational config still lacks a durable KV model. |
| Durability and audit | JSON snapshot persistence and mutation audit rows | No immutable remote audit sink, tenant boundary, or general optimistic-concurrency revision. Route items now use ETag/If-Match. |

## Route resource contract

Legacy `POST /api/routes` remains an upsert and keeps its existing response shape/status.
New clients should use the item resource:

- `GET /api/routes/{route_id}` returns the route and an `ETag` header.
- `PUT /api/routes/{route_id}` creates a missing route. Replacing an existing route requires
`If-Match` with the latest ETag (`428` when absent, `412` when stale).
- `DELETE /api/routes/{route_id}` requires `If-Match` and returns `204`.
- Writes require a write-capable admin principal. An authenticated read-only principal gets
`403`; a missing or invalid credential gets `401`. Successful replace/delete operations are
persisted and audited.

The machine-readable contract is [openapi.yaml](openapi.yaml).

## DNSBL resource contract

- `GET /api/dnsbl/{address}` returns one IPv4 or IPv6 entry and an `ETag` header.
- `PUT /api/dnsbl/{address}` creates a missing entry. Replacing an existing entry requires
`If-Match`; the path and body addresses must match.
- `DELETE /api/dnsbl/{address}` requires `If-Match` and returns `204`.
- Writes use the same write-capable admin, persistence, rollback, and audit contract as routes.
169 changes: 169 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
openapi: 3.1.0
info:
title: Wardnet operator API
version: 0.1.0
description: Operator contract for the route lifecycle. Legacy collection POST remains supported.
paths:
/api/routes:
get:
summary: List gateway routes
responses:
'200':
description: Route collection
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Route' }
post:
summary: Legacy route upsert
security: [{ AdminToken: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/Route' }
responses:
'201': { description: Route upserted }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
Comment thread
seonghobae marked this conversation as resolved.
'403': { $ref: '#/components/responses/Forbidden' }
/api/routes/{route_id}:
parameters:
- name: route_id
in: path
required: true
schema: { type: string }
get:
summary: Get one route and its concurrency token
responses:
'200':
description: Route
headers:
ETag: { schema: { type: string } }
content:
application/json:
schema: { $ref: '#/components/schemas/Route' }
'404': { $ref: '#/components/responses/NotFound' }
put:
summary: Create or conditionally replace one route
security: [{ AdminToken: [] }]
parameters:
- name: If-Match
in: header
required: false
description: Required when the route already exists.
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/Route' }
responses:
'200': { description: Route replaced }
'201': { description: Route created }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'412': { description: ETag does not match }
'428': { description: If-Match required for an existing route }
delete:
summary: Conditionally delete one route
security: [{ AdminToken: [] }]
parameters:
- name: If-Match
in: header
required: false
description: Required for every delete; a missing value returns 428.
schema: { type: string }
responses:
'204': { description: Route deleted }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'412': { description: ETag does not match or route does not exist }
'428': { description: If-Match required }
/api/dnsbl/{address}:
parameters:
- name: address
in: path
required: true
schema:
oneOf:
- { type: string, format: ipv4 }
- { type: string, format: ipv6 }
get:
summary: Get one DNSBL entry and its concurrency token
responses:
'200':
description: DNSBL entry
headers:
ETag: { schema: { type: string } }
'400': { $ref: '#/components/responses/BadRequest' }
'404': { $ref: '#/components/responses/NotFound' }
Comment thread
seonghobae marked this conversation as resolved.
put:
summary: Create or conditionally replace one DNSBL entry
security: [{ AdminToken: [] }]
parameters:
- name: If-Match
in: header
required: false
schema: { type: string }
responses:
'200': { description: DNSBL entry replaced }
'201': { description: DNSBL entry created }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'412': { description: ETag does not match }
'428': { description: If-Match required for an existing entry }
delete:
summary: Conditionally delete one DNSBL entry
security: [{ AdminToken: [] }]
parameters:
- name: If-Match
in: header
required: false
description: Required for every delete; a missing value returns 428.
schema: { type: string }
responses:
'204': { description: DNSBL entry deleted }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'412': { description: ETag does not match or entry does not exist }
'428': { description: If-Match required }
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
components:
securitySchemes:
AdminToken:
type: apiKey
in: header
name: X-Admin-Token
schemas:
Route:
type: object
additionalProperties: false
required: [id, path_prefix, upstream, mode, enabled]
properties:
id: { type: string, minLength: 1 }
path_prefix: { type: string, pattern: '^/' }
upstream: { type: string }
mode: { type: string, enum: [monitor, block] }
enabled: { type: boolean }
block_threshold: { type: [integer, 'null'], minimum: 1 }
Error:
type: object
required: [error]
properties:
error: { type: string }
responses:
BadRequest:
description: Invalid route or path/body identity mismatch
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
Unauthorized:
description: Missing or invalid admin credential
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
Forbidden:
description: Authenticated principal is read-only
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
NotFound:
description: Route not found
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
Loading
Loading