Reticulum: Escáner de Seguridad Avanzado para Infraestructura en la Nube

Reticulum: Escáner de Seguridad Avanzado para Infraestructura en la Nube

El Desafío de Seguridad DevSecOps

Tu canal de despliegue probablemente está filtrando problemas de seguridad en este momento. Cada commit, cada actualización de Helm chart, cada cambio de infraestructura introduce vulnerabilidades potenciales que se escapan de las revisiones de seguridad tradicionales. Como ingeniero DevSecOps, necesitas visibilidad en toda tu postura de seguridad en la nube, no solo en el despliegue, sino a lo largo de todo tu ciclo de vida de desarrollo.

Reticulum aborda este desafío de frente. No es solo otro escáner de seguridad, es una plataforma integral de inteligencia de seguridad DevSecOps escrita en lenguaje D y diseñada para integrarse perfectamente en tus canales CI/CD y proporcionar validación continua de seguridad para tu infraestructura nativa de la nube.

Instalación y Configuración

Reticulum está diseñado para una integración perfecta en tus flujos de trabajo DevSecOps existentes. Aquí te mostramos cómo empezar:

Instalación con Docker (Recomendada)

Reticulum soporta compilaciones multi-arquitectura nativas (Apple Silicon/ARM64 e Intel/AMD64) 1 .

# Clonar repositorio
git clone https://github.com/plexicus/reticulum.git
cd reticulum

# Construir imagen
docker build -t reticulum .

Ejecutar con Docker

# Montar directorio actual a /data y analizar
docker run --rm -v $(pwd):/data reticulum \
  -p /data/tests/monorepo-06 \
  -s /data/tests/monorepo-06/trivy.sarif

Construir desde Código Fuente

Requisitos:

  • Compilador D Language (LDC2) 2
  • DUB (Package Manager)
  • Herramientas de construcción estándar
git clone https://github.com/plexicus/reticulum.git
cd reticulum
dub build --compiler=ldc2

Inicio Rápido de DevSecOps

Comienza a escanear tu infraestructura inmediatamente:

# Análisis completo con SARIF
./reticulum -p tests/monorepo-06 -s tests/monorepo-06/trivy.sarif

# Solo análisis de exposición (sin SARIF)
./reticulum -p tests/monorepo-06 --scan-only

# Generar reporte JSON
./reticulum -p ./src -s results.sarif -o report.json

Integración en Pipeline CI/CD

Añade Reticulum a tu pipeline para escaneo de seguridad automatizado:

# Ejemplo de GitHub Actions
name: Escaneo de Seguridad
on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Construir Reticulum
        run: |
          git clone https://github.com/plexicus/reticulum.git
          cd reticulum
          docker build -t reticulum .

      - name: Ejecutar Escaneo de Seguridad
        run: |
          docker run --rm -v $(pwd):/data reticulum \
            -p /data \
            -s /data/trivy.sarif \
            -o /data/security-report.json

      - name: Subir Informe de Seguridad
        uses: actions/upload-artifact@v3
        with:
          name: security-report
          path: security-report.json

Arquitectura del Sistema

Reticulum está construido con un enfoque modular en lenguaje D, diseñado para analizar tu infraestructura sistemáticamente a través de un pipeline de tres fases 3 :

  1. Fase 1: Descubrimiento de Servicios - Mapeo de servicios y gráficos Helm
  2. Fase 2: Análisis de Exposición - Análisis de configuraciones de despliegue de Kubernetes
  3. Fase 3: Puntuación de Vulnerabilidades - Ingestión de SARIF y aplicación de puntuación contextual

Componentes Principales

  • app.d - Punto de entrada CLI y orquestador 4
  • model.d - Definiciones de datos centrales (Service, Chart, Finding, RiskProfile) 5
  • analyzer.d - Lógica de análisis de exposición 6
  • ingestor.d - Procesamiento de SARIF y puntuación contextual 7
  • rules/engine.d - Motor de reglas para análisis contextual

Patrones de Exposición y Riesgo

Reticulum detecta sistemáticamente múltiples patrones de exposición:

