lupaxa-cms-detection 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.
@@ -0,0 +1,24 @@
1
+ # OS
2
+ .DS_Store
3
+
4
+ # Cursor
5
+ .cursor/
6
+ .superpowers/
7
+ .worktrees/
8
+ cursor-docs
9
+
10
+ # Make
11
+ .makefiles/
12
+
13
+ # MkDocs
14
+ site/
15
+
16
+ # Python
17
+ .venv/
18
+ __pycache__/
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .pytest_cache/
22
+ dist/
23
+ build/
24
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) The Lupaxa Project
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,171 @@
1
+ Metadata-Version: 2.5
2
+ Name: lupaxa-cms-detection
3
+ Version: 0.1.0
4
+ Summary: Detect which CMS a website is using from public signals.
5
+ Project-URL: Homepage, https://cms-detection.thelupaxaproject.org/
6
+ Project-URL: Documentation, https://cms-detection.thelupaxaproject.org/
7
+ Project-URL: Repository, https://github.com/lupaxa-security-toolbox/cms-detection
8
+ Project-URL: Issues, https://github.com/lupaxa-security-toolbox/cms-detection/issues
9
+ Author: The Lupaxa Project
10
+ License: MIT License
11
+
12
+ Copyright (c) The Lupaxa Project
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENCE
32
+ Keywords: cli,cms,fingerprint,recon,security
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Environment :: Console
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
43
+ Classifier: Programming Language :: Python :: 3.14
44
+ Classifier: Topic :: Security
45
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
46
+ Classifier: Topic :: Utilities
47
+ Requires-Python: >=3.10
48
+ Requires-Dist: beautifulsoup4>=4.12
49
+ Requires-Dist: requests>=2.32
50
+ Provides-Extra: dev
51
+ Requires-Dist: bump-my-version>=1.2.0; extra == 'dev'
52
+ Requires-Dist: hatch>=1.12.0; extra == 'dev'
53
+ Requires-Dist: mkdocs-material==9.7.7; extra == 'dev'
54
+ Requires-Dist: mkdocs==1.6.1; extra == 'dev'
55
+ Requires-Dist: mypy>=1.12.0; extra == 'dev'
56
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
57
+ Requires-Dist: pytest>=8.0; extra == 'dev'
58
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
59
+ Provides-Extra: test
60
+ Requires-Dist: pytest-cov>=5.0; extra == 'test'
61
+ Requires-Dist: pytest>=8.0; extra == 'test'
62
+ Description-Content-Type: text/markdown
63
+
64
+ <!-- markdownlint-disable -->
65
+ <p align="center">
66
+ <a href="https://github.com/lupaxa-security-toolbox">
67
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/security-toolbox/readme-logo.png" alt="Project Logo" width="256"/><br/>
68
+ </a>
69
+ </p>
70
+ <h3 align="center">
71
+ The Lupaxa Security Toolbox<br />
72
+ Part of The Lupaxa Project
73
+ </h3>
74
+
75
+ <br />
76
+
77
+ # lupaxa-cms-detection
78
+
79
+ Identify which CMS a website is using from public signals — name, evidence,
80
+ version when it is exposed, and a confidence rating.
81
+
82
+ Built for authorised security-assessment recon used by The Lupaxa Project.
83
+
84
+ > [!WARNING]
85
+ > **Authorised use only.** Use this tool for authorised security-assessment
86
+ > recon only. You must have permission to test the target.
87
+
88
+ ## Features
89
+
90
+ - Detect a CMS from public signals: generator tags, headers, cookies, and scripts
91
+ - Report **name**, **evidence**, **version** when it is exposed, and **confidence**
92
+ - Quiet default: homepage plus confirm files for CMS that already scored
93
+ - Optional **`--active`** broader path probes, including a catalogue confirm sweep when the homepage is quiet (a path is never a hit from HTTP 200 alone)
94
+ - Library API (`detect` / `detect_many`) and CLI (`cms-detection`)
95
+ - Text, JSON, and CSV output
96
+ - Fully typed, linted, formatted, and tested
97
+
98
+ ## Installation
99
+
100
+ ### From PyPI
101
+
102
+ ```bash
103
+ pip install lupaxa-cms-detection
104
+ ```
105
+
106
+ ### From source (development mode)
107
+
108
+ ```bash
109
+ pip install -e ".[dev]"
110
+ ```
111
+
112
+ Requires Python 3.10+. Runtime dependencies: `requests`, `beautifulsoup4`.
113
+
114
+ ## Library quick start
115
+
116
+ ```python
117
+ from lupaxa.cms_detection import detect
118
+
119
+ result = detect("https://example.com", active=False)
120
+ print(result.cms, result.version, result.confidence)
121
+ ```
122
+
123
+ ## CLI quick start
124
+
125
+ ```bash
126
+ cms-detection --help
127
+ cms-detection https://example.com
128
+ cms-detection https://example.com --active
129
+ cms-detection urls.txt --format json --output results.json
130
+ ```
131
+
132
+ You can also run the CLI as a module:
133
+
134
+ ```bash
135
+ python -m lupaxa.cms_detection --help
136
+ python -m lupaxa.cms_detection --version
137
+ ```
138
+
139
+ ## Documentation
140
+
141
+ Online documentation:
142
+
143
+ [Documentation](https://cms-detection.thelupaxaproject.org/)
144
+
145
+ Source repository:
146
+
147
+ [GitHub](https://github.com/lupaxa-security-toolbox/cms-detection)
148
+
149
+ ### Serve docs locally
150
+
151
+ From a clone of the repository:
152
+
153
+ ```bash
154
+ make mkdocs-serve
155
+ ```
156
+
157
+ Then open the local URL printed by MkDocs in your browser.
158
+
159
+ ## Development
160
+
161
+ Clone the repository and install with Make:
162
+
163
+ ```bash
164
+ make init # first-time makefile-skills checkout
165
+ make python-install-dev # editable install with [dev]
166
+ make python-check # lint, type-check, and test
167
+ ```
168
+
169
+ <a href="https://github.com/the-lupaxa-project">
170
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
171
+ </a>
@@ -0,0 +1,108 @@
1
+ <!-- markdownlint-disable -->
2
+ <p align="center">
3
+ <a href="https://github.com/lupaxa-security-toolbox">
4
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/security-toolbox/readme-logo.png" alt="Project Logo" width="256"/><br/>
5
+ </a>
6
+ </p>
7
+ <h3 align="center">
8
+ The Lupaxa Security Toolbox<br />
9
+ Part of The Lupaxa Project
10
+ </h3>
11
+
12
+ <br />
13
+
14
+ # lupaxa-cms-detection
15
+
16
+ Identify which CMS a website is using from public signals — name, evidence,
17
+ version when it is exposed, and a confidence rating.
18
+
19
+ Built for authorised security-assessment recon used by The Lupaxa Project.
20
+
21
+ > [!WARNING]
22
+ > **Authorised use only.** Use this tool for authorised security-assessment
23
+ > recon only. You must have permission to test the target.
24
+
25
+ ## Features
26
+
27
+ - Detect a CMS from public signals: generator tags, headers, cookies, and scripts
28
+ - Report **name**, **evidence**, **version** when it is exposed, and **confidence**
29
+ - Quiet default: homepage plus confirm files for CMS that already scored
30
+ - Optional **`--active`** broader path probes, including a catalogue confirm sweep when the homepage is quiet (a path is never a hit from HTTP 200 alone)
31
+ - Library API (`detect` / `detect_many`) and CLI (`cms-detection`)
32
+ - Text, JSON, and CSV output
33
+ - Fully typed, linted, formatted, and tested
34
+
35
+ ## Installation
36
+
37
+ ### From PyPI
38
+
39
+ ```bash
40
+ pip install lupaxa-cms-detection
41
+ ```
42
+
43
+ ### From source (development mode)
44
+
45
+ ```bash
46
+ pip install -e ".[dev]"
47
+ ```
48
+
49
+ Requires Python 3.10+. Runtime dependencies: `requests`, `beautifulsoup4`.
50
+
51
+ ## Library quick start
52
+
53
+ ```python
54
+ from lupaxa.cms_detection import detect
55
+
56
+ result = detect("https://example.com", active=False)
57
+ print(result.cms, result.version, result.confidence)
58
+ ```
59
+
60
+ ## CLI quick start
61
+
62
+ ```bash
63
+ cms-detection --help
64
+ cms-detection https://example.com
65
+ cms-detection https://example.com --active
66
+ cms-detection urls.txt --format json --output results.json
67
+ ```
68
+
69
+ You can also run the CLI as a module:
70
+
71
+ ```bash
72
+ python -m lupaxa.cms_detection --help
73
+ python -m lupaxa.cms_detection --version
74
+ ```
75
+
76
+ ## Documentation
77
+
78
+ Online documentation:
79
+
80
+ [Documentation](https://cms-detection.thelupaxaproject.org/)
81
+
82
+ Source repository:
83
+
84
+ [GitHub](https://github.com/lupaxa-security-toolbox/cms-detection)
85
+
86
+ ### Serve docs locally
87
+
88
+ From a clone of the repository:
89
+
90
+ ```bash
91
+ make mkdocs-serve
92
+ ```
93
+
94
+ Then open the local URL printed by MkDocs in your browser.
95
+
96
+ ## Development
97
+
98
+ Clone the repository and install with Make:
99
+
100
+ ```bash
101
+ make init # first-time makefile-skills checkout
102
+ make python-install-dev # editable install with [dev]
103
+ make python-check # lint, type-check, and test
104
+ ```
105
+
106
+ <a href="https://github.com/the-lupaxa-project">
107
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
108
+ </a>
@@ -0,0 +1,63 @@
1
+ <p align="center">
2
+ <a href="https://github.com/lupaxa-security-toolbox">
3
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/security-toolbox/readme-logo.png" alt="Security Toolbox" />
4
+ </a>
5
+ </p>
6
+
7
+ <h1 align="center">cms-detection</h1>
8
+
9
+ Identify which CMS a website is using from public signals — name, evidence,
10
+ version when it is exposed, and a confidence rating.
11
+
12
+ > [!WARNING]
13
+ > **Authorised use only.** Use this tool for authorised security-assessment
14
+ > recon only. You must have permission to test the target.
15
+
16
+ <p align="center">
17
+ <a href="https://cms-detection.thelupaxaproject.org/">Documentation</a>
18
+ ·
19
+ <a href="https://github.com/lupaxa-security-toolbox/cms-detection">GitHub</a>
20
+ </p>
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install lupaxa-cms-detection
26
+ cms-detection --help
27
+ ```
28
+
29
+ ## CLI
30
+
31
+ ```bash
32
+ cms-detection https://example.com
33
+ cms-detection https://example.com --active
34
+ cms-detection urls.txt --format json --output results.json
35
+ python -m lupaxa.cms_detection --version
36
+ ```
37
+
38
+ Default mode uses the homepage plus a few well-known public files for CMS
39
+ that already scored on that page. `--active` adds broader path probes, and
40
+ when the homepage has no signal it also walks the catalogue's confirm files.
41
+ A path is never a hit from HTTP 200 alone.
42
+
43
+ ## Library
44
+
45
+ ```python
46
+ from lupaxa.cms_detection import detect
47
+
48
+ result = detect("https://example.com", active=False)
49
+ print(result.cms, result.version, result.confidence)
50
+ ```
51
+
52
+ ## Development
53
+
54
+ ```bash
55
+ make init
56
+ make python-install-dev
57
+ make python-check
58
+ make mkdocs-serve
59
+ ```
60
+
61
+ <a href="https://github.com/the-lupaxa-project">
62
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
63
+ </a>
@@ -0,0 +1,23 @@
1
+ <p align="center">
2
+ <a href="https://github.com/lupaxa-security-toolbox">
3
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/organisations/security-toolbox/readme-logo.png" alt="Organisation Logo" />
4
+ </a>
5
+ </p>
6
+
7
+ <h1 align="center">Core Documentation</h1>
8
+
9
+ The following documents define the standards that apply across **The Lupaxa Project**:
10
+
11
+ | Document | Purpose |
12
+ | :------------------------------------ | :---------------------------------------------------------------- |
13
+ | [Code of Conduct](CODE_OF_CONDUCT.md) | Expected behaviour and reporting procedures. |
14
+ | [How to Contribute](CONTRIBUTING.md) | Development workflow, coding standards, and contribution process. |
15
+ | [Governance](GOVERNANCE.md) | Project governance, roles, and decision-making. |
16
+ | [Maintainers](MAINTAINERS.md) | Maintainer responsibilities and expectations. |
17
+ | [Projects Overview](PROJECTS.md) | Overview of organisations, repositories, and project structure. |
18
+ | [Security Policy](SECURITY.md) | Responsible disclosure and secure development practices. |
19
+ | [Support Guide](SUPPORT.md) | How to obtain help and support. |
20
+
21
+ <a href="https://github.com/the-lupaxa-project">
22
+ <img src="https://raw.githubusercontent.com/the-lupaxa-project/brand-assets/master/logos/components/footer-for-child-orgs.svg" alt="The Lupaxa Project Footer" width="100%" />
23
+ </a>
@@ -0,0 +1,105 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.18"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lupaxa-cms-detection"
7
+ dynamic = ["version"]
8
+ description = "Detect which CMS a website is using from public signals."
9
+ readme = "README-PYPI.md"
10
+ license = { file = "LICENCE" }
11
+ requires-python = ">=3.10"
12
+
13
+ authors = [
14
+ { name = "The Lupaxa Project" }
15
+ ]
16
+
17
+ keywords = ["cms", "fingerprint", "recon", "security", "cli"]
18
+
19
+ dependencies = [
20
+ "requests>=2.32",
21
+ "beautifulsoup4>=4.12",
22
+ ]
23
+
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Environment :: Console",
27
+ "Intended Audience :: Developers",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Programming Language :: Python :: 3.14",
36
+ "Topic :: Security",
37
+ "Topic :: Software Development :: Libraries :: Python Modules",
38
+ "Topic :: Utilities",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://cms-detection.thelupaxaproject.org/"
43
+ Documentation = "https://cms-detection.thelupaxaproject.org/"
44
+ Repository = "https://github.com/lupaxa-security-toolbox/cms-detection"
45
+ Issues = "https://github.com/lupaxa-security-toolbox/cms-detection/issues"
46
+
47
+ [project.scripts]
48
+ cms-detection = "lupaxa.cms_detection.cli:main"
49
+
50
+ [project.optional-dependencies]
51
+ dev = [
52
+ "pytest>=8.0",
53
+ "pytest-cov>=5.0",
54
+ "ruff>=0.6.0",
55
+ "mypy>=1.12.0",
56
+ "hatch>=1.12.0",
57
+ "bump-my-version>=1.2.0",
58
+ "mkdocs==1.6.1",
59
+ "mkdocs-material==9.7.7",
60
+ ]
61
+ test = [
62
+ "pytest>=8.0",
63
+ "pytest-cov>=5.0",
64
+ ]
65
+
66
+ [tool.hatch.version]
67
+ path = "src/lupaxa/cms_detection/version.py"
68
+
69
+ [tool.hatch.build.targets.sdist]
70
+ include = [
71
+ "src/**",
72
+ "README.md",
73
+ "README-PYPI.md",
74
+ "LICENCE",
75
+ ]
76
+
77
+ [tool.hatch.build.targets.wheel]
78
+ packages = ["src/lupaxa"]
79
+
80
+ [tool.ruff]
81
+ line-length = 100
82
+ target-version = "py310"
83
+
84
+ [tool.ruff.lint]
85
+ select = ["E", "F", "I", "B", "UP", "SIM"]
86
+
87
+ [tool.ruff.lint.per-file-ignores]
88
+ "tests/**/*.py" = ["S101"]
89
+
90
+ [tool.ruff.format]
91
+ quote-style = "double"
92
+
93
+ [tool.mypy]
94
+ python_version = "3.10"
95
+ files = ["src", "tests"]
96
+ mypy_path = "src"
97
+ explicit_package_bases = true
98
+ namespace_packages = true
99
+ warn_unused_configs = true
100
+ ignore_missing_imports = true
101
+
102
+ [tool.pytest.ini_options]
103
+ addopts = "-ra -q --disable-warnings"
104
+ testpaths = ["tests"]
105
+ pythonpath = ["src"]
@@ -0,0 +1,18 @@
1
+ """lupaxa.cms_detection — identify a website CMS from public signals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .api import detect, detect_many
6
+ from .exceptions import CmsDetectionError
7
+ from .models import DetectResult, Evidence
8
+ from .version import __version__, get_version
9
+
10
+ __all__ = [
11
+ "CmsDetectionError",
12
+ "DetectResult",
13
+ "Evidence",
14
+ "__version__",
15
+ "detect",
16
+ "detect_many",
17
+ "get_version",
18
+ ]
@@ -0,0 +1,8 @@
1
+ """Allow ``python -m lupaxa.cms_detection`` to run the CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())
@@ -0,0 +1,89 @@
1
+ """Public CMS detection API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from concurrent.futures import ThreadPoolExecutor, as_completed
6
+
7
+ from .engine import run_detection
8
+ from .exceptions import CmsDetectionError
9
+ from .fetch import fetch_url
10
+ from .models import DetectResult, FetchResult
11
+
12
+
13
+ def candidate_urls(raw: str) -> list[str]:
14
+ """Expand a user-supplied host or URL into fetch candidates."""
15
+ value = raw.strip()
16
+ if not value:
17
+ raise CmsDetectionError("empty URL")
18
+ if value.startswith(("http://", "https://")):
19
+ return [value]
20
+ return [f"https://{value}", f"http://{value}"]
21
+
22
+
23
+ def _homepage(url: str, *, delay: float, retries: int, timeout: float) -> FetchResult:
24
+ last: FetchResult | None = None
25
+ for candidate in candidate_urls(url):
26
+ last = fetch_url(candidate, delay=delay, retries=retries, timeout=timeout)
27
+ if last.ok:
28
+ return last
29
+ assert last is not None
30
+ return last
31
+
32
+
33
+ def detect(
34
+ url: str,
35
+ *,
36
+ active: bool = False,
37
+ delay: float = 0,
38
+ retries: int = 2,
39
+ timeout: float = 10,
40
+ ) -> DetectResult:
41
+ """Detect the CMS used by ``url``."""
42
+ homepage = _homepage(url, delay=delay, retries=retries, timeout=timeout)
43
+ if not homepage.ok:
44
+ return DetectResult(
45
+ url=homepage.url,
46
+ cms=None,
47
+ version=None,
48
+ confidence=None,
49
+ evidence=(),
50
+ candidates=(),
51
+ error=homepage.error,
52
+ )
53
+ return run_detection(
54
+ homepage,
55
+ active=active,
56
+ fetch_path=fetch_url,
57
+ delay=delay,
58
+ retries=retries,
59
+ )
60
+
61
+
62
+ def detect_many(
63
+ urls: list[str],
64
+ *,
65
+ workers: int = 10,
66
+ active: bool = False,
67
+ delay: float = 0,
68
+ retries: int = 2,
69
+ timeout: float = 10,
70
+ ) -> list[DetectResult]:
71
+ """Detect many URLs; returned list matches input order."""
72
+ if not urls:
73
+ return []
74
+ results: list[DetectResult | None] = [None] * len(urls)
75
+ with ThreadPoolExecutor(max_workers=max(1, workers)) as pool:
76
+ futures = {
77
+ pool.submit(
78
+ detect,
79
+ item,
80
+ active=active,
81
+ delay=delay,
82
+ retries=retries,
83
+ timeout=timeout,
84
+ ): index
85
+ for index, item in enumerate(urls)
86
+ }
87
+ for future in as_completed(futures):
88
+ results[futures[future]] = future.result()
89
+ return [item for item in results if item is not None]