Metadata-Version: 2.4
Name: validin-sdk
Version: 0.1.0
Summary: A lightweight Python SDK for the Validin API
Author-email: Elliot Roe <elliot@validin.com>, Sreekar Madabushi <sreekar@validin.com>
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.31.0
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == "test"
Dynamic: license-file

# Validin Python SDK

A Python SDK for the Validin API, designed to enable large-scale monitoring and enrichment workflows.

## Requirements

- Python 3.9+
- A Validin API key

## Installation

```bash
pip install validin-python-sdk
```

```python
from validin import Client
```

## Configuration

The client requires an API key. The simplest approach is to set environment variables:

```bash
export VALIDIN_API_KEY="your-api-key"
```

Enterprise users must also set their endpoint:

```bash
export VALIDIN_BASE_URL="https://your-enterprise-endpoint.example.com"
```

You can also pass these directly to the client:

```python
from validin import Client

client = Client(api_key="your-api-key", base_url="https://your-enterprise-endpoint.example.com")
```

If `base_url` is not provided, the client defaults to `https://app.validin.com`.

## Quick Start

Monitor a DNS TXT record query and add new results to a project:

```python
from validin import Client

client = Client()

results = client.extra_history("anthropic-domain-verification*", lookback=1)
client.add_to_project("YOUR_PROJECT_ID", results.values)
```

For more advanced workflows, see the [`examples/`](examples/) directory.

## Core Concepts

### Indicators

An `Indicator` represents a queryable entity: a domain, IP address, hash, or raw string. The SDK automatically infers the type from a string, so you can pass raw strings directly to any client method:

```python
client.dns_history("example.com")       # inferred as domain
client.dns_history("1.1.1.1")           # inferred as IP
client.extra_history("some-txt-value*") # inferred as string (wildcard)
```

### ResultSet

A `ResultSet` is the container returned by every query method. It provides:

- **Iteration** — loop over individual records (`for record in results`)
- `.keys` — deduplicated list of `Indicator` objects from each record (the domains/IPs represented)
- `.keys_str` — string values of `.keys`
- `.values` — deduplicated list of record values
- `.to_rows()` — convert all records to a list of dictionaries
- `.aggregate()` — flatten nested bulk results into a single deduplicated ResultSet

### Records

Each query method returns a `ResultSet` containing typed record objects. Available record types:

- `DNSRecord`
- `CertificateRecord`
- `HostResponseRecord`
- `ScanRecord`
- `EnrichedIndicator` (from reputation lookups)
- `AnnotationRecord` (nested inside `EnrichedIndicator`)
- `LookalikeRecord`
- `RegistrationRecord`
- `YaraMatchRecord`
- `Content`

### Enriched Indicators

An `EnrichedIndicator` is a subclass of `Indicator` that carries reputation and annotation data directly on the indicator itself. You encounter enriched indicators in two places:

**1. Enrichment lookups** — `client.enrich()` returns a `ResultSet` where each record is an `EnrichedIndicator` with `score`, `verdict`, and `annotations`:

```python
results = client.enrich("example.com")
enriched = results[0]

print(enriched.value)        # "example.com"
print(enriched.score)        # 0
print(enriched.verdict)      # "benign"
print(enriched.annotations)  # tuple of AnnotationRecord objects
print(enriched.to_rows())    # annotations flattened to list of dicts
```

For bulk enrichment, use `.keys` to iterate the `EnrichedIndicator` objects:

```python
results = client.enrich(["example.com", "1.1.1.1", "google.com"])
for enriched in results.keys:
    print(f"{enriched.value}: score={enriched.score}, verdict={enriched.verdict}")
    print(enriched.to_rows())
```

**2. History queries with `annotate=True`** — When you pass `annotate=True` to `dns_history()`, `extra_history()`, `host_responses()`, or `registration_history()`, the `key` and `value` fields on returned records are upgraded from plain `Indicator` to `EnrichedIndicator` when the API provides intel data:

```python
results = client.dns_history("example.com", annotate=True)
for record in results:
    if isinstance(record.value, EnrichedIndicator):
        print(f"{record.value.value} has {len(record.value.annotations)} annotations")
        print(record.value.informational)
```

