Skip to main content

Feed web app IoCs back to OneFirewall

Many internet-facing web applications aren’t protected by a Web Application Firewall (WAF) or an advanced IDS that inspects web payloads. As a result, a large number of web attacks can go unnoticed. This page explains how to configure your web application to automatically feed Indicators of Compromise (IoCs) to OneFirewall whenever malicious activity is detected — without installing or maintaining separate security appliances.

Why this helps

  • Detect more attacks — capture suspicious requests your perimeter tools miss.
  • Centralised telemetry — OneFirewall aggregates IoCs from all your apps for faster investigation.
  • Low operational overhead — no extra WAF/IDS product to deploy or manage.
  • Faster response — automated feeds speed up blocking and threat hunting.

How it works (high level)

  1. Your web app (or its runtime/logging layer) detects suspicious activity or extracts IoCs from logs.
  2. The app sends those IoCs (IP addresses, URLs, user-agents, file hashes, etc.) to OneFirewall via a secure API.
  3. OneFirewall ingests the IoCs, enriches and correlates them, and applies blocking or alerting rules across your environment.

Step 1 — Create an Account

  1. Go to https://app.onefirewall.com and create an account.
  2. Generate an API Token and store it securely (e.g., in a password manager or secrets vault).
💡 If you don’t have an active account, please contact [email protected].

Step 2 - Set the Env Variables

Create / update your .env file (or environment variables for your app)
ONEFIREWALL_END_POINT="https://app.onefirewall.com/api/v2/ips"
ONEFIREWALL_API_KEY="<your API Key>"
ONEFIREWALL_BULK=100
ONEFIREWALL_TAG="report-XXXXXXXX"
Speak with OneFirewall team to define the tag for you report-XXXXXXXX
These settings control how your application submits unauthorized access attempts and anomalies to OneFirewall.

Step 3 — Create Middleware

In your web application, implement middleware that intercepts all incoming requests and records any response that does not return a 200 OK status. Examples of status codes to capture:
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • Other unexpected error codes
This ensures anomalous or suspicious activity is consistently detected and logged.

Step 4 — Submit Feeds to OneFirewall

  • Each time an anomaly is detected, add the event to your local feed queue.
  • Once the number of queued events reaches the value defined in ONEFIREWALL_BULK, send the batch to OneFirewall using the API.
This batching approach improves efficiency, reduces API overhead, and ensures timely reporting of Indicators of Compromise (IoCs).

Example of Code Snippets

NodeJS with Express module


const express = require("express");
const axios = require("axios");

const app = express();

app.set("trust proxy", true);

const ONEFIREWALL_END_POINT = process.env.ONEFIREWALL_END_POINT || "https://app.onefirewall.com/api/v2/ips";
const ONEFIREWALL_API_KEY = process.env.ONEFIREWALL_API_KEY || "<your-api-key>";
const ONEFIREWALL_BULK = parseInt(process.env.ONEFIREWALL_BULK || "100", 10);
const ONEFIREWALL_TAG = process.env.ONEFIREWALL_TAG || "report-example"

let feedQueue = [];

function getClientIp(req) {

  if (req && req.ip) {
    // req.ip can be in format "::ffff:198.51.100.23" — normalize IPv4-mapped IPv6
    const ip = req.ip.replace(/^::ffff:/, "");
    if (ip && ip !== "::1") return ip;
  }


  const headers = req.headers || {};

  // x-forwarded-for: comma separated list, client ip is first
  if (headers["x-forwarded-for"]) {
    const list = headers["x-forwarded-for"].split(",").map(s => s.trim());
    if (list.length > 0 && list[0]) return list[0].replace(/^::ffff:/, "");
  }

  // Cloudflare
  if (headers["cf-connecting-ip"]) {
    return headers["cf-connecting-ip"].replace(/^::ffff:/, "");
  }

  // Akamai / others
  if (headers["true-client-ip"]) {
    return headers["true-client-ip"].replace(/^::ffff:/, "");
  }

  // x-real-ip
  if (headers["x-real-ip"]) {
    return headers["x-real-ip"].replace(/^::ffff:/, "");
  }

  // Forwarded: for=<ip>
  if (headers["forwarded"]) {
    // Example: Forwarded: for=198.51.100.1;proto=https;by=203.0.113.43
    const m = headers["forwarded"].match(/for=([^;,\s]+)/i);
    if (m && m[1]) return m[1].replace(/^"|"$/g, "").replace(/^::ffff:/, "");
  }

  // Last resort: req.connection remote address (older Node)
  const conn = req.connection || req.socket || {};
  if (conn.remoteAddress) {
    return conn.remoteAddress.replace(/^::ffff:/, "");
  }

  return "unknown";
}

