ms-cloud-endpoints 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 (33) hide show
  1. ms_cloud_endpoints-0.1.0/.gitignore +12 -0
  2. ms_cloud_endpoints-0.1.0/CHANGELOG.md +27 -0
  3. ms_cloud_endpoints-0.1.0/LICENSE +21 -0
  4. ms_cloud_endpoints-0.1.0/PKG-INFO +281 -0
  5. ms_cloud_endpoints-0.1.0/README.md +253 -0
  6. ms_cloud_endpoints-0.1.0/pyproject.toml +75 -0
  7. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/__init__.py +4 -0
  8. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/__main__.py +6 -0
  9. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/cli.py +255 -0
  10. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/clouds.py +51 -0
  11. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/fetch.py +87 -0
  12. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/html_tables.py +160 -0
  13. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/model.py +10 -0
  14. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/netutil.py +100 -0
  15. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/output.py +77 -0
  16. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/py.typed +0 -0
  17. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/sources/__init__.py +1 -0
  18. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/sources/m365.py +68 -0
  19. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/sources/m365_additional.py +105 -0
  20. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/sources/private_dns.py +198 -0
  21. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/sources/service_tags.py +98 -0
  22. ms_cloud_endpoints-0.1.0/src/ms_cloud_endpoints/summary.py +82 -0
  23. ms_cloud_endpoints-0.1.0/tests/conftest.py +80 -0
  24. ms_cloud_endpoints-0.1.0/tests/fixtures/additional_endpoints.html +72 -0
  25. ms_cloud_endpoints-0.1.0/tests/fixtures/geo_code_list.html +18 -0
  26. ms_cloud_endpoints-0.1.0/tests/fixtures/m365_endpoints_worldwide.json +49 -0
  27. ms_cloud_endpoints-0.1.0/tests/fixtures/m365_version_worldwide.json +5 -0
  28. ms_cloud_endpoints-0.1.0/tests/fixtures/private_endpoint_dns.html +124 -0
  29. ms_cloud_endpoints-0.1.0/tests/fixtures/service_tags_details.html +7 -0
  30. ms_cloud_endpoints-0.1.0/tests/fixtures/service_tags_public.json +45 -0
  31. ms_cloud_endpoints-0.1.0/tests/test_cli.py +169 -0
  32. ms_cloud_endpoints-0.1.0/tests/test_helpers.py +85 -0
  33. ms_cloud_endpoints-0.1.0/tests/test_sources.py +149 -0
