Services: - ja4sentinel: TLS/JA4 fingerprint capture daemon (Go, libpcap) - logcorrelator: JA4 log correlation engine (Go, ClickHouse) - mod_reqin_log: Apache module (C, JSON request logging) - bot_detector: ML bot detection pipeline (Python) - dashboard: FastAPI/Streamlit analytics UI (Python) Shared libraries: - shared/go/ja4common: logger, config, shutdown, ipfilter (Go module) - shared/python/ja4_common: ClickHouseClient, ClickHouseSettings (Python package) - shared/clickhouse/: canonical SQL migrations (10 files) Build & packaging: - Unified 3-stage Dockerfile.package for Go RPMs (el8/el9/el10) - go.work workspace linking sentinel, correlator, ja4common - Makefile with test-all, build-all, rpm-* targets Fixes applied: - go.work: 1.21 → 1.24.6 (required by sentinel) - correlator Dockerfiles: golang:1.21 → golang:1.24 - replace directives in go.mod for ja4common local path - pyproject.toml: setuptools.backends → setuptools.build_meta - Removed static libpcap linking (unavailable on Rocky 9) - Fixed data races in output/writers_test.go (sync.Mutex + atomic.Int32) - Rewrote corrupted test files (logger_test.go × 2) Test coverage: - correlator: 67.1% total (unixsocket 80.5%, config 91.7%, app 83.3%, multi 87.7%, stdout 100%) - sentinel: all 10 packages pass (api, capture, config, fingerprint, ipfilter, logging, output, tlsparse) Documentation: - README.md + docs/ (architecture, development, 5 services, shared libs, DB schema & migrations) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
227 lines
6.0 KiB
C
227 lines
6.0 KiB
C
/*
|
|
* test_header_handling.c - Unit tests for header handling (truncation and limits)
|
|
*/
|
|
|
|
#include <stdarg.h>
|
|
#include <stddef.h>
|
|
#include <setjmp.h>
|
|
#include <cmocka.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <apr_strings.h>
|
|
#include <apr_tables.h>
|
|
#include <apr_pools.h>
|
|
#include <apr_general.h>
|
|
|
|
/* Mock header truncation function */
|
|
static char *truncate_header_value(apr_pool_t *pool, const char *value, int max_len)
|
|
{
|
|
if (value == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
size_t len = strlen(value);
|
|
if ((int)len > max_len) {
|
|
return apr_pstrmemdup(pool, value, max_len);
|
|
}
|
|
return apr_pstrdup(pool, value);
|
|
}
|
|
|
|
/* Mock header matching function */
|
|
static int header_name_matches(const char *configured, const char *actual)
|
|
{
|
|
return strcasecmp(configured, actual) == 0;
|
|
}
|
|
|
|
/* Test: Header value within limit */
|
|
static void test_header_truncation_within_limit(void **state)
|
|
{
|
|
apr_pool_t *pool;
|
|
apr_pool_create(&pool, NULL);
|
|
|
|
const char *value = "short value";
|
|
char *result = truncate_header_value(pool, value, 256);
|
|
|
|
assert_string_equal(result, "short value");
|
|
|
|
apr_pool_destroy(pool);
|
|
}
|
|
|
|
/* Test: Header value exactly at limit */
|
|
static void test_header_truncation_exact_limit(void **state)
|
|
{
|
|
apr_pool_t *pool;
|
|
apr_pool_create(&pool, NULL);
|
|
|
|
const char *value = "exactly10c";
|
|
char *result = truncate_header_value(pool, value, 10);
|
|
|
|
assert_string_equal(result, "exactly10c");
|
|
|
|
apr_pool_destroy(pool);
|
|
}
|
|
|
|
/* Test: Header value exceeds limit */
|
|
static void test_header_truncation_exceeds_limit(void **state)
|
|
{
|
|
apr_pool_t *pool;
|
|
apr_pool_create(&pool, NULL);
|
|
|
|
const char *value = "this is a very long header value that should be truncated";
|
|
char *result = truncate_header_value(pool, value, 15);
|
|
|
|
assert_string_equal(result, "this is a very ");
|
|
assert_int_equal(strlen(result), 15);
|
|
|
|
apr_pool_destroy(pool);
|
|
}
|
|
|
|
/* Test: Header value with limit of 1 */
|
|
static void test_header_truncation_limit_one(void **state)
|
|
{
|
|
apr_pool_t *pool;
|
|
apr_pool_create(&pool, NULL);
|
|
|
|
const char *value = "abc";
|
|
char *result = truncate_header_value(pool, value, 1);
|
|
|
|
assert_string_equal(result, "a");
|
|
|
|
apr_pool_destroy(pool);
|
|
}
|
|
|
|
/* Test: NULL header value */
|
|
static void test_header_truncation_null(void **state)
|
|
{
|
|
apr_pool_t *pool;
|
|
apr_pool_create(&pool, NULL);
|
|
|
|
char *result = truncate_header_value(pool, NULL, 256);
|
|
|
|
assert_null(result);
|
|
|
|
apr_pool_destroy(pool);
|
|
}
|
|
|
|
/* Test: Empty header value */
|
|
static void test_header_truncation_empty(void **state)
|
|
{
|
|
apr_pool_t *pool;
|
|
apr_pool_create(&pool, NULL);
|
|
|
|
const char *value = "";
|
|
char *result = truncate_header_value(pool, value, 256);
|
|
|
|
assert_string_equal(result, "");
|
|
|
|
apr_pool_destroy(pool);
|
|
}
|
|
|
|
/* Test: Header name matching (case-insensitive) */
|
|
static void test_header_name_matching_case_insensitive(void **state)
|
|
{
|
|
assert_true(header_name_matches("X-Request-Id", "x-request-id"));
|
|
assert_true(header_name_matches("user-agent", "User-Agent"));
|
|
assert_true(header_name_matches("HOST", "host"));
|
|
}
|
|
|
|
/* Test: Header name matching (different headers) */
|
|
static void test_header_name_matching_different(void **state)
|
|
{
|
|
assert_false(header_name_matches("X-Request-Id", "X-Trace-Id"));
|
|
assert_false(header_name_matches("Host", "User-Agent"));
|
|
}
|
|
|
|
/* Test: Multiple headers with limit */
|
|
static void test_header_count_limit(void **state)
|
|
{
|
|
apr_pool_t *pool;
|
|
apr_pool_create(&pool, NULL);
|
|
|
|
/* Simulate configured headers */
|
|
const char *configured[] = {"X-Request-Id", "X-Trace-Id", "User-Agent", "Referer"};
|
|
int configured_count = 4;
|
|
int max_headers = 2;
|
|
|
|
/* Simulate present headers */
|
|
const char *present[] = {"X-Request-Id", "User-Agent", "Referer"};
|
|
int present_count = 3;
|
|
|
|
int logged_count = 0;
|
|
for (int i = 0; i < configured_count && logged_count < max_headers; i++) {
|
|
for (int j = 0; j < present_count; j++) {
|
|
if (header_name_matches(configured[i], present[j])) {
|
|
logged_count++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
assert_int_equal(logged_count, 2);
|
|
|
|
apr_pool_destroy(pool);
|
|
}
|
|
|
|
/* Test: Header value with special JSON characters */
|
|
static void test_header_value_json_special(void **state)
|
|
{
|
|
apr_pool_t *pool;
|
|
apr_pool_create(&pool, NULL);
|
|
|
|
const char *value = "test\"value\\with\tspecial";
|
|
char *truncated = truncate_header_value(pool, value, 256);
|
|
|
|
/* Truncation should preserve the value */
|
|
assert_string_equal(truncated, "test\"value\\with\tspecial");
|
|
|
|
apr_pool_destroy(pool);
|
|
}
|
|
|
|
/* Test: Unicode in header value (UTF-8) */
|
|
static void test_header_value_unicode(void **state)
|
|
{
|
|
apr_pool_t *pool;
|
|
apr_pool_create(&pool, NULL);
|
|
|
|
const char *value = "Mozilla/5.0 (compatible; 日本語)";
|
|
char *result = truncate_header_value(pool, value, 50);
|
|
|
|
/* Should be truncated but valid */
|
|
assert_non_null(result);
|
|
assert_true(strlen(result) <= 50);
|
|
|
|
apr_pool_destroy(pool);
|
|
}
|
|
|
|
static int group_setup(void **state)
|
|
{
|
|
(void)state;
|
|
return apr_initialize();
|
|
}
|
|
|
|
static int group_teardown(void **state)
|
|
{
|
|
(void)state;
|
|
apr_terminate();
|
|
return 0;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
const struct CMUnitTest tests[] = {
|
|
cmocka_unit_test(test_header_truncation_within_limit),
|
|
cmocka_unit_test(test_header_truncation_exact_limit),
|
|
cmocka_unit_test(test_header_truncation_exceeds_limit),
|
|
cmocka_unit_test(test_header_truncation_limit_one),
|
|
cmocka_unit_test(test_header_truncation_null),
|
|
cmocka_unit_test(test_header_truncation_empty),
|
|
cmocka_unit_test(test_header_name_matching_case_insensitive),
|
|
cmocka_unit_test(test_header_name_matching_different),
|
|
cmocka_unit_test(test_header_count_limit),
|
|
cmocka_unit_test(test_header_value_json_special),
|
|
cmocka_unit_test(test_header_value_unicode),
|
|
};
|
|
|
|
return cmocka_run_group_tests(tests, group_setup, group_teardown);
|
|
}
|