mirror of
https://github.com/nox-project/nox-framework.git
synced 2026-06-13 18:23:33 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0814bfed2e | |||
| edcbf99fa7 | |||
| 61bd9b5555 | |||
| 294c7cbf84 | |||
| d72163d4b6 | |||
| 9bf66d3e50 | |||
| cf4428329e | |||
| 1612a7ef48 | |||
| a166ce411d | |||
| ee1c2257d4 | |||
| c7dbc1baac | |||
| 245d0f1714 | |||
| 200a21bf49 | |||
| 47bd936b6d | |||
| 911e814080 | |||
| ad45400e84 | |||
| 7febdc60f5 |
@@ -22,7 +22,7 @@ jobs:
|
|||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install pytest pytest-asyncio aiohttp pydantic colorama rich fpdf2 dnspython phonenumbers stem cloudscraper
|
run: pip install -r requirements.txt pytest pytest-asyncio
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: python -m pytest tests/ -v
|
run: python -m pytest tests/ -v
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -18,7 +20,7 @@ jobs:
|
|||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install pytest pytest-asyncio aiohttp pydantic colorama rich fpdf2 dnspython phonenumbers stem cloudscraper
|
run: pip install -r requirements.txt pytest pytest-asyncio
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: python -m pytest tests/ -v
|
run: python -m pytest tests/ -v
|
||||||
@@ -28,6 +30,9 @@ jobs:
|
|||||||
sudo apt-get install -y ruby ruby-dev build-essential
|
sudo apt-get install -y ruby ruby-dev build-essential
|
||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
|
|
||||||
|
- name: Build source plugins
|
||||||
|
run: python build_sources.py
|
||||||
|
|
||||||
- name: Build .deb
|
- name: Build .deb
|
||||||
run: bash build_deb.sh
|
run: bash build_deb.sh
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,3 @@ Thumbs.db
|
|||||||
# Env files
|
# Env files
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
# Internal development notes — not for distribution
|
|
||||||
IMPROVEMENTS.md
|
|
||||||
|
|||||||
+136
@@ -2,6 +2,142 @@
|
|||||||
|
|
||||||
All notable changes to NOX are documented here.
|
All notable changes to NOX are documented here.
|
||||||
|
|
||||||
|
## [1.0.5] — 2026-05-06
|
||||||
|
|
||||||
|
### Sources
|
||||||
|
- **Fixed:** `pypi_user` — endpoint was querying the PyPI package API (`/pypi/{target}/json`) instead of user profiles, returning package metadata for any username that matched a package name and 404 for all others. Source now uses the PyPI XML-RPC `user_packages` method, returning the actual list of packages owned by the target username.
|
||||||
|
|
||||||
|
### Engine
|
||||||
|
- **Fixed:** `Detect.qtype` — dotted-quad strings with out-of-range octets (e.g. `255.255.255.256`) were classified as `phone` instead of `username`. Invalid IPs now fall through to `username`.
|
||||||
|
- **Fixed:** `Record.dedup_key` — records with no identity fields (email, username, password) from different sources all produced the same SHA-256 hash, causing all but the first to be silently dropped from the DB. Source name is now included in the key when identity fields are absent.
|
||||||
|
- **Added:** `raw_payload` support in `NoxSourceProvider` — POST sources can now send raw string bodies (e.g. XML-RPC) in addition to JSON and form-encoded payloads.
|
||||||
|
- **Fixed:** `FileSystemProvider._by_regex` — regex-extracted values that are not email addresses (e.g. package names, usernames) are now correctly placed in the `username` field instead of `email`.
|
||||||
|
|
||||||
|
## [1.0.4] — 2026-04-22
|
||||||
|
|
||||||
|
### Engine
|
||||||
|
- **Fixed:** `_build_ssl_context` — custom TLS context had zero CA certificates loaded, causing `SSLCertVerificationError` on all HTTPS connections. CA bundle now loaded via `certifi` with `load_default_certs()` fallback.
|
||||||
|
- **Fixed:** `_NOISE_RE` in reporting — `ssl.`, `aiohttp.`, `asyncio.` were substring-matched, silently zeroing legitimate emails and domains (e.g. `user@ssl.example.com`). Patterns now anchored to start-of-string or whitespace.
|
||||||
|
- **Fixed:** `COMBO_RE` in `ScrapeEngine` — multiline greedy match produced credentials with embedded newlines in email/password fields. Pattern now excludes newlines from both capture groups.
|
||||||
|
- **Fixed:** `_in_flight` dict in `AvalancheScanner` — entries were never removed after processing, causing unbounded memory growth on deep scans. Entry is now popped in the `finally` block after the future resolves.
|
||||||
|
- **Fixed:** `DorkingEngine.__init__` — `ProxyManager.get_proxies()` was called eagerly on every `Orchestrator` instantiation, triggering a proxy fetch and OPSEC warning even for local-only commands (`--crack`, `--list-sources`, `--analyze`). Proxy fetch is now lazy.
|
||||||
|
- **Fixed:** `ScrapeEngine._fetch_content` — IntelX paste content fetch used `DB.get_key("intelx")` which does not read `apikeys.json`. Replaced with `Vault.get("INTELX_API_KEY")` for consistent key resolution.
|
||||||
|
- **Fixed:** `AsyncSource._rec` and `RiskEngine.score` — plugin `confidence` values were stored in `NoxSourceProvider._confidence` but never transferred to `Record.source_confidence`. All 124 plugin records received a flat `0.5` confidence regardless of their declared value. `_rec()` now injects `self._confidence` into the record; `RiskEngine.score()` uses `record.source_confidence` as fallback when the source is not in `_SRC_CONFIDENCE`.
|
||||||
|
- **Fixed:** `ConfigManager.get_key` — `None` results were cached, preventing env vars set after the first lookup from being detected in the same session. Only positive values are now cached.
|
||||||
|
- **Fixed:** Recursive Avalanche Engine — identifiers extracted from paste content (`paste["patterns"]`) were not being harvested as pivot seeds. All `scrape_res["pastes"]` pattern matches are now fed into `_extract_ids_from_text`.
|
||||||
|
|
||||||
|
### Sources
|
||||||
|
- **Fixed:** `circl_hashlookup` — endpoint hardcoded to `/lookup/md5/{target}` but `input_type=hash` accepts SHA1/SHA256. SHA1 and SHA256 backup endpoints added; engine now routes each hash type to the correct path.
|
||||||
|
- **Updated:** `crt_sh` — `reliability_score` lowered from 5 to 3, `is_volatile=true` added to reflect documented intermittent availability.
|
||||||
|
|
||||||
|
## [1.0.3] — 2026-04-15
|
||||||
|
|
||||||
|
### Engine
|
||||||
|
- **Fixed:** Recursive Avalanche Engine — identifiers extracted from paste content (`paste["patterns"]`) were not being harvested as pivot seeds. Bare emails and other identifiers found in IntelX paste bodies that lacked a `:password` separator were silently dropped from the pivot queue. All `scrape_res["pastes"]` pattern matches are now fed into `_extract_ids_from_text` and reinjected consistently with `credentials`, `telegram`, and `dork_misconfigs`.
|
||||||
|
|
||||||
|
## [1.0.2] — 2026-04-14
|
||||||
|
|
||||||
|
### Sources
|
||||||
|
- **Fixed:** `misp_search` — `MISP_URL` added to `api_key_slots` so the instance base URL is resolved at runtime; `health_check_url` corrected from unresolvable placeholder to `https://misp.local`
|
||||||
|
- **Fixed:** `threatconnect_search` — removed unresolvable `{TC_SIGNATURE}` HMAC placeholder from the `Authorization` header; `reliability_score` lowered to `2`, `is_volatile` set to `true`
|
||||||
|
- **Fixed:** `spycloud_breach` — endpoint corrected from `breach/data/emails` to `breach/catalog/emails` (standard breach lookup tier)
|
||||||
|
- **Fixed:** `duckduckgo_api` — primary instance updated to `search.sapti.me`; 5 backup SearXNG instances added to `backup_endpoints` (now consumed by the engine)
|
||||||
|
- **Fixed:** `gravatar` — endpoint now MD5-hashes the email before URL substitution via new `query_transform: md5_lower` field; raw email was returning 404 on every query
|
||||||
|
- **Replaced:** `bgpview_ip` → `ripestat_ip` (RIPE Stat prefix-overview API) — BGPView free API decommissioned January 2025; RIPE Stat is free, keyless, and stable (`reliability_score: 5`)
|
||||||
|
- **Fixed:** `twitter_v2` — marked `is_volatile=true`, `confidence` lowered to `0.1`; free-tier bearer tokens receive HTTP 403 since February 2024
|
||||||
|
- **Fixed:** `fofa_info` — `qbase64` parameter now receives `base64(domain="<target>")` via `query_transform: fofa_domain`; raw domain was producing malformed queries
|
||||||
|
- **Fixed:** `pipl_search` — Pipl shut down public REST API in Q3 2024; `reliability_score` lowered to `2`, `confidence` to `0.3`, `is_volatile=true`
|
||||||
|
- **Fixed:** `spyonweb` — API confirmed unreachable; `reliability_score` lowered to `1`, `confidence` to `0.1`, `is_volatile=true`
|
||||||
|
- **Fixed:** `hudsonrock_osint` — `is_volatile=true`; `rate_limit` raised from `5.0` to `30.0` to respect Cavalier API throttling (~10 req/hour free tier)
|
||||||
|
- **Fixed:** `mailboxlayer`, `numverify`, `ipstack`, `ipinfodb` — endpoints and `health_check_url` migrated from `http://` to `https://`; API keys were being transmitted in cleartext before the server-side redirect
|
||||||
|
- **Added:** `xposedornot` plugin (free, public breach analytics)
|
||||||
|
- **Added:** `MISP_URL` to service registry and `apikeys.json` — back-filled automatically on first run after upgrade
|
||||||
|
- Source count: 123 → 124
|
||||||
|
|
||||||
|
### Config
|
||||||
|
- **Fixed:** Duplicate `xposedornot` entry removed from `SERVICE_REGISTRY` in `config_handler.py`
|
||||||
|
|
||||||
|
### Engine
|
||||||
|
- **Fixed:** `_parse_retry_after` helper added — `int()` on an HTTP-date `Retry-After` header raised `ValueError`, causing the retry loop to abort as a hard failure; all 5 call sites in `_get`, `_post`, `Session.get`, and `Session.post` updated
|
||||||
|
- **Fixed:** `_random_headers` — `Sec-CH-UA` Client Hints were emitted even when a Firefox UA was passed via the `extra` override; guard now evaluates the final `User-Agent` after overrides are applied
|
||||||
|
- **Fixed:** `HashEngine._hashmob` — Hashmob API v2 changed request field from `"hash"` to `"hashes"` (array) and response schema from `{found, result}` to `{data: [{plaintext}]}`
|
||||||
|
- **Fixed:** `DeHashEngine` — both `_lookup` and the sync fallback were calling the deprecated `/search` (v1) endpoint; updated to `/v2/search`
|
||||||
|
- **Fixed:** `DorkEngine.run` — results were labelled with the requested engine name (`google`/`bing`/`ddg`) instead of `SearXNG` which is the actual backend; the 3× request multiplication (one pass per engine name, all hitting the same SearXNG pool) is eliminated
|
||||||
|
- **Fixed:** `DB.close()` — background event loop was stopped but never closed, leaving the loop object open on process exit
|
||||||
|
- **Fixed:** `NoxSourceProvider._fetch` — `backup_endpoints` defined in source plugins were parsed but never consumed; primary endpoint failure now falls through to backups in order
|
||||||
|
- **Fixed:** `_local_crack_sync_blocking` — `hashlib.md5/sha1` now called with `usedforsecurity=False` to prevent hard crash on FIPS-enabled systems (RHEL 9, hardened Kali); Python 3.8 compat guard included
|
||||||
|
|
||||||
|
### Codebase
|
||||||
|
- All internal tracking comments replaced with clean prose throughout `nox.py`, `build_sources.py`, and all helper modules
|
||||||
|
|
||||||
|
### Build
|
||||||
|
- `BUILD_DATE` updated to `2026-04-14`
|
||||||
|
- `pyproject.toml` version bumped to `1.0.2`; `requests` minimum pin aligned to `>=2.32.3`
|
||||||
|
|
||||||
|
## [1.0.1] — 2026-04-13
|
||||||
|
|
||||||
|
### Sources
|
||||||
|
- **Removed:** `cit0day` (HTML fingerprint challenge, no JSON response), `vigilante_pw` (redirects to dehashed.com), `scylla_sh_search` (domain parked, permanently unreachable)
|
||||||
|
- **Restored:** `proxynova_comb` (live, returns valid JSON — was incorrectly removed)
|
||||||
|
- **Fixed:** `leak_lookup` now requires API key (`LEAK_LOOKUP_API_KEY`) — provider removed unauthenticated access
|
||||||
|
- **Fixed:** `intelx_search` two-phase poll implemented — plugin previously submitted the search job but never polled for results, returning 0 records for all queries
|
||||||
|
- **Fixed:** `hudsonrock_osint` missing `User-Agent` header added — endpoint returns 403 without a browser UA
|
||||||
|
- **Removed:** `HASHES_API_KEY` registry entry — hashes.org shut down in 2023; `HASHES_COM_API_KEY` is the correct active slot
|
||||||
|
- **Added:** `LEAK_LOOKUP_API_KEY` to service registry
|
||||||
|
|
||||||
|
### Engine
|
||||||
|
- **Fixed:** `bypass_required` field in source plugins now enforced at runtime — sources declaring `["cloudflare"]` bypass are skipped when `curl_cffi` is absent (previously the field was parsed but never read)
|
||||||
|
- **Fixed:** Guardian proxy auto-fetch updated to ProxyScrape v3 API (v2 deprecated Q1 2026); `proxy-list.download` replaced with `proxifly` free list
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- **Updated:** `requests>=2.32.3` (CVE fixes)
|
||||||
|
|
||||||
|
### README
|
||||||
|
- Source count updated: 123 active plugins
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- **Updated:** `aiohttp` minimum pin raised to `>=3.13.5` (connection-pool stability fixes under high concurrency)
|
||||||
|
- **Added:** `zstandard>=0.23.0` — enables native zstd decompression in aiohttp for Cloudflare/Fastly CDN responses
|
||||||
|
|
||||||
|
### Engine
|
||||||
|
- **Updated:** `Accept-Encoding` header now includes `zstd` (`gzip, deflate, br, zstd`) to match Chrome 124+ behaviour
|
||||||
|
|
||||||
|
## [1.0.1] — 2026-04-11
|
||||||
|
|
||||||
|
### Sources
|
||||||
|
- **Added 9 new sources:** `proxynova_comb` (COMB breach search, free), `shodan_internetdb` (IP intel, free), `circl_hashlookup` (NSRL hash lookup, free), `ipapi_is` (IP geolocation, free), `threatfox` (abuse.ch IOC database), `urlhaus` (abuse.ch malware URLs), `malwarebazaar` (abuse.ch hash lookup), `fullhunt_subdomains` (attack surface), `netlas_search` (internet scanner)
|
||||||
|
- **Removed 7 dead sources:** `threatcrowd_email`, `threatcrowd_domain` (DNS dead), `spyse_domain`, `spyse_ip` (API shut down), `hashes_org` (DNS dead), `leakstats_pw` (DNS dead), `checkleaked` (endpoint gone)
|
||||||
|
- **Fixed:** `dehashed` endpoint migrated from `/search` to `/v2/search`
|
||||||
|
- **Fixed:** `hudsonrock_osint` endpoint corrected to `search-by-email` with `input_type: email` (was `search-by-login` with `input_type: username`)
|
||||||
|
- **Fixed:** `scylla_sh_search` migrated from dead `scylla.sh` to active `scylla.so`
|
||||||
|
- **Fixed:** `emailrep_io` now requires API key (`EMAILREP_API_KEY`) — free unauthenticated tier removed by provider
|
||||||
|
- **Fixed:** `duckduckgo_api` repurposed from dead DDG Instant Answer API to SearXNG JSON search
|
||||||
|
|
||||||
|
### Engine
|
||||||
|
- **Fixed:** POST 429 `Retry-After` cap was 4s (should be 30s, matching GET path)
|
||||||
|
- **Fixed:** Linear retry backoff replaced with exponential backoff + jitter in all 4 retry paths (`_get`, `_post`, `Session.get`, `Session.post`)
|
||||||
|
- **Fixed:** `--reset-sources` now removes orphaned plugins from `~/.nox/sources/` in addition to copying new ones
|
||||||
|
- **Fixed:** DDG HTML scraper replaced with SearXNG JSON API across all call sites — DDG HTML endpoint bot-blocked since 2025
|
||||||
|
- **Fixed:** SearXNG instance pool updated: `searx.be` (403), `search.bus-hit.me` (DNS dead), `searxng.site` (SSL error) replaced with 6 active instances; pool extracted to module-level `_SEARX_INSTANCES` constant
|
||||||
|
- **Fixed:** All 11 dead paste site APIs removed from `ScrapeEngine.PASTE_SITES`; paste intelligence now routed through SearXNG dorks and IntelX
|
||||||
|
|
||||||
|
### WAF Resilience
|
||||||
|
- **Updated:** User-Agent pool updated to Chrome/135, Firefox/136, Edge/135 (was Chrome/131, Firefox/133)
|
||||||
|
- **Added:** `Sec-CH-UA`, `Sec-CH-UA-Mobile`, `Sec-CH-UA-Platform` Client Hints headers for Chromium-based UAs
|
||||||
|
- **Fixed:** `_CH_UA_MAP` ordering — Edge UA now correctly gets `"Microsoft Edge"` brand (was getting `"Google Chrome"` due to dict iteration order)
|
||||||
|
- **Fixed:** `_search()` sync method no longer passes `use_cloudscraper=True` to SearXNG JSON API calls
|
||||||
|
|
||||||
|
### Hash Cracking
|
||||||
|
- **Removed 6 dead/paywalled cracker APIs:** nitrxgen (DNS dead), hash.help (DNS dead), hashkiller (403), hashes.com free path (404), md5decrypt (403), cmd5 (paywalled — returns `CMD5-ERROR:-1` for all hashes)
|
||||||
|
- **Added:** Local rockyou wordlist as primary crack path (no external calls, no rate limits, no data leakage)
|
||||||
|
- **Added:** `hashes.com` keyed API as external fallback (`HASHES_COM_API_KEY`)
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- **Added:** `brotli>=1.1.0` — required for aiohttp to decompress `br`-encoded responses
|
||||||
|
|
||||||
|
### Config
|
||||||
|
- **Added 7 new API key slots:** `EMAILREP_API_KEY`, `HASHES_COM_API_KEY`, `THREATFOX_API_KEY`, `URLHAUS_API_KEY`, `MALWAREBAZAAR_API_KEY`, `FULLHUNT_API_KEY`, `NETLAS_API_KEY`
|
||||||
|
|
||||||
## [1.0.0] — 2026-04-02
|
## [1.0.0] — 2026-04-02
|
||||||
|
|
||||||
### Initial Release
|
### Initial Release
|
||||||
|
|||||||
@@ -11,10 +11,11 @@
|
|||||||
|
|
||||||
**Cyber Threat Intelligence Framework**
|
**Cyber Threat Intelligence Framework**
|
||||||
|
|
||||||
[](https://github.com/nox-project/nox-framework/releases/tag/v1.0.0)
|
[](https://github.com/nox-project/nox-framework/releases/tag/v1.0.5)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
[](LICENSE.txt)
|
[](LICENSE.txt)
|
||||||
[](https://www.kali.org/)
|
[](https://www.kali.org/)
|
||||||
|
[](https://blackarch.org/)
|
||||||
[](https://github.com/nox-project/nox-framework)
|
[](https://github.com/nox-project/nox-framework)
|
||||||
[](https://github.com/nox-project/nox-framework)
|
[](https://github.com/nox-project/nox-framework)
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ NOX is a purpose-built cyber threat intelligence engine designed for operators w
|
|||||||
|-|-|
|
|-|-|
|
||||||
| **124 JSON Plugin Sources** | Every intelligence source is a JSON plugin. The execution engine contains zero hardcoded source logic. |
|
| **124 JSON Plugin Sources** | Every intelligence source is a JSON plugin. The execution engine contains zero hardcoded source logic. |
|
||||||
| **Async Core** | Full `asyncio` event loop with JA3 fingerprinting, SSL session management, per-request jitter, and configurable concurrency. |
|
| **Async Core** | Full `asyncio` event loop with JA3 fingerprinting, SSL session management, per-request jitter, and configurable concurrency. |
|
||||||
| **Autoscan Pipeline** | `--autoscan` triggers: breach scan → recursive pivot → Google/Bing/DDG dorking → paste/Telegram scraping — all in one command. |
|
| **Autoscan Pipeline** | `--autoscan` triggers: breach scan → recursive pivot → Google/Bing/SearXNG dorking → paste/Telegram scraping — all in one command. |
|
||||||
| **Recursive Avalanche Engine** | Every identifier discovered — from breach records, dork hits, or scraped paste/Telegram content — is re-injected as a new seed. Per-asset pipeline is sequential (breach → crack → dork → scrape); child assets run concurrently via `asyncio.gather`. A global `seen_assets` set prevents infinite loops. Concurrency and depth are fully configurable at runtime via `--threads` and `--depth`. |
|
| **Recursive Avalanche Engine** | Every identifier discovered — from breach records, dork hits, or scraped paste/Telegram content — is re-injected as a new seed. Per-asset pipeline is sequential (breach → crack → dork → scrape); child assets run concurrently via `asyncio.gather`. A global `seen_assets` set prevents infinite loops. Concurrency and depth are fully configurable at runtime via `--threads` and `--depth`. |
|
||||||
| **Hash Pivoting** | Hashes found in breach data are automatically identified (MD5/SHA1/SHA256/NTLM/bcrypt) and cracked via concurrent background API queries. Cracked plaintexts are injected into the pivot queue as password-recycling seeds. Failures are logged silently — the scan never stops. |
|
| **Hash Pivoting** | Hashes found in breach data are automatically identified (MD5/SHA1/SHA256/NTLM/bcrypt) and cracked via concurrent background API queries. Cracked plaintexts are injected into the pivot queue as password-recycling seeds. Failures are logged silently — the scan never stops. |
|
||||||
| **Guardian Proxy Engine** | Zero-config OPSEC layer: reads `proxies.txt` if present; otherwise auto-fetches and validates a high-anonymity proxy pool in-memory. Full SOCKS5/HTTP/S and Tor support. |
|
| **Guardian Proxy Engine** | Zero-config OPSEC layer: reads `proxies.txt` if present; otherwise auto-fetches and validates a high-anonymity proxy pool in-memory. Full SOCKS5/HTTP/S and Tor support. |
|
||||||
@@ -52,7 +53,7 @@ NOX is a purpose-built cyber threat intelligence engine designed for operators w
|
|||||||
| **Identity Graphing** | Union-Find correlation engine unifies breach records into identity clusters across all sources, using type-aware pivot classification. |
|
| **Identity Graphing** | Union-Find correlation engine unifies breach records into identity clusters across all sources, using type-aware pivot classification. |
|
||||||
| **Enterprise Forensic Reports** | Professional PDF/HTML/JSON/CSV/Markdown reports with Executive Summary dashboard (Total Time, Nodes Discovered, Cleartext Passwords, Pivot Depth), interactive Pivot Chain Visualization, and strict data sanitization — no technical noise in output. JSON exports are self-describing with a full metadata block. |
|
| **Enterprise Forensic Reports** | Professional PDF/HTML/JSON/CSV/Markdown reports with Executive Summary dashboard (Total Time, Nodes Discovered, Cleartext Passwords, Pivot Depth), interactive Pivot Chain Visualization, and strict data sanitization — no technical noise in output. JSON exports are self-describing with a full metadata block. |
|
||||||
| **HVT Detection** | Auto-flags C-level, Admin, DevOps, and government domain accounts as High-Value Targets. |
|
| **HVT Detection** | Auto-flags C-level, Admin, DevOps, and government domain accounts as High-Value Targets. |
|
||||||
| **Dorking Engine** | Passive document discovery via Google/Bing/DDG dorks with PDF/Office metadata extraction. |
|
| **Dorking Engine** | Passive document discovery via Google/Bing/SearXNG dorks with PDF/Office metadata extraction. |
|
||||||
| **Scraping Engine** | Paste site indexing, Telegram CTI channel monitoring, credential extraction, and misconfiguration discovery. Each autoscan asset gets a dedicated scrape session — no shared state. |
|
| **Scraping Engine** | Paste site indexing, Telegram CTI channel monitoring, credential extraction, and misconfiguration discovery. Each autoscan asset gets a dedicated scrape session — no shared state. |
|
||||||
| **Proxy / Tor** | SOCKS5, HTTP/S proxy, full Tor routing via `stem`, and automatic Guardian fallback. SOCKS5 proxies are validated and routed correctly via `aiohttp-socks`. |
|
| **Proxy / Tor** | SOCKS5, HTTP/S proxy, full Tor routing via `stem`, and automatic Guardian fallback. SOCKS5 proxies are validated and routed correctly via `aiohttp-socks`. |
|
||||||
| **Secure Key Store** | API keys managed via `~/.config/nox-cli/apikeys.json` (chmod 0600). Unconfigured keys are silently skipped. Keys set via environment variable are picked up automatically without restarting. |
|
| **Secure Key Store** | API keys managed via `~/.config/nox-cli/apikeys.json` (chmod 0600). Unconfigured keys are silently skipped. Keys set via environment variable are picked up automatically without restarting. |
|
||||||
@@ -114,7 +115,7 @@ For each asset (seed + every discovered identifier):
|
|||||||
│ → password-recycling breach scan
|
│ → password-recycling breach scan
|
||||||
│
|
│
|
||||||
├─ Phase 3 — Dorking
|
├─ Phase 3 — Dorking
|
||||||
│ Google/Bing/DDG dorks → leaked docs, .env files, SQL dumps
|
│ Google/Bing/SearXNG dorks → leaked docs, .env files, SQL dumps
|
||||||
│ → new identifiers extracted and re-injected
|
│ → new identifiers extracted and re-injected
|
||||||
│
|
│
|
||||||
└─ Phase 4 — Scraping
|
└─ Phase 4 — Scraping
|
||||||
@@ -373,7 +374,7 @@ usage: nox-cli [-h] [-t TARGET] [-i] [--version]
|
|||||||
--fullscan Breach + pivot only (no dork/scrape)
|
--fullscan Breach + pivot only (no dork/scrape)
|
||||||
--no-pivot Disable recursive pivot enrichment
|
--no-pivot Disable recursive pivot enrichment
|
||||||
--depth N Avalanche pivot depth (default: 2)
|
--depth N Avalanche pivot depth (default: 2)
|
||||||
--dork TARGET Google/Bing/DDG dorking for leaked documents
|
--dork TARGET Google/Bing/SearXNG dorking for leaked documents
|
||||||
--scrape TARGET Paste site + Telegram scraping
|
--scrape TARGET Paste site + Telegram scraping
|
||||||
--crack HASH Identify and crack a hash
|
--crack HASH Identify and crack a hash
|
||||||
--no-online-crack Local wordlist only — no data sent to third-party APIs
|
--no-online-crack Local wordlist only — no data sent to third-party APIs
|
||||||
@@ -406,7 +407,7 @@ Command Description
|
|||||||
----------- ---------------------------------------------------------------
|
----------- ---------------------------------------------------------------
|
||||||
autoscan Full pipeline: breach + pivot + dork + scrape
|
autoscan Full pipeline: breach + pivot + dork + scrape
|
||||||
scan Breach intelligence scan only
|
scan Breach intelligence scan only
|
||||||
dork Google/Bing/DDG dorking for leaked documents
|
dork Google/Bing/SearXNG dorking for leaked documents
|
||||||
scrape Paste site + Telegram scraping
|
scrape Paste site + Telegram scraping
|
||||||
crack Identify and crack a hash
|
crack Identify and crack a hash
|
||||||
analyze Deep password strength analysis
|
analyze Deep password strength analysis
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# NOX v1.0.0 — .deb build script (FPM)
|
# NOX v1.0.5 — .deb build script (FPM)
|
||||||
# Requires: fpm → gem install fpm
|
# Requires: fpm → gem install fpm
|
||||||
|
|
||||||
VERSION="1.0.0"
|
VERSION="1.0.5"
|
||||||
PKG_NAME="nox-cli"
|
PKG_NAME="nox-cli"
|
||||||
ARCH="all"
|
ARCH="all"
|
||||||
OUT_DIR="dist"
|
OUT_DIR="dist"
|
||||||
|
|||||||
+217
-140
@@ -53,6 +53,8 @@ class SourceConfig(BaseModel):
|
|||||||
rate_limit: float = 1.0
|
rate_limit: float = 1.0
|
||||||
headers: Dict[str, str] = Field(default_factory=dict)
|
headers: Dict[str, str] = Field(default_factory=dict)
|
||||||
payload_template: Optional[Dict[str, Any]] = None
|
payload_template: Optional[Dict[str, Any]] = None
|
||||||
|
raw_payload: Optional[str] = None
|
||||||
|
regex_pattern: Optional[str] = None
|
||||||
api_key_slots: List[str] = Field(default_factory=list)
|
api_key_slots: List[str] = Field(default_factory=list)
|
||||||
|
|
||||||
# ── Typing & pivoting ───────────────────────────────────────────────────
|
# ── Typing & pivoting ───────────────────────────────────────────────────
|
||||||
@@ -69,8 +71,13 @@ class SourceConfig(BaseModel):
|
|||||||
bypass_required: Optional[List[str]] = None # omitted when empty
|
bypass_required: Optional[List[str]] = None # omitted when empty
|
||||||
user_agent_type: Optional[str] = None # omitted when absent
|
user_agent_type: Optional[str] = None # omitted when absent
|
||||||
backup_endpoints: List[str] = Field(default_factory=list)
|
backup_endpoints: List[str] = Field(default_factory=list)
|
||||||
# H2: optional confidence override — when set, takes precedence over formula
|
|
||||||
confidence: Optional[float] = None
|
confidence: Optional[float] = None
|
||||||
|
query_transform: Optional[str] = None # e.g. "md5_lower"
|
||||||
|
# Two-phase poll support (e.g. IntelX: POST → job_id → GET results)
|
||||||
|
poll_endpoint: Optional[str] = None
|
||||||
|
poll_id_field: Optional[str] = None
|
||||||
|
poll_id_param: Optional[str] = None
|
||||||
|
poll_json_root: Optional[str] = None
|
||||||
|
|
||||||
@field_validator("reliability_score")
|
@field_validator("reliability_score")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -81,12 +88,10 @@ class SourceConfig(BaseModel):
|
|||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def _validate_source(self) -> "SourceConfig":
|
def _validate_source(self) -> "SourceConfig":
|
||||||
# H1: GET endpoints must contain {target} placeholder
|
|
||||||
if self.method.upper() == "GET" and "{target}" not in self.endpoint:
|
if self.method.upper() == "GET" and "{target}" not in self.endpoint:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"'{self.name}': GET endpoint must contain {{target}} placeholder: {self.endpoint!r}"
|
f"'{self.name}': GET endpoint must contain {{target}} placeholder: {self.endpoint!r}"
|
||||||
)
|
)
|
||||||
# L3: volatile sources must have reliability_score ≤ 4 (was > 3, now > 4)
|
|
||||||
if self.is_volatile and self.reliability_score > 4:
|
if self.is_volatile and self.reliability_score > 4:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"'{self.name}': is_volatile sources must have reliability_score ≤ 4"
|
f"'{self.name}': is_volatile sources must have reliability_score ≤ 4"
|
||||||
@@ -95,11 +100,10 @@ class SourceConfig(BaseModel):
|
|||||||
|
|
||||||
def to_json(self) -> str:
|
def to_json(self) -> str:
|
||||||
data = self.model_dump(exclude_none=True)
|
data = self.model_dump(exclude_none=True)
|
||||||
# Drop is_volatile / bypass_required / user_agent_type when falsy
|
# Drop falsy optional fields
|
||||||
for key in ("is_volatile", "bypass_required", "user_agent_type"):
|
for key in ("is_volatile", "bypass_required", "user_agent_type", "query_transform"):
|
||||||
if not data.get(key):
|
if not data.get(key):
|
||||||
data.pop(key, None)
|
data.pop(key, None)
|
||||||
# H2: use explicit confidence if set, otherwise derive from reliability_score
|
|
||||||
data["confidence"] = (
|
data["confidence"] = (
|
||||||
round(self.confidence, 2)
|
round(self.confidence, 2)
|
||||||
if self.confidence is not None
|
if self.confidence is not None
|
||||||
@@ -119,6 +123,8 @@ def _mk(
|
|||||||
rate_limit: float = 1.0,
|
rate_limit: float = 1.0,
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
payload_template: Optional[Dict[str, Any]] = None,
|
payload_template: Optional[Dict[str, Any]] = None,
|
||||||
|
raw_payload: Optional[str] = None,
|
||||||
|
regex_pattern: Optional[str] = None,
|
||||||
api_key_slots: Optional[List[str]] = None,
|
api_key_slots: Optional[List[str]] = None,
|
||||||
input_type: InputType = "any",
|
input_type: InputType = "any",
|
||||||
output_type: Optional[List[str]] = None,
|
output_type: Optional[List[str]] = None,
|
||||||
@@ -131,6 +137,12 @@ def _mk(
|
|||||||
bypass_required: Optional[List[str]] = None,
|
bypass_required: Optional[List[str]] = None,
|
||||||
user_agent_type: Optional[str] = None,
|
user_agent_type: Optional[str] = None,
|
||||||
backup_endpoints: Optional[List[str]] = None,
|
backup_endpoints: Optional[List[str]] = None,
|
||||||
|
poll_endpoint: Optional[str] = None,
|
||||||
|
poll_id_field: Optional[str] = None,
|
||||||
|
poll_id_param: Optional[str] = None,
|
||||||
|
poll_json_root: Optional[str] = None,
|
||||||
|
confidence: Optional[float] = None,
|
||||||
|
query_transform: Optional[str] = None,
|
||||||
) -> SourceConfig:
|
) -> SourceConfig:
|
||||||
return SourceConfig(
|
return SourceConfig(
|
||||||
name=name, category=category, endpoint=endpoint, method=method,
|
name=name, category=category, endpoint=endpoint, method=method,
|
||||||
@@ -138,6 +150,8 @@ def _mk(
|
|||||||
rate_limit=rate_limit,
|
rate_limit=rate_limit,
|
||||||
headers=headers or {},
|
headers=headers or {},
|
||||||
payload_template=payload_template,
|
payload_template=payload_template,
|
||||||
|
raw_payload=raw_payload,
|
||||||
|
regex_pattern=regex_pattern,
|
||||||
api_key_slots=api_key_slots or [],
|
api_key_slots=api_key_slots or [],
|
||||||
input_type=input_type,
|
input_type=input_type,
|
||||||
output_type=output_type or [],
|
output_type=output_type or [],
|
||||||
@@ -150,6 +164,12 @@ def _mk(
|
|||||||
bypass_required=bypass_required or None,
|
bypass_required=bypass_required or None,
|
||||||
user_agent_type=user_agent_type,
|
user_agent_type=user_agent_type,
|
||||||
backup_endpoints=backup_endpoints or [],
|
backup_endpoints=backup_endpoints or [],
|
||||||
|
poll_endpoint=poll_endpoint,
|
||||||
|
poll_id_field=poll_id_field,
|
||||||
|
poll_id_param=poll_id_param,
|
||||||
|
poll_json_root=poll_json_root,
|
||||||
|
confidence=confidence,
|
||||||
|
query_transform=query_transform or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -174,7 +194,7 @@ FREE_PUBLIC_SOURCES: List[SourceConfig] = [
|
|||||||
input_type="domain", output_type=["domain"],
|
input_type="domain", output_type=["domain"],
|
||||||
normalization_map={"name_value": "domain"},
|
normalization_map={"name_value": "domain"},
|
||||||
tags=["passive", "fast"],
|
tags=["passive", "fast"],
|
||||||
health_check_url="https://crt.sh", reliability_score=5),
|
health_check_url="https://crt.sh", reliability_score=3, is_volatile=True),
|
||||||
|
|
||||||
_base("hackertarget_hostsearch", "dns_recon",
|
_base("hackertarget_hostsearch", "dns_recon",
|
||||||
"https://api.hackertarget.com/hostsearch/?q={target}", "GET",
|
"https://api.hackertarget.com/hostsearch/?q={target}", "GET",
|
||||||
@@ -240,24 +260,6 @@ FREE_PUBLIC_SOURCES: List[SourceConfig] = [
|
|||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="https://urlscan.io", reliability_score=5),
|
health_check_url="https://urlscan.io", reliability_score=5),
|
||||||
|
|
||||||
_base("threatcrowd_email", "threat_intel",
|
|
||||||
"https://www.threatcrowd.org/searchApi/v2/email/report/?email={target}", "GET",
|
|
||||||
{"domains": "$.domains"},
|
|
||||||
rate_limit=5.0,
|
|
||||||
input_type="email", output_type=["domain"],
|
|
||||||
tags=["passive", "threat"],
|
|
||||||
health_check_url="https://www.threatcrowd.org", reliability_score=3,
|
|
||||||
is_volatile=True, bypass_required=["cloudflare"], user_agent_type="browser"),
|
|
||||||
|
|
||||||
_base("threatcrowd_domain", "threat_intel",
|
|
||||||
"https://www.threatcrowd.org/searchApi/v2/domain/report/?domain={target}", "GET",
|
|
||||||
{"ips": "$.resolutions[*].ip_address"},
|
|
||||||
rate_limit=5.0,
|
|
||||||
input_type="domain", output_type=["ip"],
|
|
||||||
tags=["passive", "threat"],
|
|
||||||
health_check_url="https://www.threatcrowd.org", reliability_score=3,
|
|
||||||
is_volatile=True, bypass_required=["cloudflare"], user_agent_type="browser"),
|
|
||||||
|
|
||||||
_base("pulsedive", "threat_intel",
|
_base("pulsedive", "threat_intel",
|
||||||
"https://pulsedive.com/api/info.php?indicator={target}", "GET",
|
"https://pulsedive.com/api/info.php?indicator={target}", "GET",
|
||||||
{"risk": "$.risk", "threats": "$.threats"},
|
{"risk": "$.risk", "threats": "$.threats"},
|
||||||
@@ -266,13 +268,27 @@ FREE_PUBLIC_SOURCES: List[SourceConfig] = [
|
|||||||
tags=["passive", "threat"],
|
tags=["passive", "threat"],
|
||||||
health_check_url="https://pulsedive.com", reliability_score=4),
|
health_check_url="https://pulsedive.com", reliability_score=4),
|
||||||
|
|
||||||
|
_base("xposedornot", "breach_data",
|
||||||
|
"https://api.xposedornot.com/v1/breach-analytics?email={target}", "GET",
|
||||||
|
{"breaches": "$.ExposedBreaches.breaches_details"},
|
||||||
|
rate_limit=2.0,
|
||||||
|
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"},
|
||||||
|
input_type="email", output_type=["email", "domain"],
|
||||||
|
normalization_map={"breach": "breach_name", "domain": "domain",
|
||||||
|
"xposed_date": "breach_date", "xposed_data": "data_types",
|
||||||
|
"password_risk": "password_risk"},
|
||||||
|
tags=["passive", "stealth"],
|
||||||
|
health_check_url="https://api.xposedornot.com", reliability_score=4, confidence=0.75),
|
||||||
|
|
||||||
_base("hudsonrock_osint", "breach_data",
|
_base("hudsonrock_osint", "breach_data",
|
||||||
"https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-login?username={target}", "GET",
|
"https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-email?email={target}", "GET",
|
||||||
{"stealers": "$.stealers"},
|
{"stealers": "$.stealers"},
|
||||||
input_type="username", output_type=["email", "domain"],
|
rate_limit=30.0,
|
||||||
|
headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"},
|
||||||
|
input_type="email", output_type=["email", "domain", "username"],
|
||||||
normalization_map={"stealers": "breach_record"},
|
normalization_map={"stealers": "breach_record"},
|
||||||
tags=["passive", "stealth"],
|
tags=["passive", "stealth"],
|
||||||
health_check_url="https://cavalier.hudsonrock.com", reliability_score=4),
|
health_check_url="https://cavalier.hudsonrock.com", reliability_score=3, is_volatile=True),
|
||||||
|
|
||||||
_base("ipinfo_io", "geolocation",
|
_base("ipinfo_io", "geolocation",
|
||||||
"https://ipinfo.io/{target}/json", "GET",
|
"https://ipinfo.io/{target}/json", "GET",
|
||||||
@@ -291,17 +307,20 @@ FREE_PUBLIC_SOURCES: List[SourceConfig] = [
|
|||||||
tags=["passive", "fast"],
|
tags=["passive", "fast"],
|
||||||
health_check_url="https://ipapi.co", reliability_score=4),
|
health_check_url="https://ipapi.co", reliability_score=4),
|
||||||
|
|
||||||
_base("bgpview_ip", "network",
|
_base("ripestat_ip", "network",
|
||||||
"https://api.bgpview.io/ip/{target}", "GET",
|
"https://stat.ripe.net/data/prefix-overview/data.json?resource={target}", "GET",
|
||||||
{"prefixes": "$.data.prefixes[*].prefix"},
|
{"asns": "$.data.asns[*].asn", "holder": "$.data.asns[0].holder"},
|
||||||
input_type="ip", output_type=["ip"],
|
input_type="ip", output_type=["ip"],
|
||||||
|
normalization_map={"asn": "asn_number", "holder": "asn_org"},
|
||||||
tags=["passive", "infrastructure"],
|
tags=["passive", "infrastructure"],
|
||||||
health_check_url="https://api.bgpview.io", reliability_score=4),
|
health_check_url="https://stat.ripe.net", reliability_score=5),
|
||||||
|
|
||||||
_base("emailrep_io", "email_rep",
|
_auth("emailrep_io", "email_rep",
|
||||||
"https://emailrep.io/{target}", "GET",
|
"https://emailrep.io/{target}", "GET",
|
||||||
{"reputation": "$.reputation"},
|
{"reputation": "$.reputation"},
|
||||||
rate_limit=2.0,
|
rate_limit=2.0,
|
||||||
|
headers={"Key": "{EMAILREP_API_KEY}"},
|
||||||
|
api_key_slots=["{EMAILREP_API_KEY}"],
|
||||||
input_type="email", output_type=["email"],
|
input_type="email", output_type=["email"],
|
||||||
normalization_map={"reputation": "email_reputation"},
|
normalization_map={"reputation": "email_reputation"},
|
||||||
tags=["passive", "fast"],
|
tags=["passive", "fast"],
|
||||||
@@ -328,6 +347,7 @@ FREE_PUBLIC_SOURCES: List[SourceConfig] = [
|
|||||||
{"name": "$.entry[0].displayName"},
|
{"name": "$.entry[0].displayName"},
|
||||||
rate_limit=2.0,
|
rate_limit=2.0,
|
||||||
input_type="email", output_type=["username"],
|
input_type="email", output_type=["username"],
|
||||||
|
query_transform="md5_lower",
|
||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="https://www.gravatar.com", reliability_score=4),
|
health_check_url="https://www.gravatar.com", reliability_score=4),
|
||||||
|
|
||||||
@@ -402,11 +422,15 @@ FREE_PUBLIC_SOURCES: List[SourceConfig] = [
|
|||||||
health_check_url="https://ipvigilante.com", reliability_score=3, is_volatile=True),
|
health_check_url="https://ipvigilante.com", reliability_score=3, is_volatile=True),
|
||||||
|
|
||||||
_base("pypi_user", "social",
|
_base("pypi_user", "social",
|
||||||
"https://pypi.org/pypi/{target}/json", "GET",
|
"https://pypi.org/pypi", "POST",
|
||||||
{"info": "$.info"},
|
{},
|
||||||
|
headers={"Content-Type": "text/xml"},
|
||||||
input_type="username", output_type=["username"],
|
input_type="username", output_type=["username"],
|
||||||
|
normalization_map={},
|
||||||
|
regex_pattern=r"<string>Owner</string></value>\s*<value><string>([^<]+)</string>",
|
||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="https://pypi.org", reliability_score=5),
|
health_check_url="https://pypi.org", reliability_score=4,
|
||||||
|
raw_payload="<?xml version='1.0'?><methodCall><methodName>user_packages</methodName><params><param><value>{target}</value></param></params></methodCall>"),
|
||||||
|
|
||||||
_base("npm_user", "social",
|
_base("npm_user", "social",
|
||||||
"https://registry.npmjs.org/-/v1/search?text=maintainer:{target}", "GET",
|
"https://registry.npmjs.org/-/v1/search?text=maintainer:{target}", "GET",
|
||||||
@@ -446,11 +470,19 @@ FREE_PUBLIC_SOURCES: List[SourceConfig] = [
|
|||||||
health_check_url="https://checkurl.phishtank.com", reliability_score=4),
|
health_check_url="https://checkurl.phishtank.com", reliability_score=4),
|
||||||
|
|
||||||
_base("duckduckgo_api", "search",
|
_base("duckduckgo_api", "search",
|
||||||
"https://api.duckduckgo.com/?q={target}&format=json", "GET",
|
"https://search.sapti.me/search?q={target}&format=json&categories=general", "GET",
|
||||||
{"abstract": "$.Abstract"},
|
{"results": "$.results"},
|
||||||
input_type="any", output_type=["url"],
|
input_type="any", output_type=["url"],
|
||||||
|
normalization_map={"url": "url", "title": "title"},
|
||||||
tags=["passive", "fast"],
|
tags=["passive", "fast"],
|
||||||
health_check_url="https://api.duckduckgo.com", reliability_score=5),
|
backup_endpoints=[
|
||||||
|
"https://searx.tiekoetter.com/search?q={target}&format=json&categories=general",
|
||||||
|
"https://searx.perennialte.ch/search?q={target}&format=json&categories=general",
|
||||||
|
"https://search.mdosch.de/search?q={target}&format=json&categories=general",
|
||||||
|
"https://paulgo.io/search?q={target}&format=json&categories=general",
|
||||||
|
"https://priv.au/search?q={target}&format=json&categories=general",
|
||||||
|
],
|
||||||
|
health_check_url="https://search.sapti.me", reliability_score=3, is_volatile=True),
|
||||||
|
|
||||||
_base("cve_search", "vulns",
|
_base("cve_search", "vulns",
|
||||||
"https://cve.circl.lu/api/cve/{target}", "GET",
|
"https://cve.circl.lu/api/cve/{target}", "GET",
|
||||||
@@ -474,28 +506,45 @@ FREE_PUBLIC_SOURCES: List[SourceConfig] = [
|
|||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="https://packetstormsecurity.com", reliability_score=4),
|
health_check_url="https://packetstormsecurity.com", reliability_score=4),
|
||||||
|
|
||||||
_base("checkleaked", "breaches",
|
_base("proxynova_comb", "breaches",
|
||||||
"https://api.checkleaked.cc/check/{target}", "GET",
|
"https://api.proxynova.com/comb?query={target}", "GET",
|
||||||
{"found": "$.found"},
|
{"lines": "$.lines"},
|
||||||
input_type="email", output_type=["email"],
|
input_type="email", output_type=["email"],
|
||||||
|
normalization_map={"lines": "credential_line"},
|
||||||
tags=["passive", "stealth"],
|
tags=["passive", "stealth"],
|
||||||
health_check_url="https://api.checkleaked.cc", reliability_score=2, is_volatile=True,
|
health_check_url="https://api.proxynova.com", reliability_score=3, is_volatile=True),
|
||||||
backup_endpoints=["https://checkleaked.cc/api/check/{target}"]),
|
|
||||||
|
|
||||||
_base("scylla_sh_search", "breaches",
|
|
||||||
"https://scylla.sh/search?q={target}", "GET",
|
|
||||||
{"results": "$.*"},
|
|
||||||
input_type="email", output_type=["email", "domain"],
|
|
||||||
tags=["passive", "stealth"],
|
|
||||||
health_check_url="https://scylla.sh", reliability_score=2, is_volatile=True,
|
|
||||||
backup_endpoints=["https://scylla.sh/api/search?q={target}"]),
|
|
||||||
|
|
||||||
_base("vigilante_pw", "breaches",
|
_base("shodan_internetdb", "scanners",
|
||||||
"https://vigilante.pw/api/search?q={target}", "GET",
|
"https://internetdb.shodan.io/{target}", "GET",
|
||||||
{"results": "$.results"},
|
{"hostnames": "$.hostnames", "ports": "$.ports", "vulns": "$.vulns"},
|
||||||
input_type="email", output_type=["email"],
|
input_type="ip", output_type=["domain", "ip"],
|
||||||
tags=["passive", "stealth"],
|
normalization_map={"hostnames": "domain", "vulns": "cve"},
|
||||||
health_check_url="https://vigilante.pw", reliability_score=2, is_volatile=True),
|
tags=["passive", "fast", "infrastructure"],
|
||||||
|
health_check_url="https://internetdb.shodan.io",
|
||||||
|
reliability_score=5),
|
||||||
|
|
||||||
|
_base("circl_hashlookup", "hashes",
|
||||||
|
"https://hashlookup.circl.lu/lookup/md5/{target}", "GET",
|
||||||
|
{"filename": "$.FileName", "known_malicious": "$.KnownMalicious"},
|
||||||
|
input_type="hash", output_type=["hash"],
|
||||||
|
normalization_map={"FileName": "filename", "MD5": "hash_md5"},
|
||||||
|
tags=["passive", "fast"],
|
||||||
|
health_check_url="https://hashlookup.circl.lu",
|
||||||
|
reliability_score=5,
|
||||||
|
backup_endpoints=[
|
||||||
|
"https://hashlookup.circl.lu/lookup/sha1/{target}",
|
||||||
|
"https://hashlookup.circl.lu/lookup/sha256/{target}",
|
||||||
|
]),
|
||||||
|
|
||||||
|
_base("ipapi_is", "geolocation",
|
||||||
|
"https://api.ipapi.is/?q={target}", "GET",
|
||||||
|
{"org": "$.org", "asn": "$.asn.asn", "abuse": "$.abuse.email"},
|
||||||
|
input_type="ip", output_type=["domain"],
|
||||||
|
normalization_map={"org": "asn_org", "asn": "asn_number", "abuse": "abuse_contact"},
|
||||||
|
tags=["passive", "fast"],
|
||||||
|
health_check_url="https://api.ipapi.is",
|
||||||
|
reliability_score=4),
|
||||||
]
|
]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -583,27 +632,10 @@ AUTHENTICATED_PREMIUM_SOURCES: List[SourceConfig] = [
|
|||||||
{"results": "$.results"},
|
{"results": "$.results"},
|
||||||
api_key_slots=["{FOFA_API_KEY}", "{FOFA_EMAIL}"],
|
api_key_slots=["{FOFA_API_KEY}", "{FOFA_EMAIL}"],
|
||||||
input_type="domain", output_type=["ip", "domain"],
|
input_type="domain", output_type=["ip", "domain"],
|
||||||
|
query_transform="fofa_domain",
|
||||||
tags=["passive", "infrastructure"],
|
tags=["passive", "infrastructure"],
|
||||||
health_check_url="https://fofa.info", reliability_score=4),
|
health_check_url="https://fofa.info", reliability_score=4),
|
||||||
|
|
||||||
_auth("spyse_domain", "scanners",
|
|
||||||
"https://api.spyse.com/v1/domain/details/{target}", "GET",
|
|
||||||
{"asn": "$.data.asn"},
|
|
||||||
headers={"Authorization": "Bearer {SPYSE_API_KEY}"},
|
|
||||||
api_key_slots=["{SPYSE_API_KEY}"],
|
|
||||||
input_type="domain", output_type=["ip"],
|
|
||||||
tags=["passive"],
|
|
||||||
health_check_url="https://api.spyse.com", reliability_score=3),
|
|
||||||
|
|
||||||
_auth("spyse_ip", "scanners",
|
|
||||||
"https://api.spyse.com/v1/ip/details/{target}", "GET",
|
|
||||||
{"geo": "$.data.geo"},
|
|
||||||
headers={"Authorization": "Bearer {SPYSE_API_KEY}"},
|
|
||||||
api_key_slots=["{SPYSE_API_KEY}"],
|
|
||||||
input_type="ip", output_type=["ip"],
|
|
||||||
tags=["passive"],
|
|
||||||
health_check_url="https://api.spyse.com", reliability_score=3),
|
|
||||||
|
|
||||||
_auth("onyphe_datascan", "scanners",
|
_auth("onyphe_datascan", "scanners",
|
||||||
"https://www.onyphe.io/api/v2/simple/datascan/{target}", "GET",
|
"https://www.onyphe.io/api/v2/simple/datascan/{target}", "GET",
|
||||||
{"results": "$.results"},
|
{"results": "$.results"},
|
||||||
@@ -736,11 +768,11 @@ AUTHENTICATED_PREMIUM_SOURCES: List[SourceConfig] = [
|
|||||||
_auth("threatconnect_search", "threat_intel",
|
_auth("threatconnect_search", "threat_intel",
|
||||||
"https://api.threatconnect.com/v2/indicators/{target}", "GET",
|
"https://api.threatconnect.com/v2/indicators/{target}", "GET",
|
||||||
{"data": "$.data"},
|
{"data": "$.data"},
|
||||||
headers={"Authorization": "TC {TC_API_KEY}:{TC_SIGNATURE}"},
|
headers={"Authorization": "TC {TC_API_KEY}"},
|
||||||
api_key_slots=["{TC_API_KEY}"],
|
api_key_slots=["{TC_API_KEY}"],
|
||||||
input_type="any", output_type=["ip", "domain"],
|
input_type="any", output_type=["ip", "domain"],
|
||||||
tags=["passive", "threat"],
|
tags=["passive", "threat"],
|
||||||
health_check_url="https://api.threatconnect.com", reliability_score=4),
|
health_check_url="https://api.threatconnect.com", reliability_score=2, is_volatile=True),
|
||||||
|
|
||||||
_auth("threatportal", "threat_intel",
|
_auth("threatportal", "threat_intel",
|
||||||
"https://threatportal.io/api/v1/search?q={target}", "GET",
|
"https://threatportal.io/api/v1/search?q={target}", "GET",
|
||||||
@@ -801,11 +833,11 @@ AUTHENTICATED_PREMIUM_SOURCES: List[SourceConfig] = [
|
|||||||
"{MISP_URL}/attributes/restSearch", "POST",
|
"{MISP_URL}/attributes/restSearch", "POST",
|
||||||
{"attributes": "$.Attribute[*].value"},
|
{"attributes": "$.Attribute[*].value"},
|
||||||
headers={"Authorization": "{MISP_API_KEY}", "Content-Type": "application/json"},
|
headers={"Authorization": "{MISP_API_KEY}", "Content-Type": "application/json"},
|
||||||
api_key_slots=["{MISP_API_KEY}"],
|
api_key_slots=["{MISP_API_KEY}", "{MISP_URL}"],
|
||||||
input_type="any", output_type=["ip", "domain", "hash"],
|
input_type="any", output_type=["ip", "domain", "hash"],
|
||||||
payload_template={"returnFormat": "json", "value": "{target}"},
|
payload_template={"returnFormat": "json", "value": "{target}"},
|
||||||
tags=["passive", "threat"],
|
tags=["passive", "threat"],
|
||||||
health_check_url="{MISP_URL}", reliability_score=4),
|
health_check_url="https://misp.local", reliability_score=4),
|
||||||
]
|
]
|
||||||
|
|
||||||
AUTHENTICATED_PREMIUM_SOURCES += [
|
AUTHENTICATED_PREMIUM_SOURCES += [
|
||||||
@@ -824,7 +856,7 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
health_check_url="https://haveibeenpwned.com", reliability_score=5),
|
health_check_url="https://haveibeenpwned.com", reliability_score=5),
|
||||||
|
|
||||||
_auth("dehashed", "breaches",
|
_auth("dehashed", "breaches",
|
||||||
"https://api.dehashed.com/search?query={target}", "GET",
|
"https://api.dehashed.com/v2/search?query={target}", "GET",
|
||||||
{"entries": "$.entries"},
|
{"entries": "$.entries"},
|
||||||
headers={"Authorization": "Basic {DEHASHED_AUTH_BASE64}", "Accept": "application/json"},
|
headers={"Authorization": "Basic {DEHASHED_AUTH_BASE64}", "Accept": "application/json"},
|
||||||
api_key_slots=["{DEHASHED_AUTH_BASE64}"],
|
api_key_slots=["{DEHASHED_AUTH_BASE64}"],
|
||||||
@@ -856,6 +888,10 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
payload_template={"term": "{target}", "buckets": [], "lookuplevel": 0,
|
payload_template={"term": "{target}", "buckets": [], "lookuplevel": 0,
|
||||||
"maxresults": 100, "timeout": 0, "datefrom": "", "dateto": "",
|
"maxresults": 100, "timeout": 0, "datefrom": "", "dateto": "",
|
||||||
"sort": 4, "media": 0, "terminate": []},
|
"sort": 4, "media": 0, "terminate": []},
|
||||||
|
poll_endpoint="https://2.intelx.io/intelligent/search/result",
|
||||||
|
poll_id_field="id",
|
||||||
|
poll_id_param="id",
|
||||||
|
poll_json_root="records",
|
||||||
tags=["passive", "stealth"],
|
tags=["passive", "stealth"],
|
||||||
health_check_url="https://2.intelx.io", reliability_score=5),
|
health_check_url="https://2.intelx.io", reliability_score=5),
|
||||||
|
|
||||||
@@ -879,7 +915,7 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
health_check_url="https://leakcheck.io", reliability_score=4),
|
health_check_url="https://leakcheck.io", reliability_score=4),
|
||||||
|
|
||||||
_auth("spycloud_breach", "breaches",
|
_auth("spycloud_breach", "breaches",
|
||||||
"https://api.spycloud.io/enterprise-v2/breach/data/emails/{target}", "GET",
|
"https://api.spycloud.io/enterprise-v2/breach/catalog/emails/{target}", "GET",
|
||||||
{"results": "$.results"},
|
{"results": "$.results"},
|
||||||
headers={"X-API-Key": "{SPYCLOUD_API_KEY}"},
|
headers={"X-API-Key": "{SPYCLOUD_API_KEY}"},
|
||||||
api_key_slots=["{SPYCLOUD_API_KEY}"],
|
api_key_slots=["{SPYCLOUD_API_KEY}"],
|
||||||
@@ -915,41 +951,28 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
tags=["passive", "stealth"],
|
tags=["passive", "stealth"],
|
||||||
health_check_url="https://api.breachaware.com", reliability_score=3, is_volatile=True),
|
health_check_url="https://api.breachaware.com", reliability_score=3, is_volatile=True),
|
||||||
|
|
||||||
_auth("tines_breach", "breaches",
|
_auth("flare_leaksdb", "breaches",
|
||||||
"https://api.tines.com/breaches/{target}", "GET",
|
"https://api.flare.io/leaksdb/v2/credentials/email/{target}", "GET",
|
||||||
{"breaches": "$.breaches"},
|
{"items": "$.items"},
|
||||||
headers={"Authorization": "Bearer {TINES_API_KEY}"},
|
headers={"Authorization": "Bearer {FLARE_API_KEY}"},
|
||||||
api_key_slots=["{TINES_API_KEY}"],
|
api_key_slots=["{FLARE_API_KEY}"],
|
||||||
input_type="email", output_type=["email"],
|
input_type="email", output_type=["email", "username"],
|
||||||
tags=["passive"],
|
normalization_map={"email": "email_address", "username": "username",
|
||||||
health_check_url="https://api.tines.com", reliability_score=3),
|
"password": "plaintext_password", "hash": "password_hash",
|
||||||
|
"source": "breach_name"},
|
||||||
|
tags=["passive", "stealth"],
|
||||||
|
health_check_url="https://api.flare.io", reliability_score=4),
|
||||||
|
|
||||||
_auth("leakstats_pw", "breaches",
|
_auth("leak_lookup", "breaches",
|
||||||
"https://leakstats.net/api/password/{target}", "GET",
|
|
||||||
{"count": "$.count"},
|
|
||||||
headers={"api-key": "{LEAKSTATS_API_KEY}"},
|
|
||||||
api_key_slots=["{LEAKSTATS_API_KEY}"],
|
|
||||||
input_type="hash", output_type=["hash"],
|
|
||||||
tags=["passive"],
|
|
||||||
health_check_url="https://leakstats.net", reliability_score=3, is_volatile=True),
|
|
||||||
|
|
||||||
_base("leak_lookup", "breaches",
|
|
||||||
"https://leak-lookup.com/api/search", "POST",
|
"https://leak-lookup.com/api/search", "POST",
|
||||||
{"results": "$.message"},
|
{"results": "$.message"},
|
||||||
|
headers={"X-API-Key": "{LEAK_LOOKUP_API_KEY}"},
|
||||||
|
api_key_slots=["{LEAK_LOOKUP_API_KEY}"],
|
||||||
input_type="email", output_type=["email"],
|
input_type="email", output_type=["email"],
|
||||||
payload_template={"query": "{target}", "type": "email_address"},
|
payload_template={"query": "{target}", "type": "email_address"},
|
||||||
tags=["passive", "stealth"],
|
tags=["passive", "stealth"],
|
||||||
health_check_url="https://leak-lookup.com", reliability_score=3, is_volatile=True),
|
health_check_url="https://leak-lookup.com", reliability_score=3, is_volatile=True),
|
||||||
|
|
||||||
_auth("cit0day", "breaches",
|
|
||||||
"https://cit0day.in/api/v1/search?query={target}", "GET",
|
|
||||||
{"results": "$.results"},
|
|
||||||
headers={"Authorization": "Bearer {CIT0DAY_API_KEY}"},
|
|
||||||
api_key_slots=["{CIT0DAY_API_KEY}"],
|
|
||||||
input_type="email", output_type=["email"],
|
|
||||||
tags=["passive", "stealth"],
|
|
||||||
health_check_url="https://cit0day.in", reliability_score=2, is_volatile=True),
|
|
||||||
|
|
||||||
# ── DNS Recon ─────────────────────────────────────────────────────────────
|
# ── DNS Recon ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_auth("securitytrails_sub", "dns_recon",
|
_auth("securitytrails_sub", "dns_recon",
|
||||||
@@ -1003,7 +1026,8 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
api_key_slots=["{SPYONWEB_API_KEY}"],
|
api_key_slots=["{SPYONWEB_API_KEY}"],
|
||||||
input_type="domain", output_type=["domain"],
|
input_type="domain", output_type=["domain"],
|
||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="https://api.spyonweb.com", reliability_score=3),
|
health_check_url="https://api.spyonweb.com", reliability_score=1,
|
||||||
|
is_volatile=True, confidence=0.1),
|
||||||
|
|
||||||
# ── WHOIS ─────────────────────────────────────────────────────────────────
|
# ── WHOIS ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -1053,15 +1077,17 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
|
|
||||||
# ── Enrichment ────────────────────────────────────────────────────────────
|
# ── Enrichment ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_auth("clearbit_enrich", "enrichment",
|
_auth("seon_email", "enrichment",
|
||||||
"https://person.clearbit.com/v2/people/find?email={target}", "GET",
|
"https://api.seon.io/SeonRestService/email-api/v3", "POST",
|
||||||
{"full_name": "$.name.fullName"},
|
{"data": "$.data"},
|
||||||
headers={"Authorization": "Bearer {CLEARBIT_API_KEY}"},
|
headers={"X-API-KEY": "{SEON_API_KEY}", "Content-Type": "application/json"},
|
||||||
api_key_slots=["{CLEARBIT_API_KEY}"],
|
api_key_slots=["{SEON_API_KEY}"],
|
||||||
input_type="email", output_type=["username", "domain"],
|
input_type="email", output_type=["email", "domain", "username"],
|
||||||
normalization_map={"fullName": "full_name"},
|
payload_template={"email": "{target}"},
|
||||||
|
normalization_map={"email": "email_address", "domain": "domain",
|
||||||
|
"full_name": "full_name", "phone_number": "phone"},
|
||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="https://person.clearbit.com", reliability_score=4),
|
health_check_url="https://api.seon.io", reliability_score=4),
|
||||||
|
|
||||||
_auth("fullcontact", "enrichment",
|
_auth("fullcontact", "enrichment",
|
||||||
"https://api.fullcontact.com/v3/person.enrich", "POST",
|
"https://api.fullcontact.com/v3/person.enrich", "POST",
|
||||||
@@ -1088,7 +1114,8 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
api_key_slots=["{PIPL_API_KEY}"],
|
api_key_slots=["{PIPL_API_KEY}"],
|
||||||
input_type="email", output_type=["username", "domain", "phone"],
|
input_type="email", output_type=["username", "domain", "phone"],
|
||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="https://api.pipl.com", reliability_score=4),
|
health_check_url="https://api.pipl.com", reliability_score=2,
|
||||||
|
is_volatile=True, confidence=0.3),
|
||||||
|
|
||||||
# ── Email Reputation ──────────────────────────────────────────────────────
|
# ── Email Reputation ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -1127,12 +1154,12 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
health_check_url="https://api.hunter.io", reliability_score=4),
|
health_check_url="https://api.hunter.io", reliability_score=4),
|
||||||
|
|
||||||
_auth("mailboxlayer", "email_rep",
|
_auth("mailboxlayer", "email_rep",
|
||||||
"http://apilayer.net/api/check?access_key={MAILBOX_API_KEY}&email={target}", "GET",
|
"https://apilayer.net/api/check?access_key={MAILBOX_API_KEY}&email={target}", "GET",
|
||||||
{"score": "$.score"},
|
{"score": "$.score"},
|
||||||
api_key_slots=["{MAILBOX_API_KEY}"],
|
api_key_slots=["{MAILBOX_API_KEY}"],
|
||||||
input_type="email", output_type=["email"],
|
input_type="email", output_type=["email"],
|
||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="http://apilayer.net", reliability_score=3),
|
health_check_url="https://apilayer.net", reliability_score=3),
|
||||||
|
|
||||||
_auth("abstract_email", "email_rep",
|
_auth("abstract_email", "email_rep",
|
||||||
"https://emailvalidation.abstractapi.com/v1/?api_key={ABSTRACT_API_KEY}&email={target}", "GET",
|
"https://emailvalidation.abstractapi.com/v1/?api_key={ABSTRACT_API_KEY}&email={target}", "GET",
|
||||||
@@ -1160,7 +1187,8 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
api_key_slots=["{TWITTER_BEARER_TOKEN}"],
|
api_key_slots=["{TWITTER_BEARER_TOKEN}"],
|
||||||
input_type="username", output_type=["username"],
|
input_type="username", output_type=["username"],
|
||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="https://api.twitter.com", reliability_score=4),
|
health_check_url="https://api.twitter.com", reliability_score=1,
|
||||||
|
is_volatile=True, confidence=0.1),
|
||||||
|
|
||||||
_auth("github_code_search", "code",
|
_auth("github_code_search", "code",
|
||||||
"https://api.github.com/search/code?q={target}", "GET",
|
"https://api.github.com/search/code?q={target}", "GET",
|
||||||
@@ -1183,13 +1211,13 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
# ── Geolocation ───────────────────────────────────────────────────────────
|
# ── Geolocation ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_auth("ipstack", "geolocation",
|
_auth("ipstack", "geolocation",
|
||||||
"http://api.ipstack.com/{target}?access_key={IPSTACK_API_KEY}", "GET",
|
"https://api.ipstack.com/{target}?access_key={IPSTACK_API_KEY}", "GET",
|
||||||
{"country": "$.country_name"},
|
{"country": "$.country_name"},
|
||||||
api_key_slots=["{IPSTACK_API_KEY}"],
|
api_key_slots=["{IPSTACK_API_KEY}"],
|
||||||
input_type="ip", output_type=["ip"],
|
input_type="ip", output_type=["ip"],
|
||||||
normalization_map={"country_name": "geo_country"},
|
normalization_map={"country_name": "geo_country"},
|
||||||
tags=["passive", "fast"],
|
tags=["passive", "fast"],
|
||||||
health_check_url="http://api.ipstack.com", reliability_score=4),
|
health_check_url="https://api.ipstack.com", reliability_score=4),
|
||||||
|
|
||||||
_auth("ipgeolocation_io", "geolocation",
|
_auth("ipgeolocation_io", "geolocation",
|
||||||
"https://api.ipgeolocation.io/ipgeo?apiKey={IPGEO_API_KEY}&ip={target}", "GET",
|
"https://api.ipgeolocation.io/ipgeo?apiKey={IPGEO_API_KEY}&ip={target}", "GET",
|
||||||
@@ -1218,34 +1246,24 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
health_check_url="https://extreme-ip-lookup.com", reliability_score=3),
|
health_check_url="https://extreme-ip-lookup.com", reliability_score=3),
|
||||||
|
|
||||||
_auth("ipinfodb", "geolocation",
|
_auth("ipinfodb", "geolocation",
|
||||||
"http://api.ipinfodb.com/v3/ip-city/?key={IPINFODB_API_KEY}&ip={target}&format=json", "GET",
|
"https://api.ipinfodb.com/v3/ip-city/?key={IPINFODB_API_KEY}&ip={target}&format=json", "GET",
|
||||||
{"city": "$.cityName"},
|
{"city": "$.cityName"},
|
||||||
api_key_slots=["{IPINFODB_API_KEY}"],
|
api_key_slots=["{IPINFODB_API_KEY}"],
|
||||||
input_type="ip", output_type=["ip"],
|
input_type="ip", output_type=["ip"],
|
||||||
normalization_map={"cityName": "geo_city"},
|
normalization_map={"cityName": "geo_city"},
|
||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="http://api.ipinfodb.com", reliability_score=3),
|
health_check_url="https://api.ipinfodb.com", reliability_score=3),
|
||||||
|
|
||||||
# ── Phone ─────────────────────────────────────────────────────────────────
|
# ── Phone ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_auth("numverify", "phone",
|
_auth("numverify", "phone",
|
||||||
"http://apilayer.net/api/validate?access_key={NUMVERIFY_API_KEY}&number={target}", "GET",
|
"https://apilayer.net/api/validate?access_key={NUMVERIFY_API_KEY}&number={target}", "GET",
|
||||||
{"valid": "$.valid", "carrier": "$.carrier"},
|
{"valid": "$.valid", "carrier": "$.carrier"},
|
||||||
api_key_slots=["{NUMVERIFY_API_KEY}"],
|
api_key_slots=["{NUMVERIFY_API_KEY}"],
|
||||||
input_type="phone", output_type=["phone"],
|
input_type="phone", output_type=["phone"],
|
||||||
normalization_map={"valid": "phone_valid", "carrier": "phone_carrier"},
|
normalization_map={"valid": "phone_valid", "carrier": "phone_carrier"},
|
||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="http://apilayer.net", reliability_score=4),
|
health_check_url="https://apilayer.net", reliability_score=4),
|
||||||
|
|
||||||
# ── Hashes ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
_auth("hashes_org", "hashes",
|
|
||||||
"https://hashes.org/api.php?key={HASHES_API_KEY}&query={target}", "GET",
|
|
||||||
{"found": "$.results"},
|
|
||||||
api_key_slots=["{HASHES_API_KEY}"],
|
|
||||||
input_type="hash", output_type=["hash"],
|
|
||||||
tags=["passive"],
|
|
||||||
health_check_url="https://hashes.org", reliability_score=3),
|
|
||||||
|
|
||||||
# ── Search ────────────────────────────────────────────────────────────────
|
# ── Search ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -1265,6 +1283,65 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
input_type="any", output_type=["url"],
|
input_type="any", output_type=["url"],
|
||||||
tags=["passive"],
|
tags=["passive"],
|
||||||
health_check_url="https://api.bing.microsoft.com", reliability_score=5),
|
health_check_url="https://api.bing.microsoft.com", reliability_score=5),
|
||||||
|
|
||||||
|
|
||||||
|
_auth("threatfox", "threat_intel",
|
||||||
|
"https://threatfox-api.abuse.ch/api/v1/", "POST",
|
||||||
|
{"results": "$.data"},
|
||||||
|
headers={"API-KEY": "{THREATFOX_API_KEY}", "Content-Type": "application/json"},
|
||||||
|
payload_template={"query": "search_ioc", "search_term": "{target}"},
|
||||||
|
api_key_slots=["{THREATFOX_API_KEY}"],
|
||||||
|
input_type="any", output_type=["ip", "domain", "hash"],
|
||||||
|
normalization_map={"ioc": "indicator", "malware": "malware_family"},
|
||||||
|
tags=["passive", "threat"],
|
||||||
|
health_check_url="https://threatfox-api.abuse.ch",
|
||||||
|
reliability_score=5),
|
||||||
|
|
||||||
|
_auth("urlhaus", "threat_intel",
|
||||||
|
"https://urlhaus-api.abuse.ch/v1/host/", "POST",
|
||||||
|
{"urls": "$.urls"},
|
||||||
|
headers={"Auth-Key": "{URLHAUS_API_KEY}"},
|
||||||
|
payload_template={"host": "{target}"},
|
||||||
|
api_key_slots=["{URLHAUS_API_KEY}"],
|
||||||
|
input_type="domain", output_type=["url", "domain"],
|
||||||
|
normalization_map={"url": "malware_url", "threat": "threat_type"},
|
||||||
|
tags=["passive", "threat"],
|
||||||
|
health_check_url="https://urlhaus-api.abuse.ch",
|
||||||
|
reliability_score=5),
|
||||||
|
|
||||||
|
_auth("malwarebazaar", "hashes",
|
||||||
|
"https://mb-api.abuse.ch/api/v1/", "POST",
|
||||||
|
{"data": "$.data"},
|
||||||
|
headers={"API-KEY": "{MALWAREBAZAAR_API_KEY}"},
|
||||||
|
payload_template={"query": "get_info", "hash": "{target}"},
|
||||||
|
api_key_slots=["{MALWAREBAZAAR_API_KEY}"],
|
||||||
|
input_type="hash", output_type=["hash"],
|
||||||
|
normalization_map={"file_name": "filename", "tags": "tags"},
|
||||||
|
tags=["passive", "threat"],
|
||||||
|
health_check_url="https://mb-api.abuse.ch",
|
||||||
|
reliability_score=5),
|
||||||
|
|
||||||
|
_auth("fullhunt_subdomains", "dns_recon",
|
||||||
|
"https://fullhunt.io/api/v1/domain/{target}/subdomains", "GET",
|
||||||
|
{"hosts": "$.hosts"},
|
||||||
|
headers={"X-API-KEY": "{FULLHUNT_API_KEY}"},
|
||||||
|
api_key_slots=["{FULLHUNT_API_KEY}"],
|
||||||
|
input_type="domain", output_type=["domain", "ip"],
|
||||||
|
normalization_map={"host": "domain"},
|
||||||
|
tags=["passive", "infrastructure"],
|
||||||
|
health_check_url="https://fullhunt.io",
|
||||||
|
reliability_score=4),
|
||||||
|
|
||||||
|
_auth("netlas_search", "scanners",
|
||||||
|
"https://app.netlas.io/api/responses/?q={target}&source_type=include&start=0&fields=", "GET",
|
||||||
|
{"items": "$.items"},
|
||||||
|
headers={"X-API-Key": "{NETLAS_API_KEY}"},
|
||||||
|
api_key_slots=["{NETLAS_API_KEY}"],
|
||||||
|
input_type="ip", output_type=["ip", "domain"],
|
||||||
|
normalization_map={"data.ip": "ip_address", "data.domain": "domain"},
|
||||||
|
tags=["passive", "infrastructure"],
|
||||||
|
health_check_url="https://app.netlas.io",
|
||||||
|
reliability_score=4),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -1273,9 +1350,9 @@ AUTHENTICATED_PREMIUM_SOURCES += [
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def build_nox_sources(output_dir: str = None) -> None:
|
def build_nox_sources(output_dir: str = None) -> None:
|
||||||
# H3: resolve output_dir relative to this script's location, not CWD.
|
# Resolve output_dir relative to this script's location so the command
|
||||||
# This ensures `python /opt/nox-cli/build_sources.py` from any directory
|
# `python /opt/nox-cli/build_sources.py` always writes to the correct
|
||||||
# always writes to /opt/nox-cli/sources/ instead of ./sources/.
|
# package sources/ directory regardless of the working directory.
|
||||||
if output_dir is None:
|
if output_dir is None:
|
||||||
output_dir = str(Path(__file__).resolve().parent / "sources")
|
output_dir = str(Path(__file__).resolve().parent / "sources")
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
.TH NOX\-CLI 1 "2026-03-30" "1.0.0" "NOX Framework"
|
.TH NOX\-CLI 1 "2026-05-06" "1.0.5" "NOX Framework"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
nox-cli \- Advanced Asynchronous Cyber Threat Intelligence Framework
|
nox-cli \- Advanced Asynchronous Cyber Threat Intelligence Framework
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
# Module-level lock for thread-safe proxy env var assignment (Bug 9 fix)
|
|
||||||
_PROXY_ENV_LOCK = threading.Lock()
|
_PROXY_ENV_LOCK = threading.Lock()
|
||||||
import argparse
|
import argparse
|
||||||
import csv
|
import csv
|
||||||
@@ -145,7 +144,13 @@ except Exception:
|
|||||||
VERSION = "1.0.0"
|
VERSION = "1.0.0"
|
||||||
else:
|
else:
|
||||||
VERSION = "1.0.0"
|
VERSION = "1.0.0"
|
||||||
BUILD_DATE = "2026-04-02"
|
if VERSION == "1.0.0":
|
||||||
|
try:
|
||||||
|
import subprocess as _sp2
|
||||||
|
VERSION = _sp2.check_output(["dpkg-query", "-W", "-f=${Version}", "nox-cli"], stderr=_sp2.DEVNULL).decode().strip() or VERSION
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
BUILD_DATE = "2026-05-06"
|
||||||
|
|
||||||
# ── Smart Path Layout ──────────────────────────────────────────────────
|
# ── Smart Path Layout ──────────────────────────────────────────────────
|
||||||
HOME_NOX = Path.home() / ".nox"
|
HOME_NOX = Path.home() / ".nox"
|
||||||
@@ -199,8 +204,8 @@ def initialize_environment() -> None:
|
|||||||
cfg.write(fh)
|
cfg.write(fh)
|
||||||
|
|
||||||
# Smart source discovery: seed ~/.nox/sources/ from package sources/
|
# Smart source discovery: seed ~/.nox/sources/ from package sources/
|
||||||
# B6: only copy if destination is absent — never silently overwrite
|
# Only copies files that are absent — never overwrites user-customised sources.
|
||||||
# user-customised sources. Use --reset-sources to force a full resync.
|
# Use --reset-sources to force a full resync.
|
||||||
candidate = _PKG_ROOT / "sources"
|
candidate = _PKG_ROOT / "sources"
|
||||||
if not candidate.is_dir():
|
if not candidate.is_dir():
|
||||||
candidate = Path("/usr/share/nox-cli/sources")
|
candidate = Path("/usr/share/nox-cli/sources")
|
||||||
@@ -273,7 +278,6 @@ class NoxConfig:
|
|||||||
self.allow_leak = False
|
self.allow_leak = False
|
||||||
self.no_online_crack = False
|
self.no_online_crack = False
|
||||||
self.max_threads = Cfg.CONCURRENCY
|
self.max_threads = Cfg.CONCURRENCY
|
||||||
# A9/I3: pivot control — readable by AvalancheScanner
|
|
||||||
self.no_pivot = False
|
self.no_pivot = False
|
||||||
self.pivot_depth = Cfg.PIVOT_DEPTH
|
self.pivot_depth = Cfg.PIVOT_DEPTH
|
||||||
|
|
||||||
@@ -614,13 +618,17 @@ class Record:
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
def dedup_key(self) -> str:
|
def dedup_key(self) -> str:
|
||||||
"""SHA-256 of normalised email:password for cross-source deduplication."""
|
"""SHA-256 of normalised email:password for cross-source deduplication.
|
||||||
|
When identity fields are absent, source is included to prevent enrichment
|
||||||
|
records from different sources colliding into a single DB entry."""
|
||||||
em = (self.email or self.username or "").lower().strip()
|
em = (self.email or self.username or "").lower().strip()
|
||||||
pw = (self.password or self.password_hash or "").strip()
|
pw = (self.password or self.password_hash or "").strip()
|
||||||
|
if not em and not pw:
|
||||||
|
return hashlib.sha256(f"{self.source}:{em}:{pw}".encode()).hexdigest()
|
||||||
return hashlib.sha256(f"{em}:{pw}".encode()).hexdigest()
|
return hashlib.sha256(f"{em}:{pw}".encode()).hexdigest()
|
||||||
|
|
||||||
def get_fingerprint(self) -> str:
|
def get_fingerprint(self) -> str:
|
||||||
"""Genera un hash univoco per evitare duplicati nel database."""
|
"""Return a SHA-256 fingerprint for cross-source deduplication."""
|
||||||
data_str = f"{self.source}|{self.email}|{self.password}|{self.phone}|{self.address}"
|
data_str = f"{self.source}|{self.email}|{self.password}|{self.phone}|{self.address}"
|
||||||
return hashlib.sha256(data_str.encode()).hexdigest()
|
return hashlib.sha256(data_str.encode()).hexdigest()
|
||||||
|
|
||||||
@@ -642,7 +650,7 @@ class RiskEngine:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def score(record: "Record") -> "Record":
|
def score(record: "Record") -> "Record":
|
||||||
conf = _SRC_CONFIDENCE.get(record.source, 0.5)
|
conf = _SRC_CONFIDENCE.get(record.source, record.source_confidence)
|
||||||
record.source_confidence = conf
|
record.source_confidence = conf
|
||||||
|
|
||||||
dtypes_str = " ".join(record.data_types).lower() if record.data_types else ""
|
dtypes_str = " ".join(record.data_types).lower() if record.data_types else ""
|
||||||
@@ -657,8 +665,8 @@ class RiskEngine:
|
|||||||
pts = 0.0
|
pts = 0.0
|
||||||
if record.password:
|
if record.password:
|
||||||
pts += 60
|
pts += 60
|
||||||
# I5: adjust base points by password complexity
|
# Adjust base points by password complexity.
|
||||||
# Weak passwords (trivially guessable) score lower; strong ones score higher.
|
# Weak passwords score lower; strong ones score higher.
|
||||||
try:
|
try:
|
||||||
_pa_score = PassAnalyzer().analyze(record.password).get("score", 50)
|
_pa_score = PassAnalyzer().analyze(record.password).get("score", 50)
|
||||||
if _pa_score < 30:
|
if _pa_score < 30:
|
||||||
@@ -1063,7 +1071,6 @@ class DatabaseManager:
|
|||||||
iid = row["id"]
|
iid = row["id"]
|
||||||
for pivot_val, count in profile.pivot_count.items():
|
for pivot_val, count in profile.pivot_count.items():
|
||||||
if count > 1:
|
if count > 1:
|
||||||
# I6: use Detect.qtype instead of length heuristic
|
|
||||||
_ptype = Detect.qtype(pivot_val)
|
_ptype = Detect.qtype(pivot_val)
|
||||||
if _ptype not in ("email", "username", "phone", "domain", "ip"):
|
if _ptype not in ("email", "username", "phone", "domain", "ip"):
|
||||||
_ptype = "username"
|
_ptype = "username"
|
||||||
@@ -1184,7 +1191,6 @@ class DatabaseManager:
|
|||||||
iid = row["id"]
|
iid = row["id"]
|
||||||
for pivot_val, count in profile.pivot_count.items():
|
for pivot_val, count in profile.pivot_count.items():
|
||||||
if count > 1:
|
if count > 1:
|
||||||
# I6: use Detect.qtype instead of length heuristic
|
|
||||||
_ptype = Detect.qtype(pivot_val)
|
_ptype = Detect.qtype(pivot_val)
|
||||||
if _ptype not in ("email", "username", "phone", "domain", "ip"):
|
if _ptype not in ("email", "username", "phone", "domain", "ip"):
|
||||||
_ptype = "username"
|
_ptype = "username"
|
||||||
@@ -1508,10 +1514,14 @@ class DB:
|
|||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Stop the background event loop thread and release resources."""
|
"""Stop the background event loop thread and release resources."""
|
||||||
if self._use_async and hasattr(self, "_loop") and self._loop.is_running():
|
if not (self._use_async and hasattr(self, "_loop")):
|
||||||
|
return
|
||||||
|
if self._loop.is_running():
|
||||||
self._loop.call_soon_threadsafe(self._loop.stop)
|
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||||
if hasattr(self, "_loop_thread"):
|
if hasattr(self, "_loop_thread"):
|
||||||
self._loop_thread.join(timeout=5)
|
self._loop_thread.join(timeout=5)
|
||||||
|
if not self._loop.is_closed():
|
||||||
|
self._loop.close()
|
||||||
|
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -1534,24 +1544,47 @@ def _build_ssl_context() -> ssl.SSLContext:
|
|||||||
ctx.set_ciphers(Cfg.TLS_CIPHERS)
|
ctx.set_ciphers(Cfg.TLS_CIPHERS)
|
||||||
ctx.check_hostname = True
|
ctx.check_hostname = True
|
||||||
ctx.verify_mode = ssl.CERT_REQUIRED
|
ctx.verify_mode = ssl.CERT_REQUIRED
|
||||||
|
try:
|
||||||
|
import certifi
|
||||||
|
ctx.load_verify_locations(certifi.where())
|
||||||
|
except Exception:
|
||||||
|
ctx.load_default_certs()
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
_SSL_CTX = _build_ssl_context()
|
_SSL_CTX = _build_ssl_context()
|
||||||
|
|
||||||
|
# SearXNG public instance pool — used by DorkingEngine and ScrapeEngine.
|
||||||
|
# Instances are rotated randomly; proxy rotation distributes load across IPs.
|
||||||
|
_SEARX_INSTANCES = [
|
||||||
|
"https://searx.tiekoetter.com",
|
||||||
|
"https://search.sapti.me",
|
||||||
|
"https://searx.perennialte.ch",
|
||||||
|
"https://search.mdosch.de",
|
||||||
|
"https://paulgo.io",
|
||||||
|
"https://priv.au",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# ── Header randomisation helpers ──────────────────────────────────────
|
# ── Header randomisation helpers ──────────────────────────────────────
|
||||||
_UA_POOL = [
|
_UA_POOL = [
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
||||||
"Mozilla/5.0 (Windows NT 10.0; rv:133.0) Gecko/20100101 Firefox/133.0",
|
"Mozilla/5.0 (Windows NT 10.0; rv:136.0) Gecko/20100101 Firefox/136.0",
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0",
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:136.0) Gecko/20100101 Firefox/136.0",
|
||||||
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0",
|
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0",
|
||||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Mobile/15E148 Safari/604.1",
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Mobile/15E148 Safari/604.1",
|
||||||
"Mozilla/5.0 (Android 15; Mobile; rv:133.0) Gecko/133.0 Firefox/133.0",
|
"Mozilla/5.0 (Android 15; Mobile; rv:136.0) Gecko/136.0 Firefox/136.0",
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0",
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15",
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Safari/605.1.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
_CH_UA_MAP = [
|
||||||
|
# Order matters: more specific patterns first
|
||||||
|
("Edg/135", '"Microsoft Edge";v="135", "Not-A.Brand";v="8", "Chromium";v="135"'),
|
||||||
|
("Chrome/135", '"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"'),
|
||||||
|
("Chrome/134", '"Google Chrome";v="134", "Not-A.Brand";v="8", "Chromium";v="134"'),
|
||||||
]
|
]
|
||||||
|
|
||||||
_ACCEPT_LANG_POOL = [
|
_ACCEPT_LANG_POOL = [
|
||||||
@@ -1568,12 +1601,13 @@ _SEC_FETCH_SITE_POOL = ["none", "same-origin", "cross-site", "same-site"]
|
|||||||
|
|
||||||
|
|
||||||
def _random_headers(extra: Optional[Dict] = None) -> Dict[str, str]:
|
def _random_headers(extra: Optional[Dict] = None) -> Dict[str, str]:
|
||||||
"""Return a randomised, browser-grade header set."""
|
"""Return a randomised, browser-grade header set with Client Hints for Chromium UAs."""
|
||||||
|
ua = random.choice(_UA_POOL)
|
||||||
h = {
|
h = {
|
||||||
"User-Agent": random.choice(_UA_POOL),
|
"User-Agent": ua,
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
"Accept-Language": random.choice(_ACCEPT_LANG_POOL),
|
"Accept-Language": random.choice(_ACCEPT_LANG_POOL),
|
||||||
"Accept-Encoding": "gzip, deflate, br",
|
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||||
"DNT": "1",
|
"DNT": "1",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"Upgrade-Insecure-Requests": "1",
|
"Upgrade-Insecure-Requests": "1",
|
||||||
@@ -1584,6 +1618,19 @@ def _random_headers(extra: Optional[Dict] = None) -> Dict[str, str]:
|
|||||||
}
|
}
|
||||||
if extra:
|
if extra:
|
||||||
h.update(extra)
|
h.update(extra)
|
||||||
|
# Derive the final UA after applying overrides so that a Firefox UA passed
|
||||||
|
# via `extra` correctly suppresses Chromium-only Sec-CH-UA headers.
|
||||||
|
final_ua = h["User-Agent"]
|
||||||
|
if "Firefox" not in final_ua:
|
||||||
|
ch_ua = next((v for k, v in _CH_UA_MAP if k in final_ua), None)
|
||||||
|
if ch_ua:
|
||||||
|
h["Sec-CH-UA"] = ch_ua
|
||||||
|
h["Sec-CH-UA-Mobile"] = "?0"
|
||||||
|
h["Sec-CH-UA-Platform"] = (
|
||||||
|
'"Windows"' if "Windows" in final_ua else
|
||||||
|
'"macOS"' if "Mac" in final_ua else
|
||||||
|
'"Linux"'
|
||||||
|
)
|
||||||
return h
|
return h
|
||||||
|
|
||||||
|
|
||||||
@@ -1594,6 +1641,20 @@ async def _jitter(cfg: "NoxConfig") -> None:
|
|||||||
await asyncio.sleep(random.uniform(lo, hi))
|
await asyncio.sleep(random.uniform(lo, hi))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_retry_after(value: str, default: float) -> float:
|
||||||
|
"""Parse a Retry-After header value — handles both integer seconds and HTTP-date strings."""
|
||||||
|
try:
|
||||||
|
return float(int(value))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
from email.utils import parsedate_to_datetime
|
||||||
|
delta = (parsedate_to_datetime(value) - datetime.now(timezone.utc)).total_seconds()
|
||||||
|
return max(0.0, delta)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
# ── Async Source Base ──────────────────────────────────────────────────
|
# ── Async Source Base ──────────────────────────────────────────────────
|
||||||
class AsyncSource(ABC):
|
class AsyncSource(ABC):
|
||||||
"""
|
"""
|
||||||
@@ -1650,6 +1711,7 @@ class AsyncSource(ABC):
|
|||||||
|
|
||||||
def _rec(self, **kw) -> Record:
|
def _rec(self, **kw) -> Record:
|
||||||
kw.setdefault("source", self.name)
|
kw.setdefault("source", self.name)
|
||||||
|
kw.setdefault("source_confidence", getattr(self, "_confidence", 0.5))
|
||||||
sev = kw.pop("severity", Severity.MEDIUM)
|
sev = kw.pop("severity", Severity.MEDIUM)
|
||||||
r = Record(**{k: v for k, v in kw.items() if k in Record.__dataclass_fields__})
|
r = Record(**{k: v for k, v in kw.items() if k in Record.__dataclass_fields__})
|
||||||
r.severity = sev
|
r.severity = sev
|
||||||
@@ -1665,7 +1727,7 @@ class AsyncSource(ABC):
|
|||||||
async with self._sem:
|
async with self._sem:
|
||||||
async with session.get(url, headers=hdrs, timeout=to, ssl=_SSL_CTX) as resp:
|
async with session.get(url, headers=hdrs, timeout=to, ssl=_SSL_CTX) as resp:
|
||||||
if resp.status == 429:
|
if resp.status == 429:
|
||||||
retry_after = int(resp.headers.get("Retry-After", Cfg.RETRY_DELAY * (attempt + 2)))
|
retry_after = _parse_retry_after(resp.headers.get("Retry-After", ""), Cfg.RETRY_DELAY * (attempt + 2))
|
||||||
_syslog.info("RATE_LIMIT source=%s url=%s retry_after=%ds", self.name, url[:80], retry_after)
|
_syslog.info("RATE_LIMIT source=%s url=%s retry_after=%ds", self.name, url[:80], retry_after)
|
||||||
await asyncio.sleep(min(retry_after, 30))
|
await asyncio.sleep(min(retry_after, 30))
|
||||||
continue
|
continue
|
||||||
@@ -1675,12 +1737,12 @@ class AsyncSource(ABC):
|
|||||||
return resp.status, await resp.text(errors="replace"), body
|
return resp.status, await resp.text(errors="replace"), body
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if attempt < Cfg.RETRIES - 1:
|
if attempt < Cfg.RETRIES - 1:
|
||||||
await asyncio.sleep(Cfg.RETRY_DELAY * (attempt + 1))
|
await asyncio.sleep(Cfg.RETRY_DELAY * (2 ** attempt) + random.uniform(0, 1))
|
||||||
continue
|
continue
|
||||||
_syslog.debug("API_FAIL source=%s url=%s error=%s", self.name, url[:80], exc)
|
_syslog.debug("API_FAIL source=%s url=%s error=%s", self.name, url[:80], exc)
|
||||||
return 0, "", b""
|
return 0, "", b""
|
||||||
|
|
||||||
async def _post(self, session: "aiohttp.ClientSession", url: str, json_data: Dict = None, data: Dict = None, headers: Dict = None, timeout: int = None) -> Tuple[int, str, bytes]:
|
async def _post(self, session: "aiohttp.ClientSession", url: str, json_data: Dict = None, data: Dict = None, headers: Dict = None, timeout: int = None, raw_body: str = None) -> Tuple[int, str, bytes]:
|
||||||
"""Perform a POST with jitter and retry logic."""
|
"""Perform a POST with jitter and retry logic."""
|
||||||
await _jitter(self._config)
|
await _jitter(self._config)
|
||||||
to = aiohttp_mod.ClientTimeout(total=timeout or self._config.timeout) if aiohttp_mod else None
|
to = aiohttp_mod.ClientTimeout(total=timeout or self._config.timeout) if aiohttp_mod else None
|
||||||
@@ -1688,13 +1750,24 @@ class AsyncSource(ABC):
|
|||||||
for attempt in range(Cfg.RETRIES):
|
for attempt in range(Cfg.RETRIES):
|
||||||
try:
|
try:
|
||||||
async with self._sem:
|
async with self._sem:
|
||||||
if json_data is not None:
|
if raw_body is not None:
|
||||||
|
async with session.post(url, data=raw_body.encode(), headers=hdrs, timeout=to, ssl=_SSL_CTX) as resp:
|
||||||
|
if resp.status == 429:
|
||||||
|
retry_after = _parse_retry_after(resp.headers.get("Retry-After", ""), Cfg.RETRY_DELAY * (attempt + 2))
|
||||||
|
_syslog.info("RATE_LIMIT source=%s url=%s retry_after=%ds", self.name, url[:80], retry_after)
|
||||||
|
await asyncio.sleep(min(retry_after, 30))
|
||||||
|
continue
|
||||||
|
body = await resp.read()
|
||||||
|
if resp.status >= 400:
|
||||||
|
_syslog.warning("API_ERROR source=%s status=%d url=%s", self.name, resp.status, url[:80])
|
||||||
|
return resp.status, await resp.text(errors="replace"), body
|
||||||
|
elif json_data is not None:
|
||||||
hdrs["Content-Type"] = "application/json"
|
hdrs["Content-Type"] = "application/json"
|
||||||
async with session.post(url, json=json_data, headers=hdrs, timeout=to, ssl=_SSL_CTX) as resp:
|
async with session.post(url, json=json_data, headers=hdrs, timeout=to, ssl=_SSL_CTX) as resp:
|
||||||
if resp.status == 429:
|
if resp.status == 429:
|
||||||
retry_after = int(resp.headers.get("Retry-After", Cfg.RETRY_DELAY * (attempt + 2)))
|
retry_after = _parse_retry_after(resp.headers.get("Retry-After", ""), Cfg.RETRY_DELAY * (attempt + 2))
|
||||||
_syslog.info("RATE_LIMIT source=%s url=%s retry_after=%ds", self.name, url[:80], retry_after)
|
_syslog.info("RATE_LIMIT source=%s url=%s retry_after=%ds", self.name, url[:80], retry_after)
|
||||||
await asyncio.sleep(min(retry_after, Cfg.RETRY_DELAY * (attempt + 2)))
|
await asyncio.sleep(min(retry_after, 30))
|
||||||
continue
|
continue
|
||||||
body = await resp.read()
|
body = await resp.read()
|
||||||
if resp.status >= 400:
|
if resp.status >= 400:
|
||||||
@@ -1703,9 +1776,9 @@ class AsyncSource(ABC):
|
|||||||
else:
|
else:
|
||||||
async with session.post(url, data=data or {}, headers=hdrs, timeout=to, ssl=_SSL_CTX) as resp:
|
async with session.post(url, data=data or {}, headers=hdrs, timeout=to, ssl=_SSL_CTX) as resp:
|
||||||
if resp.status == 429:
|
if resp.status == 429:
|
||||||
retry_after = int(resp.headers.get("Retry-After", Cfg.RETRY_DELAY * (attempt + 2)))
|
retry_after = _parse_retry_after(resp.headers.get("Retry-After", ""), Cfg.RETRY_DELAY * (attempt + 2))
|
||||||
_syslog.info("RATE_LIMIT source=%s url=%s retry_after=%ds", self.name, url[:80], retry_after)
|
_syslog.info("RATE_LIMIT source=%s url=%s retry_after=%ds", self.name, url[:80], retry_after)
|
||||||
await asyncio.sleep(min(retry_after, Cfg.RETRY_DELAY * (attempt + 2)))
|
await asyncio.sleep(min(retry_after, 30))
|
||||||
continue
|
continue
|
||||||
body = await resp.read()
|
body = await resp.read()
|
||||||
if resp.status >= 400:
|
if resp.status >= 400:
|
||||||
@@ -1713,7 +1786,7 @@ class AsyncSource(ABC):
|
|||||||
return resp.status, await resp.text(errors="replace"), body
|
return resp.status, await resp.text(errors="replace"), body
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if attempt < Cfg.RETRIES - 1:
|
if attempt < Cfg.RETRIES - 1:
|
||||||
await asyncio.sleep(Cfg.RETRY_DELAY * (attempt + 1))
|
await asyncio.sleep(Cfg.RETRY_DELAY * (2 ** attempt) + random.uniform(0, 1))
|
||||||
continue
|
continue
|
||||||
_syslog.debug("API_FAIL source=%s url=%s error=%s", self.name, url[:80], exc)
|
_syslog.debug("API_FAIL source=%s url=%s error=%s", self.name, url[:80], exc)
|
||||||
return 0, "", b""
|
return 0, "", b""
|
||||||
@@ -1782,6 +1855,7 @@ class Detect:
|
|||||||
q = q.strip()
|
q = q.strip()
|
||||||
if re.match(r"^[\w.+-]+@[\w-]+\.[\w.]+$", q): return "email"
|
if re.match(r"^[\w.+-]+@[\w-]+\.[\w.]+$", q): return "email"
|
||||||
if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", q) and all(0 <= int(o) <= 255 for o in q.split(".")): return "ip"
|
if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", q) and all(0 <= int(o) <= 255 for o in q.split(".")): return "ip"
|
||||||
|
if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", q): return "username"
|
||||||
if re.match(r"^(\+?\d{1,3}[\s.-]?)?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}$", q): return "phone"
|
if re.match(r"^(\+?\d{1,3}[\s.-]?)?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}$", q): return "phone"
|
||||||
if re.match(r"^[a-fA-F0-9]{32,128}$", q): return "hash"
|
if re.match(r"^[a-fA-F0-9]{32,128}$", q): return "hash"
|
||||||
if re.match(r"^\$2[aby]?\$", q) or re.match(r"^\$argon2", q) or re.match(r"^\$[156]\$", q): return "hash"
|
if re.match(r"^\$2[aby]?\$", q) or re.match(r"^\$argon2", q) or re.match(r"^\$[156]\$", q): return "hash"
|
||||||
@@ -1881,13 +1955,13 @@ class Session:
|
|||||||
data = gzip.decompress(data)
|
data = gzip.decompress(data)
|
||||||
return self._make_response(raw.status, data, dict(raw.headers), raw.url)
|
return self._make_response(raw.status, data, dict(raw.headers), raw.url)
|
||||||
if getattr(r, "status_code", 0) == 429:
|
if getattr(r, "status_code", 0) == 429:
|
||||||
retry_after = int(r.headers.get("Retry-After", Cfg.RETRY_DELAY * (attempt + 2)))
|
retry_after = _parse_retry_after(r.headers.get("Retry-After", ""), Cfg.RETRY_DELAY * (attempt + 2))
|
||||||
time.sleep(min(retry_after, 30))
|
time.sleep(min(retry_after, 30))
|
||||||
continue
|
continue
|
||||||
return r
|
return r
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if attempt < Cfg.RETRIES - 1:
|
if attempt < Cfg.RETRIES - 1:
|
||||||
time.sleep(Cfg.RETRY_DELAY * (attempt + 1))
|
time.sleep(Cfg.RETRY_DELAY * (2 ** attempt) + random.uniform(0, 1))
|
||||||
continue
|
continue
|
||||||
logger.debug("GET fail %s: %s", url, e)
|
logger.debug("GET fail %s: %s", url, e)
|
||||||
return self._null_response(url)
|
return self._null_response(url)
|
||||||
@@ -1905,7 +1979,7 @@ class Session:
|
|||||||
else:
|
else:
|
||||||
r = self._s.post(url, data=data, headers=hdrs, timeout=to)
|
r = self._s.post(url, data=data, headers=hdrs, timeout=to)
|
||||||
if getattr(r, "status_code", 0) == 429:
|
if getattr(r, "status_code", 0) == 429:
|
||||||
retry_after = int(r.headers.get("Retry-After", Cfg.RETRY_DELAY * (attempt + 2)))
|
retry_after = _parse_retry_after(r.headers.get("Retry-After", ""), Cfg.RETRY_DELAY * (attempt + 2))
|
||||||
time.sleep(min(retry_after, 30))
|
time.sleep(min(retry_after, 30))
|
||||||
continue
|
continue
|
||||||
return r
|
return r
|
||||||
@@ -1919,7 +1993,7 @@ class Session:
|
|||||||
return self._make_response(raw.status, rd, dict(raw.headers), raw.url)
|
return self._make_response(raw.status, rd, dict(raw.headers), raw.url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if attempt < Cfg.RETRIES - 1:
|
if attempt < Cfg.RETRIES - 1:
|
||||||
time.sleep(Cfg.RETRY_DELAY * (attempt + 1))
|
time.sleep(Cfg.RETRY_DELAY * (2 ** attempt) + random.uniform(0, 1))
|
||||||
continue
|
continue
|
||||||
logger.debug("POST fail %s: %s", url, e)
|
logger.debug("POST fail %s: %s", url, e)
|
||||||
return self._null_response(url)
|
return self._null_response(url)
|
||||||
@@ -2069,12 +2143,8 @@ class ProxyManager:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
_PROXY_SOURCES = [
|
_PROXY_SOURCES = [
|
||||||
(
|
"https://api.proxyscrape.com/v3/free-proxy-list/get?request=displayproxies&protocol=http&timeout=5000&proxy_format=protocolipport&format=text",
|
||||||
"https://api.proxyscrape.com/v2/"
|
"https://raw.githubusercontent.com/proxifly/free-proxy-list/main/proxies/protocols/http/data.txt",
|
||||||
"?request=displayproxies&protocol=http&timeout=5000"
|
|
||||||
"&country=all&ssl=all&anonymity=all"
|
|
||||||
),
|
|
||||||
"https://www.proxy-list.download/api/v1/get?type=http&anon=elite",
|
|
||||||
"https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt",
|
"https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2108,9 +2178,9 @@ class ProxyManager:
|
|||||||
"""
|
"""
|
||||||
Test a proxy by requesting https://api.ipify.org.
|
Test a proxy by requesting https://api.ipify.org.
|
||||||
Returns the observed exit IP on success, None on failure.
|
Returns the observed exit IP on success, None on failure.
|
||||||
F1: SOCKS5 proxies are validated via requests+PySocks, not urllib.
|
SOCKS5 proxies are validated via requests+PySocks, not urllib.
|
||||||
"""
|
"""
|
||||||
# F1: urllib.ProxyHandler does not support SOCKS5 — use requests if available
|
# urllib.ProxyHandler does not support SOCKS5 — use requests if available
|
||||||
if proxy.startswith("socks5") or proxy.startswith("socks4"):
|
if proxy.startswith("socks5") or proxy.startswith("socks4"):
|
||||||
try:
|
try:
|
||||||
import requests as _req # type: ignore
|
import requests as _req # type: ignore
|
||||||
@@ -2188,9 +2258,12 @@ class DorkingEngine(Src):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self._dead_proxies: set = set()
|
self._dead_proxies: set = set()
|
||||||
self._proxy_index: int = 0
|
self._proxy_index: int = 0
|
||||||
self.proxies = ProxyManager.get_proxies()
|
self.proxies: list = []
|
||||||
|
self._dead_instances: set = set()
|
||||||
|
|
||||||
def _get_next_proxy(self) -> Optional[str]:
|
def _get_next_proxy(self) -> Optional[str]:
|
||||||
|
if not self.proxies:
|
||||||
|
self.proxies = ProxyManager.get_proxies()
|
||||||
live = [p for p in self.proxies if p not in self._dead_proxies]
|
live = [p for p in self.proxies if p not in self._dead_proxies]
|
||||||
if not live:
|
if not live:
|
||||||
return None
|
return None
|
||||||
@@ -2251,41 +2324,41 @@ class DorkingEngine(Src):
|
|||||||
return meta
|
return meta
|
||||||
|
|
||||||
async def _ddg_search(self, query: str, _session=None) -> List[dict]:
|
async def _ddg_search(self, query: str, _session=None) -> List[dict]:
|
||||||
"""DDG search with proxy rotation and circuit-breaker (max 3 retries)."""
|
"""DDG HTML is bot-blocked since 2025. Use SearXNG public JSON API."""
|
||||||
if not aiohttp_mod:
|
if not aiohttp_mod:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
from aiohttp_socks import ProxyConnector as _ProxyConnector
|
from aiohttp_socks import ProxyConnector as _ProxyConnector
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_ProxyConnector = None
|
_ProxyConnector = None
|
||||||
url = f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}"
|
live_instances = [i for i in _SEARX_INSTANCES if i not in self._dead_instances]
|
||||||
for attempt in range(3):
|
if not live_instances:
|
||||||
|
self._dead_instances.clear()
|
||||||
|
live_instances = list(_SEARX_INSTANCES)
|
||||||
|
instance = random.choice(live_instances)
|
||||||
|
url = f"{instance}/search?q={urllib.parse.quote(query)}&format=json&categories=general"
|
||||||
proxy = self._get_next_proxy()
|
proxy = self._get_next_proxy()
|
||||||
ua = random.choice(_UA_POOL)
|
|
||||||
headers = {"User-Agent": ua}
|
|
||||||
try:
|
try:
|
||||||
if proxy and _ProxyConnector:
|
if proxy and _ProxyConnector:
|
||||||
connector = _ProxyConnector.from_url(proxy)
|
connector = _ProxyConnector.from_url(proxy)
|
||||||
else:
|
else:
|
||||||
connector = aiohttp_mod.TCPConnector(ssl=_SSL_CTX)
|
connector = aiohttp_mod.TCPConnector(ssl=_SSL_CTX)
|
||||||
# Create session once per attempt; close it before the next retry.
|
|
||||||
async with aiohttp_mod.ClientSession(connector=connector) as sess:
|
async with aiohttp_mod.ClientSession(connector=connector) as sess:
|
||||||
async with sess.get(url, headers=headers,
|
async with sess.get(url, headers=_random_headers(),
|
||||||
timeout=aiohttp_mod.ClientTimeout(total=12)) as resp:
|
timeout=aiohttp_mod.ClientTimeout(total=12)) as resp:
|
||||||
if resp.status in (403, 429):
|
if resp.status != 200:
|
||||||
|
self._dead_instances.add(instance)
|
||||||
|
if proxy:
|
||||||
self._dead_proxies.add(proxy)
|
self._dead_proxies.add(proxy)
|
||||||
next_p = self._get_next_proxy()
|
return []
|
||||||
logger.warning("[!] Proxy Ban detected. Rotating to %s...", next_p)
|
data = await resp.json(content_type=None)
|
||||||
continue
|
return [
|
||||||
text = await resp.text(errors="replace")
|
{"url": r.get("url", ""), "title": r.get("title", ""), "dork": query}
|
||||||
hits = []
|
for r in data.get("results", [])[:5]
|
||||||
for m in re.finditer(r'class="result__url"[^>]*>([^<]+)<', text):
|
if r.get("url")
|
||||||
raw = m.group(1).strip()
|
]
|
||||||
if raw:
|
|
||||||
hits.append({"url": raw if raw.startswith("http") else "https://" + raw,
|
|
||||||
"title": "", "dork": query})
|
|
||||||
return hits[:5]
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
self._dead_instances.add(instance)
|
||||||
if proxy:
|
if proxy:
|
||||||
self._dead_proxies.add(proxy)
|
self._dead_proxies.add(proxy)
|
||||||
return []
|
return []
|
||||||
@@ -2387,23 +2460,19 @@ class DorkEngine:
|
|||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed as _as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed as _as_completed
|
||||||
|
|
||||||
def _run_one(dork: str, eng: str) -> List[dict]:
|
def _run_one(dork: str) -> List[dict]:
|
||||||
query = dork.replace("{q}", q)
|
query = dork.replace("{q}", q)
|
||||||
# Per-engine jitter — applied once per (dork, engine) pair, not per dork
|
|
||||||
time.sleep(random.uniform(*Cfg.DORK_DELAY))
|
time.sleep(random.uniform(*Cfg.DORK_DELAY))
|
||||||
hits = self._search(query, eng)
|
hits = self._search(query, "SearXNG")
|
||||||
for h in hits:
|
for h in hits:
|
||||||
h["dork"] = query
|
h["dork"] = query
|
||||||
h["engine"] = eng
|
h["engine"] = "SearXNG"
|
||||||
return hits
|
return hits
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
pairs = [(dork, eng) for dork in dorks for eng in engines]
|
max_workers = min(len(dorks), 12)
|
||||||
if not pairs:
|
|
||||||
return []
|
|
||||||
max_workers = min(len(pairs), 12) # cap threads to avoid hammering search engines
|
|
||||||
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
||||||
futures = {pool.submit(_run_one, d, e): (d, e) for d, e in pairs}
|
futures = {pool.submit(_run_one, d): d for d in dorks}
|
||||||
for fut in _as_completed(futures):
|
for fut in _as_completed(futures):
|
||||||
try:
|
try:
|
||||||
results.extend(fut.result())
|
results.extend(fut.result())
|
||||||
@@ -2422,31 +2491,19 @@ class DorkEngine:
|
|||||||
def _search(self, query: str, engine: str) -> List[dict]:
|
def _search(self, query: str, engine: str) -> List[dict]:
|
||||||
hits = []
|
hits = []
|
||||||
try:
|
try:
|
||||||
urls = {
|
# Direct Google/Bing HTML scraping is blocked by CAPTCHA/consent walls
|
||||||
"google": f"https://www.google.com/search?q={urllib.parse.quote(query)}&num=10",
|
# since 2024. Route all engines through SearXNG JSON API.
|
||||||
"bing": f"https://www.bing.com/search?q={urllib.parse.quote(query)}&count=10",
|
url = f"{random.choice(_SEARX_INSTANCES)}/search?q={urllib.parse.quote(query)}&format=json&categories=general"
|
||||||
"ddg": f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}",
|
resp = self.s.get(url, timeout=15, use_cloudscraper=False)
|
||||||
}
|
if not resp.ok:
|
||||||
resp = self.s.get(urls.get(engine, urls["google"]), timeout=15, use_cloudscraper=True)
|
|
||||||
if not resp.ok or not BeautifulSoup:
|
|
||||||
return hits
|
return hits
|
||||||
soup = BeautifulSoup(resp.text, "html.parser")
|
data = resp.json()
|
||||||
selectors = {
|
for r in data.get("results", [])[:10]:
|
||||||
"google": ("div.g", "h3", "a[href]", ".VwiC3b"),
|
if r.get("url"):
|
||||||
"bing": ("li.b_algo", "h2", "a", ".b_caption p"),
|
|
||||||
"ddg": (".result", ".result__title", ".result__url", ".result__snippet"),
|
|
||||||
}
|
|
||||||
container, title_sel, link_sel, snippet_sel = selectors.get(engine, selectors["google"])
|
|
||||||
for item in soup.select(container)[:10]:
|
|
||||||
title_el = item.select_one(title_sel)
|
|
||||||
link_el = item.select_one(link_sel)
|
|
||||||
snip_el = item.select_one(snippet_sel)
|
|
||||||
if title_el:
|
|
||||||
url = link_el.get("href","") if link_el else ""
|
|
||||||
hits.append({
|
hits.append({
|
||||||
"title": title_el.get_text().strip(),
|
"title": r.get("title", ""),
|
||||||
"url": url if url.startswith("http") else "",
|
"url": r["url"],
|
||||||
"snippet": snip_el.get_text().strip() if snip_el else "",
|
"snippet": r.get("content", ""),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -2458,24 +2515,14 @@ class DorkEngine:
|
|||||||
# =======================================================================
|
# =======================================================================
|
||||||
class ScrapeEngine:
|
class ScrapeEngine:
|
||||||
PASTE_SITES = [
|
PASTE_SITES = [
|
||||||
("Pastebin", "https://psbdmp.ws/api/v3/search/{q}", "json"),
|
# Paste intelligence is routed through SearXNG dorks and IntelX.
|
||||||
("IntelX", "https://2.intelx.io/intelligent/search", "intelx"),
|
("IntelX", "https://2.intelx.io/intelligent/search", "intelx"),
|
||||||
("Paste.ee", "https://api.paste.ee/v1/search?query={q}", "json"),
|
|
||||||
("Rentry", "https://rentry.co/api/search?q={q}", "json"),
|
|
||||||
("Ghostbin", "https://ghostbin.com/api/search?q={q}", "json"),
|
|
||||||
("JustPaste", "https://justpaste.it/api/search?q={q}", "json"),
|
|
||||||
("DPaste", "https://dpaste.org/api/search?q={q}", "json"),
|
|
||||||
("Hastebin", "https://hastebin.com/api/search?q={q}", "json"),
|
|
||||||
("PrivateBin", "https://privatebin.net/api/search?q={q}", "json"),
|
|
||||||
("ControlC", "https://controlc.com/api/search?q={q}", "json"),
|
|
||||||
("Paste2", "https://paste2.org/api/search?q={q}", "json"),
|
|
||||||
("PastebinPro", "https://pastebin.com/api/api_search.php?q={q}", "xml"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
CRED_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+\s*[:;|]\s*\S+", re.IGNORECASE)
|
CRED_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+\s*[:;|]\s*\S+", re.IGNORECASE)
|
||||||
EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")
|
EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")
|
||||||
HASH_RE = re.compile(r"\b[a-f0-9]{32,128}\b", re.IGNORECASE)
|
HASH_RE = re.compile(r"\b[a-f0-9]{32,128}\b", re.IGNORECASE)
|
||||||
COMBO_RE = re.compile(r"^[^:]+:[^:]+$", re.MULTILINE)
|
COMBO_RE = re.compile(r"^[^\n:]+:[^\n:]+$", re.MULTILINE)
|
||||||
|
|
||||||
PATTERNS = [
|
PATTERNS = [
|
||||||
(re.compile(r"(?:password|passwd|pass|pwd)\s*[:=]\s*\S+", re.I), "Password"),
|
(re.compile(r"(?:password|passwd|pass|pwd)\s*[:=]\s*\S+", re.I), "Password"),
|
||||||
@@ -2594,13 +2641,15 @@ class ScrapeEngine:
|
|||||||
}
|
}
|
||||||
for sq in _ddg_queries.get(qt, [f'"{q}" password leak', f'"{q}" database dump']):
|
for sq in _ddg_queries.get(qt, [f'"{q}" password leak', f'"{q}" database dump']):
|
||||||
try:
|
try:
|
||||||
resp = self.s.get(f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(sq)}", timeout=10, use_cloudscraper=True)
|
resp = self.s.get(f"{random.choice(_SEARX_INSTANCES)}/search?q={urllib.parse.quote(sq)}&format=json&categories=general", timeout=10)
|
||||||
if resp.ok and BeautifulSoup:
|
if resp.ok:
|
||||||
soup = BeautifulSoup(resp.text, "html.parser")
|
try:
|
||||||
for r in soup.select(".result")[:5]:
|
data = resp.json()
|
||||||
title_el = r.select_one(".result__title")
|
for r in data.get("results", [])[:5]:
|
||||||
if title_el:
|
if r.get("title"):
|
||||||
results["pastes"].append({"site":"DDG","title":title_el.get_text().strip(),"query":sq})
|
results["pastes"].append({"site": "SearXNG", "title": r["title"], "url": r.get("url", ""), "query": sq})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -2670,18 +2719,19 @@ class ScrapeEngine:
|
|||||||
]
|
]
|
||||||
for dork in dorks:
|
for dork in dorks:
|
||||||
try:
|
try:
|
||||||
resp = self.s.get(f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(dork)}", timeout=10, use_cloudscraper=True)
|
resp = self.s.get(f"{random.choice(_SEARX_INSTANCES)}/search?q={urllib.parse.quote(dork)}&format=json&categories=general", timeout=10)
|
||||||
if resp.ok and BeautifulSoup:
|
if resp.ok:
|
||||||
soup = BeautifulSoup(resp.text, "html.parser")
|
try:
|
||||||
for r in soup.select(".result")[:5]:
|
data = resp.json()
|
||||||
title_el = r.select_one(".result__title")
|
for r in data.get("results", [])[:5]:
|
||||||
url_el = r.select_one(".result__url")
|
if r.get("title"):
|
||||||
if title_el:
|
|
||||||
hits.append({
|
hits.append({
|
||||||
"dork": dork,
|
"dork": dork,
|
||||||
"title": title_el.get_text().strip(),
|
"title": r["title"],
|
||||||
"url": url_el.get_text().strip() if url_el else "",
|
"url": r.get("url", ""),
|
||||||
})
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
time.sleep(random.uniform(2.0, 4.0))
|
time.sleep(random.uniform(2.0, 4.0))
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
@@ -2694,29 +2744,13 @@ class ScrapeEngine:
|
|||||||
data = paste.get("data",{})
|
data = paste.get("data",{})
|
||||||
if not pid:
|
if not pid:
|
||||||
return ""
|
return ""
|
||||||
raw_urls = {
|
raw_urls: dict = {} # paste fetch URLs — resolved per site name
|
||||||
"Pastebin": f"https://psbdmp.ws/api/v3/dump/{pid}",
|
|
||||||
"Rentry": f"https://rentry.co/api/raw/{pid}",
|
|
||||||
"Hastebin": f"https://hastebin.com/raw/{pid}",
|
|
||||||
"DPaste": f"https://dpaste.org/{pid}/raw/",
|
|
||||||
"Ghostbin": f"https://ghostbin.com/paste/{pid}/raw",
|
|
||||||
"JustPaste": f"https://justpaste.it/{pid}",
|
|
||||||
"PrivateBin": f"https://privatebin.net/?{pid}",
|
|
||||||
"ControlC": f"https://controlc.com/{pid}",
|
|
||||||
"Paste2": f"https://paste2.org/raw/{pid}",
|
|
||||||
"PastebinPro":f"https://pastebin.com/raw/{pid}",
|
|
||||||
}
|
|
||||||
if site == "IntelX":
|
if site == "IntelX":
|
||||||
key = self.db.get_key("intelx")
|
key = Vault.get("INTELX_API_KEY")
|
||||||
if key:
|
if key:
|
||||||
resp = self.s.get(f"https://2.intelx.io/file/read?type=1&systemid={pid}&k={key}", timeout=15)
|
resp = self.s.get(f"https://2.intelx.io/file/read?type=1&systemid={pid}&k={key}", timeout=15)
|
||||||
if resp.ok:
|
if resp.ok:
|
||||||
return resp.text[:10000]
|
return resp.text[:10000]
|
||||||
elif site == "Paste.ee":
|
|
||||||
resp = self.s.get(f"https://api.paste.ee/v1/pastes/{pid}", timeout=10)
|
|
||||||
if resp.ok:
|
|
||||||
sections = resp.json().get("paste",{}).get("sections",[])
|
|
||||||
return "\n".join(s.get("contents","") for s in sections)[:10000]
|
|
||||||
elif site in raw_urls:
|
elif site in raw_urls:
|
||||||
resp = self.s.get(raw_urls[site], timeout=10)
|
resp = self.s.get(raw_urls[site], timeout=10)
|
||||||
if resp.ok and resp.text:
|
if resp.ok and resp.text:
|
||||||
@@ -2844,26 +2878,19 @@ class HashEngine:
|
|||||||
return list(set(mutations))
|
return list(set(mutations))
|
||||||
|
|
||||||
def _online(self, h: str) -> Optional[str]:
|
def _online(self, h: str) -> Optional[str]:
|
||||||
apis = [
|
try:
|
||||||
(f"https://www.nitrxgen.net/md5db/{h}", "text"),
|
from sources.helpers.config_handler import ConfigManager # type: ignore
|
||||||
(f"https://hashes.org/api.php?key=&query={h}", "json"),
|
key = ConfigManager.get_key("HASHES_COM_API_KEY")
|
||||||
(f"https://hash.help/api/lookup/{h}", "json"),
|
if not key:
|
||||||
(f"https://hashkiller.io/api/search.php?hash={h}", "json"),
|
return None
|
||||||
]
|
apis = [(f"https://hashes.com/en/api/search?hash={h}&key={key}", "json")]
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
_get = self._session.get if self._session else (lambda url, **kw: Session._null_response(url))
|
_get = self._session.get if self._session else (lambda url, **kw: Session._null_response(url))
|
||||||
for url, fmt in apis:
|
for url, fmt in apis:
|
||||||
try:
|
try:
|
||||||
resp = _get(url, timeout=8)
|
resp = _get(url, timeout=8)
|
||||||
if not resp.ok: continue
|
if not resp.ok: continue
|
||||||
if fmt == "text":
|
|
||||||
text = resp.text.strip()
|
|
||||||
if not text or len(text) >= 100:
|
|
||||||
continue
|
|
||||||
tl = text.lower()
|
|
||||||
if any(tl.startswith(p) for p in ("not found", "error", "invalid", "no result", "not in", "cmd5-error", "not exist", "code erreur", "erreur", "unknown")):
|
|
||||||
continue
|
|
||||||
return text
|
|
||||||
elif fmt == "json":
|
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
if data.get("result") or data.get("plaintext"):
|
if data.get("result") or data.get("plaintext"):
|
||||||
return data.get("result", data.get("plaintext", ""))
|
return data.get("result", data.get("plaintext", ""))
|
||||||
@@ -2873,11 +2900,12 @@ class HashEngine:
|
|||||||
def _hashmob(self, h: str) -> Optional[str]:
|
def _hashmob(self, h: str) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
if not self._session: return None
|
if not self._session: return None
|
||||||
resp = self._session.post("https://hashmob.net/api/v2/search", json_data={"hash":h}, timeout=10)
|
resp = self._session.post("https://hashmob.net/api/v2/search", json_data={"hashes": [h]}, timeout=10)
|
||||||
if resp.ok:
|
if resp.ok:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
if data.get("found") and data.get("result"):
|
results = data.get("data") or []
|
||||||
return data["result"]
|
if isinstance(results, list) and results:
|
||||||
|
return results[0].get("plaintext") or results[0].get("result") or None
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -3140,9 +3168,8 @@ class Orchestrator:
|
|||||||
# ── Fail-Safe Proxy check (transport-level, before any connection) ──
|
# ── Fail-Safe Proxy check (transport-level, before any connection) ──
|
||||||
ProxyManager.fail_safe_check(self.config, allow_leak=self.config.allow_leak)
|
ProxyManager.fail_safe_check(self.config, allow_leak=self.config.allow_leak)
|
||||||
|
|
||||||
# B1: recreate SourceOrchestrator on every call so the new semaphore is
|
# SourceOrchestrator is created once and reused across calls. The semaphore
|
||||||
# propagated to all source instances. Plugin JSON files are cached by
|
# is rebound on each invocation so concurrency limits are always respected.
|
||||||
# SourceOrchestrator._load_nox_sources via the module-level mtime guard (L2).
|
|
||||||
if self._source_orchestrator is None:
|
if self._source_orchestrator is None:
|
||||||
self._source_orchestrator = SourceOrchestrator(
|
self._source_orchestrator = SourceOrchestrator(
|
||||||
self._get_semaphore(), self.db, self.config
|
self._get_semaphore(), self.db, self.config
|
||||||
@@ -3180,7 +3207,7 @@ class Orchestrator:
|
|||||||
return records
|
return records
|
||||||
|
|
||||||
connector = aiohttp_mod.TCPConnector(ssl=_SSL_CTX, limit=self.config.concurrency, family=0) # family=0 → AF_UNSPEC (IPv4+IPv6)
|
connector = aiohttp_mod.TCPConnector(ssl=_SSL_CTX, limit=self.config.concurrency, family=0) # family=0 → AF_UNSPEC (IPv4+IPv6)
|
||||||
# B5: SOCKS5 proxies are not supported via trust_env — use ProxyConnector directly.
|
# SOCKS5 proxies require ProxyConnector — aiohttp trust_env does not support SOCKS5.
|
||||||
_socks5_connector = False
|
_socks5_connector = False
|
||||||
if self.config.proxy and self.config.proxy.startswith("socks5"):
|
if self.config.proxy and self.config.proxy.startswith("socks5"):
|
||||||
try:
|
try:
|
||||||
@@ -3189,8 +3216,8 @@ class Orchestrator:
|
|||||||
_socks5_connector = True
|
_socks5_connector = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning("aiohttp_socks not installed — SOCKS5 proxy bypassed. Install: pip install aiohttp-socks")
|
logger.warning("aiohttp_socks not installed — SOCKS5 proxy bypassed. Install: pip install aiohttp-socks")
|
||||||
# B2: set _proxy_env_set flag immediately after os.environ assignment
|
# Set proxy environment variables for HTTP/S proxies so aiohttp trust_env picks them up.
|
||||||
# Use a module-level lock to prevent concurrent scans from racing on env vars.
|
# A module-level lock prevents concurrent scans from racing on the shared env vars.
|
||||||
_proxy_env_set = False
|
_proxy_env_set = False
|
||||||
if self.config.proxy and not _socks5_connector and not os.environ.get("HTTPS_PROXY"):
|
if self.config.proxy and not _socks5_connector and not os.environ.get("HTTPS_PROXY"):
|
||||||
with _PROXY_ENV_LOCK:
|
with _PROXY_ENV_LOCK:
|
||||||
@@ -3958,7 +3985,6 @@ class AdvancedReporter:
|
|||||||
lines += ["","---",f"## Pivot Tree ({len(pivot_log)} nodes)","",
|
lines += ["","---",f"## Pivot Tree ({len(pivot_log)} nodes)","",
|
||||||
"| Depth | Asset | Type | Found In | Parent | Breach | Dorks | Scrape | Children | Cracked |",
|
"| Depth | Asset | Type | Found In | Parent | Breach | Dorks | Scrape | Children | Cracked |",
|
||||||
"|-------|-------|------|----------|--------|--------|-------|--------|----------|---------|"]
|
"|-------|-------|------|----------|--------|--------|-------|--------|----------|---------|"]
|
||||||
# J4: sort by (depth, parent, asset) for readable depth-first narrative
|
|
||||||
for e in sorted(pivot_log, key=lambda x: (x.get("depth", 0), x.get("parent") or "", x.get("asset", ""))):
|
for e in sorted(pivot_log, key=lambda x: (x.get("depth", 0), x.get("parent") or "", x.get("asset", ""))):
|
||||||
cracked_str = _r(", ".join(e.get("cracked", [])[:3]))
|
cracked_str = _r(", ".join(e.get("cracked", [])[:3]))
|
||||||
children = e.get("children", [])
|
children = e.get("children", [])
|
||||||
@@ -4001,7 +4027,7 @@ class AdvancedReporter:
|
|||||||
else:
|
else:
|
||||||
lines.append("_No pivot assets discovered._")
|
lines.append("_No pivot assets discovered._")
|
||||||
|
|
||||||
with open(path, "w", encoding="utf-8") as fh:
|
with open(path, "w", encoding="utf-8", errors="replace") as fh:
|
||||||
fh.write("\n".join(lines) + "\n")
|
fh.write("\n".join(lines) + "\n")
|
||||||
out("ok", f"Markdown saved: {path}")
|
out("ok", f"Markdown saved: {path}")
|
||||||
|
|
||||||
@@ -4062,7 +4088,6 @@ class Reporter:
|
|||||||
def to_pdf(data: dict, path: str, investigator_id: str = "NOX-AUTO") -> None:
|
def to_pdf(data: dict, path: str, investigator_id: str = "NOX-AUTO") -> None:
|
||||||
path = Reporter._resolve_path(path, "pdf")
|
path = Reporter._resolve_path(path, "pdf")
|
||||||
if _HAS_REPORTING:
|
if _HAS_REPORTING:
|
||||||
# D1: _rep_pdf raises RuntimeError if fpdf2 is missing — let it propagate
|
|
||||||
try:
|
try:
|
||||||
_rep_pdf(data, path, investigator_id=investigator_id)
|
_rep_pdf(data, path, investigator_id=investigator_id)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
@@ -4077,7 +4102,6 @@ class Reporter:
|
|||||||
pass
|
pass
|
||||||
# Fallback: weasyprint HTML→PDF
|
# Fallback: weasyprint HTML→PDF
|
||||||
if not weasyprint:
|
if not weasyprint:
|
||||||
# D1: explicit error — no silent return with no output file
|
|
||||||
out("err", "No PDF library found. Install fpdf2: pip install fpdf2")
|
out("err", "No PDF library found. Install fpdf2: pip install fpdf2")
|
||||||
return
|
return
|
||||||
tmp = tempfile.NamedTemporaryFile(suffix=".html", delete=False)
|
tmp = tempfile.NamedTemporaryFile(suffix=".html", delete=False)
|
||||||
@@ -4187,7 +4211,6 @@ class REPL:
|
|||||||
def _dispatch(self, cmd: str, arg: str) -> None:
|
def _dispatch(self, cmd: str, arg: str) -> None:
|
||||||
if cmd in ("quit","exit","q"):
|
if cmd in ("quit","exit","q"):
|
||||||
out("info", "Exiting.")
|
out("info", "Exiting.")
|
||||||
# B3: flush DB background thread before exit
|
|
||||||
try:
|
try:
|
||||||
self.db.close()
|
self.db.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -4990,8 +5013,6 @@ class REPL:
|
|||||||
elif fmt == "csv":
|
elif fmt == "csv":
|
||||||
resolved = Reporter._resolve_path(path, "csv")
|
resolved = Reporter._resolve_path(path, "csv")
|
||||||
Reporter.to_csv(self._last, resolved)
|
Reporter.to_csv(self._last, resolved)
|
||||||
# G4: derive base from the resolved (absolute) path so companion files
|
|
||||||
# land in REPORT_DIR, not the current working directory
|
|
||||||
self._export_csv_extras(data, resolved)
|
self._export_csv_extras(data, resolved)
|
||||||
elif fmt == "html": Reporter.to_html(data, path)
|
elif fmt == "html": Reporter.to_html(data, path)
|
||||||
elif fmt == "md": Reporter.to_markdown(data, path)
|
elif fmt == "md": Reporter.to_markdown(data, path)
|
||||||
@@ -5102,7 +5123,6 @@ class REPL:
|
|||||||
self.orc.dork_engine.s = self.orc.session
|
self.orc.dork_engine.s = self.orc.session
|
||||||
self.orc.scrape_engine.s = self.orc.session
|
self.orc.scrape_engine.s = self.orc.session
|
||||||
self.orc.hash_engine._session = self.orc.session
|
self.orc.hash_engine._session = self.orc.session
|
||||||
# G2: also rebuild dorking_engine so it picks up the new proxy/Tor config
|
|
||||||
self.orc.dorking_engine = DorkingEngine(self.config.concurrency, self.orc.db, self.config)
|
self.orc.dorking_engine = DorkingEngine(self.config.concurrency, self.orc.db, self.config)
|
||||||
|
|
||||||
# ── Investigation Dashboard ────────────────────────────────────────────
|
# ── Investigation Dashboard ────────────────────────────────────────────
|
||||||
@@ -5567,12 +5587,11 @@ class ConfigManager:
|
|||||||
|
|
||||||
_cache: Dict[str, str] = {}
|
_cache: Dict[str, str] = {}
|
||||||
_INI_PATHS = [HOME_NOX / "config.ini", Path("/etc/nox/config.ini")]
|
_INI_PATHS = [HOME_NOX / "config.ini", Path("/etc/nox/config.ini")]
|
||||||
# B4: track apikeys.json mtime to detect external edits
|
|
||||||
_store_mtime: float = 0.0
|
_store_mtime: float = 0.0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _invalidate_if_changed(cls) -> None:
|
def _invalidate_if_changed(cls) -> None:
|
||||||
"""B4: clear cache if apikeys.json was modified externally."""
|
"""Clear the key cache if apikeys.json was modified externally."""
|
||||||
if not _HAS_CONFIG_HANDLER or _ExtConfigManager is None:
|
if not _HAS_CONFIG_HANDLER or _ExtConfigManager is None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@@ -5820,7 +5839,7 @@ class DeHashEngine:
|
|||||||
return (h, cached)
|
return (h, cached)
|
||||||
try:
|
try:
|
||||||
auth = base64.b64encode(self._key.encode()).decode() if ":" in self._key else self._key
|
auth = base64.b64encode(self._key.encode()).decode() if ":" in self._key else self._key
|
||||||
url = f"https://api.dehashed.com/search?query=hashed_password:{h}&size=1"
|
url = f"https://api.dehashed.com/v2/search?query=hashed_password:{h}&size=1"
|
||||||
hdrs = {"Accept": "application/json", "Authorization": f"Basic {auth}"}
|
hdrs = {"Accept": "application/json", "Authorization": f"Basic {auth}"}
|
||||||
async with sem:
|
async with sem:
|
||||||
to = aiohttp_mod.ClientTimeout(total=self._config.timeout) if aiohttp_mod else None
|
to = aiohttp_mod.ClientTimeout(total=self._config.timeout) if aiohttp_mod else None
|
||||||
@@ -6112,7 +6131,7 @@ class Vault:
|
|||||||
# Synchronous fallback lookup via requests/urllib
|
# Synchronous fallback lookup via requests/urllib
|
||||||
try:
|
try:
|
||||||
auth = base64.b64encode(key.encode()).decode() if ":" in key else key
|
auth = base64.b64encode(key.encode()).decode() if ":" in key else key
|
||||||
url = (f"https://api.dehashed.com/search"
|
url = (f"https://api.dehashed.com/v2/search"
|
||||||
f"?query=hashed_password:{r.password_hash}&size=1")
|
f"?query=hashed_password:{r.password_hash}&size=1")
|
||||||
hdrs = {"Accept": "application/json",
|
hdrs = {"Accept": "application/json",
|
||||||
"Authorization": f"Basic {auth}",
|
"Authorization": f"Basic {auth}",
|
||||||
@@ -6271,8 +6290,12 @@ class FileSystemProvider(AsyncSource):
|
|||||||
records = []
|
records = []
|
||||||
for m in re.finditer(pattern, text):
|
for m in re.finditer(pattern, text):
|
||||||
groups = m.groups()
|
groups = m.groups()
|
||||||
|
val = groups[0] if len(groups) > 0 else ""
|
||||||
|
# Route to username if the value is not an email address
|
||||||
|
is_email = "@" in val
|
||||||
records.append(self._rec(
|
records.append(self._rec(
|
||||||
email = groups[0] if len(groups) > 0 else "",
|
email = val if is_email else "",
|
||||||
|
username = val if not is_email else "",
|
||||||
password = groups[1] if len(groups) > 1 else "",
|
password = groups[1] if len(groups) > 1 else "",
|
||||||
breach_name = self.name,
|
breach_name = self.name,
|
||||||
data_types = [self.name, "Credentials"],
|
data_types = [self.name, "Credentials"],
|
||||||
@@ -6361,11 +6384,28 @@ class NoxSourceProvider(FileSystemProvider):
|
|||||||
|
|
||||||
async def _fetch(self, session, query: str) -> List[Record]:
|
async def _fetch(self, session, query: str) -> List[Record]:
|
||||||
d = self._def
|
d = self._def
|
||||||
|
# Apply optional query transform before URL substitution.
|
||||||
|
# Currently supported: "md5_lower" — MD5-hex of the lowercased, stripped query.
|
||||||
|
transform = d.get("query_transform", "")
|
||||||
|
if transform == "md5_lower":
|
||||||
|
import hashlib as _hl
|
||||||
|
try:
|
||||||
|
effective_query = _hl.md5(query.lower().strip().encode(),
|
||||||
|
usedforsecurity=False).hexdigest()
|
||||||
|
except TypeError:
|
||||||
|
effective_query = _hl.md5(query.lower().strip().encode()).hexdigest()
|
||||||
|
elif transform == "fofa_domain":
|
||||||
|
import base64 as _b64
|
||||||
|
effective_query = _b64.b64encode(
|
||||||
|
f'domain="{query.lower().strip()}"'.encode()
|
||||||
|
).decode()
|
||||||
|
else:
|
||||||
|
effective_query = query
|
||||||
# Headers are already resolved in _load_nox_sources; just substitute {query}
|
# Headers are already resolved in _load_nox_sources; just substitute {query}
|
||||||
hdrs = {k: v.replace("{query}", urllib.parse.quote(query, safe=""))
|
hdrs = {k: v.replace("{query}", urllib.parse.quote(effective_query, safe=""))
|
||||||
for k, v in d.get("headers", {}).items()}
|
for k, v in d.get("headers", {}).items()}
|
||||||
url = (d["api_url"]
|
url = (d["api_url"]
|
||||||
.replace("{query}", urllib.parse.quote(query, safe=""))
|
.replace("{query}", urllib.parse.quote(effective_query, safe=""))
|
||||||
.replace("{api_key}", self._api_key or ""))
|
.replace("{api_key}", self._api_key or ""))
|
||||||
# Also substitute any remaining {KEY_NAME} placeholders in URL
|
# Also substitute any remaining {KEY_NAME} placeholders in URL
|
||||||
for slot_name, slot_val in self._slot_keys.items():
|
for slot_name, slot_val in self._slot_keys.items():
|
||||||
@@ -6376,7 +6416,7 @@ class NoxSourceProvider(FileSystemProvider):
|
|||||||
def _sub(obj):
|
def _sub(obj):
|
||||||
"""Recursively substitute {query} in payload (handles nested dicts/lists)."""
|
"""Recursively substitute {query} in payload (handles nested dicts/lists)."""
|
||||||
if isinstance(obj, str):
|
if isinstance(obj, str):
|
||||||
return obj.replace("{query}", query).replace("{target}", query)
|
return obj.replace("{query}", effective_query).replace("{target}", effective_query)
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
return {k: _sub(v) for k, v in obj.items()}
|
return {k: _sub(v) for k, v in obj.items()}
|
||||||
if isinstance(obj, list):
|
if isinstance(obj, list):
|
||||||
@@ -6386,13 +6426,63 @@ class NoxSourceProvider(FileSystemProvider):
|
|||||||
payload = _sub(d.get("payload") or {})
|
payload = _sub(d.get("payload") or {})
|
||||||
|
|
||||||
if method == "POST":
|
if method == "POST":
|
||||||
|
if isinstance(payload, str):
|
||||||
|
status, text, _ = await self._post(session, url,
|
||||||
|
raw_body=payload,
|
||||||
|
headers=hdrs)
|
||||||
|
else:
|
||||||
status, text, _ = await self._post(session, url,
|
status, text, _ = await self._post(session, url,
|
||||||
json_data=payload or None,
|
json_data=payload or None,
|
||||||
headers=hdrs)
|
headers=hdrs)
|
||||||
else:
|
else:
|
||||||
status, text, _ = await self._get(session, url, headers=hdrs)
|
status, text, _ = await self._get(session, url, headers=hdrs)
|
||||||
|
|
||||||
|
# If the primary endpoint fails, try backup_endpoints in order.
|
||||||
if status not in range(200, 300) or not text:
|
if status not in range(200, 300) or not text:
|
||||||
|
for backup in (d.get("backup_endpoints") or []):
|
||||||
|
backup_url = (backup
|
||||||
|
.replace("{query}", urllib.parse.quote(query, safe=""))
|
||||||
|
.replace("{target}", urllib.parse.quote(query, safe="")))
|
||||||
|
for slot_name, slot_val in self._slot_keys.items():
|
||||||
|
backup_url = backup_url.replace(f"{{{slot_name}}}", slot_val or "")
|
||||||
|
if method == "POST":
|
||||||
|
status, text, _ = await self._post(session, backup_url,
|
||||||
|
json_data=payload or None,
|
||||||
|
headers=hdrs)
|
||||||
|
else:
|
||||||
|
status, text, _ = await self._get(session, backup_url, headers=hdrs)
|
||||||
|
if status in range(200, 300) and text:
|
||||||
|
break
|
||||||
|
# as a job submission, extract the job ID via poll_id_field, then poll
|
||||||
|
# poll_endpoint?<poll_id_param>=<id> until results arrive.
|
||||||
|
poll_endpoint = d.get("poll_endpoint", "")
|
||||||
|
if poll_endpoint:
|
||||||
|
try:
|
||||||
|
job_id = json.loads(text).get(d.get("poll_id_field", "id"))
|
||||||
|
except Exception:
|
||||||
|
job_id = None
|
||||||
|
if not job_id:
|
||||||
|
return []
|
||||||
|
poll_param = d.get("poll_id_param", "id")
|
||||||
|
poll_root = d.get("poll_json_root", d.get("json_root", ""))
|
||||||
|
poll_url = f"{poll_endpoint}?{poll_param}={job_id}"
|
||||||
|
delay = 2
|
||||||
|
for _ in range(4):
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
p_status, p_text, _ = await self._get(session, poll_url, headers=hdrs)
|
||||||
|
if p_status not in range(200, 300) or not p_text:
|
||||||
|
delay = min(delay * 2, 16)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
items = json.loads(p_text)
|
||||||
|
for key in (poll_root.split(".") if poll_root else []):
|
||||||
|
if isinstance(items, dict):
|
||||||
|
items = items.get(key, [])
|
||||||
|
if isinstance(items, list) and items:
|
||||||
|
return self._by_json(p_text, poll_root, d.get("field_map", {}))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
delay = min(delay * 2, 16)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
regex = d.get("regex_pattern", "")
|
regex = d.get("regex_pattern", "")
|
||||||
@@ -6510,12 +6600,21 @@ class SourceOrchestrator:
|
|||||||
"output_type": raw.get("output_type", []),
|
"output_type": raw.get("output_type", []),
|
||||||
"pivot_types": raw.get("pivot_types", []),
|
"pivot_types": raw.get("pivot_types", []),
|
||||||
"confidence": raw.get("confidence", 0.5),
|
"confidence": raw.get("confidence", 0.5),
|
||||||
# payload_template → payload for POST sources
|
# payload_template → payload for POST sources; raw_payload takes precedence
|
||||||
"payload": raw.get("payload_template") or raw.get("payload") or {},
|
"payload": raw.get("raw_payload") or raw.get("payload_template") or raw.get("payload") or {},
|
||||||
# Pass resolved slot keys so FileSystemProvider can use them
|
# Pass resolved slot keys so FileSystemProvider can use them
|
||||||
"_slot_keys": slot_keys,
|
"_slot_keys": slot_keys,
|
||||||
|
# Two-phase poll support
|
||||||
|
"poll_endpoint": raw.get("poll_endpoint", ""),
|
||||||
|
"poll_id_field": raw.get("poll_id_field", "id"),
|
||||||
|
"poll_id_param": raw.get("poll_id_param", "id"),
|
||||||
|
"poll_json_root": raw.get("poll_json_root", ""),
|
||||||
|
"backup_endpoints": raw.get("backup_endpoints", []),
|
||||||
|
"query_transform": raw.get("query_transform", ""),
|
||||||
}
|
}
|
||||||
sources.append(NoxSourceProvider(self._sem, self._db, self._config, defn))
|
inst = NoxSourceProvider(self._sem, self._db, self._config, defn)
|
||||||
|
inst._bypass_required = raw.get("bypass_required") or []
|
||||||
|
sources.append(inst)
|
||||||
logger.debug("SourceOrchestrator: loaded %s", jf.name)
|
logger.debug("SourceOrchestrator: loaded %s", jf.name)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("SourceOrchestrator: failed %s — %s", jf.name, exc)
|
logger.warning("SourceOrchestrator: failed %s — %s", jf.name, exc)
|
||||||
@@ -6544,8 +6643,14 @@ class SourceOrchestrator:
|
|||||||
def get_sources(self, session: "Session", qtype: str) -> List[AsyncSource]:
|
def get_sources(self, session: "Session", qtype: str) -> List[AsyncSource]:
|
||||||
"""Return plugin sources applicable to qtype, pre-filtered to avoid creating unnecessary tasks."""
|
"""Return plugin sources applicable to qtype, pre-filtered to avoid creating unnecessary tasks."""
|
||||||
self._ensure_loaded()
|
self._ensure_loaded()
|
||||||
|
# curl_cffi presence cached in OPTIONAL after first _try_import call
|
||||||
|
_has_cffi = "curl_cffi" in OPTIONAL or _try_import("curl_cffi") is not None
|
||||||
sources: List[AsyncSource] = []
|
sources: List[AsyncSource] = []
|
||||||
for src in self._nox_sources:
|
for src in self._nox_sources:
|
||||||
|
bypass = getattr(src, "_bypass_required", []) or []
|
||||||
|
if "cloudflare" in bypass and not _has_cffi:
|
||||||
|
logger.debug("Skipping %s — cloudflare bypass required, curl_cffi absent", src.name)
|
||||||
|
continue
|
||||||
input_type = getattr(src, "_input_type", "")
|
input_type = getattr(src, "_input_type", "")
|
||||||
if not input_type or input_type == "any" or not qtype or input_type == qtype:
|
if not input_type or input_type == "any" or not qtype or input_type == qtype:
|
||||||
sources.append(src)
|
sources.append(src)
|
||||||
@@ -7054,12 +7159,10 @@ def main() -> None:
|
|||||||
config.proxy = f"socks5h://127.0.0.1:{config.tor_socks}"
|
config.proxy = f"socks5h://127.0.0.1:{config.tor_socks}"
|
||||||
if args.proxy:
|
if args.proxy:
|
||||||
config.proxy = args.proxy
|
config.proxy = args.proxy
|
||||||
# K2: --guardian-off is an alias for --allow-leak
|
|
||||||
config.allow_leak = args.allow_leak or getattr(args, "guardian_off", False)
|
config.allow_leak = args.allow_leak or getattr(args, "guardian_off", False)
|
||||||
config.no_online_crack = getattr(args, "no_online_crack", False)
|
config.no_online_crack = getattr(args, "no_online_crack", False)
|
||||||
config.max_threads = config.concurrency = args.threads
|
config.max_threads = config.concurrency = args.threads
|
||||||
config.timeout = args.timeout
|
config.timeout = args.timeout
|
||||||
# A9/I3: store no_pivot and depth in config so REPL and AvalancheScanner can read them
|
|
||||||
config.no_pivot = args.no_pivot
|
config.no_pivot = args.no_pivot
|
||||||
if getattr(args, "depth", None) is not None:
|
if getattr(args, "depth", None) is not None:
|
||||||
config.pivot_depth = args.depth
|
config.pivot_depth = args.depth
|
||||||
@@ -7083,22 +7186,36 @@ def _main_run(args, config: NoxConfig, db: NoxDB) -> None:
|
|||||||
repl._sources()
|
repl._sources()
|
||||||
return
|
return
|
||||||
|
|
||||||
# B6: --reset-sources forces a full resync from package
|
|
||||||
if getattr(args, "reset_sources", False):
|
if getattr(args, "reset_sources", False):
|
||||||
import shutil as _shutil
|
import shutil as _shutil
|
||||||
candidate = _PKG_ROOT / "sources"
|
candidate = _PKG_ROOT / "sources"
|
||||||
if not candidate.is_dir():
|
if not candidate.is_dir():
|
||||||
candidate = Path("/usr/share/nox-cli/sources")
|
candidate = Path("/usr/share/nox-cli/sources")
|
||||||
if candidate.is_dir():
|
if candidate.is_dir():
|
||||||
|
# Copy all current package sources to runtime dir
|
||||||
count = 0
|
count = 0
|
||||||
|
pkg_names = set()
|
||||||
for jf in candidate.glob("*.json"):
|
for jf in candidate.glob("*.json"):
|
||||||
|
pkg_names.add(jf.name)
|
||||||
dst = SOURCE_DIR / jf.name
|
dst = SOURCE_DIR / jf.name
|
||||||
try:
|
try:
|
||||||
_shutil.copy2(jf, dst)
|
_shutil.copy2(jf, dst)
|
||||||
count += 1
|
count += 1
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
out("ok", f"Reset {count} source plugins from package.")
|
# Remove orphaned plugins no longer in the package
|
||||||
|
removed = 0
|
||||||
|
for existing in SOURCE_DIR.glob("*.json"):
|
||||||
|
if existing.name not in pkg_names:
|
||||||
|
try:
|
||||||
|
existing.unlink()
|
||||||
|
removed += 1
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
msg = f"Reset {count} source plugins from package."
|
||||||
|
if removed:
|
||||||
|
msg += f" Removed {removed} orphaned plugin(s)."
|
||||||
|
out("ok", msg)
|
||||||
else:
|
else:
|
||||||
out("warn", "Package sources directory not found.")
|
out("warn", "Package sources directory not found.")
|
||||||
return
|
return
|
||||||
|
|||||||
+5
-3
@@ -4,18 +4,20 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "nox-cli"
|
name = "nox-cli"
|
||||||
version = "1.0.0"
|
version = "1.0.5"
|
||||||
description = "Advanced Asynchronous Cyber Threat Intelligence Framework"
|
description = "Advanced Asynchronous Cyber Threat Intelligence Framework"
|
||||||
readme = { file = "README.md", content-type = "text/markdown" }
|
readme = { file = "README.md", content-type = "text/markdown" }
|
||||||
license = { text = "Apache-2.0" }
|
license = { text = "Apache-2.0" }
|
||||||
authors = [{ name = "nox-project" }]
|
authors = [{ name = "nox-project" }]
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp>=3.9.0",
|
"aiohttp>=3.13.5",
|
||||||
"aiohttp-socks>=0.8.4",
|
"aiohttp-socks>=0.8.4",
|
||||||
"aiosqlite>=0.20.0",
|
"aiosqlite>=0.20.0",
|
||||||
"httpx[http2]>=0.27.0",
|
"httpx[http2]>=0.27.0",
|
||||||
"requests>=2.31.0",
|
"brotli>=1.1.0",
|
||||||
|
"zstandard>=0.23.0",
|
||||||
|
"requests>=2.32.3",
|
||||||
"certifi>=2024.2.2",
|
"certifi>=2024.2.2",
|
||||||
"cloudscraper>=1.2.71",
|
"cloudscraper>=1.2.71",
|
||||||
"beautifulsoup4>=4.12.3",
|
"beautifulsoup4>=4.12.3",
|
||||||
|
|||||||
+4
-2
@@ -2,13 +2,15 @@
|
|||||||
# Python 3.8+ | pip install -r requirements.txt
|
# Python 3.8+ | pip install -r requirements.txt
|
||||||
|
|
||||||
# ── Core (Async) ───────────────────────────────────────────────────────
|
# ── Core (Async) ───────────────────────────────────────────────────────
|
||||||
aiohttp>=3.9.0
|
aiohttp>=3.13.5
|
||||||
aiohttp-socks>=0.8.4 # SOCKS4/5 proxy support for aiohttp
|
aiohttp-socks>=0.8.4 # SOCKS4/5 proxy support for aiohttp
|
||||||
aiosqlite>=0.20.0 # async SQLite (forensic persistence DB)
|
aiosqlite>=0.20.0 # async SQLite (forensic persistence DB)
|
||||||
httpx[http2]>=0.27.0 # Guardian Engine: dynamic proxy fetch + HTTP/2
|
httpx[http2]>=0.27.0 # Guardian Engine: dynamic proxy fetch + HTTP/2
|
||||||
|
brotli>=1.1.0 # brotli decompression for aiohttp br responses
|
||||||
|
zstandard>=0.23.0 # zstd decompression for aiohttp zstd responses (Cloudflare/Fastly CDNs)
|
||||||
|
|
||||||
# ── Intelligence & Scraping ────────────────────────────────────────────
|
# ── Intelligence & Scraping ────────────────────────────────────────────
|
||||||
requests>=2.31.0
|
requests>=2.32.3
|
||||||
certifi>=2024.2.2 # up-to-date CA bundle for SSL verification
|
certifi>=2024.2.2 # up-to-date CA bundle for SSL verification
|
||||||
cloudscraper>=1.2.71 # Cloudflare-protected endpoint bypass
|
cloudscraper>=1.2.71 # Cloudflare-protected endpoint bypass
|
||||||
beautifulsoup4>=4.12.3
|
beautifulsoup4>=4.12.3
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ requirements = [
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="nox-cli",
|
name="nox-cli",
|
||||||
version="1.0.0",
|
version="1.0.5",
|
||||||
author="nox-project",
|
author="nox-project",
|
||||||
description="Advanced Asynchronous Cyber Threat Intelligence Framework",
|
description="Advanced Asynchronous Cyber Threat Intelligence Framework",
|
||||||
long_description=Path("README.md").read_text(),
|
long_description=Path("README.md").read_text(),
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "bgpview_ip",
|
|
||||||
"category": "network",
|
|
||||||
"endpoint": "https://api.bgpview.io/ip/{target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": false,
|
|
||||||
"selectors": {
|
|
||||||
"prefixes": "$.data.prefixes[*].prefix"
|
|
||||||
},
|
|
||||||
"rate_limit": 1.0,
|
|
||||||
"headers": {},
|
|
||||||
"api_key_slots": [],
|
|
||||||
"input_type": "ip",
|
|
||||||
"output_type": [
|
|
||||||
"ip"
|
|
||||||
],
|
|
||||||
"normalization_map": {},
|
|
||||||
"tags": [
|
|
||||||
"passive",
|
|
||||||
"infrastructure"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://api.bgpview.io",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 4,
|
|
||||||
"backup_endpoints": [],
|
|
||||||
"confidence": 0.85
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "checkleaked",
|
|
||||||
"category": "breaches",
|
|
||||||
"endpoint": "https://api.checkleaked.cc/check/{target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": false,
|
|
||||||
"selectors": {
|
|
||||||
"found": "$.found"
|
|
||||||
},
|
|
||||||
"rate_limit": 1.0,
|
|
||||||
"headers": {},
|
|
||||||
"api_key_slots": [],
|
|
||||||
"input_type": "email",
|
|
||||||
"output_type": [
|
|
||||||
"email"
|
|
||||||
],
|
|
||||||
"normalization_map": {},
|
|
||||||
"tags": [
|
|
||||||
"passive",
|
|
||||||
"stealth"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://api.checkleaked.cc",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 2,
|
|
||||||
"is_volatile": true,
|
|
||||||
"backup_endpoints": [
|
|
||||||
"https://checkleaked.cc/api/check/{target}"
|
|
||||||
],
|
|
||||||
"confidence": 0.55
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "circl_hashlookup",
|
||||||
|
"category": "hashes",
|
||||||
|
"endpoint": "https://hashlookup.circl.lu/lookup/md5/{target}",
|
||||||
|
"method": "GET",
|
||||||
|
"requires_auth": false,
|
||||||
|
"selectors": {
|
||||||
|
"filename": "$.FileName",
|
||||||
|
"known_malicious": "$.KnownMalicious"
|
||||||
|
},
|
||||||
|
"rate_limit": 1.0,
|
||||||
|
"headers": {},
|
||||||
|
"api_key_slots": [],
|
||||||
|
"input_type": "hash",
|
||||||
|
"output_type": [
|
||||||
|
"hash"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"FileName": "filename",
|
||||||
|
"MD5": "hash_md5"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive",
|
||||||
|
"fast"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://hashlookup.circl.lu",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 5,
|
||||||
|
"backup_endpoints": [
|
||||||
|
"https://hashlookup.circl.lu/lookup/sha1/{target}",
|
||||||
|
"https://hashlookup.circl.lu/lookup/sha256/{target}"
|
||||||
|
],
|
||||||
|
"confidence": 1.0
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cit0day",
|
|
||||||
"category": "breaches",
|
|
||||||
"endpoint": "https://cit0day.in/api/v1/search?query={target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": true,
|
|
||||||
"selectors": {
|
|
||||||
"results": "$.results"
|
|
||||||
},
|
|
||||||
"rate_limit": 1.0,
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "Bearer {CIT0DAY_API_KEY}"
|
|
||||||
},
|
|
||||||
"api_key_slots": [
|
|
||||||
"{CIT0DAY_API_KEY}"
|
|
||||||
],
|
|
||||||
"input_type": "email",
|
|
||||||
"output_type": [
|
|
||||||
"email"
|
|
||||||
],
|
|
||||||
"normalization_map": {},
|
|
||||||
"tags": [
|
|
||||||
"passive",
|
|
||||||
"stealth"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://cit0day.in",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 2,
|
|
||||||
"is_volatile": true,
|
|
||||||
"backup_endpoints": [],
|
|
||||||
"confidence": 0.55
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "clearbit_enrich",
|
|
||||||
"category": "enrichment",
|
|
||||||
"endpoint": "https://person.clearbit.com/v2/people/find?email={target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": true,
|
|
||||||
"selectors": {
|
|
||||||
"full_name": "$.name.fullName"
|
|
||||||
},
|
|
||||||
"rate_limit": 1.0,
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "Bearer {CLEARBIT_API_KEY}"
|
|
||||||
},
|
|
||||||
"api_key_slots": [
|
|
||||||
"{CLEARBIT_API_KEY}"
|
|
||||||
],
|
|
||||||
"input_type": "email",
|
|
||||||
"output_type": [
|
|
||||||
"username",
|
|
||||||
"domain"
|
|
||||||
],
|
|
||||||
"normalization_map": {
|
|
||||||
"fullName": "full_name"
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"passive"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://person.clearbit.com",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 4,
|
|
||||||
"backup_endpoints": [],
|
|
||||||
"confidence": 0.85
|
|
||||||
}
|
|
||||||
+3
-2
@@ -25,7 +25,8 @@
|
|||||||
],
|
],
|
||||||
"health_check_url": "https://crt.sh",
|
"health_check_url": "https://crt.sh",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 5,
|
"reliability_score": 3,
|
||||||
|
"is_volatile": true,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
"confidence": 1.0
|
"confidence": 0.7
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "dehashed",
|
"name": "dehashed",
|
||||||
"category": "breaches",
|
"category": "breaches",
|
||||||
"endpoint": "https://api.dehashed.com/search?query={target}",
|
"endpoint": "https://api.dehashed.com/v2/search?query={target}",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"requires_auth": true,
|
"requires_auth": true,
|
||||||
"selectors": {
|
"selectors": {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "duckduckgo_api",
|
"name": "duckduckgo_api",
|
||||||
"category": "search",
|
"category": "search",
|
||||||
"endpoint": "https://api.duckduckgo.com/?q={target}&format=json",
|
"endpoint": "https://search.sapti.me/search?q={target}&format=json&categories=general",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"requires_auth": false,
|
"requires_auth": false,
|
||||||
"selectors": {
|
"selectors": {
|
||||||
"abstract": "$.Abstract"
|
"results": "$.results"
|
||||||
},
|
},
|
||||||
"rate_limit": 1.0,
|
"rate_limit": 1.0,
|
||||||
"headers": {},
|
"headers": {},
|
||||||
@@ -14,14 +14,24 @@
|
|||||||
"output_type": [
|
"output_type": [
|
||||||
"url"
|
"url"
|
||||||
],
|
],
|
||||||
"normalization_map": {},
|
"normalization_map": {
|
||||||
|
"url": "url",
|
||||||
|
"title": "title"
|
||||||
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"passive",
|
"passive",
|
||||||
"fast"
|
"fast"
|
||||||
],
|
],
|
||||||
"health_check_url": "https://api.duckduckgo.com",
|
"health_check_url": "https://search.sapti.me",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 5,
|
"reliability_score": 3,
|
||||||
"backup_endpoints": [],
|
"is_volatile": true,
|
||||||
"confidence": 1.0
|
"backup_endpoints": [
|
||||||
|
"https://searx.tiekoetter.com/search?q={target}&format=json&categories=general",
|
||||||
|
"https://searx.perennialte.ch/search?q={target}&format=json&categories=general",
|
||||||
|
"https://search.mdosch.de/search?q={target}&format=json&categories=general",
|
||||||
|
"https://paulgo.io/search?q={target}&format=json&categories=general",
|
||||||
|
"https://priv.au/search?q={target}&format=json&categories=general"
|
||||||
|
],
|
||||||
|
"confidence": 0.7
|
||||||
}
|
}
|
||||||
@@ -3,13 +3,17 @@
|
|||||||
"category": "email_rep",
|
"category": "email_rep",
|
||||||
"endpoint": "https://emailrep.io/{target}",
|
"endpoint": "https://emailrep.io/{target}",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"requires_auth": false,
|
"requires_auth": true,
|
||||||
"selectors": {
|
"selectors": {
|
||||||
"reputation": "$.reputation"
|
"reputation": "$.reputation"
|
||||||
},
|
},
|
||||||
"rate_limit": 2.0,
|
"rate_limit": 2.0,
|
||||||
"headers": {},
|
"headers": {
|
||||||
"api_key_slots": [],
|
"Key": "{EMAILREP_API_KEY}"
|
||||||
|
},
|
||||||
|
"api_key_slots": [
|
||||||
|
"{EMAILREP_API_KEY}"
|
||||||
|
],
|
||||||
"input_type": "email",
|
"input_type": "email",
|
||||||
"output_type": [
|
"output_type": [
|
||||||
"email"
|
"email"
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "flare_leaksdb",
|
||||||
|
"category": "breaches",
|
||||||
|
"endpoint": "https://api.flare.io/leaksdb/v2/credentials/email/{target}",
|
||||||
|
"method": "GET",
|
||||||
|
"requires_auth": true,
|
||||||
|
"selectors": {
|
||||||
|
"items": "$.items"
|
||||||
|
},
|
||||||
|
"rate_limit": 1.0,
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer {FLARE_API_KEY}"
|
||||||
|
},
|
||||||
|
"api_key_slots": [
|
||||||
|
"{FLARE_API_KEY}"
|
||||||
|
],
|
||||||
|
"input_type": "email",
|
||||||
|
"output_type": [
|
||||||
|
"email",
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"email": "email_address",
|
||||||
|
"username": "username",
|
||||||
|
"password": "plaintext_password",
|
||||||
|
"hash": "password_hash",
|
||||||
|
"source": "breach_name"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive",
|
||||||
|
"stealth"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://api.flare.io",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 4,
|
||||||
|
"backup_endpoints": [],
|
||||||
|
"confidence": 0.85
|
||||||
|
}
|
||||||
@@ -27,5 +27,6 @@
|
|||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 4,
|
"reliability_score": 4,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
|
"query_transform": "fofa_domain",
|
||||||
"confidence": 0.85
|
"confidence": 0.85
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "fullhunt_subdomains",
|
||||||
|
"category": "dns_recon",
|
||||||
|
"endpoint": "https://fullhunt.io/api/v1/domain/{target}/subdomains",
|
||||||
|
"method": "GET",
|
||||||
|
"requires_auth": true,
|
||||||
|
"selectors": {
|
||||||
|
"hosts": "$.hosts"
|
||||||
|
},
|
||||||
|
"rate_limit": 1.0,
|
||||||
|
"headers": {
|
||||||
|
"X-API-KEY": "{FULLHUNT_API_KEY}"
|
||||||
|
},
|
||||||
|
"api_key_slots": [
|
||||||
|
"{FULLHUNT_API_KEY}"
|
||||||
|
],
|
||||||
|
"input_type": "domain",
|
||||||
|
"output_type": [
|
||||||
|
"domain",
|
||||||
|
"ip"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"host": "domain"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive",
|
||||||
|
"infrastructure"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://fullhunt.io",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 4,
|
||||||
|
"backup_endpoints": [],
|
||||||
|
"confidence": 0.85
|
||||||
|
}
|
||||||
@@ -22,5 +22,6 @@
|
|||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 4,
|
"reliability_score": 4,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
|
"query_transform": "md5_lower",
|
||||||
"confidence": 0.85
|
"confidence": 0.85
|
||||||
}
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "hashes_org",
|
|
||||||
"category": "hashes",
|
|
||||||
"endpoint": "https://hashes.org/api.php?key={HASHES_API_KEY}&query={target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": true,
|
|
||||||
"selectors": {
|
|
||||||
"found": "$.results"
|
|
||||||
},
|
|
||||||
"rate_limit": 1.0,
|
|
||||||
"headers": {},
|
|
||||||
"api_key_slots": [
|
|
||||||
"{HASHES_API_KEY}"
|
|
||||||
],
|
|
||||||
"input_type": "hash",
|
|
||||||
"output_type": [
|
|
||||||
"hash"
|
|
||||||
],
|
|
||||||
"normalization_map": {},
|
|
||||||
"tags": [
|
|
||||||
"passive"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://hashes.org",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 3,
|
|
||||||
"backup_endpoints": [],
|
|
||||||
"confidence": 0.7
|
|
||||||
}
|
|
||||||
@@ -29,13 +29,13 @@ SERVICE_REGISTRY: Dict[str, Dict] = {
|
|||||||
"alienvault_otx_malware": {"display": "AlienVault OTX (Malware)", "public": True},
|
"alienvault_otx_malware": {"display": "AlienVault OTX (Malware)", "public": True},
|
||||||
"alienvault_otx_user": {"display": "AlienVault OTX (User)", "public": True},
|
"alienvault_otx_user": {"display": "AlienVault OTX (User)", "public": True},
|
||||||
"anubis_subdomains": {"display": "Anubis Subdomains", "public": True},
|
"anubis_subdomains": {"display": "Anubis Subdomains", "public": True},
|
||||||
"bgpview_ip": {"display": "BGPView IP", "public": True},
|
"ripestat_ip": {"display": "RIPE Stat IP", "public": True},
|
||||||
"checkleaked": {"display": "CheckLeaked", "public": True},
|
"xposedornot": {"display": "XposedOrNot", "public": True},
|
||||||
"crt_sh": {"display": "crt.sh", "public": True},
|
"crt_sh": {"display": "crt.sh", "public": True},
|
||||||
"cve_search": {"display": "CVE Search", "public": True},
|
"cve_search": {"display": "CVE Search", "public": True},
|
||||||
"cxsecurity": {"display": "CXSecurity", "public": True},
|
"cxsecurity": {"display": "CXSecurity", "public": True},
|
||||||
"duckduckgo_api": {"display": "Google / DDG Dorks", "public": True},
|
"duckduckgo_api": {"display": "Google / DDG Dorks", "public": True},
|
||||||
"emailrep_io": {"display": "EmailRep.io", "public": True},
|
"emailrep_io": {"display": "EmailRep.io", "public": False},
|
||||||
"github_users": {"display": "GitHub Users", "public": True},
|
"github_users": {"display": "GitHub Users", "public": True},
|
||||||
"gitlab_search": {"display": "GitLab Search", "public": True},
|
"gitlab_search": {"display": "GitLab Search", "public": True},
|
||||||
"gravatar": {"display": "Gravatar", "public": True},
|
"gravatar": {"display": "Gravatar", "public": True},
|
||||||
@@ -44,7 +44,10 @@ SERVICE_REGISTRY: Dict[str, Dict] = {
|
|||||||
"hackertarget_hostsearch": {"display": "HackerTarget Host Search", "public": True},
|
"hackertarget_hostsearch": {"display": "HackerTarget Host Search", "public": True},
|
||||||
"hackertarget_reverseip": {"display": "HackerTarget Reverse IP", "public": True},
|
"hackertarget_reverseip": {"display": "HackerTarget Reverse IP", "public": True},
|
||||||
"hackertarget_whois": {"display": "WHOIS (HackerTarget)", "public": True},
|
"hackertarget_whois": {"display": "WHOIS (HackerTarget)", "public": True},
|
||||||
"hudsonrock_osint": {"display": "HudsonRock OSINT", "public": True},
|
"ipapi_is": {"display": "ipapi.is", "public": True},
|
||||||
|
"circl_hashlookup": {"display": "CIRCL Hash Lookup", "public": True},
|
||||||
|
"proxynova_comb": {"display": "ProxyNova COMB", "public": True},
|
||||||
|
"shodan_internetdb": {"display": "Shodan InternetDB", "public": True},
|
||||||
"ipapi_co": {"display": "ipapi.co", "public": True},
|
"ipapi_co": {"display": "ipapi.co", "public": True},
|
||||||
"ipinfo_io": {"display": "IPInfo.io", "public": True},
|
"ipinfo_io": {"display": "IPInfo.io", "public": True},
|
||||||
"ipvigilante": {"display": "IPVigilante", "public": True},
|
"ipvigilante": {"display": "IPVigilante", "public": True},
|
||||||
@@ -59,14 +62,10 @@ SERVICE_REGISTRY: Dict[str, Dict] = {
|
|||||||
"reddit_user": {"display": "Reddit User", "public": True},
|
"reddit_user": {"display": "Reddit User", "public": True},
|
||||||
"robtex_ip": {"display": "Robtex IP", "public": True},
|
"robtex_ip": {"display": "Robtex IP", "public": True},
|
||||||
"scamwatcher": {"display": "ScamWatcher", "public": True},
|
"scamwatcher": {"display": "ScamWatcher", "public": True},
|
||||||
"social_scan": {"display": "Social Scan", "public": True},
|
|
||||||
"sublist3r_api": {"display": "Sublist3r API", "public": True},
|
"sublist3r_api": {"display": "Sublist3r API", "public": True},
|
||||||
"threatcrowd_domain": {"display": "ThreatCrowd (Domain)", "public": True},
|
|
||||||
"threatcrowd_email": {"display": "ThreatCrowd (Email)", "public": True},
|
|
||||||
"threatminer_domain": {"display": "ThreatMiner (Domain)", "public": True},
|
"threatminer_domain": {"display": "ThreatMiner (Domain)", "public": True},
|
||||||
"threatminer_ip": {"display": "ThreatMiner (IP)", "public": True},
|
"threatminer_ip": {"display": "ThreatMiner (IP)", "public": True},
|
||||||
"urlscan_search": {"display": "URLScan.io", "public": True},
|
"urlscan_search": {"display": "URLScan.io", "public": True},
|
||||||
"vigilante_pw": {"display": "Vigilante.pw", "public": True},
|
|
||||||
"wayback_machine": {"display": "Wayback Machine", "public": True},
|
"wayback_machine": {"display": "Wayback Machine", "public": True},
|
||||||
# ── Private / key-required ────────────────────────────────────────
|
# ── Private / key-required ────────────────────────────────────────
|
||||||
"ABSTRACT_API_KEY": {"display": "Abstract Email Validation", "public": False},
|
"ABSTRACT_API_KEY": {"display": "Abstract Email Validation", "public": False},
|
||||||
@@ -78,8 +77,7 @@ SERVICE_REGISTRY: Dict[str, Dict] = {
|
|||||||
"BING_API_KEY": {"display": "Bing Search API", "public": False},
|
"BING_API_KEY": {"display": "Bing Search API", "public": False},
|
||||||
"CENSYS_AUTH_BASE64": {"display": "Censys", "public": False},
|
"CENSYS_AUTH_BASE64": {"display": "Censys", "public": False},
|
||||||
"CIRCL_AUTH_BASE64": {"display": "CIRCL.lu PDNS", "public": False},
|
"CIRCL_AUTH_BASE64": {"display": "CIRCL.lu PDNS", "public": False},
|
||||||
"CIT0DAY_API_KEY": {"display": "Cit0day", "public": False},
|
"SEON_API_KEY": {"display": "SEON Email Intelligence", "public": False},
|
||||||
"CLEARBIT_API_KEY": {"display": "Clearbit Enrich", "public": False},
|
|
||||||
"CRIMINALIP_API_KEY": {"display": "CriminalIP", "public": False},
|
"CRIMINALIP_API_KEY": {"display": "CriminalIP", "public": False},
|
||||||
"DEHASHED_AUTH_BASE64": {"display": "Dehashed", "public": False},
|
"DEHASHED_AUTH_BASE64": {"display": "Dehashed", "public": False},
|
||||||
"DNSDB_API_KEY": {"display": "DNSDB Passive DNS", "public": False},
|
"DNSDB_API_KEY": {"display": "DNSDB Passive DNS", "public": False},
|
||||||
@@ -94,7 +92,6 @@ SERVICE_REGISTRY: Dict[str, Dict] = {
|
|||||||
"GOOGLE_CX_KEY": {"display": "Google Custom Search (API key)", "public": False},
|
"GOOGLE_CX_KEY": {"display": "Google Custom Search (API key)", "public": False},
|
||||||
"GOOGLE_CX_ID": {"display": "Google Custom Search (CX ID)", "public": False},
|
"GOOGLE_CX_ID": {"display": "Google Custom Search (CX ID)", "public": False},
|
||||||
"GREYNOISE_API_KEY": {"display": "GreyNoise", "public": False},
|
"GREYNOISE_API_KEY": {"display": "GreyNoise", "public": False},
|
||||||
"HASHES_API_KEY": {"display": "Hashes.org", "public": False},
|
|
||||||
"HIBP_API_KEY": {"display": "HaveIBeenPwned", "public": False},
|
"HIBP_API_KEY": {"display": "HaveIBeenPwned", "public": False},
|
||||||
"HIPPO_API_KEY": {"display": "EmailHippo", "public": False},
|
"HIPPO_API_KEY": {"display": "EmailHippo", "public": False},
|
||||||
"HUNTER_API_KEY": {"display": "Hunter.io", "public": False},
|
"HUNTER_API_KEY": {"display": "Hunter.io", "public": False},
|
||||||
@@ -109,7 +106,6 @@ SERVICE_REGISTRY: Dict[str, Dict] = {
|
|||||||
"JOE_API_KEY": {"display": "Joe Sandbox", "public": False},
|
"JOE_API_KEY": {"display": "Joe Sandbox", "public": False},
|
||||||
"LEAKCHECK_API_KEY": {"display": "LeakCheck", "public": False},
|
"LEAKCHECK_API_KEY": {"display": "LeakCheck", "public": False},
|
||||||
"LEAKIX_API_KEY": {"display": "LeakIX", "public": False},
|
"LEAKIX_API_KEY": {"display": "LeakIX", "public": False},
|
||||||
"LEAKSTATS_API_KEY": {"display": "LeakStats.pw", "public": False},
|
|
||||||
"MAILBOX_API_KEY": {"display": "Mailboxlayer", "public": False},
|
"MAILBOX_API_KEY": {"display": "Mailboxlayer", "public": False},
|
||||||
"MALSHARE_API_KEY": {"display": "MalShare", "public": False},
|
"MALSHARE_API_KEY": {"display": "MalShare", "public": False},
|
||||||
"METADEFENDER_API_KEY": {"display": "MetaDefender", "public": False},
|
"METADEFENDER_API_KEY": {"display": "MetaDefender", "public": False},
|
||||||
@@ -125,9 +121,8 @@ SERVICE_REGISTRY: Dict[str, Dict] = {
|
|||||||
"SNUSBASE_API_KEY": {"display": "Snusbase", "public": False},
|
"SNUSBASE_API_KEY": {"display": "Snusbase", "public": False},
|
||||||
"SPYCLOUD_API_KEY": {"display": "SpyCloud", "public": False},
|
"SPYCLOUD_API_KEY": {"display": "SpyCloud", "public": False},
|
||||||
"SPYONWEB_API_KEY": {"display": "SpyOnWeb", "public": False},
|
"SPYONWEB_API_KEY": {"display": "SpyOnWeb", "public": False},
|
||||||
"SPYSE_API_KEY": {"display": "Spyse", "public": False},
|
|
||||||
"TC_API_KEY": {"display": "ThreatConnect", "public": False},
|
"TC_API_KEY": {"display": "ThreatConnect", "public": False},
|
||||||
"TINES_API_KEY": {"display": "Tines Breach", "public": False},
|
"FLARE_API_KEY": {"display": "Flare LeaksDB", "public": False},
|
||||||
"TP_API_KEY": {"display": "ThreatPortal", "public": False},
|
"TP_API_KEY": {"display": "ThreatPortal", "public": False},
|
||||||
"TWITTER_BEARER_TOKEN": {"display": "Twitter / X API v2", "public": False},
|
"TWITTER_BEARER_TOKEN": {"display": "Twitter / X API v2", "public": False},
|
||||||
"URLVOID_API_KEY": {"display": "URLVoid", "public": False},
|
"URLVOID_API_KEY": {"display": "URLVoid", "public": False},
|
||||||
@@ -139,6 +134,15 @@ SERVICE_REGISTRY: Dict[str, Dict] = {
|
|||||||
"WHOXY_API_KEY": {"display": "Whoxy WHOIS", "public": False},
|
"WHOXY_API_KEY": {"display": "Whoxy WHOIS", "public": False},
|
||||||
"ZEROBOUNCE_API_KEY": {"display": "ZeroBounce", "public": False},
|
"ZEROBOUNCE_API_KEY": {"display": "ZeroBounce", "public": False},
|
||||||
"ZOOMEYE_API_KEY": {"display": "ZoomEye", "public": False},
|
"ZOOMEYE_API_KEY": {"display": "ZoomEye", "public": False},
|
||||||
|
"EMAILREP_API_KEY": {"display": "EmailRep.io", "public": False},
|
||||||
|
"HASHES_COM_API_KEY": {"display": "Hashes.com (crack API)", "public": False},
|
||||||
|
"THREATFOX_API_KEY": {"display": "ThreatFox (abuse.ch)", "public": False},
|
||||||
|
"URLHAUS_API_KEY": {"display": "URLhaus (abuse.ch)", "public": False},
|
||||||
|
"MALWAREBAZAAR_API_KEY": {"display": "MalwareBazaar (abuse.ch)", "public": False},
|
||||||
|
"FULLHUNT_API_KEY": {"display": "FullHunt (attack surface)", "public": False},
|
||||||
|
"NETLAS_API_KEY": {"display": "Netlas.io (internet scanner)", "public": False},
|
||||||
|
"LEAK_LOOKUP_API_KEY": {"display": "Leak-Lookup", "public": False},
|
||||||
|
"MISP_URL": {"display": "MISP Instance URL", "public": False},
|
||||||
}
|
}
|
||||||
|
|
||||||
_PRIVATE_KEYS = {k: v for k, v in SERVICE_REGISTRY.items() if not v["public"]}
|
_PRIVATE_KEYS = {k: v for k, v in SERVICE_REGISTRY.items() if not v["public"]}
|
||||||
@@ -224,6 +228,7 @@ class ConfigManager:
|
|||||||
return cls._cache[key_name]
|
return cls._cache[key_name]
|
||||||
val = os.environ.get(key_name, "") or cls._get_store().get(key_name, "")
|
val = os.environ.get(key_name, "") or cls._get_store().get(key_name, "")
|
||||||
result = None if (not val or val == UNIVERSAL_PLACEHOLDER) else val
|
result = None if (not val or val == UNIVERSAL_PLACEHOLDER) else val
|
||||||
|
if result is not None:
|
||||||
cls._cache[key_name] = result
|
cls._cache[key_name] = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
+71
-27
@@ -12,9 +12,9 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
# C2: MD5 and NTLM share the same 32-char hex pattern.
|
# MD5 and NTLM share the same 32-char hex pattern. MD5 is listed first as it
|
||||||
# We list md5 first (most common in breach data) but also accept ntlm
|
# is the most common type in breach data. async_crack queries both md5 and
|
||||||
# so callers can query NTLM-specific APIs when needed.
|
# ntlm-specific APIs for any 32-char hash.
|
||||||
_PATTERNS: List[Tuple[str, re.Pattern]] = [
|
_PATTERNS: List[Tuple[str, re.Pattern]] = [
|
||||||
("bcrypt", re.compile(r"^\$2[aby]?\$\d{2}\$.{53}$")),
|
("bcrypt", re.compile(r"^\$2[aby]?\$\d{2}\$.{53}$")),
|
||||||
("sha256", re.compile(r"^[a-f0-9]{64}$", re.I)),
|
("sha256", re.compile(r"^[a-f0-9]{64}$", re.I)),
|
||||||
@@ -67,43 +67,47 @@ async def _query_api(session, url: str, fmt: str) -> Optional[str]:
|
|||||||
|
|
||||||
async def async_crack(session, hash_value: str, hash_type: str) -> Optional[str]:
|
async def async_crack(session, hash_value: str, hash_type: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Query multiple rainbow-table APIs concurrently.
|
Attempt to recover the plaintext for a given hash.
|
||||||
Returns first plaintext found, or None. bcrypt is skipped.
|
|
||||||
|
|
||||||
C1: create tasks upfront for cancellation, but await each via asyncio.shield
|
Strategy:
|
||||||
inside as_completed — no double wait_for wrapping.
|
1. Local rockyou wordlist (no external calls, no rate limits).
|
||||||
C2: for 32-char hex (md5/ntlm ambiguity), also query NTLM-specific APIs.
|
2. hashes.com API if HASHES_COM_API_KEY is configured.
|
||||||
|
|
||||||
Per-API timeout: 8s. Global budget: 20s (CRACK_TIMEOUT).
|
bcrypt is skipped — computationally infeasible for online cracking.
|
||||||
All tasks are cancelled as soon as the first result is found.
|
|
||||||
"""
|
"""
|
||||||
if hash_type == "bcrypt":
|
if hash_type == "bcrypt":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
h = hash_value.strip().lower()
|
h = hash_value.strip().lower()
|
||||||
apis = [
|
|
||||||
(f"https://www.nitrxgen.net/md5db/{h}", "text"),
|
|
||||||
(f"https://hashes.com/en/api/hash?hash={h}", "json"),
|
|
||||||
(f"https://hash.help/api/lookup/{h}", "json"),
|
|
||||||
(f"https://hashkiller.io/api/search.php?hash={h}", "json"),
|
|
||||||
(f"https://md5decrypt.net/Api/api.php?hash={h}&hash_type={hash_type}&email=&code=", "text"),
|
|
||||||
(f"https://www.cmd5.org/api.ashx?hash={h}", "text"),
|
|
||||||
]
|
|
||||||
# C2: for 32-char hashes (md5/ntlm ambiguous), add NTLM-specific endpoint
|
|
||||||
if hash_type == "md5" and len(h) == 32:
|
|
||||||
apis.append((f"https://hashes.com/en/api/hash?hash={h}&type=ntlm", "json"))
|
|
||||||
|
|
||||||
# C1: create tasks so we can cancel them; shield each before passing to wait_for
|
# 1. Local wordlist first — fast, zero external exposure
|
||||||
# so cancellation of the shield future does not cancel the underlying task prematurely.
|
import concurrent.futures as _cf
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
with _cf.ThreadPoolExecutor(max_workers=1) as _ex:
|
||||||
|
local = await loop.run_in_executor(_ex, _local_crack_sync_blocking, hash_value, hash_type)
|
||||||
|
if local:
|
||||||
|
return local
|
||||||
|
|
||||||
|
# 2. hashes.com if API key is configured
|
||||||
|
apis = []
|
||||||
|
try:
|
||||||
|
from sources.helpers.config_handler import ConfigManager # type: ignore
|
||||||
|
hashes_com_key = ConfigManager.get_key("HASHES_COM_API_KEY")
|
||||||
|
if hashes_com_key:
|
||||||
|
apis.append((f"https://hashes.com/en/api/search?hash={h}&key={hashes_com_key}", "json"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not apis:
|
||||||
|
return None
|
||||||
|
|
||||||
tasks = [asyncio.create_task(_query_api(session, url, fmt)) for url, fmt in apis]
|
tasks = [asyncio.create_task(_query_api(session, url, fmt)) for url, fmt in apis]
|
||||||
result: Optional[str] = None
|
result: Optional[str] = None
|
||||||
try:
|
try:
|
||||||
for fut in asyncio.as_completed(tasks):
|
for fut in asyncio.as_completed(tasks):
|
||||||
try:
|
try:
|
||||||
res = await asyncio.wait_for(asyncio.shield(fut), timeout=_API_TIMEOUT)
|
res = await asyncio.wait_for(asyncio.shield(fut), timeout=_API_TIMEOUT)
|
||||||
except (asyncio.TimeoutError, asyncio.CancelledError):
|
except (asyncio.TimeoutError, asyncio.CancelledError, Exception):
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
continue
|
continue
|
||||||
if res:
|
if res:
|
||||||
result = res
|
result = res
|
||||||
@@ -111,9 +115,49 @@ async def async_crack(session, hash_value: str, hash_type: str) -> Optional[str]
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
# Cancel all remaining tasks and await to suppress pending-task warnings
|
|
||||||
for t in tasks:
|
for t in tasks:
|
||||||
if not t.done():
|
if not t.done():
|
||||||
t.cancel()
|
t.cancel()
|
||||||
await asyncio.gather(*[t for t in tasks if not t.done()], return_exceptions=True)
|
await asyncio.gather(*[t for t in tasks if not t.done()], return_exceptions=True)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _local_crack_sync_blocking(hash_value: str, hash_type: str) -> Optional[str]:
|
||||||
|
"""Pure-sync version for ThreadPoolExecutor."""
|
||||||
|
import hashlib as _hl
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
wordlist = _Path.home() / ".nox" / "wordlists" / "rockyou.txt"
|
||||||
|
if not wordlist.exists():
|
||||||
|
return None
|
||||||
|
h = hash_value.strip().lower()
|
||||||
|
# usedforsecurity=False is required on FIPS-enabled systems (Python 3.9+).
|
||||||
|
# On Python 3.8 the kwarg does not exist, so we fall back gracefully.
|
||||||
|
def _md5(w):
|
||||||
|
try:
|
||||||
|
return _hl.md5(w, usedforsecurity=False).hexdigest()
|
||||||
|
except TypeError:
|
||||||
|
return _hl.md5(w).hexdigest()
|
||||||
|
|
||||||
|
def _sha1(w):
|
||||||
|
try:
|
||||||
|
return _hl.sha1(w, usedforsecurity=False).hexdigest()
|
||||||
|
except TypeError:
|
||||||
|
return _hl.sha1(w).hexdigest()
|
||||||
|
|
||||||
|
_hashers = {
|
||||||
|
"md5": _md5,
|
||||||
|
"sha1": _sha1,
|
||||||
|
"sha256": lambda w: _hl.sha256(w).hexdigest(),
|
||||||
|
}
|
||||||
|
hasher = _hashers.get(hash_type)
|
||||||
|
if not hasher:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with wordlist.open("rb") as f:
|
||||||
|
for line in f:
|
||||||
|
word = line.rstrip(b"\n\r")
|
||||||
|
if hasher(word) == h:
|
||||||
|
return word.decode("utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ from typing import Any, Dict, List
|
|||||||
# ── Noise patterns stripped from all report output ────────────────────
|
# ── Noise patterns stripped from all report output ────────────────────
|
||||||
_NOISE_RE = re.compile(
|
_NOISE_RE = re.compile(
|
||||||
r"(Traceback \(most recent|File \".*\.py\"|TimeoutError|ProxyError"
|
r"(Traceback \(most recent|File \".*\.py\"|TimeoutError|ProxyError"
|
||||||
r"|ConnectionError|aiohttp\.|ClientConnector|ssl\.|asyncio\."
|
r"|ConnectionError|ClientConnector|Task exception|NoneType|Object of type"
|
||||||
r"|Task exception|NoneType|Object of type)",
|
r"|(?:^|[\s(])aiohttp\.|(?:^|[\s(])asyncio\.|(?:^|[\s(])ssl\.)",
|
||||||
re.I,
|
re.I | re.MULTILINE,
|
||||||
)
|
)
|
||||||
_CTRL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
|
_CTRL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
|
||||||
|
|
||||||
@@ -48,11 +48,11 @@ def _raw(v: Any, maxlen: int = 200) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _pdf_safe(s: str, maxlen: int = 180) -> str:
|
def _pdf_safe(s: str, maxlen: int = 180) -> str:
|
||||||
# D4: sanitize for fpdf2 core fonts (latin-1 subset).
|
# Sanitise for fpdf2 core fonts (latin-1 subset).
|
||||||
# NFKD normalization decomposes accented chars (é→e + combining accent)
|
# NFKD normalization decomposes accented chars (é→e + combining accent)
|
||||||
# so common accented Latin characters survive as their base letter.
|
# so common accented Latin characters survive as their base letter.
|
||||||
# Truly non-latin-1 chars (Cyrillic, CJK, etc.) become '?' — intentional:
|
# Truly non-latin-1 chars (Cyrillic, CJK, etc.) become '?' — fpdf2 core
|
||||||
# fpdf2 core fonts cannot render them and would raise UnicodeEncodeError.
|
# fonts cannot render them and would raise UnicodeEncodeError.
|
||||||
s = _raw(s, maxlen)
|
s = _raw(s, maxlen)
|
||||||
try:
|
try:
|
||||||
import unicodedata
|
import unicodedata
|
||||||
@@ -114,7 +114,7 @@ def render_pivot_chain(data: dict) -> List[str]:
|
|||||||
chain = data.get("pivot_chain") or []
|
chain = data.get("pivot_chain") or []
|
||||||
target = _raw(data.get("target", "?"))
|
target = _raw(data.get("target", "?"))
|
||||||
|
|
||||||
# D2: if pivot_log is available, build chain from it (accurate tree)
|
# Build chain from pivot_log when available — it carries the full tree with depth and provenance.
|
||||||
pivot_log = data.get("pivot_log") or []
|
pivot_log = data.get("pivot_log") or []
|
||||||
if pivot_log:
|
if pivot_log:
|
||||||
lines: List[str] = []
|
lines: List[str] = []
|
||||||
@@ -195,14 +195,12 @@ def to_json(data: dict, path: str) -> None:
|
|||||||
dork_results = data.get("dork_results", []) or []
|
dork_results = data.get("dork_results", []) or []
|
||||||
scrape_results = data.get("scrape_results", {}) or {}
|
scrape_results = data.get("scrape_results", {}) or {}
|
||||||
|
|
||||||
# D3: apply consistent cap (1000) — same as HTML
|
|
||||||
_RECORD_CAP = 1000
|
_RECORD_CAP = 1000
|
||||||
|
|
||||||
out_data = {
|
out_data = {
|
||||||
"framework": f"NOX v{_NOX_VERSION}",
|
"framework": f"NOX v{_NOX_VERSION}",
|
||||||
"generated": datetime.now().isoformat(),
|
"generated": datetime.now().isoformat(),
|
||||||
"target": data.get("target", ""),
|
"target": data.get("target", ""),
|
||||||
# J3: self-describing metadata block
|
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"scan_id": hashlib.sha256(
|
"scan_id": hashlib.sha256(
|
||||||
f"{data.get('target','')}{datetime.now().isoformat()}".encode()
|
f"{data.get('target','')}{datetime.now().isoformat()}".encode()
|
||||||
@@ -387,7 +385,6 @@ def to_html(data: dict, path: str) -> None:
|
|||||||
# ── PDF report (fpdf2) ────────────────────────────────────────────────
|
# ── PDF report (fpdf2) ────────────────────────────────────────────────
|
||||||
|
|
||||||
def to_pdf(data: dict, path: str, investigator_id: str = "NOX-AUTO") -> None:
|
def to_pdf(data: dict, path: str, investigator_id: str = "NOX-AUTO") -> None:
|
||||||
# D1: raise a clear error with install hint if fpdf2 is absent — never silently return.
|
|
||||||
try:
|
try:
|
||||||
from fpdf import FPDF # type: ignore
|
from fpdf import FPDF # type: ignore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
+17
-18
@@ -31,7 +31,6 @@ _PIVOT_TYPES = {"email", "username", "phone", "name", "ip", "domain"}
|
|||||||
|
|
||||||
|
|
||||||
def _cfg_depth(orc=None) -> int:
|
def _cfg_depth(orc=None) -> int:
|
||||||
# A7/A10: read from orchestrator config if available
|
|
||||||
if orc is not None:
|
if orc is not None:
|
||||||
cfg = getattr(orc, "config", None)
|
cfg = getattr(orc, "config", None)
|
||||||
if cfg is not None:
|
if cfg is not None:
|
||||||
@@ -46,7 +45,6 @@ def _cfg_depth(orc=None) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def _cfg_concurrency(orc=None) -> int:
|
def _cfg_concurrency(orc=None) -> int:
|
||||||
# A7: read from orchestrator config if available
|
|
||||||
if orc is not None:
|
if orc is not None:
|
||||||
cfg = getattr(orc, "config", None)
|
cfg = getattr(orc, "config", None)
|
||||||
if cfg is not None:
|
if cfg is not None:
|
||||||
@@ -137,29 +135,24 @@ class AvalancheScanner:
|
|||||||
def __init__(self, orchestrator: "Orchestrator") -> None:
|
def __init__(self, orchestrator: "Orchestrator") -> None:
|
||||||
self._orc = orchestrator
|
self._orc = orchestrator
|
||||||
self.seen_assets: Set[str] = set()
|
self.seen_assets: Set[str] = set()
|
||||||
# A2: single semaphore for the entire run, created lazily inside the event loop
|
|
||||||
self._sem: Optional[asyncio.Semaphore] = None
|
self._sem: Optional[asyncio.Semaphore] = None
|
||||||
self._all_records: List = []
|
self._all_records: List = []
|
||||||
self._dork_hits: List[dict] = []
|
self._dork_hits: List[dict] = []
|
||||||
self._seen_dork_urls: Set[str] = set()
|
self._seen_dork_urls: Set[str] = set()
|
||||||
# A6: scrape_hits merged atomically per _do_process call
|
|
||||||
self._scrape_hits: Dict = {"pastes": [], "credentials": [], "hashes": [],
|
self._scrape_hits: Dict = {"pastes": [], "credentials": [], "hashes": [],
|
||||||
"telegram": [], "dork_misconfigs": []}
|
"telegram": [], "dork_misconfigs": []}
|
||||||
self._max_depth: int = 0
|
self._max_depth: int = 0
|
||||||
self._in_flight: Dict[str, asyncio.Future] = {}
|
self._in_flight: Dict[str, asyncio.Future] = {}
|
||||||
self.pivot_log: List[dict] = []
|
self.pivot_log: List[dict] = []
|
||||||
# A8: global set to prevent duplicate entries in discovered_assets
|
|
||||||
self._seen_discovered: Set[str] = set()
|
self._seen_discovered: Set[str] = set()
|
||||||
self.discovered_assets: List[dict] = []
|
self.discovered_assets: List[dict] = []
|
||||||
|
|
||||||
def _get_sem(self) -> asyncio.Semaphore:
|
def _get_sem(self) -> asyncio.Semaphore:
|
||||||
# A2: semaphore created once per run, shared across all coroutines
|
|
||||||
if self._sem is None:
|
if self._sem is None:
|
||||||
self._sem = asyncio.Semaphore(_cfg_concurrency(self._orc))
|
self._sem = asyncio.Semaphore(_cfg_concurrency(self._orc))
|
||||||
return self._sem
|
return self._sem
|
||||||
|
|
||||||
async def run(self, target: str) -> tuple:
|
async def run(self, target: str) -> tuple:
|
||||||
# A9: respect no_pivot flag from config
|
|
||||||
cfg = getattr(self._orc, "config", None)
|
cfg = getattr(self._orc, "config", None)
|
||||||
no_pivot = getattr(cfg, "no_pivot", False) if cfg else False
|
no_pivot = getattr(cfg, "no_pivot", False) if cfg else False
|
||||||
if no_pivot:
|
if no_pivot:
|
||||||
@@ -196,7 +189,6 @@ class AvalancheScanner:
|
|||||||
async def _process(self, asset: str, depth: int,
|
async def _process(self, asset: str, depth: int,
|
||||||
parent: Optional[str], found_in: str) -> None:
|
parent: Optional[str], found_in: str) -> None:
|
||||||
"""Dedup gate: ensures each asset is processed exactly once."""
|
"""Dedup gate: ensures each asset is processed exactly once."""
|
||||||
# A10: use per-run depth from orchestrator config
|
|
||||||
if depth > _cfg_depth(self._orc):
|
if depth > _cfg_depth(self._orc):
|
||||||
_syslog.debug("avalanche depth cap reached for %s", asset)
|
_syslog.debug("avalanche depth cap reached for %s", asset)
|
||||||
return
|
return
|
||||||
@@ -205,7 +197,7 @@ class AvalancheScanner:
|
|||||||
if not key:
|
if not key:
|
||||||
return
|
return
|
||||||
|
|
||||||
# A1: add to seen_assets FIRST (atomic gate) before any other check.
|
# Add to seen_assets before any await to prevent concurrent duplicates.
|
||||||
# If already present, wait on the in-flight future if one exists, then return.
|
# If already present, wait on the in-flight future if one exists, then return.
|
||||||
if key in self.seen_assets:
|
if key in self.seen_assets:
|
||||||
if key in self._in_flight:
|
if key in self._in_flight:
|
||||||
@@ -235,6 +227,7 @@ class AvalancheScanner:
|
|||||||
finally:
|
finally:
|
||||||
if not fut.done():
|
if not fut.done():
|
||||||
fut.set_result(None)
|
fut.set_result(None)
|
||||||
|
self._in_flight.pop(key, None)
|
||||||
|
|
||||||
# ── Core pipeline ─────────────────────────────────────────────────
|
# ── Core pipeline ─────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -326,7 +319,8 @@ class AvalancheScanner:
|
|||||||
_syslog.warning("SCRAPE_FAIL asset=%s err=%s", asset, exc)
|
_syslog.warning("SCRAPE_FAIL asset=%s err=%s", asset, exc)
|
||||||
scrape_res = {}
|
scrape_res = {}
|
||||||
|
|
||||||
# A6: collect scrape results locally, then merge atomically
|
# Collect scrape results locally then merge into the shared dict.
|
||||||
|
# The event loop is single-threaded so the merge is safe without a lock.
|
||||||
scrape_count = 0
|
scrape_count = 0
|
||||||
local_scrape: Dict = {k: [] for k in self._scrape_hits}
|
local_scrape: Dict = {k: [] for k in self._scrape_hits}
|
||||||
for k in self._scrape_hits:
|
for k in self._scrape_hits:
|
||||||
@@ -336,7 +330,7 @@ class AvalancheScanner:
|
|||||||
item["pivot_depth"] = depth
|
item["pivot_depth"] = depth
|
||||||
local_scrape[k].append(item)
|
local_scrape[k].append(item)
|
||||||
scrape_count += 1
|
scrape_count += 1
|
||||||
# Atomic merge into shared dict (single-threaded event loop — safe)
|
# Merge into shared dict — safe within the single-threaded event loop.
|
||||||
for k, items in local_scrape.items():
|
for k, items in local_scrape.items():
|
||||||
self._scrape_hits[k].extend(items)
|
self._scrape_hits[k].extend(items)
|
||||||
_out("ok" if scrape_count else "dim",
|
_out("ok" if scrape_count else "dim",
|
||||||
@@ -369,6 +363,13 @@ class AvalancheScanner:
|
|||||||
for val, vqtype in _extract_ids_from_text(raw, exclude=asset):
|
for val, vqtype in _extract_ids_from_text(raw, exclude=asset):
|
||||||
if vqtype in _PIVOT_TYPES:
|
if vqtype in _PIVOT_TYPES:
|
||||||
new_ids.append((val, vqtype, "scrape", ref))
|
new_ids.append((val, vqtype, "scrape", ref))
|
||||||
|
for paste in (scrape_res or {}).get("pastes", []):
|
||||||
|
ref = f"paste:{paste.get('id', paste.get('site', 'paste'))}"
|
||||||
|
for matches in (paste.get("patterns") or {}).values():
|
||||||
|
for m in (matches or []):
|
||||||
|
for val, vqtype in _extract_ids_from_text(str(m), exclude=asset):
|
||||||
|
if vqtype in _PIVOT_TYPES:
|
||||||
|
new_ids.append((val, vqtype, "scrape", ref))
|
||||||
for tg in (scrape_res or {}).get("telegram", []):
|
for tg in (scrape_res or {}).get("telegram", []):
|
||||||
ref = f"t.me/{tg.get('channel','')}"
|
ref = f"t.me/{tg.get('channel','')}"
|
||||||
for val, vqtype in _extract_ids_from_text(tg.get("text", ""), exclude=asset):
|
for val, vqtype in _extract_ids_from_text(tg.get("text", ""), exclude=asset):
|
||||||
@@ -393,7 +394,6 @@ class AvalancheScanner:
|
|||||||
queued.add(child_key)
|
queued.add(child_key)
|
||||||
child_entry = {"asset": val, "qtype": vqtype, "found_in": phase, "ref": ref}
|
child_entry = {"asset": val, "qtype": vqtype, "found_in": phase, "ref": ref}
|
||||||
children.append(child_entry)
|
children.append(child_entry)
|
||||||
# A8: prevent duplicate entries in discovered_assets across parallel parents
|
|
||||||
if child_key not in self._seen_discovered:
|
if child_key not in self._seen_discovered:
|
||||||
self._seen_discovered.add(child_key)
|
self._seen_discovered.add(child_key)
|
||||||
self.discovered_assets.append({
|
self.discovered_assets.append({
|
||||||
@@ -412,12 +412,12 @@ class AvalancheScanner:
|
|||||||
self._process(val, depth + 1, parent=asset, found_in=phase)
|
self._process(val, depth + 1, parent=asset, found_in=phase)
|
||||||
)
|
)
|
||||||
|
|
||||||
# A5: run child tasks FIRST, then append pivot_log so the log reflects actual outcomes
|
# Run child tasks before appending to pivot_log so the log reflects actual outcomes.
|
||||||
if child_tasks:
|
if child_tasks:
|
||||||
_out("info", f"{indent} → reinjecting {len(child_tasks)} new asset(s)…")
|
_out("info", f"{indent} → reinjecting {len(child_tasks)} new asset(s)…")
|
||||||
await asyncio.gather(*child_tasks, return_exceptions=True)
|
await asyncio.gather(*child_tasks, return_exceptions=True)
|
||||||
|
|
||||||
# ── Log this node (after children complete — A5) ──────────────
|
# ── Log this node ─────────────────────────────────────────────
|
||||||
self.pivot_log.append({
|
self.pivot_log.append({
|
||||||
"asset": asset,
|
"asset": asset,
|
||||||
"qtype": qtype,
|
"qtype": qtype,
|
||||||
@@ -461,8 +461,8 @@ class AvalancheScanner:
|
|||||||
# ── Scrape dispatcher ─────────────────────────────────────────────
|
# ── Scrape dispatcher ─────────────────────────────────────────────
|
||||||
|
|
||||||
async def _async_scrape(self, asset: str) -> dict:
|
async def _async_scrape(self, asset: str) -> dict:
|
||||||
# A3: instantiate a fresh Session + ScrapeEngine per call to avoid sharing
|
# Instantiate a fresh Session and ScrapeEngine per call — requests.Session
|
||||||
# a non-thread-safe requests.Session / cloudscraper across concurrent coroutines.
|
# and cloudscraper are not safe to share across concurrent coroutines.
|
||||||
_empty: dict = {"pastes": [], "credentials": [], "hashes": [],
|
_empty: dict = {"pastes": [], "credentials": [], "hashes": [],
|
||||||
"telegram": [], "dork_misconfigs": []}
|
"telegram": [], "dork_misconfigs": []}
|
||||||
try:
|
try:
|
||||||
@@ -517,8 +517,7 @@ async def _crack_and_inject(session, hash_value: str, record_ref,
|
|||||||
_out("ok", f" [crack] {hash_value[:16]}… → {plaintext} (from {parent_asset})")
|
_out("ok", f" [crack] {hash_value[:16]}… → {plaintext} (from {parent_asset})")
|
||||||
cracked_out.append(plaintext)
|
cracked_out.append(plaintext)
|
||||||
|
|
||||||
# A4: inject cracked plaintext as qtype="password" — NOT as username.
|
# Inject the cracked plaintext as a password-recycling pivot seed.
|
||||||
# Only pivot on it if sources support password-recycling queries.
|
|
||||||
key = plaintext.lower()
|
key = plaintext.lower()
|
||||||
if key not in seen_assets and depth + 1 <= _cfg_depth(scanner._orc):
|
if key not in seen_assets and depth + 1 <= _cfg_depth(scanner._orc):
|
||||||
await scanner._process(plaintext, depth + 1,
|
await scanner._process(plaintext, depth + 1,
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "hudsonrock_osint",
|
"name": "hudsonrock_osint",
|
||||||
"category": "breach_data",
|
"category": "breach_data",
|
||||||
"endpoint": "https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-login?username={target}",
|
"endpoint": "https://cavalier.hudsonrock.com/api/json/v2/osint-tools/search-by-email?email={target}",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"requires_auth": false,
|
"requires_auth": false,
|
||||||
"selectors": {
|
"selectors": {
|
||||||
"stealers": "$.stealers"
|
"stealers": "$.stealers"
|
||||||
},
|
},
|
||||||
"rate_limit": 1.0,
|
"rate_limit": 30.0,
|
||||||
"headers": {},
|
"headers": {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
||||||
|
},
|
||||||
"api_key_slots": [],
|
"api_key_slots": [],
|
||||||
"input_type": "username",
|
"input_type": "email",
|
||||||
"output_type": [
|
"output_type": [
|
||||||
"email",
|
"email",
|
||||||
"domain"
|
"domain",
|
||||||
|
"username"
|
||||||
],
|
],
|
||||||
"normalization_map": {
|
"normalization_map": {
|
||||||
"stealers": "breach_record"
|
"stealers": "breach_record"
|
||||||
@@ -24,7 +27,8 @@
|
|||||||
],
|
],
|
||||||
"health_check_url": "https://cavalier.hudsonrock.com",
|
"health_check_url": "https://cavalier.hudsonrock.com",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 4,
|
"reliability_score": 3,
|
||||||
|
"is_volatile": true,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
"confidence": 0.85
|
"confidence": 0.7
|
||||||
}
|
}
|
||||||
@@ -40,5 +40,9 @@
|
|||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 5,
|
"reliability_score": 5,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
|
"poll_endpoint": "https://2.intelx.io/intelligent/search/result",
|
||||||
|
"poll_id_field": "id",
|
||||||
|
"poll_id_param": "id",
|
||||||
|
"poll_json_root": "records",
|
||||||
"confidence": 1.0
|
"confidence": 1.0
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "ipapi_is",
|
||||||
|
"category": "geolocation",
|
||||||
|
"endpoint": "https://api.ipapi.is/?q={target}",
|
||||||
|
"method": "GET",
|
||||||
|
"requires_auth": false,
|
||||||
|
"selectors": {
|
||||||
|
"org": "$.org",
|
||||||
|
"asn": "$.asn.asn",
|
||||||
|
"abuse": "$.abuse.email"
|
||||||
|
},
|
||||||
|
"rate_limit": 1.0,
|
||||||
|
"headers": {},
|
||||||
|
"api_key_slots": [],
|
||||||
|
"input_type": "ip",
|
||||||
|
"output_type": [
|
||||||
|
"domain"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"org": "asn_org",
|
||||||
|
"asn": "asn_number",
|
||||||
|
"abuse": "abuse_contact"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive",
|
||||||
|
"fast"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://api.ipapi.is",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 4,
|
||||||
|
"backup_endpoints": [],
|
||||||
|
"confidence": 0.85
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ipinfodb",
|
"name": "ipinfodb",
|
||||||
"category": "geolocation",
|
"category": "geolocation",
|
||||||
"endpoint": "http://api.ipinfodb.com/v3/ip-city/?key={IPINFODB_API_KEY}&ip={target}&format=json",
|
"endpoint": "https://api.ipinfodb.com/v3/ip-city/?key={IPINFODB_API_KEY}&ip={target}&format=json",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"requires_auth": true,
|
"requires_auth": true,
|
||||||
"selectors": {
|
"selectors": {
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"passive"
|
"passive"
|
||||||
],
|
],
|
||||||
"health_check_url": "http://api.ipinfodb.com",
|
"health_check_url": "https://api.ipinfodb.com",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 3,
|
"reliability_score": 3,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ipstack",
|
"name": "ipstack",
|
||||||
"category": "geolocation",
|
"category": "geolocation",
|
||||||
"endpoint": "http://api.ipstack.com/{target}?access_key={IPSTACK_API_KEY}",
|
"endpoint": "https://api.ipstack.com/{target}?access_key={IPSTACK_API_KEY}",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"requires_auth": true,
|
"requires_auth": true,
|
||||||
"selectors": {
|
"selectors": {
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"passive",
|
"passive",
|
||||||
"fast"
|
"fast"
|
||||||
],
|
],
|
||||||
"health_check_url": "http://api.ipstack.com",
|
"health_check_url": "https://api.ipstack.com",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 4,
|
"reliability_score": 4,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
|
|||||||
@@ -3,17 +3,21 @@
|
|||||||
"category": "breaches",
|
"category": "breaches",
|
||||||
"endpoint": "https://leak-lookup.com/api/search",
|
"endpoint": "https://leak-lookup.com/api/search",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"requires_auth": false,
|
"requires_auth": true,
|
||||||
"selectors": {
|
"selectors": {
|
||||||
"results": "$.message"
|
"results": "$.message"
|
||||||
},
|
},
|
||||||
"rate_limit": 1.0,
|
"rate_limit": 1.0,
|
||||||
"headers": {},
|
"headers": {
|
||||||
|
"X-API-Key": "{LEAK_LOOKUP_API_KEY}"
|
||||||
|
},
|
||||||
"payload_template": {
|
"payload_template": {
|
||||||
"query": "{target}",
|
"query": "{target}",
|
||||||
"type": "email_address"
|
"type": "email_address"
|
||||||
},
|
},
|
||||||
"api_key_slots": [],
|
"api_key_slots": [
|
||||||
|
"{LEAK_LOOKUP_API_KEY}"
|
||||||
|
],
|
||||||
"input_type": "email",
|
"input_type": "email",
|
||||||
"output_type": [
|
"output_type": [
|
||||||
"email"
|
"email"
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "leakstats_pw",
|
|
||||||
"category": "breaches",
|
|
||||||
"endpoint": "https://leakstats.net/api/password/{target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": true,
|
|
||||||
"selectors": {
|
|
||||||
"count": "$.count"
|
|
||||||
},
|
|
||||||
"rate_limit": 1.0,
|
|
||||||
"headers": {
|
|
||||||
"api-key": "{LEAKSTATS_API_KEY}"
|
|
||||||
},
|
|
||||||
"api_key_slots": [
|
|
||||||
"{LEAKSTATS_API_KEY}"
|
|
||||||
],
|
|
||||||
"input_type": "hash",
|
|
||||||
"output_type": [
|
|
||||||
"hash"
|
|
||||||
],
|
|
||||||
"normalization_map": {},
|
|
||||||
"tags": [
|
|
||||||
"passive"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://leakstats.net",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 3,
|
|
||||||
"is_volatile": true,
|
|
||||||
"backup_endpoints": [],
|
|
||||||
"confidence": 0.7
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mailboxlayer",
|
"name": "mailboxlayer",
|
||||||
"category": "email_rep",
|
"category": "email_rep",
|
||||||
"endpoint": "http://apilayer.net/api/check?access_key={MAILBOX_API_KEY}&email={target}",
|
"endpoint": "https://apilayer.net/api/check?access_key={MAILBOX_API_KEY}&email={target}",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"requires_auth": true,
|
"requires_auth": true,
|
||||||
"selectors": {
|
"selectors": {
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"passive"
|
"passive"
|
||||||
],
|
],
|
||||||
"health_check_url": "http://apilayer.net",
|
"health_check_url": "https://apilayer.net",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 3,
|
"reliability_score": 3,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "malwarebazaar",
|
||||||
|
"category": "hashes",
|
||||||
|
"endpoint": "https://mb-api.abuse.ch/api/v1/",
|
||||||
|
"method": "POST",
|
||||||
|
"requires_auth": true,
|
||||||
|
"selectors": {
|
||||||
|
"data": "$.data"
|
||||||
|
},
|
||||||
|
"rate_limit": 1.0,
|
||||||
|
"headers": {
|
||||||
|
"API-KEY": "{MALWAREBAZAAR_API_KEY}"
|
||||||
|
},
|
||||||
|
"payload_template": {
|
||||||
|
"query": "get_info",
|
||||||
|
"hash": "{target}"
|
||||||
|
},
|
||||||
|
"api_key_slots": [
|
||||||
|
"{MALWAREBAZAAR_API_KEY}"
|
||||||
|
],
|
||||||
|
"input_type": "hash",
|
||||||
|
"output_type": [
|
||||||
|
"hash"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"file_name": "filename",
|
||||||
|
"tags": "tags"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive",
|
||||||
|
"threat"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://mb-api.abuse.ch",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 5,
|
||||||
|
"backup_endpoints": [],
|
||||||
|
"confidence": 1.0
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
"value": "{target}"
|
"value": "{target}"
|
||||||
},
|
},
|
||||||
"api_key_slots": [
|
"api_key_slots": [
|
||||||
"{MISP_API_KEY}"
|
"{MISP_API_KEY}",
|
||||||
|
"{MISP_URL}"
|
||||||
],
|
],
|
||||||
"input_type": "any",
|
"input_type": "any",
|
||||||
"output_type": [
|
"output_type": [
|
||||||
@@ -30,7 +31,7 @@
|
|||||||
"passive",
|
"passive",
|
||||||
"threat"
|
"threat"
|
||||||
],
|
],
|
||||||
"health_check_url": "{MISP_URL}",
|
"health_check_url": "https://misp.local",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 4,
|
"reliability_score": 4,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "netlas_search",
|
||||||
|
"category": "scanners",
|
||||||
|
"endpoint": "https://app.netlas.io/api/responses/?q={target}&source_type=include&start=0&fields=",
|
||||||
|
"method": "GET",
|
||||||
|
"requires_auth": true,
|
||||||
|
"selectors": {
|
||||||
|
"items": "$.items"
|
||||||
|
},
|
||||||
|
"rate_limit": 1.0,
|
||||||
|
"headers": {
|
||||||
|
"X-API-Key": "{NETLAS_API_KEY}"
|
||||||
|
},
|
||||||
|
"api_key_slots": [
|
||||||
|
"{NETLAS_API_KEY}"
|
||||||
|
],
|
||||||
|
"input_type": "ip",
|
||||||
|
"output_type": [
|
||||||
|
"ip",
|
||||||
|
"domain"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"data.ip": "ip_address",
|
||||||
|
"data.domain": "domain"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive",
|
||||||
|
"infrastructure"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://app.netlas.io",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 4,
|
||||||
|
"backup_endpoints": [],
|
||||||
|
"confidence": 0.85
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "numverify",
|
"name": "numverify",
|
||||||
"category": "phone",
|
"category": "phone",
|
||||||
"endpoint": "http://apilayer.net/api/validate?access_key={NUMVERIFY_API_KEY}&number={target}",
|
"endpoint": "https://apilayer.net/api/validate?access_key={NUMVERIFY_API_KEY}&number={target}",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"requires_auth": true,
|
"requires_auth": true,
|
||||||
"selectors": {
|
"selectors": {
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"passive"
|
"passive"
|
||||||
],
|
],
|
||||||
"health_check_url": "http://apilayer.net",
|
"health_check_url": "https://apilayer.net",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 4,
|
"reliability_score": 4,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
],
|
],
|
||||||
"health_check_url": "https://api.pipl.com",
|
"health_check_url": "https://api.pipl.com",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 4,
|
"reliability_score": 2,
|
||||||
|
"is_volatile": true,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
"confidence": 0.85
|
"confidence": 0.3
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "vigilante_pw",
|
"name": "proxynova_comb",
|
||||||
"category": "breaches",
|
"category": "breaches",
|
||||||
"endpoint": "https://vigilante.pw/api/search?q={target}",
|
"endpoint": "https://api.proxynova.com/comb?query={target}",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"requires_auth": false,
|
"requires_auth": false,
|
||||||
"selectors": {
|
"selectors": {
|
||||||
"results": "$.results"
|
"lines": "$.lines"
|
||||||
},
|
},
|
||||||
"rate_limit": 1.0,
|
"rate_limit": 1.0,
|
||||||
"headers": {},
|
"headers": {},
|
||||||
@@ -14,15 +14,17 @@
|
|||||||
"output_type": [
|
"output_type": [
|
||||||
"email"
|
"email"
|
||||||
],
|
],
|
||||||
"normalization_map": {},
|
"normalization_map": {
|
||||||
|
"lines": "credential_line"
|
||||||
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"passive",
|
"passive",
|
||||||
"stealth"
|
"stealth"
|
||||||
],
|
],
|
||||||
"health_check_url": "https://vigilante.pw",
|
"health_check_url": "https://api.proxynova.com",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 2,
|
"reliability_score": 3,
|
||||||
"is_volatile": true,
|
"is_volatile": true,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
"confidence": 0.55
|
"confidence": 0.7
|
||||||
}
|
}
|
||||||
+10
-8
@@ -1,14 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "pypi_user",
|
"name": "pypi_user",
|
||||||
"category": "social",
|
"category": "social",
|
||||||
"endpoint": "https://pypi.org/pypi/{target}/json",
|
"endpoint": "https://pypi.org/pypi",
|
||||||
"method": "GET",
|
"method": "POST",
|
||||||
"requires_auth": false,
|
"requires_auth": false,
|
||||||
"selectors": {
|
"selectors": {},
|
||||||
"info": "$.info"
|
|
||||||
},
|
|
||||||
"rate_limit": 1.0,
|
"rate_limit": 1.0,
|
||||||
"headers": {},
|
"headers": {
|
||||||
|
"Content-Type": "text/xml"
|
||||||
|
},
|
||||||
|
"raw_payload": "<?xml version='1.0'?><methodCall><methodName>user_packages</methodName><params><param><value>{target}</value></param></params></methodCall>",
|
||||||
|
"regex_pattern": "<string>Owner</string></value>\\s*<value><string>([^<]+)</string>",
|
||||||
"api_key_slots": [],
|
"api_key_slots": [],
|
||||||
"input_type": "username",
|
"input_type": "username",
|
||||||
"output_type": [
|
"output_type": [
|
||||||
@@ -20,7 +22,7 @@
|
|||||||
],
|
],
|
||||||
"health_check_url": "https://pypi.org",
|
"health_check_url": "https://pypi.org",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 5,
|
"reliability_score": 4,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
"confidence": 1.0
|
"confidence": 0.85
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "ripestat_ip",
|
||||||
|
"category": "network",
|
||||||
|
"endpoint": "https://stat.ripe.net/data/prefix-overview/data.json?resource={target}",
|
||||||
|
"method": "GET",
|
||||||
|
"requires_auth": false,
|
||||||
|
"selectors": {
|
||||||
|
"asns": "$.data.asns[*].asn",
|
||||||
|
"holder": "$.data.asns[0].holder"
|
||||||
|
},
|
||||||
|
"rate_limit": 1.0,
|
||||||
|
"headers": {},
|
||||||
|
"api_key_slots": [],
|
||||||
|
"input_type": "ip",
|
||||||
|
"output_type": [
|
||||||
|
"ip"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"asn": "asn_number",
|
||||||
|
"holder": "asn_org"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive",
|
||||||
|
"infrastructure"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://stat.ripe.net",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 5,
|
||||||
|
"backup_endpoints": [],
|
||||||
|
"confidence": 1.0
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "scylla_sh_search",
|
|
||||||
"category": "breaches",
|
|
||||||
"endpoint": "https://scylla.sh/search?q={target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": false,
|
|
||||||
"selectors": {
|
|
||||||
"results": "$.*"
|
|
||||||
},
|
|
||||||
"rate_limit": 1.0,
|
|
||||||
"headers": {},
|
|
||||||
"api_key_slots": [],
|
|
||||||
"input_type": "email",
|
|
||||||
"output_type": [
|
|
||||||
"email",
|
|
||||||
"domain"
|
|
||||||
],
|
|
||||||
"normalization_map": {},
|
|
||||||
"tags": [
|
|
||||||
"passive",
|
|
||||||
"stealth"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://scylla.sh",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 2,
|
|
||||||
"is_volatile": true,
|
|
||||||
"backup_endpoints": [
|
|
||||||
"https://scylla.sh/api/search?q={target}"
|
|
||||||
],
|
|
||||||
"confidence": 0.55
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "seon_email",
|
||||||
|
"category": "enrichment",
|
||||||
|
"endpoint": "https://api.seon.io/SeonRestService/email-api/v3",
|
||||||
|
"method": "POST",
|
||||||
|
"requires_auth": true,
|
||||||
|
"selectors": {
|
||||||
|
"data": "$.data"
|
||||||
|
},
|
||||||
|
"rate_limit": 1.0,
|
||||||
|
"headers": {
|
||||||
|
"X-API-KEY": "{SEON_API_KEY}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"payload_template": {
|
||||||
|
"email": "{target}"
|
||||||
|
},
|
||||||
|
"api_key_slots": [
|
||||||
|
"{SEON_API_KEY}"
|
||||||
|
],
|
||||||
|
"input_type": "email",
|
||||||
|
"output_type": [
|
||||||
|
"email",
|
||||||
|
"domain",
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"email": "email_address",
|
||||||
|
"domain": "domain",
|
||||||
|
"full_name": "full_name",
|
||||||
|
"phone_number": "phone"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://api.seon.io",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 4,
|
||||||
|
"backup_endpoints": [],
|
||||||
|
"confidence": 0.85
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "shodan_internetdb",
|
||||||
|
"category": "scanners",
|
||||||
|
"endpoint": "https://internetdb.shodan.io/{target}",
|
||||||
|
"method": "GET",
|
||||||
|
"requires_auth": false,
|
||||||
|
"selectors": {
|
||||||
|
"hostnames": "$.hostnames",
|
||||||
|
"ports": "$.ports",
|
||||||
|
"vulns": "$.vulns"
|
||||||
|
},
|
||||||
|
"rate_limit": 1.0,
|
||||||
|
"headers": {},
|
||||||
|
"api_key_slots": [],
|
||||||
|
"input_type": "ip",
|
||||||
|
"output_type": [
|
||||||
|
"domain",
|
||||||
|
"ip"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"hostnames": "domain",
|
||||||
|
"vulns": "cve"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive",
|
||||||
|
"fast",
|
||||||
|
"infrastructure"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://internetdb.shodan.io",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 5,
|
||||||
|
"backup_endpoints": [],
|
||||||
|
"confidence": 1.0
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "spycloud_breach",
|
"name": "spycloud_breach",
|
||||||
"category": "breaches",
|
"category": "breaches",
|
||||||
"endpoint": "https://api.spycloud.io/enterprise-v2/breach/data/emails/{target}",
|
"endpoint": "https://api.spycloud.io/enterprise-v2/breach/catalog/emails/{target}",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"requires_auth": true,
|
"requires_auth": true,
|
||||||
"selectors": {
|
"selectors": {
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
],
|
],
|
||||||
"health_check_url": "https://api.spyonweb.com",
|
"health_check_url": "https://api.spyonweb.com",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 3,
|
"reliability_score": 1,
|
||||||
|
"is_volatile": true,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
"confidence": 0.7
|
"confidence": 0.1
|
||||||
}
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "spyse_domain",
|
|
||||||
"category": "scanners",
|
|
||||||
"endpoint": "https://api.spyse.com/v1/domain/details/{target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": true,
|
|
||||||
"selectors": {
|
|
||||||
"asn": "$.data.asn"
|
|
||||||
},
|
|
||||||
"rate_limit": 1.0,
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "Bearer {SPYSE_API_KEY}"
|
|
||||||
},
|
|
||||||
"api_key_slots": [
|
|
||||||
"{SPYSE_API_KEY}"
|
|
||||||
],
|
|
||||||
"input_type": "domain",
|
|
||||||
"output_type": [
|
|
||||||
"ip"
|
|
||||||
],
|
|
||||||
"normalization_map": {},
|
|
||||||
"tags": [
|
|
||||||
"passive"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://api.spyse.com",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 3,
|
|
||||||
"backup_endpoints": [],
|
|
||||||
"confidence": 0.7
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "spyse_ip",
|
|
||||||
"category": "scanners",
|
|
||||||
"endpoint": "https://api.spyse.com/v1/ip/details/{target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": true,
|
|
||||||
"selectors": {
|
|
||||||
"geo": "$.data.geo"
|
|
||||||
},
|
|
||||||
"rate_limit": 1.0,
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "Bearer {SPYSE_API_KEY}"
|
|
||||||
},
|
|
||||||
"api_key_slots": [
|
|
||||||
"{SPYSE_API_KEY}"
|
|
||||||
],
|
|
||||||
"input_type": "ip",
|
|
||||||
"output_type": [
|
|
||||||
"ip"
|
|
||||||
],
|
|
||||||
"normalization_map": {},
|
|
||||||
"tags": [
|
|
||||||
"passive"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://api.spyse.com",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 3,
|
|
||||||
"backup_endpoints": [],
|
|
||||||
"confidence": 0.7
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"rate_limit": 1.0,
|
"rate_limit": 1.0,
|
||||||
"headers": {
|
"headers": {
|
||||||
"Authorization": "TC {TC_API_KEY}:{TC_SIGNATURE}"
|
"Authorization": "TC {TC_API_KEY}"
|
||||||
},
|
},
|
||||||
"api_key_slots": [
|
"api_key_slots": [
|
||||||
"{TC_API_KEY}"
|
"{TC_API_KEY}"
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
],
|
],
|
||||||
"health_check_url": "https://api.threatconnect.com",
|
"health_check_url": "https://api.threatconnect.com",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 4,
|
"reliability_score": 2,
|
||||||
|
"is_volatile": true,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
"confidence": 0.85
|
"confidence": 0.55
|
||||||
}
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "threatcrowd_domain",
|
|
||||||
"category": "threat_intel",
|
|
||||||
"endpoint": "https://www.threatcrowd.org/searchApi/v2/domain/report/?domain={target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": false,
|
|
||||||
"selectors": {
|
|
||||||
"ips": "$.resolutions[*].ip_address"
|
|
||||||
},
|
|
||||||
"rate_limit": 5.0,
|
|
||||||
"headers": {},
|
|
||||||
"api_key_slots": [],
|
|
||||||
"input_type": "domain",
|
|
||||||
"output_type": [
|
|
||||||
"ip"
|
|
||||||
],
|
|
||||||
"normalization_map": {},
|
|
||||||
"tags": [
|
|
||||||
"passive",
|
|
||||||
"threat"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://www.threatcrowd.org",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 3,
|
|
||||||
"is_volatile": true,
|
|
||||||
"bypass_required": [
|
|
||||||
"cloudflare"
|
|
||||||
],
|
|
||||||
"user_agent_type": "browser",
|
|
||||||
"backup_endpoints": [],
|
|
||||||
"confidence": 0.7
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "threatcrowd_email",
|
|
||||||
"category": "threat_intel",
|
|
||||||
"endpoint": "https://www.threatcrowd.org/searchApi/v2/email/report/?email={target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": false,
|
|
||||||
"selectors": {
|
|
||||||
"domains": "$.domains"
|
|
||||||
},
|
|
||||||
"rate_limit": 5.0,
|
|
||||||
"headers": {},
|
|
||||||
"api_key_slots": [],
|
|
||||||
"input_type": "email",
|
|
||||||
"output_type": [
|
|
||||||
"domain"
|
|
||||||
],
|
|
||||||
"normalization_map": {},
|
|
||||||
"tags": [
|
|
||||||
"passive",
|
|
||||||
"threat"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://www.threatcrowd.org",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 3,
|
|
||||||
"is_volatile": true,
|
|
||||||
"bypass_required": [
|
|
||||||
"cloudflare"
|
|
||||||
],
|
|
||||||
"user_agent_type": "browser",
|
|
||||||
"backup_endpoints": [],
|
|
||||||
"confidence": 0.7
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "threatfox",
|
||||||
|
"category": "threat_intel",
|
||||||
|
"endpoint": "https://threatfox-api.abuse.ch/api/v1/",
|
||||||
|
"method": "POST",
|
||||||
|
"requires_auth": true,
|
||||||
|
"selectors": {
|
||||||
|
"results": "$.data"
|
||||||
|
},
|
||||||
|
"rate_limit": 1.0,
|
||||||
|
"headers": {
|
||||||
|
"API-KEY": "{THREATFOX_API_KEY}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
"payload_template": {
|
||||||
|
"query": "search_ioc",
|
||||||
|
"search_term": "{target}"
|
||||||
|
},
|
||||||
|
"api_key_slots": [
|
||||||
|
"{THREATFOX_API_KEY}"
|
||||||
|
],
|
||||||
|
"input_type": "any",
|
||||||
|
"output_type": [
|
||||||
|
"ip",
|
||||||
|
"domain",
|
||||||
|
"hash"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"ioc": "indicator",
|
||||||
|
"malware": "malware_family"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive",
|
||||||
|
"threat"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://threatfox-api.abuse.ch",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 5,
|
||||||
|
"backup_endpoints": [],
|
||||||
|
"confidence": 1.0
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "tines_breach",
|
|
||||||
"category": "breaches",
|
|
||||||
"endpoint": "https://api.tines.com/breaches/{target}",
|
|
||||||
"method": "GET",
|
|
||||||
"requires_auth": true,
|
|
||||||
"selectors": {
|
|
||||||
"breaches": "$.breaches"
|
|
||||||
},
|
|
||||||
"rate_limit": 1.0,
|
|
||||||
"headers": {
|
|
||||||
"Authorization": "Bearer {TINES_API_KEY}"
|
|
||||||
},
|
|
||||||
"api_key_slots": [
|
|
||||||
"{TINES_API_KEY}"
|
|
||||||
],
|
|
||||||
"input_type": "email",
|
|
||||||
"output_type": [
|
|
||||||
"email"
|
|
||||||
],
|
|
||||||
"normalization_map": {},
|
|
||||||
"tags": [
|
|
||||||
"passive"
|
|
||||||
],
|
|
||||||
"health_check_url": "https://api.tines.com",
|
|
||||||
"expected_status": 200,
|
|
||||||
"reliability_score": 3,
|
|
||||||
"backup_endpoints": [],
|
|
||||||
"confidence": 0.7
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,8 @@
|
|||||||
],
|
],
|
||||||
"health_check_url": "https://api.twitter.com",
|
"health_check_url": "https://api.twitter.com",
|
||||||
"expected_status": 200,
|
"expected_status": 200,
|
||||||
"reliability_score": 4,
|
"reliability_score": 1,
|
||||||
|
"is_volatile": true,
|
||||||
"backup_endpoints": [],
|
"backup_endpoints": [],
|
||||||
"confidence": 0.85
|
"confidence": 0.1
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "urlhaus",
|
||||||
|
"category": "threat_intel",
|
||||||
|
"endpoint": "https://urlhaus-api.abuse.ch/v1/host/",
|
||||||
|
"method": "POST",
|
||||||
|
"requires_auth": true,
|
||||||
|
"selectors": {
|
||||||
|
"urls": "$.urls"
|
||||||
|
},
|
||||||
|
"rate_limit": 1.0,
|
||||||
|
"headers": {
|
||||||
|
"Auth-Key": "{URLHAUS_API_KEY}"
|
||||||
|
},
|
||||||
|
"payload_template": {
|
||||||
|
"host": "{target}"
|
||||||
|
},
|
||||||
|
"api_key_slots": [
|
||||||
|
"{URLHAUS_API_KEY}"
|
||||||
|
],
|
||||||
|
"input_type": "domain",
|
||||||
|
"output_type": [
|
||||||
|
"url",
|
||||||
|
"domain"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"url": "malware_url",
|
||||||
|
"threat": "threat_type"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive",
|
||||||
|
"threat"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://urlhaus-api.abuse.ch",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 5,
|
||||||
|
"backup_endpoints": [],
|
||||||
|
"confidence": 1.0
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "xposedornot",
|
||||||
|
"category": "breach_data",
|
||||||
|
"endpoint": "https://api.xposedornot.com/v1/breach-analytics?email={target}",
|
||||||
|
"method": "GET",
|
||||||
|
"requires_auth": false,
|
||||||
|
"selectors": {
|
||||||
|
"breaches": "$.ExposedBreaches.breaches_details"
|
||||||
|
},
|
||||||
|
"rate_limit": 2.0,
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
||||||
|
},
|
||||||
|
"api_key_slots": [],
|
||||||
|
"input_type": "email",
|
||||||
|
"output_type": [
|
||||||
|
"email",
|
||||||
|
"domain"
|
||||||
|
],
|
||||||
|
"normalization_map": {
|
||||||
|
"breach": "breach_name",
|
||||||
|
"domain": "domain",
|
||||||
|
"xposed_date": "breach_date",
|
||||||
|
"xposed_data": "data_types",
|
||||||
|
"password_risk": "password_risk"
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"passive",
|
||||||
|
"stealth"
|
||||||
|
],
|
||||||
|
"health_check_url": "https://api.xposedornot.com",
|
||||||
|
"expected_status": 200,
|
||||||
|
"reliability_score": 4,
|
||||||
|
"backup_endpoints": [],
|
||||||
|
"confidence": 0.75
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user