netpulse-discovery 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.
- netpulse_discovery-0.1.0/.gitignore +29 -0
- netpulse_discovery-0.1.0/PKG-INFO +81 -0
- netpulse_discovery-0.1.0/README.md +68 -0
- netpulse_discovery-0.1.0/docs/architecture.md +21 -0
- netpulse_discovery-0.1.0/docs/assets/logo.png +0 -0
- netpulse_discovery-0.1.0/docs/contributing.md +31 -0
- netpulse_discovery-0.1.0/docs/usage.md +50 -0
- netpulse_discovery-0.1.0/pyproject.toml +33 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/__init__.py +0 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/api.py +20 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/cli.py +26 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/engine/__init__.py +48 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/models/__init__.py +0 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/models/device.py +88 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/models/discovery.py +87 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/models/drift.py +34 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/services/__init__.py +0 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/services/discovery.py +161 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/services/drift.py +90 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/services/mac_lookup.py +212 -0
- netpulse_discovery-0.1.0/src/netpulse/discovery/services/port_scanner.py +105 -0
- netpulse_discovery-0.1.0/tests/integration/test_api.py +29 -0
- netpulse_discovery-0.1.0/tests/integration/test_cli.py +18 -0
- netpulse_discovery-0.1.0/tests/unit/test_discovery_service.py +123 -0
- netpulse_discovery-0.1.0/tests/unit/test_drift.py +114 -0
- netpulse_discovery-0.1.0/tests/unit/test_engine.py +34 -0
- netpulse_discovery-0.1.0/tests/unit/test_mac_lookup.py +131 -0
- netpulse_discovery-0.1.0/tests/unit/test_port_scanner.py +102 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Virtual Environments
|
|
2
|
+
.venv/
|
|
3
|
+
venv/
|
|
4
|
+
ENV/
|
|
5
|
+
|
|
6
|
+
# Python Caches & Bytecode
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.pyc
|
|
9
|
+
*.pyo
|
|
10
|
+
*.pyd
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
|
|
15
|
+
# Rust / Maturin Build Artifacts
|
|
16
|
+
rust/target/
|
|
17
|
+
*.so
|
|
18
|
+
*.dylib
|
|
19
|
+
*.dll
|
|
20
|
+
*.pyd
|
|
21
|
+
|
|
22
|
+
# Local Configuration / Environment
|
|
23
|
+
.env
|
|
24
|
+
.env*.local
|
|
25
|
+
|
|
26
|
+
# Scratch / Temporary Files
|
|
27
|
+
scratch/
|
|
28
|
+
netpulse.db
|
|
29
|
+
.netpulse-ipam.db
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: netpulse-discovery
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: High-speed network discovery and drift analysis engine
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: fastapi>=0.100.0
|
|
7
|
+
Requires-Dist: netpulse-rust
|
|
8
|
+
Requires-Dist: pydantic>=2.0.0
|
|
9
|
+
Requires-Dist: rich>=13.0.0
|
|
10
|
+
Requires-Dist: typer>=0.9.0
|
|
11
|
+
Requires-Dist: uvicorn>=0.22.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="docs/assets/logo.png" alt="NetPulse Discovery Logo" width="300"/>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
# NetPulse Discovery
|
|
19
|
+
|
|
20
|
+
[](https://python.org)
|
|
21
|
+
[](https://rust-lang.org)
|
|
22
|
+
[](https://github.com/bonheur/netpulse)
|
|
23
|
+
|
|
24
|
+
**NetPulse Discovery** is a high-performance, asynchronous network mapping and drift-analysis engine. By offloading packet-level networking to a compiled Rust core, it achieves near wire-speed execution while maintaining a flexible, developer-friendly Python API.
|
|
25
|
+
|
|
26
|
+
## 🚀 Key Features
|
|
27
|
+
|
|
28
|
+
- **Blazing Fast Scans:** Leverages `libpcap` and raw sockets in Rust to execute ARP and ICMP sweeps orders of magnitude faster than standard Python libraries.
|
|
29
|
+
- **Asynchronous Port Scanning:** Native `asyncio` TCP connect scanner that can check hundreds of ports across multiple devices concurrently.
|
|
30
|
+
- **Drift Detection:** Built-in intelligence to compare historical scans and calculate exact topological drift (e.g., "Host 192.168.1.5 went offline").
|
|
31
|
+
- **MAC Vendor Resolution:** Automatically translates hardware MAC addresses into human-readable manufacturer names.
|
|
32
|
+
- **Standalone API & CLI:** Usable as a Python library, a Typer-powered CLI, or a FastAPI REST microservice.
|
|
33
|
+
|
|
34
|
+
## 📦 Installation
|
|
35
|
+
|
|
36
|
+
Since NetPulse Discovery is part of the NetPulse workspace, it relies on `uv` for dependency management and workspace resolution.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv sync
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## âš¡ Quickstart
|
|
43
|
+
|
|
44
|
+
### As a CLI Tool
|
|
45
|
+
|
|
46
|
+
The standalone CLI returns structured JSON output perfect for piping into `jq` or other tools.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Note: Raw sockets require elevated privileges
|
|
50
|
+
sudo uv run netpulse-discovery scan 192.168.1.0/24 --timeout 500
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### As a Python Library
|
|
54
|
+
|
|
55
|
+
Embed the high-performance engine directly into your own applications:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import asyncio
|
|
59
|
+
from netpulse.discovery.services.discovery import DiscoveryService
|
|
60
|
+
from netpulse.discovery.models.discovery import DiscoveryMethod
|
|
61
|
+
|
|
62
|
+
async def main():
|
|
63
|
+
service = DiscoveryService()
|
|
64
|
+
# Scans the network using ARP resolution
|
|
65
|
+
result = await service.discover_network(
|
|
66
|
+
"10.0.0.0/24",
|
|
67
|
+
methods=[DiscoveryMethod.ARP]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
for device in result.devices:
|
|
71
|
+
print(f"[{device.ip}] {device.mac} - {device.vendor}")
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 📚 Documentation
|
|
77
|
+
|
|
78
|
+
For more detailed guides, check out the `docs/` folder:
|
|
79
|
+
- [Usage Guide](docs/usage.md)
|
|
80
|
+
- [Architecture Overview](docs/architecture.md)
|
|
81
|
+
- [Contributing Guidelines](docs/contributing.md)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/logo.png" alt="NetPulse Discovery Logo" width="300"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# NetPulse Discovery
|
|
6
|
+
|
|
7
|
+
[](https://python.org)
|
|
8
|
+
[](https://rust-lang.org)
|
|
9
|
+
[](https://github.com/bonheur/netpulse)
|
|
10
|
+
|
|
11
|
+
**NetPulse Discovery** is a high-performance, asynchronous network mapping and drift-analysis engine. By offloading packet-level networking to a compiled Rust core, it achieves near wire-speed execution while maintaining a flexible, developer-friendly Python API.
|
|
12
|
+
|
|
13
|
+
## 🚀 Key Features
|
|
14
|
+
|
|
15
|
+
- **Blazing Fast Scans:** Leverages `libpcap` and raw sockets in Rust to execute ARP and ICMP sweeps orders of magnitude faster than standard Python libraries.
|
|
16
|
+
- **Asynchronous Port Scanning:** Native `asyncio` TCP connect scanner that can check hundreds of ports across multiple devices concurrently.
|
|
17
|
+
- **Drift Detection:** Built-in intelligence to compare historical scans and calculate exact topological drift (e.g., "Host 192.168.1.5 went offline").
|
|
18
|
+
- **MAC Vendor Resolution:** Automatically translates hardware MAC addresses into human-readable manufacturer names.
|
|
19
|
+
- **Standalone API & CLI:** Usable as a Python library, a Typer-powered CLI, or a FastAPI REST microservice.
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
Since NetPulse Discovery is part of the NetPulse workspace, it relies on `uv` for dependency management and workspace resolution.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv sync
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## âš¡ Quickstart
|
|
30
|
+
|
|
31
|
+
### As a CLI Tool
|
|
32
|
+
|
|
33
|
+
The standalone CLI returns structured JSON output perfect for piping into `jq` or other tools.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Note: Raw sockets require elevated privileges
|
|
37
|
+
sudo uv run netpulse-discovery scan 192.168.1.0/24 --timeout 500
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### As a Python Library
|
|
41
|
+
|
|
42
|
+
Embed the high-performance engine directly into your own applications:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import asyncio
|
|
46
|
+
from netpulse.discovery.services.discovery import DiscoveryService
|
|
47
|
+
from netpulse.discovery.models.discovery import DiscoveryMethod
|
|
48
|
+
|
|
49
|
+
async def main():
|
|
50
|
+
service = DiscoveryService()
|
|
51
|
+
# Scans the network using ARP resolution
|
|
52
|
+
result = await service.discover_network(
|
|
53
|
+
"10.0.0.0/24",
|
|
54
|
+
methods=[DiscoveryMethod.ARP]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
for device in result.devices:
|
|
58
|
+
print(f"[{device.ip}] {device.mac} - {device.vendor}")
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 📚 Documentation
|
|
64
|
+
|
|
65
|
+
For more detailed guides, check out the `docs/` folder:
|
|
66
|
+
- [Usage Guide](docs/usage.md)
|
|
67
|
+
- [Architecture Overview](docs/architecture.md)
|
|
68
|
+
- [Contributing Guidelines](docs/contributing.md)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# NetPulse Discovery Architecture
|
|
2
|
+
|
|
3
|
+
The `netpulse-discovery` package employs a hybrid architectural design, leveraging both Python and Rust to achieve high-performance network mapping while maintaining the flexibility of Python APIs.
|
|
4
|
+
|
|
5
|
+
## Core Components
|
|
6
|
+
|
|
7
|
+
### 1. `netpulse-rust` Core
|
|
8
|
+
At the lowest level, all blocking, packet-level network I/O is performed by a compiled Rust binary. This bypasses Python's Global Interpreter Lock (GIL) and provides near wire-speed packet generation for ARP and ICMP sweeps.
|
|
9
|
+
- **ARP Sweeps:** Uses `libpcap` to broadcast ARP requests rapidly across the local link.
|
|
10
|
+
- **ICMP Sweeps:** Uses raw sockets to ping targets.
|
|
11
|
+
|
|
12
|
+
### 2. The Python Engine (`netpulse.discovery.engine`)
|
|
13
|
+
This layer acts as the FFI (Foreign Function Interface) boundary. It imports the compiled Rust extension and exposes it to the higher-level Python services.
|
|
14
|
+
|
|
15
|
+
### 3. Asynchronous Services (`netpulse.discovery.services`)
|
|
16
|
+
- **DiscoveryService**: The primary orchestrator. It executes the Rust engine via `asyncio.to_thread` to ensure that Python's async event loop is never blocked by raw socket waits. It then enriches the raw results with MAC vendor lookups and port scans.
|
|
17
|
+
- **PortScannerService**: A purely native-Python TCP connect scanner using `asyncio` streams.
|
|
18
|
+
- **DriftService**: Implements state-aware logic to compare current discovery results against historical baselines, allowing for accurate mapping of network topological changes over time.
|
|
19
|
+
|
|
20
|
+
### 4. Pydantic Domain Models (`netpulse.discovery.models`)
|
|
21
|
+
The data contract layer. Raw byte responses from the network are parsed, validated, and normalized into `DiscoveryResult` and `Device` models before being returned to the user or downstream consumers.
|
|
Binary file
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Contributing to NetPulse Discovery
|
|
2
|
+
|
|
3
|
+
Welcome! We appreciate your interest in contributing to the NetPulse suite. Because `netpulse-discovery` involves low-level network system permissions and bindings to a Rust engine, developing for it requires a specific local environment configuration.
|
|
4
|
+
|
|
5
|
+
## Environment Setup
|
|
6
|
+
|
|
7
|
+
1. **Workspace Root:** Ensure you are developing from the root `netpulse` workspace.
|
|
8
|
+
2. **Install Dependencies:**
|
|
9
|
+
```bash
|
|
10
|
+
uv sync
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Running Tests
|
|
14
|
+
|
|
15
|
+
To avoid the need for `sudo` and raw socket capabilities during standard development testing, always use the `NETPULSE_MOCK` environment variable. This instructs the Rust engine and Python services to mock network traffic.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Run unit and integration tests specific to the discovery module
|
|
19
|
+
NETPULSE_MOCK=1 uv run pytest packages/netpulse-discovery/tests/
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Modifying the Rust Engine
|
|
23
|
+
|
|
24
|
+
If you need to make modifications to the low-level Rust ARP/ICMP packet generation:
|
|
25
|
+
1. Navigate to the `rust/` directory at the workspace root.
|
|
26
|
+
2. Make your modifications to the `.rs` files.
|
|
27
|
+
3. Because `netpulse-discovery` relies on `netpulse-rust` via `uv` workspace resolution, the Rust binary will automatically be recompiled using PyO3 and Maturin the next time you run `uv sync` or invoke the python environment.
|
|
28
|
+
|
|
29
|
+
## Code Standards
|
|
30
|
+
- All Python code must be typed using standard `typing` modules or Pydantic.
|
|
31
|
+
- Run `uv run ruff check` before submitting a pull request.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Usage Guide
|
|
2
|
+
|
|
3
|
+
`netpulse-discovery` can be used via its standalone CLI, its standalone REST API, or programmatically as a Python library.
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> Because `netpulse-discovery` utilizes raw sockets to generate ICMP and ARP packets, the tool must either be run with elevated privileges (e.g., `sudo`) or run in Mock Mode.
|
|
7
|
+
|
|
8
|
+
## Mock Mode (Development)
|
|
9
|
+
If you do not have root access or want to test safely, enable mock mode:
|
|
10
|
+
```bash
|
|
11
|
+
export NETPULSE_MOCK=1
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 1. Standalone CLI
|
|
15
|
+
Run a basic network scan directly from the terminal:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Scan a local /24 subnet
|
|
19
|
+
sudo uv run netpulse-discovery scan 192.168.1.0/24 --timeout 500
|
|
20
|
+
```
|
|
21
|
+
This returns a raw, structured JSON output containing the discovered devices, their MAC addresses, vendors, and RTT (Round Trip Time).
|
|
22
|
+
|
|
23
|
+
## 2. Python Library
|
|
24
|
+
Import the services to embed discovery directly into your application:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import asyncio
|
|
28
|
+
from netpulse.discovery.services.discovery import DiscoveryService
|
|
29
|
+
from netpulse.discovery.models.discovery import DiscoveryMethod
|
|
30
|
+
|
|
31
|
+
async def main():
|
|
32
|
+
service = DiscoveryService()
|
|
33
|
+
result = await service.discover_network(
|
|
34
|
+
"10.0.0.0/24",
|
|
35
|
+
methods=[DiscoveryMethod.ARP]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
for device in result.devices:
|
|
39
|
+
print(f"Found {device.ip} ({device.mac}) - Vendor: {device.vendor}")
|
|
40
|
+
|
|
41
|
+
asyncio.run(main())
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 3. Standalone API
|
|
45
|
+
To run the decoupled REST API for microservice integration:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Mount the router to a FastAPI app and run it with uvicorn
|
|
49
|
+
sudo uv run uvicorn netpulse.discovery.api:discovery_router
|
|
50
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "netpulse-discovery"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "High-speed network discovery and drift analysis engine"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"typer>=0.9.0",
|
|
13
|
+
"rich>=13.0.0",
|
|
14
|
+
"pydantic>=2.0.0",
|
|
15
|
+
"fastapi>=0.100.0",
|
|
16
|
+
"uvicorn>=0.22.0",
|
|
17
|
+
"netpulse-rust"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
netpulse-discovery = "netpulse.discovery.cli:app"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/netpulse"]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
pythonpath = ["src"]
|
|
29
|
+
asyncio_mode = "auto"
|
|
30
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
31
|
+
|
|
32
|
+
[tool.uv.sources]
|
|
33
|
+
netpulse-rust = { workspace = true }
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from fastapi import APIRouter, HTTPException, status
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from netpulse.discovery.services.discovery import DiscoveryService
|
|
6
|
+
from netpulse.discovery.models.discovery import DiscoveryResult, DiscoveryMethod
|
|
7
|
+
|
|
8
|
+
discovery_router = APIRouter(prefix="/discovery", tags=["Discovery"])
|
|
9
|
+
|
|
10
|
+
class ScanRequest(BaseModel):
|
|
11
|
+
target: str
|
|
12
|
+
timeout_ms: int = 1000
|
|
13
|
+
|
|
14
|
+
@discovery_router.post("/scan", response_model=DiscoveryResult)
|
|
15
|
+
async def scan_network(req: ScanRequest):
|
|
16
|
+
service = DiscoveryService()
|
|
17
|
+
try:
|
|
18
|
+
return await service.discover_network(req.target, [DiscoveryMethod.ARP], req.timeout_ms)
|
|
19
|
+
except Exception as e:
|
|
20
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from netpulse.discovery.services.discovery import DiscoveryService
|
|
8
|
+
from netpulse.discovery.models.discovery import DiscoveryMethod
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(name="discovery", help="Standalone network discovery and drift engine.")
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
@app.command()
|
|
14
|
+
def scan(
|
|
15
|
+
target: str = typer.Argument(..., help="Target CIDR network"),
|
|
16
|
+
timeout: int = typer.Option(1000, "--timeout", "-t"),
|
|
17
|
+
):
|
|
18
|
+
"""Stateless network scan returning raw output."""
|
|
19
|
+
with console.status(f"Scanning {target}..."):
|
|
20
|
+
service = DiscoveryService()
|
|
21
|
+
result = asyncio.run(service.discover_network(target, [DiscoveryMethod.ARP], timeout_ms=timeout))
|
|
22
|
+
|
|
23
|
+
console.print(result.model_dump_json(indent=2))
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
app()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
|
|
5
|
+
# Support forced mock mode via environment variable
|
|
6
|
+
_force_mock = os.environ.get("NETPULSE_MOCK") == "1"
|
|
7
|
+
|
|
8
|
+
# This will be imported from the compiled Rust extension
|
|
9
|
+
try:
|
|
10
|
+
if _force_mock:
|
|
11
|
+
netpulse_rust = None
|
|
12
|
+
logging.info("NetPulse engine running in FORCED MOCK mode via environment variable.")
|
|
13
|
+
else:
|
|
14
|
+
import netpulse_rust
|
|
15
|
+
except ImportError:
|
|
16
|
+
# Fallback for development/testing without compiled binary
|
|
17
|
+
netpulse_rust = None
|
|
18
|
+
logging.warning("netpulse_rust module not found. Engine is running in mock mode.")
|
|
19
|
+
|
|
20
|
+
def scan_arp(target: str, timeout_ms: int = 1000, interface: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
21
|
+
"""
|
|
22
|
+
Exposes the Rust ARP scanning capability to the Core layer.
|
|
23
|
+
"""
|
|
24
|
+
if netpulse_rust:
|
|
25
|
+
return netpulse_rust.scan_arp(target, timeout_ms, interface)
|
|
26
|
+
|
|
27
|
+
# Mock fallback for environment without Rust compiled or forced mock mode
|
|
28
|
+
return [
|
|
29
|
+
{"ip": "172.19.57.1", "mac": "00:50:56:C0:00:01", "rtt_ms": 0.35, "status": "up"},
|
|
30
|
+
{"ip": "172.19.57.10", "mac": "00:0C:29:A4:B5:C6", "rtt_ms": 1.24, "status": "up"},
|
|
31
|
+
{"ip": "172.19.57.119", "mac": "00:0C:29:8F:D2:E4", "rtt_ms": 0.08, "status": "up"},
|
|
32
|
+
{"ip": "172.19.57.150", "mac": "00:50:56:E8:A1:B2", "rtt_ms": 4.52, "status": "up"},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
def scan_icmp(target: str, timeout_ms: int = 1000, concurrency: int = 100) -> List[Dict[str, Any]]:
|
|
36
|
+
"""
|
|
37
|
+
Exposes the Rust ICMP scanning capability to the Core layer.
|
|
38
|
+
"""
|
|
39
|
+
if netpulse_rust:
|
|
40
|
+
return netpulse_rust.scan_icmp(target, timeout_ms, concurrency)
|
|
41
|
+
|
|
42
|
+
# Mock fallback for environment without Rust compiled or forced mock mode
|
|
43
|
+
return [
|
|
44
|
+
{"ip": "172.19.57.1", "mac": None, "rtt_ms": 0.42, "status": "up"},
|
|
45
|
+
{"ip": "172.19.57.10", "mac": None, "rtt_ms": 1.58, "status": "up"},
|
|
46
|
+
{"ip": "172.19.57.119", "mac": None, "rtt_ms": 0.12, "status": "up"},
|
|
47
|
+
{"ip": "172.19.57.150", "mac": None, "rtt_ms": 5.11, "status": "up"},
|
|
48
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pydantic import BaseModel, Field, IPvAnyAddress, ConfigDict
|
|
6
|
+
|
|
7
|
+
class DeviceStatus(str, Enum):
|
|
8
|
+
"""Enumeration of possible device reachability states."""
|
|
9
|
+
UP = "up"
|
|
10
|
+
DOWN = "down"
|
|
11
|
+
UNKNOWN = "unknown"
|
|
12
|
+
|
|
13
|
+
class Device(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
The Core data model for a network device in the NetPulse ecosystem.
|
|
16
|
+
|
|
17
|
+
This model is designed to be:
|
|
18
|
+
1. Strongly typed (via Pydantic V2)
|
|
19
|
+
2. Extensible (via the metadata dictionary)
|
|
20
|
+
3. Persistence-ready (with UUID and UTC timestamps)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(
|
|
24
|
+
use_enum_values=True,
|
|
25
|
+
validate_assignment=True,
|
|
26
|
+
populate_by_name=True,
|
|
27
|
+
arbitrary_types_allowed=True,
|
|
28
|
+
json_schema_extra={
|
|
29
|
+
"example": {
|
|
30
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
31
|
+
"ip": "192.168.1.1",
|
|
32
|
+
"mac": "00:11:22:33:44:55",
|
|
33
|
+
"hostname": "router.local",
|
|
34
|
+
"status": "up",
|
|
35
|
+
"rtt_ms": 1.5,
|
|
36
|
+
"metadata": {"vendor": "Cisco", "os": "IOS"}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Identification Fields
|
|
42
|
+
id: uuid.UUID = Field(
|
|
43
|
+
default_factory=uuid.uuid4,
|
|
44
|
+
description="Unique identifier for the device (UUID v4)."
|
|
45
|
+
)
|
|
46
|
+
ip: IPvAnyAddress = Field(
|
|
47
|
+
...,
|
|
48
|
+
description="The IP address of the device (IPv4 or IPv6)."
|
|
49
|
+
)
|
|
50
|
+
mac: Optional[str] = Field(
|
|
51
|
+
None,
|
|
52
|
+
pattern=r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$",
|
|
53
|
+
description="The hardware MAC address of the device."
|
|
54
|
+
)
|
|
55
|
+
hostname: Optional[str] = Field(
|
|
56
|
+
None,
|
|
57
|
+
description="The resolved hostname or mDNS name of the device."
|
|
58
|
+
)
|
|
59
|
+
vendor: Optional[str] = Field(
|
|
60
|
+
None,
|
|
61
|
+
description="The manufacturer/vendor name derived from the MAC OUI."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Discovery & State Fields
|
|
65
|
+
status: DeviceStatus = Field(
|
|
66
|
+
default=DeviceStatus.UNKNOWN,
|
|
67
|
+
description="The current reachability status of the device."
|
|
68
|
+
)
|
|
69
|
+
rtt_ms: Optional[float] = Field(
|
|
70
|
+
None,
|
|
71
|
+
description="The round-trip time in milliseconds recorded during discovery."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Audit & Lifecycle Fields
|
|
75
|
+
created_at: datetime = Field(
|
|
76
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
77
|
+
description="Timestamp when the device record was first created (UTC)."
|
|
78
|
+
)
|
|
79
|
+
last_seen: datetime = Field(
|
|
80
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
81
|
+
description="Timestamp when the device was last observed (UTC)."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Extensibility
|
|
85
|
+
metadata: Dict[str, Any] = Field(
|
|
86
|
+
default_factory=dict,
|
|
87
|
+
description="A dictionary for plugin-specific data (e.g., SNMP, SSH, OS details)."
|
|
88
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pydantic import BaseModel, Field, IPvAnyNetwork, ConfigDict
|
|
6
|
+
|
|
7
|
+
from .device import Device
|
|
8
|
+
|
|
9
|
+
class DiscoveryMethod(str, Enum):
|
|
10
|
+
"""Protocol methods used for network discovery."""
|
|
11
|
+
ARP = "arp"
|
|
12
|
+
ICMP = "icmp"
|
|
13
|
+
UDP = "udp"
|
|
14
|
+
TCP = "tcp"
|
|
15
|
+
|
|
16
|
+
class DiscoveryResult(BaseModel):
|
|
17
|
+
"""
|
|
18
|
+
Schema representing the complete result of a network discovery operation.
|
|
19
|
+
|
|
20
|
+
Refined based on data consistency and observability requirements.
|
|
21
|
+
"""
|
|
22
|
+
model_config = ConfigDict(
|
|
23
|
+
use_enum_values=True,
|
|
24
|
+
validate_assignment=True,
|
|
25
|
+
populate_by_name=True,
|
|
26
|
+
arbitrary_types_allowed=True
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Identification & Scope
|
|
30
|
+
id: uuid.UUID = Field(
|
|
31
|
+
default_factory=uuid.uuid4,
|
|
32
|
+
description="Unique identifier for the scan session."
|
|
33
|
+
)
|
|
34
|
+
network: IPvAnyNetwork = Field(
|
|
35
|
+
...,
|
|
36
|
+
description="The CIDR network range targeted by the scan."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Execution Metadata
|
|
40
|
+
methods: List[DiscoveryMethod] = Field(
|
|
41
|
+
default_factory=list,
|
|
42
|
+
description="The discovery protocols used during this session."
|
|
43
|
+
)
|
|
44
|
+
status: str = Field(
|
|
45
|
+
default="completed",
|
|
46
|
+
description="Execution status of the scan (completed, partial, failed)."
|
|
47
|
+
)
|
|
48
|
+
errors: List[str] = Field(
|
|
49
|
+
default_factory=list,
|
|
50
|
+
description="List of error messages encountered during scanning."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Data
|
|
54
|
+
devices: List[Device] = Field(
|
|
55
|
+
default_factory=list,
|
|
56
|
+
description="The list of devices discovered during the scan."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Timing (UTC)
|
|
60
|
+
started_at: datetime = Field(
|
|
61
|
+
...,
|
|
62
|
+
description="The timestamp when the scan was initiated."
|
|
63
|
+
)
|
|
64
|
+
finished_at: datetime = Field(
|
|
65
|
+
...,
|
|
66
|
+
description="The timestamp when the scan was completed."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Analytics & Future Extensibility
|
|
70
|
+
stats: Dict[str, Any] = Field(
|
|
71
|
+
default_factory=dict,
|
|
72
|
+
description="Aggregated statistics (e.g., hosts scanned, response rate)."
|
|
73
|
+
)
|
|
74
|
+
metadata: Dict[str, Any] = Field(
|
|
75
|
+
default_factory=dict,
|
|
76
|
+
description="Additional custom data for plugins or monitoring."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def duration_s(self) -> float:
|
|
81
|
+
"""Calculates scan duration in seconds."""
|
|
82
|
+
return (self.finished_at - self.started_at).total_seconds()
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def total_discovered(self) -> int:
|
|
86
|
+
"""Derived property ensuring data consistency for discovered 'up' devices."""
|
|
87
|
+
return len([d for d in self.devices if d.status == "up"])
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from netpulse.discovery.models.device import Device
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DeviceChange(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
Detailed shifts for a single host matching across both benchmark and target sweeps.
|
|
11
|
+
"""
|
|
12
|
+
ip: str = Field(..., description="The IP address of the modified host.")
|
|
13
|
+
mac_old: Optional[str] = Field(None, description="The old hardware MAC address.")
|
|
14
|
+
mac_new: Optional[str] = Field(None, description="The new hardware MAC address.")
|
|
15
|
+
rtt_old: Optional[float] = Field(None, description="The old response time in milliseconds.")
|
|
16
|
+
rtt_new: Optional[float] = Field(None, description="The new response time in milliseconds.")
|
|
17
|
+
status_old: str = Field(..., description="The previous reachability status.")
|
|
18
|
+
status_new: str = Field(..., description="The updated reachability status.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DriftResult(BaseModel):
|
|
22
|
+
"""
|
|
23
|
+
Aggregated comparison boundaries across two historical scans.
|
|
24
|
+
"""
|
|
25
|
+
network: str = Field(..., description="The targeted network CIDR block.")
|
|
26
|
+
old_scan_id: Optional[uuid.UUID] = Field(None, description="UUID of the old benchmark scan.")
|
|
27
|
+
new_scan_id: uuid.UUID = Field(..., description="UUID of the new comparison scan.")
|
|
28
|
+
old_timestamp: Optional[str] = Field(None, description="UTC ISO timestamp of the old scan.")
|
|
29
|
+
new_timestamp: str = Field(..., description="UTC ISO timestamp of the new scan.")
|
|
30
|
+
|
|
31
|
+
joined: List[Device] = Field(default_factory=list, description="Newly discovered hosts.")
|
|
32
|
+
left: List[Device] = Field(default_factory=list, description="Hosts that went offline or vanished.")
|
|
33
|
+
modified: List[DeviceChange] = Field(default_factory=list, description="Hosts with shifts in MAC or RTT parameters.")
|
|
34
|
+
unchanged: List[Device] = Field(default_factory=list, description="Hosts present in both scans without state updates.")
|
|
File without changes
|