sendermap 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.
Files changed (131) hide show
  1. sendermap-0.1.0/.github/workflows/publish.yml +96 -0
  2. sendermap-0.1.0/.github/workflows/recheck-corpus.yml +49 -0
  3. sendermap-0.1.0/.github/workflows/test.yml +40 -0
  4. sendermap-0.1.0/.github/workflows/verify-proofs.yml +62 -0
  5. sendermap-0.1.0/.gitignore +5 -0
  6. sendermap-0.1.0/ARCHITECTURE.md +811 -0
  7. sendermap-0.1.0/CHANGELOG.md +36 -0
  8. sendermap-0.1.0/LICENSE +21 -0
  9. sendermap-0.1.0/PKG-INFO +215 -0
  10. sendermap-0.1.0/README.md +187 -0
  11. sendermap-0.1.0/data/hunt_domains.txt +2004 -0
  12. sendermap-0.1.0/pyproject.toml +75 -0
  13. sendermap-0.1.0/sendermap/__init__.py +23 -0
  14. sendermap-0.1.0/sendermap/catalog.py +510 -0
  15. sendermap-0.1.0/sendermap/cli.py +265 -0
  16. sendermap-0.1.0/sendermap/data/schema/sendermap-1.json +743 -0
  17. sendermap-0.1.0/sendermap/data/vendors.json +5553 -0
  18. sendermap-0.1.0/sendermap/data/vendors_pending.json +1405 -0
  19. sendermap-0.1.0/sendermap/dns.py +252 -0
  20. sendermap-0.1.0/sendermap/engine.py +365 -0
  21. sendermap-0.1.0/sendermap/infer.py +58 -0
  22. sendermap-0.1.0/sendermap/mcp_server.py +257 -0
  23. sendermap-0.1.0/sendermap/model.py +504 -0
  24. sendermap-0.1.0/sendermap/proofs.py +89 -0
  25. sendermap-0.1.0/sendermap/py.typed +0 -0
  26. sendermap-0.1.0/sendermap/reconcile.py +65 -0
  27. sendermap-0.1.0/sendermap/render/__init__.py +165 -0
  28. sendermap-0.1.0/sendermap/render/markdown.py +89 -0
  29. sendermap-0.1.0/sendermap/render/text.py +144 -0
  30. sendermap-0.1.0/sendermap/selectors.py +251 -0
  31. sendermap-0.1.0/sendermap/setup.py +403 -0
  32. sendermap-0.1.0/sendermap/sources/__init__.py +38 -0
  33. sendermap-0.1.0/sendermap/sources/bimi.py +16 -0
  34. sendermap-0.1.0/sendermap/sources/ct.py +366 -0
  35. sendermap-0.1.0/sendermap/sources/dkim.py +529 -0
  36. sendermap-0.1.0/sendermap/sources/labels.py +80 -0
  37. sendermap-0.1.0/sendermap/sources/mta_sts.py +43 -0
  38. sendermap-0.1.0/sendermap/sources/mx.py +72 -0
  39. sendermap-0.1.0/sendermap/sources/networks.py +267 -0
  40. sendermap-0.1.0/sendermap/sources/reports.py +52 -0
  41. sendermap-0.1.0/sendermap/sources/spf.py +393 -0
  42. sendermap-0.1.0/sendermap/sources/spf_checks.py +125 -0
  43. sendermap-0.1.0/sendermap/sources/spf_eval.py +565 -0
  44. sendermap-0.1.0/sendermap/sources/subdomains.py +104 -0
  45. sendermap-0.1.0/sendermap/sources/tokens.py +27 -0
  46. sendermap-0.1.0/sendermap/sources/web.py +184 -0
  47. sendermap-0.1.0/sendermap/story.py +448 -0
  48. sendermap-0.1.0/sendermap/testing.py +274 -0
  49. sendermap-0.1.0/sendermap/web/__init__.py +0 -0
  50. sendermap-0.1.0/sendermap/web/app.py +279 -0
  51. sendermap-0.1.0/sendermap/web/static/app.js +488 -0
  52. sendermap-0.1.0/sendermap/web/static/index.html +113 -0
  53. sendermap-0.1.0/sendermap/web/static/llms.txt +28 -0
  54. sendermap-0.1.0/sendermap/web/static/style.css +185 -0
  55. sendermap-0.1.0/server.json +38 -0
  56. sendermap-0.1.0/skills/sendermap/SKILL.md +230 -0
  57. sendermap-0.1.0/tests/conftest.py +116 -0
  58. sendermap-0.1.0/tests/corpus/README.md +24 -0
  59. sendermap-0.1.0/tests/corpus/allbirds.com/case.json +8 -0
  60. sendermap-0.1.0/tests/corpus/allbirds.com/dns.json +719 -0
  61. sendermap-0.1.0/tests/corpus/allbirds.com/expected.json +4271 -0
  62. sendermap-0.1.0/tests/corpus/allbirds.com/http.json +3 -0
  63. sendermap-0.1.0/tests/corpus/casper.com/case.json +8 -0
  64. sendermap-0.1.0/tests/corpus/casper.com/dns.json +739 -0
  65. sendermap-0.1.0/tests/corpus/casper.com/expected.json +3399 -0
  66. sendermap-0.1.0/tests/corpus/casper.com/http.json +3 -0
  67. sendermap-0.1.0/tests/corpus/example.com/case.json +8 -0
  68. sendermap-0.1.0/tests/corpus/example.com/dns.json +207 -0
  69. sendermap-0.1.0/tests/corpus/example.com/expected.json +106 -0
  70. sendermap-0.1.0/tests/corpus/example.com/http.json +4 -0
  71. sendermap-0.1.0/tests/corpus/glossier.com/case.json +8 -0
  72. sendermap-0.1.0/tests/corpus/glossier.com/dns.json +692 -0
  73. sendermap-0.1.0/tests/corpus/glossier.com/expected.json +2100 -0
  74. sendermap-0.1.0/tests/corpus/glossier.com/http.json +3 -0
  75. sendermap-0.1.0/tests/corpus/mills-reeve.com/case.json +8 -0
  76. sendermap-0.1.0/tests/corpus/mills-reeve.com/dns.json +583 -0
  77. sendermap-0.1.0/tests/corpus/mills-reeve.com/expected.json +1450 -0
  78. sendermap-0.1.0/tests/corpus/mills-reeve.com/http.json +3 -0
  79. sendermap-0.1.0/tests/fixtures/crtsh_allbirds.json +123 -0
  80. sendermap-0.1.0/tests/fixtures/mcp-server-2025-12-11.schema.json +574 -0
  81. sendermap-0.1.0/tests/test_backed_only.py +139 -0
  82. sendermap-0.1.0/tests/test_caps.py +133 -0
  83. sendermap-0.1.0/tests/test_catalog.py +74 -0
  84. sendermap-0.1.0/tests/test_checked.py +88 -0
  85. sendermap-0.1.0/tests/test_cli.py +112 -0
  86. sendermap-0.1.0/tests/test_corpus.py +30 -0
  87. sendermap-0.1.0/tests/test_ct.py +346 -0
  88. sendermap-0.1.0/tests/test_delegation.py +142 -0
  89. sendermap-0.1.0/tests/test_determinism.py +172 -0
  90. sendermap-0.1.0/tests/test_dkim.py +228 -0
  91. sendermap-0.1.0/tests/test_dkim_denial.py +156 -0
  92. sendermap-0.1.0/tests/test_engine.py +160 -0
  93. sendermap-0.1.0/tests/test_evidence.py +121 -0
  94. sendermap-0.1.0/tests/test_gap_kinds.py +143 -0
  95. sendermap-0.1.0/tests/test_gaps.py +178 -0
  96. sendermap-0.1.0/tests/test_hunt_proofs.py +278 -0
  97. sendermap-0.1.0/tests/test_labels_say_what_was_seen.py +236 -0
  98. sendermap-0.1.0/tests/test_listing.py +68 -0
  99. sendermap-0.1.0/tests/test_mcp.py +230 -0
  100. sendermap-0.1.0/tests/test_networks.py +209 -0
  101. sendermap-0.1.0/tests/test_one_selector_list.py +64 -0
  102. sendermap-0.1.0/tests/test_package.py +33 -0
  103. sendermap-0.1.0/tests/test_pending_vendors.py +137 -0
  104. sendermap-0.1.0/tests/test_publish.py +97 -0
  105. sendermap-0.1.0/tests/test_recheck_corpus.py +100 -0
  106. sendermap-0.1.0/tests/test_reconcile.py +380 -0
  107. sendermap-0.1.0/tests/test_record_replay.py +135 -0
  108. sendermap-0.1.0/tests/test_resolver.py +121 -0
  109. sendermap-0.1.0/tests/test_schema.py +97 -0
  110. sendermap-0.1.0/tests/test_selectors.py +249 -0
  111. sendermap-0.1.0/tests/test_setup.py +443 -0
  112. sendermap-0.1.0/tests/test_skill.py +53 -0
  113. sendermap-0.1.0/tests/test_sources.py +134 -0
  114. sendermap-0.1.0/tests/test_spf.py +157 -0
  115. sendermap-0.1.0/tests/test_spf_checks.py +181 -0
  116. sendermap-0.1.0/tests/test_spf_eval.py +244 -0
  117. sendermap-0.1.0/tests/test_story.py +378 -0
  118. sendermap-0.1.0/tests/test_subprocessor_names.py +63 -0
  119. sendermap-0.1.0/tests/test_supporting_evidence.py +92 -0
  120. sendermap-0.1.0/tests/test_vendor_proofs.py +100 -0
  121. sendermap-0.1.0/tests/test_verify_proofs.py +282 -0
  122. sendermap-0.1.0/tests/test_web.py +55 -0
  123. sendermap-0.1.0/tests/test_web_guard.py +181 -0
  124. sendermap-0.1.0/tests/test_web_mcp.py +105 -0
  125. sendermap-0.1.0/tests/vectors/rfc7208-tests.LICENSE +27 -0
  126. sendermap-0.1.0/tests/vectors/rfc7208-tests.yml +2660 -0
  127. sendermap-0.1.0/tools/hunt_proofs.py +751 -0
  128. sendermap-0.1.0/tools/record_corpus.py +202 -0
  129. sendermap-0.1.0/tools/release_notes.py +47 -0
  130. sendermap-0.1.0/tools/verify_proofs.py +423 -0
  131. sendermap-0.1.0/uv.lock +1890 -0
