#include "audit.h" #include #include #include "util.h" static void set_err(char **err, const char *msg) { if (err && !*err) *err = xstrdup(msg); } static int64_t audit_last_seq(sqlite3 *db) { sqlite3_stmt *st = NULL; int64_t seq = 0; if (sqlite3_prepare_v2(db, "SELECT seq FROM audit_log ORDER BY seq DESC LIMIT 1", -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW) seq = sqlite3_column_int64(st, 0); sqlite3_finalize(st); return seq; } static void audit_last_hash(sqlite3 *db, unsigned char out[32]) { memset(out, 0, 32); sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2(db, "SELECT hash FROM audit_log ORDER BY seq DESC LIMIT 1", -1, &st, NULL) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW) { const void *b = sqlite3_column_blob(st, 0); if (b && sqlite3_column_bytes(st, 0) == 32) memcpy(out, b, 32); } sqlite3_finalize(st); } static void append_str16(struct buf *b, const char *s) { size_t n = s ? strlen(s) : 0; if (n > 0xffff) n = 0xffff; buf_append_u16be(b, (uint16_t)n); buf_append(b, s ? s : "", n); } static void append_str32(struct buf *b, const char *s) { size_t n = s ? strlen(s) : 0; buf_append_u32be(b, (uint32_t)n); buf_append(b, s ? s : "", n); } static void audit_hash(const unsigned char prev[32], int64_t seq, const char *at, int64_t user_id, int64_t token_id, int64_t org_id, const char *action, const char *request_json, const char *result_code, unsigned char out[32]) { struct buf b; buf_init(&b); buf_append(&b, "bokf-v1-audit\0", 14); buf_append(&b, prev, 32); buf_append_u64be(&b, (uint64_t)seq); append_str16(&b, at); buf_append_u64be(&b, (uint64_t)user_id); buf_append_u64be(&b, (uint64_t)token_id); buf_append_u64be(&b, (uint64_t)org_id); append_str16(&b, action); append_str32(&b, request_json); append_str16(&b, result_code); util_sha256(b.p, b.len, out); buf_free(&b); } int audit_append(sqlite3 *db, int64_t org_id, int64_t user_id, int64_t token_id, const char *action, const char *request_json, const char *result_code, char **err) { unsigned char prev[32], hash[32]; audit_last_hash(db, prev); int64_t seq = audit_last_seq(db) + 1; char ts[32]; util_iso8601(util_now(), ts, sizeof ts); if (!request_json) request_json = "{}"; audit_hash(prev, seq, ts, user_id, token_id, org_id, action, request_json, result_code, hash); sqlite3_stmt *st = NULL; int rc = sqlite3_prepare_v2( db, "INSERT INTO audit_log(seq,org_id,at,actor_user_id,actor_token_id," "action,request_json,result_code,hash_prev,hash)" " VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)", -1, &st, NULL); if (rc != SQLITE_OK) { set_err(err, sqlite3_errmsg(db)); return -1; } sqlite3_bind_int64(st, 1, seq); if (org_id) sqlite3_bind_int64(st, 2, org_id); else sqlite3_bind_null(st, 2); sqlite3_bind_text(st, 3, ts, -1, SQLITE_TRANSIENT); if (user_id) sqlite3_bind_int64(st, 4, user_id); else sqlite3_bind_null(st, 4); if (token_id) sqlite3_bind_int64(st, 5, token_id); else sqlite3_bind_null(st, 5); sqlite3_bind_text(st, 6, action, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 7, request_json, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 8, result_code, -1, SQLITE_TRANSIENT); sqlite3_bind_blob(st, 9, prev, 32, SQLITE_TRANSIENT); sqlite3_bind_blob(st, 10, hash, 32, SQLITE_TRANSIENT); rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) { set_err(err, sqlite3_errmsg(db)); return -1; } return 0; } int audit_verify(sqlite3 *db, int64_t *checked, int64_t *bad_seq, char **err) { sqlite3_stmt *st = NULL; int rc = sqlite3_prepare_v2( db, "SELECT seq,org_id,at,actor_user_id,actor_token_id,action,request_json," "result_code,hash_prev,hash FROM audit_log ORDER BY seq", -1, &st, NULL); if (rc != SQLITE_OK) { set_err(err, sqlite3_errmsg(db)); return -1; } unsigned char running[32]; memset(running, 0, 32); int64_t n = 0; int broken = 0; while (sqlite3_step(st) == SQLITE_ROW) { n++; int64_t seq = sqlite3_column_int64(st, 0); int64_t org_id = sqlite3_column_type(st, 1) == SQLITE_NULL ? 0 : sqlite3_column_int64(st, 1); const char *at = (const char *)sqlite3_column_text(st, 2); int64_t uid = sqlite3_column_type(st, 3) == SQLITE_NULL ? 0 : sqlite3_column_int64(st, 3); int64_t tid = sqlite3_column_type(st, 4) == SQLITE_NULL ? 0 : sqlite3_column_int64(st, 4); const char *action = (const char *)sqlite3_column_text(st, 5); const char *req = (const char *)sqlite3_column_text(st, 6); const char *code = (const char *)sqlite3_column_text(st, 7); const void *hprev = sqlite3_column_blob(st, 8); int hprev_n = sqlite3_column_bytes(st, 8); const void *h = sqlite3_column_blob(st, 9); int h_n = sqlite3_column_bytes(st, 9); unsigned char expect[32]; audit_hash(running, seq, at ? at : "", uid, tid, org_id, action ? action : "", req ? req : "", code ? code : "", expect); if (hprev_n != 32 || h_n != 32 || memcmp(hprev, running, 32) != 0 || memcmp(h, expect, 32) != 0) { if (bad_seq) *bad_seq = seq; broken = 1; break; } memcpy(running, expect, 32); } sqlite3_finalize(st); if (checked) *checked = n; return broken ? 1 : 0; } char *audit_args_json(yyjson_val *args) { if (!args || yyjson_is_null(args)) return xstrdup("{}"); if (!yyjson_is_obj(args)) { char *s = yyjson_val_write(args, 0, NULL); return s ? s : xstrdup("{}"); } yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); yyjson_mut_val *out = yyjson_mut_obj(doc); yyjson_mut_doc_set_root(doc, out); yyjson_obj_iter it = yyjson_obj_iter_with(args); yyjson_val *key; while ((key = yyjson_obj_iter_next(&it))) { const char *k = yyjson_get_str(key); yyjson_val *v = yyjson_obj_iter_get_val(key); if (!k || !v) continue; if (strcmp(k, "password") == 0 || strcmp(k, "token") == 0 || strcmp(k, "content_base64") == 0 || strcmp(k, "secret") == 0) continue; yyjson_mut_val *copy = yyjson_val_mut_copy(doc, v); if (copy) yyjson_mut_obj_add(out, yyjson_mut_strcpy(doc, k), copy); } char *json = yyjson_mut_write(doc, 0, NULL); yyjson_mut_doc_free(doc); return json ? json : xstrdup("{}"); }