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)
- Your web app (or its runtime/logging layer) detects suspicious activity or extracts IoCs from logs.
- The app sends those IoCs (IP addresses, URLs, user-agents, file hashes, etc.) to OneFirewall via a secure API.
- OneFirewall ingests the IoCs, enriches and correlates them, and applies blocking or alerting rules across your environment.
Step 1 — Create an Account
- Go to https://app.onefirewall.com and create an account.
- 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)Copy
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 a200 OK
status.
Examples of status codes to capture:
401 Unauthorized
403 Forbidden
404 Not Found
- Other unexpected error codes
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.
Example of Code Snippets
NodeJS with Express module
Copy
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
Copy
// 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
Copy
# 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