// Middleware to capture anomalies
app.use((req, res, next) => {
  const originalSend = res.send;

  res.send = function (body) {
    const statusCode = res.statusCode;

    if ([401, 403, 404].includes(statusCode)) {
      const clientIp = getClientIp(req);

      const anomaly = {
        source: req.hostname || req.get("host") || "unknown",
        ip: clientIp,
        confidence: 0.2,
        notes: req.originalUrl,
        tags: ONEFIREWALL_TAG
      };

      feedQueue.push(anomaly);
      console.log("Captured anomaly:", anomaly);

      if (feedQueue.length >= ONEFIREWALL_BULK) {
        submitToOneFirewall();
      }
    }

    // call original send
    return originalSend.apply(this, arguments);
  };

  next();
});


// Function to send anomalies to OneFirewall
let submitting = false; // prevent concurrent submissions
async function submitToOneFirewall() {
  if (submitting) return; // avoid concurrent submissions
  if (feedQueue.length === 0) return;

  submitting = true;

  // Copy payload so queue is not mutated mid-flight
  const payload = [...feedQueue];

  try {
    const response = await axios.post(ONEFIREWALL_END_POINT, payload, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${ONEFIREWALL_API_KEY}`
      },
      timeout: 10_000
    });

    console.log(
      `Submitted ${payload.length} anomalies to OneFirewall: ${response.status}`
    );

    // clear only the successfully submitted items
    // (if new items were appended while submitting, preserve them)
    feedQueue = feedQueue.slice(payload.length);
  } catch (error) {
    console.error("Error submitting to OneFirewall:", error.message);
    // keep feedQueue intact for retry
  } finally {
    submitting = false;
  }
}


GoLang with Fiber module

// main.go
package main

import (
	"bytes"
	"encoding/json"
	"log"
	"net"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gofiber/fiber/v2"
)

type Anomaly struct {
	Source     string  `json:"source"`
	IP         string  `json:"ip"`
	Confidence float64 `json:"confidence"`
	Notes      string  `json:"notes"`
	Tags       string  `json:"tags"`
}

// --- Config (env) ---
var (
	oneFirewallEndpoint = getenv("ONEFIREWALL_END_POINT", "https://app.onefirewall.com/api/v2/ips")
	oneFirewallAPIKey   = getenv("ONEFIREWALL_API_KEY", "<your-api-key>")
	oneFirewallBulk     = mustAtoi(getenv("ONEFIREWALL_BULK", "100"))
	oneFirewallTag      = getenv("ONEFIREWALL_TAG", "report-example")
	httpTimeout         = time.Second * 10
)

// --- Queue & Submission State ---
var (
	queueMu   sync.Mutex
	feedQueue []Anomaly

	submitMu   sync.Mutex
	submitting bool
)

// Helper: get env with default
func getenv(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}

func mustAtoi(s string) int {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 100
	}
	return n
}

// --- Client IP extraction (WAF/CDN aware) ---
var (
	forwardedForRe = regexp.MustCompile(`(?i)for=([^;,\s]+)`)
)

// Normalize IPv4-mapped IPv6 like "::ffff:198.51.100.23"
func normalizeIP(ip string) string {
	ip = strings.Trim(ip, `"`)
	ip = strings.TrimSpace(ip)
	ip = strings.TrimPrefix(ip, "::ffff:")
	// If it's an IP:port, strip port
	host, _, err := net.SplitHostPort(ip)
	if err == nil && host != "" {
		return host
	}
	return ip
}

func getClientIP(c *fiber.Ctx) string {
	// 1) Prefer headers set by known proxies/CDNs
	if v := c.Get("X-Forwarded-For"); v != "" {
		parts := strings.Split(v, ",")
		if len(parts) > 0 {
			return normalizeIP(strings.TrimSpace(parts[0]))
		}
	}
	if v := c.Get("CF-Connecting-IP"); v != "" {
		return normalizeIP(v)
	}
	if v := c.Get("True-Client-IP"); v != "" {
		return normalizeIP(v)
	}
	if v := c.Get("X-Real-IP"); v != "" {
		return normalizeIP(v)
	}
	if v := c.Get("Forwarded"); v != "" {
		if m := forwardedForRe.FindStringSubmatch(v); len(m) == 2 {
			return normalizeIP(m[1])
		}
	}

	// 2) Fiber’s derived IP (may use proxy headers depending on deployment)
	if ip := c.IP(); ip != "" && ip != "::1" {
		return normalizeIP(ip)
	}

	// 3) Remote address fallback
	if ra := c.Context().RemoteAddr().String(); ra != "" {
		return normalizeIP(ra)
	}

	return "unknown"
}

// --- Submit to OneFirewall ---
func submitToOneFirewall() {
	// prevent concurrent submissions
	submitMu.Lock()
	if submitting {
		submitMu.Unlock()
		return
	}
	submitting = true
	submitMu.Unlock()

	// snapshot queue
	queueMu.Lock()
	if len(feedQueue) == 0 {
		queueMu.Unlock()
		submitMu.Lock()
		submitting = false
		submitMu.Unlock()
		return
	}
	payload := make([]Anomaly, len(feedQueue))
	copy(payload, feedQueue)
	queueMu.Unlock()

	// marshal outside lock
	body, err := json.Marshal(payload)
	if err != nil {
		log.Printf("marshal error: %v", err)
		submitMu.Lock()
		submitting = false
		submitMu.Unlock()
		return
	}

	req, err := http.NewRequest(http.MethodPost, oneFirewallEndpoint, bytes.NewReader(body))
	if err != nil {
		log.Printf("request build error: %v", err)
		submitMu.Lock()
		submitting = false
		submitMu.Unlock()
		return
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+oneFirewallAPIKey)

	client := &http.Client{Timeout: httpTimeout}
	resp, err := client.Do(req)
	if err != nil {
		log.Printf("submit error: %v", err)
		submitMu.Lock()
		submitting = false
		submitMu.Unlock()
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		// success -> drop only the submitted items
		queueMu.Lock()
		if len(feedQueue) >= len(payload) {
			feedQueue = feedQueue[len(payload):]
		} else {
			// shouldn't happen, but be safe
			feedQueue = nil
		}
		queueMu.Unlock()

		log.Printf("Submitted %d anomalies to OneFirewall: %s", len(payload), resp.Status)
	} else {
		log.Printf("submit failed: %s (queue retained)", resp.Status)
	}

	submitMu.Lock()
	submitting = false
	submitMu.Unlock()
}

func main() {
	app := fiber.New(fiber.Config{
		// Optionally set a proxy header Fiber should trust (if you fully trust your proxy layer)
		// ProxyHeader: fiber.HeaderXForwardedFor,
		// EnableTrustedProxyCheck: true,
		// TrustedProxies: []string{"YOUR.WAF.CIDR/24"},
	})

	// Middleware: run AFTER handlers to inspect final status
	app.Use(func(c *fiber.Ctx) error {
		err := c.Next()

		status := c.Response().StatusCode()
		if status == 0 {
			status = http.StatusOK
		}

		if status == http.StatusUnauthorized || status == http.StatusForbidden || status == http.StatusNotFound {
			clientIP := getClientIP(c)
			host := c.Hostname()
			if host == "" {
				host = c.Get("Host")
				if host == "" {
					host = "unknown"
				}
			}

			anomaly := Anomaly{
				Source:     host,
				IP:         clientIP,
				Confidence: 0.2,
				Notes:      c.OriginalURL(),
				Tags:       oneFirewallTag,
			}

			// enqueue safely
			queueMu.Lock()
			feedQueue = append(feedQueue, anomaly)
			queueLen := len(feedQueue)
			queueMu.Unlock()

			log.Printf("Captured anomaly: %+v (queue=%d)", anomaly, queueLen)

			// flush if bulk reached
			if queueLen >= oneFirewallBulk {
				go submitToOneFirewall()
			}
		}

		return err
	})

	// --- Example routes ---
	app.Get("/", func(c *fiber.Ctx) error {
		return c.SendString("Hello, Fiber!")
	})
	app.Get("/secret", func(c *fiber.Ctx) error {
		return c.SendStatus(http.StatusForbidden) // 403
	})
	app.Get("/missing", func(c *fiber.Ctx) error {
		return c.SendStatus(http.StatusNotFound) // 404
	})

	// Optional: periodic flush so you don't wait for bulk threshold
	flushSec := mustAtoi(getenv("ONEFIREWALL_FLUSH_SEC", "60"))
	if flushSec > 0 {
		ticker := time.NewTicker(time.Duration(flushSec) * time.Second)
		go func() {
			for range ticker.C {
				queueMu.Lock()
				hasItems := len(feedQueue) > 0
				queueMu.Unlock()
				if hasItems {
					submitToOneFirewall()
				}
			}
		}()
	}

	port := getenv("PORT", "3000")
	log.Printf("App running on :%s", port)
	if err := app.Listen(":" + port); err != nil {
		log.Fatal(err)
	}
}

Python3 with FastAPI

# main.py
import asyncio
import json
import os
import re
import socket
from typing import List, Dict, Any

import httpx
from fastapi import FastAPI, Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import PlainTextResponse

# --- Config (env) ---
ONEFIREWALL_END_POINT = os.getenv("ONEFIREWALL_END_POINT", "https://app.onefirewall.com/api/v2/ips")
ONEFIREWALL_API_KEY = os.getenv("ONEFIREWALL_API_KEY", "<your-api-key>")
ONEFIREWALL_BULK = int(os.getenv("ONEFIREWALL_BULK", "100"))
ONEFIREWALL_TAG = os.getenv("ONEFIREWALL_TAG", "report-example")
ONEFIREWALL_FLUSH_SEC = int(os.getenv("ONEFIREWALL_FLUSH_SEC", "60"))  # optional periodic flush

HTTP_TIMEOUT = 10.0

# --- In-memory queue & locks ---
feed_queue: List[Dict[str, Any]] = []
queue_lock = asyncio.Lock()
submitting_lock = asyncio.Lock()
forwarded_re = re.compile(r'for=([^;,\s]+)', re.IGNORECASE)


def _normalize_ip(value: str) -> str:
    """Normalize IPv4-mapped IPv6 and strip port if present."""
    v = value.strip().strip('"')
    if v.startswith("::ffff:"):
        v = v[len("::ffff:") :]
    # strip port if present
    try:
        host, _port = v.rsplit(":", 1)
        # if host still contains colon, it was IPv6 w/o port
        socket.inet_aton(host)  # will raise if not IPv4
        return host
    except Exception:
        return v


def get_client_ip(req: Request) -> str:
    """
    Extract real client IP behind WAF/CDN by checking (in order):
    X-Forwarded-For, CF-Connecting-IP, True-Client-IP, X-Real-IP, Forwarded, then client.host
    """
    headers = req.headers

    xff = headers.get("x-forwarded-for")
    if xff:
        # client IP is first in the comma-separated list
        first = xff.split(",")[0].strip()
        if first:
            return _normalize_ip(first)

    cf = headers.get("cf-connecting-ip")
    if cf:
        return _normalize_ip(cf)

    tci = headers.get("true-client-ip")
    if tci:
        return _normalize_ip(tci)

    xri = headers.get("x-real-ip")
    if xri:
        return _normalize_ip(xri)

    fwd = headers.get("forwarded")
    if fwd:
        m = forwarded_re.search(fwd)
        if m:
            return _normalize_ip(m.group(1))

    # fallback to client address from transport
    client_host = req.client.host if req.client else "unknown"
    if client_host and client_host != "::1":
        return _normalize_ip(client_host)

    return "unknown"


async def submit_to_onefirewall():
    """Post a copy of the queue to OneFirewall. Clear only on success."""
    # avoid concurrent submissions
    if submitting_lock.locked():
        return

    async with submitting_lock:
        # snapshot queue
        async with queue_lock:
            if not feed_queue:
                return
            payload = list(feed_queue)

        try:
            async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
                resp = await client.post(
                    ONEFIREWALL_END_POINT,
                    content=json.dumps(payload),
                    headers={
                        "Content-Type": "application/json",
                        "Authorization": f"Bearer {ONEFIREWALL_API_KEY}",
                    },
                )
            if 200 <= resp.status_code < 300:
                # success: trim only the submitted items
                async with queue_lock:
                    # if new items arrived during submission, preserve them
                    del feed_queue[: len(payload)]
                print(f"Submitted {len(payload)} anomalies to OneFirewall: {resp.status_code}")
            else:
                print(f"Submit failed: {resp.status_code} (queue retained)")
        except Exception as e:
            print(f"Error submitting to OneFirewall: {e} (queue retained)")


class AnomalyCaptureMiddleware(BaseHTTPMiddleware):
    """
    Middleware runs AFTER the request is processed (call_next),
    inspects the final status code, and enqueues anomalies for 401/403/404.
    """

    async def dispatch(self, request: Request, call_next):
        response: Response
        try:
            response = await call_next(request)
        except Exception:
            # if your handler raises and you turn it into a 500 elsewhere,
            # we don't enqueue here (only 401/403/404 as requested)
            return PlainTextResponse("Internal Server Error", status_code=500)

        status = response.status_code or 200
        if status in (401, 403, 404):
            client_ip = get_client_ip(request)
            host = request.headers.get("host") or request.url.hostname or "unknown"

            anomaly = {
                "source": host,
                "ip": client_ip,
                "confidence": 0.2,
                "notes": str(request.url.path),
                "tags": ONEFIREWALL_TAG,
            }

            async with queue_lock:
                feed_queue.append(anomaly)
                qlen = len(feed_queue)

            print(f"Captured anomaly: {anomaly} (queue={qlen})")

            if qlen >= ONEFIREWALL_BULK:
                # fire and forget
                asyncio.create_task(submit_to_onefirewall())

        return response


app = FastAPI()
app.add_middleware(AnomalyCaptureMiddleware)


# --- Example routes ---
@app.get("/")
async def root():
    return {"message": "Hello, FastAPI!"}


@app.get("/secret")
async def secret():
    return Response(status_code=403)  # Forbidden


@app.get("/missing")
async def missing():
    return Response(status_code=404)  # Not Found


# Optional: periodic flush so you don't wait for bulk threshold
@app.on_event("startup")
async def startup_event():
    if ONEFIREWALL_FLUSH_SEC <= 0:
        return

    async def periodic_flush():
        while True:
            await asyncio.sleep(ONEFIREWALL_FLUSH_SEC)
            async with queue_lock:
                has_items = len(feed_queue) > 0
            if has_items:
                await submit_to_onefirewall()

    asyncio.create_task(periodic_flush())


# Run with: uvicorn main:app --host 0.0.0.0 --port 3000