#!/bin/bash
#
# ServerSnug → ipset updater (cron-friendly Bash script)
#
# - Requests threat updates from ServerSnug
# - Adds IPs with status "Suspicious", "Serious" or "Critical" to ipset
# - Removes all other IPs (Monitored / null) from ipset
# - Idempotent and safe to run repeatedly
#
# Requirements: curl, jq, ipset
set -euo pipefail
# ==================== CONFIGURATION ====================
ACCESS_ID="${SERVERSNUG_ACCESS_ID:-}"
ACCESS_CODE="${SERVERSNUG_ACCESS_CODE:-}"
IPSET_NAME="${SERVERSNUG_IPSET_NAME:-blocklist}"
UPDATES_TO_REQUEST="${SERVERSNUG_UPDATES:-30}"
# Statuses that should be blocked
BLOCK_STATUSES=("Suspicious" "Serious" "Critical")
# ======================================================
log() {
echo "[$(basename "$0")] $*"
}
error() {
echo "ERROR: $*" >&2
}
ensure_ipset_exists() {
ipset create "$IPSET_NAME" hash:ip family inet -exist 2>/dev/null || true
}
perform_update_request() {
if [[ -z "$ACCESS_ID" || -z "$ACCESS_CODE" ]]; then
error "SERVERSNUG_ACCESS_ID and SERVERSNUG_ACCESS_CODE must be set"
return 1
fi
if ! [[ "$UPDATES_TO_REQUEST" =~ ^[0-9]+$ ]] || \
(( UPDATES_TO_REQUEST < 1 || UPDATES_TO_REQUEST > 50 )); then
error "SERVERSNUG_UPDATES must be between 1 and 50"
return 1
fi
local url="https://www.serversnug.com/api/public/v1/update/"
local payload
payload=$(jq -n \
--arg access_id "$ACCESS_ID" \
--arg access_code "$ACCESS_CODE" \
--argjson updates "$UPDATES_TO_REQUEST" \
'{access_id: $access_id, access_code: $access_code, updates: $updates}')
local response http_code
http_code=$(curl -sS --max-time 30 \
-w "%{http_code}" \
-o /tmp/serversnug_response.json \
-X POST \
-H "Content-Type: application/json" \
-d "$payload" \
"$url")
local body
body=$(&2
fi
# Validate structure
if ! echo "$body" | jq -e '.response | type == "array"' >/dev/null; then
error "Missing or invalid 'response' array in API reply"
return 1
fi
echo "$body"
}
main() {
ensure_ipset_exists
log "Fetching up to $UPDATES_TO_REQUEST updates from ServerSnug..."
local data
if ! data=$(perform_update_request); then
exit 1
fi
local processed=0
local changes=0
# Process each entry
while IFS=$'\t' read -r ip status; do
[[ -z "$ip" ]] && continue
((processed++))
local should_block=false
for blocked_status in "${BLOCK_STATUSES[@]}"; do
if [[ "$status" == "$blocked_status" ]]; then
should_block=true
break
fi
done
if $should_block; then
# Add to ipset
if ipset add "$IPSET_NAME" "$ip" -exist 2>/dev/null; then
echo "✅ BLOCKED $ip (status: $status)"
((changes++))
else
echo "⚠️ Failed to add $ip" >&2
fi
else
# Remove from ipset (Monitored or null = clean)
if ipset del "$IPSET_NAME" "$ip" -exist 2>/dev/null; then
echo "✅ UNBLOCKED $ip (status: ${status:-null})"
((changes++))
fi
# If the IP wasn't in the set, ipset del fails silently because of -exist
fi
done < <(echo "$data" | jq -r '.response[] | [.ip, (.status // "null")] | @tsv')
log "Finished. Processed $processed entries, $changes changes applied to ipset '$IPSET_NAME'."
}
main "$@"
#!/usr/bin/env python3
"""
ServerSnug → ipset updater (cron-friendly Python script)
- Runs every X minutes via cron
- Requests the next batch of threat updates from ServerSnug
- Adds IPs with status "Suspicious", "Serious" or "Critical" to the ipset
- Removes IPs with status "Monitored" or null (clean) from the ipset
- Idempotent and safe to run repeatedly
- Requires: Python 3 + requests (pip install requests)
"""
import os
import sys
import subprocess
import json
import requests
from typing import Dict, Any
# ==================== CONFIGURATION ====================
# Set these as environment variables or edit here
ACCESS_ID = os.getenv("SERVERSNUG_ACCESS_ID")
ACCESS_CODE = os.getenv("SERVERSNUG_ACCESS_CODE")
IPSET_NAME = os.getenv("SERVERSNUG_IPSET_NAME", "blocklist")
UPDATES_TO_REQUEST = int(os.getenv("SERVERSNUG_UPDATES", "30")) # 1-50
# Threat levels considered "Suspicious or above" → BLOCK (add to ipset)
BLOCK_STATUSES = {"Suspicious", "Serious", "Critical"}
# ======================================================
def ensure_ipset_exists() -> None:
"""Create the ipset if it doesn't exist (IPv4 hash:ip). Safe to call every run."""
try:
subprocess.check_call([
"ipset", "create", IPSET_NAME, "hash:ip", "family", "inet", "-exist"
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError:
# Already exists or permission issue → we will fail later anyway
pass
def perform_update_request() -> Dict[str, Any]:
"""Minimal version of the update request with the same robust error handling."""
if not ACCESS_ID or not ACCESS_CODE:
raise ValueError("SERVERSNUG_ACCESS_ID and SERVERSNUG_ACCESS_CODE environment variables must be set")
if not 1 <= UPDATES_TO_REQUEST <= 50:
raise ValueError("SERVERSNUG_UPDATES must be between 1 and 50")
url = "https://www.serversnug.com/api/public/v1/update/"
payload = {
"access_id": ACCESS_ID,
"access_code": ACCESS_CODE,
"updates": UPDATES_TO_REQUEST
}
try:
r = requests.post(
url,
json=payload,
timeout=30,
allow_redirects=False
)
except requests.exceptions.Timeout:
raise ConnectionError("Request timed out")
except requests.exceptions.RequestException as e:
raise ConnectionError(f"Network error: {e}")
# Parse JSON even on error codes
try:
data: Dict[str, Any] = r.json()
except Exception:
raise ValueError(f"Invalid JSON response (HTTP {r.status_code})")
if r.status_code == 403:
raise PermissionError("403 Forbidden - Invalid credentials")
if r.status_code != 200:
raise RuntimeError(f"Unexpected HTTP status {r.status_code}")
# API-level error (still returns data)
if data.get("error") == "Allowance exceeded":
print("WARNING: ServerSnug allowance exceeded. Processing partial results.", file=sys.stderr)
# Validate structure
if "response" not in data or not isinstance(data["response"], list):
raise ValueError("Missing or invalid 'response' array in API reply")
return data
def main() -> None:
ensure_ipset_exists()
print(f"[{os.path.basename(__file__)}] Fetching up to {UPDATES_TO_REQUEST} updates...")
try:
data = perform_update_request()
except Exception as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
updated = 0
for entry in data.get("response", []):
ip = entry.get("ip")
status = entry.get("status") # can be None / null
if not ip:
continue
if status in BLOCK_STATUSES:
# ADD to blocklist
try:
subprocess.check_call(
["ipset", "-exist", "add", IPSET_NAME, ip],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
print(f"✅ BLOCKED {ip} (status: {status})")
updated += 1
except subprocess.CalledProcessError:
print(f"⚠️ Failed to add {ip}", file=sys.stderr)
else:
# REMOVE from blocklist (Monitored or null = clean)
try:
subprocess.check_call(
["ipset", "-exist", "del", IPSET_NAME, ip],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
print(f"✅ UNBLOCKED {ip} (status: {status or 'null'})")
updated += 1
except subprocess.CalledProcessError:
# Not present → harmless
pass
print(f"Finished. Processed {len(data.get('response', []))} entries, {updated} changes applied.")
if __name__ == "__main__":
main()
<?php
/**
* ServerSnug → ipset updater (cron-friendly PHP script)
*
* - Runs every X minutes via cron
* - Requests threat updates from ServerSnug
* - Adds IPs with status "Suspicious", "Serious" or "Critical" to ipset
* - Removes IPs with other statuses (Monitored / null) from ipset
* - Idempotent and safe to run repeatedly
*
* Requirements: PHP 7.4+ with cURL extension, ipset command
*/
// ==================== CONFIGURATION ====================
$ACCESS_ID = getenv('SERVERSNUG_ACCESS_ID');
$ACCESS_CODE = getenv('SERVERSNUG_ACCESS_CODE');
$IPSET_NAME = getenv('SERVERSNUG_IPSET_NAME') ?: 'blocklist';
$UPDATES_TO_REQUEST = (int)(getenv('SERVERSNUG_UPDATES') ?: 30);
// Threat levels that should be blocked
$BLOCK_STATUSES = ['Suspicious', 'Serious', 'Critical'];
// ======================================================
function logMessage(string $message): void
{
echo "[" . basename(__FILE__) . "] $message\n";
}
function errorMessage(string $message): void
{
fwrite(STDERR, "ERROR: $message\n");
}
function ensureIpsetExists(string $ipsetName): void
{
// Create ipset if it doesn't exist (safe to run every time)
exec("ipset create " . escapeshellarg($ipsetName) . " hash:ip family inet -exist 2>/dev/null", $output, $returnCode);
}
function performUpdateRequest(string $accessId, string $accessCode, int $updates): array
{
if (empty($accessId) || empty($accessCode)) {
throw new Exception("SERVERSNUG_ACCESS_ID and SERVERSNUG_ACCESS_CODE environment variables must be set");
}
if ($updates < 1 || $updates > 50) {
throw new Exception("SERVERSNUG_UPDATES must be between 1 and 50");
}
$url = "https://www.serversnug.com/api/public/v1/update/";
$payload = json_encode([
'access_id' => $accessId,
'access_code' => $accessCode,
'updates' => $updates
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Content-Length: ' . strlen($payload)
],
CURLOPT_FOLLOWLOCATION => false,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false) {
throw new Exception("Network error: $curlError");
}
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("Invalid JSON response (HTTP $httpCode)");
}
if ($httpCode === 403) {
throw new Exception("403 Forbidden - Invalid credentials");
}
if ($httpCode !== 200) {
throw new Exception("Unexpected HTTP status $httpCode");
}
// Handle allowance exceeded warning
if (isset($data['error']) && $data['error'] === 'Allowance exceeded') {
fwrite(STDERR, "WARNING: ServerSnug allowance exceeded. Processing partial results.\n");
}
if (!isset($data['response']) || !is_array($data['response'])) {
throw new Exception("Missing or invalid 'response' array in API reply");
}
return $data;
}
function main(): void
{
global $ACCESS_ID, $ACCESS_CODE, $IPSET_NAME, $UPDATES_TO_REQUEST, $BLOCK_STATUSES;
ensureIpsetExists($IPSET_NAME);
logMessage("Fetching up to $UPDATES_TO_REQUEST updates from ServerSnug...");
try {
$data = performUpdateRequest($ACCESS_ID, $ACCESS_CODE, $UPDATES_TO_REQUEST);
} catch (Exception $e) {
errorMessage($e->getMessage());
exit(1);
}
$processed = 0;
$changes = 0;
foreach ($data['response'] as $entry) {
$ip = $entry['ip'] ?? null;
$status = $entry['status'] ?? null;
if (empty($ip)) {
continue;
}
$processed++;
if (in_array($status, $BLOCK_STATUSES, true)) {
// Add to ipset (block)
$cmd = "ipset add " . escapeshellarg($IPSET_NAME) . " " . escapeshellarg($ip) . " -exist";
exec($cmd, $output, $returnCode);
if ($returnCode === 0) {
echo "✅ BLOCKED $ip (status: $status)\n";
$changes++;
} else {
errorMessage("Failed to add $ip");
}
} else {
// Remove from ipset (Monitored or null = clean)
$cmd = "ipset del " . escapeshellarg($IPSET_NAME) . " " . escapeshellarg($ip) . " -exist";
exec($cmd, $output, $returnCode);
if ($returnCode === 0) {
$displayStatus = $status ?? 'null';
echo "✅ UNBLOCKED $ip (status: $displayStatus)\n";
$changes++;
}
// If IP wasn't in the set, we ignore the error (thanks to -exist)
}
}
logMessage("Finished. Processed $processed entries, $changes changes applied to ipset '$IPSET_NAME'.");
}
// Run the script
main();
?>
/**
* ServerSnug → ipset updater (cron-friendly Node.js script)
*
* - Runs every X minutes via cron
* - Requests threat updates from ServerSnug
* - Adds IPs with status "Suspicious", "Serious" or "Critical" to ipset
* - Removes other IPs (Monitored / null) from ipset
* - Idempotent and safe to run repeatedly
*
* Requirements: Node.js 18+
*/
const https = require('https');
const { execSync } = require('child_process');
// ==================== CONFIGURATION ====================
const ACCESS_ID = process.env.SERVERSNUG_ACCESS_ID;
const ACCESS_CODE = process.env.SERVERSNUG_ACCESS_CODE;
const IPSET_NAME = process.env.SERVERSNUG_IPSET_NAME || 'blocklist';
const UPDATES_TO_REQUEST = parseInt(process.env.SERVERSNUG_UPDATES || '30', 10);
// Statuses that should be blocked
const BLOCK_STATUSES = new Set(['Suspicious', 'Serious', 'Critical']);
// ======================================================
function log(message) {
console.log(`[${require('path').basename(__filename)}] ${message}`);
}
function error(message) {
console.error(`ERROR: ${message}`);
}
function ensureIpsetExists() {
try {
execSync(
`ipset create ${IPSET_NAME} hash:ip family inet -exist`,
{ stdio: 'ignore' }
);
} catch (err) {
// Already exists or permission issue — we'll fail later if needed
}
}
function performUpdateRequest() {
return new Promise((resolve, reject) => {
if (!ACCESS_ID || !ACCESS_CODE) {
return reject(new Error('SERVERSNUG_ACCESS_ID and SERVERSNUG_ACCESS_CODE must be set'));
}
if (UPDATES_TO_REQUEST < 1 || UPDATES_TO_REQUEST > 50) {
return reject(new Error('SERVERSNUG_UPDATES must be between 1 and 50'));
}
const payload = JSON.stringify({
access_id: ACCESS_ID,
access_code: ACCESS_CODE,
updates: UPDATES_TO_REQUEST
});
const options = {
hostname: 'www.serversnug.com',
path: '/api/public/v1/update/',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload)
},
timeout: 30000
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
let json;
try {
json = JSON.parse(data);
} catch (e) {
return reject(new Error(`Invalid JSON response (HTTP ${res.statusCode})`));
}
if (res.statusCode === 403) {
return reject(new Error('403 Forbidden - Invalid credentials'));
}
if (res.statusCode !== 200) {
return reject(new Error(`Unexpected HTTP status ${res.statusCode}`));
}
// Handle allowance exceeded warning
if (json.error === 'Allowance exceeded') {
console.error('WARNING: ServerSnug allowance exceeded. Processing partial results.');
}
if (!json.response || !Array.isArray(json.response)) {
return reject(new Error("Missing or invalid 'response' array in API reply"));
}
resolve(json);
});
});
req.on('error', (err) => reject(new Error(`Network error: ${err.message}`)));
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timed out'));
});
req.write(payload);
req.end();
});
}
async function main() {
ensureIpsetExists();
log(`Fetching up to ${UPDATES_TO_REQUEST} updates from ServerSnug...`);
let data;
try {
data = await performUpdateRequest();
} catch (err) {
error(err.message);
process.exit(1);
}
let processed = 0;
let changes = 0;
for (const entry of data.response) {
const ip = entry.ip;
const status = entry.status; // can be null
if (!ip) continue;
processed++;
if (BLOCK_STATUSES.has(status)) {
// Add to ipset (block)
try {
execSync(`ipset add ${IPSET_NAME} ${ip} -exist`, { stdio: 'ignore' });
console.log(`✅ BLOCKED ${ip} (status: ${status})`);
changes++;
} catch (err) {
error(`Failed to add ${ip}`);
}
} else {
// Remove from ipset (Monitored or null = clean)
try {
execSync(`ipset del ${IPSET_NAME} ${ip} -exist`, { stdio: 'ignore' });
const displayStatus = status || 'null';
console.log(`✅ UNBLOCKED ${ip} (status: ${displayStatus})`);
changes++;
} catch (err) {
// IP not present → harmless (thanks to -exist)
}
}
}
log(`Finished. Processed ${processed} entries, ${changes} changes applied to ipset '${IPSET_NAME}'.`);
}
// Run the script
main().catch(err => {
error(err.message);
process.exit(1);
});