diff --git a/.gitignore b/.gitignore
index 37519ce3..cf1a4971 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,12 @@ Thumbs.db
# Logs - workflow logs are tracked in git for session history
# log/**/*.md # Removed - workflow logs should be committed
+*.log
+!log/**/*.md
+
+# Test artifacts and temporary files
+*.db
+test_*.py # Exclude test scripts in root (tests/ directory is fine)
# AKIS session tracking (temporary file for VSCode extension)
.akis-session.json
diff --git a/agent.py b/agent.py
deleted file mode 100644
index bd00df12..00000000
--- a/agent.py
+++ /dev/null
@@ -1 +0,0 @@
-
Connecting to the forwarded port...
\ No newline at end of file
diff --git a/backend/app/api/v1/endpoints/host.py b/backend/app/api/v1/endpoints/host.py
index d977e3ed..5115b889 100644
--- a/backend/app/api/v1/endpoints/host.py
+++ b/backend/app/api/v1/endpoints/host.py
@@ -13,7 +13,7 @@
import os
import shutil
from datetime import datetime
-from app.core.security import get_current_user
+from app.core.security import get_current_user, decode_token
router = APIRouter()
@@ -455,13 +455,21 @@ async def terminal_websocket(websocket: WebSocket, token: Optional[str] = None):
import termios
import select
- # Basic token validation
+ # Verify JWT token
if not token:
await websocket.close(code=1008, reason="Authentication required")
return
- # TODO: Add proper JWT token verification here
- # For now, just check that token is provided
+ try:
+ # Decode and verify JWT token
+ payload = decode_token(token)
+ user_id = payload.get("sub")
+ if not user_id:
+ await websocket.close(code=1008, reason="Invalid token")
+ return
+ except ValueError as e:
+ await websocket.close(code=1008, reason=f"Token validation failed: {str(e)}")
+ return
await websocket.accept()
diff --git a/backend/app/services/PingService.py b/backend/app/services/PingService.py
index 6ad77e1a..dbfddcb5 100644
--- a/backend/app/services/PingService.py
+++ b/backend/app/services/PingService.py
@@ -5,9 +5,9 @@
import subprocess
import re
import time
-import ipaddress
from typing import Optional, Dict, List, Any
from datetime import datetime
+from app.utils.validators import NetworkValidator, InputValidator
class PingService:
@@ -36,27 +36,17 @@ def _validate_target(self, target: str) -> str:
if not target:
raise ValueError("Target cannot be empty")
- # Try to parse as IP address first
- try:
- ipaddress.ip_address(target)
+ # Try to validate as IP address first
+ is_valid_ip, _ = NetworkValidator.validate_ip_address(target)
+ if is_valid_ip:
return target
- except ValueError:
- pass
-
- # Validate as hostname (RFC 1123)
- # Allow alphanumeric, hyphens, dots, max 253 chars
- if len(target) > 253:
- raise ValueError("Hostname too long")
- # Check for valid hostname pattern
- hostname_pattern = re.compile(
- r'^(?!-)[A-Za-z0-9-]{1,63}(? bool:
return mac.upper() == "FF:FF:FF:FF:FF:FF"
def _is_broadcast_ip(self, ip: str) -> bool:
- """Check if IP is broadcast (ends with .255) or 255.255.255.255"""
- return ip.endswith(".255") or ip == "255.255.255.255"
+ """Check if IP is broadcast"""
+ return NetworkValidator.is_broadcast_ip(ip)
def _is_multicast_ip(self, ip: str) -> bool:
- """Check if IP is multicast (224.0.0.0 - 239.255.255.255)"""
- try:
- first_octet = int(ip.split('.')[0])
- return 224 <= first_octet <= 239
- except:
- return False
+ """Check if IP is multicast"""
+ return NetworkValidator.is_multicast_ip(ip)
def _is_link_local_ip(self, ip: str) -> bool:
- """Check if IP is link-local (169.254.x.x)"""
- return ip.startswith("169.254.")
+ """Check if IP is link-local"""
+ return NetworkValidator.is_link_local_ip(ip)
def _is_valid_source_ip(self, ip: str) -> bool:
"""Check if IP is a valid source address (not broadcast, not 0.0.0.0, not link-local)"""
@@ -941,7 +934,7 @@ def craft_and_send_packet(self, packet_config: Dict[str, Any]) -> Dict[str, Any]
# Single packet mode - wait for response
try:
# sr1 sends packet and receives first response
- response = sr1(packet, timeout=self.PACKET_SEND_TIMEOUT, verbose=0)
+ response = sr1(packet, timeout=const.PACKET_SEND_TIMEOUT, verbose=0)
elapsed = time.time() - start_time
if response:
@@ -955,7 +948,7 @@ def craft_and_send_packet(self, packet_config: Dict[str, Any]) -> Dict[str, Any]
"source": None,
"destination": None,
"length": len(response),
- "raw_hex": response.build().hex()[:self.RESPONSE_HEX_MAX_LENGTH]
+ "raw_hex": response.build().hex()[:const.RESPONSE_HEX_MAX_LENGTH]
}
if IP in response:
@@ -989,7 +982,7 @@ def craft_and_send_packet(self, packet_config: Dict[str, Any]) -> Dict[str, Any]
"trace": trace
}
else:
- trace.append(f"No response received (timeout: {self.PACKET_SEND_TIMEOUT}s)")
+ trace.append(f"No response received (timeout: {const.PACKET_SEND_TIMEOUT}s)")
return {
"success": True,
"sent_packet": {
@@ -1041,18 +1034,17 @@ def start_storm(self, config: Dict[str, Any]) -> Dict[str, Any]:
}
# Validate PPS
- if not isinstance(pps, int) or pps < 1 or pps > 10000000:
+ if not isinstance(pps, int) or pps < const.MIN_PPS or pps > const.MAX_PPS:
return {
"success": False,
- "error": "PPS must be between 1 and 10,000,000"
+ "error": f"PPS must be between {const.MIN_PPS} and {const.MAX_PPS:,}"
}
# Validate packet type
- valid_types = ["broadcast", "multicast", "tcp", "udp", "raw_ip"]
- if packet_type not in valid_types:
+ if packet_type not in const.VALID_PACKET_TYPES:
return {
"success": False,
- "error": f"Invalid packet type. Must be one of: {', '.join(valid_types)}"
+ "error": f"Invalid packet type. Must be one of: {', '.join(const.VALID_PACKET_TYPES)}"
}
# Validate ports for TCP/UDP
@@ -1431,7 +1423,7 @@ def stop_storm(self) -> Dict[str, Any]:
self.is_storming = False
if self.storm_thread:
- self.storm_thread.join(timeout=self.STORM_THREAD_STOP_TIMEOUT)
+ self.storm_thread.join(timeout=const.STORM_THREAD_STOP_TIMEOUT)
return {
"success": True,
diff --git a/backend/app/utils/constants.py b/backend/app/utils/constants.py
new file mode 100644
index 00000000..2701a3aa
--- /dev/null
+++ b/backend/app/utils/constants.py
@@ -0,0 +1,52 @@
+"""
+Configuration constants for network services
+"""
+
+# Packet crafting constants
+PACKET_SEND_TIMEOUT = 3 # seconds
+RESPONSE_HEX_MAX_LENGTH = 200 # characters
+STORM_THREAD_STOP_TIMEOUT = 2.0 # seconds
+
+# Packet capture constants
+MAX_STORED_PACKETS = 1000
+STATS_UPDATE_INTERVAL = 1 # seconds
+TRAFFIC_HISTORY_LENGTH = 20 # data points
+INTERFACE_HISTORY_LENGTH = 30 # data points
+
+# Storm testing limits
+MIN_PPS = 1
+MAX_PPS = 10_000_000
+VALID_PACKET_TYPES = ["broadcast", "multicast", "tcp", "udp", "raw_ip"]
+
+# Network filtering defaults
+DEFAULT_TRACK_SOURCE_ONLY = True
+DEFAULT_FILTER_UNICAST = False
+DEFAULT_FILTER_MULTICAST = True
+DEFAULT_FILTER_BROADCAST = True
+
+# Ping service constants
+MIN_PING_COUNT = 1
+MAX_PING_COUNT = 100
+MIN_TIMEOUT = 1
+MAX_TIMEOUT = 30
+MIN_PACKET_SIZE = 1
+MAX_PACKET_SIZE = 65500
+
+# Port ranges
+MIN_PORT = 1
+MAX_PORT = 65535
+
+# Protocol defaults
+DEFAULT_SSH_PORT = 22
+DEFAULT_FTP_PORT = 21
+DEFAULT_HTTP_PORT = 80
+DEFAULT_HTTPS_PORT = 443
+DEFAULT_RDP_PORT = 3389
+DEFAULT_VNC_PORT = 5900
+DEFAULT_TELNET_PORT = 23
+DEFAULT_MYSQL_PORT = 3306
+DEFAULT_POSTGRES_PORT = 5432
+
+# Service detection timeouts
+CONNECTION_TIMEOUT = 5 # seconds
+HTTP_REQUEST_TIMEOUT = 10 # seconds
diff --git a/backend/app/utils/validators.py b/backend/app/utils/validators.py
new file mode 100644
index 00000000..4af06a6f
--- /dev/null
+++ b/backend/app/utils/validators.py
@@ -0,0 +1,213 @@
+"""
+Common validation utilities for the application
+"""
+import ipaddress
+import re
+from typing import Union, Tuple
+
+
+class NetworkValidator:
+ """Validator for network-related inputs"""
+
+ @staticmethod
+ def validate_ip_address(ip: str) -> Tuple[bool, str]:
+ """
+ Validate an IP address
+
+ Args:
+ ip: IP address string to validate
+
+ Returns:
+ Tuple of (is_valid, error_message)
+ """
+ try:
+ ipaddress.ip_address(ip)
+ return True, ""
+ except ValueError:
+ return False, f"Invalid IP address: {ip}"
+
+ @staticmethod
+ def validate_port(port: Union[int, str], allow_zero: bool = False) -> Tuple[bool, str]:
+ """
+ Validate a network port number
+
+ Args:
+ port: Port number to validate
+ allow_zero: Whether port 0 is allowed
+
+ Returns:
+ Tuple of (is_valid, error_message)
+ """
+ try:
+ port_num = int(port)
+ min_port = 0 if allow_zero else 1
+ if min_port <= port_num <= 65535:
+ return True, ""
+ return False, f"Port must be between {min_port} and 65535"
+ except (ValueError, TypeError):
+ return False, f"Port must be a valid integer: {port}"
+
+ @staticmethod
+ def validate_network_cidr(network: str) -> Tuple[bool, str]:
+ """
+ Validate a network in CIDR notation
+
+ Args:
+ network: Network string in CIDR notation (e.g., "192.168.1.0/24")
+
+ Returns:
+ Tuple of (is_valid, error_message)
+ """
+ try:
+ ipaddress.ip_network(network, strict=False)
+ return True, ""
+ except ValueError as e:
+ return False, f"Invalid network CIDR: {str(e)}"
+
+ @staticmethod
+ def validate_mac_address(mac: str) -> Tuple[bool, str]:
+ """
+ Validate a MAC address
+
+ Args:
+ mac: MAC address string to validate
+
+ Returns:
+ Tuple of (is_valid, error_message)
+ """
+ # MAC address patterns: XX:XX:XX:XX:XX:XX or XX-XX-XX-XX-XX-XX
+ pattern = re.compile(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$')
+ if pattern.match(mac):
+ return True, ""
+ return False, f"Invalid MAC address format: {mac}"
+
+ @staticmethod
+ def is_broadcast_ip(ip: str) -> bool:
+ """Check if an IP address is a broadcast address"""
+ try:
+ ip_obj = ipaddress.ip_address(ip)
+ # IPv4 broadcast
+ if isinstance(ip_obj, ipaddress.IPv4Address):
+ return ip.endswith('.255') or ip == '255.255.255.255'
+ return False
+ except ValueError:
+ return False
+
+ @staticmethod
+ def is_multicast_ip(ip: str) -> bool:
+ """Check if an IP address is a multicast address"""
+ try:
+ ip_obj = ipaddress.ip_address(ip)
+ return ip_obj.is_multicast
+ except ValueError:
+ return False
+
+ @staticmethod
+ def is_link_local_ip(ip: str) -> bool:
+ """Check if an IP address is link-local"""
+ try:
+ ip_obj = ipaddress.ip_address(ip)
+ return ip_obj.is_link_local
+ except ValueError:
+ return False
+
+ @staticmethod
+ def is_private_ip(ip: str) -> bool:
+ """Check if an IP address is private"""
+ try:
+ ip_obj = ipaddress.ip_address(ip)
+ return ip_obj.is_private
+ except ValueError:
+ return False
+
+
+class InputValidator:
+ """Validator for general input validation"""
+
+ @staticmethod
+ def validate_range(value: Union[int, float], min_val: Union[int, float],
+ max_val: Union[int, float], name: str = "Value") -> Tuple[bool, str]:
+ """
+ Validate a numeric value is within a range
+
+ Args:
+ value: Value to validate
+ min_val: Minimum allowed value
+ max_val: Maximum allowed value
+ name: Name of the field for error messages
+
+ Returns:
+ Tuple of (is_valid, error_message)
+ """
+ try:
+ num_value = float(value)
+ if min_val <= num_value <= max_val:
+ return True, ""
+ return False, f"{name} must be between {min_val} and {max_val}"
+ except (ValueError, TypeError):
+ return False, f"{name} must be a valid number"
+
+ @staticmethod
+ def validate_string_length(value: str, min_len: int = 0, max_len: int = None,
+ name: str = "Value") -> Tuple[bool, str]:
+ """
+ Validate string length
+
+ Args:
+ value: String to validate
+ min_len: Minimum length
+ max_len: Maximum length (None for no limit)
+ name: Name of the field for error messages
+
+ Returns:
+ Tuple of (is_valid, error_message)
+ """
+ if not isinstance(value, str):
+ return False, f"{name} must be a string"
+
+ length = len(value)
+ if length < min_len:
+ return False, f"{name} must be at least {min_len} characters"
+
+ if max_len is not None and length > max_len:
+ return False, f"{name} must be at most {max_len} characters"
+
+ return True, ""
+
+ @staticmethod
+ def validate_choice(value: str, choices: list, name: str = "Value") -> Tuple[bool, str]:
+ """
+ Validate value is one of allowed choices
+
+ Args:
+ value: Value to validate
+ choices: List of allowed values
+ name: Name of the field for error messages
+
+ Returns:
+ Tuple of (is_valid, error_message)
+ """
+ if value in choices:
+ return True, ""
+ return False, f"{name} must be one of: {', '.join(str(c) for c in choices)}"
+
+ @staticmethod
+ def validate_hostname(hostname: str) -> Tuple[bool, str]:
+ """
+ Validate a hostname according to RFC 1123
+
+ Args:
+ hostname: Hostname to validate
+
+ Returns:
+ Tuple of (is_valid, error_message)
+ """
+ if len(hostname) > 253:
+ return False, "Hostname too long (max 253 characters)"
+
+ # Check for valid hostname pattern
+ pattern = re.compile(r'^(?!-)[A-Za-z0-9-]{1,63}(? can't render element of type UUID (Background on this error at: https://sqlalche.me/e/20/l7de)
-
-The above exception was the direct cause of the following exception:
-
-Traceback (most recent call last):
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/starlette/routing.py", line 677, in lifespan
- async with self.lifespan_context(app) as maybe_state:
- ^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/contextlib.py", line 210, in __aenter__
- return await anext(self.gen)
- ^^^^^^^^^^^^^^^^^^^^^
- File "/workspace/project/NOP/backend/app/main.py", line 33, in lifespan
- await conn.run_sync(Base.metadata.create_all)
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/ext/asyncio/engine.py", line 886, in run_sync
- return await greenlet_spawn(fn, self._proxied, *arg, **kw)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 192, in greenlet_spawn
- result = context.switch(value)
- ^^^^^^^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/schema.py", line 5828, in create_all
- bind._run_ddl_visitor(
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 2447, in _run_ddl_visitor
- visitorcallable(self.dialect, self, **kwargs).traverse_single(element)
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/visitors.py", line 671, in traverse_single
- return meth(obj, **kw)
- ^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/ddl.py", line 919, in visit_metadata
- self.traverse_single(
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/visitors.py", line 671, in traverse_single
- return meth(obj, **kw)
- ^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/ddl.py", line 957, in visit_table
- )._invoke_with(self.connection)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/ddl.py", line 315, in _invoke_with
- return bind.execute(self)
- ^^^^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 1416, in execute
- return meth(
- ^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/ddl.py", line 181, in _execute_on_connection
- return connection._execute_ddl(
- ^^^^^^^^^^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 1525, in _execute_ddl
- compiled = ddl.compile(
- ^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/elements.py", line 308, in compile
- return self._compiler(dialect, **kw)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/ddl.py", line 69, in _compiler
- return dialect.ddl_compiler(dialect, self, **kw)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/compiler.py", line 867, in __init__
- self.string = self.process(self.statement, **compile_kwargs)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/compiler.py", line 912, in process
- return obj._compiler_dispatch(self, **kwargs)
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/visitors.py", line 143, in _compiler_dispatch
- return meth(self, **kw) # type: ignore # noqa: E501
- ^^^^^^^^^^^^^^^^
- File "/openhands/micromamba/envs/openhands/lib/python3.12/site-packages/sqlalchemy/sql/compiler.py", line 6497, in visit_create_table
- raise exc.CompileError(
-sqlalchemy.exc.CompileError: (in table 'users', column 'id'): Compiler