diff options
Diffstat (limited to 'src/cmd_audit.c')
| -rw-r--r-- | src/cmd_audit.c | 288 |
1 files changed, 288 insertions, 0 deletions
diff --git a/src/cmd_audit.c b/src/cmd_audit.c new file mode 100644 index 0000000..183e7e6 --- /dev/null +++ b/src/cmd_audit.c @@ -0,0 +1,288 @@ +#include "commands.h" +#include "cmd_util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> + +#include "audit.h" +#include "config.h" +#include "db.h" +#include "ledger.h" +#include "sha256.h" +#include "util.h" + +/* ------------------------------------------------------------------ */ +/* audit and backup */ +/* ------------------------------------------------------------------ */ + +static yyjson_mut_val *h_audit_list(struct req *r) +{ + int64_t cursor = 0, limit = 100; + arg_int(r->args, "cursor", &cursor); + arg_int(r->args, "limit", &limit); + if (limit < 1) + limit = 100; + if (limit > 1000) + limit = 1000; + const char *action = arg_str(r->args, "action"); + + yyjson_mut_val *items = yyjson_mut_arr(r->rdoc); + sqlite3_stmt *st = NULL; + const char *sql = action + ? "SELECT seq,at,actor_user_id,actor_token_id,action," + "request_json,result_code FROM audit_log" + " WHERE org_id=?1 AND seq>?2 AND action=?3" + " ORDER BY seq LIMIT ?4" + : "SELECT seq,at,actor_user_id,actor_token_id,action," + "request_json,result_code FROM audit_log" + " WHERE org_id=?1 AND seq>?2 ORDER BY seq LIMIT ?4"; + if (sqlite3_prepare_v2(r->db, sql, -1, &st, NULL) != SQLITE_OK) + return fail(r, "INTERNAL", "database error"); + sqlite3_bind_int64(st, 1, r->org_id); + sqlite3_bind_int64(st, 2, cursor); + if (action) + sqlite3_bind_text(st, 3, action, -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(st, 4, limit); + int64_t last_seq = cursor; + int64_t n = 0; + while (sqlite3_step(st) == SQLITE_ROW) { + n++; + last_seq = sqlite3_column_int64(st, 0); + yyjson_mut_val *o = yyjson_mut_arr_add_obj(r->rdoc, items); + yyjson_mut_obj_add_int(r->rdoc, o, "seq", last_seq); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "at", + sq(sqlite3_column_text(st, 1))); + if (sqlite3_column_type(st, 2) == SQLITE_NULL) + yyjson_mut_obj_add_null(r->rdoc, o, "actor_user_id"); + else + yyjson_mut_obj_add_int(r->rdoc, o, "actor_user_id", + sqlite3_column_int64(st, 2)); + if (sqlite3_column_type(st, 3) == SQLITE_NULL) + yyjson_mut_obj_add_null(r->rdoc, o, "actor_token_id"); + else + yyjson_mut_obj_add_int(r->rdoc, o, "actor_token_id", + sqlite3_column_int64(st, 3)); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "action", + sq(sqlite3_column_text(st, 4))); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "result_code", + sq(sqlite3_column_text(st, 6))); + const char *req = (const char *)sqlite3_column_text(st, 5); + yyjson_doc *parsed = req ? yyjson_read(req, strlen(req), 0) : NULL; + if (parsed && yyjson_is_obj(yyjson_doc_get_root(parsed))) + yyjson_mut_obj_add_val(r->rdoc, o, "request", + yyjson_val_mut_copy(r->rdoc, + yyjson_doc_get_root(parsed))); + if (parsed) + yyjson_doc_free(parsed); + } + sqlite3_finalize(st); + yyjson_mut_val *out = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_val(r->rdoc, out, "items", items); + if (n == limit) + yyjson_mut_obj_add_int(r->rdoc, out, "next_cursor", last_seq); + else + yyjson_mut_obj_add_null(r->rdoc, out, "next_cursor"); + return out; +} + +static int verify_attachments(sqlite3 *db, int64_t *checked, int64_t *bad_id, + char **err) +{ + sqlite3_stmt *st = NULL; + if (sqlite3_prepare_v2(db, + "SELECT id,sha256,size_bytes,content FROM attachments" + " ORDER BY id", + -1, &st, NULL) != SQLITE_OK) { + if (err && !*err) + *err = xstrdup(sqlite3_errmsg(db)); + return -1; + } + int ret = 0; + for (;;) { + int step = sqlite3_step(st); + if (step == SQLITE_DONE) + break; + if (step != SQLITE_ROW) { + if (err && !*err) + *err = xstrdup(sqlite3_errmsg(db)); + ret = -1; + break; + } + int64_t id = sqlite3_column_int64(st, 0); + const void *sha = sqlite3_column_blob(st, 1); + int sha_n = sqlite3_column_bytes(st, 1); + int64_t size = sqlite3_column_int64(st, 2); + const void *content = sqlite3_column_blob(st, 3); + int content_n = sqlite3_column_bytes(st, 3); + unsigned char digest[32]; + util_sha256(content, (size_t)content_n, digest); + (*checked)++; + if (sha_n != 32 || memcmp(sha, digest, 32) != 0 || + size != content_n) { + if (!*bad_id) + *bad_id = id; + ret = 1; + } + } + sqlite3_finalize(st); + return ret; +} + +static yyjson_mut_val *h_audit_verify(struct req *r) +{ + int full = 0; + arg_bool(r->args, "full", &full); + + int64_t audit_checked = 0, audit_bad = 0; + char *err = NULL; + int audit_rc = audit_verify(r->db, &audit_checked, &audit_bad, &err); + if (audit_rc < 0) { + yyjson_mut_val *res = fail(r, "INTERNAL", err ? err : "verify failed"); + free(err); + return res; + } + free(err); + err = NULL; + + struct ledger_verify_result lv; + int ledger_rc = ledger_verify(r->db, &lv, &err); + if (ledger_rc < 0) { + yyjson_mut_val *res = fail(r, "INTERNAL", err ? err : "verify failed"); + free(err); + return res; + } + free(err); + err = NULL; + + int64_t att_checked = 0, att_bad = 0; + int att_rc = 0; + if (full) { + att_rc = verify_attachments(r->db, &att_checked, &att_bad, &err); + if (att_rc < 0) { + yyjson_mut_val *res = + fail(r, "INTERNAL", err ? err : "verify failed"); + free(err); + return res; + } + free(err); + } + + int ok = audit_rc == 0 && ledger_rc == 0 && att_rc == 0; + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_bool(r->rdoc, o, "ok", ok); + yyjson_mut_obj_add_int(r->rdoc, o, "checked", audit_checked); + yyjson_mut_obj_add_int(r->rdoc, o, "vouchers_checked", + lv.vouchers_checked); + yyjson_mut_obj_add_int(r->rdoc, o, "unbalanced_vouchers", + lv.unbalanced_vouchers); + if (audit_bad) + yyjson_mut_obj_add_int(r->rdoc, o, "first_bad_seq", audit_bad); + else + yyjson_mut_obj_add_null(r->rdoc, o, "first_bad_seq"); + if (lv.first_bad_voucher_id) + yyjson_mut_obj_add_int(r->rdoc, o, "first_bad_voucher_id", + lv.first_bad_voucher_id); + else + yyjson_mut_obj_add_null(r->rdoc, o, "first_bad_voucher_id"); + if (lv.first_unbalanced_voucher_id) + yyjson_mut_obj_add_int(r->rdoc, o, "first_unbalanced_voucher_id", + lv.first_unbalanced_voucher_id); + else + yyjson_mut_obj_add_null(r->rdoc, o, "first_unbalanced_voucher_id"); + if (full) { + yyjson_mut_obj_add_int(r->rdoc, o, "attachments_checked", + att_checked); + if (att_bad) + yyjson_mut_obj_add_int(r->rdoc, o, "first_bad_attachment_id", + att_bad); + else + yyjson_mut_obj_add_null(r->rdoc, o, "first_bad_attachment_id"); + } + return o; +} + +static yyjson_mut_val *h_backup_snapshot(struct req *r) +{ + const char *dest = arg_str(r->args, "dest"); + if (mkdir_p(g_cfg.backup_dir, 0700) != 0) + return fail(r, "INTERNAL", "cannot create backup directory"); + char path[4096]; + if (dest && *dest) { + snprintf(path, sizeof path, "%s", dest); + } else { + char stamp[32]; + time_t t = (time_t)util_now(); + struct tm tm; + gmtime_r(&t, &tm); + strftime(stamp, sizeof stamp, "%Y%m%dT%H%M%SZ", &tm); + snprintf(path, sizeof path, "%s/bokfd-%s.db", g_cfg.backup_dir, stamp); + } + if (access(path, F_OK) == 0) + return fail(r, "CONFLICT", "destination already exists"); + char *sql = sqlite3_mprintf("VACUUM INTO %Q", path); + int rc = db_exec(r->db, sql, NULL); + sqlite3_free(sql); + if (rc != 0) + return fail(r, "INTERNAL", "VACUUM INTO failed"); + + struct stat sb; + if (stat(path, &sb) != 0) + return fail(r, "INTERNAL", "backup file not found after snapshot"); + FILE *f = fopen(path, "rb"); + if (!f) + return fail(r, "INTERNAL", "cannot read backup file"); + SHA256_CTX ctx; + sha256_init(&ctx); + unsigned char chunk[65536]; + size_t rn; + while ((rn = fread(chunk, 1, sizeof chunk, f)) > 0) + sha256_update(&ctx, (const BYTE *)chunk, rn); + fclose(f); + unsigned char digest[32]; + sha256_final(&ctx, digest); + char hex[65]; + util_hex(digest, 32, hex); + char ts[32]; + util_iso8601(util_now(), ts, sizeof ts); + + audit_append(r->db, r->org_id, r->sess->user_id, 0, "backup.snapshot", + "{}", "OK", NULL); + yyjson_mut_val *o = yyjson_mut_obj(r->rdoc); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "path", path); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "sha256", hex); + yyjson_mut_obj_add_int(r->rdoc, o, "size", (int64_t)sb.st_size); + yyjson_mut_obj_add_strcpy(r->rdoc, o, "at", ts); + return o; +} + +static const struct cmd_arg args_audit_list[] = { + { "cursor", ARG_INT, 0, NULL, NULL, "Cursor from next_cursor" }, + { "limit", ARG_INT, 0, "100", NULL, "Page size, 1-1000" }, + { "action", ARG_STR, 0, NULL, NULL, "Filter by action" }, +}; + +static const struct cmd_arg args_audit_verify[] = { + { "full", ARG_BOOL, 0, "false", NULL, "Also re-hash attachments" }, +}; + +static const struct cmd_arg args_backup_snapshot[] = { + { "dest", ARG_STR, 0, NULL, NULL, + "Destination path; defaults to backup_dir with a timestamp" }, +}; + +const struct command g_cmd_audit[] = { + { "audit.list", "Read the audit log for the active org", PERM_READ, 1, 0, + 0, h_audit_list, CMD_ARGS(args_audit_list) }, + { "audit.verify", "Verify the voucher and audit hash chains", PERM_READ, 0, + 0, 0, h_audit_verify, CMD_ARGS(args_audit_verify) }, + { "backup.snapshot", "Create a consistent database snapshot", PERM_ADMIN, + 0, 1, 0, h_backup_snapshot, CMD_ARGS(args_backup_snapshot) }, +}; + +const struct cmd_table g_cmd_table_audit = { + g_cmd_audit, sizeof g_cmd_audit / sizeof g_cmd_audit[0] +}; |
