findssh 2.0.0__tar.gz → 2.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.
- {findssh-2.0.0/src/findssh.egg-info → findssh-2.1.0}/PKG-INFO +35 -10
- {findssh-2.0.0 → findssh-2.1.0}/README.md +27 -5
- {findssh-2.0.0 → findssh-2.1.0}/pyproject.toml +10 -10
- findssh-2.1.0/src/findssh/__init__.py +31 -0
- findssh-2.1.0/src/findssh/__main__.py +163 -0
- {findssh-2.0.0 → findssh-2.1.0}/src/findssh/base.py +22 -12
- findssh-2.1.0/src/findssh/coro.py +250 -0
- findssh-2.1.0/src/findssh/http.py +60 -0
- findssh-2.1.0/src/findssh/tests/conftest.py +84 -0
- findssh-2.1.0/src/findssh/tests/test_discovery.py +134 -0
- findssh-2.0.0/src/findssh/tests/test_coro.py → findssh-2.1.0/src/findssh/tests/test_ssh_coroutine.py +7 -0
- findssh-2.0.0/src/findssh/tests/test_threadpool.py → findssh-2.1.0/src/findssh/tests/test_ssh_threadpool.py +1 -0
- {findssh-2.0.0 → findssh-2.1.0}/src/findssh/threadpool.py +7 -5
- {findssh-2.0.0 → findssh-2.1.0/src/findssh.egg-info}/PKG-INFO +35 -10
- {findssh-2.0.0 → findssh-2.1.0}/src/findssh.egg-info/SOURCES.txt +6 -2
- findssh-2.1.0/src/findssh.egg-info/entry_points.txt +2 -0
- findssh-2.1.0/src/findssh.egg-info/requires.txt +5 -0
- findssh-2.0.0/src/findssh/__init__.py +0 -30
- findssh-2.0.0/src/findssh/__main__.py +0 -73
- findssh-2.0.0/src/findssh/coro.py +0 -68
- findssh-2.0.0/src/findssh.egg-info/requires.txt +0 -10
- {findssh-2.0.0 → findssh-2.1.0}/LICENSE.txt +0 -0
- {findssh-2.0.0 → findssh-2.1.0}/setup.cfg +0 -0
- {findssh-2.0.0 → findssh-2.1.0}/src/findssh/tests/__init__.py +0 -0
- {findssh-2.0.0 → findssh-2.1.0}/src/findssh.egg-info/dependency_links.txt +0 -0
- {findssh-2.0.0 → findssh-2.1.0}/src/findssh.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: findssh
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: find open servers on IPv4 subnet such as SSH
|
|
5
5
|
Keywords: port-scan,ssh
|
|
6
6
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -18,23 +18,28 @@ Classifier: Programming Language :: Python :: 3
|
|
|
18
18
|
Classifier: Topic :: Security
|
|
19
19
|
Classifier: Topic :: System :: Networking
|
|
20
20
|
Classifier: Topic :: Utilities
|
|
21
|
-
Requires-Python: >=3.
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
|
-
Provides-Extra: tests
|
|
24
|
-
Provides-Extra: lint
|
|
25
23
|
License-File: LICENSE.txt
|
|
24
|
+
Provides-Extra: tests
|
|
25
|
+
Requires-Dist: pytest; extra == "tests"
|
|
26
|
+
Requires-Dist: pytest-asyncio; extra == "tests"
|
|
27
|
+
Requires-Dist: mypy; extra == "tests"
|
|
28
|
+
Dynamic: license-file
|
|
26
29
|
|
|
27
30
|
# Find SSH servers (without NMAP)
|
|
28
31
|
|
|
29
32
|
[](https://doi.org/10.5281/zenodo.3336467)
|
|
30
|
-

|
|
31
|
-
[](https://github.com/scivision/findssh/actions/workflows/ci.yml)
|
|
34
|
+
[](https://pepy.tech/project/findssh)
|
|
32
35
|
|
|
33
36
|
Platform-independently find SSH servers (or other services with open ports) on an IPv4 subnet in pure Python WITHOUT NMAP.
|
|
34
37
|
Scan entire IPv4 subnet in less than 1 second using Python standard library `asyncio` coroutines and a single thread.
|
|
35
38
|
|
|
36
|
-
The
|
|
37
|
-
[asyncio
|
|
39
|
+
The findssh
|
|
40
|
+
[asyncio](https://docs.python.org/3/library/asyncio.html)
|
|
41
|
+
coroutine
|
|
42
|
+
[TaskGroup](https://docs.python.org/3/library/asyncio-task.html#task-groups)
|
|
38
43
|
uses a single thread and is more than 10x faster than
|
|
39
44
|
[concurrent.futures.ThreadPoolExecutor](https://docs.python.org/3/library/concurrent.futures.html).
|
|
40
45
|
|
|
@@ -49,7 +54,7 @@ or from this repo:
|
|
|
49
54
|
```sh
|
|
50
55
|
git clone https://github.com/scivision/findssh
|
|
51
56
|
|
|
52
|
-
pip install -e findssh
|
|
57
|
+
pip install -e ./findssh
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
## Usage
|
|
@@ -64,6 +69,18 @@ from command line:
|
|
|
64
69
|
python -m findssh
|
|
65
70
|
```
|
|
66
71
|
|
|
72
|
+
or use project script e.g. from [pipx](https://github.com/pypa/pipx):
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
findssh
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For example, to scan your local subnet for SSH servers:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
findssh -p 22 192.168.1.0
|
|
82
|
+
```
|
|
83
|
+
|
|
67
84
|
* `-s` check the string from the server to attempt to verify the correct service has been found
|
|
68
85
|
* `-t` timeout per server (seconds) useful for high latency connection
|
|
69
86
|
* `-b` baseip (check other subnet besides your own)
|
|
@@ -88,3 +105,11 @@ Thread pool (100 thread max, slow, heavy):
|
|
|
88
105
|
|
|
89
106
|
1.39 s ± 213 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
|
|
90
107
|
```
|
|
108
|
+
|
|
109
|
+
Trying to open too many threads via ThreadPoolExecutor can cause a system error like
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
OSError: [Errno 24] Too many open files
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Thus in practical terms, using coroutines can be significantly faster than threads while using less system resources.
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# Find SSH servers (without NMAP)
|
|
2
2
|
|
|
3
3
|
[](https://doi.org/10.5281/zenodo.3336467)
|
|
4
|
-

|
|
5
|
-
[](https://github.com/scivision/findssh/actions/workflows/ci.yml)
|
|
5
|
+
[](https://pepy.tech/project/findssh)
|
|
6
6
|
|
|
7
7
|
Platform-independently find SSH servers (or other services with open ports) on an IPv4 subnet in pure Python WITHOUT NMAP.
|
|
8
8
|
Scan entire IPv4 subnet in less than 1 second using Python standard library `asyncio` coroutines and a single thread.
|
|
9
9
|
|
|
10
|
-
The
|
|
11
|
-
[asyncio
|
|
10
|
+
The findssh
|
|
11
|
+
[asyncio](https://docs.python.org/3/library/asyncio.html)
|
|
12
|
+
coroutine
|
|
13
|
+
[TaskGroup](https://docs.python.org/3/library/asyncio-task.html#task-groups)
|
|
12
14
|
uses a single thread and is more than 10x faster than
|
|
13
15
|
[concurrent.futures.ThreadPoolExecutor](https://docs.python.org/3/library/concurrent.futures.html).
|
|
14
16
|
|
|
@@ -23,7 +25,7 @@ or from this repo:
|
|
|
23
25
|
```sh
|
|
24
26
|
git clone https://github.com/scivision/findssh
|
|
25
27
|
|
|
26
|
-
pip install -e findssh
|
|
28
|
+
pip install -e ./findssh
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
## Usage
|
|
@@ -38,6 +40,18 @@ from command line:
|
|
|
38
40
|
python -m findssh
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
or use project script e.g. from [pipx](https://github.com/pypa/pipx):
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
findssh
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For example, to scan your local subnet for SSH servers:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
findssh -p 22 192.168.1.0
|
|
53
|
+
```
|
|
54
|
+
|
|
41
55
|
* `-s` check the string from the server to attempt to verify the correct service has been found
|
|
42
56
|
* `-t` timeout per server (seconds) useful for high latency connection
|
|
43
57
|
* `-b` baseip (check other subnet besides your own)
|
|
@@ -62,3 +76,11 @@ Thread pool (100 thread max, slow, heavy):
|
|
|
62
76
|
|
|
63
77
|
1.39 s ± 213 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
|
|
64
78
|
```
|
|
79
|
+
|
|
80
|
+
Trying to open too many threads via ThreadPoolExecutor can cause a system error like
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
OSError: [Errno 24] Too many open files
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Thus in practical terms, using coroutines can be significantly faster than threads while using less system resources.
|
|
@@ -4,7 +4,6 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "findssh"
|
|
7
|
-
version = "2.0.0"
|
|
8
7
|
description = "find open servers on IPv4 subnet such as SSH"
|
|
9
8
|
keywords = ["port-scan", "ssh"]
|
|
10
9
|
classifiers = [
|
|
@@ -24,22 +23,23 @@ classifiers = [
|
|
|
24
23
|
"Topic :: System :: Networking",
|
|
25
24
|
"Topic :: Utilities"
|
|
26
25
|
]
|
|
27
|
-
dynamic = ["readme"]
|
|
28
|
-
requires-python = ">=3.
|
|
26
|
+
dynamic = ["readme", "version"]
|
|
27
|
+
requires-python = ">=3.11"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
findssh = "findssh.__main__:main"
|
|
29
31
|
|
|
30
32
|
[project.optional-dependencies]
|
|
31
|
-
tests = ["pytest"]
|
|
32
|
-
lint = ["flake8", "flake8-bugbear", "flake8-builtins", "flake8-blind-except", "mypy"]
|
|
33
|
+
tests = ["pytest", "pytest-asyncio", "mypy"]
|
|
33
34
|
|
|
34
35
|
[tool.setuptools.dynamic]
|
|
35
36
|
readme = {file = ["README.md"], content-type = "text/markdown"}
|
|
37
|
+
version = {attr = "findssh.__version__"}
|
|
36
38
|
|
|
37
39
|
[tool.black]
|
|
38
40
|
line-length = 90
|
|
39
41
|
|
|
40
42
|
[tool.mypy]
|
|
41
|
-
files = ["
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
show_error_context = false
|
|
45
|
-
show_column_numbers = true
|
|
43
|
+
files = ["."]
|
|
44
|
+
install_types = true
|
|
45
|
+
exclude = ["^build([/\\\\]|$)"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import socket
|
|
3
|
+
import ipaddress
|
|
4
|
+
|
|
5
|
+
from .coro import get_hosts, discover_services
|
|
6
|
+
|
|
7
|
+
__all__ = ["get_hosts", "discover_services", "address2net", "get_lan_ip"]
|
|
8
|
+
|
|
9
|
+
__version__ = "2.1.0"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_lan_ip() -> ipaddress.IPv4Address | ipaddress.IPv6Address:
|
|
13
|
+
"""
|
|
14
|
+
get IP address of currently used LAN interface
|
|
15
|
+
ref: https://stackoverflow.com/a/23822431
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
name = socket.gethostname()
|
|
19
|
+
host = socket.gethostbyname(name)
|
|
20
|
+
|
|
21
|
+
return ipaddress.ip_address(host)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def address2net(
|
|
25
|
+
addr: ipaddress.IPv4Address | ipaddress.IPv6Address, mask: str = "24"
|
|
26
|
+
) -> ipaddress.IPv4Network | ipaddress.IPv6Network:
|
|
27
|
+
|
|
28
|
+
if isinstance(addr, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
|
|
29
|
+
return ipaddress.ip_network(f"{addr}/{mask}", strict=False)
|
|
30
|
+
|
|
31
|
+
raise TypeError(f"Unsupported address type: {type(addr).__name__} (value: {addr})")
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
scans IPv4 subnet for SSH servers on Port 22 or other server ports.
|
|
4
|
+
Useful for machines that don't/can't have NMAP installed (e.g. Windows),
|
|
5
|
+
and device does not have Avahi server.
|
|
6
|
+
I wanted to make it as cross-platform as possible,
|
|
7
|
+
where the user would have only basic Python installed (Windows)
|
|
8
|
+
|
|
9
|
+
Note:
|
|
10
|
+
timeout value bare minimum is 0.15 seconds for LAN,
|
|
11
|
+
suggest using higher values say 0.25 or 0.35 to allow for network / CPU delays
|
|
12
|
+
Wifi timeout should be 1 second or more
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
import ipaddress as ip
|
|
18
|
+
from argparse import ArgumentParser
|
|
19
|
+
|
|
20
|
+
from . import get_lan_ip, address2net, discover_services
|
|
21
|
+
from . import coro
|
|
22
|
+
from . import threadpool
|
|
23
|
+
|
|
24
|
+
PORT = 22
|
|
25
|
+
TIMEOUT = 1.0
|
|
26
|
+
|
|
27
|
+
# Default ports for known services
|
|
28
|
+
DEFAULT_SERVICE_PORTS = {
|
|
29
|
+
"ssh": 22,
|
|
30
|
+
"http": 80,
|
|
31
|
+
"https": 443,
|
|
32
|
+
"dns": 53,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_services(services_str: str) -> dict[str, int | list[int]]:
|
|
37
|
+
"""
|
|
38
|
+
Parse service specification string.
|
|
39
|
+
|
|
40
|
+
Format: "service[:port[,port,...]]" (space-separated)
|
|
41
|
+
Examples:
|
|
42
|
+
"ssh" → {"ssh": 22}
|
|
43
|
+
"ssh:22" → {"ssh": 22}
|
|
44
|
+
"http:80,443" → {"http": [80, 443]}
|
|
45
|
+
"ssh http:80,443" → {"ssh": 22, "http": [80, 443]}
|
|
46
|
+
"ssh:2222 http:8080,8443" → {"ssh": 2222, "http": [8080, 8443]}
|
|
47
|
+
"""
|
|
48
|
+
result: dict[str, int | list[int]] = {}
|
|
49
|
+
|
|
50
|
+
for spec in services_str.split():
|
|
51
|
+
if ":" in spec:
|
|
52
|
+
service_name, ports_str = spec.split(":", 1)
|
|
53
|
+
ports_list = [int(p.strip()) for p in ports_str.split(",")]
|
|
54
|
+
if len(ports_list) == 1:
|
|
55
|
+
result[service_name] = ports_list[0]
|
|
56
|
+
else:
|
|
57
|
+
result[service_name] = ports_list
|
|
58
|
+
else:
|
|
59
|
+
# Use default port for service
|
|
60
|
+
service_name = spec
|
|
61
|
+
if service_name in DEFAULT_SERVICE_PORTS:
|
|
62
|
+
result[service_name] = DEFAULT_SERVICE_PORTS[service_name]
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Unknown service: {service_name}. Use service:port format."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if not result:
|
|
69
|
+
raise ValueError("No services specified")
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main():
|
|
75
|
+
p = ArgumentParser("scan for hosts with open port, without NMAP")
|
|
76
|
+
|
|
77
|
+
# Single port mode (original behavior)
|
|
78
|
+
p.add_argument("-p", "--port", help="single port to try", default=PORT, type=int)
|
|
79
|
+
p.add_argument(
|
|
80
|
+
"-s", "--service", default="", help="string to match to qualify detections"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Multi-service mode (new feature)
|
|
84
|
+
p.add_argument(
|
|
85
|
+
"--services",
|
|
86
|
+
help=(
|
|
87
|
+
"discover multiple services. Format: 'service[:port[,port,...]]' (space-separated). "
|
|
88
|
+
"Examples: 'ssh', 'ssh http:80,443', 'ssh:2222 http:8080'. "
|
|
89
|
+
"Defaults: ssh=22, http=80, https=443, dns=53"
|
|
90
|
+
),
|
|
91
|
+
type=str,
|
|
92
|
+
default="",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
p.add_argument(
|
|
96
|
+
"-t",
|
|
97
|
+
"--timeout",
|
|
98
|
+
help="timeout to wait for server. Must be finite or will hang.",
|
|
99
|
+
type=float,
|
|
100
|
+
default=TIMEOUT,
|
|
101
|
+
)
|
|
102
|
+
p.add_argument("-b", "--baseip", help="set a specific subnet to scan")
|
|
103
|
+
p.add_argument("-v", "--verbose", action="store_true")
|
|
104
|
+
p.add_argument(
|
|
105
|
+
"-threadpool", help="use threadpool instead of asyncio", action="store_true"
|
|
106
|
+
)
|
|
107
|
+
P = p.parse_args()
|
|
108
|
+
|
|
109
|
+
ll = logging.DEBUG if P.verbose else logging.INFO
|
|
110
|
+
logging.basicConfig(level=ll)
|
|
111
|
+
|
|
112
|
+
if not P.baseip:
|
|
113
|
+
ownip = get_lan_ip()
|
|
114
|
+
print("own address", ownip)
|
|
115
|
+
else:
|
|
116
|
+
ownip = ip.ip_address(P.baseip)
|
|
117
|
+
|
|
118
|
+
net = address2net(ownip)
|
|
119
|
+
print("searching", net)
|
|
120
|
+
|
|
121
|
+
# Multi-service discovery mode
|
|
122
|
+
if P.services:
|
|
123
|
+
if P.threadpool:
|
|
124
|
+
print(
|
|
125
|
+
"Warning: threadpool not supported for multi-service discovery, using asyncio"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
service_ports = parse_services(P.services)
|
|
130
|
+
except ValueError as e:
|
|
131
|
+
print(f"Error parsing services: {e}")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
print(f"discovering services: {', '.join(service_ports.keys())}")
|
|
135
|
+
results = asyncio.run(discover_services(net, service_ports, P.timeout))
|
|
136
|
+
|
|
137
|
+
# Display results
|
|
138
|
+
print()
|
|
139
|
+
for service_name in sorted(results.keys()):
|
|
140
|
+
hosts = results[service_name]
|
|
141
|
+
print(f"{service_name}:")
|
|
142
|
+
if hosts:
|
|
143
|
+
for host, banner in hosts:
|
|
144
|
+
print(f" {host}: {banner}")
|
|
145
|
+
else:
|
|
146
|
+
print(" (none found)")
|
|
147
|
+
|
|
148
|
+
# Original single-port mode
|
|
149
|
+
else:
|
|
150
|
+
if P.threadpool:
|
|
151
|
+
for host in threadpool.get_hosts(
|
|
152
|
+
net,
|
|
153
|
+
P.port,
|
|
154
|
+
P.timeout,
|
|
155
|
+
P.service,
|
|
156
|
+
):
|
|
157
|
+
print(host)
|
|
158
|
+
else:
|
|
159
|
+
asyncio.run(coro.get_hosts(net, P.port, P.timeout, P.service))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__":
|
|
163
|
+
main()
|
|
@@ -1,26 +1,31 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
import ipaddress as ip
|
|
3
2
|
import socket
|
|
4
|
-
import
|
|
3
|
+
import logging
|
|
4
|
+
import collections.abc
|
|
5
5
|
|
|
6
|
+
HostResult = tuple[ip.IPv4Address, str]
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
def get_service(b: bytes, service: str | None = None) -> str | None:
|
|
8
10
|
"""
|
|
9
11
|
splitlines is in case the ASCII/UTF8 response is less than 32 bytes,
|
|
10
12
|
hoping server sends a \r\n
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if lines := b.splitlines():
|
|
16
|
+
svc_txt = lines[0].decode("utf-8", errors="ignore")
|
|
17
|
+
# %% optional service validation
|
|
18
|
+
if service and service not in svc_txt.lower():
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
return svc_txt
|
|
17
22
|
|
|
18
|
-
return
|
|
23
|
+
return None
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
def is_port_open(
|
|
22
27
|
host: ip.IPv4Address, port: int, timeout: float, service: str | None = None
|
|
23
|
-
) ->
|
|
28
|
+
) -> HostResult | None:
|
|
24
29
|
"""
|
|
25
30
|
is a port open? Without coroutines.
|
|
26
31
|
"""
|
|
@@ -36,9 +41,12 @@ def is_port_open(
|
|
|
36
41
|
return None
|
|
37
42
|
|
|
38
43
|
try:
|
|
44
|
+
# If recv returns empty bytes, the connection was closed by the remote host.
|
|
45
|
+
# This does not necessarily mean the port is closed, but no service banner was received.
|
|
39
46
|
if not (resp := s.recv(32)):
|
|
40
47
|
return None
|
|
41
|
-
except (socket.timeout, ConnectionError):
|
|
48
|
+
except (socket.timeout, ConnectionError) as err:
|
|
49
|
+
logging.debug("Socket error: %s", err)
|
|
42
50
|
return None
|
|
43
51
|
|
|
44
52
|
if svc_txt := get_service(resp, service):
|
|
@@ -49,9 +57,11 @@ def is_port_open(
|
|
|
49
57
|
|
|
50
58
|
def get_hosts_seq(
|
|
51
59
|
net: ip.IPv4Network, port: int, timeout: float, service: str | None = None
|
|
52
|
-
) ->
|
|
60
|
+
) -> collections.abc.Iterable[HostResult]:
|
|
53
61
|
"""
|
|
54
|
-
|
|
62
|
+
Yields hosts in the network that have the specified port open and
|
|
63
|
+
match the service (if provided),
|
|
64
|
+
sequentially (no parallelism or concurrency).
|
|
55
65
|
"""
|
|
56
66
|
|
|
57
67
|
for host in net.hosts():
|