Otacon finds domains impersonating yours — typosquats, homoglyph fakes, combosquats, IDN/punycode tricks and more. It generates hundreds of variants, checks which are actually registered, and scores each by real-world phishing risk. One command, ~10 seconds, no paid APIs.
⚠ 3 registered · crit: 1 · mx: 1 · fresh <7d: 1
Otacon · target: github.com
┌──────────────────────────────────────┬────────────────┬────────┬───────┬───────┬───────┬─────────┐
│ Domain │ Risk │ Age │ DNS │ MX │ SSL │ HTTP │
├──────────────────────────────────────┼────────────────┼────────┼───────┼───────┼───────┼─────────┤
│ githubupdate.com │ ███████░ 92 │ 3d │ ✓ │ ✓ │ ✓ │ 200 │
│ combosquat │ │ │ │ │ │ │
│ "GitHub - Security Update Required" │ │ │ │ │ │ │
│ bithub.com │ █████░░░ 68 │ 2y │ ✓ │ — │ ✓ │ 301 │
│ typo │ │ │ │ │ │ │
│ githuub.com │ ████░░░░ 48 │ 8mo │ ✓ │ — │ — │ 404 │
│ typo │ │ │ │ │ │ │
└──────────────────────────────────────┴────────────────┴────────┴───────┴───────┴───────┴─────────┘
Permutations: 143 · registered: 3 · med: 1 · high: 1 · crit: 1
Table of contents
- Why Otacon?
- Quick start
- Install
- Usage
- Modes
- Output formats
- How scoring works
- Detection techniques
- CI/CD integration
- Interactive mode
- Whitelist / defensive flag
- Comparison with dnstwist
- FAQ
- Troubleshooting
- Contributing & development
- License & ethics
Why Otacon?
Phishing campaigns almost always start with a lookalike domain. Attackers register github-update.com, paypa1.com, goog1e.com weeks (or days) before the actual attack. By the time anyone notices, credentials are already gone.
Otacon is built for the people who need to find those domains before the attack lands:
| Role | Use case |
|---|---|
| Pentester / red team | Reconnaissance — find existing lookalikes against the client's brand to include in scope or use in social-engineering tests |
| Blue team / SOC | Scheduled audits of your own domain (e.g. a daily CI cron job) to catch a new fake fast, especially one with MX or fresh registration |
| Brand protection | Audit hundreds of variants in one shot, export as JSON/HTML for the legal team or DMCA filings |
| CI/CD gate | Block deploys when a critical impersonation is live (--fail-on critical) |
Otacon is fully passive: DNS queries, a TLS handshake, one HTTP GET per variant. No exploit attempts, no auth, no scraping at scale.
Quick start
pipx install git+https://github.com/notimeftnoir/otacon.git
otacon scan example.com
That's it. You get a colored terminal table, ranked by risk, in about 10 seconds.
Need a wordlist without network checks? otacon generate example.com -o wordlist.txt
Want guided mode? Just run otacon with no arguments.
Install
Recommended — isolated global install via pipx:
pipx install git+https://github.com/notimeftnoir/otacon.git
Alternative — standard pip into any active virtual environment:
pip install git+https://github.com/notimeftnoir/otacon.git
macOS / Kali / Debian: the
aiodnsdependency needs thec-aressystem library.brew install c-ares(macOS) ·sudo apt install libc-ares-dev(Debian/Kali/Ubuntu)Windows: no extra deps — wheels are prebuilt. Otacon auto-switches to
SelectorEventLoopto avoid known Proactor issues.
Development install (from source, with tests)
git clone https://github.com/notimeftnoir/otacon.git
cd otacon
python3 -m venv .venv
source .venv/bin/activate # Linux / macOS
.venv\Scripts\activate # Windows (PowerShell)
pip install -e ".[dev]"
pytest && ruff check .
Usage
otacon # interactive mode (guided prompts)
otacon scan example.com # one-shot scan, all signals
otacon scan example.com --no-http # DNS only — faster, fewer signals
otacon scan example.com --all # include unregistered variants
otacon scan example.com --concurrency 100 # crank concurrency (default 50)
otacon scan example.com --exclude "alias.com,brand.io"
otacon scan example.com --exclude-file allowed.txt
otacon scan example.com --json r.json --html r.html --markdown r.md --csv r.csv
otacon scan example.com --fail-on high # CI gate — exit 2 on high/critical
otacon scan example.com --weights-file weights.json # custom scoring weights
# Scan multiple domains (sequential)
otacon scan github.com google.com example.com
# Scan from a file (one domain per line, # = comment)
otacon scan --domains-file brands.txt
# Combine: extra domains alongside a file
otacon scan mycompany.com --domains-file more_brands.txt
# Aggregated HTML report for all domains
otacon scan github.com google.com --html report.html
# Aggregated JSON export
otacon scan --domains-file brands.txt --json results.json
# Fail CI if any domain has a critical hit
otacon scan --domains-file brands.txt --fail-on critical
otacon generate example.com -o variants.txt # wordlist only, no network
otacon --debug scan example.com # global flags precede the subcommand
otacon --version # print version and exit
CLI flags reference
Placement matters:
--debug/--quiet/--versionbelong tootaconitself and must come before the subcommand (otacon --debug scan example.com). Every other flag below belongs toscanand comes after it (otacon scan example.com --fail-on high).
Global options (before the subcommand):
| Flag | Default | Description |
|---|---|---|
--debug, -v, --verbose |
off | Log DNS/WHOIS/HTTP graceful-degradation events to stderr. |
--quiet, -q |
off | Disable UI/banner/progress; print JSON to stdout. |
-V, --version |
— | Print version and exit. |
scan options (after otacon scan):
| Flag | Default | Description |
|---|---|---|
--no-http |
off | Skip HTTP & TLS probing. DNS+MX+WHOIS only. Disables the ⚑ defensive flag. |
--all |
off | Show unregistered variants in the output. |
-c, --concurrency |
50 |
Max concurrent DNS/HTTP checks. Raise carefully — your resolver may rate-limit. |
-x, --exclude |
— | Comma-separated whitelist: --exclude "alias.com,brand.io" |
--exclude-file |
— | Path to a file with one domain per line. # starts a comment. |
--domains-file, -D |
— | File with one domain per line (# = comment) — scan many targets in one run. |
--json |
— | Write the full report (every variant, every signal, every reason) to a file. |
--markdown / --md |
— | Write a Markdown table ready to paste into a ticket. |
--html |
— | Write a self-contained dark-theme HTML report. |
--csv |
— | Write a CSV report of registered domains (spreadsheet-safe, CWE-1236 hardened). |
--fail-on |
— | low / medium / high / critical — exit 2 when any registered variant reaches this level. |
-w, --weights-file |
— | Path to a JSON file overriding default scoring weights (see below). |
Custom scoring weights (--weights-file)
Override any subset of the default point values without touching scoring.py. Unspecified keys keep their default:
{
"points_mx": 30,
"points_ssl_fresh": 15,
"kind_base": { "homoglyph": 30 }
}
otacon scan example.com --weights-file weights.json
generate options (after otacon generate):
| Flag | Default | Description |
|---|---|---|
-n, --limit |
0 (all) |
Print only the first N variants. The file written by --output always contains every variant. |
-o, --output |
— | Write the variants, one per line, to a file — a wordlist for subfinder, nuclei, ffuf, etc. |
-x, --exclude |
— | Comma-separated whitelist, same syntax as scan. |
--exclude-file |
— | Path to a whitelist file, one domain per line. |
Exit codes (for CI gating)
| Code | Meaning |
|---|---|
0 |
Clean — nothing at/above the --fail-on threshold |
1 |
Runtime error (bad input, empty domain, file I/O error) |
2 |
Threshold breached — at least one registered variant met --fail-on |
Modes
Otacon has two subcommands plus an interactive guided mode:
| Mode | Network? | Use when |
|---|---|---|
scan |
yes | One-shot audit. Most common. |
generate |
no | Offline wordlist generation. Useful for feeding into external tooling (subfinder, nuclei, etc.) or sanity-checking what Otacon would check. |
| (no subcommand) | yes | Interactive mode — guided prompts, then a post-scan action loop (open in browser, WHOIS, rescan, allow-list). |
Output formats
One scan, five ways to consume it. Pick the one that fits your workflow:
| Format | Flag | Best for |
|---|---|---|
| Rich terminal table | (default) | Interactive triage — colors, risk bars, live streaming as hits arrive |
| JSON | --json r.json |
Pipelines, SIEM ingestion, custom analytics. Includes risk_reasons for every variant. |
| Markdown | --md r.md |
Paste straight into Jira / GitHub issues / Slack |
| HTML | --html r.html |
Hand to legal / compliance / management. Self-contained dark-theme file, no external dependencies. |
| CSV | --csv r.csv |
Spreadsheets — registered domains only, formula-injection-safe cells |
You can pass all export flags at once — every format is rendered from the same in-memory report, so they always agree.
JSON structure (excerpt)
{
"target": "github.com",
"started_at": "2026-06-08T14:23:01+00:00",
"total_permutations": 143,
"results": [
{
"domain": "githubupdate.com",
"kind": "combosquat",
"resolves": true,
"has_mx": true,
"has_ssl": true,
"http_status": 200,
"page_title": "GitHub - Security Update Required",
"created_at": "2026-06-05T09:12:00+00:00",
"age_days": 3,
"risk_score": 92,
"risk_level": "critical",
"risk_reasons": [
"technique: combosquat (+20)",
"resolves to an IP (+10)",
"has an MX record — ready for email phishing (+25)",
"active SSL certificate (+15)",
"responds HTTP 200 — active site (+15)",
"registered 3 days ago (+20)"
],
"is_likely_defensive": false
}
]
}
Every score is fully decomposed — you can always answer "why did this get 92?".
How scoring works
Every score is the sum of explicit, explainable signals — no ML, no black box. Every reason is exposed in the JSON export (risk_reasons) and in the interactive detail view.
Signal point values
| Signal | Points |
|---|---|
| MX record — ready for email phishing | +25 |
| Technique — homoglyph / IDN | +25 |
| subdomain spoof | +22 |
| combosquat · www-merge | +20 / +20 |
| typo | +18 |
| soundsquat | +16 |
| bitsquat · vowel-swap | +15 / +14 |
| hyphenation · plural · TLD-swap | +12 / +10 / +10 |
| Domain age — <7 days | +20 |
| <30 days · <90 days | +12 / +5 |
| SSL certificate active | +15 |
| HTTP 2xx live · 3xx redirect | +15 / +10 |
| 4xx · 5xx | +5 / +3 |
| Resolves to an IP | +10 |
| Redirects elsewhere (non-2xx, non-3xx) | +5 |
Score is capped at 100. Unregistered domains always score 0.
Risk levels
| Level | Score | Meaning |
|---|---|---|
| 🔴 critical | 80–100 | Active infrastructure + email-ready — treat as a live threat. Investigate immediately. |
| 🟠 high | 60–79 | Registered with serious signals (MX or live site). Add to monitoring; consider takedown. |
| 🟡 medium | 35–59 | Registered, some signals — worth watching. |
| 🔵 low | 15–34 | Registered, minimal signals. Could be parked / unused. |
| 🟢 safe | 0–14 | Unregistered or negligible. |
📐 Full architecture, pipeline, and design rationale →
docs/DESIGN.md
Detection techniques
Otacon implements 12 permutation techniques modeled on real-world attacks. The homoglyph table is cross-checked against Unicode's own confusables.txt so every look-alike character is a documented substitution, not a guess — it covers all 26 letters, not just the handful that are easy to eyeball.
| Technique | Example (example.com) |
Real attack vector |
|---|---|---|
| Homoglyph | examp1e.com, ex4mple.com |
Visual identity — humans can't tell the difference |
| IDN / Punycode | xn--exampe-7db.com (l → ł) |
ACE-encoded unicode that browsers may render natively |
| Typo | exmple.com, exsmple.com, exampel.com |
Fat-finger typing on QWERTY keyboards |
| Combosquat | example-login.com, secureexample.com |
Adds "trust" keyword — common in phishing email links |
| TLD swap | example.io, example.top, example.icu |
Same name, different (often cheap/abused) TLD |
| Subdomain spoof | example.com.login.net |
Original domain as a label; URL-bar trickery |
| Bitsquat | axample.com (e→a is one bit flip) |
DRAM/DNS memory errors flip a single bit |
| Hyphenation | ex-ample.com |
Insert/remove a hyphen |
| Soundsquat | eksample.com |
Phonetic substitution (ph/f, c/k, s/z, x/ks) |
| Vowel swap | exomple.com, exumple.com |
Replace one vowel with another |
| Plural | shops.com ← shop.com |
Singular ↔ plural variation |
| WWW-merge | wwwexample.com |
Dot dropped between "www" and the domain — easy to misread |
Every example above is real output, not an illustration. Two details worth knowing before you read a report:
- Unicode look-alikes are emitted as punycode. Swapping the
linexample.comfor a Polishłis reported asxn--exampe-7db.com, because that is the name DNS actually resolves and the form you will see in logs. Those land under IDN, which leaves the Homoglyph rows for the ASCII confusables (1/l,4/a,rn/m). - Each variant is reported once, under the first technique that produced it.
Techniques overlap, and the priority order is the one in this table. That is
why
examples.comis labelled a typo rather than a plural for a target likeexample.com:ssits next toeon QWERTY, so the typo generator reaches it first. Onshop.com, where no adjacent key produces it,shops.comcomes through as a plural.
The generator deduplicates results and never includes the original domain in the output.
CI/CD integration
Add a brand-protection gate to your release pipeline. If a critical impersonation goes live, the pipeline fails.
GitHub Actions
name: brand-protection
on:
schedule:
- cron: "0 6 * * *" # daily at 06:00 UTC
workflow_dispatch:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: sudo apt-get install -y libc-ares-dev
- run: pip install git+https://github.com/notimeftnoir/otacon.git
- run: otacon scan example.com --json report.json --fail-on critical
- if: failure()
uses: actions/upload-artifact@v4
with:
name: otacon-report
path: report.json
GitLab CI
brand-protection:
image: python:3.12
before_script:
- apt-get update && apt-get install -y libc-ares-dev
- pip install git+https://github.com/notimeftnoir/otacon.git
script:
- otacon scan $CI_PROJECT_NAME.com --html report.html --fail-on high
artifacts:
when: always
paths: [report.html]
only: { refs: [schedules] }
Interactive mode
Run otacon with no subcommand for a guided experience. After the scan, you can act on each registered domain individually:
Action for githubupdate.com:
[*] [o]pen — open in browser
[w]hois — show registration info
[e]xport — save result as JSON
[a]llow — skip in this session
[r]escan — re-check this domain now
[b]ack — pick a different domain
[q]uit — exit actions
Designed for triaging a fresh scan without leaving the terminal: open the suspicious site, check WHOIS, decide to allow-list or escalate.
Whitelist / defensive flag
Brands often own their own lookalikes defensively (e.g., google.com owns gooogle.com and redirects it). Otacon flags these with ⚑ when the redirect points back to the original:
microsft.com ⚑ → microsoft.com crit(85) — but defensive
After the scan, Otacon offers to write all ⚑-flagged domains to whitelist.txt. Future runs in the same directory pick this file up automatically; you can also point at a custom file with --exclude-file path/to/list.txt.
Whitelist file format:
# defensive registrations, owned by us
gooogle.com
goggle.com
g00gle.com
Whitelisted domains are skipped before any network call — saving both time and quota.
Comparison with dnstwist
dnstwist is the OG tool in this space. Otacon and dnstwist solve overlapping problems with different priorities.
| Otacon | dnstwist | |
|---|---|---|
| Risk score with explained signals | ✓ 0–100, every point sourced | ✗ raw signals only |
| Defensive-registration flag | ✓ ⚑ on redirect-to-original | ✗ |
| CI/CD exit code gating | ✓ --fail-on |
✗ |
| Self-contained HTML report | ✓ dark theme, no JS | partial (--format html) |
| Interactive post-scan triage | ✓ open/whois/rescan/allow | ✗ |
| Permutation techniques | 12 | 13+ |
| Visual screenshots of pages | ✗ | ✓ |
| Fuzzy/phonetic dictionary attacks | ✓ soundsquat | ✓ |
| GeoIP / Whois enrichment | WHOIS only | both |
Use dnstwist if you want screenshots, fuzzy hashing, deeper enrichment. Use Otacon if you want an opinionated risk score, defensive-flag detection, and a CI-friendly exit code.
FAQ
Is this legal? Does it touch the target domains?
Otacon performs only passive recon: standard DNS queries, a TLS handshake on :443 (no data exchange), and a single HTTP GET. No login attempts, no scanning, no scraping at scale. This is the same level of activity as visiting the page in a browser.
That said, use it on your own domains or within authorized engagements. See LICENSE and SECURITY.md.
Why no machine learning?
Because pentesters and SOC analysts need to defend their findings. "Our model gave it 87" is not a defensible answer. risk_reasons is. Every score in Otacon comes with the exact list of signals that produced it — auditable in five seconds, tunable in a one-line edit to scoring.py.
How fast is it?
A full scan with HTTP probing on a 150-permutation domain typically completes in 8–15 seconds on a residential connection. DNS-only mode (--no-http) is roughly 3× faster.
Bottlenecks are usually:
- WHOIS query rate-limiting (we cap at 4 concurrent)
- DNS resolver latency (default
--concurrency 50is conservative; raise it on a server with a fast resolver) - The
c-areslibrary — make sure it's installed natively, not falling back to Python's stdlib resolver
Will it find IDN homoglyph attacks (xn-- domains)?
Yes. The IDN technique generates punycode-encoded variants for each Unicode homoglyph. They show up as xn--... in the table. Score base is +25, same as homoglyph — these are the most dangerous because they render as the original glyph in most browsers.
Does it work on internationalized domains (non-ASCII targets)?
Partially. Otacon accepts unicode input but the permutation engine is tuned for ASCII labels. IDN/punycode encoding works for output (generated homoglyphs of an ASCII original). True i18n of the engine is on the roadmap.
Why does my scan show 0 results when I know there are fakes?
Most likely causes, in order:
- DNS-only mode missed them — with
--no-http, you only see variants that resolve. Try a full scan. - They're behind Cloudflare / a CDN — they resolve but the SSL/HTTP probe times out. Increasing
--concurrencydoesn't help; raise the per-request timeout inresolver.pyif it's a recurring issue. - Your DNS resolver is rate-limiting — try with a different resolver or lower
--concurrency. - The fakes are on a TLD not in our default list — open an issue with the TLD.
Where is the WHOIS data coming from?
We use asyncwhois, which talks directly to TLD WHOIS servers (no third-party API, no quota). Some TLDs (e.g., .ai, .io) sometimes return rate-limited or stripped responses — in that case age_days will be null and the age-based scoring contribution is just skipped (graceful degradation).
How do I add my own permutation technique?
- Add a value to the
PermutationTypeenum inmodels.py - Add a
_my_technique(label: str) -> set[str]function inpermutations.py - Add it to the pipeline list in
generate()— order matters for dedup priority - Add a base score in
scoring._KIND_BASE - Open a PR with tests in
tests/test_permutations.py
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
ModuleNotFoundError: aiodns on install |
c-ares system library missing |
apt install libc-ares-dev / brew install c-ares, then reinstall |
| Scan hangs at "Checking variants" | DNS resolver unreachable or rate-limiting | Lower --concurrency, switch resolver (1.1.1.1, 8.8.8.8) |
WHOIS always — for .ai / .io / .pl |
Registry WHOIS rate-limit | Re-run later; this is normal |
Windows: ConnectionResetError [WinError 10054] |
Should be auto-fixed in 1.0+ | Ensure you're on the latest version; this used the Proactor loop, we've switched to Selector on Windows |
Unverified HTTPS request warnings |
Suppressed intentionally — we probe bad certs on purpose | n/a, hidden by default |
If something still doesn't work, please open an issue with:
- Otacon version (
otacon --version) - Python version, OS, and architecture
- The full command and (if safe to share) the target
- The error message or unexpected behavior
Contributing & development
See CONTRIBUTING.md for the dev setup, lint, and test commands.
Quick summary:
git clone https://github.com/notimeftnoir/otacon.git
cd otacon
python -m venv .venv && source .venv/bin/activate # or .venv\Scripts\activate
pip install -e ".[dev]"
pytest && ruff check .
Architecture deep-dive: docs/DESIGN.md.
Security disclosure policy: SECURITY.md.
License & ethics
MIT License. Use it freely.
Otacon is passive only — DNS queries, a TLS handshake, a single HTTP GET per variant. No exploit attempts, no brute-forcing, no auth.
Use only on:
- Domains you own
- Domains within an authorized security testing engagement (with written scope)
- Domains you have explicit permission to monitor
Do not use to: harass, dox, or build attack tooling. If you found this useful for a defense engagement, say hi — feedback shapes the roadmap.
Release files for otacon 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| otacon-1.0.0.tar.gz | 103.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| otacon-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 164.8 kB
Release files / otacon-1.0.0.tar.gz
| Download URL | otacon-1.0.0.tar.gz |
|---|---|
| Size | 103.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e2839fc8fcdd8268e051ca5c6e26e60d59e9b0e822999b4fbb6264033751756d
|
|
BLAKE2b-256 checksum How to use checksums |
18b72c3c728641408f4c3be48937d111a7a8c3e86312a6dae1b26fc1f9951ea5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency logRelease files / otacon-1.0.0-py3-none-any.whl
| Download URL | otacon-1.0.0-py3-none-any.whl |
|---|---|
| Size | 61.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b2aa0b606f44094fcf884f438db0d5c25c026abb06545f0e61b6fd110be0b7fa
|
|
BLAKE2b-256 checksum How to use checksums |
8f7555d677a538f4027e9d57e7285a37510af9e6528d1ffd4e4a1418a1c3bb1b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 20, 2026.
Transparency log