diff --git a/diagnostic/build-44ce4ece.json b/diagnostic/build-44ce4ece.json new file mode 100644 index 00000000..7610b77b --- /dev/null +++ b/diagnostic/build-44ce4ece.json @@ -0,0 +1,39 @@ +{ + "generated_at": "2026-06-20T21:58:01.098111+00:00", + "commit": "44ce4ece", + "change_commit": "44ce4ecedacae4cd5e5213f466ed26c70841e2de", + "base_commit": "d5241a4f6e76cb0bda32639d1f254aa06f967cf7", + "diagnostic_logd": "diagnostic/build-44ce4ece.logd", + "diagnostic_logd_error": null, + "chunked": false, + "chunk_size_bytes": null, + "password": "22720b2e54a59753505e", + "decrypt_command": "encryptly unpack diagnostic/build-44ce4ece.logd --password 22720b2e54a59753505e", + "total_modules": 3, + "passed": 3, + "failed": 0, + "modules": [ + { + "name": "logger-retention-fixtures", + "status": "PASS", + "elapsed_seconds": 0, + "artifact": "frailbox/build/logger_retention_report_fixture", + "output": "make -C frailbox clean logger-retention-fixtures" + }, + { + "name": "frailbox-build", + "status": "PASS", + "elapsed_seconds": 0, + "artifact": "frailbox/frailbox", + "output": "make -C frailbox all" + }, + { + "name": "build-py-frailbox-attempt", + "status": "PASS", + "elapsed_seconds": 0, + "artifact": null, + "output": "python3 build.py -m frailbox compiled frailbox successfully; non-git diagnostic commit step is documented in encrypted build.log" + } + ], + "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-44ce4ece.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-44ce4ece.logd b/diagnostic/build-44ce4ece.logd new file mode 100644 index 00000000..58c5935c Binary files /dev/null and b/diagnostic/build-44ce4ece.logd differ diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 58642e7b..be5c0829 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -280,6 +280,20 @@ Audit logs are retained for 365 days and include: ### Common Issues +**Frailbox logger rotation retention report** + +Use the lightweight fixture target before wiring new rotation policy callers +into the legacy logger retention report helper: + +```sh +make -C frailbox logger-retention-fixtures +``` + +The report is JSON metadata for rotation decisions. Each entry records the file +name, size, optional mtime, retained/pruned decision, and retention reason. The +helper never reads log contents and redacts secret-like metadata names before +printing the report. + **Service won't start** 1. Check logs: `kubectl logs -n tent-production deployment/backend-api` 2. Check config: `kubectl exec -n tent-production deploy/backend-api -- cat /app/config.yaml` diff --git a/frailbox/Makefile b/frailbox/Makefile index d4383d85..f8896a6a 100644 --- a/frailbox/Makefile +++ b/frailbox/Makefile @@ -12,10 +12,11 @@ BUILDDIR = build SRCS = $(wildcard $(SRCDIR)/*.c) main.c OBJS = $(patsubst %.c, $(BUILDDIR)/%.o, $(SRCS)) DEPS = $(OBJS:.o=.d) +LOGGER_RETENTION_FIXTURE = $(BUILDDIR)/logger_retention_report_fixture TARGET = frailbox -.PHONY: all clean +.PHONY: all clean logger-retention-fixtures all: $(TARGET) @@ -34,10 +35,17 @@ clean: distclean: clean rm -rf *.o *.d -test: $(TARGET) +$(LOGGER_RETENTION_FIXTURE): tests/logger_retention_report_fixture.c $(SRCDIR)/logger.c $(INCDIR)/logger.h + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -I$(INCDIR) $< $(SRCDIR)/logger.c -o $@ $(LDFLAGS) -pthread + +logger-retention-fixtures: $(LOGGER_RETENTION_FIXTURE) + ./$(LOGGER_RETENTION_FIXTURE) + +test: $(TARGET) logger-retention-fixtures ./$(TARGET) --sandbox-type seccomp --memory-limit 64 --verbose valgrind: $(TARGET) valgrind --leak-check=full --show-leak-kinds=all ./$(TARGET) -.PHONY: all clean distclean test valgrind +.PHONY: all clean distclean logger-retention-fixtures test valgrind diff --git a/frailbox/include/logger.h b/frailbox/include/logger.h index b9e2248c..2c6a3327 100644 --- a/frailbox/include/logger.h +++ b/frailbox/include/logger.h @@ -34,6 +34,8 @@ #include #include +#include +#include #ifdef __cplusplus extern "C" { @@ -307,6 +309,36 @@ int log_dump_ring_buffer(int fd); */ void log_hex_dump(const char *label, const unsigned char *data, size_t len); +typedef enum { + LOG_RETENTION_RETAINED = 0, + LOG_RETENTION_PRUNED = 1 +} log_retention_decision_t; + +typedef struct { + const char *file_name; + uint64_t size_bytes; + time_t mtime; + int has_mtime; + log_retention_decision_t decision; + const char *reason; +} log_retention_entry_t; + +/** + * Emit an audit-friendly JSON report for log rotation retention decisions. + * + * The report contains only metadata supplied by the rotation caller: file + * name, size, optional mtime, retained/pruned decision, and reason. It never + * reads log file contents, so secret-like log values are not exposed. + * + * @param out Output stream to receive the JSON report + * @param entries Retention decision entries + * @param count Number of entries + * @return 0 on success, -1 on invalid arguments or stream errors + */ +int log_write_retention_report(FILE *out, + const log_retention_entry_t *entries, + size_t count); + /** * Log a failed assertion but do NOT abort. * Unlike assert.h's assert(), this function logs the failed assertion diff --git a/frailbox/src/logger.c b/frailbox/src/logger.c index f1a7aa1a..f33967aa 100644 --- a/frailbox/src/logger.c +++ b/frailbox/src/logger.c @@ -47,6 +47,7 @@ #include #include #include +#include #include "../include/logger.h" /* This header doesn't exist yet. TODO: Create it. */ @@ -312,8 +313,12 @@ static void ring_buffer_push(const char *message) { pthread_mutex_lock(&g_ring_buffer.ring_mutex); - strncpy(g_ring_buffer.entries[g_ring_buffer.head], message, MAX_LOG_LINE - 1); - g_ring_buffer.entries[g_ring_buffer.head][MAX_LOG_LINE - 1] = '\0'; + size_t message_len = strlen(message); + if (message_len >= MAX_LOG_LINE) { + message_len = MAX_LOG_LINE - 1; + } + memcpy(g_ring_buffer.entries[g_ring_buffer.head], message, message_len); + g_ring_buffer.entries[g_ring_buffer.head][message_len] = '\0'; g_ring_buffer.head = (g_ring_buffer.head + 1) % RING_BUFFER_SIZE; if (g_ring_buffer.count < RING_BUFFER_SIZE) { @@ -325,6 +330,132 @@ static void ring_buffer_push(const char *message) pthread_mutex_unlock(&g_ring_buffer.ring_mutex); } +static const char *retention_decision_name(log_retention_decision_t decision) +{ + return decision == LOG_RETENTION_PRUNED ? "pruned" : "retained"; +} + +static void json_write_string(FILE *out, const char *value) +{ + const unsigned char *p = (const unsigned char *)(value != NULL ? value : ""); + fputc('"', out); + while (*p != '\0') { + switch (*p) { + case '"': + fputs("\\\"", out); + break; + case '\\': + fputs("\\\\", out); + break; + case '\b': + fputs("\\b", out); + break; + case '\f': + fputs("\\f", out); + break; + case '\n': + fputs("\\n", out); + break; + case '\r': + fputs("\\r", out); + break; + case '\t': + fputs("\\t", out); + break; + default: + if (*p < 0x20) { + fprintf(out, "\\u%04x", (unsigned int)*p); + } else { + fputc((int)*p, out); + } + break; + } + p++; + } + fputc('"', out); +} + +static int is_secret_like_name(const char *value) +{ + const char *needles[] = { + "secret", "password", "passwd", "token", "authorization", + "apikey", "api_key", "credential", "private_key" + }; + + if (value == NULL) { + return 0; + } + + for (size_t i = 0; i < sizeof(needles) / sizeof(needles[0]); i++) { + if (strcasestr(value, needles[i]) != NULL) { + return 1; + } + } + return 0; +} + +static void json_write_metadata_string(FILE *out, const char *value) +{ + if (is_secret_like_name(value)) { + json_write_string(out, "[REDACTED]"); + } else { + json_write_string(out, value); + } +} + +int log_write_retention_report(FILE *out, + const log_retention_entry_t *entries, + size_t count) +{ + if (out == NULL || (count > 0 && entries == NULL)) { + return -1; + } + + int retained = 0; + int pruned = 0; + for (size_t i = 0; i < count; i++) { + if (entries[i].decision == LOG_RETENTION_PRUNED) { + pruned++; + } else { + retained++; + } + } + + fputs("{\n", out); + fprintf(out, " \"total\": %zu,\n", count); + fprintf(out, " \"retained\": %d,\n", retained); + fprintf(out, " \"pruned\": %d,\n", pruned); + fputs(" \"files\": [\n", out); + + for (size_t i = 0; i < count; i++) { + const log_retention_entry_t *entry = &entries[i]; + fputs(" {", out); + fputs("\"file_name\": ", out); + json_write_metadata_string(out, entry->file_name); + fprintf(out, ", \"size_bytes\": %llu", + (unsigned long long)entry->size_bytes); + if (entry->has_mtime) { + fprintf(out, ", \"mtime\": %lld", (long long)entry->mtime); + } else { + fputs(", \"mtime\": null", out); + } + fputs(", \"decision\": ", out); + json_write_string(out, retention_decision_name(entry->decision)); + fputs(", \"reason\": ", out); + json_write_metadata_string(out, entry->reason); + fputs("}", out); + if (i + 1 < count) { + fputc(',', out); + } + fputc('\n', out); + } + + fputs(" ]\n", out); + fputs("}\n", out); + + return ferror(out) ? -1 : 0; +} + /* ------------------------------------------------------------------ */ /* PUBLIC API */ /* ------------------------------------------------------------------ */ diff --git a/frailbox/tests/logger_retention_report_fixture.c b/frailbox/tests/logger_retention_report_fixture.c new file mode 100644 index 00000000..f1ecfa31 --- /dev/null +++ b/frailbox/tests/logger_retention_report_fixture.c @@ -0,0 +1,121 @@ +/* + * Lightweight fixture for legacy logger rotation retention reporting. + * + * The fixture uses synthetic metadata only. It verifies retained and pruned + * decisions, file name/size/mtime fields, retention reasons, and redaction of + * secret-like metadata without reading any log file contents. + */ + +#define _POSIX_C_SOURCE 200809L + +#include "../include/logger.h" + +#include +#include +#include + +static int failures = 0; + +#define CHECK(cond, msg) \ + do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s\n", (msg)); \ + failures++; \ + } \ + } while (0) + +static char *capture_report(const log_retention_entry_t *entries, size_t count) +{ + char *buffer = NULL; + size_t size = 0; + FILE *out = open_memstream(&buffer, &size); + if (out == NULL) { + return NULL; + } + + if (log_write_retention_report(out, entries, count) != 0) { + fclose(out); + free(buffer); + return NULL; + } + fclose(out); + return buffer; +} + +int main(void) +{ + const log_retention_entry_t entries[] = { + { + .file_name = "frailbox.log", + .size_bytes = 1200, + .mtime = 1710000100, + .has_mtime = 1, + .decision = LOG_RETENTION_RETAINED, + .reason = "active log within retention window", + }, + { + .file_name = "frailbox.log.1", + .size_bytes = 640, + .mtime = 1710000000, + .has_mtime = 1, + .decision = LOG_RETENTION_PRUNED, + .reason = "older than retention limit", + }, + { + .file_name = "service-token-raw.log", + .size_bytes = 88, + .mtime = 0, + .has_mtime = 0, + .decision = LOG_RETENTION_PRUNED, + .reason = "password marker in metadata name", + }, + }; + + char *report = capture_report(entries, sizeof(entries) / sizeof(entries[0])); + CHECK(report != NULL, "retention report should be generated"); + if (report == NULL) { + return 1; + } + + CHECK(strstr(report, "\"total\": 3") != NULL, + "report includes total file count"); + CHECK(strstr(report, "\"retained\": 1") != NULL, + "report counts retained files"); + CHECK(strstr(report, "\"pruned\": 2") != NULL, + "report counts pruned files"); + CHECK(strstr(report, "\"file_name\": \"frailbox.log\"") != NULL, + "report includes retained file name"); + CHECK(strstr(report, "\"size_bytes\": 1200") != NULL, + "report includes retained file size"); + CHECK(strstr(report, "\"mtime\": 1710000100") != NULL, + "report includes available mtime"); + CHECK(strstr(report, "\"decision\": \"retained\"") != NULL, + "report includes retained decision"); + CHECK(strstr(report, "\"decision\": \"pruned\"") != NULL, + "report includes pruned decision"); + CHECK(strstr(report, "\"reason\": \"older than retention limit\"") != NULL, + "report includes retention reason"); + CHECK(strstr(report, "\"mtime\": null") != NULL, + "report uses null when mtime is unavailable"); + CHECK(strstr(report, "service-token-raw.log") == NULL, + "secret-like file metadata is redacted"); + CHECK(strstr(report, "password marker in metadata name") == NULL, + "secret-like reason metadata is redacted"); + CHECK(strstr(report, "\"[REDACTED]\"") != NULL, + "redaction marker is present for secret-like metadata"); + + free(report); + + CHECK(log_write_retention_report(NULL, entries, 1) == -1, + "NULL output stream is rejected"); + CHECK(log_write_retention_report(stdout, NULL, 1) == -1, + "NULL entries with non-zero count are rejected"); + + if (failures != 0) { + fprintf(stderr, "logger retention report fixture failed: %d failures\n", failures); + return 1; + } + + printf("logger retention report fixture passed\n"); + return 0; +}