stablescripting included in bundle

js v0.3.0

Write custom request/response processing logic in JavaScript using the QuickJS engine.

Protocol v2 Features

As of v0.2.0, the JavaScript agent supports protocol v2 with:

  • Capability negotiation: Reports supported features during handshake
  • Health reporting: Exposes health status with draining awareness
  • Metrics export: Counter metrics for requests processed/blocked and script errors
  • gRPC transport: High-performance gRPC transport via --grpc-address
  • Lifecycle hooks: Graceful shutdown and drain handling

Overview

JavaScript scripting agent for Zentinel reverse proxy. Write custom request/response processing logic in JavaScript using the fast, lightweight QuickJS engine.

Features

  • QuickJS Engine: Fast, embedded JavaScript runtime
  • Request/Response Hooks: Process requests and responses
  • Header Manipulation: Add/remove headers on requests and responses
  • Return-Based Decisions: Allow, block, or redirect requests
  • Console API: Logging with console.log, console.warn, console.error
  • Fail-Open Mode: Graceful error handling
  • Audit Tags: Add tags for logging and analytics

Installation

The easiest way to install this agent is via the Zentinel bundle command:

# Install just this agent
zentinel bundle install js

# Or install all available agents
zentinel bundle install --all

The bundle command automatically downloads the correct binary for your platform and places it in ~/.zentinel/agents/.

Using Cargo

cargo install zentinel-agent-js

Configuration

Command Line

zentinel-js-agent --socket /var/run/zentinel/js.sock \
  --script /etc/zentinel/scripts/handler.js

Environment Variables

OptionEnv VarDescriptionDefault
--socketAGENT_SOCKETUnix socket path/tmp/zentinel-js.sock
--grpc-addressAGENT_GRPC_ADDRESSgRPC listen address (e.g., 0.0.0.0:50052)-
--scriptJS_SCRIPTJavaScript script file(required)
--verboseJS_VERBOSEEnable debug loggingfalse
--fail-openFAIL_OPENAllow requests on script errorsfalse

Zentinel Configuration

agent "js" {
    socket "/var/run/zentinel/js.sock"
    timeout 100ms
    events ["request_headers" "response_headers"]
}

route {
    match { path-prefix "/" }
    agents ["js"]
    upstream "backend"
}

Writing Scripts

Basic Example

function on_request_headers(request) {
    // Block admin access
    if (request.uri.includes("/admin")) {
        return { decision: "block", status: 403, body: "Forbidden" };
    }

    // Allow all other requests
    return { decision: "allow" };
}

Available Hooks

HookDescription
on_request_headers(request)Called when request headers are received
on_request_body(request)Called when request body is available
on_response_headers(response)Called when response headers are received
on_response_body(response)Called when response body is available

Note: Body hooks require events ["request_headers" "request_body_chunk" "response_headers" "response_body_chunk"] in the Zentinel configuration.

Request Object

{
    method: "GET",              // HTTP method
    uri: "/api/users?page=1",   // Request URI with query string
    client_ip: "192.168.1.1",   // Client IP address
    correlation_id: "abc123",   // Request correlation ID
    headers: {                  // Request headers
        "Content-Type": "application/json",
        "User-Agent": "Mozilla/5.0..."
    }
}

Return Values

// Allow the request
return { decision: "allow" };

// Block with custom status and body
return { decision: "block", status: 403, body: "Access Denied" };

// Deny is an alias for block
return { decision: "deny", status: 403, body: "Access Denied" };

// Redirect to another URL
return { decision: "redirect", status: 302, body: "https://example.com/login" };

// Challenge the client (CAPTCHA, JS challenge, etc.)
return {
    decision: "challenge",
    challenge_type: "captcha",
    challenge_params: {
        site_key: "your-captcha-site-key",
        action: "login"
    }
};

Header Manipulation

function on_request_headers(request) {
    return {
        decision: "allow",
        add_request_headers: {
            "X-Processed-By": "js-agent",
            "X-Client-IP": request.client_ip
        },
        remove_request_headers: ["X-Debug"],
        add_response_headers: {
            "X-Frame-Options": "DENY"
        }
    };
}

Audit Tags

function on_request_headers(request) {
    if (request.headers["User-Agent"]?.includes("bot")) {
        return {
            decision: "allow",
            tags: ["bot-detected", "monitoring"]
        };
    }
    return { decision: "allow" };
}

Extended Audit Metadata

For detailed audit logging, you can include rule IDs, confidence scores, and reason codes:

function on_request_headers(request) {
    if (request.uri.includes("..")) {
        return {
            decision: "block",
            status: 403,
            tags: ["path-traversal"],
            rule_ids: ["SEC-001", "OWASP-930"],
            confidence: 0.95,
            reason_codes: ["PATH_TRAVERSAL_DETECTED"]
        };
    }
    return { decision: "allow" };
}
FieldTypeDescription
tagsarrayFreeform tags for categorization
rule_idsarraySpecific rule identifiers that triggered
confidencenumberConfidence score (0.0 to 1.0)
reason_codesarrayStructured reason codes