Since `EnrichedIndicator` is a subclass of `Indicator`, enriched indicators work everywhere a plain indicator does — you can pass them directly to `add_to_project()`, use them as input to other queries, or access `.value` and `.type` as usual.

### Bulk Queries and Aggregation

Any method that supports bulk queries can also accept a list of indicators instead of a single value:

```python
results = client.enrich(["example.com", "1.1.1.1", "google.com"])
```

When you pass a list, the SDK:

- Rate-limits requests to 5 queries/second
- Retries transient failures (408, 429, 500, 502, 503, 504) with exponential backoff
- Silently skips 404 responses

The returned `ResultSet` contains one child `ResultSet` per indicator. To flatten all results into a single deduplicated list, call `.aggregate()`:

```python
results = client.dns_history(["example.com", "google.com", "cloudflare.com"])

# Iterate per-indicator results
for child_result_set in results:
    print(child_result_set.query_key, len(child_result_set))

# Or flatten into one deduplicated set
all_records = results.aggregate()
print(all_records.to_rows())
```

## Available Methods

### `client.dns_history(indicator)`

DNS record history for a domain, IP, or string.


| Indicator Type | API Route                              |
| -------------- | -------------------------------------- |
| Domain         | `/api/axon/domain/dns/history/:domain` |
| IP             | `/api/axon/ip/dns/history/:ip`         |
| String         | `/api/axon/string/dns/history`         |


Supports bulk queries. Options: `limit`, `first_seen`, `last_seen`, `lookback`, `annotate`, `wildcard`, `categories_include`, `exclude_nx`.

---

### `client.extra_history(indicator)`

Extended DNS record history (TXT, SPF, etc.) for a domain, IP, or string.


| Indicator Type | API Route                            |
| -------------- | ------------------------------------ |
| Domain         | `/api/axon/domain/dns/extra/:domain` |
| IP             | `/api/axon/ip/dns/extra/:ip`         |
| String         | `/api/axon/string/dns/extra2`        |


Supports bulk queries. Options: `limit`, `first_seen`, `last_seen`, `lookback`, `annotate`, `wildcard`, `categories_include`, `exclude_nx`.

---

### `client.host_responses(indicator)`

HTTP crawl/response history for a domain, IP, hash, or string.


| Indicator Type | API Route                                |
| -------------- | ---------------------------------------- |
| Domain         | `/api/axon/domain/crawl/history/:domain` |
| IP             | `/api/axon/ip/crawl/history/:ip`         |
| Hash           | `/api/axon/hash/crawl/history/:hash`     |
| String         | `/api/axon/string/crawl/history`         |


Supports bulk queries. Options: `limit`, `lookback`, `annotate`.

---

### `client.enrich(indicator)`

Enrich a domain or IP with reputation score, verdict, and annotations. Returns a `ResultSet` of `EnrichedIndicator` objects (one per queried indicator), each carrying `score`, `verdict`, `annotations`, and `informational` fields.

Also available as `client.reputation()` (alias).


| Indicator Type | API Route                                   |
| -------------- | ------------------------------------------- |
| Domain         | `/api/axon/domain/reputation/quick/:domain` |
| IP             | `/api/axon/ip/reputation/quick/:ip`         |


Supports bulk queries. No additional options.

---

### `client.lookalikes(target)`

Find newly registered lookalike domains matching a domain or regex pattern.


| Target Type | API Route                       |
| ----------- | ------------------------------- |
| Domain      | `/api/lookalike/domain/:domain` |
| Regex       | `/api/lookalike/regex`          |


Does not support bulk queries. Options: `exclude`, `limit` (1–250), `lookback` (1–90 days), `depth` (`labels`, `subdomains`, `fqdns`), `similarity` (0–4, domain targets only).

Regex targets can be passed as a compiled `re.Pattern` or a slash-delimited string:

```python
client.lookalikes("/^(t-mobile|att|verizon)\.([a-z]{5,6})\.(icu|cc)$/")
```

---

### `client.certificates(indicator)`

Historic certificate transparency matches for a domain.


| Indicator Type | API Route                              |
| -------------- | -------------------------------------- |
| Domain         | `/api/axon/domain/certificates/:domain` |


Does not support bulk queries. Options: `wildcard`, `limit` (1–20000), `first_seen`, `last_seen`, `lookback`, `time_format` (`unix` or `iso`).

