{
  "openapi": "3.1.0",
  "info": {
    "title": "ThreatRecall API",
    "description": "CTI memory system for SOC teams — natural language recall, knowledge graph, STIX 2.1 export, and structured ingest.\n\n## Base URL\n```\nhttps://app.threatrecall.ai\n```\n\n## Authentication\n\nAuthenticated endpoints require a Bearer token in the `Authorization` header:\n\n```\nAuthorization: Bearer <token>\n```\n\nTokens are workspace-scoped JWTs obtained via [POST /api/auth/login](#tag/auth/operation/login)\nwith valid credentials, or via Google/GitHub OAuth. Tokens expire after 24 hours.\n\n## TLP Enforcement\n\nThreatRecall enforces Traffic Light Protocol (TLP) marking at the row-level (SQL WHERE clause).\nTLP:AMBER+ and TLP:RED nodes are **never returned** to unauthenticated callers or to workspaces\nwithout explicit RED export grants. STIX bundles include TLP marking definitions per STIX 2.1 spec.\n\n## Rate Limits\n\n| Endpoint group | Limit |\n|---|---|\n| `/openapi.json` | 60 req/min per IP |\n| `/api/public/demo-*` | 10 req/hour, 200 req/day per IP |\n| Authenticated API | Tier-based (100–5000 req/hour) |\n\n## STIX 2.1 Export\n\nSTIX bundles are validated against the STIX 2.1 meta-schema (fail-closed, HTTP 422 on validation failure).\nBundles include object marking refs for TLP markings and confidence scores scaled 0–100.\n",
    "version": "1.0.0",
    "contact": {
      "email": "security@threatengram.com"
    }
  },
  "servers": [
    {
      "url": "https://app.threatrecall.ai",
      "description": "Production"
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "summary": "Public health check",
        "description": "Lightweight health check for load balancers. Does not query the database.",
        "operationId": "getHealth",
        "tags": ["Health"],
        "security": [],
        "responses": {
          "200": {
            "description": "Service is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "healthy" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "summary": "API health check",
        "description": "Returns API service identity and version. No auth required.",
        "operationId": "getApiHealth",
        "tags": ["Health"],
        "security": [],
        "responses": {
          "200": {
            "description": "API is operational",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "ok" },
                    "service": { "type": "string", "example": "threatrecall" },
                    "version": { "type": "string", "example": "1.0.0" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/public/demo-recall": {
      "post": {
        "summary": "Public demo recall (no auth)",
        "description": "Keyword search against the live demo workspace (APT29, FIN7, Lazarus campaign pack). **No auth required.** Only TLP:WHITE and TLP:GREEN nodes are returned. PII patterns (email, SSN, phone, credit card) are rejected before search. Rate limited: 10 req/hour, 200 req/day per IP.",
        "operationId": "publicDemoRecall",
        "tags": ["Recall (Public)"],
        "security": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["query"],
                "properties": {
                  "query": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 300,
                    "description": "Natural language or keyword query. PII is rejected before search."
                  }
                }
              },
              "examples": {
                "APT29": {
                  "summary": "Search APT29 indicators",
                  "value": { "query": "APT29 Spear Phone" }
                },
                "FIN7": {
                  "summary": "Search FIN7 campaign",
                  "value": { "query": "FIN7 CARBANAK restaurant" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Demo recall results (TLP:WHITE/GREEN only)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/DemoRecallResponse" }
              }
            }
          },
          "400": {
            "description": "Missing query or PII detected",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/public/demo-export.stix": {
      "get": {
        "summary": "Public STIX 2.1 export of demo recall (no auth)",
        "description": "Searches the demo workspace and returns a STIX 2.1 bundle of matching nodes. **No auth required.** Only TLP:WHITE and TLP:GREEN nodes are included. TLP marking refs are applied per STIX 2.1 spec. Rate limited: 10 req/hour, 200 req/day per IP.",
        "operationId": "publicDemoStixExport",
        "tags": ["STIX Export (Public)"],
        "security": [],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Search query",
            "schema": { "type": "string", "minLength": 1, "maxLength": 300 }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum results (default 20, max 100)",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
          }
        ],
        "responses": {
          "200": {
            "description": "STIX 2.1 bundle — Content-Type: application/stix+json;version=2.1",
            "content": {
              "application/stix+json": {
                "schema": { "$ref": "#/components/schemas/StixBundle" }
              }
            }
          },
          "400": { "description": "Missing q parameter", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "No results for this query", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "STIX bundle validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/recall/search": {
      "get": {
        "summary": "Search the knowledge graph (auth required)",
        "description": "Natural language or structured query against the tenant's knowledge graph. Uses blended search (keyword + embedding hybrid) by default; pass `mode=structured` for structured extraction. Results include a `recall_id` for STIX export and TLP scope (`max_tlp`).\n\n**TLP enforcement:** results are filtered to the caller's workspace TLP ceiling. TLP:AMBER+ rows are excluded unless the workspace has elevated clearance.",
        "operationId": "recallSearch",
        "tags": ["Recall"],
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Search query",
            "schema": { "type": "string", "minLength": 1 }
          },
          {
            "name": "mode",
            "in": "query",
            "description": "Search mode: `blended` (default, keyword+embedding hybrid) or `structured` (LLM extraction + DB match)",
            "schema": { "type": "string", "enum": ["blended", "structured"], "default": "blended" }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max results (default 20, max 100)",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
          },
          {
            "name": "include_rejected",
            "in": "query",
            "description": "Include nodes with status=rejected (admin/analyst role only)",
            "schema": { "type": "boolean", "default": false }
          }
        ],
        "responses": {
          "200": {
            "description": "Recall results",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RecallSearchResponse" }
              }
            }
          },
          "400": { "description": "Missing q parameter", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Insufficient permissions (recall:read required)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/recall/sessions/{id}": {
      "get": {
        "summary": "Get recall session metadata",
        "description": "Returns the metadata for a prior recall session. Use the `recall_id` from a prior `/api/recall/search` response. Use `node_ids` from this response to call `/api/recall/sessions/{id}/export.stix`.",
        "operationId": "getRecallSession",
        "tags": ["Recall"],
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Recall session ID (UUID)",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Recall session metadata",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RecallSession" }
              }
            }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Session not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/recall/sessions/{id}/export.stix": {
      "get": {
        "summary": "Export recall session as STIX 2.1 bundle (auth required)",
        "description": "Downloads the recall session as a STIX 2.1 JSON bundle. Validated against the STIX 2.1 meta-schema (fail-closed: HTTP 422 on validation failure).\n\n**TLP:RED enforcement:** If the session contains TLP:RED nodes, the workspace must have `red_export_grant=true` set. Without it, this returns HTTP 403 and logs a denied event.",
        "operationId": "exportRecallSessionStix",
        "tags": ["STIX Export"],
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Recall session ID",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "STIX 2.1 bundle — Content-Type: application/stix+json;version=2.1",
            "content": {
              "application/stix+json": {
                "schema": { "$ref": "#/components/schemas/StixBundle" }
              }
            }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": {
            "description": "TLP:RED export denied — workspace requires red_export_grant",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "404": { "description": "Session not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "STIX bundle validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/recall/semantic": {
      "get": {
        "summary": "Semantic (vector-only) search",
        "description": "Pure embedding-based search. No keyword fallback. Use when you want precise vector similarity and accept empty results.",
        "operationId": "recallSemantic",
        "tags": ["Recall"],
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "q", "in": "query", "required": true, "description": "Search query", "schema": { "type": "string" } },
          { "name": "limit", "in": "query", "description": "Max results (default 20, max 100)", "schema": { "type": "integer", "default": 20, "minimum": 1, "maximum": 100 } },
          { "name": "include_rejected", "in": "query", "description": "Include rejected nodes", "schema": { "type": "boolean", "default": false } }
        ],
        "responses": {
          "200": {
            "description": "Semantic search results",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "results": { "type": "array", "items": { "$ref": "#/components/schemas/KgNode" } }, "count": { "type": "integer" } } } } }
          },
          "400": { "description": "Missing q", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/ingest/stix": {
      "post": {
        "summary": "Ingest STIX 2.1 bundle",
        "description": "Parses a STIX 2.1 bundle (or array of STIX objects) and creates kg_nodes with linked evidence records. Supported object types: `indicator`, `malware`, `threat-actor`, `campaign`, `vulnerability`, `attack-pattern`, `course-of-action`, `intrusion-set`.\n\n**TLP handling:** TLP is extracted from `x_mitre_tlp` custom property first, then from object `labels` (TLP:RED/AMBER/GREEN), then from `default_tlp` body parameter (default: TLP:GREEN).\n\n**Idempotency:** Nodes are deduplicated by `stix_id` (the STIX object ID). Re-submitting the same bundle is safe — no duplicate nodes are created.",
        "operationId": "ingestStix",
        "tags": ["Ingest"],
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["bundle"],
                "properties": {
                  "bundle": {
                    "type": "object",
                    "description": "STIX 2.1 bundle JSON (requires `id`, `type: bundle`, `objects[]`)",
                    "properties": {
                      "id": { "type": "string" },
                      "type": { "type": "string", "const": "bundle" },
                      "objects": { "type": "array", "items": { "type": "object" } }
                    }
                  },
                  "default_tlp": {
                    "type": "string",
                    "enum": ["TLP:RED", "TLP:AMBER+PK", "TLP:AMBER", "TLP:GREEN", "TLP:WHITE"],
                    "default": "TLP:GREEN",
                    "description": "Applied to objects with no TLP marking"
                  },
                  "default_confidence": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 1,
                    "default": 0.7,
                    "description": "Default confidence (0-1 scale) for objects without a confidence field"
                  }
                }
              },
              "example": {
                "bundle": {
                  "id": "bundle--4bd9e3a0-1234-5678-9abc-def012345678",
                  "type": "bundle",
                  "spec_version": "2.1",
                  "objects": [
                    {
                      "type": "malware",
                      "spec_version": "2.1",
                      "id": "malware--a3c2b1d0-1234-5678-9abc-def012345678",
                      "name": "CozyCar",
                      "description": "Custom malware used by APT29",
                      "x_mitre_tlp": "GREEN",
                      "confidence": 85,
                      "created": "2024-01-15T00:00:00Z",
                      "external_references": [{ "source_name": "ThreatRecall", "description": "APT29 tool" }]
                    }
                  ]
                },
                "default_tlp": "TLP:GREEN",
                "default_confidence": 0.7
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Ingest complete",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "results": {
                      "type": "object",
                      "properties": {
                        "nodes_created": { "type": "integer", "example": 1 },
                        "evidence_created": { "type": "integer", "example": 1 },
                        "skipped": { "type": "integer", "example": 7 }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Missing bundle", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Insufficient permissions or feature not available", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "STIX ingest failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/ingest/osint": {
      "post": {
        "summary": "Ingest OSINT collector feed",
        "description": "Bulk-import structured OSINT items from threat intelligence collectors. Each item becomes an evidence-linked node in the knowledge graph.\n\nNode type is inferred from item keywords if not explicitly set: vulnerability (CVE/ransomware/exploit), actor (APT/threat-actors), attck (mitre/att&ck).",
        "operationId": "ingestOsint",
        "tags": ["Ingest"],
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["collector", "items"],
                "properties": {
                  "collector": { "type": "string", "description": "Name of the OSINT collector source" },
                  "items": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "properties": {
                        "title": { "type": "string" },
                        "description": { "type": "string" },
                        "url": { "type": "string" },
                        "type": { "type": "string", "enum": ["actor", "cve", "ioc", "attck", "vulnerability", "osint", "tool"] },
                        "tlp": { "type": "string", "enum": ["TLP:RED", "TLP:AMBER+PK", "TLP:AMBER", "TLP:GREEN", "TLP:WHITE"] },
                        "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
                        "tags": { "type": "array", "items": { "type": "string" } },
                        "published": { "type": "string", "format": "date-time" }
                      },
                      "required": ["title"]
                    }
                  },
                  "default_tlp": { "type": "string", "enum": ["TLP:RED", "TLP:AMBER+PK", "TLP:AMBER", "TLP:GREEN", "TLP:WHITE"], "default": "TLP:AMBER" },
                  "default_confidence": { "type": "number", "default": 0.5 }
                }
              },
              "example": {
                "collector": "alienvault-otx",
                "items": [{ "title": "APT29 DNS tunneling indicator", "description": "Malicious DNS resolution pattern", "url": "https://otx.alienvault.com/pulse/abc123", "type": "ioc", "tlp": "TLP:GREEN", "tags": ["apt29", "dns"] }],
                "default_tlp": "TLP:AMBER",
                "default_confidence": 0.5
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OSINT ingest complete",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "results": { "type": "object", "properties": { "nodes_created": { "type": "integer" }, "evidence_created": { "type": "integer" } } } } } } }
          },
          "400": { "description": "Missing collector or items", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/ingest/misp": {
      "post": {
        "summary": "Ingest MISP JSON event export",
        "description": "Parses a MISP event JSON (exported from MISP platform) and imports attributes as IOC nodes. Event-level evidence is created and linked to all attribute nodes.\n\nTLP mapping: MISP distribution 0 → TLP:RED, distribution 1 → TLP:AMBER, others use `default_tlp`.",
        "operationId": "ingestMisp",
        "tags": ["Ingest"],
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["event"],
                "properties": {
                  "event": {
                    "type": "object",
                    "description": "MISP event JSON (full export format, must contain `uuid` and `Attribute[]`)",
                    "properties": {
                      "uuid": { "type": "string" },
                      "Attribute": { "type": "array", "items": { "type": "object" } }
                    }
                  },
                  "default_tlp": { "type": "string", "enum": ["TLP:RED", "TLP:AMBER+PK", "TLP:AMBER", "TLP:GREEN", "TLP:WHITE"], "default": "TLP:AMBER" },
                  "default_confidence": { "type": "number", "default": 0.6 }
                }
              },
              "example": {
                "event": { "uuid": "abc123", "date": "2024-03-15", "Attribute": [{ "uuid": "def456", "type": "md5", "value": "d41d8cd98f00b204e9800998ecf8427e", "to_ids": true, "category": "Payload delivery" }] },
                "default_tlp": "TLP:AMBER",
                "default_confidence": 0.6
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "MISP ingest complete",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "results": { "type": "object", "properties": { "nodes_created": { "type": "integer" }, "evidence_created": { "type": "integer" } } } } } } }
          },
          "400": { "description": "Missing event", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/ingest/analyst": {
      "post": {
        "summary": "Ingest analyst notes / manual CTI entry",
        "description": "Create nodes directly from analyst notes. Use this for manual CTI entry, analyst assessments, or data that doesn't fit STIX/OSINT/MISP formats.",
        "operationId": "ingestAnalyst",
        "tags": ["Ingest"],
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["nodes"],
                "properties": {
                  "nodes": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": ["name", "node_type"],
                      "properties": {
                        "name": { "type": "string" },
                        "node_type": { "type": "string", "enum": ["actor", "cve", "ioc", "attck", "osint", "tool", "vulnerability"] },
                        "description": { "type": "string" },
                        "tlp": { "type": "string", "enum": ["TLP:RED", "TLP:AMBER+PK", "TLP:AMBER", "TLP:GREEN", "TLP:WHITE"] },
                        "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
                        "properties": { "type": "object" }
                      }
                    }
                  },
                  "default_tlp": { "type": "string", "default": "TLP:GREEN" },
                  "default_confidence": { "type": "number", "default": 0.8 }
                }
              },
              "example": {
                "nodes": [{ "name": "OPNUM21", "node_type": "actor", "description": "Probing infrastructure observed in Jan 2025", "tlp": "TLP:AMBER", "confidence": 0.75 }],
                "default_tlp": "TLP:GREEN",
                "default_confidence": 0.8
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Analyst ingest complete",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "results": { "type": "object", "properties": { "nodes_created": { "type": "integer" }, "evidence_created": { "type": "integer" } } } } } } }
          },
          "400": { "description": "Missing or invalid nodes array", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/ingest/report": {
      "post": {
        "summary": "Ingest structured report",
        "description": "Import a parsed threat report (PDF/text processed offline, structured as JSON). Each `content_block` becomes an entity node linked to the report evidence.",
        "operationId": "ingestReport",
        "tags": ["Ingest"],
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["title", "content_blocks"],
                "properties": {
                  "title": { "type": "string" },
                  "source_name": { "type": "string" },
                  "content_blocks": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": ["entity_name", "node_type"],
                      "properties": {
                        "entity_name": { "type": "string" },
                        "node_type": { "type": "string" },
                        "description": { "type": "string" },
                        "tlp": { "type": "string" },
                        "confidence": { "type": "number" },
                        "source_url": { "type": "string" },
                        "excerpt": { "type": "string" }
                      }
                    }
                  },
                  "default_tlp": { "type": "string", "default": "TLP:GREEN" },
                  "default_confidence": { "type": "number", "default": 0.6 }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Report ingest complete",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "results": { "type": "object", "properties": { "nodes_created": { "type": "integer" }, "evidence_created": { "type": "integer" } } } } } } }
          },
          "400": { "description": "Missing title or content_blocks", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/ingest/preview": {
      "post": {
        "summary": "Preview ingest — LLM extraction without commit",
        "description": "Sends raw text to the LLM (OpenAI or Ollama) for entity extraction. Entities are staged in `ingest_staging` for review before commit. Set `ingest_mode=auto` to skip review and commit immediately.\n\nReturns batch summary and per-entity details including duplicate candidates and TLP sensitivity warnings.",
        "operationId": "ingestPreview",
        "tags": ["Ingest"],
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["text"],
                "properties": {
                  "text": { "type": "string", "minLength": 10, "description": "Raw CTI text (report snippet, analyst note, IOC list)" },
                  "source_type": { "type": "string", "default": "manual_text", "enum": ["manual_text", "analyst_report", "osint_feed", "incident_writeup", "chat"] },
                  "session_id": { "type": "string", "description": "Optional session grouping ID (for multi-part extractions)" },
                  "ingest_mode": { "type": "string", "enum": ["auto", "review"], "description": "auto: commits immediately. review (default): stages for review." },
                  "default_tlp": { "type": "string" },
                  "default_confidence": { "type": "number" }
                }
              },
              "example": { "text": "APT29 used Cobalt Strike beacons with C2 domains pointing to 198.51.100.42. The malware was delivered via phishing with a macro-enabled Word document.", "ingest_mode": "review" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "LLM extraction complete — staged entities returned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "batch_id": { "type": "string" },
                    "summary": {
                      "type": "object",
                      "properties": {
                        "total_parsed": { "type": "integer" },
                        "total_to_add": { "type": "integer" },
                        "total_to_update": { "type": "integer" },
                        "duplicates": { "type": "integer" },
                        "missing_tlp": { "type": "integer" },
                        "sensitive_warns": { "type": "integer" }
                      }
                    },
                    "entities": { "type": "array", "items": { "$ref": "#/components/schemas/StagedEntity" } },
                    "auto_ingest": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "400": { "description": "Text too short (< 10 chars)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/ingest/batch/{id}": {
      "get": {
        "summary": "Get ingest batch metadata and staged entities",
        "description": "Returns the batch metadata and all staged entities for review. Use after calling `/api/ingest/preview` with `ingest_mode=review`.",
        "operationId": "getIngestBatch",
        "tags": ["Ingest"],
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Batch metadata + staged entities",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "batch": { "type": "object" }, "entities": { "type": "array", "items": { "$ref": "#/components/schemas/StagedEntity" } } } } } }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Batch not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/ingest/batch/{id}/commit": {
      "post": {
        "summary": "Commit ingest batch — move staged entities to live tables",
        "description": "Atomically commits all `accepted` staging records to `kg_nodes` and `evidence_records`. `rejected` records are discarded. Creates audit log entries for the commit.",
        "operationId": "commitIngestBatch",
        "tags": ["Ingest"],
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Batch committed",
            "content": { "application/json": { "schema": { "type": "object" } } }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Commit failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/ingest/staging/{id}": {
      "patch": {
        "summary": "Update a single staging entity",
        "description": "Accept, reject, merge, or mark uncertain a single staged entity. Set `status=merged` and `merge_target_id` to redirect this entity to an existing node.",
        "operationId": "patchStagingEntity",
        "tags": ["Ingest"],
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["status"],
                "properties": {
                  "status": { "type": "string", "enum": ["pending", "accepted", "rejected", "merged", "uncertain"] },
                  "resolved_tlp": { "type": "string" },
                  "merge_target_id": { "type": "string", "format": "uuid" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Entity updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "entity": { "type": "object" } } } } } },
          "400": { "description": "Invalid status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Staging record not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/audit/export": {
      "get": {
        "summary": "Export audit logs as CSV (admin/audit role)",
        "description": "Exports audit log entries as a CSV file. Requires `audit:export` permission (admin or audit role). Time range is filtered by `from` and `to` query params.",
        "operationId": "exportAuditLogs",
        "tags": ["Audit"],
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "from", "in": "query", "description": "ISO 8601 start time", "schema": { "type": "string", "format": "date-time" } },
          { "name": "to", "in": "query", "description": "ISO 8601 end time", "schema": { "type": "string", "format": "date-time" } },
          { "name": "limit", "in": "query", "description": "Max records (default 1000, max 5000)", "schema": { "type": "integer", "minimum": 1, "maximum": 5000, "default": 1000 } }
        ],
        "responses": {
          "200": {
            "description": "CSV file download",
            "content": {
              "text/csv": {
                "schema": { "type": "string", "format": "binary" },
                "example": "id,tenant_id,user_id,user_email,action,resource,resource_id,outcome,ip_address,metadata,created_at\n\"abc123\",\"tenant-uuid\",\"user-uuid\",\"[user_email]\",\"recall.search\",\"recall\",\"\",\"success\",\"192.168.1.1\",\"{}\",\"2024-01-15T12:00:00Z\""
              }
            }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "audit:export permission required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/audit/logs": {
      "get": {
        "summary": "Read audit logs (audit:read permission)",
        "description": "Returns audit log entries with optional filtering by user, time range, and pagination. Write-once enforced at DB trigger level — UPDATE/DELETE blocked.",
        "operationId": "getAuditLogs",
        "tags": ["Audit"],
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "user_id", "in": "query", "description": "Filter by user ID", "schema": { "type": "string", "format": "uuid" } },
          { "name": "from", "in": "query", "schema": { "type": "string", "format": "date-time" } },
          { "name": "to", "in": "query", "schema": { "type": "string", "format": "date-time" } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "default": 100 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "default": 0 } }
        ],
        "responses": {
          "200": {
            "description": "Audit log entries",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "logs": { "type": "array", "items": { "type": "object" } }, "count": { "type": "integer" } } } } }
          },
          "401": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "audit:read permission required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/pilot": {
      "post": {
        "summary": "Submit Design Partner Pilot application (public)",
        "description": "Public form endpoint — no auth required. Creates a `pilot_requests` record and notifies the founder via email. Rate limited: 5 requests/minute per IP.",
        "operationId": "submitPilotRequest",
        "tags": ["Pilot"],
        "security": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "email", "org", "role"],
                "properties": {
                  "name": { "type": "string", "maxLength": 200 },
                  "email": { "type": "string", "format": "email" },
                  "org": { "type": "string", "maxLength": 200 },
                  "role": { "type": "string", "enum": ["cti-analyst", "soc-manager", "threat-intel-lead", "security-engineer", "ciso", "other"] },
                  "cti_stack": { "type": "string", "maxLength": 500 }
                }
              },
              "example": { "name": "Alex Chen", "email": "alex.chen@contoso.com", "org": "Contoso Security", "role": "threat-intel-lead", "cti_stack": "OpenCTI, Splunk, MISP" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Application submitted",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } }
          },
          "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Workspace-scoped JWT obtained via POST /api/auth/login. Tokens expire after 24 hours."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "details": { "type": "string" }
        },
        "required": ["error"]
      },
      "DemoRecallResponse": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "description": "Evidence contract array — TLP:WHITE/GREEN only",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "node_type": { "type": "string" },
                "name": { "type": "string" },
                "description": { "type": "string" },
                "source": { "type": "string" },
                "tlp": { "type": "string" },
                "confidence": { "type": "number" },
                "properties": { "type": "object" },
                "linked_evidence_ids": { "type": "array", "items": { "type": "string" } }
              }
            }
          },
          "seeded": { "type": "boolean", "description": "True if demo data is loaded" },
          "query": { "type": "string" },
          "result_count": { "type": "integer" },
          "latency_ms": { "type": "integer" }
        }
      },
      "RecallSearchResponse": {
        "type": "object",
        "properties": {
          "results": { "type": "array", "items": { "$ref": "#/components/schemas/KgNode" } },
          "count": { "type": "integer" },
          "query": { "type": "string" },
          "mode": { "type": "string", "enum": ["blended", "structured"] },
          "include_rejected": { "type": "boolean" },
          "recall_id": { "type": "string", "description": "Use this ID to call /api/recall/sessions/{id}/export.stix" },
          "max_tlp": { "type": "string", "description": "Highest TLP in results (controls export eligibility)" }
        }
      },
      "RecallSession": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "query": { "type": "string" },
          "mode": { "type": "string" },
          "result_count": { "type": "integer" },
          "max_tlp": { "type": "string" },
          "has_red_node": { "type": "boolean" },
          "node_ids": { "type": "array", "items": { "type": "string", "format": "uuid" } },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "KgNode": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "node_type": { "type": "string", "enum": ["actor", "cve", "ioc", "attck", "osint", "tool", "vulnerability"] },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "source": { "type": "string" },
          "source_type": { "type": "string" },
          "tlp": { "type": "string" },
          "confidence": { "type": "number" },
          "properties": { "type": "object" },
          "ingested_at": { "type": "string", "format": "date-time" },
          "linked_evidence_ids": { "type": "array", "items": { "type": "string" } },
          "status": { "type": "string", "enum": ["active", "rejected", "merged"] }
        }
      },
      "StixBundle": {
        "type": "object",
        "description": "STIX 2.1 bundle — validated against meta-schema, fail-closed on HTTP 422",
        "properties": {
          "type": { "type": "string", "const": "bundle" },
          "id": { "type": "string", "pattern": "^bundle--" },
          "spec_version": { "type": "string", "const": "2.1" },
          "objects": { "type": "array", "minItems": 1 }
        },
        "required": ["type", "id", "spec_version", "objects"]
      },
      "StagedEntity": {
        "type": "object",
        "description": "LLM-extracted entity from /api/ingest/preview — not yet committed to kg_nodes",
        "properties": {
          "id": { "type": "string" },
          "status": { "type": "string", "enum": ["pending", "accepted", "rejected", "merged", "uncertain"] },
          "node_type": { "type": "string" },
          "name": { "type": "string" },
          "proposed_tlp": { "type": "string" },
          "proposed_confidence": { "type": "number" },
          "duplicate_candidates": { "type": "array", "items": { "type": "object" } },
          "warnings": { "type": "array", "items": { "type": "string" } },
          "extraction_reasoning": { "type": "string" }
        }
      }
    }
  },
  "tags": [
    { "name": "Health", "description": "Health check endpoints" },
    { "name": "Recall (Public)", "description": "No-auth demo recall — TLP:WHITE/GREEN only" },
    { "name": "STIX Export (Public)", "description": "No-auth STIX 2.1 export — TLP:WHITE/GREEN only" },
    { "name": "Recall", "description": "Authenticated recall search, sessions, and STIX export" },
    { "name": "STIX Export", "description": "Authenticated STIX 2.1 bundle export with TLP enforcement" },
    { "name": "Ingest", "description": "STIX, OSINT, MISP, analyst, and report ingest. Auth required. Requires recall:write permission and knowledge_graph feature." },
    { "name": "Audit", "description": "Audit log read and CSV export. Admin/audit role required." },
    { "name": "Pilot", "description": "Design Partner Pilot intake form — public, rate-limited" }
  ]
}
