Metadata-Version: 2.5
Name: handle-correlation
Version: 0.6.0
Summary: Calibrated same-actor scoring for observed usernames and handles
Project-URL: Homepage, https://github.com/OWNER/handle-correlation
Author: Tushar Karumudi
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: actor-attribution,attribution,entity-resolution,osint,sockpuppet,threat-intelligence,username
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Information Technology
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Requires-Python: >=3.11
Requires-Dist: attribution-graph>=0.1.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# handle-correlation

[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](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.

```bash
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.

```python
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:

```python
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.

```python
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

```bash
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:

```bash
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

```python
>>> 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

```csv
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](https://github.com/OWNER/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](https://github.com/apurvsinghgautam/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.
