domainwalk 0.3.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TU NOMBRE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,350 @@
1
+ Metadata-Version: 2.4
2
+ Name: domainwalk
3
+ Version: 0.3.0
4
+ Summary: CLI que audita la superficie pública de un dominio: DNS, DNSSEC, SPF/DKIM/DMARC, TLS y cabeceras HTTP
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/FrancisRavn/Domainwalk
7
+ Project-URL: Issues, https://github.com/FrancisRavn/Domainwalk/issues
8
+ Keywords: dns,dnssec,spf,dkim,dmarc,tls,ssl,certificate,security-headers,caa,security-txt,domain,audit,cli
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Internet :: Name Service (DNS)
16
+ Classifier: Topic :: Security
17
+ Classifier: Topic :: System :: Systems Administration
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: dnspython>=2.6.1
22
+ Requires-Dist: rich>=13.7.1
23
+ Provides-Extra: crypto
24
+ Requires-Dist: cryptography>=42; extra == "crypto"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.0; extra == "dev"
27
+ Requires-Dist: cryptography>=42; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # domainwalk
31
+
32
+ ![tests](https://github.com/FrancisRavn/Domainwalk/actions/workflows/tests.yml/badge.svg)
33
+ ![python](https://img.shields.io/badge/python-3.11%2B-blue)
34
+ ![license](https://img.shields.io/badge/license-MIT-green)
35
+
36
+ A command line tool that audits the public surface of a domain and tells you what
37
+ is set up correctly and what is not. DNS records, DNSSEC, SPF, DKIM, DMARC, TLS
38
+ certificates, HTTP security headers, `security.txt` and `robots.txt`, in a single
39
+ pass and a single report.
40
+
41
+ Point it at a domain and you get a graded list of findings. Green for what is
42
+ fine, yellow and red for what is not, and for every problem the exact line you
43
+ need to publish to close it.
44
+
45
+ Everything domainwalk reads is already public. Anyone can query your DNS. Your TLS
46
+ certificate goes to whoever connects. Your response headers ship with every page.
47
+ It is a passive audit of visible configuration, which is why you can run it
48
+ against your own domain or against one you are just checking out.
49
+
50
+ ## What it checks
51
+
52
+ Four collectors run against the domain. Each produces findings with a severity
53
+ (`fail`, `warn`, `info`, `ok`), a stable id, and the concrete fix when something
54
+ is wrong.
55
+
56
+ ### DNS and email authentication
57
+
58
+ Resolves `A`, `AAAA`, `MX`, `NS`, `TXT`, `CAA`, `DS` and `DNSKEY`, plus `_dmarc`
59
+ and twelve common DKIM selectors including `default`, `google`, `selector1` and
60
+ `protonmail`.
61
+
62
+ SPF is parsed properly, so a hard fail (`-all`) and a softfail (`~all`) are not
63
+ the same finding. DMARC reports its real policy, because `p=none` monitors
64
+ nothing while `p=reject` is the one that actually stops spoofed mail. DKIM lists
65
+ which selectors are really published, not just whether a record exists.
66
+
67
+ DNSSEC has three states and domainwalk separates all three. No signing at all.
68
+ Signed with a `DS` record in the parent zone. And the trap in the middle, a
69
+ `DNSKEY` published without a `DS`, which means the zone is signed but nobody
70
+ validates the signature.
71
+
72
+ Every query runs in parallel, one resolver per thread.
73
+
74
+ ### TLS certificates
75
+
76
+ Connects on port 443 and reads issuer, subject, SANs, validity window and
77
+ negotiated protocol version.
78
+
79
+ The interesting part is what happens when OpenSSL rejects the certificate. Most
80
+ tools stop there and hand you a cryptic handshake error. domainwalk reconnects
81
+ without verification, only to read the certificate anyway and explain what went
82
+ wrong. An expired certificate tells you how many days ago it died and who issued
83
+ it. A hostname mismatch tells you which names the certificate does cover.
84
+
85
+ Wildcard matching follows the real rules. `*.example.com` covers `a.example.com`
86
+ but not `example.com` and not `a.b.example.com`.
87
+
88
+ Expiry thresholds scale with the lifetime of the certificate. A fixed 45 day
89
+ warning flags every healthy 90 day ACME certificate for half its life, since
90
+ normal renewal passes through that window on every cycle. A 90 day certificate
91
+ warns under 14 days and fails under 6. A 398 day certificate keeps the classic
92
+ 45 and 21.
93
+
94
+ ### HTTP and security headers
95
+
96
+ Checks whether port 80 redirects to HTTPS without following the redirect. That
97
+ detail matters. If you follow it, a broken certificate on the HTTPS side gets
98
+ reported as "port 80 did not answer", which is false and sends you looking in the
99
+ wrong place. Two different problems, two different findings.
100
+
101
+ Then it fetches over HTTPS and evaluates `Strict-Transport-Security`,
102
+ `Content-Security-Policy`, `X-Content-Type-Options`, `X-Frame-Options` (a CSP
103
+ `frame-ancestors` directive counts as equivalent), `Referrer-Policy` and the
104
+ cross-origin family.
105
+
106
+ ### Well-known paths
107
+
108
+ Looks for `/.well-known/security.txt` and verifies it carries a `Contact:` line
109
+ instead of trusting a bare 200. Also checks `/robots.txt`.
110
+
111
+ When TLS fails to verify, every HTTPS request would fail with the same error, so
112
+ those checks come back marked as not evaluated. One problem, one line, instead of
113
+ the same OpenSSL message copied across four findings.
114
+
115
+ ## What makes the report usable
116
+
117
+ **It hands you the fix.** "Missing Referrer-Policy" sends you to a search engine.
118
+ `Referrer-Policy: strict-origin-when-cross-origin` is a line you paste into a
119
+ config. Every actionable finding carries the literal value or the concrete step.
120
+
121
+ **It knows a fact from a problem.** A domain with no `MX` is not broken, it just
122
+ does not receive mail. A missing `robots.txt` is not a security issue. Those are
123
+ `info` and they never drag the grade down, so a well configured domain comes back
124
+ clean. That is what makes the red mean something when it shows up.
125
+
126
+ **It explains the failure instead of reporting it.** The certificate that OpenSSL
127
+ rejects is the clearest case. Knowing that a handshake failed is nearly useless.
128
+ Knowing it expired 4,157 days ago and was issued by COMODO tells you the whole
129
+ story.
130
+
131
+ **It is honest about scope.** domainwalk is not a vulnerability scanner, a port
132
+ scanner or a fuzzer. It does not read code, audit dependencies or hunt CVEs, and
133
+ it never sends a single request that a browser would not send. It checks
134
+ configuration, meaning things you fix by publishing a DNS record or adding a line
135
+ to a server config.
136
+
137
+ ## Install
138
+
139
+ Python 3.11 or newer. On Debian and Ubuntu, install `python3-venv` first, since
140
+ those distros ship it separately and block `pip` outside a virtualenv.
141
+
142
+ ```bash
143
+ python3 -m venv .venv
144
+ source .venv/bin/activate
145
+ pip install "domainwalk[crypto] @ git+https://github.com/FrancisRavn/Domainwalk.git"
146
+ ```
147
+
148
+ Or from a clone, which is what you want if you plan to change anything.
149
+
150
+ ```bash
151
+ git clone https://github.com/FrancisRavn/Domainwalk.git
152
+ cd Domainwalk
153
+ python3 -m venv .venv
154
+ source .venv/bin/activate
155
+ pip install -e ".[crypto]"
156
+ ```
157
+
158
+ Two runtime dependencies, `dnspython` and `rich`. The `crypto` extra pulls in
159
+ `cryptography` and is recommended. Without it, reading a certificate that OpenSSL
160
+ rejected falls back to a private CPython API that works today but carries no
161
+ stability promise.
162
+
163
+ ## Usage
164
+
165
+ ```bash
166
+ domainwalk example.com
167
+ domainwalk example.com --json
168
+ domainwalk example.com --json -o report.json
169
+ domainwalk example.com --timeout 8
170
+ ```
171
+
172
+ Input is normalized, so all of these end up the same. A bare domain, a full URL
173
+ with path and query string, a `host:port` pair, a trailing dot, or an
174
+ internationalized name like `dominó.es`, which becomes `xn--domin-4ta.es`.
175
+
176
+ Every run makes real DNS queries and real HTTP requests against the domain you
177
+ pass. Nothing else, and nothing an ordinary browser would not do.
178
+
179
+ ### Sample output
180
+
181
+ ```
182
+ domainwalk cloudflare.com OK
183
+ ok=19 warn=0 fail=0 info=1 | 2026-08-30T08:54:47+00:00
184
+
185
+ level id detail
186
+ INFO tls.san Covers cloudflare.com but not www.cloudflare.com
187
+ OK dns.address A=2 AAAA=2
188
+ OK dns.caa 0 iodef "mailto:tls-abuse@cloudflare.com"; 0 issue "comodoca.com"; ...
189
+ OK dns.dnssec DS published (1), DNSKEY=2
190
+ OK dns.mx 4 MX
191
+ OK hdr.frame SAMEORIGIN
192
+ OK hdr.hsts max-age=31536000; includeSubDomains
193
+ OK hdr.referrer strict-origin-when-cross-origin
194
+ OK hdr.xcto nosniff
195
+ OK http.redirect HTTP 301 -> https://www.cloudflare.com/
196
+ OK https.status HTTPS 200
197
+ OK mail.dkim Selectors: k1, s1
198
+ OK mail.dmarc v=DMARC1; p=reject; sp=reject; adkim=r; aspf=r; pct=100; ...
199
+ OK tls.expiry Expires in 37 days (2026-10-06T22:47:27+00:00) - 90d lifetime
200
+ OK tls.version TLSv1.3
201
+ OK wk.robots_txt https://cloudflare.com/robots.txt
202
+ OK wk.security_txt https://cloudflare.com/.well-known/security.txt
203
+ ```
204
+
205
+ And a domain with problems, where the *How to fix* section does the real work.
206
+
207
+ ```
208
+ domainwalk example.com FAIL
209
+ ok=11 warn=6 fail=1 info=1 | 2026-08-30T09:12:04+00:00
210
+
211
+ level id detail
212
+ FAIL hdr.hsts No Strict-Transport-Security
213
+ WARN dns.caa No CAA
214
+ WARN dns.dnssec No DNSSEC
215
+ WARN hdr.csp No Content-Security-Policy
216
+ WARN mail.spf SPF softfail: v=spf1 include:_spf.example.net ~all
217
+ WARN wk.security_txt https://example.com/.well-known/security.txt -> 404
218
+ OK tls.expiry Expires in 89 days (2026-11-27T11:37:46+00:00) - 90d lifetime
219
+ OK tls.san example.com, www.example.com
220
+
221
+ How to fix
222
+ hdr.hsts Strict-Transport-Security: max-age=63072000; includeSubDomains
223
+ dns.caa Add CAA: 0 issue "letsencrypt.org" (adjust for your CA) to limit who can issue.
224
+ dns.dnssec Enable it at your registrar and publish the DS in the parent zone.
225
+ hdr.csp Content-Security-Policy: default-src 'self'; frame-ancestors 'none'
226
+ mail.spf Switch ~all to -all once you confirm all legitimate mail passes.
227
+ wk.security_txt Publish /.well-known/security.txt with Contact: and Expires: lines.
228
+ ```
229
+
230
+ Broken TLS is worth seeing too.
231
+
232
+ ```
233
+ domainwalk expired.badssl.com FAIL
234
+
235
+ level id detail
236
+ FAIL tls.expiry Expired 4157 days ago (2015-04-12T23:59:59+00:00)
237
+ FAIL tls.verify Invalid chain: certificate has expired
238
+ INFO https.skipped Not evaluated, the certificate does not validate
239
+ INFO wk.skipped Not evaluated, the certificate does not validate
240
+ OK http.redirect HTTP 301 -> https://expired.badssl.com/
241
+ ```
242
+
243
+ ### Comparing two runs
244
+
245
+ Save a report now, compare against it later. Useful when you are fixing a domain
246
+ and want to confirm what actually moved, or when you revisit a domain you audited
247
+ months ago.
248
+
249
+ ```bash
250
+ domainwalk example.com -o audits/example-2026-08.json
251
+ domainwalk example.com --diff audits/example-2026-08.json
252
+ ```
253
+
254
+ The comparison flags severity changes as regressions or improvements, findings
255
+ that appeared or disappeared, and DNS records added or removed.
256
+
257
+ ```
258
+ domainwalk diff example.com
259
+ 2026-08-01T07:00:11+00:00 -> 2026-09-01T07:00:09+00:00
260
+
261
+ Severity changes
262
+ ^ dns.caa warn -> ok 0 issue "letsencrypt.org"
263
+ v mail.dmarc ok -> warn DMARC p=none: v=DMARC1; p=none
264
+
265
+ DNS records
266
+ + caa 0 issue "letsencrypt.org"
267
+ - dmarc v=DMARC1; p=quarantine
268
+ ```
269
+
270
+ Output is deterministic. Record lists are sorted and hostnames normalized, so the
271
+ RRset rotation your resolver performs on every query never shows up as a fake
272
+ change. `-o` always writes the plain report, even alongside `--diff`, so any
273
+ saved report works as a baseline later. The comparison itself goes to
274
+ `--diff-output`, or to stdout with `--json`.
275
+
276
+ ### Muting findings you cannot fix
277
+
278
+ Some findings are real but unfixable in a given setup. You cannot set custom
279
+ response headers on GitHub Pages, for example. Drop a `.domainwalk.toml` in the
280
+ working directory, or in `~/.config/domainwalk/config.toml`.
281
+
282
+ ```toml
283
+ timeout = 8.0
284
+
285
+ [mute]
286
+ "hdr.*" = "GitHub Pages does not allow custom response headers"
287
+ ```
288
+
289
+ Muted findings fall to `INFO`, stop counting toward the grade, and print with
290
+ their reason attached, so months later you know why they are quiet. Patterns
291
+ work, so `hdr.*` covers every header check. Use `--no-config` to ignore
292
+ configuration entirely.
293
+
294
+ ## Exit codes
295
+
296
+ - `0` nothing red
297
+ - `1` at least one failure
298
+ - `2` usage error, unreadable config, or the domain does not resolve
299
+
300
+ ## JSON output
301
+
302
+ `--json` prints the full report, including everything the terminal view trims.
303
+ Complete header values, all SANs, every TXT record. Finding ids are stable and
304
+ language independent, so they are what any script should key off.
305
+
306
+ ```bash
307
+ domainwalk example.com --json | jq '.summary'
308
+ domainwalk example.com --json | jq '.dns.findings[] | select(.level == "fail")'
309
+ ```
310
+
311
+ ## Tests
312
+
313
+ ```bash
314
+ pip install -e ".[dev]"
315
+ pytest
316
+ ```
317
+
318
+ Nothing touches the network. The TLS and redirect tests spin up local servers on
319
+ ephemeral ports. The certificate in `tests/fixtures/` is self signed and exists
320
+ only for that, and its private key protects nothing.
321
+
322
+ The suite covers domain normalization, wildcard SAN matching, expiry threshold
323
+ scaling, record sorting, mute semantics, diff behavior, and three integration
324
+ paths. Reading a certificate OpenSSL rejected, reporting a redirect without
325
+ following it, and checking that both certificate decoders agree so results do not
326
+ depend on which extras are installed.
327
+
328
+ ## Implementation notes
329
+
330
+ - Certificates are decoded with `cryptography` when available. The fallback is
331
+ `ssl._ssl._test_decode_cert`, a private CPython API that works on 3.11 to 3.13
332
+ with no promises beyond that. A test asserts both paths return identical fields.
333
+ - Certificate dates use `ssl.cert_time_to_seconds`, which hardcodes month names
334
+ and ignores the locale. Parsing with `strptime` and `%b` raises `ValueError`
335
+ under a non English `LC_TIME`, which is a fun one to debug in production.
336
+ - DNS queries and the two network phases run in parallel. Finding order is
337
+ computed at print time, so it never depends on which check finishes first.
338
+ - Hostnames are lowercased for `MX`, `NS`, `CNAME`, `DS` and `PTR`. `CAA` values
339
+ are left alone since their parameters can be case sensitive.
340
+
341
+ ## Contributing
342
+
343
+ Issues and pull requests are welcome. New checks should follow the existing
344
+ shape, a stable id, a severity that reflects real impact, and a `fix` that tells
345
+ the user exactly what to publish. If a finding cannot be acted on, it is probably
346
+ `info`.
347
+
348
+ ## License
349
+
350
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,321 @@
1
+ # domainwalk
2
+
3
+ ![tests](https://github.com/FrancisRavn/Domainwalk/actions/workflows/tests.yml/badge.svg)
4
+ ![python](https://img.shields.io/badge/python-3.11%2B-blue)
5
+ ![license](https://img.shields.io/badge/license-MIT-green)
6
+
7
+ A command line tool that audits the public surface of a domain and tells you what
8
+ is set up correctly and what is not. DNS records, DNSSEC, SPF, DKIM, DMARC, TLS
9
+ certificates, HTTP security headers, `security.txt` and `robots.txt`, in a single
10
+ pass and a single report.
11
+
12
+ Point it at a domain and you get a graded list of findings. Green for what is
13
+ fine, yellow and red for what is not, and for every problem the exact line you
14
+ need to publish to close it.
15
+
16
+ Everything domainwalk reads is already public. Anyone can query your DNS. Your TLS
17
+ certificate goes to whoever connects. Your response headers ship with every page.
18
+ It is a passive audit of visible configuration, which is why you can run it
19
+ against your own domain or against one you are just checking out.
20
+
21
+ ## What it checks
22
+
23
+ Four collectors run against the domain. Each produces findings with a severity
24
+ (`fail`, `warn`, `info`, `ok`), a stable id, and the concrete fix when something
25
+ is wrong.
26
+
27
+ ### DNS and email authentication
28
+
29
+ Resolves `A`, `AAAA`, `MX`, `NS`, `TXT`, `CAA`, `DS` and `DNSKEY`, plus `_dmarc`
30
+ and twelve common DKIM selectors including `default`, `google`, `selector1` and
31
+ `protonmail`.
32
+
33
+ SPF is parsed properly, so a hard fail (`-all`) and a softfail (`~all`) are not
34
+ the same finding. DMARC reports its real policy, because `p=none` monitors
35
+ nothing while `p=reject` is the one that actually stops spoofed mail. DKIM lists
36
+ which selectors are really published, not just whether a record exists.
37
+
38
+ DNSSEC has three states and domainwalk separates all three. No signing at all.
39
+ Signed with a `DS` record in the parent zone. And the trap in the middle, a
40
+ `DNSKEY` published without a `DS`, which means the zone is signed but nobody
41
+ validates the signature.
42
+
43
+ Every query runs in parallel, one resolver per thread.
44
+
45
+ ### TLS certificates
46
+
47
+ Connects on port 443 and reads issuer, subject, SANs, validity window and
48
+ negotiated protocol version.
49
+
50
+ The interesting part is what happens when OpenSSL rejects the certificate. Most
51
+ tools stop there and hand you a cryptic handshake error. domainwalk reconnects
52
+ without verification, only to read the certificate anyway and explain what went
53
+ wrong. An expired certificate tells you how many days ago it died and who issued
54
+ it. A hostname mismatch tells you which names the certificate does cover.
55
+
56
+ Wildcard matching follows the real rules. `*.example.com` covers `a.example.com`
57
+ but not `example.com` and not `a.b.example.com`.
58
+
59
+ Expiry thresholds scale with the lifetime of the certificate. A fixed 45 day
60
+ warning flags every healthy 90 day ACME certificate for half its life, since
61
+ normal renewal passes through that window on every cycle. A 90 day certificate
62
+ warns under 14 days and fails under 6. A 398 day certificate keeps the classic
63
+ 45 and 21.
64
+
65
+ ### HTTP and security headers
66
+
67
+ Checks whether port 80 redirects to HTTPS without following the redirect. That
68
+ detail matters. If you follow it, a broken certificate on the HTTPS side gets
69
+ reported as "port 80 did not answer", which is false and sends you looking in the
70
+ wrong place. Two different problems, two different findings.
71
+
72
+ Then it fetches over HTTPS and evaluates `Strict-Transport-Security`,
73
+ `Content-Security-Policy`, `X-Content-Type-Options`, `X-Frame-Options` (a CSP
74
+ `frame-ancestors` directive counts as equivalent), `Referrer-Policy` and the
75
+ cross-origin family.
76
+
77
+ ### Well-known paths
78
+
79
+ Looks for `/.well-known/security.txt` and verifies it carries a `Contact:` line
80
+ instead of trusting a bare 200. Also checks `/robots.txt`.
81
+
82
+ When TLS fails to verify, every HTTPS request would fail with the same error, so
83
+ those checks come back marked as not evaluated. One problem, one line, instead of
84
+ the same OpenSSL message copied across four findings.
85
+
86
+ ## What makes the report usable
87
+
88
+ **It hands you the fix.** "Missing Referrer-Policy" sends you to a search engine.
89
+ `Referrer-Policy: strict-origin-when-cross-origin` is a line you paste into a
90
+ config. Every actionable finding carries the literal value or the concrete step.
91
+
92
+ **It knows a fact from a problem.** A domain with no `MX` is not broken, it just
93
+ does not receive mail. A missing `robots.txt` is not a security issue. Those are
94
+ `info` and they never drag the grade down, so a well configured domain comes back
95
+ clean. That is what makes the red mean something when it shows up.
96
+
97
+ **It explains the failure instead of reporting it.** The certificate that OpenSSL
98
+ rejects is the clearest case. Knowing that a handshake failed is nearly useless.
99
+ Knowing it expired 4,157 days ago and was issued by COMODO tells you the whole
100
+ story.
101
+
102
+ **It is honest about scope.** domainwalk is not a vulnerability scanner, a port
103
+ scanner or a fuzzer. It does not read code, audit dependencies or hunt CVEs, and
104
+ it never sends a single request that a browser would not send. It checks
105
+ configuration, meaning things you fix by publishing a DNS record or adding a line
106
+ to a server config.
107
+
108
+ ## Install
109
+
110
+ Python 3.11 or newer. On Debian and Ubuntu, install `python3-venv` first, since
111
+ those distros ship it separately and block `pip` outside a virtualenv.
112
+
113
+ ```bash
114
+ python3 -m venv .venv
115
+ source .venv/bin/activate
116
+ pip install "domainwalk[crypto] @ git+https://github.com/FrancisRavn/Domainwalk.git"
117
+ ```
118
+
119
+ Or from a clone, which is what you want if you plan to change anything.
120
+
121
+ ```bash
122
+ git clone https://github.com/FrancisRavn/Domainwalk.git
123
+ cd Domainwalk
124
+ python3 -m venv .venv
125
+ source .venv/bin/activate
126
+ pip install -e ".[crypto]"
127
+ ```
128
+
129
+ Two runtime dependencies, `dnspython` and `rich`. The `crypto` extra pulls in
130
+ `cryptography` and is recommended. Without it, reading a certificate that OpenSSL
131
+ rejected falls back to a private CPython API that works today but carries no
132
+ stability promise.
133
+
134
+ ## Usage
135
+
136
+ ```bash
137
+ domainwalk example.com
138
+ domainwalk example.com --json
139
+ domainwalk example.com --json -o report.json
140
+ domainwalk example.com --timeout 8
141
+ ```
142
+
143
+ Input is normalized, so all of these end up the same. A bare domain, a full URL
144
+ with path and query string, a `host:port` pair, a trailing dot, or an
145
+ internationalized name like `dominó.es`, which becomes `xn--domin-4ta.es`.
146
+
147
+ Every run makes real DNS queries and real HTTP requests against the domain you
148
+ pass. Nothing else, and nothing an ordinary browser would not do.
149
+
150
+ ### Sample output
151
+
152
+ ```
153
+ domainwalk cloudflare.com OK
154
+ ok=19 warn=0 fail=0 info=1 | 2026-08-30T08:54:47+00:00
155
+
156
+ level id detail
157
+ INFO tls.san Covers cloudflare.com but not www.cloudflare.com
158
+ OK dns.address A=2 AAAA=2
159
+ OK dns.caa 0 iodef "mailto:tls-abuse@cloudflare.com"; 0 issue "comodoca.com"; ...
160
+ OK dns.dnssec DS published (1), DNSKEY=2
161
+ OK dns.mx 4 MX
162
+ OK hdr.frame SAMEORIGIN
163
+ OK hdr.hsts max-age=31536000; includeSubDomains
164
+ OK hdr.referrer strict-origin-when-cross-origin
165
+ OK hdr.xcto nosniff
166
+ OK http.redirect HTTP 301 -> https://www.cloudflare.com/
167
+ OK https.status HTTPS 200
168
+ OK mail.dkim Selectors: k1, s1
169
+ OK mail.dmarc v=DMARC1; p=reject; sp=reject; adkim=r; aspf=r; pct=100; ...
170
+ OK tls.expiry Expires in 37 days (2026-10-06T22:47:27+00:00) - 90d lifetime
171
+ OK tls.version TLSv1.3
172
+ OK wk.robots_txt https://cloudflare.com/robots.txt
173
+ OK wk.security_txt https://cloudflare.com/.well-known/security.txt
174
+ ```
175
+
176
+ And a domain with problems, where the *How to fix* section does the real work.
177
+
178
+ ```
179
+ domainwalk example.com FAIL
180
+ ok=11 warn=6 fail=1 info=1 | 2026-08-30T09:12:04+00:00
181
+
182
+ level id detail
183
+ FAIL hdr.hsts No Strict-Transport-Security
184
+ WARN dns.caa No CAA
185
+ WARN dns.dnssec No DNSSEC
186
+ WARN hdr.csp No Content-Security-Policy
187
+ WARN mail.spf SPF softfail: v=spf1 include:_spf.example.net ~all
188
+ WARN wk.security_txt https://example.com/.well-known/security.txt -> 404
189
+ OK tls.expiry Expires in 89 days (2026-11-27T11:37:46+00:00) - 90d lifetime
190
+ OK tls.san example.com, www.example.com
191
+
192
+ How to fix
193
+ hdr.hsts Strict-Transport-Security: max-age=63072000; includeSubDomains
194
+ dns.caa Add CAA: 0 issue "letsencrypt.org" (adjust for your CA) to limit who can issue.
195
+ dns.dnssec Enable it at your registrar and publish the DS in the parent zone.
196
+ hdr.csp Content-Security-Policy: default-src 'self'; frame-ancestors 'none'
197
+ mail.spf Switch ~all to -all once you confirm all legitimate mail passes.
198
+ wk.security_txt Publish /.well-known/security.txt with Contact: and Expires: lines.
199
+ ```
200
+
201
+ Broken TLS is worth seeing too.
202
+
203
+ ```
204
+ domainwalk expired.badssl.com FAIL
205
+
206
+ level id detail
207
+ FAIL tls.expiry Expired 4157 days ago (2015-04-12T23:59:59+00:00)
208
+ FAIL tls.verify Invalid chain: certificate has expired
209
+ INFO https.skipped Not evaluated, the certificate does not validate
210
+ INFO wk.skipped Not evaluated, the certificate does not validate
211
+ OK http.redirect HTTP 301 -> https://expired.badssl.com/
212
+ ```
213
+
214
+ ### Comparing two runs
215
+
216
+ Save a report now, compare against it later. Useful when you are fixing a domain
217
+ and want to confirm what actually moved, or when you revisit a domain you audited
218
+ months ago.
219
+
220
+ ```bash
221
+ domainwalk example.com -o audits/example-2026-08.json
222
+ domainwalk example.com --diff audits/example-2026-08.json
223
+ ```
224
+
225
+ The comparison flags severity changes as regressions or improvements, findings
226
+ that appeared or disappeared, and DNS records added or removed.
227
+
228
+ ```
229
+ domainwalk diff example.com
230
+ 2026-08-01T07:00:11+00:00 -> 2026-09-01T07:00:09+00:00
231
+
232
+ Severity changes
233
+ ^ dns.caa warn -> ok 0 issue "letsencrypt.org"
234
+ v mail.dmarc ok -> warn DMARC p=none: v=DMARC1; p=none
235
+
236
+ DNS records
237
+ + caa 0 issue "letsencrypt.org"
238
+ - dmarc v=DMARC1; p=quarantine
239
+ ```
240
+
241
+ Output is deterministic. Record lists are sorted and hostnames normalized, so the
242
+ RRset rotation your resolver performs on every query never shows up as a fake
243
+ change. `-o` always writes the plain report, even alongside `--diff`, so any
244
+ saved report works as a baseline later. The comparison itself goes to
245
+ `--diff-output`, or to stdout with `--json`.
246
+
247
+ ### Muting findings you cannot fix
248
+
249
+ Some findings are real but unfixable in a given setup. You cannot set custom
250
+ response headers on GitHub Pages, for example. Drop a `.domainwalk.toml` in the
251
+ working directory, or in `~/.config/domainwalk/config.toml`.
252
+
253
+ ```toml
254
+ timeout = 8.0
255
+
256
+ [mute]
257
+ "hdr.*" = "GitHub Pages does not allow custom response headers"
258
+ ```
259
+
260
+ Muted findings fall to `INFO`, stop counting toward the grade, and print with
261
+ their reason attached, so months later you know why they are quiet. Patterns
262
+ work, so `hdr.*` covers every header check. Use `--no-config` to ignore
263
+ configuration entirely.
264
+
265
+ ## Exit codes
266
+
267
+ - `0` nothing red
268
+ - `1` at least one failure
269
+ - `2` usage error, unreadable config, or the domain does not resolve
270
+
271
+ ## JSON output
272
+
273
+ `--json` prints the full report, including everything the terminal view trims.
274
+ Complete header values, all SANs, every TXT record. Finding ids are stable and
275
+ language independent, so they are what any script should key off.
276
+
277
+ ```bash
278
+ domainwalk example.com --json | jq '.summary'
279
+ domainwalk example.com --json | jq '.dns.findings[] | select(.level == "fail")'
280
+ ```
281
+
282
+ ## Tests
283
+
284
+ ```bash
285
+ pip install -e ".[dev]"
286
+ pytest
287
+ ```
288
+
289
+ Nothing touches the network. The TLS and redirect tests spin up local servers on
290
+ ephemeral ports. The certificate in `tests/fixtures/` is self signed and exists
291
+ only for that, and its private key protects nothing.
292
+
293
+ The suite covers domain normalization, wildcard SAN matching, expiry threshold
294
+ scaling, record sorting, mute semantics, diff behavior, and three integration
295
+ paths. Reading a certificate OpenSSL rejected, reporting a redirect without
296
+ following it, and checking that both certificate decoders agree so results do not
297
+ depend on which extras are installed.
298
+
299
+ ## Implementation notes
300
+
301
+ - Certificates are decoded with `cryptography` when available. The fallback is
302
+ `ssl._ssl._test_decode_cert`, a private CPython API that works on 3.11 to 3.13
303
+ with no promises beyond that. A test asserts both paths return identical fields.
304
+ - Certificate dates use `ssl.cert_time_to_seconds`, which hardcodes month names
305
+ and ignores the locale. Parsing with `strptime` and `%b` raises `ValueError`
306
+ under a non English `LC_TIME`, which is a fun one to debug in production.
307
+ - DNS queries and the two network phases run in parallel. Finding order is
308
+ computed at print time, so it never depends on which check finishes first.
309
+ - Hostnames are lowercased for `MX`, `NS`, `CNAME`, `DS` and `PTR`. `CAA` values
310
+ are left alone since their parameters can be case sensitive.
311
+
312
+ ## Contributing
313
+
314
+ Issues and pull requests are welcome. New checks should follow the existing
315
+ shape, a stable id, a severity that reflects real impact, and a `fix` that tells
316
+ the user exactly what to publish. If a finding cannot be acted on, it is probably
317
+ `info`.
318
+
319
+ ## License
320
+
321
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"