All dispatches/Security

Scoring Real-Time Abuse Velocity: How security_extensions Detects Scanners and Credential Stuffing

How ipscope aggregates distributed darknet sensor hits, SSH/TLS brute-force telemetry, and port-sweep cadence into a calibrated 0-to-100 abuse_velocity_score with exponential time decay.

Why static IP blocklists lose to ephemeral botnets and residential DHCP churn

Traditional IP threat intelligence relies on static, flat blocklists: CSV or text files of "bad IPv4 addresses" distributed once per day. In modern production security engineering, static blocklists fail in two opposite directions simultaneously:

  1. Too slow for cloud-native and proxy-rotated attacks: Automated credential-stuffing frameworks, CVE exploitation bots, and LLM scrapers spin up ephemeral cloud instances or cycle through residential proxy pools in minutes. By the time a flat daily blocklist is compiled and pushed to your firewall, the attacker has already rotated to a fresh /24 or a new set of residential proxy exits.
  2. Too sticky for dynamic residential and cellular ISPs: When a compromised home IoT device or malware-infected laptop sits behind a consumer ISP router, its outbound attack traffic originates from a dynamically leased residential IPv4 address. When that home router reboots or the ISP rotates PPPoE/DHCP leases 24 hours later, that IPv4 address is assigned to a completely innocent household. Static blocklists that lack continuous time decay keep blocking that residential IP for weeks or months, generating endless customer lockouts.

To solve both problems, ipscope cleanly decouples structural network anonymity (security) from time-decayed hostile traffic velocity (security_extensions).

This engineering post explains how our global darknet and honeypot sensor mesh collects hostile packet telemetry, how we compute the calibrated 0 to 100 abuse_velocity_score using multi-ASN target entropy and exponential half-life decay, and how to wire security_extensions into your API gateway, WAF, and login endpoints.

Anatomy of the security_extensions block

When your API plan or custom database SKU includes the security_extensions intelligence family, every IP lookup returns three real-time activity metrics alongside the core security block:

{
  "ip": "198.51.100.214",
  "ip_version": 4,
  "matched_network": "198.51.100.0/24",
  "connection": {
    "connection_type": "hosting",
    "is_residential": false,
    "is_mobile": false,
    "is_business": false,
    "is_hosting": true
  },
  "security": {
    "is_vpn": false,
    "is_hosting": true,
    "is_tor_exit": false,
    "is_proxy": false,
    "anonymizer_type": "none",
    "confidence": 0.99,
    "reason_codes": [
      "datacenter_asn_range"
    ]
  },
  "security_extensions": {
    "abuse_velocity_score": 92,
    "is_known_scanner": true,
    "observation_recency_hours": 1
  }
}

Field definitions in security_extensions

FieldData TypeRange / ValuesOperational Definition
abuse_velocity_scoreuint80 to 100Calibrated, time-decayed severity and breadth score of hostile scanning, brute-forcing, and exploit attempts observed from this IP. 0 indicates zero observed hostile telemetry.
is_known_scannerbooleantrue or falsetrue when the IP exhibits systematic horizontal port sweeps (SYN / UDP scanning) or automated vulnerability enumeration across multiple independent /16 networks.
observation_recency_hoursuint32 (nullable)0 to 720 (null if 0 score)Elapsed hours since our sensor fleet last observed hostile or scanning packets from this IP address.

How our sensor mesh captures hostile telemetry

Our telemetry ingestion pipeline aggregates packet and application-layer events from three distinct sensor tiers deployed across 45+ autonomous systems and all five RIR regions:

1. Passive darknet (network telescope) radiation sensors

We monitor unallocated and unrouted-to-application /24 and /23 IPv4 blocks and /48 IPv6 honeypot subnets that host zero legitimate public services (no DNS A/AAAA records, no web links, no TLS certificates in CT logs). Because no legitimate human browser or misconfigured client has any reason to send TCP SYN packets to unused darknet address space, every unsolicited packet arriving at a darknet sensor is, by definition, either:

  • Horizontal internet-wide port scanning (ZMap, Masscan, Nmap),
  • Automated worm / botnet propagation probing (TCP/22, TCP/23, TCP/445, TCP/3389, TCP/6379, TCP/27017), or
  • Spoofed UDP reflection backscatter (which we filter out by requiring a completed 3-way TCP handshake or stateful protocol payload before attributing application-layer abuse).

2. High-interaction protocol honeypots (SSH, HTTP/TLS, SMTP, Redis, Database)

Completing a TCP 3-way handshake proves that the source IP is not spoofed (the sender had to receive our SYN-ACK sequence number and return a valid ACK). Behind our handshake listeners, lightweight protocol state machines classify the exact intent of the connection:

  • Credential Brute-Forcing: Repeated SSH password/key authentication attempts, SMTP AUTH LOGIN dictionary attacks, PostgreSQL/MySQL/Redis authentication sweeps, and HTTP POST /wp-login.php, /api/v1/login, or OAuth token endpoint stuffing.
  • Vulnerability & Path Exploitation: HTTP requests probing for exposed .env files, .git/config, cloud metadata SSRF headers, Log4Shell/Spring4Shell payloads, PHPUnit RCE paths, and unauthenticated Kubernetes/Docker API endpoints.
  • TLS / JA4 Fingerprint Clustering: Captures the exact TLS ClientHello structure (JA4) and HTTP/2 settings frame to correlate distributed botnet nodes executing the same automation binary across thousands of source IPs.

