visidata: add IOC types with cached, throttled lookups

Centralize provider caching and rate-limit handling, then add Domain/URL/Hash IOC types and safer VT/IPInfo key resolution so lookups stay reliable on free-tier APIs.
This commit is contained in:
tobias
2026-02-21 23:10:44 +01:00
parent a931be4707
commit 84d912ac0a
9 changed files with 1048 additions and 173 deletions

View File

@@ -17,6 +17,18 @@ try:
except ModuleNotFoundError:
pass
try:
import plugins.ioc
except ModuleNotFoundError:
pass
# Optional local lookup settings (tokens, key preference, throttling).
# Keep this as a separate module so secrets can stay out of versioned config.
try:
import lookup_config
except ModuleNotFoundError:
pass
from datetime import datetime
import functools
import json
@@ -171,12 +183,23 @@ def vendor(mac):
def _get_vt():
try:
from virus_total_apis import PublicApi as VirusTotalPublicApi
import os.path
with open(os.path.expanduser('~/.virustotal_api_key')) as af:
API_KEY = af.readline().strip()
vt = VirusTotalPublicApi(API_KEY)
api_key = str(
getattr(options, 'tke_vt_api_key', '')
or os.getenv('VT_API_KEY')
or os.getenv('VIRUSTOTAL_API_KEY')
or ''
)
if not api_key:
try:
with open(os.path.expanduser('~/.virustotal_api_key')) as af:
api_key = af.readline().strip()
except Exception:
api_key = ''
if not api_key:
return None
vt = VirusTotalPublicApi(api_key)
return vt
except:
except Exception:
return None
@disk_cache_decorator()
@@ -204,19 +227,20 @@ def dns_lookup(domain, record='A'):
try:
import dns
import dns.resolver as rs
except ModuleNotFoundError:
return "module not available"
try:
# dnspython 2.x prefers resolve(); keep a fallback for older versions.
try:
result = rs.resolve(domain, record)
except AttributeError:
result = rs.query(domain, record)
return ",".join([x.to_text() for x in result])
except dns.resolver.NoAnswer as e:
except dns.resolver.NoAnswer:
return ""
except dns.exception.DNSException as e:
except dns.exception.DNSException:
# return e.msg
return ""
except ModuleNotFoundError:
return "module not available"
@disk_cache_decorator()
def _asn(ip):
@@ -250,14 +274,28 @@ def asn(ip, type="asn"):
@disk_cache_decorator()
def _ipinfo(ip):
token = str(getattr(options, 'tke_ipinfo_token', '') or os.getenv('IPINFO_TOKEN') or '')
url = 'https://ipinfo.io/{}/json'.format(ip)
if token:
url = '{}?token={}'.format(url, token)
try:
import requests
import json
r = requests.get(url='http://ipinfo.io/{}/json'.format(ip))
return r.json()
except json.JSONDecodeError as e:
return None
from plugins.lookupcore import http_get_json
return http_get_json(url, provider='ipinfo')
except ModuleNotFoundError:
try:
import requests
import json
r = requests.get(url=url, timeout=10)
if not r.ok:
return None
return r.json()
except json.JSONDecodeError:
return None
except ModuleNotFoundError:
return None
except Exception:
return None
@functools.lru_cache(maxsize=1000)
@@ -284,7 +322,8 @@ def split_number2ip(number):
@functools.lru_cache(maxsize=1000)
def mx_lookup(domain):
domain = domain.lstrip("www.")
if domain.startswith("www."):
domain = domain[4:]
try:
mxs = dns_lookup(domain, 'MX').split(",")
mxt = [x.split(" ")[1] for x in mxs if len(x.split(" ")) == 2]