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.
- domainwalk-0.3.0/LICENSE +21 -0
- domainwalk-0.3.0/PKG-INFO +350 -0
- domainwalk-0.3.0/README.md +321 -0
- domainwalk-0.3.0/domainwalk/__init__.py +1 -0
- domainwalk-0.3.0/domainwalk/__main__.py +4 -0
- domainwalk-0.3.0/domainwalk/cli.py +161 -0
- domainwalk-0.3.0/domainwalk/config.py +79 -0
- domainwalk-0.3.0/domainwalk/diff.py +60 -0
- domainwalk-0.3.0/domainwalk/dns_checks.py +267 -0
- domainwalk-0.3.0/domainwalk/findings.py +49 -0
- domainwalk-0.3.0/domainwalk/http_checks.py +423 -0
- domainwalk-0.3.0/domainwalk/report.py +116 -0
- domainwalk-0.3.0/domainwalk.egg-info/PKG-INFO +350 -0
- domainwalk-0.3.0/domainwalk.egg-info/SOURCES.txt +20 -0
- domainwalk-0.3.0/domainwalk.egg-info/dependency_links.txt +1 -0
- domainwalk-0.3.0/domainwalk.egg-info/entry_points.txt +2 -0
- domainwalk-0.3.0/domainwalk.egg-info/requires.txt +9 -0
- domainwalk-0.3.0/domainwalk.egg-info/top_level.txt +1 -0
- domainwalk-0.3.0/pyproject.toml +49 -0
- domainwalk-0.3.0/setup.cfg +4 -0
- domainwalk-0.3.0/tests/test_local_servers.py +187 -0
- domainwalk-0.3.0/tests/test_unit.py +199 -0
domainwalk-0.3.0/LICENSE
ADDED
|
@@ -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
|
+

|
|
33
|
+

|
|
34
|
+

|
|
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
|
+

|
|
4
|
+

|
|
5
|
+

|
|
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"
|