Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
* ni-logos-xt outbound traffic is now permitted on the firewall's 'work' zone. (#66)
* `usbguard` configuration is verified when installed - requires manual installation (#68)


## [2.1.0] - 2025-06-12
Expand Down
5 changes: 2 additions & 3 deletions nilrt_snac/_configs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@
from nilrt_snac._configs._tmux_config import _TmuxConfig
from nilrt_snac._configs._wifi_config import _WIFIConfig
from nilrt_snac._configs._wireguard_config import _WireguardConfig

# USBGuard is not supported for 1.0, but may be added in the future
# from nilrt_snac._configs._usbguard_config import _USBGuardConfig
from nilrt_snac._configs._usbguard_config import _USBGuardConfig

CONFIGS: List[_BaseConfig] = [
_NTPConfig(),
Expand All @@ -40,4 +38,5 @@
_FirewallConfig(),
_AuditdConfig(),
_SyslogConfig(),
_USBGuardConfig(),
]
20 changes: 14 additions & 6 deletions nilrt_snac/_configs/_config_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,17 @@ def contains_exact(self, key: str) -> bool:

class EqualsDelimitedConfigFile(_ConfigFile):
def get(self, key: str) -> str:
value_pattern = rf"{key}\s*=\s*(.*)"
match = re.search(value_pattern, self._config)
if match:
return match.group(1).strip()
else:
return ""
"""
Return the value for the first line where the left side of '=' matches the key (ignoring whitespace).

Args:
key: The key to search for (left side of equals).

Returns:
The value (right side of equals) for the first matching line, or an empty string if not found.
"""
for line in self._config.splitlines():
parts = line.split("=", 1)
if len(parts) > 1 and parts[0].replace(" ","").replace("\t","") == key:
return parts[1].strip()
return ""
77 changes: 32 additions & 45 deletions nilrt_snac/_configs/_usbguard_config.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,48 @@
import argparse
import pathlib
import shutil
import subprocess
import tempfile

from nilrt_snac._configs._base_config import _BaseConfig
from nilrt_snac._configs._config_file import EqualsDelimitedConfigFile

from nilrt_snac import logger
from nilrt_snac.opkg import opkg_helper

USBGUARD_SRC_URL = (
"https://github.com/USBGuard/usbguard/releases/download/usbguard-1.1.2/usbguard-1.1.2.tar.gz"
)


class _USBGuardConfig(_BaseConfig):
"""USBGuard configuration handler."""

def __init__(self):
self._src_path = pathlib.Path("/usr/local/src")
self.config_file_path = "/etc/usbguard/usbguard-daemon.conf"
self.package_name = "usbguard"
self._opkg_helper = opkg_helper

def configure(self, args: argparse.Namespace) -> None:
print("Installing USBGuard...")
dry_run: bool = args.dry_run

logger.debug(f"Ensure {self._src_path} exists")
self._src_path.mkdir(parents=True, exist_ok=True)

logger.debug("Clean up any previous copy of USB Guard")
installer_path = self._src_path / "usbguard"
shutil.rmtree(installer_path)

logger.debug(f"Download and extract {USBGUARD_SRC_URL}")
# There is not proper typing support for NamedTemporaryFile
with tempfile.NamedTemporaryFile(delete_on_close=False) as fp: # type: ignore
subprocess.run(["wget", USBGUARD_SRC_URL, "-O", fp.name], check=True)
subprocess.run(["tar", "xz", "-f", fp.name, "-C", self._src_path], check=True)

logger.debug("Install prereq")
self._opkg_helper.install("libqb-dev")

logger.debug("Configure and install USBGuard")
cmd = [
"./configure",
"--with-crypto-library=openssl",
"--with-bundled-catch",
"--with-bundled-pegtl",
"--without-dbus",
"--without-polkit",
"--prefix=/",
]
if not dry_run:
subprocess.run(cmd, check=True, cwd=installer_path)
subprocess.run(["make", "install"], check=True, cwd=installer_path)
# TODO: make initscript
"""USBGuard must be installed manually by the user."""
if not self._opkg_helper.is_installed(self.package_name):
print("USBGuard configuration: Manual installation required")

def verify(self, args: argparse.Namespace) -> bool:
print("Verifying USBGuard configuration...")
valid = True
# TODO: figure out what needs to be verified
return valid
"""Verify USBGuard configuration if the package is installed."""
if self._opkg_helper.is_installed(self.package_name):
print("Verifying usbguard configuration...")
conf_file = EqualsDelimitedConfigFile(self.config_file_path)
if not conf_file.exists():
logger.error(f"USBGuard config file missing: {self.config_file_path}")
return False
# We make sure we get the RuleFile that does not have a comment
rule_file_path = conf_file.get("RuleFile")
if rule_file_path == "":
logger.error(f"USBGuard RuleFile not specified in {self.config_file_path}")
return False
rules_file = pathlib.Path(rule_file_path)
if not rules_file.exists():
logger.error(f"USBGuard rules file missing: {rules_file_path}")
return False
if rules_file.stat().st_size == 0:
logger.error(f"USBGuard rules file is empty: {rules_file_path}")
return False
return True
else:
print("USBGuard is not installed; skipping verification.")
return True

Loading