chore(trends): refresh edge-exploits from Jul 3 export, flag gap + row-cap day

Merges the new Defused export (Jun 10 - Jul 3) into the accumulating
edge-exploits history: 25,420 -> 75,420 events. CitrixBleed 2 (CVE-2025-5777)
exploitation jumped 11,145 -> 56,338 hits, NetScaler now >90% of decoy traffic.

transform_defused_csv.py now detects two conditions automatically instead of
relying on hardcoded date constants:
- Gap days: no export covers May 20 - Jun 9, 2026 (21 days), rendered as a
  visible gap on the page.
- Row-cap truncation: this export hit a suspected 50,000-row console cap
  (unverified exact limit) with a clean mid-record cutoff on its oldest day,
  Jun 10 -- flagged partial (undercounts) rather than dropped or trusted as-is.

index.html's gap/volume text is now Liquid-bound to meta.date_range_note and
meta.live_decoy_count instead of hardcoded, so it won't go stale on the next
refresh. The daily chart distinguishes row-cap-partial days from the existing
export-cutoff artifact day.

--check-seed passes clean against the original seed data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011suj1d1CVCVeJDtgPKrMzi
This commit is contained in:
imposter
2026-07-03 13:58:31 -06:00
co-authored by Claude Sonnet 5
parent 3853e3c041
commit 3ace655d55
5 changed files with 649 additions and 206 deletions
+16
View File
@@ -2,6 +2,22 @@
All notable changes to this detection chokepoints repository will be documented in this file.
## [2026-07-03] - Edge-exploits trend refresh (Jun 10 - Jul 3, 2026 export)
Source: Defused Cyber honeypot telemetry, manual console export (`export_shared_20260703_183931.csv`)
### Changed
- `_data/edge_exploits.yml`, `_data/edge_exploits_provenance.yml` — merged the new export; total events 25,420 → 75,420. CitrixBleed 2 (CVE-2025-5777) exploitation jumped 11,145 → 56,338 hits, Citrix NetScaler now >90% of decoy traffic.
- `trends/edge-exploits/index.html` — chart caption and volume blurb now pull `meta.date_range_note` / `meta.live_decoy_count` via Liquid instead of hardcoded text, so they stay in sync on future refreshes. Daily chart flags row-cap-truncated days (new `partial` styling, distinct from the existing export-cutoff `artifact` styling).
- `scripts/transform_defused_csv.py` — generalized gap detection (previously hardcoded to the one-time Apr 14-18 baseline seam) to scan the full live window every run, and added detection for row-capped exports (Defused's console appears to cap at 50,000 rows; `unverified:` no documented limit found, inferred from this export's clean mid-record cutoff).
### Notes
- Confirmed gap: May 20 - Jun 9, 2026 (21 days) — no export was taken, no data recoverable.
- Confirmed partial day: Jun 10, 2026 — the new export hit the suspected 50,000-row cap sorted newest-first, cutting off before covering the full day. Flagged on the page rather than excluded or silently trusted.
- `--check-seed` regression-tested clean against the original May-only seed before merging the new export.
## [2026-05-29] - AiTM / Tycoon 2FA Chokepoints (4 new entries)
Source: Elastic Security Labs — Tycoon 2FA AiTM Detection Engineering (2026-05-27)
+445 -118
View File
@@ -7,114 +7,138 @@ meta:
source_url: https://defusedcyber.com/
severity_scope: high and critical severity alerts only
baseline_window: 'Mar 14 - Apr 13, 2026'
live_window: 'Apr 19 - May 19, 2026'
date_range: 'Mar 14 - May 19, 2026'
date_range_note: 'two export windows, 6-day gap Apr 14-18'
total_events: 25420
total_display: 25.4k
total_events_display: '25,420'
live_window: 'Apr 19 - Jul 3, 2026'
date_range: 'Mar 14 - Jul 3, 2026'
date_range_note: 'two export windows, 6-day gap Apr 14-18; gap May 20-Jun 9 (no export covers this period); Jun 10 partial (export hit the row cap; data begins partway through the day)'
total_events: 75420
total_display: 75.4k
total_events_display: '75,420'
decoy_count_display: 25+
cve_count_display: 50+
live_decoy_count: 27
live_cve_count: 38
live_unique_ips: 1034
generated: '2026-06-14'
live_decoy_count: 35
live_cve_count: 54
live_unique_ips: 1662
generated: '2026-07-03'
headline:
- key: citrixbleed2
label: CitrixBleed 2
cve: CVE-2025-5777
count: 11145
display: '11,145'
count: 56338
display: '56,338'
- key: nextjs_rce
label: Next.js RCE (new)
cve: CVE-2025-55182
count: 2683
display: '2,683'
count: 4997
display: '4,997'
- key: cpanel_whm
label: cPanel WHM chain
cve: CVE-2026-41940
count: 1515
display: '1,515'
count: 1728
display: '1,728'
targets:
- name: Citrix NetScaler
count: 11995
display: '11,995'
count: 57261
display: '57,261'
- name: React Server
count: 2683
display: '2,683'
count: 4997
display: '4,997'
- name: FortiWeb
count: 2037
display: '2,037'
count: 2336
display: '2,336'
- name: cPanel WHM
count: 1515
display: '1,515'
- name: Cisco SD-WAN
count: 1383
display: '1,383'
count: 1728
display: '1,728'
- name: SAP Netweaver
count: 1341
display: '1,341'
- name: Ivanti Connect Secure
count: 1035
display: '1,035'
count: 1465
display: '1,465'
- name: Cisco SD-WAN
count: 1435
display: '1,435'
- name: SonicWall SMA
count: 834
display: '834'
count: 1257
display: '1,257'
- name: Ivanti Connect Secure
count: 1240
display: '1,240'
- name: F5 Big-IP Legacy
count: 272
display: '272'
count: 507
display: '507'
- name: F5 Big-IP
count: 213
display: '213'
count: 304
display: '304'
- name: Fortinet FortiSandbox
count: 184
display: '184'
- name: Palo Alto GlobalProtect
count: 145
display: '145'
count: 172
display: '172'
- name: Cisco Unified Communications Manager
count: 167
display: '167'
- name: Ivanti EPMM
count: 72
display: '72'
- name: Cisco Identity Services Engine
count: 56
display: '56'
count: 151
display: '151'
- name: Ubiquiti UniFi OS Server
count: 126
display: '126'
- name: SharePoint
count: 48
display: '48'
count: 81
display: '81'
- name: FortiGate
count: 41
display: '41'
count: 64
display: '64'
- name: Cisco Identity Services Engine
count: 60
display: '60'
- name: Fortinet FortiClient EMS
count: 25
display: '25'
count: 46
display: '46'
- name: Drupal CMS (PostgreSQL)
count: 33
display: '33'
- name: Oracle E-Business
count: 18
display: '18'
count: 31
display: '31'
- name: FortiSIEM
count: 9
display: '9'
count: 17
display: '17'
- name: Sitecore XP
count: 8
display: '8'
count: 16
display: '16'
- name: BeyondTrust Remote Support
count: 14
display: '14'
- name: Check Point Mobile Access SSL VPN
count: 13
display: '13'
- name: Cisco Smart Software Manager On-Prem
count: 7
display: '7'
count: 12
display: '12'
- name: GoAnywhere MFT
count: 12
display: '12'
- name: SolarWinds Serv-U
count: 7
display: '7'
- name: VMware vCenter
count: 6
display: '6'
- name: Cisco IOS XE
count: 4
display: '4'
- name: Jenkins Server
count: 4
display: '4'
- name: Windows WSUS
count: 4
display: '4'
- name: BeyondTrust Remote Support
count: 3
display: '3'
- name: Atlassian Jira
count: 2
display: '2'
- name: SolarWinds Web Help Desk
count: 2
display: '2'
- name: Cisco IOS XE
count: 1
display: '1'
- name: VMware vCenter
- name: N8N
count: 1
display: '1'
daily:
@@ -347,9 +371,169 @@ daily:
total: 101
unique_ips: 52
- date: '2026-05-19'
label: May 19*
label: May 19
total: 3084
unique_ips: 45
- date: '2026-05-20'
label: ''
total: null
- date: '2026-05-21'
label: ''
total: null
- date: '2026-05-22'
label: ''
total: null
- date: '2026-05-23'
label: ''
total: null
- date: '2026-05-24'
label: ''
total: null
- date: '2026-05-25'
label: ''
total: null
- date: '2026-05-26'
label: ''
total: null
- date: '2026-05-27'
label: ''
total: null
- date: '2026-05-28'
label: ''
total: null
- date: '2026-05-29'
label: ''
total: null
- date: '2026-05-30'
label: ''
total: null
- date: '2026-05-31'
label: ''
total: null
- date: '2026-06-01'
label: ''
total: null
- date: '2026-06-02'
label: ''
total: null
- date: '2026-06-03'
label: ''
total: null
- date: '2026-06-04'
label: ''
total: null
- date: '2026-06-05'
label: ''
total: null
- date: '2026-06-06'
label: ''
total: null
- date: '2026-06-07'
label: ''
total: null
- date: '2026-06-08'
label: ''
total: null
- date: '2026-06-09'
label: ''
total: null
- date: '2026-06-10'
label: Jun 10†
total: 36528
unique_ips: 25
partial: true
- date: '2026-06-11'
label: Jun 11
total: 145
unique_ips: 56
- date: '2026-06-12'
label: Jun 12
total: 7090
unique_ips: 67
- date: '2026-06-13'
label: Jun 13
total: 161
unique_ips: 48
- date: '2026-06-14'
label: Jun 14
total: 105
unique_ips: 47
- date: '2026-06-15'
label: Jun 15
total: 70
unique_ips: 31
- date: '2026-06-16'
label: Jun 16
total: 139
unique_ips: 59
- date: '2026-06-17'
label: Jun 17
total: 149
unique_ips: 63
- date: '2026-06-18'
label: Jun 18
total: 126
unique_ips: 63
- date: '2026-06-19'
label: Jun 19
total: 182
unique_ips: 61
- date: '2026-06-20'
label: Jun 20
total: 100
unique_ips: 50
- date: '2026-06-21'
label: Jun 21
total: 115
unique_ips: 57
- date: '2026-06-22'
label: Jun 22
total: 237
unique_ips: 66
- date: '2026-06-23'
label: Jun 23
total: 1147
unique_ips: 50
- date: '2026-06-24'
label: Jun 24
total: 1062
unique_ips: 56
- date: '2026-06-25'
label: Jun 25
total: 152
unique_ips: 58
- date: '2026-06-26'
label: Jun 26
total: 149
unique_ips: 54
- date: '2026-06-27'
label: Jun 27
total: 128
unique_ips: 51
- date: '2026-06-28'
label: Jun 28
total: 603
unique_ips: 49
- date: '2026-06-29'
label: Jun 29
total: 1112
unique_ips: 44
- date: '2026-06-30'
label: Jun 30
total: 161
unique_ips: 53
- date: '2026-07-01'
label: Jul 1
total: 135
unique_ips: 50
- date: '2026-07-02'
label: Jul 2
total: 120
unique_ips: 61
- date: '2026-07-03'
label: Jul 3*
total: 84
unique_ips: 30
artifact: true
cb2_daily:
labels:
@@ -382,85 +566,117 @@ cb2_daily:
- 270
cves:
- id: CVE-2025-5777
count: 3033
count: 48226
- id: CVE-2025-55182
count: 2683
count: 4997
- id: CVE-2026-41940
count: 1515
count: 1728
- id: CVE-2025-25257
count: 966
count: 1210
- id: CVE-2025-40599
count: 356
count: 779
- id: CVE-2022-1388
count: 314
count: 593
- id: CVE-2023-46805
count: 246
- id: CVE-2023-46747
count: 171
count: 404
- id: CVE-2025-31324
count: 162
- id: CVE-2024-3400
count: 147
count: 286
- id: CVE-2023-46747
count: 218
- id: CVE-2026-3055
count: 146
count: 201
- id: CVE-2026-20127
count: 175
- id: CVE-2024-3400
count: 155
- id: CVE-2026-20230
count: 123
- id: CVE-2019-19781
count: 92
- id: CVE-2026-39808
count: 92
- id: CVE-2026-39813
count: 92
- id: CVE-2025-53770
count: 81
- id: CVE-2026-34910
count: 78
- id: CVE-2023-4966
count: 74
- id: CVE-2025-20281
count: 54
- id: CVE-2025-53770
count: 48
count: 76
- id: CVE-2025-64446
count: 44
- id: CVE-2022-40684
count: 41
count: 73
- id: CVE-2025-4427
count: 39
- id: CVE-2019-11510
count: 37
- id: CVE-2026-21643
count: 25
count: 59
- id: CVE-2026-1281
count: 18
- id: CVE-2024-21887
count: 16
count: 59
- id: CVE-2019-11510
count: 58
- id: CVE-2025-20281
count: 58
- id: CVE-2022-40684
count: 49
- id: CVE-2026-34908
count: 48
- id: CVE-2026-20045
count: 44
- id: CVE-2023-35081
count: 15
- id: CVE-2025-61882
count: 12
count: 33
- id: CVE-2026-9082
count: 33
- id: CVE-2024-21887
count: 29
- id: CVE-2026-21643
count: 28
- id: CVE-2026-0257
count: 19
- id: CVE-2018-13379
count: 18
- id: CVE-2026-35616
count: 18
- id: CVE-2025-64155
count: 9
- id: CVE-2023-35813
count: 8
- id: CVE-2024-0204
count: 7
- id: CVE-2024-20419
count: 7
count: 17
- id: CVE-2022-21587
count: 16
- id: CVE-2023-35813
count: 16
- id: CVE-2025-61882
count: 15
- id: CVE-2026-1731
count: 14
- id: CVE-2026-50751
count: 13
- id: CVE-2024-0204
count: 12
- id: CVE-2024-20419
count: 12
- id: CVE-2023-27997
count: 8
- id: CVE-2024-21893
count: 8
- id: CVE-2024-28995
count: 7
- id: CVE-2021-21972
count: 6
- id: CVE-2023-20198
count: 4
- id: CVE-2024-43044
count: 4
- id: CVE-2025-59287
count: 4
- id: CVE-2026-1731
count: 3
- id: CVE-2022-0540
count: 2
- id: CVE-2023-3519
count: 2
- id: CVE-2025-20337
count: 2
- id: CVE-2021-21972
count: 1
- id: CVE-2023-20198
count: 1
- id: CVE-2024-21893
- id: CVE-2026-8451
count: 2
- id: CVE-2025-68613
count: 1
exploit_recon:
exploit_total: 7005
recon_total: 3414
exploit_pct: 67
exploit_total: 53946
recon_total: 6473
exploit_pct: 89
daily:
- date: '2026-04-19'
label: Apr 19
@@ -586,7 +802,118 @@ exploit_recon:
label: May 19
exploit: 3030
recon: 54
- date: '2026-06-10'
label: Jun 10
exploit: 36508
recon: 20
- date: '2026-06-11'
label: Jun 11
exploit: 95
recon: 50
- date: '2026-06-12'
label: Jun 12
exploit: 6997
recon: 93
- date: '2026-06-13'
label: Jun 13
exploit: 126
recon: 35
- date: '2026-06-14'
label: Jun 14
exploit: 46
recon: 59
- date: '2026-06-15'
label: Jun 15
exploit: 38
recon: 32
- date: '2026-06-16'
label: Jun 16
exploit: 88
recon: 51
- date: '2026-06-17'
label: Jun 17
exploit: 71
recon: 78
- date: '2026-06-18'
label: Jun 18
exploit: 67
recon: 59
- date: '2026-06-19'
label: Jun 19
exploit: 83
recon: 99
- date: '2026-06-20'
label: Jun 20
exploit: 49
recon: 51
- date: '2026-06-21'
label: Jun 21
exploit: 69
recon: 46
- date: '2026-06-22'
label: Jun 22
exploit: 170
recon: 67
- date: '2026-06-23'
label: Jun 23
exploit: 1091
recon: 56
- date: '2026-06-24'
label: Jun 24
exploit: 915
recon: 147
- date: '2026-06-25'
label: Jun 25
exploit: 49
recon: 103
- date: '2026-06-26'
label: Jun 26
exploit: 45
recon: 104
- date: '2026-06-27'
label: Jun 27
exploit: 68
recon: 60
- date: '2026-06-28'
label: Jun 28
exploit: 82
recon: 521
- date: '2026-06-29'
label: Jun 29
exploit: 34
recon: 1078
- date: '2026-06-30'
label: Jun 30
exploit: 106
recon: 55
- date: '2026-07-01'
label: Jul 1
exploit: 57
recon: 78
- date: '2026-07-02'
label: Jul 2
exploit: 49
recon: 71
- date: '2026-07-03'
label: Jul 3
exploit: 38
recon: 46
lead_times:
- cve: CVE-2024-21893
recon_first: '2026-05-01'
exploit_first: '2026-06-17'
lead_days: 47
exploit_count: 1
- cve: CVE-2023-20198
recon_first: '2026-05-18'
exploit_first: '2026-06-27'
lead_days: 40
exploit_count: 1
- cve: CVE-2026-20045
recon_first: '2026-06-12'
exploit_first: '2026-06-27'
lead_days: 15
exploit_count: 4
- cve: CVE-2025-55182
recon_first: '2026-04-19'
exploit_first: '2026-04-25'
@@ -596,9 +923,9 @@ lead_times:
recon_first: '2026-04-22'
exploit_first: '2026-04-26'
lead_days: 4
exploit_count: 113
exploit_count: 130
- cve: CVE-2025-4427
recon_first: '2026-04-27'
exploit_first: '2026-04-29'
lead_days: 2
exploit_count: 24
exploit_count: 44
+90 -74
View File
@@ -3,15 +3,37 @@
meta:
source: 'Defused honeypot attacker IPs, enriched via Team Cymru (+ IPinfo cross-check)'
generated: '2026-06-15'
window: Apr 2026 - May 2026
cumulative_unique_ips: 1029
generated: '2026-07-03'
window: Apr 2026 - Jul 2026
cumulative_unique_ips: 1664
bulletproof_pct: 0
total_events: 10419
total_events: 60419
month_labels:
- Apr 2026
- May 2026
- Jun 2026
- Jul 2026
providers:
- name: Data Campus Limited
asn: 215929
bulletproof: false
total: 36484
display: '36,484'
series:
- 0
- 1
- 36483
- 0
- name: Emil Vitukhnovskii trading a
asn: 202226
bulletproof: false
total: 4710
display: '4,710'
series:
- 0
- 9
- 4701
- 0
- name: H2NEXUS LTD
asn: 215730
bulletproof: false
@@ -20,102 +42,96 @@ providers:
series:
- 43
- 3067
- 0
- 0
- name: Fast Servers (Pty) Ltd
asn: 43444
bulletproof: false
total: 2193
display: '2,193'
series:
- 0
- 2
- 2191
- 0
- name: 'Amazon.com, Inc.'
asn: 16509
bulletproof: false
total: 1049
display: '1,049'
total: 2083
display: '2,083'
series:
- 1039
- 10
- name: 'No.31,Jin-rong Street'
asn: 4134
- 1034
- 0
- name: Omegatech LTD
asn: 202412
bulletproof: false
total: 883
display: '883'
total: 1933
display: '1,933'
series:
- 864
- 19
- name: 'DigitalOcean, LLC'
asn: 14061
bulletproof: false
total: 498
display: '498'
series:
- 120
- 378
- name: Hurricane Electric LLC
asn: 6939
bulletproof: false
total: 452
display: '452'
series:
- 126
- 326
- name: 'The Constant Company, LLC'
asn: 20473
bulletproof: false
total: 357
display: '357'
series:
- 164
- 193
- 0
- 17
- 1915
- 1
- name: Other
asn: null
bulletproof: false
total: 4070
display: '4,070'
total: 9906
display: '9,906'
series:
- 1276
- 2794
- 2550
- 3681
- 3337
- 338
asn_totals:
- name: Data Campus Limited
asn: 215929
bulletproof: false
events: 36484
display: '36,484'
- name: Emil Vitukhnovskii trading a
asn: 202226
bulletproof: false
events: 4710
display: '4,710'
- name: H2NEXUS LTD
asn: 215730
bulletproof: false
events: 3110
display: '3,110'
- name: Fast Servers (Pty) Ltd
asn: 43444
bulletproof: false
events: 2193
display: '2,193'
- name: 'Amazon.com, Inc.'
asn: 16509
bulletproof: false
events: 1049
display: '1,049'
- name: 'No.31,Jin-rong Street'
asn: 4134
events: 2083
display: '2,083'
- name: Omegatech LTD
asn: 202412
bulletproof: false
events: 883
display: '883'
events: 1933
display: '1,933'
- name: 'DigitalOcean, LLC'
asn: 14061
bulletproof: false
events: 498
display: '498'
events: 984
display: '984'
- name: 'No.31,Jin-rong Street'
asn: 4134
bulletproof: false
events: 898
display: '898'
- name: Hurricane Electric LLC
asn: 6939
bulletproof: false
events: 452
display: '452'
- name: 'The Constant Company, LLC'
asn: 20473
events: 713
display: '713'
- name: Google LLC
asn: 396982
bulletproof: false
events: 357
display: '357'
- name: TechTies Inc.
asn: 197170
bulletproof: false
events: 334
display: '334'
- name: VPSVAULT.HOST LTD
asn: 215925
bulletproof: false
events: 253
display: '253'
- name: Tianfeng (Hong Kong) Communi
asn: 213802
bulletproof: false
events: 245
display: '245'
- name: PacketHub S.A.
asn: 147049
bulletproof: false
events: 209
display: '209'
events: 682
display: '682'
+91 -9
View File
@@ -33,7 +33,7 @@ import glob
import os
import re
from collections import Counter
from datetime import date
from datetime import date, timedelta
from pathlib import Path
import yaml
@@ -66,6 +66,14 @@ HEADLINE = [
# The 6-day hole between the two export windows; rendered as a visible gap.
GAP_DAYS = ["2026-04-14", "2026-04-15", "2026-04-16", "2026-04-17", "2026-04-18"]
# Defused's console export appears to cap at 50,000 rows: the 2026-07-03 export hit
# exactly 50,000 with a clean mid-record cutoff on its oldest day (sorted newest-first,
# so the cap truncates the START of the window, not the end). unverified: no documented
# limit found; inferred from one observation. When a file hits this, its oldest day is
# an undercount -- flagged as partial rather than dropped, matching how the artifact day
# (newest day, export-time cutoff) is already flagged rather than excluded.
EXPORT_ROW_CAP = 50000
class _QuoteCommaDumper(yaml.SafeDumper):
"""SafeDumper that single-quotes strings containing a comma, so display
@@ -86,6 +94,22 @@ def label_for(iso: str) -> str:
return f"{MONTHS[m]} {d}"
def _consecutive_ranges(isos):
"""Sorted iso date strings -> list of (start, end) for each consecutive run.
Used to collapse a list of missing days into human-readable gap ranges."""
if not isos:
return []
isos = sorted(isos)
ranges = [[isos[0], isos[0]]]
for iso in isos[1:]:
prev_end = date.fromisoformat(ranges[-1][1])
if date.fromisoformat(iso) == prev_end + timedelta(days=1):
ranges[-1][1] = iso
else:
ranges.append([iso, iso])
return [(r[0], r[1]) for r in ranges]
def human(n: int) -> str:
return f"{n / 1000:.1f}k" if n >= 1000 else str(n)
@@ -100,14 +124,17 @@ def classify(alert: str) -> str:
def aggregate_file(path):
"""One export CSV -> {iso_date: {total, ips:set, cve:Counter, decoy:Counter}}.
"""One export CSV -> ({iso_date: {total, ips:set, cve:Counter, decoy:Counter}}, row_count).
Counts every row -- see module docstring on why there is no row-level dedup.
Cross-export overlap is resolved at the day level in build() (newest wins).
row_count lets build() detect a row-capped (truncated) export.
"""
days = {}
n = 0
with open(path, encoding="utf-8", newline="") as f:
for r in csv.DictReader(f):
n += 1
iso = (r.get("Datetime") or "")[:10]
if not iso:
continue
@@ -127,7 +154,7 @@ def aggregate_file(path):
if m:
rec["cve"][m.group(0)] += 1
rec["cve_cls"][(m.group(0), cls)] += 1
return days
return days, n
def build(paths):
@@ -137,17 +164,35 @@ def build(paths):
# one (newest is most complete). Disjoint windows simply union.
live_days = {}
seen_days = set()
# (path, oldest_date, that_file's_count_for_oldest_date) for every capped export --
# resolved to a final partial-day set AFTER the merge, since a later (uncapped)
# export covering the same day would overwrite it with a complete count.
capped_candidates = []
for p in sorted(paths):
fdays = aggregate_file(p)
fdays, row_count = aggregate_file(p)
overlap = seen_days & set(fdays)
if overlap:
print(f" WARN {p.name}: {len(overlap)} day(s) overlap an earlier "
"export; replaced with this (newer) export's counts.")
if row_count >= EXPORT_ROW_CAP and fdays:
oldest = min(fdays)
capped_candidates.append((p, oldest, fdays[oldest]["total"]))
live_days.update(fdays)
seen_days |= set(fdays)
if not live_days:
raise SystemExit("No dated rows parsed from the export(s).")
partial_old_days = set()
for p, oldest, day_total in capped_candidates:
if live_days[oldest]["total"] == day_total:
partial_old_days.add(oldest)
print(f" WARN {p.name}: hit the {EXPORT_ROW_CAP}-row export cap -- "
f"oldest day {oldest} is likely PARTIAL (undercounts; flagged "
"on the page, not excluded).")
else:
print(f" {p.name}: oldest day {oldest} was capped in this export but "
"a later export supplied a complete count for that day -- not flagged.")
live_total = sum(d["total"] for d in live_days.values())
live_cve, live_decoy, live_ips = Counter(), Counter(), set()
for d in live_days.values():
@@ -197,25 +242,62 @@ def build(paths):
"count": count, "display": f"{count:,}"})
artifact_day = max(live_days)
live_min, live_max = min(live_days), max(live_days)
# Any day between the earliest and latest live-window date with no export
# covering it is a genuine gap -- distinct from GAP_DAYS (the one-time, frozen
# baseline-to-live seam). Detected fresh every run so a new gap (e.g. exports
# not taken for weeks) shows up automatically instead of needing a code edit.
full_range = []
d = date.fromisoformat(live_min)
end = date.fromisoformat(live_max)
while d <= end:
full_range.append(d.isoformat())
d += timedelta(days=1)
missing_days = [iso for iso in full_range if iso not in live_days]
if missing_days:
print(f" WARN: {len(missing_days)} day(s) in the live window have no export "
f"coverage ({missing_days[0]} to {missing_days[-1]}) -- rendered as a gap.")
daily = []
for row in baseline["daily"]:
daily.append({"date": row["date"], "label": label_for(row["date"]),
"total": row["total"]})
for iso in GAP_DAYS:
daily.append({"date": iso, "label": "", "total": None})
for iso in sorted(live_days):
for iso in full_range:
if iso in missing_days:
daily.append({"date": iso, "label": "", "total": None})
continue
d = live_days[iso]
entry = {"date": iso,
"label": label_for(iso) + ("*" if iso == artifact_day else ""),
flags = ""
if iso == artifact_day:
flags += "*"
if iso in partial_old_days:
flags += "†"
entry = {"date": iso, "label": label_for(iso) + flags,
"total": d["total"], "unique_ips": len(d["ips"])}
if iso == artifact_day:
entry["artifact"] = True
if iso in partial_old_days:
entry["partial"] = True
daily.append(entry)
total_events = baseline["total_events"] + live_total
live_min, live_max = min(live_days), max(live_days)
base_min = baseline["daily"][0]["date"]
notes = ["two export windows, 6-day gap Apr 14-18"]
for gap_start, gap_end in _consecutive_ranges(missing_days):
notes.append(
f"gap {label_for(gap_start)}" +
(f"-{label_for(gap_end)}" if gap_end != gap_start else "") +
" (no export covers this period)")
if partial_old_days:
partial_label = ", ".join(label_for(iso) for iso in sorted(partial_old_days))
notes.append(f"{partial_label} partial (export hit the row cap; "
"data begins partway through the day)")
date_range_note = "; ".join(notes)
out = {
"meta": {
"source": "Defused Cyber honeypot telemetry",
@@ -224,7 +306,7 @@ def build(paths):
"baseline_window": baseline["window"],
"live_window": f"{label_for(live_min)} - {label_for(live_max)}, {live_max[:4]}",
"date_range": f"{label_for(base_min)} - {label_for(live_max)}, {live_max[:4]}",
"date_range_note": "two export windows, 6-day gap Apr 14-18",
"date_range_note": date_range_note,
"total_events": total_events,
"total_display": human(total_events),
"total_events_display": f"{total_events:,}",
+7 -5
View File
@@ -355,11 +355,11 @@ permalink: /trends/edge-exploits/
<!-- ===== VOLUME ANALYSIS ===== -->
<h2 id="volume">Monthly Volume: Daily Exploit Attempts</h2>
<p>Daily hit volume across 22 decoy types. The spikes aren't gradual trends. They're specific campaigns lighting up.</p>
<p>Daily hit volume across {{ site.data.edge_exploits.meta.live_decoy_count }} decoy types. The spikes aren't gradual trends. They're specific campaigns lighting up.</p>
<div class="chart-container">
<canvas id="dailyChart" height="200"></canvas>
<div class="chart-label">Daily exploit attempts. Mar 14 – May 19, 2026 (gap Apr 14–18 = no export data; May 19* = export cutoff artifact)</div>
<div class="chart-label">Daily exploit attempts. {{ site.data.edge_exploits.meta.date_range }} ({{ site.data.edge_exploits.meta.date_range_note }})</div>
</div>
<div class="callout callout-red">
@@ -1079,6 +1079,7 @@ Chart.defaults.font.family = 'ui-monospace, monospace';
Chart.defaults.font.size = 11;
var edgeDaily = {{ site.data.edge_exploits.daily | jsonify | replace: '</', '<\/' }};
var edgeArtifact = edgeDaily.map(function(d) { return !!d.artifact; });
var edgePartial = edgeDaily.map(function(d) { return !!d.partial; });
const dailyData = {
labels: edgeDaily.map(function(d) { return d.label; }),
datasets: [{
@@ -1087,6 +1088,7 @@ const dailyData = {
backgroundColor: function(ctx) {
if (ctx.raw === null) return 'transparent';
if (edgeArtifact[ctx.dataIndex]) return 'rgba(107,114,128,0.5)'; // export-cutoff artifact
if (edgePartial[ctx.dataIndex]) return 'rgba(139,92,246,0.5)'; // export row-cap truncation
var v = ctx.raw;
if (v > 1000) return 'rgba(218,54,51,0.8)';
if (v > 500) return 'rgba(240,136,62,0.7)';
@@ -1112,9 +1114,9 @@ new Chart(document.getElementById('dailyChart'), {
callbacks: {
label: function(ctx) {
if (ctx.raw === null) return null;
return edgeArtifact[ctx.dataIndex]
? ctx.raw + ' (export cutoff - likely artifact)'
: ctx.raw + ' attempts';
if (edgeArtifact[ctx.dataIndex]) return ctx.raw + ' (export cutoff - likely artifact)';
if (edgePartial[ctx.dataIndex]) return ctx.raw + ' (partial day - export hit the row cap)';
return ctx.raw + ' attempts';
}
}
}