woninet 2.0.1__py3-none-any.whl
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.
- woninet/__init__.py +2 -0
- woninet/__main__.py +4 -0
- woninet/core/__init__.py +2 -0
- woninet/core/alerts.py +251 -0
- woninet/core/collectors.py +303 -0
- woninet/core/engine.py +353 -0
- woninet/core/graph.py +63 -0
- woninet/core/manual_ip_enumerator.py +33 -0
- woninet/core/models.py +124 -0
- woninet/core/storage.py +163 -0
- woninet/core/subnet_enumerator.py +32 -0
- woninet/database/__init__.py +2 -0
- woninet/database/base.py +9 -0
- woninet/database/engine.py +44 -0
- woninet/database/repositories/__init__.py +2 -0
- woninet/database/repositories/alert_event_repository.py +69 -0
- woninet/database/repositories/alert_state_repository.py +81 -0
- woninet/database/repositories/device_repository.py +105 -0
- woninet/database/repositories/metric_repository.py +75 -0
- woninet/database/tables.py +89 -0
- woninet/logging.yaml +49 -0
- woninet/main.py +225 -0
- woninet/server/__init__.py +2 -0
- woninet/server/app.py +101 -0
- woninet/server/dependencies.py +25 -0
- woninet/server/routes/__init__.py +2 -0
- woninet/server/routes/api/__init__.py +0 -0
- woninet/server/routes/api/devices.py +43 -0
- woninet/server/routes/api/stats.py +34 -0
- woninet/server/routes/page/__init__.py +0 -0
- woninet/server/routes/page/devices.py +28 -0
- woninet/server/static/css/404.css +164 -0
- woninet/server/static/css/style.css +368 -0
- woninet/server/static/js/app.js +242 -0
- woninet/server/static/js/device_info.js +95 -0
- woninet/server/static/swagger-ui/favicon-32x32.png +0 -0
- woninet/server/static/swagger-ui/swagger-ui-bundle.js +2 -0
- woninet/server/static/swagger-ui/swagger-ui.css +3 -0
- woninet/server/templates/404.html +76 -0
- woninet/server/templates/dashboard.html +118 -0
- woninet/server/templates/device_info.html +93 -0
- woninet/utilities/__init__.py +2 -0
- woninet/utilities/arguments.py +94 -0
- woninet/utilities/cli_parser.py +90 -0
- woninet/utilities/detect_ip_range.py +21 -0
- woninet/utilities/ip_validator.py +39 -0
- woninet/utilities/logger.py +97 -0
- woninet-2.0.1.dist-info/METADATA +243 -0
- woninet-2.0.1.dist-info/RECORD +53 -0
- woninet-2.0.1.dist-info/WHEEL +5 -0
- woninet-2.0.1.dist-info/entry_points.txt +2 -0
- woninet-2.0.1.dist-info/licenses/LICENSE +21 -0
- woninet-2.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Device Info</title>
|
|
7
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
8
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css">
|
|
9
|
+
<link rel="stylesheet" href="/static/css/style.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div class="dashboard">
|
|
13
|
+
<header class="topbar">
|
|
14
|
+
<div>
|
|
15
|
+
<p class="eyebrow">Device Details</p>
|
|
16
|
+
<h1 id="device-ip"></h1>
|
|
17
|
+
<p class="subtitle">Live monitoring and alert history</p>
|
|
18
|
+
</div>
|
|
19
|
+
</header>
|
|
20
|
+
|
|
21
|
+
<section class="overview-grid">
|
|
22
|
+
<div class="kpi-card">
|
|
23
|
+
<div class="kpi-icon primary">
|
|
24
|
+
<i class="fa-solid fa-network-wired"></i>
|
|
25
|
+
<span>MAC Address</span>
|
|
26
|
+
</div>
|
|
27
|
+
<h2 style="font-size: 1rem;" id="device-mac"></h2>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="kpi-card">
|
|
30
|
+
<div class="kpi-icon primary">
|
|
31
|
+
<i class="fa-solid fa-wave-square"></i>
|
|
32
|
+
<span>Latency</span>
|
|
33
|
+
</div>
|
|
34
|
+
<h2 id="device-latency"></h2>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="kpi-card">
|
|
37
|
+
<div class="kpi-icon warning">
|
|
38
|
+
<i class="fa-solid fa-triangle-exclamation"></i>
|
|
39
|
+
<span>Packet Loss</span>
|
|
40
|
+
</div>
|
|
41
|
+
<h2 id="device-packet-loss"></h2>
|
|
42
|
+
</div>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<main class="main-layout">
|
|
46
|
+
<section class="panel">
|
|
47
|
+
<div class="panel-header">
|
|
48
|
+
<h2>Device Details</h2>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="device-grid">
|
|
51
|
+
<article class="device-card" id="device-card">
|
|
52
|
+
<div class="device-header">
|
|
53
|
+
<div class="device-status" id="device-status"></div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="metrics">
|
|
57
|
+
<div class="metric">
|
|
58
|
+
<div class="metric-label">Last Seen</div>
|
|
59
|
+
<div class="metric-value" id="device-last-seen"></div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="metric">
|
|
62
|
+
<div class="metric-label">Latency State</div>
|
|
63
|
+
<div class="metric-value" id="alert-state-latency"></div>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="metric">
|
|
66
|
+
<div class="metric-label">Packet Loss State</div>
|
|
67
|
+
<div class="metric-value" id="alert-state-packet-loss">}</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</article>
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
|
|
74
|
+
<aside class="events-panel">
|
|
75
|
+
<h3>Alert Events</h3>
|
|
76
|
+
<div id="events"></div>
|
|
77
|
+
</aside>
|
|
78
|
+
</main>
|
|
79
|
+
|
|
80
|
+
<br />
|
|
81
|
+
<section class="panel">
|
|
82
|
+
<h2>Recent Device Events Graph</h2>
|
|
83
|
+
<img width="100%" src="placeholder" alt="Device recent events line graph" id="device-recent-events-line-graph"/>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<div style="margin-top: 20px;">
|
|
87
|
+
<a href="/" class="back-link">Back to Dashboard</a>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<script src="/static/js/device_info.js"></script>
|
|
92
|
+
</body>
|
|
93
|
+
</html>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from woninet.utilities.cli_parser import list_devices, show_device_info, report_stats
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def args() -> argparse.Namespace:
|
|
6
|
+
"""
|
|
7
|
+
The argument handler.
|
|
8
|
+
|
|
9
|
+
check `woninet -h` for more info.
|
|
10
|
+
"""
|
|
11
|
+
description = """
|
|
12
|
+
A local network monitoring system. It enumerates all candidate IP addresses in your /24 subnet
|
|
13
|
+
and performs ARP + ICMP host detection to determine existence, reachability, and latency. Metrics
|
|
14
|
+
are collected continuously and processed through an alert engine."""
|
|
15
|
+
|
|
16
|
+
epilog = ""
|
|
17
|
+
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
description=description,
|
|
20
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
21
|
+
prog="woninet",
|
|
22
|
+
epilog=epilog,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
26
|
+
|
|
27
|
+
device_parser = subparsers.add_parser(
|
|
28
|
+
"device", help="Fetch Device informations from the database."
|
|
29
|
+
)
|
|
30
|
+
device_subparsers = device_parser.add_subparsers(dest="action", required=True)
|
|
31
|
+
|
|
32
|
+
device_list = device_subparsers.add_parser(
|
|
33
|
+
"list", description="List all devices.", help="List all devices."
|
|
34
|
+
)
|
|
35
|
+
device_list.set_defaults(func=list_devices)
|
|
36
|
+
|
|
37
|
+
device_show = device_subparsers.add_parser(
|
|
38
|
+
"show", description="Show device info for a given IP.", help="Show device info."
|
|
39
|
+
)
|
|
40
|
+
device_show.add_argument("ip", help="Device IP address")
|
|
41
|
+
device_show.set_defaults(func=show_device_info)
|
|
42
|
+
|
|
43
|
+
stats_parser = subparsers.add_parser(
|
|
44
|
+
"stats", help="Output a JSON report for the woninet statistics."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
stats_parser.add_argument(
|
|
48
|
+
"--output",
|
|
49
|
+
default="stats.json",
|
|
50
|
+
help="Output path for the JSON report for the woninet statistics (default: %(default)s).",
|
|
51
|
+
)
|
|
52
|
+
stats_parser.set_defaults(func=report_stats)
|
|
53
|
+
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--cli",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Run woninet in CLI mode.",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"-p",
|
|
62
|
+
"--port",
|
|
63
|
+
type=int,
|
|
64
|
+
default=8080,
|
|
65
|
+
help="Port number to serve the web dashboard (defaut: %(default)s).",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--logs",
|
|
70
|
+
type=str,
|
|
71
|
+
help="Output path for storing logs.",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--color",
|
|
76
|
+
type=str,
|
|
77
|
+
choices=["true", "false"],
|
|
78
|
+
default="true",
|
|
79
|
+
help="Whether to enable colored console log output (default: %(default)s)",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"-v",
|
|
84
|
+
"--verbose",
|
|
85
|
+
action="count",
|
|
86
|
+
default=0,
|
|
87
|
+
help="Increase verbosity (repeat for more detail)",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"-V", "--version", action="store_true", help="Prints the version and quit."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return parser.parse_args()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
from woninet.main import NetworkMonitorCore
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def list_devices(args):
|
|
7
|
+
"""
|
|
8
|
+
Prints list of recorded devices information from the database to stdout.
|
|
9
|
+
Following entries are shown:
|
|
10
|
+
1. Device IP address.
|
|
11
|
+
2. Device MAC address.
|
|
12
|
+
3. Device last seen timestamp.
|
|
13
|
+
"""
|
|
14
|
+
monitor: NetworkMonitorCore = args.monitor
|
|
15
|
+
devices = monitor.get_device_history()
|
|
16
|
+
if len(devices) == 0:
|
|
17
|
+
print("No recorded device was found.")
|
|
18
|
+
else:
|
|
19
|
+
for device in devices:
|
|
20
|
+
row = f"Device {device.ip}: MAC={device.mac}, Last seen={device.last_seen}"
|
|
21
|
+
print(row)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def show_device_info(args):
|
|
25
|
+
"""
|
|
26
|
+
Prints information for a given IP address. Following entries are shown:
|
|
27
|
+
1. Device IP address.
|
|
28
|
+
2. Device MAC address.
|
|
29
|
+
3. Device last captured latency.
|
|
30
|
+
4. Device last seen timestamp.
|
|
31
|
+
5. Last alert state of device.
|
|
32
|
+
6. Last 10 alert events of device.
|
|
33
|
+
"""
|
|
34
|
+
monitor: NetworkMonitorCore = args.monitor
|
|
35
|
+
device, devices_state, recent_device_alert_events = monitor.get_device_info(args.ip)
|
|
36
|
+
if device is None:
|
|
37
|
+
print(f"Device {args.ip} was not found.")
|
|
38
|
+
else:
|
|
39
|
+
latency = device.latency
|
|
40
|
+
if latency == 0:
|
|
41
|
+
latency = "OFFLINE"
|
|
42
|
+
print(
|
|
43
|
+
f"Found device {device.ip}: MAC={device.mac}, Last latency: {latency}, Last seen={device.last_seen}"
|
|
44
|
+
)
|
|
45
|
+
for metric, state in devices_state.items():
|
|
46
|
+
print(f"Current alert state for {metric}: state={state}")
|
|
47
|
+
print("Recent alert events:")
|
|
48
|
+
for event in recent_device_alert_events:
|
|
49
|
+
content = f"ID: {event.id} - Metric: {event.metric},\t Value={event.value},"
|
|
50
|
+
content += (
|
|
51
|
+
f"\t Event type={event.event_type},\t Timestamp={event.timestamp}"
|
|
52
|
+
)
|
|
53
|
+
print(content)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def report_stats(args) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Output statistics report into given file.
|
|
59
|
+
following statistics are provided:
|
|
60
|
+
1. devices count.
|
|
61
|
+
2. devices list.
|
|
62
|
+
3. recent alert events.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
monitor: NetworkMonitorCore = args.monitor
|
|
66
|
+
devices_count = monitor.count_resources()[0]
|
|
67
|
+
devices = monitor.get_device_history()
|
|
68
|
+
output: str = args.output
|
|
69
|
+
result: dict[str, Any] = {}
|
|
70
|
+
|
|
71
|
+
result["devices_count"] = devices_count
|
|
72
|
+
result["devices"] = [
|
|
73
|
+
{
|
|
74
|
+
"ip": dev.ip,
|
|
75
|
+
"mac": dev.mac,
|
|
76
|
+
"last_seen": dev.last_seen.isoformat(),
|
|
77
|
+
"alert_state": monitor.storage.get_device_alert_state(ip=dev.ip),
|
|
78
|
+
}
|
|
79
|
+
for dev in devices
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
result["recent_alert_events"] = monitor.classify_recent_alert_events()
|
|
83
|
+
|
|
84
|
+
with open(output, "w") as stats:
|
|
85
|
+
json.dump(result, stats, indent=2)
|
|
86
|
+
|
|
87
|
+
print(f"Statistics report saved to {output}")
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print(e)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
def detect_ip_range(ip: str) -> bool:
|
|
2
|
+
"""
|
|
3
|
+
Detect if an IP address is provided as a range.
|
|
4
|
+
(e.g., `192.168.10.10-20`)
|
|
5
|
+
|
|
6
|
+
Args:
|
|
7
|
+
ip (str): IP address to detect.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
bool: Boolean indicating if IP address is provided as a range.
|
|
11
|
+
|
|
12
|
+
Raises:
|
|
13
|
+
ValueError: If the range notation is used in any of the first three octets.\n
|
|
14
|
+
(e.g., `192.168.1-10.10` - `192.168-188.1.10` - `192.180.168.1.10`)
|
|
15
|
+
"""
|
|
16
|
+
base, last_octet = ip.rsplit(".", maxsplit=1)
|
|
17
|
+
if "-" in base:
|
|
18
|
+
raise ValueError("Ip addess range is only allowed in the last octet.")
|
|
19
|
+
if "-" in last_octet:
|
|
20
|
+
return True
|
|
21
|
+
return False
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from ipaddress import ip_address
|
|
2
|
+
from woninet.core.models import Device
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def is_ip_list_valid(ip_list: list[str]) -> bool:
|
|
6
|
+
"""
|
|
7
|
+
Validate a list of IP addresses.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
ip_list (list[str]): IP addresses to validate.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
bool: Boolean indicating if IP addresses are valid.
|
|
14
|
+
"""
|
|
15
|
+
for ip in ip_list:
|
|
16
|
+
try:
|
|
17
|
+
ip_address(ip)
|
|
18
|
+
except ValueError:
|
|
19
|
+
return False
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_device_ip_valid(devices: dict[str, Device]) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Validate the dictionary of candidate devices.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
devices (dict[str,Device]): Dictrionary of IP addresses and Device model
|
|
29
|
+
instances to validate.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
bool: Boolean indicating if IP addresses are valid.
|
|
33
|
+
"""
|
|
34
|
+
for ip, dev in devices.items():
|
|
35
|
+
try:
|
|
36
|
+
ip_address(ip)
|
|
37
|
+
except ValueError:
|
|
38
|
+
return False
|
|
39
|
+
return True
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from sys import stdout
|
|
3
|
+
|
|
4
|
+
# Trace level definition
|
|
5
|
+
TRACE_LEVEL = 5
|
|
6
|
+
logging.addLevelName(TRACE_LEVEL, "TRACE")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def trace(self, msg, *args, **kwargs) -> None:
|
|
10
|
+
if self.isEnabledFor(TRACE_LEVEL):
|
|
11
|
+
self._log(TRACE_LEVEL, msg, args, **kwargs)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logging.Logger.trace = trace
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ColorFormatter(logging.Formatter):
|
|
18
|
+
"""
|
|
19
|
+
Applies ANSI color codes to log level names.
|
|
20
|
+
Colors are applied only when the output stream is an
|
|
21
|
+
interactive terminal.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
COLORS = {
|
|
25
|
+
TRACE_LEVEL: "\033[34m",
|
|
26
|
+
logging.DEBUG: "\033[36m",
|
|
27
|
+
logging.INFO: "\033[32m",
|
|
28
|
+
logging.WARNING: "\033[33m",
|
|
29
|
+
logging.ERROR: "\033[31m",
|
|
30
|
+
logging.CRITICAL: "\033[1;31m",
|
|
31
|
+
}
|
|
32
|
+
RESET = "\033[0m"
|
|
33
|
+
|
|
34
|
+
def formatMessage(self, record) -> str:
|
|
35
|
+
# Only colorize output if writing to an interactive terminal
|
|
36
|
+
if stdout.isatty():
|
|
37
|
+
color = self.COLORS.get(record.levelno, "")
|
|
38
|
+
|
|
39
|
+
# Apply ANSI color escape codes to the level name
|
|
40
|
+
level_name = record.levelname
|
|
41
|
+
colored = f"{color}{level_name}{self.RESET}"
|
|
42
|
+
|
|
43
|
+
# Clone the LogRecord to avoid mutating the original record object
|
|
44
|
+
new_record = logging.makeLogRecord(record.__dict__.copy())
|
|
45
|
+
new_record.levelname = colored
|
|
46
|
+
|
|
47
|
+
return super().formatMessage(new_record)
|
|
48
|
+
|
|
49
|
+
# Fallback - No colors for redirected outputs
|
|
50
|
+
return super().formatMessage(record)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_core_logger(
|
|
54
|
+
log_output: str | None = None, use_color: bool = True
|
|
55
|
+
) -> logging.Logger:
|
|
56
|
+
"""
|
|
57
|
+
Create and modify the core logger.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
log_output (str|None): Output path for storing logs; `None` if not provided.
|
|
61
|
+
use_color (bool): Whether to enable colored console log output.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Logger: The core logger.
|
|
65
|
+
"""
|
|
66
|
+
console_format = "%(asctime)s [%(levelname)s] %(message)s"
|
|
67
|
+
file_format = "%(asctime)s [%(levelname)s] %(message)s"
|
|
68
|
+
date_format = "%Y-%m-%d %H:%M:%S"
|
|
69
|
+
|
|
70
|
+
core_logger = logging.getLogger("core")
|
|
71
|
+
|
|
72
|
+
# Prevent duplicated logging
|
|
73
|
+
if not core_logger.handlers:
|
|
74
|
+
console_handler = logging.StreamHandler()
|
|
75
|
+
if use_color:
|
|
76
|
+
console_handler.setFormatter(
|
|
77
|
+
ColorFormatter(fmt=console_format, datefmt=date_format)
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
console_handler.setFormatter(
|
|
81
|
+
logging.Formatter(file_format, datefmt=date_format)
|
|
82
|
+
)
|
|
83
|
+
console_handler.setLevel(logging.NOTSET)
|
|
84
|
+
|
|
85
|
+
if log_output:
|
|
86
|
+
file_handler = logging.FileHandler(log_output)
|
|
87
|
+
file_handler.setFormatter(
|
|
88
|
+
logging.Formatter(file_format, datefmt=date_format)
|
|
89
|
+
)
|
|
90
|
+
file_handler.setLevel(logging.NOTSET)
|
|
91
|
+
core_logger.addHandler(file_handler)
|
|
92
|
+
|
|
93
|
+
core_logger.addHandler(console_handler)
|
|
94
|
+
|
|
95
|
+
core_logger.setLevel(logging.INFO)
|
|
96
|
+
|
|
97
|
+
return core_logger
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: woninet
|
|
3
|
+
Version: 2.0.1
|
|
4
|
+
Summary: A Python-based ARP+ICMP network monitoring tool
|
|
5
|
+
Author-email: Alireza Teymuri <teymuri.alireza98@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/teymuri-alireza/woninet
|
|
8
|
+
Project-URL: Documentation, https://github.com/teymuri-alireza/woninet/tree/main/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/teymuri-alireza/woninet
|
|
10
|
+
Project-URL: Issues, https://github.com/teymuri-alireza/woninet/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/teymuri-alireza/woninet/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: network,network-monitoring,icmp,arp
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: icmplib<3.1,>=3.0.4
|
|
17
|
+
Requires-Dist: fastapi<0.137,>=0.136.0
|
|
18
|
+
Requires-Dist: Jinja2<3.2,>=3.1.6
|
|
19
|
+
Requires-Dist: uvicorn<0.45,>=0.44.0
|
|
20
|
+
Requires-Dist: PyYAML<6.1,>=6.0.3
|
|
21
|
+
Requires-Dist: SQLAlchemy<2.1,>=2.0.46
|
|
22
|
+
Requires-Dist: matplotlib<3.12,>=3.11.0
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest<9.2,>=9.1.1; extra == "test"
|
|
25
|
+
Requires-Dist: httpx<0.29,>=0.28.1; extra == "test"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# woninet
|
|
29
|
+
|
|
30
|
+
A local network monitoring system.
|
|
31
|
+
|
|
32
|
+
**woninet** continuously monitors devices on a local network using ARP
|
|
33
|
+
and ICMP probes. It enumerates candidate IP addresses in a /24 subnet,
|
|
34
|
+
determines device availability, measures latency and packet loss, stores
|
|
35
|
+
metrics in SQLite, and evaluates configurable alert rules in real time.
|
|
36
|
+
|
|
37
|
+
**Note: This script requires sudo privileges.**
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **ARP + ICMP Host Detection** (Distinguishes real devices from nonexistent
|
|
42
|
+
IPs and filters out Wi‑Fi ARP‑delay noise.)
|
|
43
|
+
|
|
44
|
+
- **Alert Engine** (Evaluates real‑time alert rules (e.g., latency thresholds))
|
|
45
|
+
|
|
46
|
+
- **Threaded Collectors**
|
|
47
|
+
|
|
48
|
+
- **SQLite database** (for devices, recorded metrics, alert states, and alert events.)
|
|
49
|
+
|
|
50
|
+
- **Positional Argument for easier access to the database from the CLI**
|
|
51
|
+
|
|
52
|
+
- **Detailed Logging**
|
|
53
|
+
|
|
54
|
+
## Future Plans
|
|
55
|
+
|
|
56
|
+
- Add ARP table caching to further reduce Wi‑Fi latency noise.
|
|
57
|
+
- Accept MAC addresses as command‑line arguments.
|
|
58
|
+
- Fetch MAC address from database for offline devices.
|
|
59
|
+
- Use ARP noise limit as device state for styling in front-end.
|
|
60
|
+
- Add CLI argument to accept target IP addresses from a file.
|
|
61
|
+
|
|
62
|
+
## Known Issues
|
|
63
|
+
|
|
64
|
+
- Monitoring Scope Limited to /24 Subnet
|
|
65
|
+
|
|
66
|
+
## How To Run
|
|
67
|
+
|
|
68
|
+
**Note:** *woninet* is under active development.
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
Install from PyPI:
|
|
73
|
+
|
|
74
|
+
```shell
|
|
75
|
+
pip install woninet
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then run:
|
|
79
|
+
|
|
80
|
+
```shell
|
|
81
|
+
woninet --help
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Installation from Source
|
|
85
|
+
|
|
86
|
+
### 1. Clone the repository
|
|
87
|
+
|
|
88
|
+
```shell
|
|
89
|
+
git clone https://github.com/teymuri-alireza/woninet
|
|
90
|
+
cd woninet
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 2. Create and activate a virtual environment:
|
|
94
|
+
|
|
95
|
+
**Note:** Create the virtual environment outside the project directory to avoid
|
|
96
|
+
`Multiple top-level packages discovered in a flat-layout` errors.
|
|
97
|
+
|
|
98
|
+
**Unix/Linux note:** The `--copies` flag forces Python to copy the interpreter
|
|
99
|
+
instead of using a symbolic link. This is required because you will apply Linux
|
|
100
|
+
capabilities directly to the Python binary in the virtual environment.
|
|
101
|
+
|
|
102
|
+
#### Linux/macOS
|
|
103
|
+
|
|
104
|
+
```shell
|
|
105
|
+
python3 -m venv --copies ../venv
|
|
106
|
+
source ../venv/bin/activate
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Windows (PowerShell)
|
|
110
|
+
|
|
111
|
+
```shell
|
|
112
|
+
py -m venv ..\venv
|
|
113
|
+
..\venv\Scripts\Activate.ps1
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 3. Install woninet locally:
|
|
117
|
+
|
|
118
|
+
```shell
|
|
119
|
+
pip3 install .
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 4. Run woninet
|
|
123
|
+
|
|
124
|
+
**Unix/Linux only:** To allow raw ICMP without running as root, grant the required
|
|
125
|
+
capability to the Python interpreter inside the virtual environment:
|
|
126
|
+
|
|
127
|
+
```shell
|
|
128
|
+
sudo setcap cap_net_raw=eip ../venv/bin/python3
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Finally, run woninet normally:
|
|
132
|
+
|
|
133
|
+
```shell
|
|
134
|
+
woninet --version
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Alternative (not recommended):** You can run woninet with sudo instead of using
|
|
138
|
+
setcap, but this is discouraged for security reasons:
|
|
139
|
+
|
|
140
|
+
```shell
|
|
141
|
+
sudo ../venv/bin/python3 -m woninet
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
You can define alert rules in a JSON file. Rename the example file at
|
|
147
|
+
`woninet/config.example.json` to `config.json`. The application will load the alert
|
|
148
|
+
rules from that file at startup.
|
|
149
|
+
|
|
150
|
+
### Example:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"monitoring": {
|
|
155
|
+
"arp_noise_limit": 300.0,
|
|
156
|
+
"max_workers": 4
|
|
157
|
+
},
|
|
158
|
+
"database": "woninet.db",
|
|
159
|
+
"target_ip_list": [
|
|
160
|
+
"192.168.1.1-100"
|
|
161
|
+
// or
|
|
162
|
+
"192.168.1.1", "192.168.1.10", "192.168.1.20"
|
|
163
|
+
],
|
|
164
|
+
"alert_rules":[
|
|
165
|
+
{
|
|
166
|
+
"metric": "latency",
|
|
167
|
+
"threshold": 100,
|
|
168
|
+
"consecutive_checks": 3
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"metric": "packet_loss",
|
|
172
|
+
"threshold": 0.0,
|
|
173
|
+
"consecutive_checks": 1
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Explanations:
|
|
180
|
+
|
|
181
|
+
#### Monitoring Settings
|
|
182
|
+
|
|
183
|
+
- **arp_noise_limit**: Latency threshold in milliseconds used to filter ARP
|
|
184
|
+
resolution noise. Set to 0 to disable filtering. Helps eliminate Wi‑Fi latency spikes.
|
|
185
|
+
- **max_workers**: Maximum number of thread workers used to send ICMP pings.
|
|
186
|
+
Higher values may increase system load and latency.
|
|
187
|
+
|
|
188
|
+
#### Database
|
|
189
|
+
|
|
190
|
+
- **database**: The path to the SQLite database file where metrics and alerts are stored.
|
|
191
|
+
|
|
192
|
+
#### Target IP List
|
|
193
|
+
|
|
194
|
+
- A single or range of target IP addresses to scan.
|
|
195
|
+
|
|
196
|
+
#### Alert Rules
|
|
197
|
+
|
|
198
|
+
- **metric**: The metric name to evaluate (`latency` or `packet_loss`)
|
|
199
|
+
- **threshold**: The value that triggers the alert. For latency, this is in milliseconds.
|
|
200
|
+
For packet loss, this is a percentage (0.0-100.0).
|
|
201
|
+
- **consecutive_checks**: The number of consecutive evaluations required before the alert
|
|
202
|
+
state changes. Higher values reduce false positives.
|
|
203
|
+
|
|
204
|
+
#### Notes
|
|
205
|
+
|
|
206
|
+
- **Packet Loss**: Values are in the range [0.0, 100.0]. A value of 100.0 indicates 100%
|
|
207
|
+
packet loss. A value of 0.0 will trigger on any packet loss.
|
|
208
|
+
- **Latency**: All latency thresholds are evaluated in milliseconds.
|
|
209
|
+
|
|
210
|
+
## Usage
|
|
211
|
+
|
|
212
|
+
This [guide](./docs/usage.md) explains common ways to run *woninet* and how to resolve common issues.
|
|
213
|
+
|
|
214
|
+
## Testing
|
|
215
|
+
|
|
216
|
+
If you want to run the test suite locally, install the optional test dependencies:
|
|
217
|
+
|
|
218
|
+
```shell
|
|
219
|
+
pip install -e ".[test]"
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Then run:
|
|
223
|
+
|
|
224
|
+
```shell
|
|
225
|
+
pytest
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The test suite currently focuses on API behavior and core logic, and it uses pytest for automated verification.
|
|
229
|
+
|
|
230
|
+
## Requirements
|
|
231
|
+
|
|
232
|
+
- Python >= 3.12
|
|
233
|
+
- Root privileges
|
|
234
|
+
|
|
235
|
+
## Documentation
|
|
236
|
+
|
|
237
|
+
if you have any question or need clarification, check the [documentation](./docs/documentation.md) guide.
|
|
238
|
+
|
|
239
|
+
## Contribution
|
|
240
|
+
|
|
241
|
+
Thanks for considering contributing to *woninet!*
|
|
242
|
+
To help us maintain code quality, stability, and a predictable release process, please review
|
|
243
|
+
and follow the guidelines in our [Contributing Guide.](./CONTRIBUTING.md)
|