fix(trends): use AS handle when the provenance org label overflows

transform_provenance.py short() hard-truncated the AS org description at 28
chars, cutting mid-word -- the top provider read "Emil Vitukhnovskii trading
a". Fall back to the AS handle when the org would overflow, so the label reads
the recognizable "GreatFlower". Labels that already fit are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bx33CDbC3G8DQMQMARewof
This commit is contained in:
imposter
2026-07-16 17:45:25 -06:00
co-authored by Claude Opus 4.8
parent 810f810d85
commit 2e45a13223
3 changed files with 16 additions and 6 deletions
+1
View File
@@ -12,6 +12,7 @@ Source: Defused Cyber honeypot telemetry, two manual console exports. A May 20 -
- `trends/edge-exploits/index.html` — refreshed the volume callout, target-distribution intro, and the CitrixBleed 2 / Next.js / cPanel sections to the new totals and the May 26 single-source surge; extended the CitrixBleed 2 daily chart through Jul 16 (it stopped at the Apr 13 baseline before). Removed the now-dormant `partial`-day chart styling, superseded by the gap treatment below.
- `scripts/transform_defused_csv.py` — four changes: (1) day merge is now per-day MAX-wins instead of newest-wins, so a newer export's partial window-start day can no longer overwrite an older export's complete count (fixed a Jun 16 undercount, 10 vs the true 139); (2) a row-capped export's truncated oldest day now renders as a GAP rather than a flagged partial bar (supersedes the 2026-07-03 partial approach; Jun 10 is the first such gap), fillable later by a narrow uncapped export that wins the max; (3) strips the source `:port` that newer exports append to the Attacker IP field, so unique-IP counts stay consistent across the two export formats; (4) the CitrixBleed 2 daily series now includes the live window, not just the frozen baseline.
- `_data/edge_exploits_provenance.yml` — regenerated to the Apr - Jul 2026 window (was Jul 3), 2,998 cumulative unique source IPs. The local (gitignored) `enrich_asns.py` got the same per-day MAX-wins merge + Jun 10 gap + `:port` strip: it had been raw-summing overlapping exports, inflating June 6.6x (105,888 vs the correct 16,113) and floating one ASN (datacampus, the truncated Jun 10 spike's host) into the top purely as a double-counting artifact. Deduped per-month ASN totals now match the event page exactly (May 50,016 / Jun 16,113 / Jul 3,537).
- `scripts/transform_provenance.py` — `short()` now falls back to the AS handle when the org description would overflow the label, instead of a mid-word cut (top provider reads `GreatFlower`, not `Emil Vitukhnovskii trading a`). Other labels unchanged.
### Notes
+2 -2
View File
@@ -14,7 +14,7 @@ month_labels:
- Jun 2026
- Jul 2026
providers:
- name: Emil Vitukhnovskii trading a
- name: GreatFlower
asn: 202226
bulletproof: false
total: 33999
@@ -85,7 +85,7 @@ providers:
- 5270
- 1856
asn_totals:
- name: Emil Vitukhnovskii trading a
- name: GreatFlower
asn: 202226
bulletproof: false
events: 33999
+13 -4
View File
@@ -49,10 +49,19 @@ def month_label(ym: str) -> str: # "2026-04" -> "Apr 2026"
def short(name: str) -> str:
"""Readable provider label from a Cymru AS name or a bulletproof-map name.
'H2NEXUS-AS - H2NEXUS LTD, GB' -> 'H2NEXUS LTD'."""
s = name.split(" - ", 1)[-1].strip()
s = re.sub(r",\s*[A-Z]{2}$", "", s) # drop trailing country code
return s[:28]
Cymru gives 'HANDLE - Org Description, CC'. Prefer the org (usually the
recognizable company: 'H2NEXUS-AS - H2NEXUS LTD, GB' -> 'H2NEXUS LTD'), but
fall back to the short AS handle when the org would overflow the label, so a
long trade name isn't cut mid-word ('GreatFlower - Emil Vitukhnovskii trading
as Great Flower, IL' -> 'GreatFlower', not 'Emil Vitukhnovskii trading a')."""
LIMIT = 28
handle = name.split(" - ", 1)[0].strip()
org = re.sub(r",\s*[A-Z]{2}$", "", name.split(" - ", 1)[-1].strip())
if len(org) <= LIMIT:
return org
if 0 < len(handle) <= LIMIT and handle != org:
return handle # long org -> use the clean AS handle instead
return org[:LIMIT].rsplit(" ", 1)[0] or org[:LIMIT] # last resort: trim at a word
def key(rec) -> str: