OpenCTI compatibility inventory¶
This document is the Phase 0 compatibility baseline for replacing the Elasticsearch/OpenSearch dependency used by OpenCTI. It is an inventory and a parity contract, not a provider implementation.
Pinned reference¶
The inventory is reproducible against:
- OpenCTI
7.260722.0, commite41adc1c3fd98a849602db33dbe550f689fe6d83; - Elasticsearch
8.19.18; - OpenSearch
3.7.0.
The exact repositories, images and source locations are in
compatibility/opencti/7.260722.0/source-lock.json. The OpenCTI versions of
Elasticsearch and OpenSearch come from its development Compose and Dockerfile,
not from an inferred compatibility range.
What exists in Corrobore today¶
The current code already provides useful foundations:
graph-coreowns typed nodes, relationships, versions, bitemporal facts, snapshots and a bounded working-set model;graph-storagecontains append-only records, catalog rebuild, atomic mutation, WAL, checkpoint, recovery and compaction primitives;- the embedded engine and HTTP server expose policy-checked Cypher reads and writes;
- the HTTP server can load and persist a graph through an
EnginePersistenceadapter and exposes a STIX import endpoint.
The remaining gap to OpenCTI parity is now concentrated outside core reads:
- the versioned
KnowledgeDataEngineimplements fundamental point, list, count, cursor, neighbor, traversal, and subgraph operations; - the persistent standalone server resolves OpenCTI identifiers, types, scalar and temporal predicates, and bounded graph adjacency without scanning all hydrated records;
- mapped OpenCTI access metadata is enforced during candidate selection on the supported read surface, including complete relationship paths and policy-bound cursors and caches;
- full-text search, nested boolean planning, aggregation, file extraction, write parity, and production routing remain later delivery work.
See OpenCTI core reads for the delivered semantics and explicit limits, and OpenCTI query authorization for the policy, non-inference and invalidation contract.
Machine-readable bundle¶
The bundle under compatibility/opencti/7.260722.0/ contains:
| File | Purpose |
|---|---|
source-lock.json |
Exact upstream and engine versions |
operations.json |
Logical operation classes and their PRD, criticality, security, ordering, lifecycle and contract metadata |
knowledge-data-engine-mapping.json |
Exhaustive mapping from the 32 logical operations to the versioned provider contract or an explicit unsupported boundary |
catalogue.json |
612 production callsites from 183 OpenCTI source files |
parity-corpus.json |
Fully synthetic objects, relations, access controls, files and lifecycle fixtures |
reference-results.json |
Canonical expected IDs, properties, ordering, cursors, aggregations, authorization and errors |
decisions.json |
Accepted Phase 0 architecture decisions |
benchmark-profiles.json |
Dataset, hardware, warmup and measurement protocol |
benchmark-results.json |
Elasticsearch/OpenSearch small and medium reference measurements |
full-text-relevance.json |
Annotated synthetic queries and the MRR@10 acceptance threshold |
full-text-benchmark-results.json |
Corrobore/Tantivy small-profile latency, memory and disk measurements |
Test and fixture directories are excluded from callsite discovery. The scanner includes:
- calls to helpers imported from OpenCTI's
database/engineanddatabase/file-searchmodules; - direct client calls in those modules, including casted Elasticsearch and OpenSearch clients.
Every discovered symbol must belong to exactly one operation. A new imported helper or direct client method therefore fails generation as unmapped, while a changed line, deletion or classification fails verification as missing or stale. Each operation also names its downstream delivery issue: #39 for the provider lifecycle, #44 for core reads, #46 for full-text, #47 for advanced queries, #48 for files, #50 for transactional writes, #51 for merge/reconciliation and #52 for migrations and maintenance.
Verify or update the inventory¶
Check out the pinned OpenCTI commit, then run:
node --test scripts/opencti-compatibility.test.mjs
node scripts/opencti-compatibility.mjs verify \
--source /path/to/opencti/opencti-platform/opencti-graphql/src
When a reviewed upstream change intentionally alters the surface, regenerate and inspect the diff:
node scripts/opencti-compatibility.mjs generate \
--source /path/to/opencti/opencti-platform/opencti-graphql/src
Generation refuses unclassified symbols. Update operations.json with the PRD
mapping and contracts before generating again. CI independently checks out the
exact commit and runs both commands, so a hand-edited catalogue or changed
corpus hash cannot pass.
Corpus and capture rules¶
The corpus is fully synthetic and uses only:
example.com,example.netandexample.org;- IPv4 documentation ranges from RFC 5737;
- IPv6
2001:db8::/32; - deterministic synthetic identifiers and timestamps.
The validator recursively rejects likely non-example email addresses, public IP addresses, bearer tokens, API keys and secret-bearing fields. No production capture may be committed directly. A future capture tool must normalize a real request into this synthetic schema or keep the data outside the repository.
Canonical JSON orders object keys but preserves arrays. Arrays encode result order, bucket order, pagination and mutation sequence and must never be sorted as a formatting step.
Accepted boundaries¶
The full rationale is machine-readable in decisions.json. The main decisions
are:
- keep the provider boundary in OpenCTI and the CTI mapping outside
graph-core; - capture at the logical provider boundary;
- require read-your-writes and transaction-versioned, snapshot-consistent pagination;
- put Tantivy behind a Corrobore-owned full-text abstraction;
- isolate file extraction in a worker while Corrobore owns the index;
- use one portable snapshot artifact for local and S3/MinIO storage;
- implement only the observed aggregation subset: count, cardinality, terms, date histogram, sum, filter, nested and reverse-nested;
- use the PRD small and medium volume profiles and defer large/distributed validation.
Reference benchmark¶
The benchmark ran one engine at a time in the same Podman VM: 7 ARM64 vCPUs, 5,743,632,384 bytes of memory, no swap, XFS-backed overlay storage and a 2 GiB JVM heap. Each index used one shard, zero replicas and disabled refresh during initial ingestion. Twenty warmup iterations preceded sixty measured iterations of each workload at concurrency one.
The workloads are get by ID, a marking/tenant/type filtered list, phrase full-text search, search-after deep pagination, terms aggregation and date histogram. The aggregate latency values below combine those six equally weighted workloads.
| Engine | Profile | Documents | Ingest docs/s | P50 ms | P95 ms | P99 ms | Query ops/s | CPU | Heap | Disk |
|---|---|---|---|---|---|---|---|---|---|---|
| Elasticsearch 8.19.18 | small | 600,000 | 46,812.735 | 4.107 | 5.063 | 5.736 | 247.641 | 16.706% | 1,477,616,160 B | 55,399,040 B |
| Elasticsearch 8.19.18 | medium | 6,000,000 | 65,598.335 | 3.175 | 3.977 | 4.228 | 317.176 | 16.740% | 792,184,832 B | 524,844,463 B |
| OpenSearch 3.7.0 | small | 600,000 | 42,048.865 | 4.110 | 5.084 | 5.672 | 250.822 | 15.826% | 826,802,176 B | 54,714,118 B |
| OpenSearch 3.7.0 | medium | 6,000,000 | 67,005.110 | 3.170 | 3.792 | 4.262 | 325.277 | 23.105% | 754,974,720 B | 882,200,664 B |
The medium latency being lower than small is not treated as a scaling claim:
these are single runs after ingestion, JIT compilation and warmup. The raw
per-workload figures, timestamps and deterministic dataset-manifest hashes are
kept in benchmark-results.json.
Reproduce the full four-cell matrix with:
The script also accepts a profile and engine for a focused rerun, for example:
The medium matrix indexes twelve million documents across both engines. It is intentionally not part of pull-request CI; its committed results and methodology are validated there, while explicit benchmark reruns remain a controlled release activity.