dshield-parser 0.1.7__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dshield_parser-0.2.0/CHANGELOG.md +55 -0
- dshield_parser-0.2.0/MANIFEST.in +3 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/PKG-INFO +5 -1
- dshield_parser-0.2.0/README.md +84 -0
- dshield_parser-0.2.0/docs/IP-ENRICHMENT.md +404 -0
- dshield_parser-0.2.0/dshield_parser/__init__.py +3 -0
- dshield_parser-0.2.0/dshield_parser/_version.py +3 -0
- dshield_parser-0.2.0/dshield_parser/test/test_enrichment_api.py +303 -0
- dshield_parser-0.2.0/dshield_parser/test/test_enrichment_cache.py +112 -0
- dshield_parser-0.2.0/dshield_parser/test/test_enrichment_dns.py +321 -0
- dshield_parser-0.2.0/dshield_parser/test/test_enrichment_http.py +447 -0
- dshield_parser-0.2.0/dshield_parser/test/test_enrichment_local.py +381 -0
- dshield_parser-0.2.0/dshield_parser/test/test_enrichment_workflow.py +490 -0
- dshield_parser-0.2.0/dshield_parser/utils/_enrichment_cache.py +137 -0
- dshield_parser-0.2.0/dshield_parser/utils/_enrichment_dns.py +299 -0
- dshield_parser-0.2.0/dshield_parser/utils/_enrichment_http.py +675 -0
- dshield_parser-0.2.0/dshield_parser/utils/_enrichment_local.py +367 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/utils/enrichment.py +173 -159
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser.egg-info/PKG-INFO +5 -1
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser.egg-info/SOURCES.txt +22 -1
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser.egg-info/requires.txt +4 -0
- dshield_parser-0.2.0/examples/README.md +53 -0
- dshield_parser-0.2.0/examples/clear_research_ips.py +469 -0
- dshield_parser-0.2.0/examples/dshield_honeypot_comparisons_dbs_exclusions.py +142 -0
- dshield_parser-0.2.0/examples/enrich_summary_hashes.py +60 -0
- dshield_parser-0.2.0/examples/url-command-clustering.py +319 -0
- dshield_parser-0.2.0/requirements-enrichment.txt +3 -0
- dshield_parser-0.2.0/requirements.txt +9 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/setup.py +11 -1
- dshield_parser-0.1.7/README.md +0 -14
- dshield_parser-0.1.7/dshield_parser/__init__.py +0 -1
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/__main__.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/cowrie_processor/__init__.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/cowrie_processor/__main__.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/cowrie_processor/reports.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/firewall_processor/__init__.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/firewall_processor/__main__.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/firewall_processor/reports.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/test/__init__.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/test/test_json.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/utils/__init__.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/utils/__main__.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/utils/file_io.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/utils/graphs.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/utils/json.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/utils/ml.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/utils/sql.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/utils/ssh.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/web_processor/__init__.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/web_processor/__main__.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser/web_processor/reports.py +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser.egg-info/dependency_links.txt +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/dshield_parser.egg-info/top_level.txt +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/pyproject.toml +0 -0
- {dshield_parser-0.1.7 → dshield_parser-0.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 — 2026-11-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Local GeoLite2 ASN and GeoLite2/GeoIP2 Country/City lookup functions, with an
|
|
8
|
+
optional `geoip` installation extra and reusable reader support.
|
|
9
|
+
- Validated, reusable IPv4/IPv6 IPtoASN TSV/gzip indexes and binary-search lookups.
|
|
10
|
+
- Selective authoritative RDAP lookups with IANA bootstrap, bounded referrals,
|
|
11
|
+
returned contact extraction, and exact-query caching. The former
|
|
12
|
+
`get_whois_data` placeholder now delegates to RDAP.
|
|
13
|
+
- Thread-safe SQLite enrichment cache with independent provider namespaces,
|
|
14
|
+
record expiration, persistent cooldowns, and context-manager cleanup.
|
|
15
|
+
- Structured PTR results and bounded DNS batch iteration, including authoritative
|
|
16
|
+
negative TTLs, short transient-failure cooldowns, and resolver-specific cache keys.
|
|
17
|
+
- Atomic, size-limited ISC `daily_sources` and `threatintel` feed downloads.
|
|
18
|
+
- Offline enrichment regression tests, Linux/Windows CI, usage documentation,
|
|
19
|
+
and release tag/version verification before PyPI publishing.
|
|
20
|
+
|
|
21
|
+
### Corrected
|
|
22
|
+
|
|
23
|
+
- ISC IP lookups now use explicit timeouts and bounded retries. Rate limits raise
|
|
24
|
+
`EnrichmentRateLimitError` with retry timing instead of sleeping indefinitely.
|
|
25
|
+
Missing optional fields and malformed 429 responses are handled safely.
|
|
26
|
+
- Related ISC/scanner HTTP helpers no longer retry forever; contact User-Agent
|
|
27
|
+
interpolation is corrected for the ISC intel helper, and user-agent lookup
|
|
28
|
+
paths correctly encode spaces and reserved URL characters.
|
|
29
|
+
- Legacy `get_reverse_dns` retains its string result while using bounded DNS
|
|
30
|
+
resolution and correct exception handling.
|
|
31
|
+
- Example PTR processing covers all three log sources and uses public result
|
|
32
|
+
interfaces. Completed DNS results are persistently cached.
|
|
33
|
+
- Example IP metadata is unique by IP, with validated joins. Multiple scanner
|
|
34
|
+
labels are aggregated before joining, preserving raw label tables.
|
|
35
|
+
- Onyphe records are labeled `Onyphe`; actual LeakIX records remain `LeakIX`.
|
|
36
|
+
- Example credentials are read from environment variables; absent optional
|
|
37
|
+
Shodan configuration skips its lookups safely.
|
|
38
|
+
- Optional scanner/researcher feed failures no longer stop local ASN enrichment.
|
|
39
|
+
Source availability and retry timing are recorded separately in the
|
|
40
|
+
`enrichment_sources` table, preserving unknown labels and UA evidence.
|
|
41
|
+
|
|
42
|
+
### Migration
|
|
43
|
+
|
|
44
|
+
- New ASN providers are opt-in through function calls or example configuration.
|
|
45
|
+
MaxMind database files and IPtoASN downloads are supplied by the caller.
|
|
46
|
+
- Catch `EnrichmentRateLimitError` and defer work until `retry_at`. Live provider
|
|
47
|
+
failures are visible instead of causing an unbounded wait.
|
|
48
|
+
- The updated example uses the new SQLite cache and does not automatically import
|
|
49
|
+
`ip_lookups.txt`, whose entries lack reliable fetch/expiration timestamps.
|
|
50
|
+
- Geolocation, registry country, ASN country, lookup range, and ASN size remain
|
|
51
|
+
separate concepts. Fields unavailable from a provider are left empty.
|
|
52
|
+
- The package version is maintained in `dshield_parser/_version.py` and exposed as
|
|
53
|
+
`dshield_parser.__version__`; `setup.py` reads that same value.
|
|
54
|
+
|
|
55
|
+
See [IP enrichment and migration examples](docs/IP-ENRICHMENT.md).
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dshield_parser
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: This program has utilities to download, parse and enrich DShield honeypot data
|
|
5
5
|
Home-page: https://github.com/jslagrew/dshield-parser
|
|
6
6
|
Author: Jesse La Grew
|
|
7
7
|
Author-email: jlagrew.github@proton.me
|
|
8
8
|
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: dnspython>=2.6.1
|
|
9
10
|
Requires-Dist: matplotlib>=3.6.3
|
|
10
11
|
Requires-Dist: numpy>=1.26.0
|
|
11
12
|
Requires-Dist: pandas>=2.0.3
|
|
@@ -13,9 +14,12 @@ Requires-Dist: requests>=2.32.3
|
|
|
13
14
|
Requires-Dist: scikit-learn>=1.2.2
|
|
14
15
|
Requires-Dist: shodan>=1.28.0
|
|
15
16
|
Requires-Dist: SQLAlchemy>=2.0.31
|
|
17
|
+
Provides-Extra: geoip
|
|
18
|
+
Requires-Dist: geoip2>=4.8.0; extra == "geoip"
|
|
16
19
|
Dynamic: author
|
|
17
20
|
Dynamic: author-email
|
|
18
21
|
Dynamic: home-page
|
|
22
|
+
Dynamic: provides-extra
|
|
19
23
|
Dynamic: requires-dist
|
|
20
24
|
Dynamic: requires-python
|
|
21
25
|
Dynamic: summary
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# dshield-parser
|
|
2
|
+
|
|
3
|
+
Python utilities for reading and summarizing DShield Cowrie, web honeypot, and firewall logs. The package exposes importable readers and report helpers. The scripts in [`examples/`](examples/README.md) show a research workflow that compares multiple honeypots, writes SQLite extracts, enriches selected records, and clusters URLs or commands.
|
|
4
|
+
|
|
5
|
+
## New in 0.2.0: local and cached IP enrichment
|
|
6
|
+
|
|
7
|
+
`dshield_parser.utils.enrichment` now provides local GeoLite2 ASN/Country and
|
|
8
|
+
IPtoASN lookups, selective RDAP, persistent TTL-aware reverse DNS, bounded ISC
|
|
9
|
+
requests, and atomic ISC static-feed downloads. The example workflow fixes
|
|
10
|
+
Onyphe labeling and validates enrichment joins so repeated metadata cannot
|
|
11
|
+
multiply event counts.
|
|
12
|
+
|
|
13
|
+
See the [complete enrichment guide](docs/IP-ENRICHMENT.md) for installation,
|
|
14
|
+
database downloads, function examples, result fields, cache behavior, and
|
|
15
|
+
migration from 0.1.x. The [changelog](CHANGELOG.md) lists release changes.
|
|
16
|
+
|
|
17
|
+
From a checkout, install the optional MaxMind reader with
|
|
18
|
+
`python -m pip install -e ".[geoip]"`. Database files remain caller-managed.
|
|
19
|
+
|
|
20
|
+
## Installing
|
|
21
|
+
|
|
22
|
+
Python 3.12 or later is required by this repository's `setup.py`. A virtual environment is recommended:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
python3 -m venv .venv
|
|
26
|
+
source .venv/bin/activate
|
|
27
|
+
python -m pip install --upgrade pip
|
|
28
|
+
python -m pip install dshield-parser
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
On Windows, activate with `.venv\Scripts\activate` instead. To work from a checkout instead of the published package, run `python -m pip install -e .` in the repository root. The package declares dependencies for data frames, plotting, SQLite access, and optional enrichment services; the pinned development versions are in `requirements.txt`.
|
|
32
|
+
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
| Log family | Main helper module | Typical fields |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Cowrie | `dshield_parser.cowrie_processor.reports` | Source IP, username, password, command input, file hash |
|
|
38
|
+
| Web honeypot | `dshield_parser.web_processor.reports` | Source IP, URL, headers, method, user agent |
|
|
39
|
+
| Firewall | `dshield_parser.firewall_processor.reports` | Source and destination IP, port, protocol, flags |
|
|
40
|
+
|
|
41
|
+
`dshield_parser.utils.file_io` reads JSON files and JSON lines. `dshield_parser.utils.json` extracts fields and builds summaries. `dshield_parser.utils.sql`, `graphs`, `ml`, and `enrichment` support the example workflows. The repository does not define a supported command-line entry point; use the importable functions or adapt an example script.
|
|
42
|
+
|
|
43
|
+
```mermaid
|
|
44
|
+
flowchart LR
|
|
45
|
+
A[Cowrie JSON lines] --> D[File readers]
|
|
46
|
+
B[Web honeypot logs] --> D
|
|
47
|
+
C[Firewall logs] --> D
|
|
48
|
+
D --> E[Field extraction and summaries]
|
|
49
|
+
E --> F[Data frames or SQLite extracts]
|
|
50
|
+
F --> G[Optional enrichment and clustering]
|
|
51
|
+
G --> H[Comparisons and plots]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Using the library
|
|
55
|
+
|
|
56
|
+
Count readable Cowrie JSON events without loading the whole file into a list:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from dshield_parser.utils.file_io import read_file_json_generator
|
|
60
|
+
|
|
61
|
+
count = sum(1 for _ in read_file_json_generator("cowrie.json.2024-05-01"))
|
|
62
|
+
print(f"Parsed events: {count}")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Summarize source IPs from dated Cowrie log files:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from pathlib import Path
|
|
69
|
+
from dshield_parser.cowrie_processor import reports
|
|
70
|
+
|
|
71
|
+
logs = [Path("cowrie.json.2024-05-01"), Path("cowrie.json.2024-05-02")]
|
|
72
|
+
window = {"start_date": "2024-05-01", "end_date": "2024-05-02"}
|
|
73
|
+
counts = reports.get_summary_src_ip(logs, datespan=window)
|
|
74
|
+
print(counts["src_ip"])
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This summary helper uses dates found in Cowrie **filenames** to select files. Keep dated filenames and check the actual events when you need precise event-time boundaries. It accepts an `exclusions` dictionary for exact field-value exclusions, for example `exclusions={"src_ip": "192.0.2.10"}`. Use the corresponding web or firewall report module for those log families; their field names differ.
|
|
78
|
+
|
|
79
|
+
For a multi-honeypot comparison, see [the example sequence](examples/README.md). Its scripts contain sample paths, dates, and sensor names that must be adapted to your own input layout. They are examples of a research process, not a single automatic CLI pipeline.
|
|
80
|
+
|
|
81
|
+
## Data handling
|
|
82
|
+
|
|
83
|
+
The package reads local log paths supplied by your code. Examples may create SQLite files, text, plots, or enrichment results in their working directory. Review the script's input and output paths before running it on production data. Keep logs, databases, API keys, and generated output outside the source repository. Enrichment functions that call external services require their own service configuration and may have rate or availability limits.
|
|
84
|
+
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# IP enrichment in 0.2.0
|
|
2
|
+
|
|
3
|
+
Routine ASN attribution can now run against a local database. DNS, ISC and
|
|
4
|
+
registration detail can be collected separately with explicit deadlines,
|
|
5
|
+
resumable caching, and visible failure states. All public entry points remain in
|
|
6
|
+
`dshield_parser.utils.enrichment`; the private implementation modules separate
|
|
7
|
+
the local, DNS, HTTP, and SQLite responsibilities.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
From this checkout:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
python -m pip install -e ".[geoip]"
|
|
15
|
+
python -c "import dshield_parser; print(dshield_parser.__version__)"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The version is `0.2.0`. The `geoip` extra installs the optional MaxMind Python
|
|
19
|
+
reader. Use `python -m pip install -e .` if only using IPtoASN, DNS, RDAP, or ISC.
|
|
20
|
+
Dnspython is a core dependency because the legacy DNS function now uses it.
|
|
21
|
+
After 0.2.0 is published, the equivalent package installation is
|
|
22
|
+
`python -m pip install "dshield-parser[geoip]==0.2.0"`.
|
|
23
|
+
|
|
24
|
+
Creating the changes in GitHub does not publish the package to PyPI. The existing
|
|
25
|
+
release workflow still requires a matching version tag; its test and version
|
|
26
|
+
checks now run before publication. The single version source is
|
|
27
|
+
`dshield_parser/_version.py`, read by both the package and `setup.py`.
|
|
28
|
+
|
|
29
|
+
## Public API
|
|
30
|
+
|
|
31
|
+
| Entry point | Purpose | External lookup |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `get_geoip_asn(ip, database_path=None, *, reader=None)` | ASN and associated organization from a local GeoLite2 ASN MMDB | None |
|
|
34
|
+
| `get_geoip_country(ip, database_path=None, *, reader=None)` | Separate geolocation and registered-country fields from a local Country/City MMDB | None |
|
|
35
|
+
| `load_iptoasn_database(path)` | Validate and index a plain/gzip IPtoASN TSV once | None |
|
|
36
|
+
| `get_iptoasn_data(ip, database)` | Binary-search the reusable IPtoASN index | None |
|
|
37
|
+
| `get_rdap_data(ip, *, email, ...)` | Selective registration lookup via IANA bootstrap and authoritative RDAP | HTTPS on an uncached lookup |
|
|
38
|
+
| `get_whois_data(ip, email, **kwargs)` | Alias for `get_rdap_data`; implements the former placeholder | Same as RDAP |
|
|
39
|
+
| `get_reverse_dns_info(ip, ...)` | PTR names, status, and expiration | DNS on a cache miss |
|
|
40
|
+
| `iter_reverse_dns(ips, *, max_workers=16, ...)` | Stream completed, deduplicated PTR results with bounded work | DNS on cache misses |
|
|
41
|
+
| `get_reverse_dns(ip, **kwargs)` | Compatibility wrapper returning one hostname or `""` | Same as DNS |
|
|
42
|
+
| `isc_ipinfo(ip, email, ...)` | Legacy ISC field dictionary with bounded requests and an optional exact-IP cache | HTTPS on a cache miss |
|
|
43
|
+
| `download_isc_feed(feed, destination, *, email, ...)` | Atomically download `daily_sources` or `threatintel` | HTTPS when a refresh is needed |
|
|
44
|
+
| `EnrichmentCache(path)` | Explicit, thread-safe SQLite cache shared across script runs | None |
|
|
45
|
+
|
|
46
|
+
New structured results include `ip`, `source`, `status`, `fetched_at`, and
|
|
47
|
+
`cache_hit`. DNS and RDAP add `expires_at`; local datasets instead report dataset
|
|
48
|
+
provenance. Times in these fields are **UTC Unix seconds**, not the time of the
|
|
49
|
+
original honeypot event. `isc_ipinfo` retains its `{ip: fields}` shape.
|
|
50
|
+
|
|
51
|
+
```mermaid
|
|
52
|
+
flowchart TD
|
|
53
|
+
A["Unique IPs from all logs"] --> B["Local ASN database"]
|
|
54
|
+
A --> C["PTR cache and bounded DNS workers"]
|
|
55
|
+
A -->|"Selected detail lookups"| D["RDAP or ISC with provider cooldown"]
|
|
56
|
+
E["Downloaded ISC feeds"] --> F["Enrichment records with source and time"]
|
|
57
|
+
B --> F
|
|
58
|
+
C --> F
|
|
59
|
+
D --> F
|
|
60
|
+
F --> G["Validated joins into analysis tables"]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Local ASN and country databases
|
|
64
|
+
|
|
65
|
+
### GeoLite2 ASN
|
|
66
|
+
|
|
67
|
+
Download and unpack the MMDB using your MaxMind account, following the official
|
|
68
|
+
[GeoLite integration guide](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data/).
|
|
69
|
+
For regular updates, MaxMind recommends
|
|
70
|
+
[GeoIP Update](https://dev.maxmind.com/geoip/updating-databases/).
|
|
71
|
+
Keep download credentials outside the repository and follow the provider's
|
|
72
|
+
current attribution, update, and retention requirements.
|
|
73
|
+
|
|
74
|
+
Reuse one reader for a batch:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import geoip2.database
|
|
78
|
+
from dshield_parser.utils import enrichment
|
|
79
|
+
|
|
80
|
+
ips = ["1.1.1.1", "8.8.8.8"]
|
|
81
|
+
with geoip2.database.Reader("GeoLite2-ASN.mmdb") as reader:
|
|
82
|
+
records = [enrichment.get_geoip_asn(ip, reader=reader) for ip in ips]
|
|
83
|
+
|
|
84
|
+
for record in records:
|
|
85
|
+
print(record["ip"], record["asn"], record["asn_organization"], record["status"])
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For a single lookup, supply `database_path="GeoLite2-ASN.mmdb"` instead. Exactly
|
|
89
|
+
one of `reader` and `database_path` is required. A caller-owned reader is never
|
|
90
|
+
closed by the helper; a reader created for a single lookup is closed afterwards.
|
|
91
|
+
The supported ASN database is GeoLite2-ASN. Paid ISP databases are not handled by
|
|
92
|
+
this ASN method.
|
|
93
|
+
|
|
94
|
+
ASN result fields are `asn`, `asn_organization`, and `lookup_network`, plus
|
|
95
|
+
`dataset_build_epoch`, `dataset_type`, and `dataset_path` where available. A
|
|
96
|
+
caller-supplied reader has no known path, so `dataset_path` is `None`. An address
|
|
97
|
+
absent from the database returns `status="not_found"`. A corrupt database,
|
|
98
|
+
incompatible database, or invalid address raises an error instead of being
|
|
99
|
+
misreported as absent.
|
|
100
|
+
|
|
101
|
+
### Country or City MMDB
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from dshield_parser.utils.enrichment import get_geoip_country
|
|
105
|
+
|
|
106
|
+
record = get_geoip_country("1.1.1.1", "GeoLite2-Country.mmdb")
|
|
107
|
+
print(record["geo_country"], record["registered_country"])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
GeoLite2/GeoIP2 Country and City databases are supported. `geo_country` is the
|
|
111
|
+
source's IP geolocation country; `registered_country` is a distinct registration
|
|
112
|
+
attribute. Neither is automatically the ISC `ascountry` field or evidence of an
|
|
113
|
+
attacker's physical location.
|
|
114
|
+
|
|
115
|
+
### IPtoASN
|
|
116
|
+
|
|
117
|
+
Download an ASN dataset from [IPtoASN](https://www.iptoasn.com/), keep it outside
|
|
118
|
+
the repository, then load it once:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from dshield_parser.utils import enrichment
|
|
122
|
+
|
|
123
|
+
database = enrichment.load_iptoasn_database("ip2asn-combined.tsv.gz")
|
|
124
|
+
records = [
|
|
125
|
+
enrichment.get_iptoasn_data(ip, database)
|
|
126
|
+
for ip in ["1.1.1.1", "8.8.8.8", "2606:4700:4700::1111"]
|
|
127
|
+
]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The five TSV columns are `range_start`, `range_end`, `AS_number`, `country_code`,
|
|
131
|
+
and `AS_description`. Plain and gzip files, integer IPv4 endpoints, IPv6, and
|
|
132
|
+
combined files are supported. The loader rejects malformed, overlapping,
|
|
133
|
+
unsorted, empty, and truncated datasets. Lookups use binary search; they do not
|
|
134
|
+
reopen or scan the file for each IP. Reload the index after replacing the file.
|
|
135
|
+
|
|
136
|
+
Results include `asn`, `asn_organization`, `source_country`, `range_start`, and
|
|
137
|
+
`range_end`. Inclusive source ranges can span multiple CIDRs, so no BGP prefix is
|
|
138
|
+
invented. AS0/unallocated ranges and missing ranges return `not_found`.
|
|
139
|
+
`dataset_mtime`, `dataset_mtime_ns`, and `dataset_size_bytes` identify the local
|
|
140
|
+
file at load time; its modification time is not claimed to be the upstream build
|
|
141
|
+
date. The IPtoASN public API is discontinued; these functions use downloads.
|
|
142
|
+
|
|
143
|
+
### Field meanings when migrating
|
|
144
|
+
|
|
145
|
+
| Old ISC field | Local replacement |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `as` | `asn`, with provider recorded |
|
|
148
|
+
| `asname` | `asn_organization`; names can differ across providers |
|
|
149
|
+
| `network` | `lookup_network` for MaxMind, or explicit start/end ranges for IPtoASN |
|
|
150
|
+
| `ascountry` | No silent substitution; retain separate country fields |
|
|
151
|
+
| `assize` | No direct replacement in these basic lookups |
|
|
152
|
+
| `updated` | Preserve source meaning; use separate fetch/build timestamps |
|
|
153
|
+
|
|
154
|
+
A matched lookup prefix, routing announcement, and registered address range are
|
|
155
|
+
different concepts. The size of one prefix is not the size of an entire ASN.
|
|
156
|
+
Local ASN records do not contain complete WHOIS contacts or ISC activity counts.
|
|
157
|
+
|
|
158
|
+
## Persistent cache and reverse DNS
|
|
159
|
+
|
|
160
|
+
Use one cache file at a stable location across runs. The path is explicit for
|
|
161
|
+
library calls. The cache stores JSON records with unique `(namespace, key)` pairs
|
|
162
|
+
and per-record expiration. It may be shared by threads; independent instances can
|
|
163
|
+
open the same local SQLite file. Close it after its workers have finished.
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from pathlib import Path
|
|
167
|
+
from dshield_parser.utils import enrichment
|
|
168
|
+
|
|
169
|
+
cache_file = Path.home() / "dshield-parser-cache" / "enrichment.sqlite"
|
|
170
|
+
ips = ["1.1.1.1", "8.8.8.8", "1.1.1.1"]
|
|
171
|
+
with enrichment.EnrichmentCache(cache_file) as cache:
|
|
172
|
+
for record in enrichment.iter_reverse_dns(
|
|
173
|
+
ips, cache=cache, max_workers=16, timeout=1.0, lifetime=3.0,
|
|
174
|
+
):
|
|
175
|
+
print(record["ip"], record["reverse_dns"], record["status"])
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The generator yields **completion order**. Join using `record["ip"]`, never list
|
|
179
|
+
position. Inputs are canonicalized and deduplicated, with at most `max_workers`
|
|
180
|
+
lookups in flight. The deduplication set still grows with distinct IPs. Each result
|
|
181
|
+
is saved before it is yielded, so an interrupted run can reuse completed fresh
|
|
182
|
+
records. Closing the generator cancels unstarted work and waits for active DNS
|
|
183
|
+
lookups to complete within their resolver lifetime.
|
|
184
|
+
|
|
185
|
+
| DNS status | Meaning | Cache behavior |
|
|
186
|
+
| --- | --- | --- |
|
|
187
|
+
| `ok` | At least one PTR was returned | Uses the answer's original expiration |
|
|
188
|
+
| `nxdomain` | DNS returned name nonexistence | Uses the negative SOA lifetime, including alias limits |
|
|
189
|
+
| `no_ptr` | A DNS answer contains no PTR | Uses its negative lifetime when supplied |
|
|
190
|
+
| `timeout` | Total DNS lookup lifetime expired | Separate short failure cooldown |
|
|
191
|
+
| `resolver_error` | Nameservers/transport could not resolve the question | Separate short failure cooldown |
|
|
192
|
+
|
|
193
|
+
`ptr_names` retains all returned names. `reverse_dns` provides the first name or
|
|
194
|
+
an empty string. A negative response without a defensible SOA lifetime is not
|
|
195
|
+
persisted as fresh. A timeout is never treated as proof of no PTR. Temporary
|
|
196
|
+
failures default to a 60-second cooldown; set `failure_ttl=0` to disable that.
|
|
197
|
+
|
|
198
|
+
The default resolver uses the host's configured DNS service and an explicit
|
|
199
|
+
dnspython in-memory cache. `timeout` is the per-server wait; `lifetime` bounds the
|
|
200
|
+
DNS question. A supplied dnspython resolver is copied before changing timing
|
|
201
|
+
settings, preserving the caller's configuration. Configure a resolver before
|
|
202
|
+
sharing it between workers. A custom resolver must honor the supplied lifetime;
|
|
203
|
+
it can provide `enrichment_cache_identity` for a stable cache view. Normal
|
|
204
|
+
resolver cache keys include resolver/transport identity to separate DNS views.
|
|
205
|
+
|
|
206
|
+
For compatibility, `get_reverse_dns(ip, cache=cache)` still returns a string.
|
|
207
|
+
Use the structured function when the distinction between absence and failure
|
|
208
|
+
matters. Putting a timeout on a thread Future alone does not stop the underlying
|
|
209
|
+
OS resolver; this implementation places the deadline inside the DNS resolver.
|
|
210
|
+
|
|
211
|
+
## Selective RDAP registration detail
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
import os
|
|
215
|
+
import requests
|
|
216
|
+
from dshield_parser.utils import enrichment
|
|
217
|
+
|
|
218
|
+
with enrichment.EnrichmentCache("enrichment.sqlite") as cache, requests.Session() as session:
|
|
219
|
+
try:
|
|
220
|
+
record = enrichment.get_rdap_data(
|
|
221
|
+
"1.1.1.1", email=os.environ["DSHIELD_CONTACT_EMAIL"],
|
|
222
|
+
cache=cache, session=session,
|
|
223
|
+
)
|
|
224
|
+
except enrichment.EnrichmentRateLimitError as exc:
|
|
225
|
+
print("Save this lookup as pending until", exc.retry_at)
|
|
226
|
+
except enrichment.EnrichmentError as exc:
|
|
227
|
+
print("Provider lookup failed:", exc)
|
|
228
|
+
else:
|
|
229
|
+
print(record["status"], record["network"], record["abuse_contacts"])
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The client caches IANA IPv4/IPv6 bootstrap data, selects the most-specific matching
|
|
233
|
+
service, and retrieves the authoritative IP network record over HTTPS. You can
|
|
234
|
+
instead pass `bootstrap` as an IANA-style dictionary, a prefix-to-HTTPS-base map,
|
|
235
|
+
or a path to either JSON form. HTTPS redirects and exact-IP RDAP referrals are
|
|
236
|
+
bounded. The returned range must contain the requested IP.
|
|
237
|
+
|
|
238
|
+
Results include `network` (handle, name, start/end addresses, country and related
|
|
239
|
+
registration fields), `entities`, `abuse_contacts`, and `raw`. Contacts are
|
|
240
|
+
extracted only from entities already in the response. There is no recursive
|
|
241
|
+
entity retrieval, extra ASN query, or port-43 WHOIS request. Data omitted by the
|
|
242
|
+
registry remains absent. A non-global address returns `not_global`; HTTP 404
|
|
243
|
+
becomes a short-lived `not_found` result. Other failures raise `EnrichmentError`.
|
|
244
|
+
|
|
245
|
+
The default successful RDAP cache lifetime is seven days and is configurable with
|
|
246
|
+
`cache_ttl`. Cache identity includes the exact query URL; records are not applied
|
|
247
|
+
to an entire parent subnet or ASN. Registration data remains separate from ASN,
|
|
248
|
+
geolocation, per-IP threat observations, and PTR data.
|
|
249
|
+
|
|
250
|
+
## ISC requests and static feeds
|
|
251
|
+
|
|
252
|
+
The legacy call remains usable:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from dshield_parser.utils import enrichment
|
|
256
|
+
|
|
257
|
+
with enrichment.EnrichmentCache("enrichment.sqlite") as cache:
|
|
258
|
+
data = enrichment.isc_ipinfo("1.1.1.1", "analyst@example.org", cache=cache)
|
|
259
|
+
print(data["1.1.1.1"]["asname"])
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Its default exact-IP cache lifetime is 24 hours. The inner field dictionary also
|
|
263
|
+
includes `fetched_at`, `expires_at`, and `cache_hit`, so callers can distinguish
|
|
264
|
+
the original observation time from cache reuse. Optional missing XML fields are
|
|
265
|
+
`None`; HTML or malformed success responses are errors. Contact information is
|
|
266
|
+
required for HTTP User-Agent identification. Use the rate-limit handling shown
|
|
267
|
+
above around this call in long-running jobs.
|
|
268
|
+
|
|
269
|
+
HTTP functions default to `(3.05, 10)` seconds for connect/read timeouts and three
|
|
270
|
+
attempts. These are connection/read-inactivity limits, **not a total wall-clock
|
|
271
|
+
deadline for a complete download**. Temporary transport errors and selected 5xx
|
|
272
|
+
responses receive short capped backoff. Permanent HTTP failures do not retry
|
|
273
|
+
forever. HTTP 429, and 503 with Retry-After, immediately raise
|
|
274
|
+
`EnrichmentRateLimitError`, exposing `origin`, `retry_after`, and `retry_at`.
|
|
275
|
+
Retry-After seconds and HTTP dates are supported; ISC's HTML delay is a fallback,
|
|
276
|
+
then a conservative 300-second fallback. No worker sleeps for that five-minute
|
|
277
|
+
interval. Known origin-wide cooldowns prevent subsequent requests, including
|
|
278
|
+
lookups for different IPs. Passing the persistent cache retains cooldowns across
|
|
279
|
+
processes. Cached successful data can still be used while a provider is cooling
|
|
280
|
+
down. Respect `retry_at`; resume pending work later rather than increasing load.
|
|
281
|
+
|
|
282
|
+
Static ISC feeds can be downloaded separately:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
from dshield_parser.utils import enrichment
|
|
286
|
+
|
|
287
|
+
with enrichment.EnrichmentCache("enrichment.sqlite") as cache:
|
|
288
|
+
result = enrichment.download_isc_feed(
|
|
289
|
+
"threatintel", "feeds/threatintel.txt",
|
|
290
|
+
email="analyst@example.org", cache=cache,
|
|
291
|
+
)
|
|
292
|
+
print(result["path"], result["status"])
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The other supported feed name is `daily_sources`. The helper keeps the original
|
|
296
|
+
feed text; it does not guess a schema or claim that a daily feed replaces every
|
|
297
|
+
field of `/api/ip`. Ingest only fields and time windows appropriate for your
|
|
298
|
+
analysis. Absence from a feed does not mean benign.
|
|
299
|
+
|
|
300
|
+
Reuse a stable destination. An unchanged file downloaded within the last hour is
|
|
301
|
+
reused for that feed/path; cache metadata makes that behavior survive restarts.
|
|
302
|
+
The default maximum download size is 256 MiB, configurable with `max_bytes`.
|
|
303
|
+
Downloads stream to a temporary file in the destination directory and replace the
|
|
304
|
+
destination only after a successful nonempty download. HTML/error responses,
|
|
305
|
+
oversized downloads, and interrupted transfers leave the last good file intact.
|
|
306
|
+
Refresh jobs should follow [ISC's feed guidance](https://isc.sans.edu/feeds_doc.html)
|
|
307
|
+
and avoid fetching more frequently than hourly.
|
|
308
|
+
|
|
309
|
+
## Updating the example workflow
|
|
310
|
+
|
|
311
|
+
`examples/clear_research_ips.py` uses a shared cache path and the bounded DNS batch
|
|
312
|
+
helper. It supports these environment settings (set them in the same shell that
|
|
313
|
+
starts the script):
|
|
314
|
+
|
|
315
|
+
| Variable | Purpose |
|
|
316
|
+
| --- | --- |
|
|
317
|
+
| `DSHIELD_CONTACT_EMAIL` | Contact address for ISC and other HTTP requests |
|
|
318
|
+
| `SHODAN_API_KEY` | Optional Shodan key; an unset key skips Shodan-only lookups |
|
|
319
|
+
| `DSHIELD_ENRICHMENT_CACHE` | Cache file; defaults to `~/dshield-parser-cache/enrichment.sqlite` |
|
|
320
|
+
| `DSHIELD_ASN_MMDB` | GeoLite2 ASN MMDB path for local ASN attribution |
|
|
321
|
+
| `DSHIELD_IPTOASN_FILE` | IPtoASN TSV/gzip path as an alternative local provider |
|
|
322
|
+
|
|
323
|
+
Set at most one local ASN path. Without either, the example continues to use ISC
|
|
324
|
+
for ASN/network context, with bounded requests and the new exact-IP cache. The
|
|
325
|
+
example still uses other external scanner/researcher feeds even when ASN lookups
|
|
326
|
+
are local; the standalone local functions themselves perform no network calls.
|
|
327
|
+
Optional feed failures do not stop local ASN processing. The separate
|
|
328
|
+
`enrichment_sources` output table records `source`, `status`, `checked_at`,
|
|
329
|
+
`retry_at`, and a sanitized `error` category. Its statuses are `ok`, `deferred`,
|
|
330
|
+
`error`, `unavailable`, or `skipped`. `checked_at` is the source availability
|
|
331
|
+
assessment time, not the source record's observation time. Check this table
|
|
332
|
+
before interpreting missing labels; missing evidence never proves an IP benign.
|
|
333
|
+
|
|
334
|
+
PowerShell configuration example:
|
|
335
|
+
|
|
336
|
+
```powershell
|
|
337
|
+
$env:DSHIELD_CONTACT_EMAIL = "analyst@example.org"
|
|
338
|
+
$env:DSHIELD_ASN_MMDB = "C:\Enrichment\GeoLite2-ASN.mmdb"
|
|
339
|
+
$env:DSHIELD_ENRICHMENT_CACHE = "C:\Enrichment\enrichment.sqlite"
|
|
340
|
+
python examples/clear_research_ips.py
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Edit the example's input database and date settings for your run. Add the Shodan
|
|
344
|
+
environment variable only if you want those lookups. Keep actual keys outside
|
|
345
|
+
tracked files.
|
|
346
|
+
|
|
347
|
+
The example reports `enrichment_status` and `enrichment_retry_at` for pending
|
|
348
|
+
ISC data. It does not pretend a partially enriched output is complete. Rerun
|
|
349
|
+
after the provider cooldown to resume using fresh cached successes. Local
|
|
350
|
+
results retain `enrichment_source`, a local lookup timestamp, and separate
|
|
351
|
+
IPtoASN range/country fields. `ascountry`, `assize`, and ISC `updated` are left
|
|
352
|
+
empty when the local provider cannot supply their original meanings.
|
|
353
|
+
|
|
354
|
+
The old `ip_lookups.txt` is no longer automatically consumed by this example.
|
|
355
|
+
Its entries have no reliable local fetch/expiration time and previously reused
|
|
356
|
+
arbitrary parent networks. Keep it as a historical artifact if needed. A first
|
|
357
|
+
run with the new cache needs fresh ISC results or a configured local dataset;
|
|
358
|
+
subsequent runs reuse fresh cache entries. This change prevents cached subnet
|
|
359
|
+
copies from multiplying event rows.
|
|
360
|
+
|
|
361
|
+
Scanner/research labels are aggregated for one-row-per-IP joins while raw label
|
|
362
|
+
records remain in their separate output tables. Joins validate many-to-one
|
|
363
|
+
cardinality. Onyphe and LeakIX now have separate correct labels. PTR results cover
|
|
364
|
+
the union of web, Cowrie, and firewall IPs. Logging and provider status distinguish
|
|
365
|
+
lookup failures from confirmed absence.
|
|
366
|
+
|
|
367
|
+
## Testing and release checks
|
|
368
|
+
|
|
369
|
+
```sh
|
|
370
|
+
python -m pip install ".[geoip]" build setuptools wheel
|
|
371
|
+
python -m unittest discover -s dshield_parser/test -p "test_enrichment*.py" -v
|
|
372
|
+
python -m build
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Tests use synthetic addresses, fake provider responses, temporary SQLite files,
|
|
376
|
+
and DNS records. They do not send real DNS/ISC/RDAP requests. Coverage includes
|
|
377
|
+
range boundaries and IPv6, optional reader behavior, cache persistence/expiry,
|
|
378
|
+
negative TTLs, bounded DNS work, retry/cooldown behavior, response validation,
|
|
379
|
+
atomic feed updates, public API compatibility, and workflow merge cardinality.
|
|
380
|
+
|
|
381
|
+
The older `test_json.py` suite references honeypot fixture files absent from this
|
|
382
|
+
repository; it is not represented as passing by these enrichment checks. CI runs
|
|
383
|
+
the self-contained enrichment suite and builds the package on Linux and Windows.
|
|
384
|
+
The release workflow verifies that the pushed tag matches the package version
|
|
385
|
+
and runs the same enrichment suite before publishing.
|
|
386
|
+
|
|
387
|
+
## Source references and remaining scope
|
|
388
|
+
|
|
389
|
+
- [MaxMind ASN fields](https://dev.maxmind.com/geoip/docs/databases/asn/binary/)
|
|
390
|
+
- [MaxMind Python reader](https://github.com/maxmind/GeoIP2-python)
|
|
391
|
+
- [IPtoASN downloadable datasets](https://www.iptoasn.com/)
|
|
392
|
+
- [ISC API usage and throttling](https://isc.sans.edu/api/)
|
|
393
|
+
- [ISC static feeds](https://isc.sans.edu/feeds_doc.html)
|
|
394
|
+
- [Authoritative RDAP discovery, RFC 9224](https://www.rfc-editor.org/rfc/rfc9224.html)
|
|
395
|
+
- [RDAP rate limits, RFC 7480](https://www.rfc-editor.org/rfc/rfc7480.html#section-5.5)
|
|
396
|
+
- [Dnspython resolver timing](https://dnspython.readthedocs.io/en/stable/resolver-class.html)
|
|
397
|
+
- [Dnspython cache behavior](https://dnspython.readthedocs.io/en/stable/resolver-caching.html)
|
|
398
|
+
|
|
399
|
+
Bulk RIR mirrors and licensed Internet-wide PTR snapshots require separate data
|
|
400
|
+
acquisition, terms, and import design. This release does not download or mirror
|
|
401
|
+
them. It supplies local ASN/country lookups, selective RDAP, TTL-aware PTR results,
|
|
402
|
+
and safe ISC feed downloads for the pipeline's immediate needs. For historical
|
|
403
|
+
research, record event time, enrichment observation time, and dataset provenance
|
|
404
|
+
separately; current attribution is not proof of ownership at an earlier date.
|