Routing Metadata

Control upstream selection dynamically:

function on_request_headers(request) {
    // Route to different backends based on request
    if (request.uri.startsWith("/api/v2")) {
        return {
            decision: "allow",
            routing_metadata: {
                upstream: "api-v2-backend",
                priority: "high"
            }
        };
    }

    // A/B testing: route 10% to canary
    if (Math.random() < 0.1) {
        return {
            decision: "allow",
            routing_metadata: {
                upstream: "canary-backend"
            },
            tags: ["canary"]
        };
    }

    return { decision: "allow" };
}

Body Mutation

Modify request or response bodies:

function on_request_body(request) {
    // Pass through unchanged
    return {
        decision: "allow",
        request_body_mutation: {
            action: "pass_through",
            chunk_index: 0
        }
    };
}

function on_response_body(response) {
    // Replace response body content
    return {
        decision: "allow",
        response_body_mutation: {
            action: "replace",
            chunk_index: 0,
            data: "Modified response content"
        }
    };
}
ActionDescription
pass_throughPass the chunk unchanged
replaceReplace chunk with data field content
dropDrop the chunk entirely

Needs More Data

Signal that you need the request body before making a decision:

function on_request_headers(request) {
    // For POST requests, wait for body before deciding
    if (request.method === "POST" && request.uri.startsWith("/api/")) {
        return {
            decision: "allow",
            needs_more: true  // Wait for body
        };
    }
    return { decision: "allow" };
}

function on_request_body(request) {
    // Now inspect the body
    const body = request.body || "";

    if (body.includes("malicious_pattern")) {
        return {
            decision: "block",
            status: 403,
            body: "Request blocked"
        };
    }

    return { decision: "allow" };
}

Examples

Block Bad User-Agents

function on_request_headers(request) {
    const ua = request.headers["User-Agent"] || "";
    const badBots = ["sqlmap", "nikto", "nessus", "masscan"];

    for (const bot of badBots) {
        if (ua.toLowerCase().includes(bot)) {
            return {
                decision: "block",
                status: 403,
                tags: ["bot-blocked", bot]
            };
        }
    }
    return { decision: "allow" };
}

Require Authentication

function on_request_headers(request) {
    // Skip for public paths
    if (request.uri.startsWith("/public/") || request.uri === "/health") {
        return { decision: "allow" };
    }

    // Check for auth header
    if (!request.headers["Authorization"]) {
        return {
            decision: "block",
            status: 401,
            body: "Authentication required"
        };
    }

    return { decision: "allow" };
}

Add Security Headers

function on_response_headers(response) {
    return {
        decision: "allow",
        add_response_headers: {
            "X-Content-Type-Options": "nosniff",
            "X-Frame-Options": "DENY",
            "X-XSS-Protection": "1; mode=block",
            "Strict-Transport-Security": "max-age=31536000"
        }
    };
}

Rate Limit Tiers

function on_request_headers(request) {
    let tier = "standard";
    if (request.uri.startsWith("/api/v1/")) {
        tier = "api";
    } else if (request.uri.startsWith("/admin/")) {
        tier = "admin";
    }

    return {
        decision: "allow",
        add_request_headers: {
            "X-Rate-Limit-Tier": tier
        }
    };
}

Bot Protection with Challenge

function on_request_headers(request) {
    const ua = request.headers["User-Agent"] || "";
    const suspiciousBots = ["curl", "wget", "python-requests", "scrapy"];

    // No User-Agent - issue JS challenge
    if (!ua) {
        return {
            decision: "challenge",
            challenge_type: "js_challenge",
            tags: ["no-user-agent"]
        };
    }

    // Suspicious User-Agent - issue CAPTCHA
    for (const bot of suspiciousBots) {
        if (ua.toLowerCase().includes(bot)) {
            return {
                decision: "challenge",
                challenge_type: "captcha",
                challenge_params: {
                    site_key: "your-captcha-site-key"
                },
                tags: ["suspicious-ua", bot]
            };
        }
    }

    return { decision: "allow" };
}

Error Handling

ModeOn Error
--fail-open enabledLog error, allow request, add js-error and fail-open tags
--fail-open disabledLog error, block with 500 status, add js-error tag

Comparison with Lua Agent

FeatureJavaScriptLua
EngineQuickJS (ES2020)mlua (Lua 5.4)
ScriptingSingle script fileSingle script file
String handlingFull regexPattern matching
Standard Libraryconsole.log/warn/errorNone
Use CaseComplex logic, regexSimple scripts

Use JavaScript when:

  • You need full regular expression support
  • Familiar with JavaScript syntax
  • Complex string manipulation

Use Lua agent when:

  • Familiar with Lua syntax
  • Simple pattern matching is sufficient
  • Prefer Lua’s concise table syntax
AgentIntegration
LuaAlternative scripting with Lua syntax
WebAssemblyHigh-performance custom logic
WAFCombine with security rules