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 rule 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 rule 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 rule 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 rule sync. The new IDs appear under only in files. Fix: run ssg rule 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 rule 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 rule 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 rule sync
only in dbHub / Fleet / cloud-synced rulesOrphans from deleted files

A quick rule of thumb: only in files > 0 always calls for ssg rule 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 rule 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 rule 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​