@@ -0,0 +1,96 @@
1
+ # Publish a release to PyPI when a tag v* is pushed, then make the GitHub release.
2
+ #
3
+ # PYPI_READY: publishing is held until the repository variable PYPI_READY is true. It is set
4
+ # under Settings, Secrets and variables, Actions, Variables, once the PyPI trusted publisher
5
+ # exists. Until then a v* tag still builds the sdist and wheel and uploads them as the workflow
6
+ # artifact "dist", and the publish and release jobs are skipped, not failed.
7
+ #
8
+ # build: the sdist and wheel with python -m build, from a full clone so hatch-vcs reads the tag,
9
+ # checked to carry the tag's version; the tag's section of CHANGELOG.md becomes the release
10
+ # notes, and a tag without one fails here, before anything is published.
11
+ # publish: uploads to PyPI by trusted publishing (no token: the pypi environment and the
12
+ # id-token permission), with attestations.
13
+ # release: the GitHub release for the tag, with the notes and the built files.
14
+
15
+ name: Publish
16
+
17
+ on:
18
+ push:
19
+ tags: ["v*"]
20
+
21
+ permissions:
22
+ contents: read
23
+
24
+ jobs:
25
+ build:
26
+ runs-on: ubuntu-latest
27
+ timeout-minutes: 10
28
+ steps:
29
+ - uses: actions/checkout@v5
30
+ with:
31
+ fetch-depth: 0 # hatch-vcs takes the version from the tag
32
+ - uses: actions/setup-python@v6
33
+ with:
34
+ python-version: "3.12"
35
+ - name: Build
36
+ run: |
37
+ python -m pip install build
38
+ python -m build
39
+ - name: Check the version is the tag's
40
+ run: |
41
+ version="${GITHUB_REF_NAME#v}"
42
+ ls dist
43
+ test -f "dist/sendermap-${version}.tar.gz"
44
+ test -f "dist/sendermap-${version}-py3-none-any.whl"
45
+ - name: Release notes from CHANGELOG.md
46
+ run: python tools/release_notes.py "$GITHUB_REF_NAME" > release-notes.md
47
+ - uses: actions/upload-artifact@v4
48
+ with:
49
+ name: dist
50
+ path: dist/
51
+ - uses: actions/upload-artifact@v4
52
+ with:
53
+ name: release-notes
54
+ path: release-notes.md
55
+
56
+ publish:
57
+ needs: build
58
+ if: vars.PYPI_READY == 'true'
59
+ runs-on: ubuntu-latest
60
+ timeout-minutes: 10
61
+ environment:
62
+ name: pypi
63
+ url: https://pypi.org/p/sendermap
64
+ permissions:
65
+ id-token: write # trusted publishing
66
+ steps:
67
+ - uses: actions/download-artifact@v4
68
+ with:
69
+ name: dist
70
+ path: dist/
71
+ - uses: pypa/gh-action-pypi-publish@release/v1
72
+ with:
73
+ attestations: true
74
+
75
+ release:
76
+ needs: publish
77
+ runs-on: ubuntu-latest
78
+ timeout-minutes: 5
79
+ permissions:
80
+ contents: write # create the release
81
+ steps:
82
+ - uses: actions/download-artifact@v4
83
+ with:
84
+ name: dist
85
+ path: dist/
86
+ - uses: actions/download-artifact@v4
87
+ with:
88
+ name: release-notes
89
+ - name: Create the GitHub release
90
+ env:
91
+ GH_TOKEN: ${{ github.token }}
92
+ run: >
93
+ gh release create "$GITHUB_REF_NAME" dist/*
94
+ --repo "$GITHUB_REPOSITORY"
95
+ --title "Sendermap ${GITHUB_REF_NAME#v}"
96
+ --notes-file release-notes.md
@@ -0,0 +1,49 @@
1
+ # Map every case in tests/corpus live again, weekly (an hour after the proofs job) and by hand,
2
+ # and report how each live map differs from its expected.json, leaving out checked_at and the
3
+ # two run fields. Drift is reported in the job summary and fails nothing: the world changing is
4
+ # not a bug, and a drifted case is re-recorded by hand after reading the diff. The fresh
5
+ # recordings are uploaded for 30 days; nothing is committed. The job fails only when a live map
6
+ # could not be built at all.
7
+
8
+ name: Recheck corpus
9
+
10
+ on:
11
+ schedule:
12
+ - cron: "0 10 * * 1" # Monday 10:00 UTC
13
+ workflow_dispatch:
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ recheck:
20
+ runs-on: ubuntu-latest
21
+ timeout-minutes: 20
22
+ steps:
23
+ - uses: actions/checkout@v5
24
+ - uses: astral-sh/setup-uv@v6
25
+ with:
26
+ python-version: "3.12"
27
+ - name: Install
28
+ run: uv sync --locked --no-dev
29
+ - name: Map each case live and compare
30
+ run: |
31
+ failed=()
32
+ for case in tests/corpus/*/case.json; do
33
+ domain=$(basename "$(dirname "$case")")
34
+ if ! uv run --no-sync python tools/record_corpus.py "$domain" --recheck recheck >>"$GITHUB_STEP_SUMMARY"; then
35
+ failed+=("$domain")
36
+ printf '### %s\n\nThe live map could not be built.\n\n' "$domain" >>"$GITHUB_STEP_SUMMARY"
37
+ fi
38
+ done
39
+ if (( ${#failed[@]} )); then
40
+ echo "::error::no live map for: ${failed[*]}"
41
+ exit 1
42
+ fi
43
+ - uses: actions/upload-artifact@v4
44
+ if: always()
45
+ with:
46
+ name: corpus-recheck-${{ github.run_id }}
47
+ path: recheck/
48
+ retention-days: 30
49
+ if-no-files-found: ignore
@@ -0,0 +1,40 @@
1
+ # Run the suite on every push to main, on every pull request and by hand: ruff check, ruff
2
+ # format --check, mypy and pytest, on each supported Python, installed from uv.lock exactly.
3
+ # pytest takes its addopts from pyproject.toml, so the network is blocked here the same way it
4
+ # is locally. Any of the four failing fails the job.
5
+
6
+ name: Test
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ pull_request:
12
+ workflow_dispatch:
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ test:
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 10
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ python-version: ["3.12", "3.13"]
25
+ steps:
26
+ - uses: actions/checkout@v5
27
+ - uses: astral-sh/setup-uv@v6
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+ enable-cache: true
31
+ - name: Install
32
+ run: uv sync --locked --extra web --extra mcp # mypy and the tests read fastapi and fastmcp
33
+ - name: ruff check
34
+ run: uv run --no-sync ruff check
35
+ - name: ruff format --check
36
+ run: uv run --no-sync ruff format --check
37
+ - name: mypy
38
+ run: uv run --no-sync mypy
39
+ - name: pytest
40
+ run: uv run --no-sync pytest
@@ -0,0 +1,62 @@
1
+ # Recheck every proof in vendors.json against live DNS and the web: on any pull request that
2
+ # touches the file or the checker, weekly, and by hand. A proof not found fails the job, which
3
+ # blocks a pull request; on the weekly run it also opens an issue, or comments on the open one. A
4
+ # proof that could not be checked (timeout, lookup failure, a page that refuses the request) is
5
+ # listed in the output and fails nothing.
6
+
7
+ name: Verify proofs
8
+
9
+ on:
10
+ pull_request:
11
+ paths:
12
+ - sendermap/data/vendors.json
13
+ - tools/verify_proofs.py
14
+ schedule:
15
+ - cron: "0 9 * * 1" # Monday 09:00 UTC
16
+ workflow_dispatch:
17
+
18
+ permissions:
19
+ contents: read
20
+ issues: write
21
+
22
+ defaults:
23
+ run:
24
+ shell: bash # -eo pipefail, so the checker's exit code survives the tee
25
+
26
+ jobs:
27
+ verify:
28
+ runs-on: ubuntu-latest
29
+ timeout-minutes: 10
30
+ steps:
31
+ - uses: actions/checkout@v5
32
+ - uses: astral-sh/setup-uv@v6
33
+ with:
34
+ python-version: "3.12"
35
+ - name: Install
36
+ run: uv sync --locked --no-dev
37
+ - name: Check every proof
38
+ id: verify
39
+ run: uv run --no-sync python tools/verify_proofs.py --json | tee results.jsonl
40
+ - name: Open or update an issue per failing proof
41
+ if: failure() && steps.verify.outcome == 'failure' && github.event_name == 'schedule'
42
+ env:
43
+ GH_TOKEN: ${{ github.token }}
44
+ run: |
45
+ today=$(date -u +%F)
46
+ open=$(gh issue list --state open --limit 1000 --json number,title)
47
+ jq -c 'select(.status == "not found")' results.jsonl | while read -r row; do
48
+ title=$(jq -r '"Proof failed: \(.vendor) \(.field) \(.pattern)"' <<<"$row")
49
+ body=$(jq -r --arg today "$today" '[
50
+ "Proof domain: \(.domain)",
51
+ "Record expected: \(.record), a \(.expected)",
52
+ "Published now: \(.observed)",
53
+ "Checked: \($today)"
54
+ ] | join("\n\n")' <<<"$row")
55
+ number=$(jq -r --arg title "$title" \
56
+ 'first(.[] | select(.title == $title) | .number) // empty' <<<"$open")
57
+ if [[ -n $number ]]; then
58
+ gh issue comment "$number" --body "$body" </dev/null
59
+ else
60
+ gh issue create --title "$title" --body "$body" </dev/null
61
+ fi
62
+ done
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ _site/
3
+ .DS_Store
4
+ *.log
5
+ __pycache__/