ENISA Reporting — REST API & Swagger
The ENISA SRP module exposes a versioned OpenAPI 3.1 REST API with a bundled
Swagger UI, all under /api/v1/enisa. The OpenAPI document and the routes are
generated from the same handlers (via utoipa), so they never drift.
Endpoints
Section titled “Endpoints”| Path | Method | Auth | Purpose |
|---|---|---|---|
/api/v1/enisa/notifications |
POST | ✅ | Create a notification case (vulnerability or incident) |
/api/v1/enisa/notifications |
GET | ✅ | List cases |
/api/v1/enisa/notifications/{id} |
GET | ✅ | Case with full revision history |
/api/v1/enisa/notifications/{id}/revisions |
POST | ✅ | Submit a staged revision (validated) |
/api/v1/enisa/notifications/{id}/revisions |
GET | ✅ | Revision history |
/api/v1/enisa/obligations |
GET | ✅ | The versioned obligation matrix |
/api/v1/enisa/openapi.json |
GET | — | OpenAPI 3.1 document |
/api/v1/enisa/swagger-ui |
GET | — | Swagger UI |
/api/v1/enisa/graphql |
POST | ✅ | GraphQL queries (see GraphQL) |
/api/v1/enisa/graphql/playground |
GET | — | GraphQL playground |
The documentation surface (Swagger UI, OpenAPI doc, GraphQL playground) is public so it loads in a browser; the data surface requires a FLEET API key.
Authentication, scopes & the reporter identity
Section titled “Authentication, scopes & the reporter identity”Data endpoints use FLEET’s standard API-key auth:
Authorization: Bearer fleet_xxxxxxxx…Each request is checked for the appropriate scope:
| Operation | Required scope |
|---|---|
Reads (GET …, and GraphQL queries) |
enisa:read |
| Writes (create case, submit revision) | enisa:write |
A wildcard (*) key satisfies either. A valid key with the wrong scope gets
403 (Missing required scope: …); a missing/invalid key gets 401.
The full catalogue of selectable scopes (including enisa:read/enisa:write) is
available at GET /api/v1/admin/scopes (requires admin:read), or via the
fleet_list_scopes MCP tool — use it when minting keys with fleet_create_api_key
or POST /api/v1/admin/api-keys.
On success, FLEET injects the authenticated key’s name as the reporting
entity (field 6, “automated”) via the x-enisa-reporter header. This value
overrides any reporter in the request body, so the reporter cannot be
spoofed.
Lifecycle walkthrough
Section titled “Lifecycle walkthrough”1. Create a case
Section titled “1. Create a case”curl -X POST https://your-fleet/api/v1/enisa/notifications \ -H "Authorization: Bearer $FLEET_KEY" -H "content-type: application/json" \ -d '{"notification_type":"vulnerability"}'# → { "id": "…", "notification_type": "vulnerability", "reporter": "…", … }2. Submit the 24h early warning
Section titled “2. Submit the 24h early warning”Only the obligatory fields for this stage are required (see Obligation Matrix).
curl -X POST https://your-fleet/api/v1/enisa/notifications/$ID/revisions \ -H "Authorization: Bearer $FLEET_KEY" -H "content-type: application/json" \ -d '{ "stage": "early_warning_24h", "manufacturer_name": "ACME Corp", "product": "Widget Server", "title": "Actively exploited RCE in config parser", "cve_id": "CVE-2026-0001", "member_states": ["DE", "FR"] }'3. Submit the 72h update
Section titled “3. Submit the 72h update”Fields from the 24h warning (title, CVE, manufacturer…) are carried forward automatically — you only send what’s new.
curl -X POST https://your-fleet/api/v1/enisa/notifications/$ID/revisions \ -H "Authorization: Bearer $FLEET_KEY" -H "content-type: application/json" \ -d '{ "stage": "update_72h", "vuln_nature": "Heap overflow in the configuration parser", "exploit_nature": "Remote, unauthenticated, via crafted config", "vuln_corrective_taken": "Hotfix 1.2.3 released", "vuln_user_measures": "Upgrade to 1.2.3; restrict config sources" }'4. Submit the final report
Section titled “4. Submit the final report”curl -X POST https://your-fleet/api/v1/enisa/notifications/$ID/revisions \ -H "Authorization: Bearer $FLEET_KEY" -H "content-type: application/json" \ -d '{ "stage": "final", "vuln_measure_available_date": "2026-06-10", "vuln_severity": "Critical (CVSS 9.8)", "vuln_impact": "Full host compromise", "vuln_security_update_details": "Fixed in 1.2.3; backported to 1.1.9" }'Stages must not go backwards: submitting an earlier stage than the latest
recorded one returns 409 STAGE_REGRESSION.
Validation errors
Section titled “Validation errors”Required-ness is stage-specific. A missing obligatory field returns 422 with a
structured list of violations:
{ "error": { "code": "VALIDATION_FAILED", "message": "one or more fields are invalid for this stage", "violations": [ { "field_id": "12", "stage": "early_warning_24h", "kind": "missing_obligatory", "message": "field 12 is required at this stage" } ] }}Submitting a field that belongs to the other notification type (e.g. incident
data on a vulnerability case) yields a not_applicable violation.
Error codes
Section titled “Error codes”| Status | code |
When |
|---|---|---|
| 404 | NOT_FOUND |
Unknown notification id |
| 409 | STAGE_REGRESSION |
New stage is earlier than the latest recorded stage |
| 422 | VALIDATION_FAILED |
Obligatory field missing, or field not applicable to the type |
Drafts (read / edit / promote)
Section titled “Drafts (read / edit / promote)”Submitted revisions are immutable, so editing happens on a mutable draft — a working copy of the report’s fields you change freely, then promote into an immutable 24h/72h/final revision.
| Path | Method | Purpose |
|---|---|---|
/notifications/{id}/draft |
POST | Create a draft (?seed=true pre-fills from the latest revision) |
/notifications/{id}/draft |
GET | Read the current draft |
/notifications/{id}/draft |
PATCH | Merge field updates (null clears a field) |
/notifications/{id}/draft |
DELETE | Discard the draft |
/notifications/{id}/draft/validate |
POST | Check readiness ({valid, violations}) without promoting |
/notifications/{id}/draft/promote |
POST | Validate + write an immutable revision, then clear the draft |
curl -X POST .../notifications/$ID/draft -H "Authorization: Bearer $K"curl -X PATCH .../notifications/$ID/draft -H "Authorization: Bearer $K" \ -H 'content-type: application/json' \ -d '{"stage":"early_warning_24h","manufacturer_name":"ACME","product":"Widget","title":"RCE"}'curl -X POST .../notifications/$ID/draft/validate -H "Authorization: Bearer $K" # {"valid":true,...}curl -X POST .../notifications/$ID/draft/promote -H "Authorization: Bearer $K" # → immutable revisionThe same flow is available over MCP (fleet_enisa_get_draft,
fleet_enisa_edit_draft, fleet_enisa_promote_draft).
Try it locally
Section titled “Try it locally”Run the module standalone (no auth) for exploration:
DATABASE_URL=postgres://you@localhost/fleet_test \ cargo run -p enisa-srp --example serve# http://127.0.0.1:8099/swagger-ui · /openapi.json