hostlens 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.
- hostlens-0.1.0/.github/workflows/workflow.yml +71 -0
- hostlens-0.1.0/.gitignore +9 -0
- hostlens-0.1.0/.pre-commit-config.yaml +7 -0
- hostlens-0.1.0/CHANGELOG.md +12 -0
- hostlens-0.1.0/CONTRIBUTING.md +15 -0
- hostlens-0.1.0/LICENSE +21 -0
- hostlens-0.1.0/PKG-INFO +415 -0
- hostlens-0.1.0/README.md +379 -0
- hostlens-0.1.0/benchmarks/local_scan.py +45 -0
- hostlens-0.1.0/examples/discover.py +11 -0
- hostlens-0.1.0/examples/evidence.py +12 -0
- hostlens-0.1.0/examples/fingerbank.py +12 -0
- hostlens-0.1.0/examples/identify.py +4 -0
- hostlens-0.1.0/examples/passive.py +11 -0
- hostlens-0.1.0/examples/scan.py +11 -0
- hostlens-0.1.0/pyproject.toml +71 -0
- hostlens-0.1.0/src/hostlens/__init__.py +18 -0
- hostlens-0.1.0/src/hostlens/api.py +28 -0
- hostlens-0.1.0/src/hostlens/cache.py +30 -0
- hostlens-0.1.0/src/hostlens/cli/__init__.py +5 -0
- hostlens-0.1.0/src/hostlens/cli/__main__.py +3 -0
- hostlens-0.1.0/src/hostlens/cli/app.py +88 -0
- hostlens-0.1.0/src/hostlens/cli/output.py +39 -0
- hostlens-0.1.0/src/hostlens/client.py +271 -0
- hostlens-0.1.0/src/hostlens/collectors/__init__.py +21 -0
- hostlens-0.1.0/src/hostlens/collectors/base.py +41 -0
- hostlens-0.1.0/src/hostlens/collectors/basic.py +65 -0
- hostlens-0.1.0/src/hostlens/collectors/mdns.py +96 -0
- hostlens-0.1.0/src/hostlens/collectors/netbios.py +49 -0
- hostlens-0.1.0/src/hostlens/collectors/services.py +45 -0
- hostlens-0.1.0/src/hostlens/collectors/ssdp.py +65 -0
- hostlens-0.1.0/src/hostlens/collectors/upnp.py +41 -0
- hostlens-0.1.0/src/hostlens/config.py +71 -0
- hostlens-0.1.0/src/hostlens/exceptions.py +37 -0
- hostlens-0.1.0/src/hostlens/identity/__init__.py +5 -0
- hostlens-0.1.0/src/hostlens/identity/fingerbank.py +63 -0
- hostlens-0.1.0/src/hostlens/identity/fingerprints.py +51 -0
- hostlens-0.1.0/src/hostlens/identity/fusion.py +140 -0
- hostlens-0.1.0/src/hostlens/models/__init__.py +30 -0
- hostlens-0.1.0/src/hostlens/models/device.py +66 -0
- hostlens-0.1.0/src/hostlens/models/events.py +42 -0
- hostlens-0.1.0/src/hostlens/models/evidence.py +45 -0
- hostlens-0.1.0/src/hostlens/models/target.py +13 -0
- hostlens-0.1.0/src/hostlens/network/__init__.py +6 -0
- hostlens-0.1.0/src/hostlens/network/addressing.py +74 -0
- hostlens-0.1.0/src/hostlens/network/discovery.py +131 -0
- hostlens-0.1.0/src/hostlens/parsers/__init__.py +7 -0
- hostlens-0.1.0/src/hostlens/parsers/dhcp.py +45 -0
- hostlens-0.1.0/src/hostlens/parsers/ssdp.py +14 -0
- hostlens-0.1.0/src/hostlens/parsers/upnp.py +44 -0
- hostlens-0.1.0/tests/unit/test_cli.py +21 -0
- hostlens-0.1.0/tests/unit/test_client.py +59 -0
- hostlens-0.1.0/tests/unit/test_config_cache.py +22 -0
- hostlens-0.1.0/tests/unit/test_fingerbank.py +24 -0
- hostlens-0.1.0/tests/unit/test_fusion.py +37 -0
- hostlens-0.1.0/tests/unit/test_models.py +33 -0
- hostlens-0.1.0/tests/unit/test_networking.py +45 -0
- hostlens-0.1.0/tests/unit/test_parsers.py +41 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: Test and publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
quality:
|
|
15
|
+
name: Python ${{ matrix.python-version }}
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
cache: pip
|
|
29
|
+
|
|
30
|
+
- run: python -m pip install -e ".[dev]"
|
|
31
|
+
- run: python -m ruff format --check .
|
|
32
|
+
- run: python -m ruff check .
|
|
33
|
+
- run: python -m pyright
|
|
34
|
+
- run: python -m pytest --cov=hostlens --cov-report=term-missing
|
|
35
|
+
|
|
36
|
+
build:
|
|
37
|
+
name: Build package
|
|
38
|
+
needs: quality
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- uses: actions/setup-python@v5
|
|
45
|
+
with:
|
|
46
|
+
python-version: "3.13"
|
|
47
|
+
|
|
48
|
+
- run: python -m pip install build
|
|
49
|
+
- run: python -m build
|
|
50
|
+
|
|
51
|
+
- uses: actions/upload-artifact@v4
|
|
52
|
+
with:
|
|
53
|
+
name: python-package
|
|
54
|
+
path: dist/
|
|
55
|
+
|
|
56
|
+
publish:
|
|
57
|
+
name: Publish to PyPI
|
|
58
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
59
|
+
needs: build
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
permissions:
|
|
62
|
+
contents: read
|
|
63
|
+
id-token: write
|
|
64
|
+
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/download-artifact@v4
|
|
67
|
+
with:
|
|
68
|
+
name: python-package
|
|
69
|
+
path: dist/
|
|
70
|
+
|
|
71
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-09-20
|
|
4
|
+
|
|
5
|
+
- First public API for host identification and subnet scanning
|
|
6
|
+
- Fast, normal, deep, and passive profiles
|
|
7
|
+
- Neighbor-table and ARP discovery with progressive events
|
|
8
|
+
- Reverse DNS, OUI, mDNS, SSDP, UPnP, NetBIOS, and selected service collectors
|
|
9
|
+
- Explainable evidence fusion and opt-in Fingerbank enrichment
|
|
10
|
+
- Typer and Rich CLI
|
|
11
|
+
- Clean feature folders for models, network code, collectors, parsers, identity, and CLI
|
|
12
|
+
- PyPI trusted publishing workflow
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Create a focused branch, add observable-behavior tests, and run
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
ruff check .
|
|
7
|
+
ruff format --check .
|
|
8
|
+
pyright
|
|
9
|
+
pytest
|
|
10
|
+
python -m build
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Keep collection, parsing, fingerprinting, fusion, and presentation separate
|
|
14
|
+
|
|
15
|
+
Unit tests should not need network access
|
hostlens-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HostLens contributors
|
|
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.
|
hostlens-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hostlens
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: High-level Python device discovery and identification for local networks
|
|
5
|
+
Project-URL: Homepage, https://github.com/cyber0x3a/hostlens
|
|
6
|
+
Project-URL: Issues, https://github.com/cyber0x3a/hostlens/issues
|
|
7
|
+
Author: HostLens contributors
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: arp,discovery,fingerprinting,lan,network,upnp
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: System :: Networking
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: httpx<1,>=0.27
|
|
20
|
+
Requires-Dist: ifaddr<1,>=0.2
|
|
21
|
+
Requires-Dist: manuf<2,>=1.1.5
|
|
22
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
23
|
+
Requires-Dist: rich<15,>=13.7
|
|
24
|
+
Requires-Dist: scapy<3,>=2.5
|
|
25
|
+
Requires-Dist: typer<1,>=0.12
|
|
26
|
+
Requires-Dist: zeroconf<1,>=0.132
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
29
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
30
|
+
Requires-Dist: pyright>=1.1.380; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
<div align="center">
|
|
38
|
+
|
|
39
|
+
# HostLens
|
|
40
|
+
|
|
41
|
+
### Find and understand devices on your local network from Python
|
|
42
|
+
|
|
43
|
+
[](https://github.com/Cyber0x3a/hostlens/actions/workflows/workflow.yml)
|
|
44
|
+
[](https://pypi.org/project/hostlens/)
|
|
45
|
+
[](https://pypi.org/project/hostlens/)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
Simple public API · Async first · Explainable results · No cloud calls by default
|
|
49
|
+
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
HostLens discovers devices on a LAN, collects useful facts about them, and turns
|
|
53
|
+
those facts into one clean device profile
|
|
54
|
+
|
|
55
|
+
It uses ARP, the local neighbor table, reverse DNS, OUI data, mDNS, SSDP, UPnP,
|
|
56
|
+
NetBIOS, and a small set of useful service checks
|
|
57
|
+
|
|
58
|
+
The normal API stays small even though the scan can use several protocols behind
|
|
59
|
+
the scenes
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install hostlens
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
HostLens supports Python 3.11 and newer on Windows, Linux, and macOS
|
|
68
|
+
|
|
69
|
+
For local development
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/Cyber0x3a/hostlens.git
|
|
73
|
+
cd hostlens
|
|
74
|
+
python -m venv .venv
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Windows PowerShell
|
|
78
|
+
|
|
79
|
+
```powershell
|
|
80
|
+
.\.venv\Scripts\Activate.ps1
|
|
81
|
+
python -m pip install -e ".[dev]"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Linux and macOS
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
source .venv/bin/activate
|
|
88
|
+
python -m pip install -e ".[dev]"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Identify one device
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from hostlens import identify
|
|
95
|
+
|
|
96
|
+
device = identify("192.168.1.42")
|
|
97
|
+
|
|
98
|
+
print(device.best_name)
|
|
99
|
+
print(device.manufacturer)
|
|
100
|
+
print(device.device_type)
|
|
101
|
+
print(device.model)
|
|
102
|
+
print(device.os)
|
|
103
|
+
print(device.confidence)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Possible result
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
Samsung QN90C
|
|
110
|
+
Samsung Electronics
|
|
111
|
+
smart_tv
|
|
112
|
+
QN90C
|
|
113
|
+
Tizen
|
|
114
|
+
0.94
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Unknown values stay `None`
|
|
118
|
+
|
|
119
|
+
HostLens does not fill gaps with strings like `Unknown` or `N/A`
|
|
120
|
+
|
|
121
|
+
## Async API
|
|
122
|
+
|
|
123
|
+
Async is the main implementation and the sync helpers are only thin wrappers
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from hostlens import HostLens
|
|
127
|
+
|
|
128
|
+
intel = HostLens()
|
|
129
|
+
device = await intel.identify("192.168.1.42")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Scan one host deeply
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
device = await intel.scan_host("192.168.1.42", mode="deep")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Scan several hosts with bounded concurrency
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
devices = await intel.scan_hosts(
|
|
142
|
+
[
|
|
143
|
+
"192.168.1.20",
|
|
144
|
+
"192.168.1.21",
|
|
145
|
+
"192.168.1.30",
|
|
146
|
+
],
|
|
147
|
+
mode="normal",
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Scan a subnet
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
devices = await intel.scan_network("192.168.1.0/24")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Let HostLens detect the local subnet
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
devices = await intel.scan_network()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Pick an interface when the machine has more than one active connection
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
intel = HostLens(interface="Wi-Fi")
|
|
167
|
+
devices = await intel.scan_network()
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Scan modes
|
|
171
|
+
|
|
172
|
+
| Mode | Good for | What it does |
|
|
173
|
+
| --- | --- | --- |
|
|
174
|
+
| `fast` | A quick inventory | Neighbor table, ARP, reverse DNS, and OUI |
|
|
175
|
+
| `normal` | Everyday use | Fast scan plus mDNS, SSDP, UPnP, and local rules |
|
|
176
|
+
| `deep` | More detail | Normal scan plus NetBIOS and selected TCP services |
|
|
177
|
+
| `passive` | Quiet observation | Reads devices already visible in the OS neighbor table |
|
|
178
|
+
|
|
179
|
+
Normal mode is the default
|
|
180
|
+
|
|
181
|
+
Deep mode checks a small curated set of useful ports rather than scanning every
|
|
182
|
+
port on every device
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from hostlens import HostLens, ScanMode
|
|
186
|
+
|
|
187
|
+
intel = HostLens(timeout=3, concurrency=64)
|
|
188
|
+
devices = await intel.scan_network(mode=ScanMode.FAST)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Progressive results
|
|
192
|
+
|
|
193
|
+
Applications do not need to wait for every collector to finish
|
|
194
|
+
|
|
195
|
+
HostLens first reports the device and then sends enriched updates
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from hostlens.models import DeviceFound, DeviceUpdated, ScanCompleted
|
|
199
|
+
|
|
200
|
+
async for event in intel.scan_stream("192.168.1.0/24", mode="normal"):
|
|
201
|
+
match event:
|
|
202
|
+
case DeviceFound(device=device):
|
|
203
|
+
print("found", device.ip)
|
|
204
|
+
|
|
205
|
+
case DeviceUpdated(device=device):
|
|
206
|
+
print("updated", device.best_name)
|
|
207
|
+
|
|
208
|
+
case ScanCompleted(summary=summary):
|
|
209
|
+
print(summary)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Ask for a full scan result when summary data matters
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
result = await intel.scan_network(mode="fast", return_result=True)
|
|
216
|
+
|
|
217
|
+
print(result.summary.duration)
|
|
218
|
+
print(result.summary.targets_checked)
|
|
219
|
+
print(result.summary.devices_found)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Device profile
|
|
223
|
+
|
|
224
|
+
Every scan returns the same public model
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
device.ip
|
|
228
|
+
device.mac
|
|
229
|
+
device.hostname
|
|
230
|
+
device.manufacturer
|
|
231
|
+
device.device_type
|
|
232
|
+
device.model
|
|
233
|
+
device.os
|
|
234
|
+
device.services
|
|
235
|
+
device.confidence
|
|
236
|
+
device.fields
|
|
237
|
+
device.evidence
|
|
238
|
+
device.best_name
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Pydantic serialization is available without another conversion layer
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
data = device.model_dump()
|
|
245
|
+
json_data = device.model_dump_json(indent=2)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Evidence and confidence
|
|
249
|
+
|
|
250
|
+
HostLens keeps the raw facts that produced a profile
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
for item in device.evidence:
|
|
254
|
+
print(item.source, item.field, item.value, item.confidence)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
You can also get a readable explanation
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
print(device.explain())
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```text
|
|
264
|
+
Samsung QN90C
|
|
265
|
+
Confidence: 94%
|
|
266
|
+
|
|
267
|
+
Evidence:
|
|
268
|
+
- MAC prefix matches Samsung Electronics
|
|
269
|
+
- UPnP manufacturer reports Samsung Electronics
|
|
270
|
+
- UPnP modelName reports QN90C
|
|
271
|
+
- mDNS advertises _airplay._tcp.local
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Confidence values are heuristic scores for ranking evidence
|
|
275
|
+
|
|
276
|
+
They are not presented as mathematically calibrated probabilities
|
|
277
|
+
|
|
278
|
+
## Privacy and Fingerbank
|
|
279
|
+
|
|
280
|
+
Local scanning works without an account or API key
|
|
281
|
+
|
|
282
|
+
Cloud enrichment is disabled by default and HostLens never sends device data to
|
|
283
|
+
Fingerbank unless the caller enables it
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
intel = HostLens(
|
|
287
|
+
cloud=True,
|
|
288
|
+
fingerbank_api_key="your-api-key",
|
|
289
|
+
)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Preview the payload before any cloud lookup
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
payload = intel.preview_cloud_payload(device)
|
|
296
|
+
print(payload)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
API keys are not stored in evidence and are not written to logs
|
|
300
|
+
|
|
301
|
+
## CLI
|
|
302
|
+
|
|
303
|
+
The CLI calls the same Python API used by applications
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
hostlens identify 192.168.1.20
|
|
307
|
+
hostlens identify 192.168.1.20 --deep
|
|
308
|
+
|
|
309
|
+
hostlens scan
|
|
310
|
+
hostlens scan --fast
|
|
311
|
+
hostlens scan --deep
|
|
312
|
+
hostlens scan 192.168.1.0/24
|
|
313
|
+
|
|
314
|
+
hostlens discover
|
|
315
|
+
hostlens watch --passive
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## How the code is organized
|
|
319
|
+
|
|
320
|
+
```text
|
|
321
|
+
src/hostlens/
|
|
322
|
+
├── api.py sync and async convenience functions
|
|
323
|
+
├── client.py public orchestration API
|
|
324
|
+
├── config.py scan mode defaults
|
|
325
|
+
├── models/ public Pydantic models and scan events
|
|
326
|
+
├── network/ addressing, neighbor table, and ARP discovery
|
|
327
|
+
├── collectors/ one focused module for each protocol
|
|
328
|
+
├── parsers/ deterministic protocol parsing
|
|
329
|
+
├── identity/ fingerprints, Fingerbank, and evidence fusion
|
|
330
|
+
└── cli/ commands and terminal output
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The dependency flow stays simple
|
|
334
|
+
|
|
335
|
+
```text
|
|
336
|
+
network discovery
|
|
337
|
+
↓
|
|
338
|
+
evidence collectors
|
|
339
|
+
↓
|
|
340
|
+
local or cloud identity hints
|
|
341
|
+
↓
|
|
342
|
+
evidence fusion
|
|
343
|
+
↓
|
|
344
|
+
DeviceProfile
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Collectors collect facts
|
|
348
|
+
|
|
349
|
+
Parsers parse protocol data
|
|
350
|
+
|
|
351
|
+
Fingerprint rules interpret facts
|
|
352
|
+
|
|
353
|
+
Fusion picks the final values
|
|
354
|
+
|
|
355
|
+
The client only coordinates those steps
|
|
356
|
+
|
|
357
|
+
## Platform notes
|
|
358
|
+
|
|
359
|
+
Active ARP discovery uses Scapy
|
|
360
|
+
|
|
361
|
+
Windows may need [Npcap](https://npcap.com/) and an elevated terminal for raw ARP
|
|
362
|
+
access
|
|
363
|
+
|
|
364
|
+
Linux may need root or the relevant raw socket capability
|
|
365
|
+
|
|
366
|
+
When raw ARP is unavailable, HostLens can still use the local neighbor table and
|
|
367
|
+
the collectors that work in the current environment
|
|
368
|
+
|
|
369
|
+
## Development
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
python -m ruff format --check .
|
|
373
|
+
python -m ruff check .
|
|
374
|
+
python -m pyright
|
|
375
|
+
python -m pytest
|
|
376
|
+
python -m build
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The normal unit test suite does not need a live network
|
|
380
|
+
|
|
381
|
+
Tests that use a real LAN should use the `network` marker
|
|
382
|
+
|
|
383
|
+
## Local benchmark
|
|
384
|
+
|
|
385
|
+
The benchmark runs three fast scans and prints only aggregate timing and counts
|
|
386
|
+
|
|
387
|
+
It never prints local IP addresses or MAC addresses
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
python benchmarks/local_scan.py
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Fast scan performance depends on ARP access, the operating system neighbor table,
|
|
394
|
+
DNS response time, interface size, and the number of visible devices
|
|
395
|
+
|
|
396
|
+
## Publishing
|
|
397
|
+
|
|
398
|
+
The GitHub Actions workflow lives at `.github/workflows/workflow.yml`
|
|
399
|
+
|
|
400
|
+
Every push and pull request runs formatting, linting, typing, tests, and package
|
|
401
|
+
builds
|
|
402
|
+
|
|
403
|
+
Publishing uses PyPI trusted publishing when a version tag such as `v0.1.0` is pushed
|
|
404
|
+
|
|
405
|
+
The PyPI project needs a trusted publisher for
|
|
406
|
+
|
|
407
|
+
```text
|
|
408
|
+
Owner Cyber0x3a
|
|
409
|
+
Repository hostlens
|
|
410
|
+
Workflow workflow.yml
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## License
|
|
414
|
+
|
|
415
|
+
[MIT](LICENSE)
|