#!/usr/bin/env python3
"""
Can you actually reach an npm maintainer?
If you find a vulnerability in a package, the contact path is whatever the maintainer put in
package.json. This measures whether those addresses can receive mail at all — MX present,
not a null MX (RFC 7505), and the MX hostname actually resolving.
It checks reachability of mail infrastructure only. It sends nothing, and it deliberately
does not record which package maps to which address — the output is counts, plus a
per-package verdict without the address itself.
"""
import json, subprocess, sys, time, urllib.parse, urllib.request
UA = {"User-Agent": "Mozilla/5.0 (compatible; agentatwork/1.0; +https://agentatwork.xyz)"}
OUT = "/var/www/aaw/status/npm-contact.json"
QUERIES = [
"x402", "agent payments", "erc-8004", "ai agent sdk", "crypto wallet sdk",
"ethereum signer", "solana pay", "mcp server", "llm tools", "web3 auth",
]
_dns_cache = {}
def dig(name, rrtype):
key = (name, rrtype)
if key in _dns_cache:
return _dns_cache[key]
try:
out = subprocess.run(["dig", "+short", "+time=3", "+tries=1", name, rrtype],
capture_output=True, text=True, timeout=12).stdout.strip()
except Exception:
out = ""
_dns_cache[key] = out
return out
def classify(email):
"""Return one of: ok | no_mx | null_mx | mx_unresolvable | bad_address"""
if not email or "@" not in email:
return "bad_address"
domain = email.rsplit("@", 1)[1].strip().lower()
if not domain or "." not in domain:
return "bad_address"
mx = dig(domain, "MX")
if not mx:
return "no_mx"
hosts = []
for line in mx.splitlines():
parts = line.split()
if len(parts) == 2:
host = parts[1].rstrip(".")
if host == "":
return "null_mx" # RFC 7505: domain accepts no mail, by declaration
hosts.append(host)
if not hosts:
return "no_mx"
for h in hosts:
if dig(h, "A") or dig(h, "AAAA"):
return "ok"
return "mx_unresolvable"
def get(url):
return json.load(urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=25))
def main():
names = []
for q in QUERIES:
try:
r = get("https://registry.npmjs.org/-/v1/search?text=" +
urllib.parse.quote(q) + "&size=25")
names += [o["package"]["name"] for o in r.get("objects", [])]
except Exception:
pass
time.sleep(0.4)
names = sorted(set(names))
print(f"{len(names)} unique packages", file=sys.stderr)
results, counts = [], {}
for n in names:
try:
d = get("https://registry.npmjs.org/" + urllib.parse.quote(n, safe="@"))
latest = d.get("dist-tags", {}).get("latest")
v = d.get("versions", {}).get(latest, {})
a = v.get("author") or d.get("author") or {}
email = a.get("email") if isinstance(a, dict) else None
bugs = v.get("bugs")
if not email and isinstance(bugs, dict):
email = bugs.get("email")
verdict = "no_email_published" if not email else classify(email)
except Exception:
continue
counts[verdict] = counts.get(verdict, 0) + 1
results.append({"package": n, "version": latest, "verdict": verdict})
time.sleep(0.15)
total = len(results)
reachable = counts.get("ok", 0)
data = {
"measured_utc": time.strftime("%Y-%m-%d %H:%M UTC", time.gmtime()),
"packages_checked": total,
"reachable_by_email": reachable,
"reachable_pct": round(100.0 * reachable / total, 1) if total else 0,
"breakdown": counts,
"queries": QUERIES,
"method": ("package.json author.email or bugs.email; domain must publish an MX that is "
"not a null MX and whose hostname resolves. No mail was sent."),
"results": results,
}
json.dump(data, open(OUT, "w"), indent=1)
print(json.dumps({k: data[k] for k in
("packages_checked", "reachable_by_email", "reachable_pct", "breakdown")}, indent=1))
if __name__ == "__main__":
main()