Niveles de Exposición

  • ALTO: Requiere atención inmediata

    • Servicios LoadBalancer → Acceso directo a internet
    • Servicios NodePort → A través del firewall del nodo
    • Controladores de Ingress → Puerta de entrada al tráfico externo
    • Acceso a la Red del Host → Compartiendo la pila de red del host
  • MEDIO: Monitorear de cerca

    • Ingress Estándar → A través de tu puerta de enlace
    • Malla de Servicios → A través de tu infraestructura de malla
  • BAJO: Generalmente seguro

    • Servicios Internos → Solo accesibles desde dentro
    • Servicios de Base de Datos → Almacenamiento de datos interno

Patrones de Riesgo

  • Contenedores Privilegiados
  • Ejecución de Usuario Root
  • Acceso al Namespace del Host
  • Capacidades Peligrosas de Linux (SYS_ADMIN, SYS_PTRACE)
  • Escalamiento de Privilegios
  • Gestión de Recursos

Formatos de Salida

Reticulum proporciona múltiples formatos de salida:

Salida de Consola Codificada por Colores

La salida CLI proporciona indicadores visuales claros con códigos de color ANSI 8 :

  • Rojo: P0_BLEEDING (90-100) - Acción inmediata requerida
  • Amarillo: P1_CRITICAL (70-89) - Monitorear de cerca
  • Azul: P2_HIGH (50-69) - Generalmente seguro

Reportes JSON

Datos estructurados para automatización con servicios y hallazgos 9 .

SARIF Enriquecido

SARIF original aumentado con puntuaciones contextuales de Reticulum.

Ejemplos Prácticos

Escaneo Básico

# Escanear repositorio de microservicios
docker run --rm -v $(pwd):/data reticulum \
  -p /data/microservices \
  -s /data/trivy.sarif

Análisis Detallado

# Generar salida JSON para automatización
./reticulum -p ./microservices -s ./trivy.sarif -o security-report.json

Modo Solo Exposición

# Análisis de exposición sin procesar vulnerabilidades
./reticulum -p ./charts --scan-only -o exposure.json

Rendimiento

Reticulum está optimizado para alto rendimiento:

  • Tiempo de Escaneo: Menos de 30 segundos para repositorios complejos
  • Uso de Memoria: Menos de 512MB en pico
  • Escalabilidad: Maneja más de 100 gráficos sin problemas
  • Procesamiento Concurrente: Múltiples trabajadores escaneando en paralelo

Casos de Uso de DevSecOps

1. Auditorías Pre-Despliegue

# Ejecutar escaneo de seguridad completo
docker run --rm -v $(pwd):/data reticulum \
  -p /data/k8s-manifests \
  -s /data/trivy.sarif \
  -o /data/pre-deployment-audit.json

2. Integración CI/CD

#!/bin/bash
# security-check.sh - Añadir a tu pipeline CI/CD
echo "🔍 Ejecutando escaneo de seguridad Reticulum..."
docker run --rm -v $(pwd):/data reticulum \
  -p /data -s /data/trivy.sarif -o /data/security-report.json

# Verificar hallazgos de alto riesgo
HIGH_RISK=$(jq '.services[] | select(.findings[].priority == "P0_BLEEDING")' /data/security-report.json | wc -l)
if [ "$HIGH_RISK" -gt 0 ]; then
    echo "❌ $HIGH_RISK hallazgos críticos detectados"
    exit 1
fi

3. Validación Multi-Entorno

# Comparar entornos
./reticulum -p ./charts-dev --scan-only -o dev-exposure.json
./reticulum -p ./charts-prod --scan-only -o prod-exposure.json
diff dev-exposure.json prod-exposure.json

Comienza Hoy

Inicio Rápido (5 minutos)

# Construir y ejecutar con Docker
git clone https://github.com/plexicus/reticulum.git
cd reticulum
docker build -t reticulum .
docker run --rm -v $(pwd):/data reticulum -p /data/tests/monorepo-06 -s /data/tests/monorepo-06/trivy.sarif

Próximos Pasos

  1. Analizar Resultados: Revisar los niveles de exposición y prioridades P0-P4
  2. Solucionar Problemas Críticos: Abordar primero los hallazgos P0_BLEEDING
  3. Integrar en CI/CD: Añadir escaneo de seguridad a tu pipeline
  4. Monitorear Regularmente: Configurar escaneos periódicos

Obtener Ayuda y Soporte

  • Documentación: Repositorio de GitHub
  • Código Fuente: Escrito en lenguaje D con arquitectura modular
  • Problemas: Reportar errores o solicitar características en GitHub

