faultypacket-audit 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- faultypacket_audit/__init__.py +15 -0
- faultypacket_audit/__main__.py +2 -0
- faultypacket_audit/audit.py +52 -0
- faultypacket_audit/checks/__init__.py +0 -0
- faultypacket_audit/checks/_common.py +36 -0
- faultypacket_audit/checks/_mtasts_engine.py +630 -0
- faultypacket_audit/checks/_spf_engine.py +484 -0
- faultypacket_audit/checks/bimi.py +117 -0
- faultypacket_audit/checks/blocklist.py +92 -0
- faultypacket_audit/checks/bulk_sender.py +173 -0
- faultypacket_audit/checks/caa.py +59 -0
- faultypacket_audit/checks/dane.py +46 -0
- faultypacket_audit/checks/dkim.py +130 -0
- faultypacket_audit/checks/dmarc.py +352 -0
- faultypacket_audit/checks/dnssec.py +33 -0
- faultypacket_audit/checks/lookalike.py +92 -0
- faultypacket_audit/checks/mtasts.py +36 -0
- faultypacket_audit/checks/mx.py +53 -0
- faultypacket_audit/checks/rdns.py +32 -0
- faultypacket_audit/checks/spf.py +278 -0
- faultypacket_audit/cli.py +166 -0
- faultypacket_audit/core/__init__.py +0 -0
- faultypacket_audit/core/finding.py +40 -0
- faultypacket_audit/core/inputs.py +69 -0
- faultypacket_audit/core/resolver.py +151 -0
- faultypacket_audit/core/rules.py +85 -0
- faultypacket_audit/data/__init__.py +0 -0
- faultypacket_audit/data/rules.json +2046 -0
- faultypacket_audit/fixes.py +237 -0
- faultypacket_audit/headers.py +318 -0
- faultypacket_audit/mcp.py +112 -0
- faultypacket_audit/output/__init__.py +0 -0
- faultypacket_audit/output/csvout.py +21 -0
- faultypacket_audit/output/diff.py +74 -0
- faultypacket_audit/output/formats.py +114 -0
- faultypacket_audit/output/html.py +144 -0
- faultypacket_audit/output/jsonout.py +18 -0
- faultypacket_audit/output/sarif.py +54 -0
- faultypacket_audit/output/text.py +47 -0
- faultypacket_audit/tools/__init__.py +0 -0
- faultypacket_audit/tools/spf_tree.py +127 -0
- faultypacket_audit-1.3.0.dist-info/METADATA +228 -0
- faultypacket_audit-1.3.0.dist-info/RECORD +48 -0
- faultypacket_audit-1.3.0.dist-info/WHEEL +5 -0
- faultypacket_audit-1.3.0.dist-info/entry_points.txt +2 -0
- faultypacket_audit-1.3.0.dist-info/licenses/LICENSE +661 -0
- faultypacket_audit-1.3.0.dist-info/licenses/NOTICE +11 -0
- faultypacket_audit-1.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""faultypacket-audit: cited, standards-current mail and DNS hygiene checks.
|
|
2
|
+
|
|
3
|
+
Library use:
|
|
4
|
+
from faultypacket_audit import audit, load_rules, Resolver
|
|
5
|
+
report = audit("example.com", Resolver(doh="https://cloudflare-dns.com/dns-query"), load_rules())
|
|
6
|
+
|
|
7
|
+
Checks are pure functions of (domain, resolver, rules, options); they never print.
|
|
8
|
+
"""
|
|
9
|
+
__version__ = "1.3.0"
|
|
10
|
+
|
|
11
|
+
from .core.resolver import Resolver, Cached # noqa: E402
|
|
12
|
+
from .core.rules import load_rules # noqa: E402
|
|
13
|
+
from .audit import audit, CHECKS # noqa: E402
|
|
14
|
+
|
|
15
|
+
__all__ = ["__version__", "Resolver", "Cached", "load_rules", "audit", "CHECKS"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Run checks for one domain. Pure: returns a report dict, never prints."""
|
|
2
|
+
import datetime as dt
|
|
3
|
+
|
|
4
|
+
from .checks import spf, dmarc, dkim, bulk_sender, mx, mtasts, caa, rdns, dnssec, dane, bimi, blocklist, lookalike
|
|
5
|
+
from .core.finding import worst
|
|
6
|
+
from .core.resolver import Cached
|
|
7
|
+
|
|
8
|
+
CHECKS = {"spf": spf.check, "dmarc": dmarc.check, "dkim": dkim.check, "mx": mx.check,
|
|
9
|
+
"mta-sts": mtasts.check, "dnssec": dnssec.check, "dane": dane.check, "caa": caa.check,
|
|
10
|
+
"rdns": rdns.check, "bimi": bimi.check, "bulk-sender": bulk_sender.check,
|
|
11
|
+
"blocklist": blocklist.check, "lookalike": lookalike.check}
|
|
12
|
+
# order matters: bulk-sender reuses spf, dmarc and dkim data; bimi reuses dmarc
|
|
13
|
+
ORDER = ["spf", "dmarc", "dkim", "mx", "mta-sts", "dnssec", "dane", "caa", "rdns", "bimi", "bulk-sender"]
|
|
14
|
+
# opt-in: they query third parties (blocklists) or many names (lookalike)
|
|
15
|
+
EXTRA = ["blocklist", "lookalike"]
|
|
16
|
+
ALL = ORDER + EXTRA
|
|
17
|
+
INTERNAL = {"spf": "spf.internal", "dmarc": "dmarc.internal", "bulk-sender": "bulk.internal",
|
|
18
|
+
"mta-sts": "mtasts.internal",
|
|
19
|
+
"blocklist": "blocklist.internal", "lookalike": "lookalike.internal"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def now_utc():
|
|
23
|
+
return dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def audit(domain, res, rules, checks=None, options=None):
|
|
27
|
+
"""-> {domain, checked_at_utc, rules_version, status, checks: [CheckResult]}.
|
|
28
|
+
Each check is isolated: an exception becomes a cited TOOL finding for that
|
|
29
|
+
check only."""
|
|
30
|
+
res = res if isinstance(res, Cached) else Cached(res)
|
|
31
|
+
wanted = [c for c in ALL if c in (checks or ORDER)]
|
|
32
|
+
opts = dict(options or {})
|
|
33
|
+
opts["results"] = {}
|
|
34
|
+
out = []
|
|
35
|
+
for name in wanted:
|
|
36
|
+
try:
|
|
37
|
+
r = CHECKS[name](domain, res, rules, opts)
|
|
38
|
+
except Exception as e: # per-check isolation
|
|
39
|
+
r = {"check": name, "domain": domain, "data": {},
|
|
40
|
+
"findings": [rules.finding(name, "error", INTERNAL.get(name, "core.internal"),
|
|
41
|
+
f"{type(e).__name__}: {e}")]}
|
|
42
|
+
r["status"] = worst(f.severity for f in r["findings"])
|
|
43
|
+
opts["results"][name] = r["data"]
|
|
44
|
+
out.append(r)
|
|
45
|
+
from .fixes import suggest
|
|
46
|
+
try:
|
|
47
|
+
fixes = suggest(domain, out)
|
|
48
|
+
except Exception: # a fix suggestion must never break an audit
|
|
49
|
+
fixes = []
|
|
50
|
+
return {"domain": domain, "checked_at_utc": now_utc(), "rules_version": rules.version,
|
|
51
|
+
"resolvers": res.servers, "status": worst(c["status"] for c in out), "checks": out,
|
|
52
|
+
"fixes": fixes}
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Small shared helpers for the DNS checks."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def tags(record):
|
|
5
|
+
"""DKIM-style tag=value list (RFC 6376 3.2) -> ordered dict, lower-case names.
|
|
6
|
+
Duplicate names: first wins, duplicates listed under '__dup__'."""
|
|
7
|
+
out, dup = {}, []
|
|
8
|
+
for part in record.split(";"):
|
|
9
|
+
if "=" not in part:
|
|
10
|
+
continue
|
|
11
|
+
k, _, v = part.partition("=")
|
|
12
|
+
k, v = k.strip().lower(), "".join(v.split())
|
|
13
|
+
if k in out:
|
|
14
|
+
dup.append(k)
|
|
15
|
+
continue
|
|
16
|
+
out[k] = v
|
|
17
|
+
if dup:
|
|
18
|
+
out["__dup__"] = dup
|
|
19
|
+
return out
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def mx_hosts(domain, res):
|
|
23
|
+
"""-> (list of (pref, host) or None on DNS error, status)."""
|
|
24
|
+
vals, st = res.query(domain, "MX")
|
|
25
|
+
if st.startswith("error"):
|
|
26
|
+
return None, st
|
|
27
|
+
out = []
|
|
28
|
+
for v in vals:
|
|
29
|
+
p = v.split()
|
|
30
|
+
if len(p) == 2 and p[0].isdigit():
|
|
31
|
+
out.append((int(p[0]), p[1].lower().rstrip(".")))
|
|
32
|
+
return sorted(out), st
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def result(name, domain, findings, data):
|
|
36
|
+
return {"check": name, "domain": domain, "findings": findings, "data": data}
|