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.
Files changed (28) hide show
  1. netpulse_discovery-0.1.0/.gitignore +29 -0
  2. netpulse_discovery-0.1.0/PKG-INFO +81 -0
  3. netpulse_discovery-0.1.0/README.md +68 -0
  4. netpulse_discovery-0.1.0/docs/architecture.md +21 -0
  5. netpulse_discovery-0.1.0/docs/assets/logo.png +0 -0
  6. netpulse_discovery-0.1.0/docs/contributing.md +31 -0
  7. netpulse_discovery-0.1.0/docs/usage.md +50 -0
  8. netpulse_discovery-0.1.0/pyproject.toml +33 -0
  9. netpulse_discovery-0.1.0/src/netpulse/discovery/__init__.py +0 -0
  10. netpulse_discovery-0.1.0/src/netpulse/discovery/api.py +20 -0
  11. netpulse_discovery-0.1.0/src/netpulse/discovery/cli.py +26 -0
  12. netpulse_discovery-0.1.0/src/netpulse/discovery/engine/__init__.py +48 -0
  13. netpulse_discovery-0.1.0/src/netpulse/discovery/models/__init__.py +0 -0
  14. netpulse_discovery-0.1.0/src/netpulse/discovery/models/device.py +88 -0
  15. netpulse_discovery-0.1.0/src/netpulse/discovery/models/discovery.py +87 -0
  16. netpulse_discovery-0.1.0/src/netpulse/discovery/models/drift.py +34 -0
  17. netpulse_discovery-0.1.0/src/netpulse/discovery/services/__init__.py +0 -0
  18. netpulse_discovery-0.1.0/src/netpulse/discovery/services/discovery.py +161 -0
  19. netpulse_discovery-0.1.0/src/netpulse/discovery/services/drift.py +90 -0
  20. netpulse_discovery-0.1.0/src/netpulse/discovery/services/mac_lookup.py +212 -0
  21. netpulse_discovery-0.1.0/src/netpulse/discovery/services/port_scanner.py +105 -0
  22. netpulse_discovery-0.1.0/tests/integration/test_api.py +29 -0
  23. netpulse_discovery-0.1.0/tests/integration/test_cli.py +18 -0
  24. netpulse_discovery-0.1.0/tests/unit/test_discovery_service.py +123 -0
  25. netpulse_discovery-0.1.0/tests/unit/test_drift.py +114 -0
  26. netpulse_discovery-0.1.0/tests/unit/test_engine.py +34 -0
  27. netpulse_discovery-0.1.0/tests/unit/test_mac_lookup.py +131 -0
  28. 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
+ [![Python](https://img.shields.io/badge/Python-3.10%2B-blue?logo=python&logoColor=white)](https://python.org)
21
+ [![Rust](https://img.shields.io/badge/Powered%20by-Rust-orange?logo=rust&logoColor=white)](https://rust-lang.org)
22
+ [![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen?logo=github)](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
+ [![Python](https://img.shields.io/badge/Python-3.10%2B-blue?logo=python&logoColor=white)](https://python.org)
8
+ [![Rust](https://img.shields.io/badge/Powered%20by-Rust-orange?logo=rust&logoColor=white)](https://rust-lang.org)
9
+ [![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen?logo=github)](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.
@@ -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 }
@@ -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
+ ]
@@ -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.")