Skip to main content

handle-correlation

License

Calibrated same-actor scoring for usernames and handles.

Given handles you have already observed — forum accounts, commit authors, Telegram and X profiles, malware config strings, ad network records — decide which ones belong to the same actor, with a probability you can defend.

pip install handle-correlation
handlecorr score --observations case.csv --corpus usernames.txt

What this is, and what it deliberately is not

It scores handles. It does not discover them. There is no enumeration here: nothing in this package queries a platform to ask whether a username exists. Input is a CSV or JSON of observations the analyst collected under their own authority.

That line is where it is for two reasons.

The analytic reason. Enumeration is solved — maigret and Sherlock check hundreds of sites and do it well. What none of them do is tell you how much a match is worth. They report that dave exists on 180 sites and that kr4ken_x99 exists on 3, with no indication that the second result is worth a thousand times more than the first. The unsolved problem is the scoring, and scoring is what this does.

The design reason. An engine that takes a bare handle and returns profiles plus a probable email is the same artifact whether the input is a ransomware affiliate or someone's ex-partner. There is no technical signal that distinguishes them. Requiring the analyst to supply observations they already collected keeps the collection decision — and its legal basis — with the person who has the authority to make it, rather than buried inside a library.

If you have an authorized investigation that needs enumeration, run maigret and feed its output in. The interface is a CSV.

The headline property

Handle reuse across eight platforms cannot produce an attribution on its own.

o = [Observation("kr4ken", p) for p in
     ("github", "telegram", "x", "forum", "gitlab", "npm", "reddit", "keybase")]

a = assess(all_claims(o), corpus.holders)
a.independent_groups   # 1
a.band                 # Band.WEAK

That is the correct answer and most tooling gets it wrong. Eight platforms showing the same handle is one observation of one naming habit, made once. Treating it as eight independent confirmations is how a confident false accusation gets built. Every same-root claim lands in a single correlation group, so it aggregates as max + log(1+n) rather than summing.

Add one cryptographic binding and it resolves:

o = [Observation("kr4ken",   "github",   linked={"pgp": FPR}),
     Observation("kraken_x", "telegram", linked={"pgp": FPR})]
# independent_groups = 2, band = PROBABLE

A shared PGP fingerprint is a durable identifier the platform published. That is what turns reuse into attribution, and the model makes the difference explicit.

Correlation-point policy

Handle correlation gets a harder floor than the generic model, because handle reuse is simultaneously the commonest signal and the weakest — people pick similar names independently all the time, and an adversary can adopt a target's handle deliberately.

Points Level Meaning
0–1 INSUFFICIENT Not a lead. Consistent with two unrelated people.
2–3 LOW A lead for further collection. Never an attribution. Do not act on it and do not report it as identifying anyone.
4–5 MODERATE Only with a durable identifier; otherwise held at LOW.
6+ HIGH Requires a durable identifier (key, verified email, avatar hash).

Points count distinct evidence types, not claims. Eight platforms showing one handle is one point. Two profiles publishing the same PGP key is one point — one identifier corroborated twice, not two independent facts.

hc = correlation_points(all_claims(observations))
hc.points, hc.level.value, hc.caveat

The caveat string is written to be pasted directly into a report.

Worked example

python examples/end_to_end_handles.py

Offline and deterministic. Four scenarios showing where the policy bites, plus a section on why the handle policy and the generic scoring model can disagree — and why the handle policy governs when they do.

Signals

Signal Weight Notes
Same root after mutation MODERATE–STRONG kr4ken / xXkrakenXx / kraken.1988 → one root
Homoglyph substitution MODERATE Cyrillic а for Latin a is deliberate; raises confidence
Platform-published email STRONG From a profile you observed, not derived
PGP / SSH fingerprint AUTHORITATIVE Cryptographic binding
Gravatar hash STRONG
Linked domain MODERATE
Display name WEAK
Activity-hour overlap WEAK, corroborative-only Cannot create a link, only strengthen one
Timezone conflict ≥8h negative
Non-overlapping lifespans negative

Handle selectivity is the big one. Supply a username frequency corpus and dave becomes worth almost nothing while kr4ken_x99 becomes worth a great deal — automatically, without a hand-tuned weight table:

