sightingdb 0.5.2

A database designed for Sightings, a technique to count items
# SightingDB configuration.
#
# Comments here are preserved: the management interface never rewrites this
# file. It writes only the separate acl_file below.

[daemon]
# Set to false to run without the HTTP API (DNS or ZMQ ingest only).
enabled = true
listen_ip = "0.0.0.0"
listen_port = 9999

# Require an API key on the sighting API. The management interface always
# requires one regardless of this setting.
authenticate = false

# Detaching is opt-in; under systemd or Docker leave this false and let the
# supervisor do it. See etc/sightingdb.service.
daemonize = false

ssl = true
ssl_cert = "ssl/cert.pem"
ssl_key = "ssl/key.pem"

# Largest accepted bulk POST body, in bytes.
post_limit = 2_500_000_000

# Only used when daemonize = true.
log_out = "/var/log/sightingdb/sightingdb.log"
log_err = "/var/log/sightingdb/sightingdb.error.log"

# Where the database is snapshotted. Comment this out to run purely in memory,
# losing everything when the process stops.
dbdir = "/var/lib/sighting"

# Seconds between snapshots. The database is always saved on a clean shutdown;
# this bounds how much a crash can lose. 0 saves only on shutdown.
#
# Storage is split into one zstd-compressed file per top-level namespace, so
# "myorg/thisone" lives in myorg-<hash>.json.zst and a periodic snapshot only
# rewrites the files that changed. Everything internal (_all, _shadow, _config)
# shares sightingdb.json.zst.
snapshot_interval = 300

# zstd level for those files. 3 is the knee of the curve: measured on a 50 MB
# snapshot it reaches 7.3x in 0.07s, where level 19 takes 25s for 11.3x. Higher
# levels cost compression time on every save for very little space.
compression_level = 3

# Seconds between eviction sweeps, which reclaim expired values and the
# namespaces left empty by them. 0 disables the sweeper.
sweep_interval = 60

# Hourly statistics buckets kept per value. 720 is 30 days. Without a limit
# every value gains a bucket per hour for as long as it is written to.
# 0 keeps all of them.
stats_retention = 720

# How long a shadow sighting (the record that something was searched for) is
# kept, in seconds. 0 keeps them forever, so `_shadow/*` grows without bound.
# 2592000 is 30 days.
shadow_ttl = 2_592_000

# How long each top-level namespace stays in memory.
#
#   hot   never evicted
#   warm  written out and dropped once untouched for warm_idle seconds; any
#         read or write starts the window again
#   cold  dropped at the next sweep once idle
#
# "cold" does not mean a disk round trip per operation: a cold shard is loaded
# on demand and dropped at the next sweep, so a burst of writes costs one load
# rather than one per write. Internal state (_all, _config) is always hot,
# whatever is configured, because consensus and API keys are consulted
# constantly.
#
# Eviction needs somewhere to write, so it only happens when dbdir is set.
[storage]
default_tier = "hot"
warm_idle = 3600

# Rewritten by the management interface whenever a tier is changed there, which
# is why it is a file of its own: rewriting this one would discard its comments.
# Without it, tiers come from [storage.tiers] below and the interface can show
# them but not change them.
tiers_file = "tiers.toml"

# Per-namespace overrides, keyed by the top-level namespace.
# [storage.tiers]
# myorg = "hot"
# archive = "cold"
# occasional = "warm"

# API keys live in their own file because the management interface rewrites it
# whenever a key is created or revoked. Without this, keys can be declared in an
# [acl] table below, but the interface will not be able to edit them.
acl_file = "acl.toml"

# API keys, used only when acl_file is not set.
#
#   "<apikey>" = "<grant>[, <grant>...]"
#
# A grant is r, w or rw, optionally scoped with :<namespace prefix>, plus
# "admin" for access to the management interface at /_management/. Prefixes
# match whole path segments, so rw:feeds/misp covers feeds/misp/ips but not
# feeds/misp-internal.
#
# [acl]
# changeme = "rw, admin"
# analyst = "r"
# feed-misp = "rw:feeds/misp"

# ---------------------------------------------------------------------------
# Answer sightings over DNS, using the DNSBL conventions security tooling
# already speaks.
#
# READ THIS BEFORE ENABLING: DNS has no authentication. The ACL does not apply,
# so anything reachable here is readable by anyone who can send a UDP packet.
# Only the namespaces listed below answer, the listener binds to loopback unless
# you change it, and rate_limit exists because an open UDP responder is a
# reflection amplifier.
# ---------------------------------------------------------------------------
# [dns]
# enabled = true
# listen_ip = "127.0.0.1"
# listen_port = 5353
# zone = "sdb.example.com"
# TTL on answers, and how long resolvers cache a "never seen" reply.
# ttl = 60
# Queries per second per source address; 0 disables the limit.
# rate_limit = 100
# threads = 2
# Whether a DNS lookup counts as a search. Off by default: it is an
# unauthenticated write path into _shadow/*.
# shadow = false
#
# How a value is spelled in the query name:
#   ip      4.3.2.1.malware.sdb.example.com  -> 1.2.3.4   (DNSBL convention)
#   domain  evil.com.domains.sdb.example.com -> evil.com
#   base32  <base32 of the value>.hashes.sdb.example.com
# [dns.namespaces]
# malware = { namespace = "malware/ips", encoding = "ip" }
# domains = { namespace = "malware/domains", encoding = "domain" }
# hashes = { namespace = "malware/hashes", encoding = "base32" }

# ---------------------------------------------------------------------------
# Ingest sightings from a ZeroMQ publisher such as MISP's. This is a SUB socket
# that connects out to a publisher you trust; nothing is exposed.
# ---------------------------------------------------------------------------
# [zmq]
# enabled = true
# endpoint = "tcp://misp.example.com:50000"
# MISP publishes "<topic> <json>"; subscribing to a prefix matches it.
# topics = ["misp_json_attribute", "misp_json"]
# "misp" reads MISP attributes and events; "native" reads
# {"items":[{"namespace":..,"value":..,"timestamp":..}]}
# format = "misp"
# Only ingest attributes MISP flagged as actionable.
# require_to_ids = true
# Where unmapped attribute types go; omit to discard them.
# default_namespace = ""
# TTL on ingested sightings, and seconds between reconnection attempts.
# ttl = 0
# reconnect = 5
#
# [zmq.types]
# ip-src = "misp/ips"
# ip-dst = "misp/ips"
# domain = "misp/domains"
# hostname = "misp/domains"
# url = "misp/urls"
# md5 = "misp/hashes"
# sha1 = "misp/hashes"
# sha256 = "misp/hashes"

# ---------------------------------------------------------------------------
# Importing STIX 2.1 bundles with --import-stix <file or directory>. Counts and
# time windows come from the bundle: an observed-data seen 12 times between two
# instants becomes 12 sightings spanning that window.
# ---------------------------------------------------------------------------
# [stix]
# default_namespace = ""
# ttl = 0
#
# Keys containing a dot must be QUOTED, or TOML reads them as a nested table.
# [stix.types]
# ipv4-addr = "stix/ips"
# ipv6-addr = "stix/ips"
# domain-name = "stix/domains"
# url = "stix/urls"
# "file.MD5" = "stix/hashes"
# "file.SHA-1" = "stix/hashes"
# "file.SHA-256" = "stix/hashes"