The mathematics of abuse_velocity_score: Breadth, Intent, and Exponential Decay

A common mistake in naive threat-scoring systems is scoring an IP based on raw packet count. If you score by raw packet count, a single developer who accidentally runs an aggressive nmap -p 1-65535 scan against one test server looks worse than a stealthy botnet node sending 1 login attempt per minute against 500 different banks.

To prevent single-target noise from skewing the score, the ipscope compiler calculates abuse_velocity_score across three normalized dimensions:

Score(ip, t) = round( 100 * (0.45 * Breadth + 0.35 * Intent + 0.20 * Cadence) * exp(-lambda * delta_t_hours) )

Dimension 1: Target Breadth & ASN Entropy (Breadth, 45% weight)

Instead of counting total packets sent to a single sensor, Breadth measures the Shannon entropy and count of distinct destination /16 networks and distinct destination ASNs touched by the source IP:

  • A source IP that sends 10,000 packets to a single destination IP receives a low breadth score because the activity is localized to one host.
  • A source IP that hits 40 distinct /16 sensors across 25 different autonomous systems in North America, Europe, and Asia within 15 minutes approaches Breadth = 1.0 and triggers is_known_scanner = true.

Dimension 2: Protocol Intent Severity (Intent, 35% weight)

Not all internet scanners have the same intent. Academic and commercial internet topology mapping projects (Censys, Shodan, Shadowserver) perform regular port sweeps and publish clear PTR reverse DNS records, WHOIS abuse contacts, and HTTP opt-out headers. Conversely, exploit botnets (Mirai, Mozi, credential-stuffing clusters) send active RCE payloads and dictionary attacks after completing the TCP handshake.

Observed Behavior TierIntent Multiplier (Intent)is_known_scannerTypical Score Range (When Active)
Zero observed darknet/honeypot hits0.00false0
Low-rate single-port probing / background noise0.25false10 to 29
Identified benign research / topology scanner0.45true35 to 55
Unattributed horizontal port / vulnerability scanner0.75true60 to 84
Active credential brute-forcing or RCE exploit payloads1.00true85 to 100

Dimension 3: Cadence Burstiness (Cadence, 20% weight) and Exponential Time Decay (exp(-lambda * delta_t))

Every observation is timestamped to the second. Let delta_t_hours equal observation_recency_hours (the hours elapsed since the most recent confirmed TCP/application-layer hit from ip).

We apply an exponential decay factor exp(-lambda * delta_t_hours) calibrated to the underlying connection.connection_type:

  • Residential and Mobile CGNAT (is_residential or is_mobile): Uses a fast decay half-life (half_life = 12 hours). If a compromised home device stops transmitting because the ISP rotated its DHCP lease, an 88 score drops to 44 after 12 hours, 22 after 24 hours, and < 6 after 48 hours.
  • Dedicated Cloud / Datacenter Hosting (is_hosting): Uses a standard decay half-life (half_life = 24 hours), because static cloud IPs rarely rotate to consumer households.

Practical score bands for WAF and API Gateway policies

When integrating abuse_velocity_score into your edge middleware, we recommend dividing the 0..100 scale into four operational action bands:

abuse_velocity_score BandClassificationRecommended Edge / Gateway Action
0 to 19Clean / BaselineNormal traffic handling and standard rate limits.
20 to 49Elevated Background NoiseTypical of shared NAT gateways or historical low-volume probing; log for telemetry, apply standard authenticated rate limits.
50 to 74Active Reconnaissance / Suspected AutomationRequire Proof-of-Work / Turnstile / CAPTCHA on unauthenticated endpoints; tighten per-IP rate limits by 4x.
75 to 100High-Velocity Attacker / Exploit BotnetBlock unauthenticated mutations (/login, /signup, /password-reset, /checkout); drop scanner probes at the edge WAF before hitting origin databases.

Production implementation: Adaptive rate limiting and scanner blocking in Node.js

Here is how to combine connection, security, and security_extensions in an Express, Fastify, or Next.js edge middleware to block active scanners and throttle brute-force bots without ever locking out shared 5G cellular CGNAT users:

import { IpScopeClient } from '@ipscope/sdk-node';

const ipscope = new IpScopeClient({
  apiKey: process.env.IPSCOPE_KEY!,
  maxCacheEntries: 16_384,
  cacheTtlMs: 60_000,
});

export interface EdgeInspectionResult {
  action: 'allow' | 'throttle' | 'challenge' | 'block';
  rateLimitPerMinute: number;
  reason: string;
}

