diff --git a/.github/workflows/agfast-quality.yml b/.github/workflows/agfast-quality.yml new file mode 100644 index 0000000..6ac4f76 --- /dev/null +++ b/.github/workflows/agfast-quality.yml @@ -0,0 +1,81 @@ +name: AGFast Quality + +on: + push: + branches: + - main + - "fase-*" + pull_request: + branches: + - main + workflow_dispatch: + schedule: + - cron: "0 8 * * 1" + +jobs: + build-and-test: + name: Build, tests and evaluation + runs-on: ubuntu-latest + + steps: + - name: Descargar repositorio + uses: actions/checkout@v4 + + - name: Instalar dependencias base + run: | + sudo apt-get update + sudo apt-get install -y build-essential make python3 libssl-dev + + - name: Compilar proyecto + run: | + make clean + make + + - name: Ejecutar pruebas principales + run: | + make test-fastpath + make test-regression + make test-streaming + make test-guardsketch + make test-unit + + - name: Ejecutar evaluacion experimental reproducible + env: + AGFAST_EVAL_EVENTS: "10000" + AGFAST_EVAL_PIDS: "300" + AGFAST_EVAL_DIR: "/tmp/agfast_ci_evaluacion" + run: | + bash scripts/run_evaluation.sh + + - name: Limpiar artefactos locales + run: | + make clean + git status --short + + valgrind: + name: Valgrind optional + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' + + steps: + - name: Descargar repositorio + uses: actions/checkout@v4 + + - name: Instalar dependencias con Valgrind + run: | + sudo apt-get update + sudo apt-get install -y build-essential make python3 libssl-dev valgrind + + - name: Compilar proyecto + run: | + make clean + make + + - name: Ejecutar Valgrind + run: | + make test-valgrind + + - name: Limpiar artefactos locales + run: | + make clean + git status --short diff --git a/README.md b/README.md index 6ff9220..c37b6d9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ ### AgentGuard-C y AgentGuard FastPath +[![AGFast Quality](https://github.com/kapumota/AgentGuard-FastPath/actions/workflows/agfast-quality.yml/badge.svg)](https://github.com/kapumota/AgentGuard-FastPath/actions/workflows/agfast-quality.yml) + +[![CI](https://github.com/kapumota/AgentGuard-FastPath/actions/workflows/ci.yml/badge.svg)](https://github.com/kapumota/AgentGuard-FastPath/actions/workflows/ci.yml) + AgentGuard FastPath es un software de análisis defensivo de eventos de seguridad escrito en C. Procesa telemetría en formato JSONL o CSV, aplica una política configurable y produce métricas, alertas, relaciones proceso-archivo-red, líneas de tiempo y puntajes de riesgo. El repositorio contiene dos componentes: diff --git a/docs/CI.md b/docs/CI.md new file mode 100644 index 0000000..f1040b2 --- /dev/null +++ b/docs/CI.md @@ -0,0 +1,105 @@ +### CI/CD visible + +#### Objetivo + +La Fase 9.2 agrega validación automática con GitHub Actions. + +El objetivo es que cada push y cada pull request ejecuten compilación, pruebas y evaluación mínima antes de fusionar cambios a `main`. + +#### Archivo principal + +```text +.github/workflows/ci.yml +``` + +#### Eventos del workflow + +El workflow se ejecuta en: + +- push a `main`; +- push a ramas `fase-*`; +- pull request hacia `main`; +- ejecución manual con `workflow_dispatch`; +- ejecución programada semanal. + +#### Job principal + +El job principal ejecuta: + +```bash +make clean +make +make test-fastpath +make test-regression +make test-streaming +make test-guardsketch +make test-unit +bash scripts/run_evaluation.sh +make clean +``` + +#### Evaluación en CI + +La evaluación experimental usa un dataset reducido para evitar tiempos excesivos en GitHub Actions. + +Variables usadas: + +```text +AGFAST_EVAL_EVENTS=10000 +AGFAST_EVAL_PIDS=300 +AGFAST_EVAL_DIR=/tmp/agfast_ci_evaluacion +``` + +Esto mantiene el flujo reproducible sin generar archivos pesados dentro del repositorio. + +#### Job opcional de Valgrind + +Valgrind se ejecuta en un job separado. + +Ese job corre solo en: + +- ejecución manual; +- ejecución programada semanal. + +Motivo: + +- Valgrind puede ser más lento; +- no debe bloquear cada push pequeño; +- sigue disponible para validación profunda. + +Comando ejecutado: + +```bash +make test-valgrind +``` + +#### Badge en README + +El README incluye un badge de estado: + +```text +CI +``` + +Ese badge permite verificar rápidamente si el proyecto compila y pasa pruebas. + +#### Criterio de aceptación + +La Fase 9.2 se considera correcta si: + +- GitHub Actions aparece en la pestaña Actions; +- el workflow `CI` se ejecuta en PR; +- el job principal pasa; +- el badge aparece en README; +- Valgrind queda disponible como validación manual o programada; +- la ruta normal del proyecto no cambia. + +#### Alcance + +Esta fase no modifica código fuente. + +Esta fase no modifica pruebas existentes. + +Esta fase no agrega dependencias runtime al proyecto. + +Esta fase solo agrega automatización de calidad. diff --git a/scripts/run_evaluation.sh b/scripts/run_evaluation.sh index 84f0371..6539212 100755 --- a/scripts/run_evaluation.sh +++ b/scripts/run_evaluation.sh @@ -250,19 +250,57 @@ run_timed() { local label="$1" shift local out_file="$EVAL_DIR/time_${label}.txt" + local status_file="$EVAL_DIR/status_${label}.txt" + local status=0 + + set +e /usr/bin/time -f "%e %M" -o "$out_file" "$@" + status=$? + set -e + + echo "$status" > "$status_file" + return "$status" +} + +accept_status() { + local label="$1" + local status="$2" + shift 2 + + for allowed in "$@"; do + if [ "$status" -eq "$allowed" ]; then + echo "$label retorno codigo $status aceptado para esta evaluacion" + return 0 + fi + done + + echo "$label retorno codigo $status no esperado" >&2 + return "$status" } echo "Ejecutando agfast sobre dataset de evaluacion" -run_timed agfast ./bin/agfast analyze "$EVENTS_FILE" \ +if run_timed agfast ./bin/agfast analyze "$EVENTS_FILE" \ --policy "$POLICY_FILE" \ --risk \ --report "$REPORT_JSON" \ --html "$REPORT_HTML" \ - --alerts-csv "$ALERTS_CSV" + --alerts-csv "$ALERTS_CSV"; then + echo "agfast finalizo sin codigo de alerta" +else + status=$? + # En evaluacion, codigo 2 se acepta como hallazgo de alertas/riesgo. + # No representa fallo del binario si los reportes fueron generados. + accept_status "agfast analyze" "$status" 2 +fi echo "Evaluando robustez con entradas corruptas usando tail" -run_timed corrupt_tail ./bin/agfast tail "$CORRUPT_FILE" --policy "$POLICY_FILE" > "$TAIL_OUT" +if run_timed corrupt_tail ./bin/agfast tail "$CORRUPT_FILE" --policy "$POLICY_FILE" > "$TAIL_OUT"; then + echo "tail con entradas corruptas finalizo sin codigo de alerta" +else + status=$? + # Se acepta codigo 2 si tail detecta alertas durante la evaluacion. + accept_status "agfast tail" "$status" 2 +fi echo "Ejecutando simulacion de ranking de sketches" python3 experiments/sketch_rank_simulation.py > "$SKETCH_SIM_OUT" @@ -332,11 +370,31 @@ def read_time(label): p = out_dir / ("time_%s.txt" % label) if not p.exists(): return {"seconds": None, "max_rss_kb": None} - raw = p.read_text(encoding="utf-8").strip().split() - if len(raw) >= 2: - return {"seconds": float(raw[0]), "max_rss_kb": int(raw[1])} + + # /usr/bin/time puede agregar una linea como: + # Command exited with non-zero status 2 + # Por eso se busca la ultima linea con dos valores numericos. + lines = p.read_text(encoding="utf-8", errors="ignore").splitlines() + for line in reversed(lines): + raw = line.strip().split() + if len(raw) >= 2: + try: + return { + "seconds": float(raw[0]), + "max_rss_kb": int(float(raw[1])), + } + except ValueError: + continue + return {"seconds": None, "max_rss_kb": None} +def read_status(label): + p = out_dir / ("status_%s.txt" % label) + if not p.exists(): + return None + raw = p.read_text(encoding="utf-8").strip() + return int(raw) if raw.isdigit() else None + report_size = report_path.stat().st_size if report_path.exists() else 0 alerts_size = alerts_path.stat().st_size if alerts_path.exists() else 0 tail_text = tail_path.read_text(encoding="utf-8", errors="ignore") if tail_path.exists() else "" @@ -344,7 +402,9 @@ robust_tail = "Eventos procesados en tail" in tail_text summary = { "agfast": read_time("agfast"), + "agfast_exit_code": read_status("agfast"), "corrupt_tail": read_time("corrupt_tail"), + "corrupt_tail_exit_code": read_status("corrupt_tail"), "precision_alerts_vs_labels": precision, "recall_alerts_vs_labels": recall, "true_positives": tp, @@ -381,6 +441,13 @@ md.append("| tail con entradas corruptas | {seconds} | {rss} |".format( rss=summary["corrupt_tail"]["max_rss_kb"], )) md.append("") +md.append("#### Códigos de salida") +md.append("") +md.append("| Componente | Código | Interpretación |") +md.append("|---|---:|---|") +md.append("| agfast analyze | %s | 0 normal, 2 alertas detectadas aceptadas en evaluación |" % summary.get("agfast_exit_code")) +md.append("| tail con entradas corruptas | %s | 0 normal, 2 alertas detectadas aceptadas en evaluación |" % summary.get("corrupt_tail_exit_code")) +md.append("") md.append("#### Calidad de detección") md.append("") md.append("| Métrica | Valor |") @@ -397,14 +464,14 @@ md.append("#### Tamaño de reportes") md.append("") md.append("| Archivo | Bytes |") md.append("|---|---:|") -md.append("| reporte JSON | %d |" % report_size) -md.append("| alertas CSV | %d |" % alerts_size) +md.append("| reporte JSON | %d |" % summary["report_size_bytes"]) +md.append("| alertas CSV | %d |" % summary["alerts_size_bytes"]) md.append("") md.append("#### Robustez") md.append("") md.append("| Prueba | Resultado |") md.append("|---|---|") -md.append("| tail con líneas corruptas | %s |" % ("ok" if robust_tail else "fallo")) +md.append("| tail con líneas corruptas | %s |" % ("ok" if summary["robust_tail_with_corrupt_lines"] else "fallo")) md.append("| GuardSketch userspace | %s |" % guardsketch_status) md.append("") md.append("#### Archivos generados")