last_pulled timestamp. // Each run logs itself in the info table using the current timestamp as list value. // Safe to run repeatedly (e.g. via cron) — uses ON DUPLICATE KEY UPDATE. set_time_limit(0); date_default_timezone_set("America/Montreal"); include('conn.php'); include('functions.php'); $api_url = "https://www.home.daprogs.net/portspoof/api/frequent_ips.php"; $api_token = "a2b6ac44bcbf33764c066892f739e546a319e0d68356d08579c2b7e32218f240"; $pull_type = 3; // blacklist type ID for portspoof entries $enddate_days = 600; // how many days until the ban expires $reason = "Portspoof Auto-Ban"; $now = date("YmdHis"); $enddate = date("YmdHis", strtotime("+" . $enddate_days . " days")); $info_list = 0; // unique run identifier logged in info table (YYYYMMDDHHmmss) // ------------------------------------------------------------------ // 1. Get last-pulled timestamp from the most recent previous run // (info rows where list > 1 are portspoof pull log entries) // ------------------------------------------------------------------ $SQL = "SELECT MAX(list) AS prev FROM info WHERE list > 1"; $result = mysqli_query($con, $SQL); $row = mysqli_fetch_array($result); if (!empty($row['prev'])) { $last_pulled = DateTime::createFromFormat("YmdHis", $row['prev']); } else { // First run — accept all IPs from the feed $last_pulled = new DateTime("@0"); } // ------------------------------------------------------------------ // 2. Fetch portspoof API // ------------------------------------------------------------------ $ch = curl_init($api_url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $api_token]); curl_setopt($ch, CURLOPT_TIMEOUT, 30); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($response === false || $http_code !== 200) { echo "Error: API fetch failed (HTTP " . $http_code . ")\n"; $con->close(); exit; } $data = json_decode($response, true); if (!isset($data['ips']) || !is_array($data['ips'])) { echo "Error: Unexpected API response format\n"; $con->close(); exit; } // ------------------------------------------------------------------ // 3. Ingest IPs newer than last_pulled (single bulk query) // ------------------------------------------------------------------ $skipped = 0; $values = []; $reason_safe = mysqli_real_escape_string($con, $reason); foreach ($data['ips'] as $entry) { $src_ip = $entry['src_ip']; $last_seen_str = $entry['last_seen']; // last_seen format: "2026-03-14 10:14:37.000000" — strip microseconds $last_seen = DateTime::createFromFormat("Y-m-d H:i:s", substr($last_seen_str, 0, 19)); if ($last_seen === false || $last_seen <= $last_pulled) { $skipped++; continue; } // Normalise to CIDR notation if (strpos($src_ip, '/') === false) { $src_ip .= '/32'; } $ip_safe = mysqli_real_escape_string($con, $src_ip); $values[] = "('" . $ip_safe . "', " . $pull_type . ", " . $now . ", " . $enddate . ", '" . $reason_safe . "')"; } $processed = 0; $chunk_size = 100; foreach (array_chunk($values, $chunk_size) as $chunk) { $SQL = "INSERT INTO blacklist (ip, type, adddate, enddate, reason) VALUES " . implode(", ", $chunk) . " ON DUPLICATE KEY UPDATE enddate=" . $enddate . ", type=" . $pull_type . ", reason='" . $reason_safe . "'"; if ($con->query($SQL) === TRUE) { $processed += count($chunk); } else { echo "Error: " . $con->error . "\n"; } } // ------------------------------------------------------------------ // 4. Update timestamps // ------------------------------------------------------------------ // Log this run — each run gets a unique row (list = current timestamp) $SQL_pull = "INSERT INTO info (list, last) VALUES (" . $info_list . ", " . $now . ")"; $con->query($SQL_pull); // Update blacklist last-modified only when rows were actually written if ($processed > 0) { $SQL_bl = "UPDATE info SET last=" . $now . " WHERE list=0"; $con->query($SQL_bl); } echo "Done. Processed: " . $processed . ", Skipped: " . $skipped . "\n"; $con->close(); ?>