handlecorr score --observations case.csv --corpus top-usernames.txt

Without --corpus every handle looks unique and the CLI warns you. Scores from a corpus-less run are upper bounds, not assessments.

Mutation handling

>>> normalize("xXkr4ken_1988Xx").root
'kraken'
>>> compare("github:kr4ken", "telegram:kraken_x").relation
'same_root_mutated'
>>> compare("github:admin", "telegram:admin").meaningful
False

Leetspeak, separators, repeated characters, wrapper affixes (xX, _official, TTV), numeric suffixes, and Cyrillic/Greek homoglyphs. Edit-distance thresholds scale with root length, because two edits on a six-character root is coincidence and two edits on a thirteen-character root is a variant.

Generic and short handles are rejected outright rather than scored low — admin on two platforms is not weak evidence, it is no evidence.

Input format

handle,platform,first_seen,last_seen,source_url,link_email,link_pgp,hours,case_ref
kr4ken,github,2019-03-01,2024-11-02,https://...,,ABCD1234...,"{""14"":22,""15"":31}",CASE-1
kraken_x,telegram,2021-06-14,2026-01-20,https://...,k@example.com,,,CASE-1

Only handle and platform are required. Everything else improves the assessment; link_* columns are the ones that turn leads into findings.

Output

Feeds directly into attribution-graph for resolution and ICD 203 reporting:

     p  band          grp  pair
 0.961  ATTRIBUTED      2  handle:github:kr4ken  <->  handle:telegram:kraken_x
 0.029  WEAK            1  handle:x:kraken1988   <->  handle:npm:krakendev

1 actor cluster(s)
  - github:kr4ken, telegram:kraken_x

Not included: breach corpora

Credential dumps are BREACH_CORPUS in the source-class deny list and raise at collector load in the wider toolkit.

The distinction worth being precise about: correlating handles you observed on a forum — posts, profiles, public member lists you collected in an authorized investigation — is what this package is for. Ingesting a leaked credential dump to look up an email for a handle is a different thing. The second is unlawfully obtained data in most jurisdictions, has unmeasured accuracy, and taints the investigation that touches it. A dump-derived email is the one piece of evidence in your case you cannot put in a declaration.

Dark web sources

adtx_attribution.to_handle_observations() converts a Robin dark web investigation into observation rows, carrying the durable identifiers found on the same page as each handle. That is what can lift two forum accounts above the correlation-point floor — a shared PGP key on both pages, rather than the handle similarity alone.

Anything Robin's LLM concluded arrives capped at UNCERTAIN and confined to one correlation group. It can corroborate; it cannot establish.

Status

0.1.0, API unstable. Calibration is not validated against labelled ground truth; bands are principled, not fitted. Stylometry is stubbed deliberately — the literature does not support the accuracy that commercial tools claim at short text lengths, and a weak signal presented confidently is worse than no signal.

License

Apache-2.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

handle_correlation-0.6.0.tar.gz (28.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

handle_correlation-0.6.0-py3-none-any.whl (22.1 kB view details)

Uploaded Python 3

File details

Details for the file handle_correlation-0.6.0.tar.gz.

File metadata

  • Download URL: handle_correlation-0.6.0.tar.gz
  • Upload date:
  • Size: 28.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for handle_correlation-0.6.0.tar.gz
Algorithm Hash digest
SHA256 1aa7650c348872d61243e21ce9a1d0873e265448c9e07c5c3e9b73f2fa5c7296
MD5 7ec9d85f1dd2cfd8f525a7299b440f4d
BLAKE2b-256 67868df1ffe66aaf71566edfaaa8498ba4af71345031f07269d47974b0547fea

See more details on using hashes here.

File details

Details for the file handle_correlation-0.6.0-py3-none-any.whl.

File metadata

File hashes

Hashes for handle_correlation-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b06d748527f78d9c8a3c65eafbfb399766143c720b6423aa22b4485de4992d53
MD5 cc63b5f889082c4ff7aa8b9bbcb4f061
BLAKE2b-256 ce59f3c7ddb41299461d47324718a454cda1ad0d5be73c564c712ead2f92a6bd

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page