namespy 0.1.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.
- namespy-0.1.0/.gitattributes +27 -0
- namespy-0.1.0/.github/ISSUE_TEMPLATE/bug-report.yml +73 -0
- namespy-0.1.0/.github/ISSUE_TEMPLATE/config.yml +14 -0
- namespy-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +48 -0
- namespy-0.1.0/.github/dependabot.yml +34 -0
- namespy-0.1.0/.github/workflows/ci.yml +155 -0
- namespy-0.1.0/.github/workflows/release.yml +152 -0
- namespy-0.1.0/.github/workflows/security.yml +78 -0
- namespy-0.1.0/.github/workflows/socket.yml +37 -0
- namespy-0.1.0/.gitignore +72 -0
- namespy-0.1.0/AGENTS.md +52 -0
- namespy-0.1.0/CHANGELOG.md +1920 -0
- namespy-0.1.0/CONTRIBUTING.md +114 -0
- namespy-0.1.0/LICENSE +202 -0
- namespy-0.1.0/PKG-INFO +491 -0
- namespy-0.1.0/README.md +455 -0
- namespy-0.1.0/SECURITY.md +144 -0
- namespy-0.1.0/docs/HANDOFF.md +3969 -0
- namespy-0.1.0/docs/legacy/README.md +32 -0
- namespy-0.1.0/docs/legacy/database-schema.png +0 -0
- namespy-0.1.0/docs/legacy/schema.sql +162 -0
- namespy-0.1.0/docs/rfc-map.md +296 -0
- namespy-0.1.0/docs/roadmap.md +352 -0
- namespy-0.1.0/docs/strategy.md +120 -0
- namespy-0.1.0/examples/blocklist.py +160 -0
- namespy-0.1.0/examples/dynamic_responders.py +122 -0
- namespy-0.1.0/namespy/__init__.py +176 -0
- namespy-0.1.0/namespy/__main__.py +1727 -0
- namespy-0.1.0/namespy/aggressive.py +488 -0
- namespy-0.1.0/namespy/anchors.py +524 -0
- namespy-0.1.0/namespy/apps/__init__.py +60 -0
- namespy-0.1.0/namespy/apps/forwarder.py +275 -0
- namespy-0.1.0/namespy/apps/hosts.py +226 -0
- namespy-0.1.0/namespy/apps/lookup.py +319 -0
- namespy-0.1.0/namespy/apps/routing.py +203 -0
- namespy-0.1.0/namespy/apps/stub.py +257 -0
- namespy-0.1.0/namespy/bundle.py +352 -0
- namespy-0.1.0/namespy/cache/__init__.py +863 -0
- namespy-0.1.0/namespy/cache/sqlite.py +436 -0
- namespy-0.1.0/namespy/crypto.py +555 -0
- namespy-0.1.0/namespy/dnssec.py +1320 -0
- namespy-0.1.0/namespy/doh.py +431 -0
- namespy-0.1.0/namespy/edns.py +850 -0
- namespy-0.1.0/namespy/engine.py +554 -0
- namespy-0.1.0/namespy/enums.py +492 -0
- namespy-0.1.0/namespy/filtering.py +179 -0
- namespy-0.1.0/namespy/idna.py +361 -0
- namespy-0.1.0/namespy/maintenance.py +434 -0
- namespy-0.1.0/namespy/minimis.py +148 -0
- namespy-0.1.0/namespy/mqtype.py +320 -0
- namespy-0.1.0/namespy/named.root +92 -0
- namespy-0.1.0/namespy/names.py +587 -0
- namespy-0.1.0/namespy/nsec.py +1237 -0
- namespy-0.1.0/namespy/opportunistic.py +594 -0
- namespy-0.1.0/namespy/packet.py +934 -0
- namespy-0.1.0/namespy/py.typed +3 -0
- namespy-0.1.0/namespy/querylog.py +89 -0
- namespy-0.1.0/namespy/rbl.py +216 -0
- namespy-0.1.0/namespy/rdata.py +1919 -0
- namespy-0.1.0/namespy/resolver.py +5588 -0
- namespy-0.1.0/namespy/server.py +2377 -0
- namespy-0.1.0/namespy/special.py +149 -0
- namespy-0.1.0/namespy/stub.py +622 -0
- namespy-0.1.0/namespy/svcb.py +1891 -0
- namespy-0.1.0/namespy/trace.py +279 -0
- namespy-0.1.0/namespy/validate.py +1110 -0
- namespy-0.1.0/namespy/verify.py +519 -0
- namespy-0.1.0/namespy/wire.py +138 -0
- namespy-0.1.0/namespy/zone.py +780 -0
- namespy-0.1.0/pyproject.toml +192 -0
- namespy-0.1.0/tests/certs/dot-test-key.pem +5 -0
- namespy-0.1.0/tests/certs/dot-test.pem +9 -0
- namespy-0.1.0/tests/test_aggressive_nsec.py +433 -0
- namespy-0.1.0/tests/test_anchor_sentinel.py +289 -0
- namespy-0.1.0/tests/test_apps_forwarder.py +273 -0
- namespy-0.1.0/tests/test_apps_hosts.py +257 -0
- namespy-0.1.0/tests/test_apps_lookup.py +229 -0
- namespy-0.1.0/tests/test_apps_routing.py +148 -0
- namespy-0.1.0/tests/test_apps_stub.py +208 -0
- namespy-0.1.0/tests/test_bundle.py +425 -0
- namespy-0.1.0/tests/test_cache.py +403 -0
- namespy-0.1.0/tests/test_chain_cache.py +265 -0
- namespy-0.1.0/tests/test_cli.py +1337 -0
- namespy-0.1.0/tests/test_compact_denial.py +554 -0
- namespy-0.1.0/tests/test_crypto_backend.py +326 -0
- namespy-0.1.0/tests/test_dnssec_anchors.py +527 -0
- namespy-0.1.0/tests/test_dnssec_canonical.py +346 -0
- namespy-0.1.0/tests/test_dnssec_codecs.py +489 -0
- namespy-0.1.0/tests/test_dnssec_deprecation.py +233 -0
- namespy-0.1.0/tests/test_dnssec_ecc.py +592 -0
- namespy-0.1.0/tests/test_dnssec_proofs.py +510 -0
- namespy-0.1.0/tests/test_dnssec_rsa.py +173 -0
- namespy-0.1.0/tests/test_dnssec_serving.py +477 -0
- namespy-0.1.0/tests/test_dnssec_validate.py +1147 -0
- namespy-0.1.0/tests/test_dnssec_verify.py +535 -0
- namespy-0.1.0/tests/test_dnssec_wiring.py +1197 -0
- namespy-0.1.0/tests/test_dot_server.py +348 -0
- namespy-0.1.0/tests/test_dual_stack_policy.py +298 -0
- namespy-0.1.0/tests/test_dynamic_responders.py +298 -0
- namespy-0.1.0/tests/test_edns.py +652 -0
- namespy-0.1.0/tests/test_edns_error.py +299 -0
- namespy-0.1.0/tests/test_edns_keytag.py +362 -0
- namespy-0.1.0/tests/test_engine.py +537 -0
- namespy-0.1.0/tests/test_enums.py +266 -0
- namespy-0.1.0/tests/test_failure_cache.py +290 -0
- namespy-0.1.0/tests/test_failure_cache_resolver.py +690 -0
- namespy-0.1.0/tests/test_filtering.py +309 -0
- namespy-0.1.0/tests/test_fuzz_wire.py +455 -0
- namespy-0.1.0/tests/test_glue_policy.py +299 -0
- namespy-0.1.0/tests/test_idna.py +669 -0
- namespy-0.1.0/tests/test_key_tag_query.py +274 -0
- namespy-0.1.0/tests/test_minimis.py +93 -0
- namespy-0.1.0/tests/test_mqtype.py +367 -0
- namespy-0.1.0/tests/test_mqtype_interop.py +292 -0
- namespy-0.1.0/tests/test_names.py +83 -0
- namespy-0.1.0/tests/test_names_wire.py +241 -0
- namespy-0.1.0/tests/test_nsec_cache.py +467 -0
- namespy-0.1.0/tests/test_nsec_proofs.py +381 -0
- namespy-0.1.0/tests/test_nsec_responses.py +614 -0
- namespy-0.1.0/tests/test_nxdomain_cut.py +555 -0
- namespy-0.1.0/tests/test_opportunistic.py +1032 -0
- namespy-0.1.0/tests/test_packet.py +86 -0
- namespy-0.1.0/tests/test_packet_wire.py +613 -0
- namespy-0.1.0/tests/test_padding_policy.py +275 -0
- namespy-0.1.0/tests/test_public_api.py +85 -0
- namespy-0.1.0/tests/test_qname_min.py +225 -0
- namespy-0.1.0/tests/test_querylog.py +267 -0
- namespy-0.1.0/tests/test_rbl.py +295 -0
- namespy-0.1.0/tests/test_rdata_wrapping.py +154 -0
- namespy-0.1.0/tests/test_referral_glue.py +164 -0
- namespy-0.1.0/tests/test_resolve_steps.py +442 -0
- namespy-0.1.0/tests/test_resolver_alias.py +541 -0
- namespy-0.1.0/tests/test_resolver_classify.py +482 -0
- namespy-0.1.0/tests/test_resolver_e2e.py +387 -0
- namespy-0.1.0/tests/test_resolver_lifecycle.py +125 -0
- namespy-0.1.0/tests/test_resolver_matching.py +224 -0
- namespy-0.1.0/tests/test_resolver_mqtype.py +921 -0
- namespy-0.1.0/tests/test_resolver_truncation.py +340 -0
- namespy-0.1.0/tests/test_review_fixes.py +168 -0
- namespy-0.1.0/tests/test_serve_stale.py +319 -0
- namespy-0.1.0/tests/test_server.py +1562 -0
- namespy-0.1.0/tests/test_server_stream.py +356 -0
- namespy-0.1.0/tests/test_special_names.py +183 -0
- namespy-0.1.0/tests/test_stream_reuse.py +258 -0
- namespy-0.1.0/tests/test_stub_doh.py +457 -0
- namespy-0.1.0/tests/test_stub_dot.py +329 -0
- namespy-0.1.0/tests/test_stub_resolver.py +229 -0
- namespy-0.1.0/tests/test_svcb_presentation.py +474 -0
- namespy-0.1.0/tests/test_svcb_rdata.py +257 -0
- namespy-0.1.0/tests/test_svcb_wire.py +417 -0
- namespy-0.1.0/tests/test_trace_mermaid.py +213 -0
- namespy-0.1.0/tests/test_trace_text.py +130 -0
- namespy-0.1.0/tests/test_trust_maintenance.py +264 -0
- namespy-0.1.0/tests/test_wildcard_synthesis.py +394 -0
- namespy-0.1.0/tests/test_zone.py +462 -0
- namespy-0.1.0/tools/check_dist.py +133 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Line endings are a correctness issue here, not a style one: CI runs the same
|
|
2
|
+
# suite on Ubuntu and Windows, and a checkout that rewrites bytes per platform
|
|
3
|
+
# means the formatter, the tests and the reviewer are looking at different
|
|
4
|
+
# files. The index stores LF; core.autocrlf is a per-machine setting that cannot
|
|
5
|
+
# be relied on to agree, so the policy is declared in the repository instead.
|
|
6
|
+
* text=auto eol=lf
|
|
7
|
+
|
|
8
|
+
# Shell must stay LF or it will not execute when checked out on Windows and
|
|
9
|
+
# copied into a Linux container.
|
|
10
|
+
*.sh text eol=lf
|
|
11
|
+
|
|
12
|
+
# Windows files that must keep CRLF to be useful as-is.
|
|
13
|
+
*.bat text eol=crlf
|
|
14
|
+
*.cmd text eol=crlf
|
|
15
|
+
|
|
16
|
+
# Never run end-of-line conversion or diff over these.
|
|
17
|
+
*.png binary
|
|
18
|
+
*.jpg binary
|
|
19
|
+
*.gif binary
|
|
20
|
+
*.ico binary
|
|
21
|
+
*.sqlite binary
|
|
22
|
+
*.db binary
|
|
23
|
+
*.pdf binary
|
|
24
|
+
|
|
25
|
+
# The root hints file is data with an exact on-the-wire meaning per RFC 1035
|
|
26
|
+
# section 5; keep its LF and treat edits to it as substantive.
|
|
27
|
+
namespy/named.root text eol=lf
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: A crash, a wrong answer, or a divergence from a DNS specification
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
DNS bugs are usually invisible without the bytes. If something parsed
|
|
9
|
+
wrong, crashed, or produced an unexpected answer, **include the packet**
|
|
10
|
+
as a hex string — that single thing decides whether the report is
|
|
11
|
+
actionable.
|
|
12
|
+
- type: dropdown
|
|
13
|
+
id: area
|
|
14
|
+
attributes:
|
|
15
|
+
label: Which part?
|
|
16
|
+
options:
|
|
17
|
+
- Wire parsing (a crash or a wrong decode)
|
|
18
|
+
- Resolver behaviour (referrals, caching, aliases, glue)
|
|
19
|
+
- Server (`--serve`)
|
|
20
|
+
- CLI output
|
|
21
|
+
- Packaging / install
|
|
22
|
+
- Something else
|
|
23
|
+
validations:
|
|
24
|
+
required: true
|
|
25
|
+
- type: textarea
|
|
26
|
+
id: what-happened
|
|
27
|
+
attributes:
|
|
28
|
+
label: What happened, and what did you expect?
|
|
29
|
+
description: >
|
|
30
|
+
For a parsing bug, paste the datagram as hex (`data.hex()`), plus the
|
|
31
|
+
offset in the error if one was raised. For a resolver bug, `namespy
|
|
32
|
+
--trace NAME` and the resulting `last_resolution.md` are usually enough
|
|
33
|
+
on their own.
|
|
34
|
+
render: shell
|
|
35
|
+
validations:
|
|
36
|
+
required: true
|
|
37
|
+
- type: textarea
|
|
38
|
+
id: repro
|
|
39
|
+
attributes:
|
|
40
|
+
label: Shortest reproduction
|
|
41
|
+
description: >
|
|
42
|
+
A few lines that show it, run against a clean checkout. Please prefer
|
|
43
|
+
`pip install -e ".[dev]"` at the commit you tested over a released
|
|
44
|
+
version, since this repository is not on PyPI.
|
|
45
|
+
render: python
|
|
46
|
+
validations:
|
|
47
|
+
required: true
|
|
48
|
+
- type: textarea
|
|
49
|
+
id: spec
|
|
50
|
+
attributes:
|
|
51
|
+
label: Specification
|
|
52
|
+
description: >
|
|
53
|
+
If you know which section this violates, cite it — e.g. "RFC 9267
|
|
54
|
+
section 5: RDLENGTH must be checked before the RDATA is read". If you do
|
|
55
|
+
not know, leave this empty and say so; guessing is still useful.
|
|
56
|
+
placeholder: RFC nnnn section n.n
|
|
57
|
+
- type: input
|
|
58
|
+
id: environment
|
|
59
|
+
attributes:
|
|
60
|
+
label: Environment
|
|
61
|
+
description: OS, Python version, and whether you ran with `-O` / `PYTHONOPTIMIZE`
|
|
62
|
+
placeholder: Windows 11, Python 3.12.7, no -O
|
|
63
|
+
validations:
|
|
64
|
+
required: true
|
|
65
|
+
- type: checkboxes
|
|
66
|
+
id: gates
|
|
67
|
+
attributes:
|
|
68
|
+
label: Checked the local gates?
|
|
69
|
+
description: These are the exact commands CI runs, so failing them here saves a round trip.
|
|
70
|
+
options:
|
|
71
|
+
- label: "`pytest -q` passes on a clean checkout"
|
|
72
|
+
- label: "`python -O -m pytest -q` also passes (assertions stripped)"
|
|
73
|
+
- label: "`ruff check .` and `mypy namespy` are clean"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
blank_issues_enabled: true
|
|
2
|
+
contact_links:
|
|
3
|
+
- name: Report a security issue
|
|
4
|
+
url: https://github.com/b1tninja/namespy/security/advisories/new
|
|
5
|
+
about: >
|
|
6
|
+
Parser and resolver bugs are often remotely reachable from one malformed
|
|
7
|
+
datagram. Please use a private advisory, not a public issue — see
|
|
8
|
+
SECURITY.md.
|
|
9
|
+
- name: Spec and design questions
|
|
10
|
+
url: https://github.com/b1tninja/namespy/discussions
|
|
11
|
+
about: >
|
|
12
|
+
"Should this reject or tolerate that label?" and similar questions are
|
|
13
|
+
about which RFC reading we adopt. docs/rfc-map.md records the current
|
|
14
|
+
reading; a discussion is the right place to argue with it.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Delete any section that genuinely does not apply, but keep the gate list.
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
## What problem does this solve?
|
|
6
|
+
|
|
7
|
+
One or two sentences. If it fixes an issue, link it.
|
|
8
|
+
|
|
9
|
+
## Behaviour change?
|
|
10
|
+
|
|
11
|
+
Say so explicitly if any of these moved, and update `CHANGELOG.md` under
|
|
12
|
+
Unreleased:
|
|
13
|
+
|
|
14
|
+
- [ ] A packet that used to parse now raises (or the reverse)
|
|
15
|
+
- [ ] A TTL, cache, or negative-answer decision changed
|
|
16
|
+
- [ ] A public name changed (module, class, enum member, CLI flag)
|
|
17
|
+
- [ ] Nothing observable changed
|
|
18
|
+
|
|
19
|
+
A parser that starts rejecting input is a *feature* here if the input was
|
|
20
|
+
malformed — RFC 5625 section 6.3 permits dropping a broken message and does not
|
|
21
|
+
require tolerating it. But it must be a decision, not a surprise.
|
|
22
|
+
|
|
23
|
+
## Gates
|
|
24
|
+
|
|
25
|
+
These are the commands CI runs. Run them locally before pushing; the `-O` run is
|
|
26
|
+
the one that catches assertions carrying real validation, which is this codebase's
|
|
27
|
+
main historical failure mode.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pytest -q # coverage floor enforced by pyproject
|
|
31
|
+
python -O -m pytest -q
|
|
32
|
+
ruff check . && ruff format --check .
|
|
33
|
+
mypy namespy
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- [ ] All four pass locally
|
|
37
|
+
|
|
38
|
+
## Spec
|
|
39
|
+
|
|
40
|
+
Which document and section required this? If the change *is* a spec question, the
|
|
41
|
+
reading you chose belongs in `docs/rfc-map.md` — that file exists so the next
|
|
42
|
+
person does not have to re-litigate it.
|
|
43
|
+
|
|
44
|
+
## Tests
|
|
45
|
+
|
|
46
|
+
Every parser change needs a byte-level test that pins the exact failure, not just
|
|
47
|
+
a round-trip. For a new rejection, name the condition in the test. For a new
|
|
48
|
+
acceptance, assert the decoded value, not merely that nothing raised.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# Only the dev extra resolves here -- the library itself declares no runtime
|
|
4
|
+
# dependencies, which is a deliberate property of this codebase rather than an
|
|
5
|
+
# oversight. Keeping the toolchain fresh is still worth a pull request, because
|
|
6
|
+
# a stale ruff or pytest is what makes CI disagree with a local run.
|
|
7
|
+
- package-ecosystem: pip
|
|
8
|
+
directory: "/"
|
|
9
|
+
schedule:
|
|
10
|
+
interval: weekly
|
|
11
|
+
day: monday
|
|
12
|
+
open-pull-requests-limit: 5
|
|
13
|
+
commit-message:
|
|
14
|
+
prefix: "deps"
|
|
15
|
+
groups:
|
|
16
|
+
# One PR for the test/lint toolchain instead of five, since they are
|
|
17
|
+
# upgraded together or not at all.
|
|
18
|
+
dev-toolchain:
|
|
19
|
+
patterns:
|
|
20
|
+
- "pytest*"
|
|
21
|
+
- "ruff"
|
|
22
|
+
- "mypy"
|
|
23
|
+
- "hypothesis"
|
|
24
|
+
- "build"
|
|
25
|
+
|
|
26
|
+
# The actions themselves are third-party code executing with a repository
|
|
27
|
+
# token, so their pins get the same review as dependencies.
|
|
28
|
+
- package-ecosystem: github-actions
|
|
29
|
+
directory: "/"
|
|
30
|
+
schedule:
|
|
31
|
+
interval: weekly
|
|
32
|
+
day: monday
|
|
33
|
+
commit-message:
|
|
34
|
+
prefix: "ci"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
env:
|
|
17
|
+
# Hypothesis writes its counterexample database into the repo, which on a
|
|
18
|
+
# shared runner means a failure reproduced locally never reaches CI. Cached
|
|
19
|
+
# explicitly below instead of being thrown away with the rest of the checkout.
|
|
20
|
+
HYPOTHESIS_STORAGE_DIRECTORY: .hypothesis
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
lint:
|
|
24
|
+
name: Lint
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v7
|
|
28
|
+
- uses: actions/setup-python@v7
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.13"
|
|
31
|
+
cache: pip
|
|
32
|
+
- run: python -m pip install --upgrade pip && pip install -e ".[dev]"
|
|
33
|
+
|
|
34
|
+
# `check` reports, `format --check` is the one that notices drift: a
|
|
35
|
+
# formatter that only runs on a developer's machine diverges silently and
|
|
36
|
+
# then produces whole-file diffs in the next unrelated commit.
|
|
37
|
+
- run: ruff check . --output-format=github
|
|
38
|
+
- run: ruff format --check .
|
|
39
|
+
|
|
40
|
+
# Blocking at the level the code is actually at: mypy namespy exits 0 today,
|
|
41
|
+
# so a check that merely printed the count would let regressions through
|
|
42
|
+
# while looking like a gate. --strict measures 274 errors, and 245 of them
|
|
43
|
+
# are no-untyped-def plus no-untyped-call -- one root cause, functions with
|
|
44
|
+
# no annotations, concentrated in resolver.py (132) rather than spread
|
|
45
|
+
# evenly. That is a per-module campaign, not a flag to flip: enabling
|
|
46
|
+
# strict globally would mean silencing the result, which is how a type
|
|
47
|
+
# checker stops being read.
|
|
48
|
+
- run: mypy namespy
|
|
49
|
+
|
|
50
|
+
# The optional backends, exercised where they exist. The Test matrix runs
|
|
51
|
+
# without a crypto backend -- that is the configuration most people install, and the
|
|
52
|
+
# no-backend path is what must stay clean -- so without this job the [crypto] extra
|
|
53
|
+
# would ship untested and the integration points its transports rest on would be
|
|
54
|
+
# assumptions. The [doh] extra (pure Python, no wheels to advise against) does ride
|
|
55
|
+
# the matrix, because an optional transport the matrix cannot run is a transport the
|
|
56
|
+
# matrix cannot cover either: the TLS fixtures are static stdlib material, so with
|
|
57
|
+
# h2 present every DoT and DoH line is exercised in all four matrix jobs -- and the
|
|
58
|
+
# coverage floor is the proof: when the dot/doh tests skipped for lack of a
|
|
59
|
+
# certificate generator, doh.py measured 15% and the floor broke the commit.
|
|
60
|
+
crypto_extra:
|
|
61
|
+
name: Crypto extra
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v7
|
|
65
|
+
- uses: actions/setup-python@v7
|
|
66
|
+
with:
|
|
67
|
+
python-version: "3.13"
|
|
68
|
+
cache: pip
|
|
69
|
+
# Resolving the extras is itself the check: it fails if a floor cannot be satisfied
|
|
70
|
+
# by a wheel on a supported platform, which is the failure mode an optional
|
|
71
|
+
# dependency has that no unit test can see.
|
|
72
|
+
- run: python -m pip install --upgrade pip && pip install -e ".[dev,crypto,doh]"
|
|
73
|
+
- run: python -c "import cryptography, namespy.crypto as c; b=c.require_backend('probe'); print(b.name, b.version)"
|
|
74
|
+
- run: python -m pytest -q tests/test_crypto_backend.py
|
|
75
|
+
- run: python -c "import h2, namespy.doh as d; assert d.H2_AVAILABLE; print('h2', h2.__version__)"
|
|
76
|
+
- run: python -m pytest -q tests/test_stub_doh.py
|
|
77
|
+
|
|
78
|
+
test:
|
|
79
|
+
name: Test py${{ matrix.python-version }} on ${{ matrix.os }}
|
|
80
|
+
runs-on: ${{ matrix.os }}
|
|
81
|
+
strategy:
|
|
82
|
+
fail-fast: false
|
|
83
|
+
matrix:
|
|
84
|
+
# Windows is not decoration. The bundled server used to call
|
|
85
|
+
# setsockopt(IPPROTO_IP, IP_MTU_DISCOVER, ...) unconditionally with
|
|
86
|
+
# hardcoded Linux constants, which raised on Windows in connection_made
|
|
87
|
+
# -- a bug that no Linux-only CI could ever see. asyncio also swaps to
|
|
88
|
+
# ProactorEventLoop here, so socket lifetime differs by platform.
|
|
89
|
+
os: [ubuntu-latest, windows-latest]
|
|
90
|
+
python-version: ["3.12", "3.13"]
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/checkout@v7
|
|
93
|
+
- uses: actions/setup-python@v7
|
|
94
|
+
with:
|
|
95
|
+
python-version: ${{ matrix.python-version }}
|
|
96
|
+
cache: pip
|
|
97
|
+
- run: python -m pip install --upgrade pip && pip install -e ".[dev,doh]"
|
|
98
|
+
|
|
99
|
+
# The coverage floor is fail_under in pyproject.toml, not duplicated here,
|
|
100
|
+
# so the threshold and the explanation for it stay in one file. The --cov
|
|
101
|
+
# flag is what makes pytest read that file: pytest-cov measures only when
|
|
102
|
+
# asked, so this step exited 0 whatever the tests covered and the floor was
|
|
103
|
+
# decoration: the floor (50012b4) and the CI that never asked for a
|
|
104
|
+
# measurement (ecedb5c) both landed 2026-10-27. Checked both ways: one test
|
|
105
|
+
# file now exits 1 with "Required test coverage of 80.0% not reached", the
|
|
106
|
+
# whole suite exits 0 at 87%.
|
|
107
|
+
- name: pytest
|
|
108
|
+
run: python -m pytest -q --cov
|
|
109
|
+
|
|
110
|
+
# This step is the reason the suite is worth running at all here. Every
|
|
111
|
+
# bounds check on a wire path was once an `assert`, which python -O strips
|
|
112
|
+
# out of the bytecode; the parser's hardening is therefore only real if the
|
|
113
|
+
# hostile-input tests still pass with assertions compiled away. Without
|
|
114
|
+
# this leg, "no assert on network data" is a comment rather than a policy.
|
|
115
|
+
- name: pytest under -O (assertions stripped)
|
|
116
|
+
run: python -O -m pytest -q -p no:cacheprovider
|
|
117
|
+
|
|
118
|
+
- uses: actions/upload-artifact@v7
|
|
119
|
+
if: failure()
|
|
120
|
+
with:
|
|
121
|
+
name: pytest-${{ matrix.os }}-py${{ matrix.python-version }}
|
|
122
|
+
path: |
|
|
123
|
+
.hypothesis/
|
|
124
|
+
last_resolution.md
|
|
125
|
+
if-no-files-found: ignore
|
|
126
|
+
|
|
127
|
+
dist:
|
|
128
|
+
name: Build and audit distributions
|
|
129
|
+
runs-on: ubuntu-latest
|
|
130
|
+
steps:
|
|
131
|
+
- uses: actions/checkout@v7
|
|
132
|
+
- uses: actions/setup-python@v7
|
|
133
|
+
with:
|
|
134
|
+
python-version: "3.13"
|
|
135
|
+
cache: pip
|
|
136
|
+
- run: python -m pip install --upgrade pip && pip install -e ".[dev]"
|
|
137
|
+
- run: python -m build
|
|
138
|
+
- run: python tools/check_dist.py dist/
|
|
139
|
+
# Installs the wheel in a clean venv and asserts the root hints arrive
|
|
140
|
+
# through importlib.resources -- the one failure mode unit tests cannot
|
|
141
|
+
# see, because they always run against the source tree.
|
|
142
|
+
#
|
|
143
|
+
# The `cd /` is load-bearing: the checkout contains an namespy/ directory,
|
|
144
|
+
# so importing from the workspace would silently import the source tree and
|
|
145
|
+
# "pass" no matter what the wheel actually contains.
|
|
146
|
+
- name: Install the wheel and load hints from it
|
|
147
|
+
run: |
|
|
148
|
+
python -m venv /tmp/verify
|
|
149
|
+
/tmp/verify/bin/python -m pip install --no-deps dist/*.whl
|
|
150
|
+
cd /
|
|
151
|
+
/tmp/verify/bin/python -c "import namespy, importlib.resources as r; print(len(r.files('namespy').joinpath('named.root').read_text().splitlines()), 'root hint lines')"
|
|
152
|
+
- uses: actions/upload-artifact@v7
|
|
153
|
+
with:
|
|
154
|
+
name: dist
|
|
155
|
+
path: dist/
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishing is deliberately tag-gated rather than push-to-master: this project
|
|
4
|
+
# has never been on PyPI, and the first upload should be an explicit act with a
|
|
5
|
+
# version that exists in the tree. Cut a release with:
|
|
6
|
+
#
|
|
7
|
+
# git tag v0.1.0 && git push origin v0.1.0
|
|
8
|
+
#
|
|
9
|
+
# The tag must agree with namespy.__init__.__version__, which is what hatchling
|
|
10
|
+
# reads for the wheel's own version -- an unforced mismatch would put a name on
|
|
11
|
+
# PyPI that no source tree anywhere claims to be.
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
tags: ["v*"]
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
inputs:
|
|
18
|
+
version:
|
|
19
|
+
description: "Release this version; must equal namespy.__version__ (e.g. 0.1.0)"
|
|
20
|
+
required: true
|
|
21
|
+
type: string
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
build:
|
|
28
|
+
name: Build release artifacts
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
# id-token + attestations are for the provenance step below; everything
|
|
31
|
+
# else in this job stays under contents: read.
|
|
32
|
+
permissions:
|
|
33
|
+
contents: read
|
|
34
|
+
id-token: write
|
|
35
|
+
attestations: write
|
|
36
|
+
artifact-metadata: write
|
|
37
|
+
outputs:
|
|
38
|
+
digest: ${{ steps.hash.outputs.digest }}
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v7
|
|
41
|
+
- uses: actions/setup-python@v7
|
|
42
|
+
with:
|
|
43
|
+
python-version: "3.13"
|
|
44
|
+
cache: pip
|
|
45
|
+
- run: python -m pip install --upgrade pip && pip install -e ".[dev]"
|
|
46
|
+
|
|
47
|
+
- name: Refuse a version that disagrees with the package
|
|
48
|
+
env:
|
|
49
|
+
# Passed through the environment rather than interpolated into the
|
|
50
|
+
# script: an input is data, not shell text, even a maintainer's.
|
|
51
|
+
INPUT_VERSION: ${{ inputs.version }}
|
|
52
|
+
run: |
|
|
53
|
+
# A dispatched run carries no tag, so GITHUB_REF_NAME would be a
|
|
54
|
+
# branch name and the comparison would always fail; the input stands
|
|
55
|
+
# in for the tag. The rule itself is the same either way: what lands
|
|
56
|
+
# on PyPI must be a version some source tree actually claims.
|
|
57
|
+
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
|
58
|
+
WANT="${INPUT_VERSION}"
|
|
59
|
+
else
|
|
60
|
+
WANT="${GITHUB_REF_NAME#v}"
|
|
61
|
+
fi
|
|
62
|
+
ACTUAL="$(python -c 'import namespy; print(namespy.__version__)')"
|
|
63
|
+
if [ "$WANT" != "$ACTUAL" ]; then
|
|
64
|
+
echo "release says $WANT but namespy.__version__ is $ACTUAL" >&2
|
|
65
|
+
exit 1
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
- run: python -m build
|
|
69
|
+
- run: python tools/check_dist.py dist/
|
|
70
|
+
|
|
71
|
+
# GitHub-side SLSA provenance, signed by Sigstore against this run's OIDC
|
|
72
|
+
# identity: `gh attestation verify` ties the shipped files back to this
|
|
73
|
+
# repo and commit. PyPI receives its own PEP 740 attestations from the
|
|
74
|
+
# publish action; two independent proofs of the same build.
|
|
75
|
+
- name: Attest build provenance
|
|
76
|
+
uses: actions/attest@v4
|
|
77
|
+
with:
|
|
78
|
+
subject-path: dist/*
|
|
79
|
+
|
|
80
|
+
# SLSA provenance attribute: the SHA256 of every artifact the publish job
|
|
81
|
+
# will upload. Recorded here so a later audit can tie a file on PyPI back
|
|
82
|
+
# to this exact run without trusting the PyPI entry.
|
|
83
|
+
- name: Collect artifact digests
|
|
84
|
+
id: hash
|
|
85
|
+
run: |
|
|
86
|
+
cd dist && LANG=C LC_ALL=C sha256sum * | sort | sed 's/ * */ /' \
|
|
87
|
+
| base64 -w0 > ../digest.b64
|
|
88
|
+
echo "digest=$(cat ../digest.b64)" >> "$GITHUB_OUTPUT"
|
|
89
|
+
|
|
90
|
+
- uses: actions/upload-artifact@v7
|
|
91
|
+
with:
|
|
92
|
+
name: release-dist
|
|
93
|
+
path: dist/
|
|
94
|
+
|
|
95
|
+
publish:
|
|
96
|
+
name: Publish to PyPI
|
|
97
|
+
needs: build
|
|
98
|
+
runs-on: ubuntu-latest
|
|
99
|
+
# Trusted publishing (OIDC) rather than a stored API token: no long-lived
|
|
100
|
+
# credential exists to leak, and the audience/environment gate means only
|
|
101
|
+
# this repository's tagged builds can mint an upload credential.
|
|
102
|
+
#
|
|
103
|
+
# The `pypi` environment exists in Settings -> Environments; the job
|
|
104
|
+
# names it, so only runs from this repo/workflow can mint its token.
|
|
105
|
+
# The remaining half is on PyPI: the project `namespy` must list this
|
|
106
|
+
# repo, this workflow file and this environment name as a trusted
|
|
107
|
+
# publisher before any upload will be accepted. Until then a misfired
|
|
108
|
+
# tag fails at `twine` with a legible OIDC error rather than publishing.
|
|
109
|
+
environment:
|
|
110
|
+
name: pypi
|
|
111
|
+
url: https://pypi.org/p/namespy
|
|
112
|
+
permissions:
|
|
113
|
+
id-token: write
|
|
114
|
+
steps:
|
|
115
|
+
- uses: actions/download-artifact@v8
|
|
116
|
+
with:
|
|
117
|
+
name: release-dist
|
|
118
|
+
path: dist/
|
|
119
|
+
- uses: actions/setup-python@v7
|
|
120
|
+
with:
|
|
121
|
+
python-version: "3.13"
|
|
122
|
+
- run: python -m pip install --upgrade twine
|
|
123
|
+
- run: python -m twine check dist/*
|
|
124
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
125
|
+
with:
|
|
126
|
+
print-hash: true
|
|
127
|
+
# PEP 740 digital attestations are generated and uploaded by the
|
|
128
|
+
# action automatically for trusted publishing; there is nothing to
|
|
129
|
+
# enable here.
|
|
130
|
+
|
|
131
|
+
release:
|
|
132
|
+
name: Create the GitHub Release
|
|
133
|
+
needs: [build, publish]
|
|
134
|
+
# A dispatched run has no tag to cut a release from: it is a rehearsal of
|
|
135
|
+
# the build/publish path and stays out of the releases list.
|
|
136
|
+
if: github.event_name == 'push'
|
|
137
|
+
runs-on: ubuntu-latest
|
|
138
|
+
permissions:
|
|
139
|
+
contents: write
|
|
140
|
+
steps:
|
|
141
|
+
- uses: actions/download-artifact@v8
|
|
142
|
+
with:
|
|
143
|
+
name: release-dist
|
|
144
|
+
path: dist/
|
|
145
|
+
# The GitHub Release is the human-facing artifact page, and it only
|
|
146
|
+
# appears after PyPI accepted the files: a failed publish leaves no
|
|
147
|
+
# half-release pointing at binaries that are not on PyPI.
|
|
148
|
+
- name: Publish the release
|
|
149
|
+
run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes
|
|
150
|
+
env:
|
|
151
|
+
GH_TOKEN: ${{ github.token }}
|
|
152
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
name: Security
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
# Weekly even without pushes, so a new CodeQL rule or a newly disclosed
|
|
9
|
+
# advisory surfaces against this code. Push-triggered analysis only ever
|
|
10
|
+
# asks about today's code with today's tools.
|
|
11
|
+
- cron: "17 3 * * 1"
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
codeql:
|
|
19
|
+
name: CodeQL (${{ matrix.language }})
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
timeout-minutes: 360
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
security-events: write
|
|
25
|
+
strategy:
|
|
26
|
+
fail-fast: false
|
|
27
|
+
matrix:
|
|
28
|
+
# Only Python source here ships logic; the workflow files themselves
|
|
29
|
+
# are covered by review + Dependabot, not by a query pack.
|
|
30
|
+
language: [python]
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v7
|
|
33
|
+
- uses: actions/setup-python@v7
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.13"
|
|
36
|
+
- uses: github/codeql-action/init@v4
|
|
37
|
+
with:
|
|
38
|
+
languages: ${{ matrix.language }}
|
|
39
|
+
# security-extended over security-and-quality: this is a parser
|
|
40
|
+
# library whose real risk is hostile input, and the extended pack is
|
|
41
|
+
# the one that queries for it. The quality queries are already
|
|
42
|
+
# ruff's job and are enforced in ci.yml.
|
|
43
|
+
queries: security-extended
|
|
44
|
+
- uses: github/codeql-action/autobuild@v4
|
|
45
|
+
- uses: github/codeql-action/analyze@v4
|
|
46
|
+
with:
|
|
47
|
+
category: "/language:${{ matrix.language }}"
|
|
48
|
+
|
|
49
|
+
dependency-review:
|
|
50
|
+
# Diff-aware: it flags a newly *introduced* dependency on the PR that
|
|
51
|
+
# adds it, which no post-merge scanner does at the moment it matters.
|
|
52
|
+
# Only meaningful on pull_request, where there is a base to diff against.
|
|
53
|
+
if: github.event_name == 'pull_request'
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
permissions:
|
|
56
|
+
contents: read
|
|
57
|
+
pull-requests: write
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v7
|
|
60
|
+
- uses: actions/dependency-review-action@v5
|
|
61
|
+
with:
|
|
62
|
+
fail-on-severity: high
|
|
63
|
+
comment-summary-in-pr: always
|
|
64
|
+
|
|
65
|
+
pip-audit:
|
|
66
|
+
# Audits the fully resolved dev-extra environment against the PyPA
|
|
67
|
+
# advisory DB. The library itself declares zero runtime dependencies, so
|
|
68
|
+
# what this can actually catch lives in the toolchain that builds, tests
|
|
69
|
+
# and publishes it -- which is where a Python supply-chain attack lands.
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v7
|
|
73
|
+
- uses: actions/setup-python@v7
|
|
74
|
+
with:
|
|
75
|
+
python-version: "3.13"
|
|
76
|
+
cache: pip
|
|
77
|
+
- run: python -m pip install --upgrade pip && pip install -e ".[dev]" pip-audit
|
|
78
|
+
- run: python -m pip_audit
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Socket
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: socket-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
firewall:
|
|
17
|
+
name: Screen dependency installs (Socket Firewall)
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v7
|
|
21
|
+
# setup-python deliberately comes *before* Socket (per Socket's own
|
|
22
|
+
# ordering guidance) and the pip step deliberately does not use
|
|
23
|
+
# actions/setup-python's cache: a cached wheel install never talks to
|
|
24
|
+
# the index, so nothing gets screened and the job would green-light a
|
|
25
|
+
# compromised release.
|
|
26
|
+
- uses: actions/setup-python@v7
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.13"
|
|
29
|
+
# Pinned to the immutable tag Socket's own docs recommend; Dependabot
|
|
30
|
+
# (github-actions ecosystem) keeps it current.
|
|
31
|
+
- uses: SocketDev/action@v1.3.2
|
|
32
|
+
with:
|
|
33
|
+
mode: firewall-free
|
|
34
|
+
# The point of the job: every dev dependency crosses the firewall on
|
|
35
|
+
# its way in. The library has no runtime dependencies to screen; the
|
|
36
|
+
# dev extra is the real attack surface.
|
|
37
|
+
- run: sfw pip install -e ".[dev]"
|