Skip to content
hasecon

openkat

Adding Hosts, IP Ranges and URLs to OpenKAT via the API

Edward Hasekamp ·

A question that comes up regularly in the OpenKAT community: “Does OpenKAT have an API to add new (sub)domains or IP ranges programmatically?” The short answer is yes, but there are a couple of things worth knowing first. We’re actively involved in OpenKAT’s development and maintenance, and we use this API daily to spin up test environments. Here’s a practical guide based on a real, running stack.

Where objects live: Octopoes

In OpenKAT, everything you scan is an Object of Interest (OOI): a hostname, an IP address, an IP range, a URL or a network. These objects live in Octopoes, the graph database connector built on XTDB. Octopoes exposes a FastAPI REST API, and it ships with an interactive documentation page you can explore yourself:

http://<your-host>:8001/docs

This is where you add objects. The endpoints relevant for creating objects are:

Method Endpoint Purpose
POST /{org}/declarations Declare a single object (the normal way to add a host, IP or URL)
POST /{org}/declarations/save_many Declare multiple objects in one request
POST /{org}/observations Add objects as a scanner or normalizer result, with a source reference
POST /{org}/affirmations Affirm a derived or assumed object
PUT /{org}/scan_profiles Set the clearance level on an object

{org} is your organisation code, the same code you see in the Rocky URL.

The key insight: adding an object is two steps

This is the part that trips people up. Adding an object through the API takes two separate calls:

  1. Declare the object. This creates it in Octopoes.
  2. Set a scan profile. This assigns a clearance level, which is what actually authorises the boefjes (the scanners) to run against it.

A bare declaration lands the object at clearance level 0, which means no boefje will ever scan it. The web interface and the CSV upload bundle these two steps together, but against the raw API you do them yourself.

Step 1: declare the object

curl -X POST 'http://localhost:8001/myorg/declarations?sync=true' 
  -H 'Content-Type: application/json' 
  -d '{
    "ooi": {
      "object_type": "Hostname",
      "network": "Network|internet",
      "name": "api-test.example.com"
    },
    "valid_time": "2026-06-18T20:00:00+00:00"
  }'

The request body fields:

  • ooi (required): the object itself (see the types below).
  • valid_time (required): an ISO datetime that includes a timezone. OpenKAT is bitemporal, so every fact is valid from a point in time.
  • end_valid_time, method, source_method and task_id are all optional.

The ?sync=true query parameter processes the declaration synchronously, so the object is immediately queryable.

Step 2: set the clearance level

Without this, the object stays at level 0 and nothing scans it:

curl -X PUT 'http://localhost:8001/myorg/scan_profiles?valid_time=2026-06-18T20%3A00%3A00%2B00%3A00' 
  -H 'Content-Type: application/json' 
  -d '{
    "scan_profile_type": "declared",
    "reference": "Hostname|internet|api-test.example.com",
    "level": 4
  }'

Once the scan profile is set, the scheduler (which ticks roughly every minute) picks the object up and the enabled boefjes start running against it: DNS records, nmap, SSL checks, and so on.

Watch out: when you pass valid_time as a query parameter, it has to be URL-encoded. The + in the timezone offset (+00:00) turns into a space otherwise, and you get an HTTP 422 validation error. In the JSON body, a plain ISO string is fine.

Object types for the common cases

The original question was about (sub)domains and IP ranges. Here are the OOI shapes for those:

// (sub)domain
{ "object_type": "Hostname",     "network": "Network|internet", "name": "sub.example.com" }

// single IPv4 address
{ "object_type": "IPAddressV4",  "network": "Network|internet", "address": "192.0.2.1" }

// IPv4 range / subnet
{ "object_type": "IPV4NetBlock", "network": "Network|internet",
  "start_ip": "IPAddressV4|internet|192.0.2.0", "mask": 24 }

// URL
{ "object_type": "URL",          "network": "Network|internet", "raw": "https://example.com/" }

The same pattern applies to IPAddressV6 and IPV6NetBlock for IPv6.

Adding objects in bulk

For more than a handful of objects, you have two good options.

Via the API. POST /{org}/declarations/save_many accepts an array of declarations in a single request, which is far more efficient than looping one by one.

Via the Rocky web interface. Under Objects > Add > Upload CSV you can upload a CSV, and Rocky handles both the declaration and the scan profile for you. This is the user-friendly route and needs no scripting:

name,network
example.com,internet
sub.example.com,internet
address,network
192.0.2.1,internet

What about Rocky’s public API?

Rocky does expose a small REST API at /api/v1/, but it does not currently let you create objects. It only covers organisations (/api/v1/organization/), reports (/api/v1/report/) and report recipes (/api/v1/report-recipe/).

One thing that catches people out: the API is built on Django REST Framework’s SimpleRouter, which doesn’t register an API root view. Hitting /api/v1/ directly returns a 404, which is expected behaviour rather than a sign that something is broken. Go straight to a resource endpoint instead, and authenticate, because without credentials those endpoints return a 401.

So for adding objects programmatically today, the Octopoes declarations API and the CSV upload are your two routes. A dedicated public endpoint for adding objects isn’t part of Rocky’s API yet.

Doing it from Python

If you are scripting from within the OpenKAT ecosystem (a custom boefje, a management command, an internal integration), the cleanest approach is the connector that OpenKAT itself uses:

from datetime import datetime, timezone
from octopoes.connector.octopoes import OctopoesAPIConnector
from octopoes.api.models import Declaration
from octopoes.models.ooi.dns.zone import Hostname

connector = OctopoesAPIConnector("http://octopoes_api", client="myorg")

hostname = Hostname(network="Network|internet", name="api-test.example.com")
connector.save_declaration(
    Declaration(ooi=hostname, valid_time=datetime.now(timezone.utc))
)

This is exactly what OpenKAT’s own tooling does under the hood, and it handles the model serialisation for you.

Summary

Goal How
One object via API POST /{org}/declarations then PUT /{org}/scan_profiles
Many objects via API POST /{org}/declarations/save_many
Bulk as an end user CSV upload in Rocky
From Python OctopoesAPIConnector.save_declaration()

So yes, OpenKAT lets you add hostnames, subdomains, IP addresses, IP ranges and URLs through an API. Just remember the two-step pattern: declare the object, then set a clearance level, and URL-encode your valid_time when it goes in a query string.


At Hasecon we are actively involved in OpenKAT’s development and maintenance, and we deploy and integrate it for clients across the Netherlands. Questions about automating OpenKAT or integrating it into your own tooling? Get in touch.

Want to know where you are exposed?

Book a free 30-minute call, or send an email.