Submit a URL from another app and receive a structured JSON security report. Supports synchronous responses or asynchronous webhook delivery. Rate-limited to 30 scans per hour per API key.
Node.js (Express) — verify signature + scan idjs
import crypto from "node:crypto";
import express from "express";
const app = express();
const API_KEY = process.env.SITESENTRY_API_KEY; // same key used to call the API
// Track scans you initiated. In production, store these in your database
// (e.g. a 'sitesentry_scans' table with columns: scan_id, status, received_at).
const initiatedScans = new Map(); // scan_id -> { status: 'pending' | 'received' }
export function rememberScan(scanId) {
initiatedScans.set(scanId, { status: "pending" });
}
// IMPORTANT: use the raw body, not the parsed JSON, when computing the HMAC.
app.post(
"/sitesentry/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
// 1. Signature check
const header = req.header("X-SiteSentry-Signature") || "";
const received = header.startsWith("sha256=") ? header.slice(7) : header;
const expected = crypto.createHmac("sha256", API_KEY).update(req.body).digest("hex");
const a = Buffer.from(received, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("invalid signature");
}
// 2. Scan-Id check (idempotency + replay protection)
const headerScanId = req.header("X-SiteSentry-Scan-Id") || "";
const payload = JSON.parse(req.body.toString("utf8"));
if (!headerScanId || headerScanId !== payload.scan_id) {
return res.status(400).send("scan id header / body mismatch");
}
const known = initiatedScans.get(headerScanId);
if (!known) {
// We never asked for this scan - reject.
return res.status(404).send("unknown scan id");
}
if (known.status === "received") {
// Duplicate delivery - ack 200 so SiteSentry stops retrying, but skip side effects.
return res.status(200).send("duplicate");
}
known.status = "received";
console.log("scan", payload.scan_id, "status", payload.status);
res.status(200).send("ok");
},
);
Deno / Edge — verify signature + scan idts
const API_KEY = Deno.env.get("SITESENTRY_API_KEY")!;
// Replace this with a real lookup against your DB / KV store.
const initiated = new Map<string, "pending" | "received">();
export function rememberScan(scanId: string) { initiated.set(scanId, "pending"); }
async function verifySignature(req: Request, rawBody: string): Promise<boolean> {
const header = req.headers.get("X-SiteSentry-Signature") ?? "";
const received = header.startsWith("sha256=") ? header.slice(7) : header;
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(API_KEY),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(rawBody));
const expected = Array.from(new Uint8Array(sig))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
if (received.length !== expected.length) return false;
let diff = 0;
for (let i = 0; i < expected.length; i++) diff |= received.charCodeAt(i) ^ expected.charCodeAt(i);
return diff === 0;
}
Deno.serve(async (req) => {
const raw = await req.text();
if (!(await verifySignature(req, raw))) {
return new Response("invalid signature", { status: 401 });
}
const headerScanId = req.headers.get("X-SiteSentry-Scan-Id") ?? "";
const payload = JSON.parse(raw);
if (!headerScanId || headerScanId !== payload.scan_id) {
return new Response("scan id header / body mismatch", { status: 400 });
}
const known = initiated.get(headerScanId);
if (!known) return new Response("unknown scan id", { status: 404 });
if (known === "received") return new Response("duplicate", { status: 200 });
initiated.set(headerScanId, "received");
console.log("scan", payload.scan_id, payload.status);
return new Response("ok");
});
Python (Flask) — verify signature + scan idpython
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
API_KEY = os.environ["SITESENTRY_API_KEY"].encode()
# In production, store initiated scans in your database instead of memory.
INITIATED: dict[str, str] = {} # scan_id -> 'pending' | 'received'
def remember_scan(scan_id: str) -> None:
INITIATED[scan_id] = "pending"
@app.post("/sitesentry/webhook")
def webhook():
# 1. Signature check
header = request.headers.get("X-SiteSentry-Signature", "")
received = header[7:] if header.startswith("sha256=") else header
expected = hmac.new(API_KEY, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(received, expected):
abort(401, "invalid signature")
# 2. Scan-Id check (idempotency + replay protection)
header_scan_id = request.headers.get("X-SiteSentry-Scan-Id", "")
payload = request.get_json(force=True)
if not header_scan_id or header_scan_id != payload.get("scan_id"):
abort(400, "scan id header / body mismatch")
state = INITIATED.get(header_scan_id)
if state is None:
abort(404, "unknown scan id")
if state == "received":
return "duplicate", 200 # ack so SiteSentry stops retrying
INITIATED[header_scan_id] = "received"
print("scan", payload["scan_id"], payload["status"])
return "ok", 200