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
| Store | Location | Role |
|---|---|---|
.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 value | Meaning |
|---|---|
/abs/path/to/foo.rules | Loaded from a local .rules file via ssg sync |
NULL | DB-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():
- Parse every
.rulesfile → list ofRuleobjects. UPSERTeach one into therulestable (keyed byid).DELETEany DB row wheresource_file IS NOT NULLand the ID is no longer present in the parsed set. These are orphans — rules whose file was deleted or whose ID was renamed.- Leave every
source_file IS NULLrow 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:
| Metric | Meaning |
|---|---|
.rules files | Count of files on disk under .sigmashake/ |
db rules | Row count of the rules table (all, including disabled) |
only in files | Rule IDs parsed from files that have no DB row |
only in db | Rule 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:
| Cause | Signature |
|---|---|
| Multiple rules per file | One .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 edits | You 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 rules | Rules 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 rules | The 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 files | If 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 renamed | Renaming 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 bucket | Healthy reason | Problem reason |
|---|---|---|
only in files | Never — always means an unsynced edit | Forgot ssg sync |
only in db | Hub / Fleet / cloud-synced rules | Orphans 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
- CLI Reference —
ssg sync - Dashboard — the Observability tab that surfaces drift
- Architecture — overall component layout
- Evaluation — how the daemon uses the loaded rule set