Metadata-Version: 2.4
Name: ioc-triage
Version: 0.3.2
Summary: Bulk IOC triage CLI: VirusTotal, AbuseIPDB, Kaspersky OpenTIP, URLhaus, ThreatFox, MalwareBazaar
Author: AlbedoGown
License-Expression: MIT
Project-URL: Homepage, https://github.com/AlbedoGown/ioc-triage
Project-URL: Issues, https://github.com/AlbedoGown/ioc-triage/issues
Keywords: ioc,threat-intelligence,soc,dfir,virustotal,abuseipdb,abuse.ch,opentip
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: System Administrators
Classifier: Natural Language :: Russian
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

<div align="center">

**English** | [Русский](https://github.com/AlbedoGown/ioc-triage/blob/main/README.ru.md)

# ioc-triage

**Check a whole batch of IOCs with one command**

IP · domains · URLs · MD5 / SHA1 / SHA256 → VirusTotal, AbuseIPDB, Kaspersky OpenTIP, URLhaus, ThreatFox, MalwareBazaar

[![tests](https://github.com/AlbedoGown/ioc-triage/actions/workflows/tests.yml/badge.svg)](https://github.com/AlbedoGown/ioc-triage/actions/workflows/tests.yml)
![python](https://img.shields.io/badge/python-3.10%2B-blue)
![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
[![license](https://img.shields.io/badge/license-MIT-green)](https://github.com/AlbedoGown/ioc-triage/blob/main/LICENSE)

</div>

---

`ioc-triage` checks a list of indicators of compromise against several reputation sources in a single run. The type of each indicator is detected automatically, and every request goes through the official APIs with your own keys, with no scraping of web interfaces.

## What it does

- **Detects the type for you.** IP, domain, URL, MD5, SHA1, SHA256: no hints needed.
- **Six sources.** All through official APIs, each in its own module behind a common interface.
- **Internal addresses stay internal.** RFC1918, CGNAT, link-local and loopback are flagged as internal right away, without a single outbound request.
- **Parallel requests within rate limits.** Sources are queried at the same time, each with its own pacing (free VirusTotal gives you 4 requests a minute). An HTTP 429 shows up as a clear message in the results.
- **Remembers what it already checked.** Results stay in a local cache for 24 hours, so re-running the same indicators doesn't burn your quota. Network errors never get cached.
- **One flaky source won't take it down.** A missing key, a dropped connection or a rejected key is just a row in the results, not a traceback halfway through.
- **Talks to humans and scripts.** A table in the terminal, `--output csv` or `--output json`.
- **Zero dependencies.** Python standard library only.

## Sources

| Source | Checks | Key | Free tier |
|---|---|---|---|
| [VirusTotal](https://www.virustotal.com/) API v3 | hashes, URLs, domains, IPs | `VT_API_KEY` | 4 requests a minute, 500 a day |
| [AbuseIPDB](https://www.abuseipdb.com/) API v2 | IPs | `ABUSEIPDB_API_KEY` | 1000 checks a day |
| [Kaspersky OpenTIP](https://opentip.kaspersky.com/) | hashes, URLs, domains, IPs | `OPENTIP_API_KEY` | not published |
| [MalwareBazaar](https://bazaar.abuse.ch/) | hashes | `ABUSECH_AUTH_KEY` | not stated |
| [URLhaus](https://urlhaus.abuse.ch/) | URLs, plus IPs and domains as a fallback | `ABUSECH_AUTH_KEY` | not stated |
| [ThreatFox](https://threatfox.abuse.ch/) | IPs, domains | `ABUSECH_AUTH_KEY` | not stated |

All keys are free. If a key is missing, the run doesn't break: that source is simply reported as skipped.

## Quick start

```bash
pip install ioc-triage
```

Then put your keys into a `.env` (variable names are in the table above) and check your first indicator:

```bash
cat > .env <<'KEYS'
VT_API_KEY=
ABUSEIPDB_API_KEY=
OPENTIP_API_KEY=
ABUSECH_AUTH_KEY=
KEYS

echo "8.8.8.8" | ioc-triage
```

A `.env` in the current directory is picked up automatically. If your keys live somewhere else, point to the file with `--env-file`. Variables already set in the environment win over the file.

## Usage

```bash
# one indicator per line
ioc-triage --input iocs.txt

# CSV: tell it which column holds the indicator
ioc-triage --input alerts.csv --column src_ip

# via stdin, e.g. every unique IP from a firewall log
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' firewall.log | sort -u | ioc-triage

# structured output for further processing
ioc-triage --input iocs.txt --output json > result.json
ioc-triage --input iocs.txt --output csv  > result.csv

# cache: your own lifetime, or none at all
ioc-triage --input iocs.txt --cache-ttl 6
ioc-triage --input iocs.txt --no-cache

# keys from a specific file and fewer parallel requests
ioc-triage --input iocs.txt --env-file ~/secrets/ioc.env --workers 2
```

The cache lives in `~/.cache/ioc-triage/cache.json` and only you can read it. It stores real answers from the sources, "not found" included. Errors, skipped sources and rate-limit responses never go in.

You can tune the pause for each source with `VT_RATE_LIMIT_PER_MIN`, `OPENTIP_RATE_LIMIT_PER_MIN`, `ABUSEIPDB_RATE_LIMIT_PER_MIN` and `ABUSECH_RATE_LIMIT_PER_MIN`. An empty value means no pause; VT defaults to 4.

### What's in the results

One row for every indicator and source pair. The fields are the same everywhere: table columns, CSV columns and JSON keys.

| Field | What it holds |
|---|---|
| `indicator`, `indicator_type` | the indicator and its type |
| `source` | `virustotal`, `abuseipdb`, `opentip`, `abuse.ch/...`, or `local` for internal addresses |
| `detections` | how many engines fired (VT), confidence and report count (AbuseIPDB), malware family (abuse.ch), detection names (OpenTIP) |
| `first_seen` | when the indicator was first spotted, if the source knows |
| `context` | tags, country, provider and other details from the source |
| `verdict` | the conclusion, or why the source was skipped or didn't answer |

The tool never fills in the blanks: a field only holds what the source actually returned. If a source has no such field, it stays empty (`null` in JSON).

## Getting API keys

<details>
<summary><b>VirusTotal</b></summary>

1. Sign up at https://www.virustotal.com/
2. Grab your key at https://www.virustotal.com/gui/my-apikey and put it into `VT_API_KEY`.

The free key gives you just 4 requests a minute, so the tool waits 15 seconds between requests. At the start it prints on stderr how long the run will take, and in a terminal it shows a progress line. A hundred indicators take about 25 minutes. Ctrl+C stops the run right away. With a premium key, raise the limit via `VT_RATE_LIMIT_PER_MIN`.

How the verdict works: 3 or more engines means "malicious", 1 or 2 means "suspicious". A single detection on a popular domain is almost always a false positive, which is why the threshold isn't one. Change it with `VT_MALICIOUS_THRESHOLD`.

</details>

<details>
<summary><b>AbuseIPDB</b></summary>

1. Sign up at https://www.abuseipdb.com/
2. Create a key at https://www.abuseipdb.com/account/api and put it into `ABUSEIPDB_API_KEY`.

Reports from the last 90 days count. Confidence of 75% or more means "malicious", 25% or more means "suspicious".

</details>

<details>
<summary><b>Kaspersky OpenTIP</b></summary>

1. Sign up at https://opentip.kaspersky.com/ and accept the terms.
2. Create a key at https://opentip.kaspersky.com/token/ and put it into `OPENTIP_API_KEY`.

The verdict comes from the `Zone` field: Red is "malicious", Orange and Yellow are "suspicious", Green is "looks legitimate", Grey is "no data".

A few things to know:
- Kaspersky doesn't publish its limits, so set your own pacing with `OPENTIP_RATE_LIMIT_PER_MIN`.
- HTTP 403 can mean either a bad key or a used-up quota.
- The tool only makes reputation lookups and never uploads files to the cloud.
- OpenTIP won't take a URL without a path (like `https://example.com/`) as a URL and answers 400. The tool checks such a URL as a domain or IP instead and says so in `context`.
- URL indicators travel as a query parameter, so they may end up in the service's logs. Keep that in mind for sensitive URLs.

</details>

<details>
<summary><b>abuse.ch</b> (URLhaus, ThreatFox, MalwareBazaar)</summary>

Since 2024 the abuse.ch APIs answer `401 Unauthorized` without a key. One key covers all three services.

1. Sign in at https://auth.abuse.ch/ with GitHub, Google or LinkedIn.
2. Click "Generate Key" in your profile and put the key into `ABUSECH_AUTH_KEY`.

| Type | Where the query goes |
|---|---|
| MD5 / SHA1 / SHA256 | MalwareBazaar `get_info` |
| URL | URLhaus `/v1/url/` |
| IP / domain | ThreatFox `search_ioc`, and if nothing turns up, URLhaus `/v1/host/` |

ThreatFox searches by substring, so `google.com` matches, for example, `proxy.google.com`. The tool only counts exact matches. The exception: `ip:port` entries count for a bare IP.

</details>

## How it's built

```
ioc_triage/
├── cli.py            # arguments, input reading, routing by type
├── detect.py         # indicator type detection, private networks
├── cache.py          # local TTL cache
├── envfile.py        # .env loader
├── output.py         # table / csv / json
└── sources/
    ├── __init__.py   # Verdict dataclass
    ├── virustotal.py
    ├── abuseipdb.py
    ├── opentip.py    # Kaspersky OpenTIP
    ├── abusech.py    # URLhaus, ThreatFox, MalwareBazaar
    └── ratelimit.py  # per-source request pacing
```

Every source is a module with a `check(indicator, indicator_type) -> Verdict` function. A new source takes one file and one line in `SOURCES_BY_TYPE` in `cli.py`.

## Development

```bash
git clone https://github.com/AlbedoGown/ioc-triage.git
cd ioc-triage
pip install -e .
python -m unittest discover -s tests -v
```

Tests mock HTTP responses and never touch the network, so you don't need any keys to run them.

## Disclaimer

This is an educational and portfolio project. The tool brings public reputation data together in one place, but it doesn't replace incident analysis and doesn't vouch for the accuracy of third-party verdicts. And please respect each API's terms of use.

## License

[MIT](https://github.com/AlbedoGown/ioc-triage/blob/main/LICENSE) © 2026 AlbedoGown
