import json
import os
import re
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlparse
import requests
BASE_URL = "https://fullhunt.io/api/v1"
API_KEY = os.environ["FULLHUNT_API_KEY"]
COMPANY = os.environ["FULLHUNT_MA_COMPANY"]
CASE_ID = os.environ["FULLHUNT_MA_CASE_ID"]
VERIFIED = {
value.strip().lower()
for value in os.environ.get("FULLHUNT_VERIFIED_DOMAINS", "").split(",")
if value.strip()
}
MAX_VERIFIED = int(os.environ.get("FULLHUNT_MA_MAX_VERIFIED", "25"))
OUTPUT_PATH = Path(os.environ.get("FULLHUNT_MA_REPORT", f"ma-assessment-{CASE_ID}.json"))
HEADERS = {"X-API-KEY": API_KEY}
DOMAIN_PATTERN = re.compile(r"^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$", re.I)
DOMAIN_KEYS = {"domain", "domains", "website", "websites", "homepage", "url"}
def get(session, path, params=None):
response = session.get(
f"{BASE_URL}{path}",
headers=HEADERS,
params=params,
timeout=60,
)
response.raise_for_status()
return response.json()
def normalize_domain(value):
candidate = str(value).strip().lower()
if "://" in candidate:
candidate = urlparse(candidate).hostname or ""
candidate = candidate.split("/")[0].rstrip(".")
return candidate if DOMAIN_PATTERN.fullmatch(candidate) else None
def extract_domains(value, parent_key=None):
found = set()
if isinstance(value, dict):
for key, child in value.items():
found.update(extract_domains(child, str(key).lower()))
elif isinstance(value, list):
for child in value:
found.update(extract_domains(child, parent_key))
elif isinstance(value, str) and parent_key in DOMAIN_KEYS:
domain = normalize_domain(value)
if domain:
found.add(domain)
return found
def write_atomic(value):
temporary = OUTPUT_PATH.with_suffix(OUTPUT_PATH.suffix + ".tmp")
temporary.write_text(json.dumps(value, indent=2, default=str) + "\n", encoding="utf-8")
temporary.replace(OUTPUT_PATH)
def main():
if len(VERIFIED) > MAX_VERIFIED:
raise ValueError("Verified-domain count exceeds FULLHUNT_MA_MAX_VERIFIED")
with requests.Session() as session:
organizations = get(session, "/organizations-db/search", {"query": COMPANY})
domain_collection = get(
session,
"/nexus/domain-collection/company-lookup",
{"query": COMPANY},
)
sources = {
"organizations_database": extract_domains(organizations),
"nexus_domain_collection": extract_domains(domain_collection),
}
candidates = sorted(set().union(*sources.values()))
evidence = {}
for domain in sorted(VERIFIED):
evidence[domain] = {
"relationship_state": "verified",
"domain_details": get(session, f"/domain/{domain}/details"),
"whois": get(session, "/nexus/whois/lookup", {"domain": domain}),
}
report = {
"case_id": CASE_ID,
"company_query": COMPANY,
"collected_at": datetime.now(timezone.utc).isoformat(),
"candidate_domains": [
{
"domain": domain,
"relationship_state": "candidate",
"sources": [name for name, values in sources.items() if domain in values],
}
for domain in candidates
if domain not in VERIFIED
],
"verified_domain_evidence": evidence,
"unmatched_verified_domains": sorted(VERIFIED - set(candidates)),
"raw_discovery": {
"organizations_database": organizations,
"nexus_domain_collection": domain_collection,
},
}
write_atomic(report)
print(
json.dumps(
{
"report": str(OUTPUT_PATH),
"candidates": len(report["candidate_domains"]),
"verified": len(evidence),
"unmatched_verified": report["unmatched_verified_domains"],
},
indent=2,
)
)
if __name__ == "__main__":
main()