Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,13 @@ ebpf-info:
.PHONY: test-guardsketch
test-guardsketch:
bash tests/test_guardsketch.sh

.PHONY: test-unit
test-unit:
mkdir -p obj/unit
$(CC) $(CFLAGS) -Ithird_party/unity tests/unit/test_probabilistic.c third_party/unity/unity.c -lm -o obj/unit/test_probabilistic
$(CC) $(CFLAGS) -Ithird_party/unity tests/unit/test_graph_model.c third_party/unity/unity.c -o obj/unit/test_graph_model
$(CC) $(CFLAGS) -Ithird_party/unity tests/unit/test_risk_helpers.c third_party/unity/unity.c -o obj/unit/test_risk_helpers
./obj/unit/test_probabilistic
./obj/unit/test_graph_model
./obj/unit/test_risk_helpers
122 changes: 122 additions & 0 deletions docs/TESTS_UNITARIOS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
### Tests unitarios en C con Unity

#### Objetivo

La Fase 9.1 agrega pruebas unitarias reales en C para fortalecer la base interna de AgentGuard FastPath antes del release `v1.0.0`.

Hasta esta fase, el proyecto ya cuenta con pruebas de integración, regresión, streaming, GuardSketch y evaluación experimental.

Esta fase agrega una capa más pequeña y controlada para validar propiedades internas.

#### Por qué Unity

Unity se usa porque:

- es liviano;
- puede versionarse en `third_party/`;
- no requiere instalación del sistema;
- funciona bien con C puro;
- es apropiado para CI;
- permite probar invariantes simples sin depender de servicios externos.

#### Estructura agregada

```text
third_party/unity/
tests/unit/
tests/unit/test_probabilistic.c
tests/unit/test_graph_model.c
tests/unit/test_risk_helpers.c
```

#### Target agregado

```bash
make test-unit
```

#### Pruebas probabilísticas

El archivo `tests/unit/test_probabilistic.c` valida modelos unitarios para:

- Bloom Filter;
- Count-Min Sketch;
- HyperLogLog;
- Space-Saving;
- Odd Sketch.

Propiedades cubiertas:

- un elemento insertado en Bloom debe encontrarse;
- Bloom no debe producir falsos negativos para elementos insertados;
- Count-Min Sketch no debe subestimar frecuencias insertadas;
- claves distintas deben seguir siendo consultables;
- HyperLogLog para conjunto vacío debe estimar cerca de cero;
- elementos repetidos no deben crecer como cardinalidad única;
- Space-Saving debe conservar elementos frecuentes;
- Odd Sketch debe dar más coincidencia para entradas similares.

#### Pruebas de grafo

El archivo `tests/unit/test_graph_model.c` valida un modelo pequeño de grafo.

Propiedades cubiertas:

- insertar nodo de proceso;
- insertar relación proceso-archivo;
- insertar relación proceso-destino;
- rechazar arista si falta un nodo;
- consultar PID inexistente;
- respetar límite de recorrido.

#### Pruebas de riesgo

El archivo `tests/unit/test_risk_helpers.c` valida helpers pequeños para scoring.

Propiedades cubiertas:

- score cero sin señales;
- score alto con archivo sensible y destino bloqueado;
- score máximo limitado a 100;
- niveles de riesgo;
- incremento por destinos únicos.

#### Alcance

Esta fase no modifica `src/`.

Esta fase no cambia el CLI.

Esta fase no cambia el motor de detección.

Esta fase no agrega dependencias del sistema.

Esta fase no integra todavía los tests con una biblioteca interna porque el proyecto aún conserva una arquitectura de binario principal.

#### Limitación honesta

Estos tests son unitarios autocontenidos y validan invariantes de diseño.

Una fase posterior puede conectar estos tests directamente a una biblioteca `libagfast` cuando el motor se separe formalmente del CLI.

#### Validación completa

```bash
make clean
make
make test-fastpath
make test-regression
make test-streaming
make test-guardsketch
make test-unit
make clean
```

#### Criterio de aceptación

La fase se considera correcta si:

- `make test-unit` compila y ejecuta las tres suites;
- no se requiere instalar Unity;
- no se rompe ninguna prueba previa;
- no se modifica el código fuente de producción.
138 changes: 138 additions & 0 deletions tests/unit/test_graph_model.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#include "unity.h"

#include <stdbool.h>
#include <stdio.h>
#include <string.h>

/* Tests unitarios autocontenidos para un modelo de grafo mínimo. */

#define GRAPH_MAX_NODES 32u
#define GRAPH_MAX_EDGES 64u
#define GRAPH_NAME_SIZE 64u

typedef enum { GRAPH_NODE_PROCESS = 1, GRAPH_NODE_FILE = 2, GRAPH_NODE_DESTINATION = 3 } graph_node_type_t;

typedef struct { int id; graph_node_type_t type; char name[GRAPH_NAME_SIZE]; bool used; } graph_node_t;

typedef struct { int from; int to; char relation[GRAPH_NAME_SIZE]; bool used; } graph_edge_t;