export async function inspectIncomingRequest(
  clientIp: string,
  requestPath: string,
  isAuthenticatedSession: boolean,
): Promise<EdgeInspectionResult> {
  const res = await ipscope.lookupIp(clientIp);
  const { connection, security, security_extensions } = res;

  const velocity = security_extensions?.abuse_velocity_score ?? 0;
  const isScanner = security_extensions?.is_known_scanner ?? false;
  const recencyHours = security_extensions?.observation_recency_hours ?? 999;

  const isSensitiveAuthRoute =
    requestPath.startsWith('/api/auth/') ||
    requestPath.startsWith('/api/checkout');

  // 1. Drop active high-velocity scanners and exploit bots on unauthenticated requests
  if (!isAuthenticatedSession && (velocity >= 80 || (isScanner && recencyHours <= 6))) {
    return {
      action: 'block',
      rateLimitPerMinute: 0,
      reason: `high_abuse_velocity:${velocity}:recency_${recencyHours}h`,
    };
  }

  // 2. Challenge medium-high velocity IPs or open proxies hitting auth/checkout routes
  if (isSensitiveAuthRoute && (velocity >= 50 || security.is_proxy || security.is_tor_exit)) {
    return {
      action: 'challenge',
      rateLimitPerMinute: 10,
      reason: `sensitive_route_challenge:velocity_${velocity}:type_${security.anonymizer_type}`,
    };
  }

  // 3. Shared 4G/5G Mobile CGNAT gateways: allocate high aggregate concurrency bucket
  //    so 4,000 mobile users sharing one carrier IP do not starve each other
  if (connection?.is_mobile && !security.is_vpn && !security.is_proxy) {
    return {
      action: 'allow',
      rateLimitPerMinute: 1200,
      reason: 'mobile_cgnat_shared_pool',
    };
  }

  // 4. Commercial VPN or raw datacenter egress: tighter bucket on unauthenticated traffic
  if (security.is_vpn || security.is_hosting) {
    return {
      action: velocity >= 35 ? 'throttle' : 'allow',
      rateLimitPerMinute: isAuthenticatedSession ? 120 : 30,
      reason: `hosting_or_vpn_egress:${security.anonymizer_type}`,
    };
  }

  // 5. Clean residential or enterprise business broadband
  return {
    action: 'allow',
    rateLimitPerMinute: 300,
    reason: 'clean_access_network',
  };
}

Enforcing fail2ban-style SSH and admin protection in Go

For infrastructure teams protecting SSH bastions, Kubernetes API servers, or VPN concentrators, you can query security_extensions directly in Go (github.com/badele-llc/ipscope-go) or via a local .mmdb database to reject known internet scanners before spending CPU cycles on cryptographic key exchange:

package main

import (
	"context"
	"fmt"
	"os"
	"time"

	ipscope "github.com/badele-llc/ipscope-go"
)

func ShouldAllowAdminIngress(ctx context.Context, client *ipscope.Client, remoteIP string) (bool, string) {
	res, err := client.LookupIP(ctx, remoteIP)
	if err != nil {
		// Fail-open to local auth if network lookup times out
		return true, "lookup_fallback"
	}

	if res.SecurityExtensions != nil {
		if res.SecurityExtensions.IsKnownScanner && res.SecurityExtensions.AbuseVelocityScore >= 60 {
			return false, fmt.Sprintf(
				"blocked_scanner(score=%d,recency=%dh)",
				res.SecurityExtensions.AbuseVelocityScore,
				res.SecurityExtensions.ObservationRecencyHours,
			)
		}
	}

	if res.Security.IsTorExit || res.Security.IsProxy {
		return false, fmt.Sprintf("blocked_anonymizer(%s)", res.Security.AnonymizerType)
	}

	return true, "allowed"
}

func main() {
	client := ipscope.NewClient(os.Getenv("IPSCOPE_KEY"), ipscope.WithTimeout(2*time.Second))
	allowed, reason := ShouldAllowAdminIngress(context.Background(), client, "198.51.100.214")
	fmt.Printf("allowed=%v reason=%s\n", allowed, reason)
}

Frequently asked questions (FAQ)

What is the difference between security.confidence and security_extensions.abuse_velocity_score?

They measure two completely orthogonal properties:

  • security.confidence (0.0 to 1.0) measures our structural certainty that an IP address belongs to an anonymizing service (such as a commercial VPN, SOCKS proxy, or Tor exit). A clean, privacy-focused commercial VPN server may have security.is_vpn: true, security.confidence: 0.99 while having an abuse_velocity_score of 0 if no hostile scanning or brute-force traffic has originated from it.
  • security_extensions.abuse_velocity_score (0 to 100) measures the intensity, target breadth, and recency of hostile scanning or exploit traffic observed from that IP address, regardless of whether the IP is a VPN, a compromised cloud VM, or an infected home router.

How does observation_recency_hours help me avoid false positives on offline .mmdb databases?

If your infrastructure downloads the .mmdb database on a Weekly or Monthly schedule rather than Daily, you can inspect observation_recency_hours alongside the database's build_epoch to see how fresh the abuse observation was at compile time. For customers using security_extensions in offline .mmdb or .parquet deployments, we strongly recommend configuring your database subscription to the Daily (06:00 UTC) update cadence so exponential score decay and newly detected scanner IPs are refreshed every 24 hours.

Start with 30K free lookups a month.

No card, no trial clock. Upgrade when your traffic does, or download the database when you would rather keep it in-house.