1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! Append-only audit-log schema fixture (SG-06 + FC-06).
//!
//! This module publishes the DDL Phase 2 loads at simulator startup. The
//! schema itself — specifically, the `BEFORE UPDATE` / `BEFORE DELETE`
//! triggers on `audit_events` — enforces append-only semantics. Runtime
//! Rust code never needs to "remember" not to delete; the DB refuses.
//!
//! # Dual-schema audit design (POLISH2-22, friction row 12)
//!
//! reposix intentionally ships **two** append-only audit schemas, each
//! owned by the crate that writes to it. Neither is the canonical
//! superset; they capture disjoint event classes:
//!
//! - **`audit_events`** (this crate, `reposix-core`) — **backend-side
//! mutating writes**. Inserters: `reposix-sim::middleware::audit`,
//! `reposix-confluence::lib::record_audit`, `reposix-jira::lib`. Rows
//! describe `create_record` / `update_record` / `delete_or_close`
//! calls and the simulator's HTTP-handler audit.
//! - **`audit_events_cache`** (`reposix-cache::audit`, schema in
//! `crates/reposix-cache/fixtures/cache_schema.sql`) — **cache-internal
//! operations**. Inserters: `reposix-cache::builder`, `reposix-cache::gc`,
//! the helper's `stateless-connect` path. Rows describe blob
//! materialization, gc eviction, every helper RPC turn (fetch / push),
//! integrity events, and sync-tag writes.
//!
//! The split is intentional. It keeps `reposix-cache` strictly cache-side
//! and lets backend adapters fail closed without coupling to the cache's
//! `SQLite` connection lifecycle. A given operational query (e.g., "what
//! did we write to JIRA in the last 24h?") may need to read **both**
//! tables — the backend mutations live here in `audit_events`, while the
//! helper-level `helper_push_accepted` row that committed them lives in
//! `audit_events_cache`.
//!
//! `reposix doctor` currently checks the `audit_events_cache` schema
//! only; backend-mutation audit is verifiable via per-backend integration
//! tests + the `audit_events` table directly. Future v0.12.0+ work
//! (POLISH2-22 deferred unification per code-quality CC-3) may fold the
//! two schemas behind a single `dyn AuditSink` trait, but the dual-table
//! shape is the supported design for the v0.11.x line.
//!
//! # Schema-attack hardening (H-02, phase-1 review)
//!
//! Row-level UPDATE/DELETE triggers are only half the SG-06 story. An
//! attacker with the same DB handle could otherwise disable the triggers
//! with `DROP TRIGGER audit_no_delete`, remove the whole table with
//! `DROP TABLE audit_events`, or flip `PRAGMA writable_schema=ON` and edit
//! `sqlite_master` directly. This module's [`open_audit_db`] opens the
//! connection with `SQLite`'s `SQLITE_DBCONFIG_DEFENSIVE` flag, which makes
//! `writable_schema=ON` a no-op and (crucially for us) prevents
//! `sqlite_master` edits that could strip the triggers. Integration tests
//! in `tests/audit_schema.rs` prove the three schema-attack vectors are
//! rejected or rendered harmless on a defensively-opened handle.
//!
//! Callers in Phase 2 MUST open the runtime audit DB via [`open_audit_db`]
//! (not via raw `rusqlite::Connection::open`) so the DEFENSIVE flag is set
//! before any schema statement executes.
//!
//! T-01-13 (exfiltration via raw body content) is `accept`ed at this layer:
//! the schema defines `request_body TEXT` but Phase 2's insert path is
//! responsible for hashing / redacting sensitive content before insert.
use Path;
use DbConfig;
use crate::;
/// Canonical DDL for the `audit_events` table and its append-only triggers.
pub const SCHEMA_SQL: &str = include_str!;
/// Load the schema into an open `SQLite` connection. Idempotent: every
/// statement uses `IF NOT EXISTS`, so calling this twice on the same
/// connection is a no-op.
///
/// # Errors
/// Returns [`Error::Other`] wrapping the underlying `rusqlite::Error` if
/// the batch execute fails (typically a bad connection or concurrent-schema
/// race).
/// Enable `SQLITE_DBCONFIG_DEFENSIVE` on `conn`.
///
/// The DEFENSIVE flag (`SQLite` 3.26+) refuses edits to `sqlite_master` via
/// `PRAGMA writable_schema=ON`. On an in-process attacker path, this means
/// `DROP TRIGGER` / `DROP TABLE` remain syntactically legal (they take
/// the *authenticated* schema-edit path) but attempts to bypass them by
/// writing to `sqlite_master` directly are rejected.
///
/// # Errors
/// Returns [`Error::Other`] wrapping the underlying `rusqlite::Error` if
/// the flag cannot be set (typically: `SQLite` built without defensive
/// support — `SQLITE_DBCONFIG_DEFENSIVE` is in the stable API since 3.26
/// and the `rusqlite` bundled build is well past that).
/// Open the audit `SQLite` DB at `path` with the append-only invariant
/// hardened against schema-level attacks.
///
/// Opens the file with default read-write flags, enables
/// `SQLITE_DBCONFIG_DEFENSIVE` (see [`enable_defensive`]), then loads the
/// canonical schema via [`load_schema`]. The returned connection is ready
/// for inserts; further schema edits from this handle are either rejected
/// by DEFENSIVE (`writable_schema` path) or will still be caught by the
/// row-level triggers (UPDATE/DELETE path).
///
/// # Errors
/// Returns [`Error::Other`] if the file cannot be opened, the defensive
/// flag cannot be set, or the schema batch fails.