lacs-project / lacs-project/sysknife
open_read_only writes: audit verify and audit export migrate the database and fail on a read-only copy
- Dominant language
- Rust
- Stars
- 12
- Forks
- 19
- Avg merge
- 18h 57m
- Merged PRs (30d)
- 116
Description
`TransactionStore::open_read_only` (`crates/sysknife-daemon/src/transactions.rs:331`) calls `initialize()`, and `initialize()` opens a write transaction, creates `schema_migrations` if absent, applies pending migrations, and sets `journal_mode=WAL` on the way in:
```rust
pub fn open_read_only(path: impl AsRef) -> Result {
...
store.initialize()?;
```
```rust
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
... "CREATE TABLE IF NOT EXISTS schema_migrations ("
```
So all three commands that claim to read the audit chain without touching it, `sysknife audit verify`, `sysknife audit export` and `sysknife audit checkpoint`, write to it.
## Why it matters
Four consequences, in rising order of how much they hurt:
1. `-wal` and `-shm` sidecars appear next to a database the operator only read.
2. A newer CLI silently upgrades an older audit database's schema and inserts a `schema_migrations` row.
3. An older CLI refuses outright: `initialize()` returns `sqlite schema version N is newer than this binary supports`, so a binary that could read all 17 columns fine will not read any.
4. A read-only copy fails. `PRAGMA journal_mode=WAL` on a 0400 file or a read-only mount returns `attempt to write a readonly database`, so the offline-auditor workflow `docs/the-audit-chain.md` advertises for `audit export` cannot run on the artifact an auditor would actually be handed.
There is also a contention path: `BEGIN IMMEDIATE` takes a write lock with a 5s busy timeout, so `sysknife audit export > rows.json` in cron against a busy daemon intermittently exits 4 after the shell has already truncated the output file.
`ensure_private_dir` is not part of this problem: it creates a missing directory at 0700 and leaves an existing one alone.
## Scope
Give the read paths a genuinely read-only open: `OpenFlags::SQLITE_OPEN_READ_ONLY`, no pragma writes, no `initialize()`, and a schema-version check by `SELECT` rather than by running migrations. Keep `open_with_key` as it is, since the daemon does need to migrate.
## Tests first
The failing test is short: write a database with `open_with_key`, `chmod 0400` it, then open it read-only and read the chain rows. That fails today with a write error, and no existing test covers a non-writable database.
## Difficulty
`medium`. The open path is small, but the schema-version check has to move from migration to inspection.
## Getting started
[CONTRIBUTING.md](https://github.com/lacs-project/sysknife/blob/main/CONTRIBUTING.md) has the build and test commands, and [docs/developer-guide.md](https://github.com/lacs-project/sysknife/blob/main/docs/developer-guide.md) covers the setup steps and how to reproduce each required check locally. No CLA and no copyright waiver. The project is MIT.
Contributor guide
Research direction
Start in crates/sysknife-daemon/src/transactions.rs at TransactionStore::open_read_only and compare it with open_with_key and initialize(). Add a test that creates a database with open_with_key, makes it 0400, opens it read-only, and reads the chain rows. Done means audit verify, export, and checkpoint use read-only flags and SELECT-based schema inspection without migrations, pragma writes, or WAL sidecars.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, sqlite
- Domain
- cli, databases, security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100