From 380195f7cd5e57acf2c1cf2bc41069e6b0b979ed Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Thu, 17 Sep 2026 19:55:36 +0200 Subject: Initial commit: daemon, clients, docs, Docker deploy pipeline --- src/audit.c | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/audit.c (limited to 'src/audit.c') diff --git a/src/audit.c b/src/audit.c new file mode 100644 index 0000000..2a3565d --- /dev/null +++ b/src/audit.c @@ -0,0 +1,214 @@ +#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("{}"); +} -- cgit v1.3