Skip to main content

Rule Storage

SigmaShake keeps rules in two places, and the relationship between them is explicit — not automatic. Understanding this model is essential when reading the dashboard's Observability → Rule Drift Detector.

The two stores

StoreLocationRole
.rules files.sigmashake/rules/*.rules (plus .sigmashake/*.rules)Human-authored source of truth, checked into git
SQLite DB~/.sigmashake/rules.db (table rules)Runtime evaluation target — what the daemon actually queries

Both exist for distinct reasons:

  • Files are what a developer edits, version-controls, and reviews.
  • The DB is what the daemon reads on every tool call. Indexed SQLite queries evaluate in sub-millisecond time; parsing text files on each call would not.

Schema

The rules table (src/engine/db.ts):

CREATE TABLE rules (
id TEXT PRIMARY KEY,
priority INTEGER NOT NULL DEFAULT 50,
severity TEXT NOT NULL DEFAULT 'warning',
decision TEXT NOT NULL DEFAULT 'log',
target TEXT NOT NULL DEFAULT 'any',
groups TEXT NOT NULL,
message TEXT NOT NULL,
prompt TEXT,
substitute TEXT,
enabled INTEGER NOT NULL DEFAULT 1,
source_file TEXT -- provenance (added v0.22.2)
);

The source_file column records where a row came from:

source_file valueMeaning
/abs/path/to/foo.rulesLoaded from a local .rules file via ssg sync
NULLDB-only — created by Hub sync, Fleet bundle, cloud push, or direct insert

Provenance is what lets reconciliation prune safely without clobbering remote-sourced rules.

How files become DB rows

Syncing is explicit. Nothing auto-watches the filesystem by default. The DB picks up file changes only when one of these runs:

ssg sync # CLI — reconcile .rules files into rules.db

or the dashboard button ↑ sync files → db (which calls POST /api/rules/sync-to-db).

Both paths invoke reconcileRulesFromFiles():

  1. Parse every .rules file → list of Rule objects.
  2. UPSERT each one into the rules table (keyed by id).
  3. DELETE any DB row where source_file IS NOT NULL and the ID is no longer present in the parsed set. These are orphans — rules whose file was deleted or whose ID was renamed.
  4. Leave every source_file IS NULL row alone. Hub, Fleet, and cloud-synced rules are never pruned by file sync.

Daemon precedence: DB-first, files as fallback

When the daemon loads rules (src/commands/daemon/rule-pipeline.ts):

1. SELECT from rules table
2. If zero rows → fall back to loading .rules files directly
3. Merge Fleet bundle rules (if enrolled)

The practical consequence: once the DB has any rows, it is the source of truth at runtime. Editing a .rules file and forgetting to ssg sync means the daemon keeps using the stale DB row. The Observability tab's drift detector exists to surface exactly this case.

The Drift Detector

The dashboard's Observability tab shows four numbers:

MetricMeaning
.rules filesCount of files on disk under .sigmashake/
db rulesRow count of the rules table (all, including disabled)
only in filesRule IDs parsed from files that have no DB row
only in dbRule IDs in the DB table with no matching file rule (includes DB-only rules with source_file IS NULL and orphans with source_file set)

Drift is detected when either only in files or only in db is non-zero.

Why .rules file count ≠ db rules count

A mismatch here is expected in most real setups. Below are the legitimate causes:

CauseSignature
Multiple rules per fileOne .rules file may define many rule blocks. 10 files can produce 30+ DB rows. The file_rule_count sub-metric (under .rules files) is the parsed rule count, which is what should be compared against db_rule_count.
Unsynced editsYou added or edited a rule in a file but have not run ssg sync. The new IDs appear under only in files. Fix: run ssg sync (or click ↑ sync files → db).
Hub / cloud / Fleet rulesRules pulled from the Hub, a Fleet bundle, or cloud push have source_file IS NULL and exist only in the DB. They legitimately raise db rules above file_rule_count.
Disabled rulesThe db rules count includes rows where enabled = 0. The sub-metric db_enabled_count excludes them. Disabled rules participate in the row count but not in evaluation.
Orphans from deleted filesIf you delete a .rules file and don't re-sync, file-backed DB rows linger until the next reconciliation. They show up under only in db. Fix: run ssg sync.
Rule ID renamedRenaming a rule ID in a file creates a new DB row on next sync and leaves the old one as an orphan. Fix: ssg sync prunes the old ID if source_file is set.

When drift is a problem vs. expected

Drift bucketHealthy reasonProblem reason
only in filesNever — always means an unsynced editForgot ssg sync
only in dbHub / Fleet / cloud-synced rulesOrphans from deleted files

A quick rule of thumb: only in files > 0 always calls for ssg sync. only in db > 0 deserves a look at whether those IDs came from Hub/Fleet (healthy) or are leftover from deleted local files (run ssg sync).

Operator commands

# Preview what's on disk
ssg lint # parse + validate .rules files
ssg check . # scan for violations using current rules

# Sync file ↔ DB
ssg sync # reconcile .rules files into rules.db

# Inspect the DB
ssg status # quick health overview
sqlite3 ~/.sigmashake/rules.db \
"SELECT id, source_file FROM rules" # raw row listing

See also