This commit is contained in:
Sebastian Lenzlinger 2024-06-28 20:43:23 +02:00
parent 87dd548a42
commit 5ccb9daca0
10 changed files with 253 additions and 12 deletions

2
.gitignore vendored
View File

@ -31,3 +31,5 @@ logs/
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.private/

View File

@ -1,10 +1,12 @@
from pathlib import Path
import logging
import click
from iottb.main import DB_NAME, logger, CFG_FILE_PATH
from iottb.definitions import DB_NAME, CFG_FILE_PATH
from iottb.models.iottb_config import IottbConfig
logger = logging.getLogger(__name__)
@click.command()
@click.option('--file', default=DB_NAME)

View File

@ -1,8 +1,133 @@
import click
import subprocess
import json
from pathlib import Path
import logging
from iottb import definitions
import re
from datetime import datetime
from iottb.definitions import APP_NAME, CFG_FILE_PATH
from iottb.models.iottb_config import IottbConfig
from iottb.utils.string_processing import make_canonical_name
# Setup logger
logger = logging.getLogger('iottb.sniff')
def is_ip_address(address):
ip_pattern = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$")
return ip_pattern.match(address) is not None
def is_mac_address(address):
mac_pattern = re.compile(r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$")
return mac_pattern.match(address) is not None
def load_config(cfg_file):
"""Loads configuration from the given file path."""
with open(cfg_file, 'r') as config_file:
return json.load(config_file)
def validate_sniff(ctx, param, value):
logger.info('Validating sniff...')
if ctx.params.get('unsafe') and not value:
return None
if not ctx.params.get('unsafe') and not value:
raise click.BadParameter('Address is required unless --unsafe is set.')
return value
@click.command('sniff', help='Sniff packets with tcpdump')
@click.argument('device')
@click.option('-i', '--interface', callback=validate_sniff, help='Network interface to capture on',
envvar='IOTTB_CAPTURE_INTERFACE')
@click.option('-a', '--address', callback=validate_sniff, help='IP or MAC address to filter packets by',
envvar='IOTTB_CAPTURE_ADDRESS')
@click.option('--db', '--database', type=click.Path(exists=True, file_okay=False), envvar='IOTTB_DB',
help='Database of device. Only needed if not current default.')
@click.option('--unsafe', is_flag=True, default=False, envvar='IOTTB_UNSAFE', is_eager=True,
help='Disable checks for otherwise required options')
@click.option('--guided', is_flag=True, default=False)
def sniff(device, interface, address, db, unsafe, guided):
""" Sniff packets from a device """
logger.info('sniff command invoked')
# Step1: Load Config
config = IottbConfig(Path(CFG_FILE_PATH))
logger.debug(f'Config loaded: {config}')
# Step2: determine relevant database
database = db if db else config.default_database
path = config.default_db_location[database]
full_db_path = Path(path) / database
logger.debug(f'Full db path is {str(path)}')
# Check if it exists
assert full_db_path.is_dir(), "DB unexpectedly missing"
canonical_name = make_canonical_name(device)
click.echo(f'Using canonical device name {canonical_name}')
if not database_path:
logger.error('No default database path found in configuration')
click.echo('No default database path found in configuration')
return
# Verify device directory
device_path = Path(database_path) / device
if not device_path.exists():
logger.error(f'Device path {device_path} does not exist')
click.echo(f'Device path {device_path} does not exist')
return
# Generate filter
if not unsafe:
if is_ip_address(address):
packet_filter = f"host {address}"
elif is_mac_address(address):
packet_filter = f"ether host {address}"
else:
logger.error('Invalid address format')
click.echo('Invalid address format')
return
else:
packet_filter = None
# Prepare capture directory
capture_dir = device_path / 'captures' / datetime.now().strftime('%Y%m%d_%H%M%S')
capture_dir.mkdir(parents=True, exist_ok=True)
# Prepare capture file
pcap_file = capture_dir / f"{device}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pcap"
# Build tcpdump command
cmd = ['sudo', 'tcpdump', '-i', interface, '-w', str(pcap_file)]
if packet_filter:
cmd.append(packet_filter)
logger.info(f'Executing: {" ".join(cmd)}')
# Execute tcpdump
try:
subprocess.run(cmd, check=True)
click.echo(f"Capture complete. Saved to {pcap_file}")
except subprocess.CalledProcessError as e:
logger.error(f'Failed to capture packets: {e}')
click.echo(f'Failed to capture packets: {e}')
@click.command('sniff', help='Sniff packets with tcpdump')
@click.argument('device')
@click.option('-i', '--interface', required=False, help='Network interface to capture on', envvar='IOTTB_CAPTURE_INTERFACE')
@click.option('-a', '--address', required=True, help='IP or MAC address to filter packets by', envvar='IOTTB_CAPTURE_ADDRESS')
@click.option('--db', '--database', type=click.Path(exists=True, file_okay=False), envvar='IOTTB_DB',
help='Database of device. Only needed if not current default.')
@click.option('--unsafe', is_flag=True, default=False, envvar='IOTTB_UNSAFE',
help='Disable checks for otherwise required options')
@click.option('--guided', is_flag=True)
def sniff2(device, interface, address, cfg_file):
""" Sniff packets from a device """
logger.info('sniff command invoked')
# Step 1: Load Config
# Dependency: Config file must exist
config = IottbConfig(Path(CFG_FILE_PATH))
logger.debug(f'Config loaded: {config}')

View File

@ -2,6 +2,7 @@ import click
from pathlib import Path
import logging
from iottb.commands.sniff import sniff
from iottb.commands.developer import set_key_in_table_to, rm_cfg, rm_dbs, show_cfg
##################################################
# Import package modules
@ -41,7 +42,7 @@ def cli(ctx, verbosity, debug, cfg_file):
ctx.ensure_object(dict) # Make sure context is ready for use
logger.info("Starting execution.")
ctx.obj['CONFIG'] = IottbConfig(cfg_file) # Load configuration directly
# ctx.meta['FULL_PATH_CONFIG_FILE'] =
ctx.meta['FULL_PATH_CONFIG_FILE'] = str(cfg_file)
##################################################################################
@ -55,6 +56,7 @@ cli.add_command(rm_dbs)
# noinspection PyTypeChecker
cli.add_command(add_device)
cli.add_command(show_cfg)
cli.add_command(sniff)
if __name__ == '__main__':
cli(auto_envvar_prefix='IOTTB', show_default=True, show_envvars=True)

View File

@ -10,7 +10,7 @@ DB_NAME = definitions.DB_NAME
class IottbConfig:
""" Class to handle operations on the testbed configuration.
""" Class to handle testbed configuration.
TODO: Add instead of overwrite Database locations when initializing if a location with valid db
exists.

View File

@ -0,0 +1,4 @@
import logging
logger = logging.getLogger('iottb.sniff') # Log with sniff subcommand

View File

@ -1,10 +1,7 @@
import json
import click
from pathlib import Path
import logging
from logging.handlers import RotatingFileHandler
import sys
from iottb.models.iottb_config import IottbConfig
from logging.handlers import RotatingFileHandler
from iottb import definitions
from iottb.definitions import MAX_VERBOSITY, CONSOLE_LOG_FORMATS, APP_NAME, LOGFILE_LOG_FORMAT

View File

@ -0,0 +1,7 @@
import click
import logging
from iottb import definitions
class IottbPrompt:
pass

100
poetry.lock generated
View File

@ -1,7 +1,103 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
package = []
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "packaging"
version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pytest"
version = "8.2.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
{file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2.0"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "scapy"
version = "2.5.0"
description = "Scapy: interactive packet manipulation tool"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
files = [
{file = "scapy-2.5.0.tar.gz", hash = "sha256:5b260c2b754fd8d409ba83ee7aee294ecdbb2c235f9f78fe90bc11cb6e5debc2"},
]
[package.extras]
basic = ["ipython"]
complete = ["cryptography (>=2.0)", "ipython", "matplotlib", "pyx"]
docs = ["sphinx (>=3.0.0)", "sphinx_rtd_theme (>=0.4.3)", "tox (>=3.0.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "34e39677d8527182346093002688d17a5d2fc204b9eb3e094b2e6ac519028228"
content-hash = "10b2c268b0f10db15eab2cca3d2dc9dc25bc60f4b218ebf786fb780fa85557e0"

View File

@ -4,13 +4,19 @@ version = "0.1.0"
description = "IoT Testbed"
authors = ["Sebastian Lenzlinger <sebastian.lenzlinger@unibas.ch>"]
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "^3.12"
click = "^8.1"
scapy = "^2.5"
[tool.poetry.scripts]
iottb = "iottb.main:cli"
[tool.poetry.group.test.dependencies]
pytest = "^8.2.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"