La seguridad de tu infraestructura en la nube comienza con Reticulum - la potencia del lenguaje D para análisis de seguridad contextual.


Notas

  • Esta versión refleja la implementación actual en lenguaje D del proyecto Reticulum
  • Los comandos de instalación usan Docker y DUB en lugar de pip/poetry
  • La arquitectura modular está implementada con archivos .d específicos
  • Los ejemplos de CI/CD usan Docker para consistencia multi-plataforma
  • Los formatos de salida y características técnicas coinciden con la implementación real del códigobase

Wiki pages you might want to explore:

Citations

File: Dockerfile (L1-52)

# --- Build Stage ---
FROM debian:bookworm-slim AS builder

ARG TARGETARCH
ARG LDC_VERSION="1.36.0"

WORKDIR /opt

# Install build dependencies
RUN apt-get update && apt-get install -y \
    curl \
    xz-utils \
    build-essential \
    libxml2 \
    git \
    && rm -rf /var/lib/apt/lists/*

# Download LDC based on architecture (amd64 vs arm64)
RUN if [ "$TARGETARCH" = "amd64" ]; then \
        LDC_ARCH="x86_64"; \
    elif [ "$TARGETARCH" = "arm64" ]; then \
        LDC_ARCH="aarch64"; \
    else \
        echo "Unsupported architecture: $TARGETARCH"; exit 1; \
    fi && \
    curl -L -o ldc.tar.xz "https://github.com/ldc-developers/ldc/releases/download/v${LDC_VERSION}/ldc2-${LDC_VERSION}-linux-${LDC_ARCH}.tar.xz" && \
    tar -xf ldc.tar.xz && \
    mv "ldc2-${LDC_VERSION}-linux-${LDC_ARCH}" ldc && \
    rm ldc.tar.xz

ENV PATH="/opt/ldc/bin:${PATH}"

WORKDIR /app

# Copy source and build
COPY . .
RUN dub build --build=release --compiler=ldc2

# --- Runtime Stage ---
FROM debian:bookworm-slim

WORKDIR /app

# Install runtime dependencies
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

# Copy binary and assets
COPY --from=builder /app/reticulum /usr/local/bin/reticulum
COPY --from=builder /app/rules /app/rules

ENTRYPOINT ["reticulum"]

File: README.md (L75-78)

#### Prerequisites
- [D Language Compiler](https://dlang.org/download.html) (DMD or LDC2)
- [DUB](https://code.dlang.org/download) (Package Manager)
- [Trivy](https://trivy.dev/) & [Semgrep](https://semgrep.dev/) (Scanners)

File: src/app.d (L1-315)

/**
 * Reticulum - Contextual Security Prioritizer for Kubernetes
 * 
 * Author: Jose Ramon Palanco <jose.palanco@plexicus.ai>
 * By: PLEXICUS (https://www.plexicus.ai)
 * License: MIT
 */

module app;

import std.stdio;
import std.getopt;
import std.json;
import mapper;
import analyzer;
import ingestor;
import rules.engine;
import std.file;
import std.path;
import std.algorithm;
import std.string;
import std.conv;

// ANSI Color Codes - Red Alert Palette
enum Color : string
{
    RESET = "\033[0m",
    BOLD = "\033[1m",
    DIM = "\033[2m",
    RED = "\033[31m",
    GREEN = "\033[32m",
    YELLOW = "\033[33m",
    BLUE = "\033[34m",
    MAGENTA = "\033[35m",
    CYAN = "\033[36m",
    WHITE = "\033[37m",
    BRIGHT_RED = "\033[91m",
    BRIGHT_GREEN = "\033[92m",
    BRIGHT_YELLOW = "\033[93m",
    BRIGHT_BLUE = "\033[94m",
    BRIGHT_MAGENTA = "\033[95m",
    BRIGHT_CYAN = "\033[96m",
    // Extended colors for brutal theme
    COL_RED = "\033[38;5;196m", // Laser red
    COL_GRY = "\033[38;5;244m", // Metallic gray
    BG_RED = "\033[48;5;196m\033[38;5;232m" // Red background, black text
}