---

### `client.ptr_history(indicator)`

Historic PTR/hostname records for a domain or IP.


| Indicator Type | API Route                              |
| -------------- | -------------------------------------- |
| Domain         | `/api/axon/domain/dns/hostname/:domain` |
| IP             | `/api/axon/ip/dns/hostname/:ip`         |


Supports bulk queries. Options: `limit`, `first_seen`, `last_seen`, `lookback`, `annotate`, `time_format` (`unix` or `iso`), and `wildcard` for domain queries only.

---

### `client.start_scan(find)`

Start an enterprise live HTTP/S scan and return a `ScanJob`.


| API Route                      |
| ----------------------------- |
| `/api/axon/live/scan/start`   |


Does not support bulk queries. Options: `countries`, `refer`, `user_agent`.

---

### `client.get_scan(scan_id)`

Fetch live scan status and return a `ScanJob`.


| API Route                                  |
| ------------------------------------------ |
| `/api/axon/live/scan/results/:scan_id`     |


Does not support bulk queries.

---

### `client.scan(find)`

Convenience wrapper around `start_scan()`. By default it waits for completion and returns a `ResultSet` of `ScanRecord` objects. Pass `wait=False` to get a `ScanJob` immediately.


| API Route                                             |
| ----------------------------------------------------- |
| `/api/axon/live/scan/start` -> poll results -> crawl  |


Options: `countries`, `refer`, `user_agent`, `wait`, `poll_interval`, `timeout`.

---

### `client.registration_history(indicator)`

WHOIS/RDAP registration history for a domain or string.


| Indicator Type | API Route                                       |
| -------------- | ----------------------------------------------- |
| Domain         | `/api/axon/domain/registration/history/:domain` |
| String         | `/api/axon/string/registration/history2`        |


Supports bulk queries. Options: `limit`, `first_seen`, `last_seen`, `lookback`, `annotate`, `wildcard`, `include_fields`.

---

### `client.yara_matches(project_id, rule_id)`

Fetch YARA rule match results from a project. Auto-paginates all results by default.


| API Route                                              |
| ------------------------------------------------------ |
| `/api/project/:project_id/yara/rules/:rule_id/matches` |


Does not support bulk queries. Options: `page`, `page_size` (provide both for manual pagination), `include_html`, `first_seen`, `last_seen`.

---

### `client.fetch_content(indicator)`

Fetch stored HTML content by body hash.


| API Route                                |
| ---------------------------------------- |
| `/api/axon/hash/content/html/sha1/:hash` |


Supports bulk queries. Accepts a hash string, `Indicator`, or `HostResponse` object with a `body_hash`.

---

### `client.add_to_project(project_id, indicators)`

Add indicators to a Validin project.


| API Route                                 |
| ----------------------------------------- |
| `/api/project/:project_id/indicators/add` |


Accepts a single indicator or a list. Options: `note`, `tags`.

## Examples

Example scripts are in the `examples/` directory:

- `**lookalike_monitor.py**` — Regex lookalike search for telecom smishing domains, filter by registrar, add to project.
- `**dns_txt_record_monitor.py**` — Monitor a DNS TXT record wildcard query and add results to a project.
- `**bulk_indicator_enrichment.py**` — Bulk enrichment lookup across multiple indicators.
- `**ingest_yara_matches.py**` — Fetch all YARA rule matches for a project and print aggregated results.
- `**sample_host_responses.py**` — Bulk host response history with HTML content download.

## Error Handling

The SDK raises `ApiError` for HTTP failures and `ValidinError` as a base exception class:

```python
from validin import Client
from validin_sdk.errors import ApiError, ValidinError

client = Client()

try:
    results = client.dns_history("example.com")
except ApiError as e:
    print(f"HTTP {e.status_code}: {e.message}")
except ValidinError as e:
    print(f"SDK error: {e}")
```

## API Coverage

The SDK currently covers **24 of 76** API endpoints.

