mirror of
https://github.com/nox-project/nox-framework.git
synced 2026-06-08 16:07:17 +00:00
252 lines
16 KiB
Python
252 lines
16 KiB
Python
"""
|
|
sources/helpers/config_handler.py — NOX Framework
|
|
Unified credential management via ~/.config/nox-cli/apikeys.json (XDG).
|
|
|
|
Priority: environment variable → apikeys.json → None
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Dict, Optional
|
|
|
|
# ── Shared constant — import this everywhere instead of a raw string ───
|
|
UNIVERSAL_PLACEHOLDER = "INSERT_API_KEY_HERE"
|
|
|
|
# ── XDG config path ────────────────────────────────────────────────────
|
|
_CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "nox-cli"
|
|
_APIKEYS_FILE = _CONFIG_DIR / "apikeys.json"
|
|
|
|
# ── Complete service registry ──────────────────────────────────────────
|
|
# Format: key_name → {"display": str, "public": bool}
|
|
# public=True → no key needed, always active
|
|
# public=False → requires a real API key (goes into apikeys.json)
|
|
SERVICE_REGISTRY: Dict[str, Dict] = {
|
|
# ── Public / keyless ──────────────────────────────────────────────
|
|
"alienvault_otx_domain": {"display": "AlienVault OTX (Domain)", "public": True},
|
|
"alienvault_otx_ip": {"display": "AlienVault OTX (IP)", "public": True},
|
|
"alienvault_otx_malware": {"display": "AlienVault OTX (Malware)", "public": True},
|
|
"alienvault_otx_user": {"display": "AlienVault OTX (User)", "public": True},
|
|
"anubis_subdomains": {"display": "Anubis Subdomains", "public": True},
|
|
"bgpview_ip": {"display": "BGPView IP", "public": True},
|
|
"checkleaked": {"display": "CheckLeaked", "public": True},
|
|
"crt_sh": {"display": "crt.sh", "public": True},
|
|
"cve_search": {"display": "CVE Search", "public": True},
|
|
"cxsecurity": {"display": "CXSecurity", "public": True},
|
|
"duckduckgo_api": {"display": "Google / DDG Dorks", "public": True},
|
|
"emailrep_io": {"display": "EmailRep.io", "public": True},
|
|
"github_users": {"display": "GitHub Users", "public": True},
|
|
"gitlab_search": {"display": "GitLab Search", "public": True},
|
|
"gravatar": {"display": "Gravatar", "public": True},
|
|
"hackernews_user": {"display": "HackerNews User", "public": True},
|
|
"hackertarget_dnslookup": {"display": "HackerTarget DNS Lookup", "public": True},
|
|
"hackertarget_hostsearch": {"display": "HackerTarget Host Search", "public": True},
|
|
"hackertarget_reverseip": {"display": "HackerTarget Reverse IP", "public": True},
|
|
"hackertarget_whois": {"display": "WHOIS (HackerTarget)", "public": True},
|
|
"hudsonrock_osint": {"display": "HudsonRock OSINT", "public": True},
|
|
"ipapi_co": {"display": "ipapi.co", "public": True},
|
|
"ipinfo_io": {"display": "IPInfo.io", "public": True},
|
|
"ipvigilante": {"display": "IPVigilante", "public": True},
|
|
"keybase_lookup": {"display": "Keybase Lookup", "public": True},
|
|
"keybase_proofs": {"display": "Keybase Proofs", "public": True},
|
|
"maltiverse_ip": {"display": "Maltiverse IP", "public": True},
|
|
"npm_user": {"display": "NPM User", "public": True},
|
|
"packetstorm": {"display": "PacketStorm", "public": True},
|
|
"phishtank_check": {"display": "PhishTank", "public": True},
|
|
"pulsedive": {"display": "Pulsedive (Free)", "public": True},
|
|
"pypi_user": {"display": "PyPI User", "public": True},
|
|
"reddit_user": {"display": "Reddit User", "public": True},
|
|
"robtex_ip": {"display": "Robtex IP", "public": True},
|
|
"scamwatcher": {"display": "ScamWatcher", "public": True},
|
|
"social_scan": {"display": "Social Scan", "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_ip": {"display": "ThreatMiner (IP)", "public": True},
|
|
"urlscan_search": {"display": "URLScan.io", "public": True},
|
|
"vigilante_pw": {"display": "Vigilante.pw", "public": True},
|
|
"wayback_machine": {"display": "Wayback Machine", "public": True},
|
|
# ── Private / key-required ────────────────────────────────────────
|
|
"ABSTRACT_API_KEY": {"display": "Abstract Email Validation", "public": False},
|
|
"ABUSEIPDB_API_KEY": {"display": "AbuseIPDB", "public": False},
|
|
"ANYRUN_API_KEY": {"display": "Any.run", "public": False},
|
|
"BA_API_KEY": {"display": "BreachAware", "public": False},
|
|
"BD_API_KEY": {"display": "BreachDirectory", "public": False},
|
|
"BINARYEDGE_API_KEY": {"display": "BinaryEdge", "public": False},
|
|
"BING_API_KEY": {"display": "Bing Search API", "public": False},
|
|
"CENSYS_AUTH_BASE64": {"display": "Censys", "public": False},
|
|
"CIRCL_AUTH_BASE64": {"display": "CIRCL.lu PDNS", "public": False},
|
|
"CIT0DAY_API_KEY": {"display": "Cit0day", "public": False},
|
|
"CLEARBIT_API_KEY": {"display": "Clearbit Enrich", "public": False},
|
|
"CRIMINALIP_API_KEY": {"display": "CriminalIP", "public": False},
|
|
"DEHASHED_AUTH_BASE64": {"display": "Dehashed", "public": False},
|
|
"DNSDB_API_KEY": {"display": "DNSDB Passive DNS", "public": False},
|
|
"DT_AUTH_BASE64": {"display": "DomainTools WHOIS", "public": False},
|
|
"EXTREME_API_KEY": {"display": "Extreme IP Lookup", "public": False},
|
|
"FLP_API_KEY": {"display": "FraudLabsPro", "public": False},
|
|
"FOFA_API_KEY": {"display": "FOFA", "public": False},
|
|
"FOFA_EMAIL": {"display": "FOFA (account email)", "public": False},
|
|
"FULLCONTACT_API_KEY": {"display": "FullContact", "public": False},
|
|
"GITHUB_TOKEN": {"display": "GitHub (Code/Repo Search)", "public": False},
|
|
"GOOGLE_API_KEY": {"display": "Google Safe Browsing", "public": False},
|
|
"GOOGLE_CX_KEY": {"display": "Google Custom Search (API key)", "public": False},
|
|
"GOOGLE_CX_ID": {"display": "Google Custom Search (CX ID)", "public": False},
|
|
"GREYNOISE_API_KEY": {"display": "GreyNoise", "public": False},
|
|
"HASHES_API_KEY": {"display": "Hashes.org", "public": False},
|
|
"HIBP_API_KEY": {"display": "HaveIBeenPwned", "public": False},
|
|
"HIPPO_API_KEY": {"display": "EmailHippo", "public": False},
|
|
"HUNTER_API_KEY": {"display": "Hunter.io", "public": False},
|
|
"HYBRID_API_KEY": {"display": "Hybrid Analysis", "public": False},
|
|
"INTELX_API_KEY": {"display": "IntelX", "public": False},
|
|
"INTEZER_API_KEY": {"display": "Intezer", "public": False},
|
|
"IPDATA_API_KEY": {"display": "IPData.co", "public": False},
|
|
"IPGEO_API_KEY": {"display": "IPGeolocation.io", "public": False},
|
|
"IPINFODB_API_KEY": {"display": "IPInfoDB", "public": False},
|
|
"IPQS_API_KEY": {"display": "IPQualityScore", "public": False},
|
|
"IPSTACK_API_KEY": {"display": "IPStack", "public": False},
|
|
"JOE_API_KEY": {"display": "Joe Sandbox", "public": False},
|
|
"LEAKCHECK_API_KEY": {"display": "LeakCheck", "public": False},
|
|
"LEAKIX_API_KEY": {"display": "LeakIX", "public": False},
|
|
"LEAKSTATS_API_KEY": {"display": "LeakStats.pw", "public": False},
|
|
"MAILBOX_API_KEY": {"display": "Mailboxlayer", "public": False},
|
|
"MALSHARE_API_KEY": {"display": "MalShare", "public": False},
|
|
"METADEFENDER_API_KEY": {"display": "MetaDefender", "public": False},
|
|
"MISP_API_KEY": {"display": "MISP", "public": False},
|
|
"NUMVERIFY_API_KEY": {"display": "Numverify", "public": False},
|
|
"ONYPHE_API_KEY": {"display": "Onyphe", "public": False},
|
|
"PASSIVETOTAL_AUTH_BASE64": {"display": "PassiveTotal / RiskIQ", "public": False},
|
|
"PIPL_API_KEY": {"display": "Pipl", "public": False},
|
|
"PULSEDIVE_API_KEY": {"display": "Pulsedive (Premium)", "public": False},
|
|
"RF_TOKEN": {"display": "Recorded Future", "public": False},
|
|
"SECURITYTRAILS_API_KEY": {"display": "SecurityTrails", "public": False},
|
|
"SHODAN_API_KEY": {"display": "Shodan", "public": False},
|
|
"SNUSBASE_API_KEY": {"display": "Snusbase", "public": False},
|
|
"SPYCLOUD_API_KEY": {"display": "SpyCloud", "public": False},
|
|
"SPYONWEB_API_KEY": {"display": "SpyOnWeb", "public": False},
|
|
"SPYSE_API_KEY": {"display": "Spyse", "public": False},
|
|
"TC_API_KEY": {"display": "ThreatConnect", "public": False},
|
|
"TINES_API_KEY": {"display": "Tines Breach", "public": False},
|
|
"TP_API_KEY": {"display": "ThreatPortal", "public": False},
|
|
"TWITTER_BEARER_TOKEN": {"display": "Twitter / X API v2", "public": False},
|
|
"URLVOID_API_KEY": {"display": "URLVoid", "public": False},
|
|
"VIEWDNS_API_KEY": {"display": "ViewDNS", "public": False},
|
|
"VIRUSTOTAL_API_KEY": {"display": "VirusTotal", "public": False},
|
|
"VULNERS_API_KEY": {"display": "Vulners", "public": False},
|
|
"WF_API_KEY": {"display": "WhoisFreaks", "public": False},
|
|
"WHOISXML_API_KEY": {"display": "WhoisXML API", "public": False},
|
|
"WHOXY_API_KEY": {"display": "Whoxy WHOIS", "public": False},
|
|
"ZEROBOUNCE_API_KEY": {"display": "ZeroBounce", "public": False},
|
|
"ZOOMEYE_API_KEY": {"display": "ZoomEye", "public": False},
|
|
# ── Added in v1.0.1 ───────────────────────────────────────────────
|
|
"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},
|
|
}
|
|
|
|
_PRIVATE_KEYS = {k: v for k, v in SERVICE_REGISTRY.items() if not v["public"]}
|
|
|
|
|
|
# ── Store helpers ──────────────────────────────────────────────────────
|
|
|
|
def _default_store() -> Dict[str, str]:
|
|
"""Return a dict of all private service keys set to UNIVERSAL_PLACEHOLDER."""
|
|
return {k: UNIVERSAL_PLACEHOLDER for k in _PRIVATE_KEYS}
|
|
|
|
|
|
def _write_store(data: Dict[str, str]) -> None:
|
|
"""Atomically write data to apikeys.json with chmod 0600."""
|
|
try:
|
|
_CONFIG_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
_CONFIG_DIR.chmod(0o700)
|
|
tmp = _APIKEYS_FILE.with_suffix(".tmp")
|
|
tmp.write_text(json.dumps(data, indent=4, sort_keys=True), encoding="utf-8")
|
|
tmp.replace(_APIKEYS_FILE)
|
|
_APIKEYS_FILE.chmod(0o600)
|
|
except PermissionError as exc:
|
|
raise RuntimeError(f"[config_handler] Cannot write {_APIKEYS_FILE}: {exc}") from exc
|
|
|
|
|
|
def _load_store() -> Dict[str, str]:
|
|
"""Load apikeys.json, creating it with defaults if absent. Self-heals on corrupt files."""
|
|
_CONFIG_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
_CONFIG_DIR.chmod(0o700)
|
|
if not _APIKEYS_FILE.exists():
|
|
print(" \033[92m[+]\033[0m Initializing NOX Environment in ~/.config/nox-cli/")
|
|
_write_store(_default_store())
|
|
return _default_store()
|
|
try:
|
|
text = _APIKEYS_FILE.read_text(encoding="utf-8").strip()
|
|
if not text:
|
|
raise json.JSONDecodeError("Empty file", "", 0)
|
|
data = json.loads(text)
|
|
if not isinstance(data, dict):
|
|
raise json.JSONDecodeError("Root is not a JSON object", text, 0)
|
|
# Back-fill keys added in newer versions
|
|
new_keys = {k: UNIVERSAL_PLACEHOLDER for k in _PRIVATE_KEYS if k not in data}
|
|
if new_keys:
|
|
data.update(new_keys)
|
|
_write_store(data)
|
|
return data
|
|
except json.JSONDecodeError:
|
|
bak = _APIKEYS_FILE.with_suffix(".json.bak")
|
|
_APIKEYS_FILE.rename(bak)
|
|
print(f"[!] Malformed apikeys.json detected — backed up to {bak.name} and reset to defaults.")
|
|
defaults = _default_store()
|
|
_write_store(defaults)
|
|
return defaults
|
|
except PermissionError as exc:
|
|
raise RuntimeError(f"[config_handler] Cannot read {_APIKEYS_FILE}: {exc}") from exc
|
|
|
|
|
|
# ── ConfigManager ──────────────────────────────────────────────────────
|
|
|
|
class ConfigManager:
|
|
"""
|
|
Unified API key manager.
|
|
|
|
Resolution order per key:
|
|
1. Environment variable (exact key name)
|
|
2. ~/.config/nox-cli/apikeys.json
|
|
3. Returns None if value equals UNIVERSAL_PLACEHOLDER or is absent
|
|
"""
|
|
|
|
_cache: Dict[str, Optional[str]] = {}
|
|
_store: Optional[Dict[str, str]] = None
|
|
|
|
@classmethod
|
|
def _get_store(cls) -> Dict[str, str]:
|
|
if cls._store is None:
|
|
cls._store = _load_store()
|
|
return cls._store
|
|
|
|
@classmethod
|
|
def get_key(cls, key_name: str) -> Optional[str]:
|
|
"""Return the configured value, or None if missing/placeholder."""
|
|
if key_name in cls._cache:
|
|
return cls._cache[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
|
|
cls._cache[key_name] = result
|
|
return result
|
|
|
|
# Backward-compatible alias used by nox.py internals
|
|
get = get_key
|
|
|
|
@classmethod
|
|
def set(cls, key_name: str, value: str) -> None:
|
|
"""Persist a key to apikeys.json and update the in-memory cache."""
|
|
store = cls._get_store()
|
|
store[key_name] = value
|
|
_write_store(store)
|
|
cls._cache[key_name] = None if value == UNIVERSAL_PLACEHOLDER else value
|
|
|
|
@classmethod
|
|
def config_path(cls) -> Path:
|
|
return _APIKEYS_FILE
|