void printBanner()
{
    // Header bar - full width tactical style (Purple background, black text)
    write(
        "\033[48;5;129m\033[38;5;232m\033[1m CLOUD-NATIVE CONTEXTUAL SECURITY PRIORITIZER BY PLEXICUS ");
    write("                    \033[0m\n");
    write("\033[100m\033[30m RETICULUM v1.0 ");
    write("                                                                \033[0m\n\n");

    // RETICULUM logo in ANSI Shadow font
    const string[] logo = [
        "██████╗ ███████╗████████╗██╗ ██████╗██╗   ██╗██╗     ██╗   ██╗███╗   ███╗",
        "██╔══██╗██╔════╝╚══██╔══╝██║██╔════╝██║   ██║██║     ██║   ██║████╗ ████║",
        "██████╔╝█████╗     ██║   ██║██║     ██║   ██║██║     ██║   ██║██╔████╔██║",
        "██╔══██╗██╔══╝     ██║   ██║██║     ██║   ██║██║     ██║   ██║██║╚██╔╝██║",
        "██║  ██║███████╗   ██║   ██║╚██████╗╚██████╔╝███████╗╚██████╔╝██║ ╚═╝ ██║",
        "╚═╝  ╚═╝╚══════╝   ╚═╝   ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝     ╚═╝"
    ];

    // Render logo with purple/violet color (129)
    foreach (line; logo)
    {
        write("\033[38;5;129m", line, "\033[0m\n");
    }

    writeln();
    write(
        "   \033[38;5;250m[+] MODULE: \033[38;5;129mPRIORITIZER\033[0m \033[38;5;250mONLINE\033[0m\n");
    writeln();
}

void printPhase(string phase, string description)
{
    write("\033[94m\n┌─\033[0m\033[1m " ~ phase ~ "\033[0m\033[94m ─────────────────────────────────────────────────────────\033[0m\n");
    write("\033[94m│ \033[0m\033[2m" ~ description ~ "\033[0m\n");
    write("\033[94m└────────────────────────────────────────────────────────────────────\033[0m\n");
}

void printSuccess(string message)
{
    writeln("\033[92m[+] \033[0m" ~ message);
}

void printWarning(string message)
{
    writeln("\033[93m[!] \033[0m" ~ message);
}

void printError(string message)
{
    writeln("\033[91m[x] \033[0m" ~ message);
}

void printInfo(string message)
{
    writeln("\033[96m[*] \033[0m" ~ message);
}

// Track if this is the first finding for header
bool firstFinding = true;

void printMatch(string service, string cveId, string filePath, int lineNum, int baseScore, int finalScore, string priority, string[] appliedRules, string description)
{
    if (firstFinding)
    {
        writeln("");
        firstFinding = false;
    }

    // Color code the Reticulum score based on priority
    string scoreColor;
    if (priority.startsWith("P0"))
        scoreColor = "\033[91m"; // Bright red for P0_BLEEDING
    else if (priority.startsWith("P1"))
        scoreColor = "\033[31m"; // Red for P1_CRITICAL
    else if (priority.startsWith("P2"))
        scoreColor = "\033[33m"; // Yellow for P2_HIGH
    else if (priority.startsWith("P3"))
        scoreColor = "\033[34m"; // Blue for P3_MEDIUM
    else
        scoreColor = "\033[2m"; // Dim for P4_LOW

    import std.string : leftJustify;
    import std.array : join;

    // Format file path with line number
    string lineStr = lineNum > 0 ? ":" ~ to!string(lineNum) : "";
    string fullPath = filePath ~ lineStr;

    // Format rules
    string rulesStr = appliedRules.length > 0 ? appliedRules.join(", ") : "none";

    // Print finding in a clean, simple format
    writeln("   \033[96m▸\033[0m \033[1m" ~ service ~ "\033[0m | \033[93m" ~ cveId ~ "\033[0m");
    writeln("     \033[2m" ~ fullPath ~ "\033[0m");
    writeln("     \033[2m" ~ description ~ "\033[0m");
    writeln("     Tool: \033[2m" ~ to!string(baseScore) ~ "\033[0m → Reticulum: " ~ scoreColor ~ to!string(
            finalScore) ~ "\033[0m | Rules: \033[2m" ~ rulesStr ~ "\033[0m");
    writeln("");
}