typedef struct { graph_node_t nodes[GRAPH_MAX_NODES]; graph_edge_t edges[GRAPH_MAX_EDGES]; } graph_model_t;

static void graphInit(graph_model_t *graph) { memset(graph, 0, sizeof(*graph)); }

static bool graphAddNode(graph_model_t *graph, int id, graph_node_type_t type, const char *name) {
for (size_t i = 0; i < GRAPH_MAX_NODES; ++i) {
if (graph->nodes[i].used && graph->nodes[i].id == id) { return true; }
}
for (size_t i = 0; i < GRAPH_MAX_NODES; ++i) {
if (!graph->nodes[i].used) {
graph->nodes[i].used = true;
graph->nodes[i].id = id;
graph->nodes[i].type = type;
snprintf(graph->nodes[i].name, sizeof(graph->nodes[i].name), "%s", name);
return true;
}
}
return false;
}

static const graph_node_t *graphFindNode(const graph_model_t *graph, int id) {
for (size_t i = 0; i < GRAPH_MAX_NODES; ++i) {
if (graph->nodes[i].used && graph->nodes[i].id == id) { return &graph->nodes[i]; }
}
return NULL;
}

static bool graphAddEdge(graph_model_t *graph, int from, int to, const char *relation) {
if (graphFindNode(graph, from) == NULL || graphFindNode(graph, to) == NULL) { return false; }
for (size_t i = 0; i < GRAPH_MAX_EDGES; ++i) {
if (!graph->edges[i].used) {
graph->edges[i].used = true;
graph->edges[i].from = from;
graph->edges[i].to = to;
snprintf(graph->edges[i].relation, sizeof(graph->edges[i].relation), "%s", relation);
return true;
}
}
return false;
}

static int graphCountEdgesFrom(const graph_model_t *graph, int from) {
int count = 0;
for (size_t i = 0; i < GRAPH_MAX_EDGES; ++i) {
if (graph->edges[i].used && graph->edges[i].from == from) { count += 1; }
}
return count;
}

static int graphTraverseLimited(const graph_model_t *graph, int from, int limit) {
int emitted = 0;
for (size_t i = 0; i < GRAPH_MAX_EDGES && emitted < limit; ++i) {
if (graph->edges[i].used && graph->edges[i].from == from) { emitted += 1; }
}
return emitted;
}

static void testGraphInsertProcessNode(void) {
graph_model_t graph;
const graph_node_t *node = NULL;
graphInit(&graph);
TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python"));
node = graphFindNode(&graph, 501);
TEST_ASSERT_NOT_NULL(node);
TEST_ASSERT_EQUAL_INT(GRAPH_NODE_PROCESS, node->type);
}

static void testGraphInsertProcessFileRelation(void) {
graph_model_t graph;
graphInit(&graph);
TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python"));
TEST_ASSERT_TRUE(graphAddNode(&graph, 1001, GRAPH_NODE_FILE, "/etc/passwd"));
TEST_ASSERT_TRUE(graphAddEdge(&graph, 501, 1001, "open"));
TEST_ASSERT_EQUAL_INT(1, graphCountEdgesFrom(&graph, 501));
}

static void testGraphInsertProcessDestinationRelation(void) {
graph_model_t graph;
graphInit(&graph);
TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python"));
TEST_ASSERT_TRUE(graphAddNode(&graph, 2001, GRAPH_NODE_DESTINATION, "203.0.113.10"));
TEST_ASSERT_TRUE(graphAddEdge(&graph, 501, 2001, "connect"));
TEST_ASSERT_EQUAL_INT(1, graphCountEdgesFrom(&graph, 501));
}

static void testGraphRejectEdgeWithMissingNode(void) {
graph_model_t graph;
graphInit(&graph);
TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python"));
TEST_ASSERT_FALSE(graphAddEdge(&graph, 501, 9999, "connect"));
}

static void testGraphFindMissingPid(void) {
graph_model_t graph;
graphInit(&graph);
TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python"));
TEST_ASSERT_TRUE(graphFindNode(&graph, 9999) == NULL);
}

static void testGraphTraversalLimit(void) {
graph_model_t graph;
graphInit(&graph);
TEST_ASSERT_TRUE(graphAddNode(&graph, 501, GRAPH_NODE_PROCESS, "python"));
for (int i = 0; i < 8; ++i) {
TEST_ASSERT_TRUE(graphAddNode(&graph, 2000 + i, GRAPH_NODE_DESTINATION, "destino"));
TEST_ASSERT_TRUE(graphAddEdge(&graph, 501, 2000 + i, "connect"));
}
TEST_ASSERT_EQUAL_INT(3, graphTraverseLimited(&graph, 501, 3));
}

int main(void) {
UNITY_BEGIN();
RUN_TEST(testGraphInsertProcessNode);
RUN_TEST(testGraphInsertProcessFileRelation);
RUN_TEST(testGraphInsertProcessDestinationRelation);
RUN_TEST(testGraphRejectEdgeWithMissingNode);
RUN_TEST(testGraphFindMissingPid);
RUN_TEST(testGraphTraversalLimit);
return UNITY_END();
}
Loading
Loading