@@ -0,0 +1,12 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+
11
+ # Generated output
12
+ output/
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
6
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - `--ipv6`, the explicit counterpart of `--no-ipv6`. IPv6 is still included by default, and the last of the two
13
+ options wins.
14
+
15
+ ## [0.1.0]
16
+
17
+ ### Added
18
+
19
+ - `ms-cloud-endpoints` command (also `python -m ms_cloud_endpoints`) that writes JSON and/or YAML.
20
+ - Sources: Microsoft 365 IP Address and URL web service, the "other endpoints" Learn page (heuristic extraction),
21
+ Azure IP Ranges and Service Tags (download link discovered from the download page), Azure Private Endpoint
22
+ private DNS zone values, and Azure Backup geo-codes.
23
+ - Public, China and US Government clouds (`--cloud`), source selection (`--source`), service tag filtering (`--tag`),
24
+ `--no-ipv6`, `--no-summary` and `--keep-going`.
25
+ - Private Endpoint zone template expansion for `{regionName}`/`{region}` and `{regionCode}`.
26
+ - Aggregated `summary` with deduplicated domains and collapsed IPv4/IPv6 prefixes.
27
+ - A persisted per-machine `ClientRequestId` for the Microsoft 365 web service.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Radux Technologies
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,281 @@
1
+ Metadata-Version: 2.5
2
+ Name: ms-cloud-endpoints
3
+ Version: 0.1.0
4
+ Summary: Collect Microsoft 365, Entra and Azure domains and IP ranges from the documented Microsoft sources and export them as JSON/YAML.
5
+ Project-URL: Homepage, https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter
6
+ Project-URL: Source, https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints
7
+ Project-URL: Documentation, https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints/blob/main/README.md
8
+ Project-URL: Issues, https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints/issues
9
+ Project-URL: Changelog, https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints/blob/main/CHANGELOG.md
10
+ Author-email: Jayonics <jameshopwood1998@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: azure,endpoints,entra,ip-ranges,microsoft-365,office-365,private-endpoint,service-tags,split-tunnel
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: System :: Networking
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.12
26
+ Requires-Dist: pyyaml>=6.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # ms-cloud-endpoints
30
+
31
+ Collect the **domains and IP ranges of Microsoft 365, Microsoft Entra and Azure** from the sources Microsoft
32
+ documents, and export them as one normalised **JSON and/or YAML** document.
33
+
34
+ It is the data-collection stage of
35
+ [Azure-O365-DNS-UDR-SplitRouter](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter), which routes
36
+ Microsoft traffic over a dedicated link with `systemd-resolved` routing domains and `systemd-networkd` routes. The
37
+ output is also usable on its own, for example for firewall allow-lists, proxy PAC files or split-tunnel VPN
38
+ profiles.
39
+
40
+ - [Features](#features)
41
+ - [Requirements](#requirements)
42
+ - [Installation](#installation)
43
+ - [Usage](#usage)
44
+ - [Sources and clouds](#sources-and-clouds)
45
+ - [Options](#options)
46
+ - [Output schema](#output-schema)
47
+ - [Notes and limitations](#notes-and-limitations)
48
+ - [Development](#development)
49
+ - [License](#license)
50
+
51
+ ## Features
52
+
53
+ - Fetches all documented sources, finding the weekly Service Tag download link on the download page each run
54
+ instead of hard-coding it.
55
+ - Public cloud by default, with China and US Government (Microsoft 365 GCC High/DoD) selectable.
56
+ - Normalises everything: `snake_case` keys, lower-cased domains, IPv4 and IPv6 prefixes separated.
57
+ - Expands Private Endpoint DNS zone templates (`{regionName}`, `{regionCode}`) into concrete zone names.
58
+ - Optional `summary`: deduplicated domains, wildcard suffixes, root domains and collapsed IPv4/IPv6 prefix lists
59
+ across all sources.
60
+ - Records the upstream versions (Microsoft 365 `version`, Service Tag `changeNumber`) so runs can be compared.
61
+ - The only runtime dependency is [PyYAML](https://pypi.org/project/PyYAML/). HTTP and HTML parsing use the
62
+ standard library.
63
+
64
+ ## Requirements
65
+
66
+ - Python 3.12 or later (tested on 3.12, 3.13 and 3.14)
67
+ - Outbound HTTPS to `endpoints.office.com`, `www.microsoft.com`, `download.microsoft.com` and `learn.microsoft.com`
68
+
69
+ ## Installation
70
+
71
+ From a clone of this repository (or the `ms-cloud-endpoints/` submodule of the
72
+ [parent project](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter)):
73
+
74
+ ```bash
75
+ pipx install . # isolated command-line install (recommended)
76
+ # or
77
+ pip install . # into the current environment
78
+ ```
79
+
80
+ Directly from GitHub:
81
+
82
+ ```bash
83
+ pipx install "git+https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints.git"
84
+ ```
85
+
86
+ Both install the `ms-cloud-endpoints` command. `python -m ms_cloud_endpoints` works too.
87
+
88
+ For a version-pinned deployment once the release exists on PyPI:
89
+
90
+ ```bash
91
+ pipx install "ms-cloud-endpoints==0.1.0"
92
+ ms-cloud-endpoints --version
93
+ # Upgrade or roll back by explicitly selecting the desired published version:
94
+ pipx install --force "ms-cloud-endpoints==0.1.0"
95
+ ```
96
+
97
+ Do not use `pipx upgrade`/`upgrade-all` for pinned deployments. Pins select the application version, not a full
98
+ transitive dependency lock. For system-wide installation alongside the router, use `sudo pipx install --global`
99
+ (pipx >= 1.5); see the parent's [deployment guide](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter/blob/main/USAGE.md#version-pinned-deployment-from-pypi).
100
+
101
+ ## Usage
102
+
103
+ ```bash
104
+ # Public cloud, all sources -> output/ms-cloud-endpoints.json and output/ms-cloud-endpoints.yaml
105
+ ms-cloud-endpoints -o output/
106
+
107
+ # Every cloud (Public, China, US Government)
108
+ ms-cloud-endpoints --cloud all -o output/
109
+
110
+ # Only the Entra ID service tags, IPv4 only, as YAML on stdout
111
+ ms-cloud-endpoints --source service-tags --tag 'AzureActiveDirectory*' --no-ipv6 --format yaml
112
+
113
+ # Microsoft 365 only, JSON to a file, keep going if a source fails
114
+ ms-cloud-endpoints --source m365 --source m365-additional -o m365.json --keep-going
115
+ ```
116
+
117
+ ### Exit codes
118
+
119
+ | Code | Meaning |
120
+ |------|---------|
121
+ | `0` | Success |
122
+ | `1` | A source or writing the output failed (nothing is written) |
123
+ | `2` | Usage error (invalid option or `ClientRequestId`) |
124
+ | `3` | Partial output: one or more sources failed with `--keep-going` (failures are listed in `metadata.sources`) |
125
+
126
+ ## Sources and clouds
127
+
128
+ `--cloud` selects which clouds to collect. It is repeatable, and `all` selects every cloud. The default is `public`.
129
+
130
+ | Source (`--source`) | `public` | `china` | `usgov` |
131
+ |---------------------|----------|---------|---------|
132
+ | `m365`: [Microsoft 365 IP Address and URL web service](https://learn.microsoft.com/en-us/microsoft-365/enterprise/microsoft-365-ip-web-service) `version` + `endpoints` | `Worldwide` | `China` | `USGovDoD`, `USGovGCCHigh` |
133
+ | `m365-additional`: [Other endpoints not included in the web service](https://learn.microsoft.com/en-us/microsoft-365/enterprise/additional-office365-ip-addresses-and-urls) | one page for every cloud. Rows are split by their cloud labels ("Public Cloud:", "GCC High, DoD …:", "…21Vianet:") | | |
134
+ | `service-tags`: [Azure IP Ranges and Service Tags](https://www.microsoft.com/en-us/download/details.aspx?id=56519) | `Public` (id 56519) | `China` (id 57062) | `AzureGovernment` (id 57063) |
135
+ | `private-dns`: [Private Endpoint DNS zone values](https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-dns) + [Azure Backup geo-codes](https://learn.microsoft.com/en-us/azure/backup/scripts/geo-code-list) | `Commercial` | `China` | `Government` |
136
+
137
+ Microsoft Entra is covered by the Microsoft 365 `Common` endpoint sets, the `AzureActiveDirectory*` service tags and
138
+ the Entra rows on the "other endpoints" page.
139
+
140
+ The Microsoft 365 `changes` method isn't used, because `endpoints` always returns a complete snapshot. Compare the
141
+ recorded versions between runs instead.
142
+
143
+ ## Options
144
+
145
+ | Option | Default | Meaning |
146
+ |--------|---------|---------|
147
+ | `--cloud public\|china\|usgov\|all` | `public` | Clouds to collect (repeatable) |
148
+ | `--source m365\|m365-additional\|service-tags\|private-dns` | all | Sources to collect (repeatable) |
149
+ | `--tag GLOB` | all tags | Only keep service tags whose name matches (case-insensitive glob, repeatable), e.g. `AzureCloud`, `Storage.*` |
150
+ | `--ipv6`, `--no-ipv6` | `--ipv6` | Include IPv6 prefixes, or drop them everywhere. Recorded in `metadata.options.ipv6`. The last one given wins |
151
+ | `--no-summary` | off | Omit the `summary` section |
152
+ | `--format json\|yaml\|both` | `json` on stdout, `both` with `-o` | Output format |
153
+ | `-o`, `--output PATH` | stdout | File, or directory (existing or ending in `/`) that receives `ms-cloud-endpoints.json`/`.yaml`. With `both` and a file path, the suffix is replaced per format |
154
+ | `--client-request-id GUID` | see below | `ClientRequestId` sent to the Microsoft 365 web service |
155
+ | `--keep-going` | off | Record failed sources in `metadata.sources` and still write the output |
156
+ | `--timeout SECONDS` | `60` | HTTP timeout (three attempts with backoff on network errors, 429 and 5xx) |
157
+ | `-v`, `-vv` | warnings | Info or debug logging on stderr |
158
+ | `--version` | | Print the version |
159
+
160
+ **ClientRequestId.** Microsoft asks for one GUID per machine. The value comes from `--client-request-id` first, then
161
+ `$MS_ENDPOINTS_CLIENT_REQUEST_ID`. Otherwise a GUID is generated once and stored in
162
+ `$XDG_CONFIG_HOME/ms-cloud-endpoints/client-request-id` (default `~/.config/…`). It's only created when the `m365`
163
+ source is selected.
164
+
165
+ ## Output schema
166
+
167
+ JSON and YAML have the same structure. Keys are `snake_case`, domains are lower-cased, and IP prefixes are
168
+ normalised and split by family.
169
+
170
+ ```yaml
171
+ metadata: # generator, generator_version, generated_at, clouds, sources_selected, options, client_request_id
172
+ sources: # one record per fetched document: source, scope, url, version, fetched_at, status[, error]
173
+ m365:
174
+ Worldwide:
175
+ version: '2026081400'
176
+ endpoint_sets: # id, service_area, service_area_display_name, category, required, express_route,
177
+ # urls, ipv4, ipv6, tcp_ports, udp_ports, notes
178
+ m365_additional:
179
+ source_url: …
180
+ rows: # row, purpose, source, destination_text (raw), type, domains, ipv4, ipv6,
181
+ # by_cloud.{public,usgov,china}.{domains,ipv4,ipv6}
182
+ azure_service_tags:
183
+ Public: # cloud, change_number, regions, source_url
184
+ tags: # name, id, region, region_id, system_service, platform, network_features, change_number, ipv4, ipv6
185
+ private_dns_zones:
186
+ backup_geo_codes: # geo_code, region_name, region
187
+ Commercial: # category, resource_type, subresources, zone_templates, zones, forwarder_templates,
188
+ # forwarders, unresolved_placeholders, footnotes
189
+ summary:
190
+ domains: # exact, wildcard, wildcard_suffixes, root_domains
191
+ ipv4: [] # collapsed across all selected sources and clouds
192
+ ipv6: []
193
+ counts: {}
194
+ ```
195
+
196
+ ## Notes and limitations
197
+
198
+ - **Private Endpoint templates.**
199
+ - `{regionName}` and `{region}` are expanded with the regions from the same cloud's service tags, so they stay
200
+ unexpanded unless `service-tags` is collected in the same run.
201
+ - `{regionCode}` is expanded with the lower-cased Azure Backup geo-codes.
202
+ - Other placeholders (`{dnsPrefix}`, `{subzone}`, `{partitionId}`, …) can't be expanded and are listed in
203
+ `unresolved_placeholders`.
204
+ - In the summary every placeholder counts as a `*` wildcard, so `{regionName}.kusto.windows.net` adds the suffix
205
+ `kusto.windows.net`.
206
+ - **"Other endpoints" page.** This page is free text, so extraction is heuristic: FQDNs, wildcards, IPs and CIDRs
207
+ are pulled out of the prose. The raw `destination_text` is always kept. When the destination is empty, the
208
+ purpose text is scanned instead.
209
+ - **Third-party domains.** The summary includes the certificate authority and CDN domains Microsoft lists (for
210
+ example `digicert.com`, `akamaihd.net`). Filter them out if you use the list for DNS routing. `root_domains` takes
211
+ the last two labels (three for known multi-label suffixes such as `com.cn`). It doesn't use the Public Suffix
212
+ List.
213
+ - **Upstream HTML changes.** The Learn pages are parsed by section id and table header text. If Microsoft changes
214
+ their structure, the affected source fails with a clear error instead of returning partial data.
215
+ - **Size.** All Public service tags make the JSON about 6 MB. Use `--tag` to narrow it.
216
+ - **Refresh cadence.** Microsoft 365 endpoints change monthly (with 30 days' notice). Service tags are published
217
+ weekly. Check the Microsoft 365 `version` at most once an hour.
218
+
219
+ ## Development
220
+
221
+ Development dependencies are declared as a [PEP 735](https://peps.python.org/pep-0735/) dependency group (`dev`:
222
+ pytest and ruff).
223
+
224
+ ```bash
225
+ python -m venv .venv && . .venv/bin/activate
226
+ pip install -e . --group dev # pip >= 25.1; with uv: `uv sync`
227
+
228
+ pytest # offline tests against the fixtures in tests/fixtures/
229
+ ruff check . && ruff format --check .
230
+ python -m build # sdist + wheel in dist/ (pip install build)
231
+ ```
232
+
233
+ Layout:
234
+
235
+ ```
236
+ src/ms_cloud_endpoints/
237
+ cli.py command line, orchestration, exit codes
238
+ clouds.py cloud name -> upstream identifiers
239
+ fetch.py urllib client with retries (Fetcher protocol)
240
+ html_tables.py html.parser DOM + Learn page section/table helpers
241
+ netutil.py prefix/domain normalisation and extraction
242
+ output.py JSON/YAML writers (atomic file writes)
243
+ summary.py cross-source aggregation
244
+ sources/ one module per upstream source
245
+ tests/ pytest suite; fixtures/ holds trimmed copies of each upstream document
246
+ ```
247
+
248
+ The repository's [pre-commit configuration](.pre-commit-config.yaml)
249
+ runs ruff on this project (`pre-commit install`). CI runs lint, the tests on Python 3.12 to 3.14, and a package
250
+ build.
251
+
252
+ ### Releases
253
+
254
+ The existing workflow also runs on `v*` tag pushes. Stable tags such as `v0.1.0` must exactly match the version
255
+ in the built wheel (sourced from `src/ms_cloud_endpoints/__init__.py`); prerelease or mismatched tags fail.
256
+ After lint, tests and build checks pass, the `pypi` environment gates PyPI Trusted Publishing. A subsequent
257
+ job creates a GitHub Release with the same wheel and source distribution. Pull requests, branch pushes and
258
+ manual runs never publish.
259
+
260
+ Before tagging, configure this repository's `pypi` GitHub environment to allow deployments only from `v*`
261
+ tags (optionally with required reviewers, where the GitHub plan supports them). Register a PyPI Trusted Publisher for project `ms-cloud-endpoints`,
262
+ owner `Jayonics`, repository `Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints`, workflow
263
+ `ms-cloud-endpoints.yml`, environment `pypi`. No PyPI API token is required. Optionally restrict release-tag
264
+ creation and mutation with a tag ruleset.
265
+
266
+ Commit the version and changelog first, then release the reviewed commit:
267
+
268
+ ```bash
269
+ git tag -a v0.1.0 -m "Release 0.1.0"
270
+ git push origin main
271
+ git push origin v0.1.0
272
+ ```
273
+
274
+ Pushing the tag publishes automatically once checks pass (or after approval, if reviewers are required). Never move a published tag or reuse an uploaded PyPI
275
+ version. If GitHub release creation fails after PyPI succeeds, re-run only failed jobs, not the whole workflow.
276
+ See the parent's [release procedure](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter/blob/main/USAGE.md#publishing-releases)
277
+ for the complete setup and coordinated deployment process.
278
+
279
+ ## License
280
+
281
+ [MIT](LICENSE) © Radux Technologies
@@ -0,0 +1,253 @@
1
+ # ms-cloud-endpoints
2
+
3
+ Collect the **domains and IP ranges of Microsoft 365, Microsoft Entra and Azure** from the sources Microsoft
4
+ documents, and export them as one normalised **JSON and/or YAML** document.
5
+
6
+ It is the data-collection stage of
7
+ [Azure-O365-DNS-UDR-SplitRouter](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter), which routes
8
+ Microsoft traffic over a dedicated link with `systemd-resolved` routing domains and `systemd-networkd` routes. The
9
+ output is also usable on its own, for example for firewall allow-lists, proxy PAC files or split-tunnel VPN
10
+ profiles.
11
+
12
+ - [Features](#features)
13
+ - [Requirements](#requirements)
14
+ - [Installation](#installation)
15
+ - [Usage](#usage)
16
+ - [Sources and clouds](#sources-and-clouds)
17
+ - [Options](#options)
18
+ - [Output schema](#output-schema)
19
+ - [Notes and limitations](#notes-and-limitations)
20
+ - [Development](#development)
21
+ - [License](#license)
22
+
23
+ ## Features
24
+
25
+ - Fetches all documented sources, finding the weekly Service Tag download link on the download page each run
26
+ instead of hard-coding it.
27
+ - Public cloud by default, with China and US Government (Microsoft 365 GCC High/DoD) selectable.
28
+ - Normalises everything: `snake_case` keys, lower-cased domains, IPv4 and IPv6 prefixes separated.
29
+ - Expands Private Endpoint DNS zone templates (`{regionName}`, `{regionCode}`) into concrete zone names.
30
+ - Optional `summary`: deduplicated domains, wildcard suffixes, root domains and collapsed IPv4/IPv6 prefix lists
31
+ across all sources.
32
+ - Records the upstream versions (Microsoft 365 `version`, Service Tag `changeNumber`) so runs can be compared.
33
+ - The only runtime dependency is [PyYAML](https://pypi.org/project/PyYAML/). HTTP and HTML parsing use the
34
+ standard library.
35
+
36
+ ## Requirements
37
+
38
+ - Python 3.12 or later (tested on 3.12, 3.13 and 3.14)
39
+ - Outbound HTTPS to `endpoints.office.com`, `www.microsoft.com`, `download.microsoft.com` and `learn.microsoft.com`
40
+
41
+ ## Installation
42
+
43
+ From a clone of this repository (or the `ms-cloud-endpoints/` submodule of the
44
+ [parent project](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter)):
45
+
46
+ ```bash
47
+ pipx install . # isolated command-line install (recommended)
48
+ # or
49
+ pip install . # into the current environment
50
+ ```
51
+
52
+ Directly from GitHub:
53
+
54
+ ```bash
55
+ pipx install "git+https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints.git"
56
+ ```
57
+
58
+ Both install the `ms-cloud-endpoints` command. `python -m ms_cloud_endpoints` works too.
59
+
60
+ For a version-pinned deployment once the release exists on PyPI:
61
+
62
+ ```bash
63
+ pipx install "ms-cloud-endpoints==0.1.0"
64
+ ms-cloud-endpoints --version
65
+ # Upgrade or roll back by explicitly selecting the desired published version:
66
+ pipx install --force "ms-cloud-endpoints==0.1.0"
67
+ ```
68
+
69
+ Do not use `pipx upgrade`/`upgrade-all` for pinned deployments. Pins select the application version, not a full
70
+ transitive dependency lock. For system-wide installation alongside the router, use `sudo pipx install --global`
71
+ (pipx >= 1.5); see the parent's [deployment guide](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter/blob/main/USAGE.md#version-pinned-deployment-from-pypi).
72
+
73
+ ## Usage
74
+
75
+ ```bash
76
+ # Public cloud, all sources -> output/ms-cloud-endpoints.json and output/ms-cloud-endpoints.yaml
77
+ ms-cloud-endpoints -o output/
78
+
79
+ # Every cloud (Public, China, US Government)
80
+ ms-cloud-endpoints --cloud all -o output/
81
+
82
+ # Only the Entra ID service tags, IPv4 only, as YAML on stdout
83
+ ms-cloud-endpoints --source service-tags --tag 'AzureActiveDirectory*' --no-ipv6 --format yaml
84
+
85
+ # Microsoft 365 only, JSON to a file, keep going if a source fails
86
+ ms-cloud-endpoints --source m365 --source m365-additional -o m365.json --keep-going
87
+ ```
88
+
89
+ ### Exit codes
90
+
91
+ | Code | Meaning |
92
+ |------|---------|
93
+ | `0` | Success |
94
+ | `1` | A source or writing the output failed (nothing is written) |
95
+ | `2` | Usage error (invalid option or `ClientRequestId`) |
96
+ | `3` | Partial output: one or more sources failed with `--keep-going` (failures are listed in `metadata.sources`) |
97
+
98
+ ## Sources and clouds
99
+
100
+ `--cloud` selects which clouds to collect. It is repeatable, and `all` selects every cloud. The default is `public`.
101
+
102
+ | Source (`--source`) | `public` | `china` | `usgov` |
103
+ |---------------------|----------|---------|---------|
104
+ | `m365`: [Microsoft 365 IP Address and URL web service](https://learn.microsoft.com/en-us/microsoft-365/enterprise/microsoft-365-ip-web-service) `version` + `endpoints` | `Worldwide` | `China` | `USGovDoD`, `USGovGCCHigh` |
105
+ | `m365-additional`: [Other endpoints not included in the web service](https://learn.microsoft.com/en-us/microsoft-365/enterprise/additional-office365-ip-addresses-and-urls) | one page for every cloud. Rows are split by their cloud labels ("Public Cloud:", "GCC High, DoD …:", "…21Vianet:") | | |
106
+ | `service-tags`: [Azure IP Ranges and Service Tags](https://www.microsoft.com/en-us/download/details.aspx?id=56519) | `Public` (id 56519) | `China` (id 57062) | `AzureGovernment` (id 57063) |
107
+ | `private-dns`: [Private Endpoint DNS zone values](https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-dns) + [Azure Backup geo-codes](https://learn.microsoft.com/en-us/azure/backup/scripts/geo-code-list) | `Commercial` | `China` | `Government` |
108
+
109
+ Microsoft Entra is covered by the Microsoft 365 `Common` endpoint sets, the `AzureActiveDirectory*` service tags and
110
+ the Entra rows on the "other endpoints" page.
111
+
112
+ The Microsoft 365 `changes` method isn't used, because `endpoints` always returns a complete snapshot. Compare the
113
+ recorded versions between runs instead.
114
+
115
+ ## Options
116
+
117
+ | Option | Default | Meaning |
118
+ |--------|---------|---------|
119
+ | `--cloud public\|china\|usgov\|all` | `public` | Clouds to collect (repeatable) |
120
+ | `--source m365\|m365-additional\|service-tags\|private-dns` | all | Sources to collect (repeatable) |
121
+ | `--tag GLOB` | all tags | Only keep service tags whose name matches (case-insensitive glob, repeatable), e.g. `AzureCloud`, `Storage.*` |
122
+ | `--ipv6`, `--no-ipv6` | `--ipv6` | Include IPv6 prefixes, or drop them everywhere. Recorded in `metadata.options.ipv6`. The last one given wins |
123
+ | `--no-summary` | off | Omit the `summary` section |
124
+ | `--format json\|yaml\|both` | `json` on stdout, `both` with `-o` | Output format |
125
+ | `-o`, `--output PATH` | stdout | File, or directory (existing or ending in `/`) that receives `ms-cloud-endpoints.json`/`.yaml`. With `both` and a file path, the suffix is replaced per format |
126
+ | `--client-request-id GUID` | see below | `ClientRequestId` sent to the Microsoft 365 web service |
127
+ | `--keep-going` | off | Record failed sources in `metadata.sources` and still write the output |
128
+ | `--timeout SECONDS` | `60` | HTTP timeout (three attempts with backoff on network errors, 429 and 5xx) |
129
+ | `-v`, `-vv` | warnings | Info or debug logging on stderr |
130
+ | `--version` | | Print the version |
131
+
132
+ **ClientRequestId.** Microsoft asks for one GUID per machine. The value comes from `--client-request-id` first, then
133
+ `$MS_ENDPOINTS_CLIENT_REQUEST_ID`. Otherwise a GUID is generated once and stored in
134
+ `$XDG_CONFIG_HOME/ms-cloud-endpoints/client-request-id` (default `~/.config/…`). It's only created when the `m365`
135
+ source is selected.
136
+
137
+ ## Output schema
138
+
139
+ JSON and YAML have the same structure. Keys are `snake_case`, domains are lower-cased, and IP prefixes are
140
+ normalised and split by family.
141
+
142
+ ```yaml
143
+ metadata: # generator, generator_version, generated_at, clouds, sources_selected, options, client_request_id
144
+ sources: # one record per fetched document: source, scope, url, version, fetched_at, status[, error]
145
+ m365:
146
+ Worldwide:
147
+ version: '2026081400'
148
+ endpoint_sets: # id, service_area, service_area_display_name, category, required, express_route,
149
+ # urls, ipv4, ipv6, tcp_ports, udp_ports, notes
150
+ m365_additional:
151
+ source_url: …
152
+ rows: # row, purpose, source, destination_text (raw), type, domains, ipv4, ipv6,
153
+ # by_cloud.{public,usgov,china}.{domains,ipv4,ipv6}
154
+ azure_service_tags:
155
+ Public: # cloud, change_number, regions, source_url
156
+ tags: # name, id, region, region_id, system_service, platform, network_features, change_number, ipv4, ipv6
157
+ private_dns_zones:
158
+ backup_geo_codes: # geo_code, region_name, region
159
+ Commercial: # category, resource_type, subresources, zone_templates, zones, forwarder_templates,
160
+ # forwarders, unresolved_placeholders, footnotes
161
+ summary:
162
+ domains: # exact, wildcard, wildcard_suffixes, root_domains
163
+ ipv4: [] # collapsed across all selected sources and clouds
164
+ ipv6: []
165
+ counts: {}
166
+ ```
167
+
168
+ ## Notes and limitations
169
+
170
+ - **Private Endpoint templates.**
171
+ - `{regionName}` and `{region}` are expanded with the regions from the same cloud's service tags, so they stay
172
+ unexpanded unless `service-tags` is collected in the same run.
173
+ - `{regionCode}` is expanded with the lower-cased Azure Backup geo-codes.
174
+ - Other placeholders (`{dnsPrefix}`, `{subzone}`, `{partitionId}`, …) can't be expanded and are listed in
175
+ `unresolved_placeholders`.
176
+ - In the summary every placeholder counts as a `*` wildcard, so `{regionName}.kusto.windows.net` adds the suffix
177
+ `kusto.windows.net`.
178
+ - **"Other endpoints" page.** This page is free text, so extraction is heuristic: FQDNs, wildcards, IPs and CIDRs
179
+ are pulled out of the prose. The raw `destination_text` is always kept. When the destination is empty, the
180
+ purpose text is scanned instead.
181
+ - **Third-party domains.** The summary includes the certificate authority and CDN domains Microsoft lists (for
182
+ example `digicert.com`, `akamaihd.net`). Filter them out if you use the list for DNS routing. `root_domains` takes
183
+ the last two labels (three for known multi-label suffixes such as `com.cn`). It doesn't use the Public Suffix
184
+ List.
185
+ - **Upstream HTML changes.** The Learn pages are parsed by section id and table header text. If Microsoft changes
186
+ their structure, the affected source fails with a clear error instead of returning partial data.
187
+ - **Size.** All Public service tags make the JSON about 6 MB. Use `--tag` to narrow it.
188
+ - **Refresh cadence.** Microsoft 365 endpoints change monthly (with 30 days' notice). Service tags are published
189
+ weekly. Check the Microsoft 365 `version` at most once an hour.
190
+
191
+ ## Development
192
+
193
+ Development dependencies are declared as a [PEP 735](https://peps.python.org/pep-0735/) dependency group (`dev`:
194
+ pytest and ruff).
195
+
196
+ ```bash
197
+ python -m venv .venv && . .venv/bin/activate
198
+ pip install -e . --group dev # pip >= 25.1; with uv: `uv sync`
199
+
200
+ pytest # offline tests against the fixtures in tests/fixtures/
201
+ ruff check . && ruff format --check .
202
+ python -m build # sdist + wheel in dist/ (pip install build)
203
+ ```
204
+
205
+ Layout:
206
+
207
+ ```
208
+ src/ms_cloud_endpoints/
209
+ cli.py command line, orchestration, exit codes
210
+ clouds.py cloud name -> upstream identifiers
211
+ fetch.py urllib client with retries (Fetcher protocol)
212
+ html_tables.py html.parser DOM + Learn page section/table helpers
213
+ netutil.py prefix/domain normalisation and extraction
214
+ output.py JSON/YAML writers (atomic file writes)
215
+ summary.py cross-source aggregation
216
+ sources/ one module per upstream source
217
+ tests/ pytest suite; fixtures/ holds trimmed copies of each upstream document
218
+ ```
219
+
220
+ The repository's [pre-commit configuration](.pre-commit-config.yaml)
221
+ runs ruff on this project (`pre-commit install`). CI runs lint, the tests on Python 3.12 to 3.14, and a package
222
+ build.
223
+
224
+ ### Releases
225
+
226
+ The existing workflow also runs on `v*` tag pushes. Stable tags such as `v0.1.0` must exactly match the version
227
+ in the built wheel (sourced from `src/ms_cloud_endpoints/__init__.py`); prerelease or mismatched tags fail.
228
+ After lint, tests and build checks pass, the `pypi` environment gates PyPI Trusted Publishing. A subsequent
229
+ job creates a GitHub Release with the same wheel and source distribution. Pull requests, branch pushes and
230
+ manual runs never publish.
231
+
232
+ Before tagging, configure this repository's `pypi` GitHub environment to allow deployments only from `v*`
233
+ tags (optionally with required reviewers, where the GitHub plan supports them). Register a PyPI Trusted Publisher for project `ms-cloud-endpoints`,
234
+ owner `Jayonics`, repository `Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints`, workflow
235
+ `ms-cloud-endpoints.yml`, environment `pypi`. No PyPI API token is required. Optionally restrict release-tag
236
+ creation and mutation with a tag ruleset.
237
+
238
+ Commit the version and changelog first, then release the reviewed commit:
239
+
240
+ ```bash
241
+ git tag -a v0.1.0 -m "Release 0.1.0"
242
+ git push origin main
243
+ git push origin v0.1.0
244
+ ```
245
+
246
+ Pushing the tag publishes automatically once checks pass (or after approval, if reviewers are required). Never move a published tag or reuse an uploaded PyPI
247
+ version. If GitHub release creation fails after PyPI succeeds, re-run only failed jobs, not the whole workflow.
248
+ See the parent's [release procedure](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter/blob/main/USAGE.md#publishing-releases)
249
+ for the complete setup and coordinated deployment process.
250
+
251
+ ## License
252
+
253
+ [MIT](LICENSE) © Radux Technologies
@@ -0,0 +1,75 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ms-cloud-endpoints"
7
+ dynamic = ["version"]
8
+ description = "Collect Microsoft 365, Entra and Azure domains and IP ranges from the documented Microsoft sources and export them as JSON/YAML."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Jayonics", email = "jameshopwood1998@gmail.com" }]
14
+ keywords = [
15
+ "microsoft-365",
16
+ "office-365",
17
+ "azure",
18
+ "entra",
19
+ "service-tags",
20
+ "ip-ranges",
21
+ "endpoints",
22
+ "split-tunnel",
23
+ "private-endpoint",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 3 - Alpha",
27
+ "Environment :: Console",
28
+ "Intended Audience :: System Administrators",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3 :: Only",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: 3.14",
35
+ "Topic :: System :: Networking",
36
+ "Typing :: Typed",
37
+ ]
38
+ dependencies = ["PyYAML>=6.0"]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter"
42
+ Source = "https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints"
43
+ Documentation = "https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints/blob/main/README.md"
44
+ Issues = "https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints/issues"
45
+ Changelog = "https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints/blob/main/CHANGELOG.md"
46
+
47
+ [project.scripts]
48
+ ms-cloud-endpoints = "ms_cloud_endpoints.cli:main"
49
+
50
+ [dependency-groups]
51
+ dev = ["pytest>=8", "ruff>=0.6"]
52
+
53
+ [tool.hatch.version]
54
+ path = "src/ms_cloud_endpoints/__init__.py"
55
+
56
+ [tool.hatch.build.targets.wheel]
57
+ packages = ["src/ms_cloud_endpoints"]
58
+
59
+ [tool.hatch.build.targets.sdist]
60
+ include = ["src/", "tests/", "README.md", "CHANGELOG.md", "LICENSE"]
61
+
62
+ [tool.pytest.ini_options]
63
+ testpaths = ["tests"]
64
+ addopts = ["--import-mode=importlib"]
65
+ filterwarnings = ["error"]
66
+
67
+ [tool.ruff]
68
+ line-length = 120
69
+ target-version = "py312"
70
+
71
+ [tool.ruff.lint]
72
+ select = ["E", "F", "W", "I", "UP", "B"]
73
+
74
+ [tool.ruff.lint.isort]
75
+ known-first-party = ["ms_cloud_endpoints"]