void printHelp()
{
    writeln("\033[1mUSAGE:\033[0m");
    writeln("  reticulum [OPTIONS]\n");
    writeln("\033[1mOPTIONS:\033[0m");
    writeln(
        "  \033[96m-p, --path\033[0m          Path to the source repository \033[2m(Required)\033[0m");
    writeln(
        "  \033[96m-s, --sarif\033[0m         Path to input SARIF file \033[2m(Required unless --scan-only)\033[0m");
    writeln("  \033[96m-o, --output\033[0m        Path to save the output JSON report");
    writeln(
        "  \033[96m    --sarif-output\033[0m  Path to save enriched SARIF \033[2m(optional)\033[0m");
    writeln(
        "  \033[96m    --scan-only\033[0m     Perform exposure analysis only \033[2m(ignores SARIF)\033[0m");
    writeln("  \033[96m-h, --help\033[0m          This help information\n");
    writeln("\033[1mEXAMPLES:\033[0m");
    writeln("  \033[2m# Full analysis with SARIF input\033[0m");
    writeln("  \033[32m./reticulum -p ./src -s results.sarif\033[0m\n");
    writeln("  \033[2m# Generate enriched SARIF output\033[0m");
    writeln(
        "  \033[32m./reticulum -p ./src -s results.sarif --sarif-output enriched.sarif\033[0m\n");
    writeln("  \033[2m# Exposure analysis only\033[0m");
    writeln("  \033[32m./reticulum -p ./src --scan-only -o exposure.json\033[0m\n");
}

