Updating blocklist

#!/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); });