| Status | Method | Endpoint | Summary | SDK Method |
|:------:|:------:|----------|---------|------------|
| | | **Bulk** | | |
| ❌ | POST | `/api/axon/bulk/osint/context` | Bulk OSINT Context | |
| | | **Domain** | | |
| ✅ | GET | `/api/axon/domain/certificates/{domain}` | Domain Certificates | `certificates()` |
| ✅ | GET | `/api/axon/domain/crawl/history/{domain}` | Domain Crawl History | `host_responses()` |
| ✅ | GET | `/api/axon/domain/dns/extra/{domain}` | Domain DNS Extra | `extra_history()` |
| ✅ | GET | `/api/axon/domain/dns/history/{domain}` | Domain DNS History | `dns_history()` |
| ❌ | GET | `/api/axon/domain/dns/history/{domain}/A` | Domain DNS History - A | |
| ❌ | GET | `/api/axon/domain/dns/history/{domain}/AAAA` | Domain DNS History - AAAA | |
| ❌ | GET | `/api/axon/domain/dns/history/{domain}/NS` | Domain DNS History - NS | |
| ❌ | GET | `/api/axon/domain/dns/history/{domain}/NS_FOR` | Domain DNS History - NS_FOR | |
| ✅ | GET | `/api/axon/domain/dns/hostname/{domain}` | Domain DNS Hostname | `ptr_history()` |
| ❌ | GET | `/api/axon/domain/osint/history/{domain}` | Domain OSINT History | |
| ❌ | GET | `/api/axon/domain/pivots/{domain}` | Domain Pivots | |
| ❌ | GET | `/api/axon/domain/pivots/{domain}/{category}` | Domain Pivots - Category | |
| ✅ | GET | `/api/axon/domain/registration/history/{domain}` | Domain Registration History | `registration_history()` |
| ❌ | GET | `/api/axon/domain/registration/live/{domain}` | Domain Registration Live | |
| ✅ | GET | `/api/axon/domain/reputation/quick/{domain}` | Domain Reputation Quick | `enrich()` |
| ❌ | GET | `/api/axon/domain/subdomains/{domain}` | Domain Subdomains | |
| ✅ | GET | `/api/lookalike/domain/{domain}` | Lookalike Domain | `lookalikes()` |
| ✅ | GET | `/api/lookalike/regex` | Lookalike Regex | `lookalikes()` |
| ❌ | GET | `/api/v2/domain/combined/connections/{domain}` | V2 Domain Combined Connections | |
| | | **Hash** | | |
| ❌ | GET | `/api/axon/hash/content/certificate/sha1/{hash}` | Hash Content Certificate (SHA1) | |
| ❌ | GET | `/api/axon/hash/content/favicon/md5/{hash}` | Hash Content Favicon (MD5) | |
| ✅ | GET | `/api/axon/hash/content/html/sha1/{hash}` | Hash Content HTML (SHA1) | `fetch_content()` |
| ✅ | GET | `/api/axon/hash/crawl/history/{hash}` | Hash Crawl History | `host_responses()` |
| ❌ | GET | `/api/axon/hash/pivots/{hash}` | Hash Pivots | |
| ❌ | GET | `/api/axon/hash/pivots/{hash}/{category}` | Hash Pivots - Category | |
| | | **IP** | | |
| ✅ | GET | `/api/axon/ip/crawl/history/{ip}` | IP Crawl History | `host_responses()` |
| ❌ | GET | `/api/axon/ip/crawl/history/{ip}/{cidr}` | IP Crawl History - (CIDR) | |
| ✅ | GET | `/api/axon/ip/dns/extra/{ip}` | IP DNS Extra | `extra_history()` |
| ❌ | GET | `/api/axon/ip/dns/extra/{ip}/{cidr}` | IP DNS Extra - (CIDR) | |
| ✅ | GET | `/api/axon/ip/dns/history/{ip}` | IP DNS History | `dns_history()` |
| ❌ | GET | `/api/axon/ip/dns/history/{ip}/{cidr}` | IP DNS History - (CIDR) | |
| ✅ | GET | `/api/axon/ip/dns/hostname/{ip}` | IP DNS Hostname | `ptr_history()` |
| ❌ | GET | `/api/axon/ip/dns/hostname/{ip}/{cidr}` | IP DNS Hostname - (CIDR) | |
| ❌ | GET | `/api/axon/ip/osint/history/{ip}` | IP OSINT History | |
| ❌ | GET | `/api/axon/ip/osint/history/{ip}/{cidr}` | IP OSINT History - (CIDR) | |
| ❌ | GET | `/api/axon/ip/pivots/{ip}` | IP Pivots | |
| ❌ | GET | `/api/axon/ip/pivots/{ip}/{cidr}` | IP Pivots - (CIDR) | |
| ❌ | GET | `/api/axon/ip/pivots2/{ip}/{category}` | IP Pivots - Category | |
| ✅ | GET | `/api/axon/ip/reputation/quick/{ip}` | IP Reputation Quick | `enrich()` |
| | | **Projects** | | |
| ❌ | GET | `/api/project/list` | Project List | |
| ❌ | GET | `/api/project/{project_id}` | Project Details | |
| ❌ | GET | `/api/project/{project_id}/alerts/dates` | Project - List Dates that have Alerts | |
| ❌ | GET | `/api/project/{project_id}/alerts/latest` | Project - Latest Alerts | |
| ❌ | GET | `/api/project/{project_id}/alerts/{date}` | Project - Alerts for Date | |
| ❌ | GET | `/api/project/{project_id}/indicators` | Project - List Indicators | |
| ✅ | POST | `/api/project/{project_id}/indicators/add` | Project - Add Indicators | `add_to_project()` |
| ❌ | POST | `/api/project/{project_id}/indicators/add_note` | Project - Add Note to Indicators | |
| ❌ | POST | `/api/project/{project_id}/indicators/delete` | Project - Delete Indicators | |
| ❌ | POST | `/api/project/{project_id}/indicators/tags/add` | Project - Add Tags to Indicators | |
| ❌ | POST | `/api/project/{project_id}/indicators/tags/delete` | Project - Delete Tags from Indicators | |
| ❌ | GET | `/api/project/{project_id}/scans` | Project - List Scans | |
| ❌ | GET | `/api/project/{project_id}/yara/rules` | Project - List Yara Rules | |
| ✅ | GET | `/api/project/{project_id}/yara/rules/{rule_id}/matches` | Project - List Matches for YARA rule | `yara_matches()` |
| | | **Scans** | | |
| ✅ | GET | `/api/axon/live/scan/results/{scan_id}` | Live Scan Results | `get_scan()` |
| ✅ | GET | `/api/axon/live/scan/results/{scan_id}/crawl` | Live Scan Results - Crawl | `scan()`, `get_scan_crawl_results()` |
| ✅ | POST | `/api/axon/live/scan/start` | Live Scan Start | `start_scan()`, `scan()` |
| | | **String** | | |
| ✅ | GET | `/api/axon/string/dns/extra2` | String DNS Extra History | `extra_history()` |
| ✅ | GET | `/api/axon/string/dns/history` | String DNS History | `dns_history()` |
| ❌ | GET | `/api/axon/string/pivots2` | String Pivots | |
| ❌ | GET | `/api/axon/string/pivots2/{category}` | String Pivots - Category | |
| ✅ | GET | `/api/axon/string/registration/history2` | String Registration History | `registration_history()` |
| ❌ | GET | `/api/axon/string/registration/history2/{field}` | String Registration History - Category | |
| | | **Threats** | | |
| ❌ | GET | `/api/threat/group/{threat_key}/indicators` | Threat Group - List Indicators | |
| ❌ | GET | `/api/threat/group/{threat_key}/reports` | Threat Group - List Reports | |
| ❌ | GET | `/api/threat/group/{threat_key}/summary` | Threat Group - Summary | |
| ❌ | GET | `/api/threat/indicators/recent` | Recent Threat Indicators | |
| ❌ | GET | `/api/threat/names` | Threat Names | |
| ❌ | GET | `/api/threat/reports/recent` | Recent Threat Reports | |
| | | **Utilities** | | |
| ❌ | GET | `/api/axon/advanced/query` | Advanced Query | |
| ❌ | POST | `/api/axon/submissions/domains` | Submissions Domains | |
| ❌ | GET | `/api/paths` | Paths | |
| ❌ | GET | `/api/ping` | Ping | |
| ❌ | GET | `/api/profile/token` | Profile Token | |
| ❌ | GET | `/api/profile/usage` | Profile Usage | |
| ❌ | GET | `/api/profile/usage/daily` | Profile Usage Daily | |

## License

MIT