void main(string[] args)
{
    string repoPath;
    string sarifInput;
    string jsonOutput;
    string sarifOutput;
    bool scanOnly = false;

    printBanner();

    try
    {
        auto helpInfo = getopt(
            args,
            "path|p", "Path to the source repository (Required)", &repoPath,
            "sarif|s", "Path to input SARIF file (Required unless --scan-only)", &sarifInput,
            "output|o", "Path to save the output JSON report", &jsonOutput,
            "sarif-output", "Path to save enriched SARIF (optional)", &sarifOutput,
            "scan-only", "Perform exposure analysis only (ignores SARIF)", &scanOnly
        );

        if (helpInfo.helpWanted || repoPath == "")
        {
            printHelp();
            return;
        }
    }
    catch (Exception e)
    {
        printError("Error parsing arguments: " ~ e.msg);
        return;
    }

    if (!scanOnly && sarifInput == "")
    {
        printError("--sarif is required unless --scan-only is used.");
        return;
    }

    // 1. Resolve absolute path for the repo
    string absRepoPath = repoPath.absolutePath.buildNormalizedPath;
    printInfo("Target Repository: " ~ absRepoPath);

    // --- Initialize Rule Engine ---
    printInfo("Initializing Rule Engine...");
    RuleEngine engine = new RuleEngine();

    // Load rules from organized directories
    if (exists("rules/exposure"))
        engine.loadRules("rules/exposure");
    if (exists("rules/security"))
        engine.loadRules("rules/security");
    if (exists("rules/scoring"))
        engine.loadRules("rules/scoring");

    // Load custom rules last (can override defaults)
    if (exists("rules/custom"))
    {
        engine.loadRules("rules/custom");
    }

    printPhase("Phase 1: Service Discovery", "Mapping services and Helm charts");
    Mapper m = new Mapper();
    m.walk(absRepoPath); // Changed to use absRepoPath
    m.link();

    printPhase("Phase 2: Exposure Analysis", "Analyzing Kubernetes deployment configurations");
    foreach (s; m.services)
    {
        if (s.chart)
            analyzer.analyzeExposure(s.chart, engine);
    }

    if (scanOnly)
    {
        printWarning("Mode: Exposure Analysis Only (Skipping SARIF ingestion)");

        // 1. Build the Rich JSON
        JSONValue[] serviceList;
        foreach (s; m.services)
        {
            serviceList ~= s.toJson();
        }

        JSONValue root = parseJSON("{}");
        root.object["services"] = JSONValue(serviceList);
        root.object["totalServices"] = JSONValue(cast(long) m.services.length);
        root.object["scanType"] = JSONValue("exposure-audit");

        // 2. Output Handling
        if (jsonOutput != "")
        {
            try
            {
                std.file.write(jsonOutput, root.toPrettyString());
                printSuccess("Exposure report saved to: " ~ jsonOutput);
            }
            catch (Exception e)
            {
                printError("Error writing output: " ~ e.msg);
            }
        }
        else
        {
            // Fallback: If no file specified, print to stdout
            writeln(root.toPrettyString());
        }

        return; // EXIT PROGRAM HERE
    }

    printPhase("Phase 3: Vulnerability Scoring", "Ingesting SARIF and applying contextual scoring");
    processSarif(sarifInput, m.services, absRepoPath, engine, sarifOutput);

    // --- Generate JSON Report (Full Scan) ---
    if (jsonOutput != "")
    {
        JSONValue[] serviceList;
        foreach (s; m.services)
        {
            serviceList ~= s.toJson();
        }

        JSONValue root = parseJSON("{}");
        root.object["services"] = JSONValue(serviceList);
        root.object["totalServices"] = JSONValue(cast(long) m.services.length);
        root.object["scanType"] = JSONValue("full-analysis");

        try
        {
            std.file.write(jsonOutput, root.toPrettyString());
            printSuccess("Full analysis report saved to: " ~ jsonOutput);
        }
        catch (Exception e)
        {
            printError("Error writing JSON output: " ~ e.msg);
        }
    }

    printSuccess("Reticulum Analysis Complete.");

File: src/model.d (L1-90)

module model;

import std.json;
import std.algorithm; // for min, max
import std.conv; // for to!string

// The Decision Matrix Levels
enum Priority
{
    P0_BLEEDING, // Score 90-100: Public + Critical
    P1_CRITICAL, // Score 70-89:  Public + High OR Internal + Critical
    P2_HIGH, // Score 50-69:  Internal + High
    P3_MEDIUM, // Score 30-49:  Medium issues
    P4_LOW // Score 0-29:   Low/Info
}

class RiskProfile
{
    // --- Context Flags ---
    bool isPublic = false; // Ingress / LoadBalancer
    bool isPrivileged = false; // hostNetwork / privileged: true / runs as root
    bool hasFix = true; // Patch available?
    bool hasDangerousCaps = false; // SYS_ADMIN, NET_ADMIN, etc.
    bool hasInternetEgress = false; // 0.0.0.0/0 allowed
    bool mountServiceToken = true; // automountServiceAccountToken (default true is risky)

    // --- Dynamic Scoring ---
    float[] multipliers;
    int[] boosts;
    string[] appliedRuleIds; // Track which rules were applied

    void addMultiplier(float m)
    {
        multipliers ~= m;
    }

    void addBoost(int b)
    {
        boosts ~= b;
    }

    JSONValue toJson()
    {
        JSONValue j = parseJSON("{}");
        j.object["isPublic"] = JSONValue(isPublic);
        j.object["isPrivileged"] = JSONValue(isPrivileged);
        j.object["hasDangerousCaps"] = JSONValue(hasDangerousCaps);
        j.object["hasInternetEgress"] = JSONValue(hasInternetEgress);
        j.object["mountServiceToken"] = JSONValue(mountServiceToken);
        if (appliedRuleIds.length > 0)
        {
            j.object["appliedRuleIds"] = JSONValue(appliedRuleIds);
        }
        // Calculate a raw score for the report based on a baseline (e.g., 50) just for visibility
        j.object["baseRiskScore"] = JSONValue(calculateScore(50));
        return j;
    }

    // --- robustCalculateScore ---
    // Transforms a raw severity (0-100) into a Contextual Risk Score
    int calculateScore(int baseSeverity)
    {
        float score = cast(float) baseSeverity;

        // 1. Apply Multipliers (Exposure, Context)
        // Default to 1.0 if no multipliers added
        if (multipliers.length == 0)
        {
            // Fallback to legacy logic if no rules ran (or no rules matched)
            // This ensures backward compatibility during migration if rules aren't loaded
            // But ideally we want rules to drive this.
            // For now, let's assume if no multipliers, we treat it as neutral (1.0) 
            // OR we could keep the hardcoded logic here as a fallback?
            // Let's keep it pure: if no rules, no multipliers.
        }
        else
        {
            foreach (m; multipliers)
            {
                score *= m;
            }
        }

        // 2. Apply Boosts (Threats)
        foreach (b; boosts)
        {
            score += b;
        }

        // 3. Actionability Modifier (Hardcoded for now as it's not a rule per se, but could be)

File: src/analyzer.d (L1-90)

module analyzer;

import model;
import rules.engine; // Import RuleEngine
import std.file;
import std.path;
import std.stdio;
import std.string;
import std.algorithm;
import std.array;
import dyaml.loader;
import dyaml.node;

// ========================
// MAIN LOGIC
// ========================

void analyzeExposure(Chart chart, RuleEngine engine)
{
    if (chart is null)
        return;

    writeln("  [Analyzer] Analyzing chart: ", chart.name, " → ", chart.path);
    chart.risk.reset();

    // 1. Evaluate Metadata Rules
    engine.evaluateMetadata(chart);

    string[] valueFiles;
    try
    {
        foreach (string entry; dirEntries(chart.path, SpanMode.shallow))
        {
            string fname = baseName(entry).toLower;

            // --- FIX: Strictly exclude non-value files ---
            if (fname == "chart.yaml" || fname == "chart.yml" || fname == "chart.lock" || fname.startsWith(
                    ".helm"))
            {
                continue;
            }

            // Allow standard values.yaml, variations like values-prod.yaml, or specific environment files
            bool isYaml = (fname.endsWith(".yaml") || fname.endsWith(".yml"));
            bool looksLikeValues = (fname.startsWith("values") || fname == "prod.yaml" || fname == "staging.yaml" || fname == "dev.yaml");

            if (isYaml && looksLikeValues)
            {
                valueFiles ~= entry;
            }
        }
    }
    catch (Throwable)
    {
    }

    valueFiles.sort();

    foreach (path; valueFiles)
    {
        if (path.exists)
        {
            writeln("    → Loading values: ", baseName(path));
            analyzeValuesFile(chart, path, engine);
        }
    }

    // analyzeTemplates(chart); // Deprecated for now, or needs to be ported to RuleEngine (FILE_CONTENT target)
    // For now, we rely on values.yaml analysis as per the plan. Template analysis is complex to do with simple rules.
    // If we need template analysis, we should add a FILE_CONTENT target to RuleEngine later.

    writeln("    [Final Risk Profile]");
    writeln("      • Public Exposure    : ", chart.risk.isPublic ? "YES" : "NO");
    writeln("      • Privileged         : ", chart.risk.isPrivileged ? "YES" : "NO");
    writeln("      • Dangerous Caps     : ", chart.risk.hasDangerousCaps ? "YES" : "NO");
    writeln("      • Svc Token Mount    : ", chart.risk.mountServiceToken ? "YES (Default)"
            : "NO (Secured)");
}

private void analyzeValuesFile(Chart chart, string path, RuleEngine engine)
{
    try
    {
        Node root = Loader.fromFile(path).load();

        if (root.type != NodeType.mapping)
            return;

        // 2. Evaluate Values Rules
        engine.evaluateValues(chart, root);

File: src/ingestor.d (L31-90)

        {
            return to!float(v);
        }
        catch (Throwable)
        {
        }
    }
    return 5.0;
}

string floatToSeverityLabel(float score)
{
    if (score >= 9.0)
        return "CRITICAL";
    if (score >= 7.0)
        return "HIGH";
    if (score >= 4.0)
        return "MEDIUM";
    return "LOW";
}

bool isFixable(JSONValue result)
{
    JSONValue props = ("properties" in result) ? result["properties"] : parseJSON("{}");

    if ("trivy:fixedVersion" in props)
    {
        string fixedVer = props["trivy:fixedVersion"].str;
        return (fixedVer != "" && fixedVer != "null");
    }
    if ("github:fixAvailable" in props)
    {
        // Handle boolean or string "true"
        auto val = props["github:fixAvailable"];
        if (val.type == JSONType.true_)
            return true;
        if (val.type == JSONType.string)
            return val.str == "true";
        return false;
    }
    // Static analysis fixes
    if ("fix" in result && result["fix"].type != JSONType.null_)
        return true;

    // Default assumption for SAST (Code) is that it is fixable by dev
    return true;
}

float extractSeverity(JSONValue result, JSONValue rules)
{
    // 1. GitHub security-severity
    if ("properties" in result && "security-severity" in result["properties"])
    {
        auto s = result["properties"]["security-severity"];
        if (s.type == JSONType.string)
            return to!float(s.str);
        if (s.type == JSONType.float_)
            return s.floating;
    }