xssmap 2.8.0__tar.gz → 2.9.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.
- xssmap-2.9.0/CHANGELOG.md +91 -0
- xssmap-2.9.0/LICENSE +21 -0
- xssmap-2.9.0/MANIFEST.in +4 -0
- {xssmap-2.8.0/xssmap.egg-info → xssmap-2.9.0}/PKG-INFO +37 -2
- xssmap-2.8.0/PKG-INFO → xssmap-2.9.0/README.md +28 -17
- {xssmap-2.8.0 → xssmap-2.9.0}/pyproject.toml +17 -4
- xssmap-2.9.0/tests/test_crawlperf.py +230 -0
- xssmap-2.9.0/tests/test_tech.py +150 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/__init__.py +1 -1
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/cli.py +17 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/crawler.py +86 -39
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/net.py +85 -11
- xssmap-2.9.0/xssmap/py.typed +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/report.py +40 -4
- xssmap-2.9.0/xssmap/tech.py +261 -0
- xssmap-2.8.0/README.md → xssmap-2.9.0/xssmap.egg-info/PKG-INFO +52 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap.egg-info/SOURCES.txt +7 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/setup.cfg +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_bench.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_brute.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_budget.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_charset.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_csp.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_domscan.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_domstate.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_efficiency.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_exploit.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_fuzz.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_honesty.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_inline.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_jslib.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_leads.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_mxss.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_ops.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_params.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_recon_report.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_session.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_subdomains.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_units.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/tests/test_waf.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/__main__.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/browser.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/brute.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/callback.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/charset.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/contexts.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/csp.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/domscan.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/domstate.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/efficiency.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/exploit.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/filters.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/fuzz.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/htmlparse.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/jslib.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/mxss.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/params.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/payloads.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/repro.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/sarif.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/scanner.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/session.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/state.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/stored.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/subdomains.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/verify.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap/waf.py +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap.egg-info/dependency_links.txt +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap.egg-info/entry_points.txt +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap.egg-info/requires.txt +0 -0
- {xssmap-2.8.0 → xssmap-2.9.0}/xssmap.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
Releases before 2.8.0 predate this file; their history is in the git log.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [2.9.0] - 2026-11-13
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Server-stack fingerprinting. `Server`, `X-Powered-By`, `X-AspNet-Version`,
|
|
16
|
+
`X-Generator`, `<meta name="generator">` and session cookie names are read
|
|
17
|
+
from the seed response the crawl already fetched, so the pass sends no extra
|
|
18
|
+
request. Rows are printed in the console and rendered in the Markdown and
|
|
19
|
+
HTML reports under "Server stack".
|
|
20
|
+
- Outdated-version detection for components that advertise a version, using a
|
|
21
|
+
deliberately short policy table: Apache 2.4.49/2.4.50 (CVE-2021-41773,
|
|
22
|
+
CVE-2021-42013), nginx before 1.20.1 (CVE-2021-23017), PHP branches before
|
|
23
|
+
8.2 (out of security support) and IIS 6.0-8.5 (shipped only with an
|
|
24
|
+
end-of-life Windows Server). A flagged version is a lead for the operator,
|
|
25
|
+
never a finding on its own.
|
|
26
|
+
- 35 tests pinning the new crawl scheduling, the transport cap, the throttle
|
|
27
|
+
rules and the stack policy table.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Crawl scheduling is continuous instead of level-by-level. `--depth` counts
|
|
32
|
+
hops from the seed and `--max-pages` is a hard stop, so one slow branch no
|
|
33
|
+
longer holds back pages that are already queued behind it.
|
|
34
|
+
- `robots.txt` and `sitemap.xml` are fetched concurrently, turning two round
|
|
35
|
+
trips into one.
|
|
36
|
+
- `Throttle` no longer reads an HTTP 500 as backpressure. An application error
|
|
37
|
+
is not load, so only 429/503 count, and only after a short streak; a target
|
|
38
|
+
that answers a fraction of URLs with 500 used to stack seconds of sleep into
|
|
39
|
+
the crawl and dominate its duration.
|
|
40
|
+
- Binary bodies are read up to a 256 KB cap and marked truncated instead of
|
|
41
|
+
being buffered whole. A truncated body drops its connection rather than
|
|
42
|
+
returning a half-drained socket to the pool.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Crawling a target that 500s part of its URL space is ~24x faster at the same
|
|
47
|
+
page count: 18.90 s to 0.79 s for 68 requests in the bundled benchmark
|
|
48
|
+
(identical page sets, deterministic error injection).
|
|
49
|
+
|
|
50
|
+
## [2.8.1] - 2026-11-13
|
|
51
|
+
|
|
52
|
+
Packaging release. No scanner behaviour changed.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- `LICENSE` (MIT).
|
|
57
|
+
- `CHANGELOG.md`.
|
|
58
|
+
- `xssmap/py.typed` so downstream type checkers use the project's inline
|
|
59
|
+
annotations (PEP 561), shipped inside the wheel as package data.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- `pyproject.toml` now declares the license with PEP 639 metadata
|
|
64
|
+
(`license = "MIT"`, `license-files`) instead of carrying a comment saying no
|
|
65
|
+
license was stated, so the wheel and sdist advertise the terms and PyPI
|
|
66
|
+
renders them.
|
|
67
|
+
- Build backend floor raised to `setuptools>=77`, which is the first release
|
|
68
|
+
that understands `license-files` and an SPDX license expression.
|
|
69
|
+
- Author metadata now names the maintainer instead of repeating the project
|
|
70
|
+
name.
|
|
71
|
+
|
|
72
|
+
## [2.8.0] - 2026-11-13
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- DOM state-store and hydration flows. A value written to
|
|
77
|
+
`localStorage`/`sessionStorage` on one page and rendered as HTML on another,
|
|
78
|
+
and a server-rendered state blob (`__INITIAL_STATE__`, `__NEXT_DATA__`,
|
|
79
|
+
`data-*`) that a bundle re-reads after hydration, are now replayed step by
|
|
80
|
+
step in the browser instead of being left as unprovable static guesses. The
|
|
81
|
+
new `xssmap/domstate.py` engine drives both, and the bundled lab serves
|
|
82
|
+
`/dom-state-read` and `/dom-hydrate` endpoints to exercise them.
|
|
83
|
+
- Splash banner in the sqlmap idiom, drawn on every interactive run and
|
|
84
|
+
suppressed with `--no-color`.
|
|
85
|
+
- 21 tests covering the DOM state-store and hydration engine.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- The remaining DOM-analysis gap: storage-mediated and hydration-mediated
|
|
90
|
+
source-to-sink flows that the static tracer could see but not confirm are now
|
|
91
|
+
confirmed in a real browser.
|
xssmap-2.9.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ian Mattas
|
|
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.
|
xssmap-2.9.0/MANIFEST.in
ADDED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xssmap
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0
|
|
4
4
|
Summary: Context-aware XSS scanner: crawler, browser verification, mXSS, charset and stored-XSS stages
|
|
5
|
-
Author:
|
|
5
|
+
Author: Ian Mattas
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/imattas/xssmap
|
|
8
|
+
Project-URL: Repository, https://github.com/imattas/xssmap
|
|
9
|
+
Project-URL: Changelog, https://github.com/imattas/xssmap/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/imattas/xssmap/issues
|
|
6
11
|
Keywords: security,xss,scanner,pentest,sarif
|
|
7
12
|
Classifier: Environment :: Console
|
|
8
13
|
Classifier: Intended Audience :: Information Technology
|
|
@@ -11,9 +16,11 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
11
16
|
Classifier: Topic :: Security
|
|
12
17
|
Requires-Python: >=3.9
|
|
13
18
|
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
14
20
|
Provides-Extra: dev
|
|
15
21
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
16
22
|
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
17
24
|
|
|
18
25
|
# xssmap
|
|
19
26
|
|
|
@@ -51,6 +58,12 @@ actually execute in a real browser.
|
|
|
51
58
|
friends from body or filename, compares versions against each advisory's affected range
|
|
52
59
|
(so 2.3.2 is not blamed for a 3.4.1 fix) and can merge fresh advisories from osv.dev
|
|
53
60
|
with `--osv`.
|
|
61
|
+
- **Server stack and outdated versions.** Reads what each host says about itself - `Server`,
|
|
62
|
+
`X-Powered-By`, `X-AspNet-Version`, `X-Generator`, `<meta name="generator">` and session
|
|
63
|
+
cookie names - from the seed response the crawl already received, so the pass costs no
|
|
64
|
+
extra request. Versions whose exact build is known-vulnerable or out of support (Apache
|
|
65
|
+
2.4.49/2.4.50, nginx before 1.20.1, PHP before 8.2, IIS 6.0-8.5) are flagged with the
|
|
66
|
+
CVE, and every row names the header it came from.
|
|
54
67
|
- **Leads, not silence.** A parameter that reflects but swallows every payload is recorded
|
|
55
68
|
as an unconfirmed lead with the measured filter and how much of the probe survived
|
|
56
69
|
(verbatim, recased, escaped, truncated), in its own report section - never presented as a
|
|
@@ -476,6 +489,28 @@ OSV enrichment is verified against the live service, not only against fixtures.
|
|
|
476
489
|
and Lodash 4.17.4 (9) into the local verdicts, then stopped: the fourth library was never
|
|
477
490
|
queried and stayed unchanged, so the cap is visibly enforced.
|
|
478
491
|
|
|
492
|
+
### Server stack and outdated versions
|
|
493
|
+
|
|
494
|
+
The headers a host sends are also an inventory: `Server: nginx/1.18.0` is a version, not
|
|
495
|
+
just a name. The crawler keeps the headers and the `<meta name="generator">` value of the
|
|
496
|
+
first HTML response it already fetched, and the stack pass reads them without a second
|
|
497
|
+
request. Three things make the result usable rather than noisy:
|
|
498
|
+
|
|
499
|
+
- A row always cites its evidence (`Server`, `X-Powered-By`, `X-AspNet-Version`,
|
|
500
|
+
`X-Generator`, `meta generator`, or `cookie <name>`), so a claim can be checked against
|
|
501
|
+
the response that produced it.
|
|
502
|
+
- Versions are compared numerically, so nginx 1.20.10 is newer than 1.20.9 and is not
|
|
503
|
+
blamed for a fix that shipped in 1.20.1, and the IIS rules match `8.5` without also
|
|
504
|
+
matching `10.0`.
|
|
505
|
+
- The policy table only holds issues that are public, fixed and cheap to verify: Apache
|
|
506
|
+
2.4.49/2.4.50 (CVE-2021-41773, CVE-2021-42013), nginx before 1.20.1 (CVE-2021-23017),
|
|
507
|
+
PHP branches before 8.2 (out of security support) and IIS 6.0-8.5 (shipped only with an
|
|
508
|
+
end-of-life Windows Server). A CMS such as WordPress is reported with its disclosed
|
|
509
|
+
version and no end-of-life claim, because that judgement is not the scanner's to make.
|
|
510
|
+
|
|
511
|
+
A flagged version is a lead: it says the build has a published problem, not that this
|
|
512
|
+
host is exploitable through it. The finding standard for XSS is unchanged.
|
|
513
|
+
|
|
479
514
|
### Browser cost
|
|
480
515
|
|
|
481
516
|
`--browser` reuses one browser and one page per run. The report's `browser_verification`
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: xssmap
|
|
3
|
-
Version: 2.8.0
|
|
4
|
-
Summary: Context-aware XSS scanner: crawler, browser verification, mXSS, charset and stored-XSS stages
|
|
5
|
-
Author: xssmap
|
|
6
|
-
Keywords: security,xss,scanner,pentest,sarif
|
|
7
|
-
Classifier: Environment :: Console
|
|
8
|
-
Classifier: Intended Audience :: Information Technology
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
-
Classifier: Topic :: Security
|
|
12
|
-
Requires-Python: >=3.9
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
Provides-Extra: dev
|
|
15
|
-
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
16
|
-
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
17
|
-
|
|
18
1
|
# xssmap
|
|
19
2
|
|
|
20
3
|
A context-aware cross-site scripting scanner with a crawler, built in the spirit of
|
|
@@ -51,6 +34,12 @@ actually execute in a real browser.
|
|
|
51
34
|
friends from body or filename, compares versions against each advisory's affected range
|
|
52
35
|
(so 2.3.2 is not blamed for a 3.4.1 fix) and can merge fresh advisories from osv.dev
|
|
53
36
|
with `--osv`.
|
|
37
|
+
- **Server stack and outdated versions.** Reads what each host says about itself - `Server`,
|
|
38
|
+
`X-Powered-By`, `X-AspNet-Version`, `X-Generator`, `<meta name="generator">` and session
|
|
39
|
+
cookie names - from the seed response the crawl already received, so the pass costs no
|
|
40
|
+
extra request. Versions whose exact build is known-vulnerable or out of support (Apache
|
|
41
|
+
2.4.49/2.4.50, nginx before 1.20.1, PHP before 8.2, IIS 6.0-8.5) are flagged with the
|
|
42
|
+
CVE, and every row names the header it came from.
|
|
54
43
|
- **Leads, not silence.** A parameter that reflects but swallows every payload is recorded
|
|
55
44
|
as an unconfirmed lead with the measured filter and how much of the probe survived
|
|
56
45
|
(verbatim, recased, escaped, truncated), in its own report section - never presented as a
|
|
@@ -476,6 +465,28 @@ OSV enrichment is verified against the live service, not only against fixtures.
|
|
|
476
465
|
and Lodash 4.17.4 (9) into the local verdicts, then stopped: the fourth library was never
|
|
477
466
|
queried and stayed unchanged, so the cap is visibly enforced.
|
|
478
467
|
|
|
468
|
+
### Server stack and outdated versions
|
|
469
|
+
|
|
470
|
+
The headers a host sends are also an inventory: `Server: nginx/1.18.0` is a version, not
|
|
471
|
+
just a name. The crawler keeps the headers and the `<meta name="generator">` value of the
|
|
472
|
+
first HTML response it already fetched, and the stack pass reads them without a second
|
|
473
|
+
request. Three things make the result usable rather than noisy:
|
|
474
|
+
|
|
475
|
+
- A row always cites its evidence (`Server`, `X-Powered-By`, `X-AspNet-Version`,
|
|
476
|
+
`X-Generator`, `meta generator`, or `cookie <name>`), so a claim can be checked against
|
|
477
|
+
the response that produced it.
|
|
478
|
+
- Versions are compared numerically, so nginx 1.20.10 is newer than 1.20.9 and is not
|
|
479
|
+
blamed for a fix that shipped in 1.20.1, and the IIS rules match `8.5` without also
|
|
480
|
+
matching `10.0`.
|
|
481
|
+
- The policy table only holds issues that are public, fixed and cheap to verify: Apache
|
|
482
|
+
2.4.49/2.4.50 (CVE-2021-41773, CVE-2021-42013), nginx before 1.20.1 (CVE-2021-23017),
|
|
483
|
+
PHP branches before 8.2 (out of security support) and IIS 6.0-8.5 (shipped only with an
|
|
484
|
+
end-of-life Windows Server). A CMS such as WordPress is reported with its disclosed
|
|
485
|
+
version and no end-of-life claim, because that judgement is not the scanner's to make.
|
|
486
|
+
|
|
487
|
+
A flagged version is a lead: it says the build has a published problem, not that this
|
|
488
|
+
host is exploitable through it. The finding standard for XSS is unchanged.
|
|
489
|
+
|
|
479
490
|
### Browser cost
|
|
480
491
|
|
|
481
492
|
`--browser` reuses one browser and one page per run. The report's `browser_verification`
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
|
|
2
|
+
# 77 is the first setuptools that understands PEP 639 license expressions and
|
|
3
|
+
# `license-files`, which is how the metadata below states the MIT terms.
|
|
4
|
+
requires = ["setuptools>=77"]
|
|
3
5
|
build-backend = "setuptools.build_meta"
|
|
4
6
|
|
|
5
7
|
[project]
|
|
@@ -8,9 +10,9 @@ dynamic = ["version"]
|
|
|
8
10
|
description = "Context-aware XSS scanner: crawler, browser verification, mXSS, charset and stored-XSS stages"
|
|
9
11
|
readme = "README.md"
|
|
10
12
|
requires-python = ">=3.9"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
authors = [{ name = "
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
authors = [{ name = "Ian Mattas" }]
|
|
14
16
|
keywords = ["security", "xss", "scanner", "pentest", "sarif"]
|
|
15
17
|
classifiers = [
|
|
16
18
|
"Environment :: Console",
|
|
@@ -23,6 +25,12 @@ classifiers = [
|
|
|
23
25
|
# an assessment host without a package install.
|
|
24
26
|
dependencies = []
|
|
25
27
|
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/imattas/xssmap"
|
|
30
|
+
Repository = "https://github.com/imattas/xssmap"
|
|
31
|
+
Changelog = "https://github.com/imattas/xssmap/blob/main/CHANGELOG.md"
|
|
32
|
+
Issues = "https://github.com/imattas/xssmap/issues"
|
|
33
|
+
|
|
26
34
|
[project.optional-dependencies]
|
|
27
35
|
dev = ["ruff>=0.6", "mypy>=1.10"]
|
|
28
36
|
|
|
@@ -32,6 +40,11 @@ xssmap = "xssmap.cli:main"
|
|
|
32
40
|
[tool.setuptools]
|
|
33
41
|
packages = ["xssmap"]
|
|
34
42
|
|
|
43
|
+
# PEP 561 marker: tells type checkers the inline annotations are usable and must
|
|
44
|
+
# travel with the installed package, not just the source tree.
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
xssmap = ["py.typed"]
|
|
47
|
+
|
|
35
48
|
[tool.setuptools.dynamic]
|
|
36
49
|
version = { attr = "xssmap.__version__" }
|
|
37
50
|
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Crawl scheduling and transport cost.
|
|
2
|
+
|
|
3
|
+
Three behaviours are pinned here, all of them things that made a crawl slower
|
|
4
|
+
than the work it was doing:
|
|
5
|
+
|
|
6
|
+
* the crawl queue is fed continuously, so ``max_depth`` counts hops from the
|
|
7
|
+
seed and ``max_pages`` is a hard stop;
|
|
8
|
+
* a 500 from the target is not read as backpressure (only 429/503 are, and
|
|
9
|
+
only after a short streak);
|
|
10
|
+
* binary bodies are read up to a cap and their connection is dropped instead
|
|
11
|
+
of being returned to the pool half-drained.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import unittest
|
|
19
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
22
|
+
|
|
23
|
+
from xssmap.crawler import Crawler, Scope
|
|
24
|
+
from xssmap.net import HTTP, MAX_BINARY_BODY, Throttle
|
|
25
|
+
|
|
26
|
+
SEED = "http://app.test/"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class QuietLog:
|
|
30
|
+
def info(self, *_a, **_k):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def debug(self, *_a, **_k):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def warn(self, *_a, **_k):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def ok(self, *_a, **_k):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def action(self, *_a, **_k):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PageResp:
|
|
47
|
+
"""Enough of :class:`xssmap.net.Response` for the crawler to parse."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, url, status=200, text="", content_type="text/html"):
|
|
50
|
+
self.url = url
|
|
51
|
+
self.status = status
|
|
52
|
+
self.text = text
|
|
53
|
+
self.body = text.encode()
|
|
54
|
+
self.content_type = content_type
|
|
55
|
+
self.history: list = []
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_html(self):
|
|
59
|
+
return self.content_type.startswith("text/html")
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def is_javascript(self):
|
|
63
|
+
return "javascript" in self.content_type
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RouterHTTP:
|
|
67
|
+
def __init__(self, pages):
|
|
68
|
+
self.pages = pages
|
|
69
|
+
self.urls: list[str] = []
|
|
70
|
+
|
|
71
|
+
def get(self, url, headers=None, **_kwargs):
|
|
72
|
+
self.urls.append(url)
|
|
73
|
+
if url.endswith("/robots.txt") or url.endswith("/sitemap.xml"):
|
|
74
|
+
return PageResp(url, 404, "not found", "text/plain")
|
|
75
|
+
text = self.pages.get(url)
|
|
76
|
+
if text is None:
|
|
77
|
+
return PageResp(url, 404, "not found", "text/html")
|
|
78
|
+
ctype = "application/javascript" if url.endswith(".js") else "text/html"
|
|
79
|
+
return PageResp(url, 200, text, ctype)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def crawl(pages, depth, max_pages=50):
|
|
83
|
+
http = RouterHTTP(pages)
|
|
84
|
+
crawler = Crawler(http, Scope(SEED), QuietLog(), max_depth=depth,
|
|
85
|
+
max_pages=max_pages, crawl_js=True)
|
|
86
|
+
return crawler.run([SEED]), http
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DepthTests(unittest.TestCase):
|
|
90
|
+
PAGES = {
|
|
91
|
+
SEED: '<a href="/a">a</a><a href="/b">b</a>',
|
|
92
|
+
"http://app.test/a": '<a href="/a1">a1</a>',
|
|
93
|
+
"http://app.test/b": '<a href="/b1">b1</a>',
|
|
94
|
+
"http://app.test/a1": '<a href="/a2">a2</a>',
|
|
95
|
+
"http://app.test/b1": "leaf",
|
|
96
|
+
"http://app.test/a2": "leaf",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def test_depth_counts_hops_from_seed(self):
|
|
100
|
+
_result, http = crawl(self.PAGES, depth=1)
|
|
101
|
+
fetched = {u for u in http.urls if not u.endswith(("/robots.txt", "/sitemap.xml"))}
|
|
102
|
+
self.assertEqual(fetched, {SEED, "http://app.test/a", "http://app.test/b"})
|
|
103
|
+
self.assertNotIn("http://app.test/a1", fetched)
|
|
104
|
+
|
|
105
|
+
def test_seed_is_depth_zero(self):
|
|
106
|
+
result, _http = crawl(self.PAGES, depth=1)
|
|
107
|
+
self.assertEqual(result.pages[SEED].depth, 0)
|
|
108
|
+
self.assertEqual(result.pages["http://app.test/a"].depth, 1)
|
|
109
|
+
|
|
110
|
+
def test_page_budget_stops_the_crawl(self):
|
|
111
|
+
_result, http = crawl(self.PAGES, depth=5, max_pages=2)
|
|
112
|
+
fetched = [u for u in http.urls if not u.endswith(("/robots.txt", "/sitemap.xml"))]
|
|
113
|
+
self.assertEqual(len(fetched), 2)
|
|
114
|
+
|
|
115
|
+
def test_boundary_pages_are_fetched_but_not_expanded(self):
|
|
116
|
+
result, _http = crawl(self.PAGES, depth=2)
|
|
117
|
+
# The seed is hop 0, so depth 2 reaches /a1 at hop 2 and stops there.
|
|
118
|
+
self.assertEqual(result.pages["http://app.test/a1"].depth, 2)
|
|
119
|
+
self.assertNotIn("http://app.test/a2", result.pages)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ThrottleTests(unittest.TestCase):
|
|
123
|
+
def test_application_error_is_not_backpressure(self):
|
|
124
|
+
throttle = Throttle(delay=0.0)
|
|
125
|
+
for _ in range(6):
|
|
126
|
+
throttle.on_status(500)
|
|
127
|
+
self.assertEqual(throttle.delay, 0.0)
|
|
128
|
+
|
|
129
|
+
def test_single_load_signal_does_not_slow_the_scan(self):
|
|
130
|
+
throttle = Throttle(delay=0.0)
|
|
131
|
+
throttle.on_status(503)
|
|
132
|
+
self.assertEqual(throttle.delay, 0.0)
|
|
133
|
+
|
|
134
|
+
def test_repeated_load_signals_slow_then_recover(self):
|
|
135
|
+
throttle = Throttle(delay=0.0)
|
|
136
|
+
throttle.on_status(429)
|
|
137
|
+
throttle.on_status(429)
|
|
138
|
+
slowed = throttle.delay
|
|
139
|
+
self.assertGreater(slowed, 0.0)
|
|
140
|
+
for _ in range(6):
|
|
141
|
+
throttle.on_status(200)
|
|
142
|
+
self.assertLess(throttle.delay, slowed)
|
|
143
|
+
self.assertGreaterEqual(throttle.delay, throttle.base_delay)
|
|
144
|
+
|
|
145
|
+
def test_adaptive_delay_is_capped(self):
|
|
146
|
+
throttle = Throttle(delay=0.0)
|
|
147
|
+
for _ in range(20):
|
|
148
|
+
throttle.on_status(503)
|
|
149
|
+
self.assertLessEqual(throttle.delay, 3.0)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class _BodyHandler(BaseHTTPRequestHandler):
|
|
153
|
+
protocol_version = "HTTP/1.1"
|
|
154
|
+
disable_nagle_algorithm = True
|
|
155
|
+
|
|
156
|
+
def log_message(self, *_a):
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
def do_GET(self):
|
|
160
|
+
if self.path == "/big.bin":
|
|
161
|
+
body = b"\x00" * (MAX_BINARY_BODY + 500_000)
|
|
162
|
+
ctype = "application/octet-stream"
|
|
163
|
+
elif self.path == "/small.bin":
|
|
164
|
+
body = b"\x00" * 2048
|
|
165
|
+
ctype = "application/octet-stream"
|
|
166
|
+
else:
|
|
167
|
+
body = b"<html><body>hello</body></html>"
|
|
168
|
+
ctype = "text/html"
|
|
169
|
+
self.send_response(200)
|
|
170
|
+
self.send_header("Content-Type", ctype)
|
|
171
|
+
self.send_header("Content-Length", str(len(body)))
|
|
172
|
+
self.end_headers()
|
|
173
|
+
self.wfile.write(body)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class _QuietServer(ThreadingHTTPServer):
|
|
177
|
+
daemon_threads = True
|
|
178
|
+
|
|
179
|
+
def handle_error(self, request, client_address):
|
|
180
|
+
# A client that abandons a capped binary body leaves the server thread
|
|
181
|
+
# writing into a closed socket; that is the behaviour under test.
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class BodyCapTests(unittest.TestCase):
|
|
186
|
+
@classmethod
|
|
187
|
+
def setUpClass(cls):
|
|
188
|
+
cls.server = _QuietServer(("127.0.0.1", 0), _BodyHandler)
|
|
189
|
+
cls.server.daemon_threads = True
|
|
190
|
+
threading.Thread(target=cls.server.serve_forever, daemon=True).start()
|
|
191
|
+
cls.base = f"http://127.0.0.1:{cls.server.server_address[1]}"
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def tearDownClass(cls):
|
|
195
|
+
cls.server.shutdown()
|
|
196
|
+
|
|
197
|
+
def test_binary_body_is_capped_and_flagged(self):
|
|
198
|
+
http = HTTP(timeout=10, retries=0, throttle=Throttle())
|
|
199
|
+
resp = http.get(self.base + "/big.bin")
|
|
200
|
+
self.assertTrue(resp.truncated)
|
|
201
|
+
self.assertEqual(len(resp.body), MAX_BINARY_BODY)
|
|
202
|
+
|
|
203
|
+
def test_truncated_connection_is_not_pooled(self):
|
|
204
|
+
http = HTTP(timeout=10, retries=0, throttle=Throttle())
|
|
205
|
+
http.get(self.base + "/big.bin")
|
|
206
|
+
self.assertEqual(len(http._pool()), 0)
|
|
207
|
+
|
|
208
|
+
def test_full_binary_body_is_pooled(self):
|
|
209
|
+
# Under the cap nothing is truncated, so the socket stays reusable.
|
|
210
|
+
http = HTTP(timeout=10, retries=0, throttle=Throttle())
|
|
211
|
+
resp = http.get(self.base + "/small.bin")
|
|
212
|
+
self.assertFalse(resp.truncated)
|
|
213
|
+
pool = http._pool()
|
|
214
|
+
self.assertEqual(len(pool), 1)
|
|
215
|
+
for _key, conn, _used in pool:
|
|
216
|
+
conn.close()
|
|
217
|
+
|
|
218
|
+
def test_text_body_is_read_in_full_and_pooled(self):
|
|
219
|
+
http = HTTP(timeout=10, retries=0, throttle=Throttle())
|
|
220
|
+
resp = http.get(self.base + "/page.html")
|
|
221
|
+
self.assertFalse(resp.truncated)
|
|
222
|
+
self.assertIn(b"hello", resp.body)
|
|
223
|
+
pool = http._pool()
|
|
224
|
+
self.assertEqual(len(pool), 1)
|
|
225
|
+
for _key, conn, _used in pool:
|
|
226
|
+
conn.close()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
unittest.main()
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Server-stack fingerprinting: header parsing, version rules and report shape."""
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from xssmap import tech
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def names(techs):
|
|
8
|
+
return [t.name for t in techs]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def by_name(techs, name):
|
|
12
|
+
return next(t for t in techs if t.name == name)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HeaderParsingTests(unittest.TestCase):
|
|
16
|
+
def test_nginx_version_is_read_from_server(self):
|
|
17
|
+
found = tech.detect([("Server", "nginx/1.24.0")])
|
|
18
|
+
self.assertEqual(names(found), ["nginx"])
|
|
19
|
+
self.assertEqual(found[0].version, "1.24.0")
|
|
20
|
+
self.assertEqual(found[0].source, "Server")
|
|
21
|
+
|
|
22
|
+
def test_header_lookup_is_case_insensitive(self):
|
|
23
|
+
found = tech.detect([("server", "Apache/2.4.51 (Debian)")])
|
|
24
|
+
self.assertEqual(found[0].name, "Apache httpd")
|
|
25
|
+
self.assertEqual(found[0].version, "2.4.51")
|
|
26
|
+
|
|
27
|
+
def test_products_without_a_version_are_still_named(self):
|
|
28
|
+
found = tech.detect([("Server", "cloudflare")])
|
|
29
|
+
self.assertEqual(found[0].name, "Cloudflare")
|
|
30
|
+
self.assertEqual(found[0].version, "")
|
|
31
|
+
|
|
32
|
+
def test_several_products_in_one_header(self):
|
|
33
|
+
found = tech.detect([("Server", "nginx/1.18.0 (Ubuntu) OpenSSL/1.1.1f")])
|
|
34
|
+
self.assertEqual(names(found), ["nginx"])
|
|
35
|
+
|
|
36
|
+
def test_powered_by_and_aspnet_version(self):
|
|
37
|
+
found = tech.detect([("X-Powered-By", "PHP/8.1.2"),
|
|
38
|
+
("X-AspNet-Version", "4.0.30319")])
|
|
39
|
+
self.assertEqual(set(names(found)), {"PHP", "ASP.NET"})
|
|
40
|
+
self.assertEqual(by_name(found, "ASP.NET").version, "4.0.30319")
|
|
41
|
+
|
|
42
|
+
def test_meta_generator_identifies_the_cms(self):
|
|
43
|
+
found = tech.detect([], generator="WordPress 6.2")
|
|
44
|
+
self.assertEqual(found[0].name, "WordPress")
|
|
45
|
+
self.assertEqual(found[0].version, "6.2")
|
|
46
|
+
self.assertEqual(found[0].source, "meta generator")
|
|
47
|
+
|
|
48
|
+
def test_cookie_names_identify_the_runtime(self):
|
|
49
|
+
found = tech.detect([("Set-Cookie", "PHPSESSID=abc123; path=/; HttpOnly")])
|
|
50
|
+
self.assertEqual(found[0].name, "PHP")
|
|
51
|
+
self.assertEqual(found[0].version, "")
|
|
52
|
+
self.assertTrue(found[0].source.startswith("cookie "))
|
|
53
|
+
|
|
54
|
+
def test_cookie_does_not_repeat_a_versioned_product(self):
|
|
55
|
+
found = tech.detect([("X-Powered-By", "PHP/7.4.33"),
|
|
56
|
+
("Set-Cookie", "PHPSESSID=abc; path=/")])
|
|
57
|
+
self.assertEqual(names(found), ["PHP"])
|
|
58
|
+
self.assertEqual(found[0].version, "7.4.33")
|
|
59
|
+
|
|
60
|
+
def test_duplicate_evidence_is_collapsed(self):
|
|
61
|
+
found = tech.detect([("Server", "nginx/1.24.0"), ("Server", "nginx/1.24.0")])
|
|
62
|
+
self.assertEqual(len(found), 1)
|
|
63
|
+
|
|
64
|
+
def test_empty_headers_yield_nothing(self):
|
|
65
|
+
self.assertEqual(tech.detect([]), [])
|
|
66
|
+
self.assertEqual(tech.detect([], generator=""), [])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class VersionRuleTests(unittest.TestCase):
|
|
70
|
+
def test_apache_2449_is_flagged_critical(self):
|
|
71
|
+
row = tech.detect([("Server", "Apache/2.4.49")])[0]
|
|
72
|
+
self.assertEqual(row.status, "vulnerable")
|
|
73
|
+
self.assertEqual(row.severity, "critical")
|
|
74
|
+
self.assertIn("CVE-2021-41773", row.cves)
|
|
75
|
+
|
|
76
|
+
def test_apache_2450_is_flagged_with_its_own_cve(self):
|
|
77
|
+
row = tech.detect([("Server", "Apache/2.4.50")])[0]
|
|
78
|
+
self.assertIn("CVE-2021-42013", row.cves)
|
|
79
|
+
|
|
80
|
+
def test_patched_apache_is_not_flagged(self):
|
|
81
|
+
row = tech.detect([("Server", "Apache/2.4.51")])[0]
|
|
82
|
+
self.assertEqual(row.status, "detected")
|
|
83
|
+
self.assertEqual(row.cves, [])
|
|
84
|
+
|
|
85
|
+
def test_old_nginx_matches_the_resolver_cve(self):
|
|
86
|
+
row = tech.detect([("Server", "nginx/1.18.0")])[0]
|
|
87
|
+
self.assertEqual(row.status, "vulnerable")
|
|
88
|
+
self.assertEqual(row.severity, "high")
|
|
89
|
+
self.assertIn("CVE-2021-23017", row.cves)
|
|
90
|
+
|
|
91
|
+
def test_nginx_1201_is_the_fix_and_is_clean(self):
|
|
92
|
+
row = tech.detect([("Server", "nginx/1.20.1")])[0]
|
|
93
|
+
self.assertEqual(row.status, "detected")
|
|
94
|
+
|
|
95
|
+
def test_version_compare_is_numeric_not_lexicographic(self):
|
|
96
|
+
# 1.20.10 > 1.20.9 > 1.20.1, so none of these are "older than 1.20.1".
|
|
97
|
+
for version in ("1.20.9", "1.20.10", "1.21.0"):
|
|
98
|
+
row = tech.detect([("Server", f"nginx/{version}")])[0]
|
|
99
|
+
self.assertEqual(row.status, "detected", version)
|
|
100
|
+
|
|
101
|
+
def test_php_below_82_is_out_of_support_without_a_cve_claim(self):
|
|
102
|
+
row = tech.detect([("X-Powered-By", "PHP/7.4.33")])[0]
|
|
103
|
+
self.assertEqual(row.status, "outdated")
|
|
104
|
+
self.assertEqual(row.cves, [])
|
|
105
|
+
self.assertTrue(row.detail)
|
|
106
|
+
|
|
107
|
+
def test_supported_php_is_clean(self):
|
|
108
|
+
row = tech.detect([("X-Powered-By", "PHP/8.3.1")])[0]
|
|
109
|
+
self.assertEqual(row.status, "detected")
|
|
110
|
+
|
|
111
|
+
def test_iis_10_is_not_flagged(self):
|
|
112
|
+
row = tech.detect([("Server", "Microsoft-IIS/10.0")])[0]
|
|
113
|
+
self.assertEqual(row.status, "detected")
|
|
114
|
+
|
|
115
|
+
def test_iis_85_is_flagged_but_prefix_does_not_swallow_10(self):
|
|
116
|
+
self.assertEqual(tech.detect([("Server", "Microsoft-IIS/8.5")])[0].status,
|
|
117
|
+
"outdated")
|
|
118
|
+
self.assertEqual(tech.detect([("Server", "Microsoft-IIS/10.0")])[0].status,
|
|
119
|
+
"detected")
|
|
120
|
+
|
|
121
|
+
def test_cms_versions_are_reported_without_eol_claims(self):
|
|
122
|
+
row = tech.detect([], generator="Drupal 9")[0]
|
|
123
|
+
self.assertEqual(row.status, "detected")
|
|
124
|
+
self.assertEqual(row.cves, [])
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ReportShapeTests(unittest.TestCase):
|
|
128
|
+
def test_rows_carry_every_report_field(self):
|
|
129
|
+
rows = tech.rows(tech.detect([("Server", "Apache/2.4.49")]))
|
|
130
|
+
self.assertEqual(set(rows[0]), {"name", "version", "source", "status",
|
|
131
|
+
"severity", "cves", "detail"})
|
|
132
|
+
self.assertIsInstance(rows[0]["cves"], list)
|
|
133
|
+
|
|
134
|
+
def test_summary_counts_by_status(self):
|
|
135
|
+
found = (tech.detect([("Server", "Apache/2.4.49")])
|
|
136
|
+
+ tech.detect([("X-Powered-By", "PHP/7.4")])
|
|
137
|
+
+ tech.detect([("Server", "Microsoft-IIS/10.0")]))
|
|
138
|
+
self.assertEqual(tech.summary(found)["vulnerable"], 1)
|
|
139
|
+
self.assertEqual(tech.summary(found)["outdated"], 1)
|
|
140
|
+
self.assertEqual(tech.summary(found)["detected"], 1)
|
|
141
|
+
|
|
142
|
+
def test_line_names_the_version_and_evidence(self):
|
|
143
|
+
row = tech.detect([("Server", "nginx/1.18.0")])[0]
|
|
144
|
+
self.assertIn("nginx 1.18.0", row.line())
|
|
145
|
+
self.assertIn("Server", row.line())
|
|
146
|
+
self.assertIn("1.20.1", row.line())
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
unittest.main()
|