Skip to content
Closed
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
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ SRC_FILES = \
src/nilrt-snac-conflicts/control \
src/ni-wireguard-labview/ni-wireguard-labview.initd \
src/ni-wireguard-labview/wglv0.conf \
src/snac.conf \
src/nilrt-snac \

DIST_FILES = \
Expand Down Expand Up @@ -93,6 +94,11 @@ install : all mkinstalldirs $(DIST_FILES)
install --mode=0644 -t "$(DESTDIR)$(datarootdir)/$(PACKAGE)" \
src/nilrt-snac-conflicts/nilrt-snac-conflicts.ipk

# snac configuration file
install --mode=0444 \
src/snac.conf \
"$(DESTDIR)$(docdir)/$(PACKAGE)/snac.conf.example"

# ni-wireguard-labview
install --mode=0660 \
src/ni-wireguard-labview/wglv0.conf \
Expand Down Expand Up @@ -122,6 +128,7 @@ installcheck :


mkinstalldirs :
mkdir -p --mode=0744 "$(DESTDIR)/etc/snac"
mkdir -p --mode=0700 "$(DESTDIR)/etc/wireguard"
mkdir -p --mode=0755 "$(DESTDIR)/etc/init.d"
mkdir -p "$(DESTDIR)$(datarootdir)/$(PACKAGE)"
Expand All @@ -138,6 +145,7 @@ uninstall :
rm -rvf "$(DESTDIR)$(libdir)/$(PACKAGE)"

# files
rm -vf "$(DESTDIR)/etc/snac/snac.conf"
rm -vf "$(DESTDIR)/etc/init.d/ni-wireguard-labview"
rm -vf "$(DESTDIR)/etc/wireguard"/wglv0.*
rm -vf "$(DESTDIR)$(sbindir)/nilrt-snac"
Expand Down
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

* Add support for `/etc/snac/snac.conf` to control which modules are configured

## [3.0.0] - 2025-09-18

Release corresponding to the LV 2025Q4 / NILRT 11.3 release.
Expand Down
60 changes: 54 additions & 6 deletions nilrt_snac/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@
import argparse
import logging
import sys
from typing import List, Optional
import configparser
from pathlib import Path
from typing import Dict, List, Optional

from nilrt_snac._pre_reqs import verify_prereqs
from nilrt_snac.opkg import opkg_helper
from nilrt_snac._configs import CONFIGS


from nilrt_snac import Errors, logger, SNACError, __version__

PROG_NAME = "nilrt-snac"
VERSION_DESCRIPTION = \
f"""\
VERSION_DESCRIPTION = f"""\
nilrt-snac {__version__}
Copyright (C) 2024 NI (Emerson Electric)
License MIT: MIT License <https://spdx.org/licenses/MIT.html>
Expand All @@ -23,6 +22,38 @@
"""


def _get_enabled_modules(config_file_path: Path = Path("/etc/snac/snac.conf")) -> Dict[str, bool]:
"""Read the config file and return a dict of module enabled states. Strict validation and error reporting."""
enabled_modules: Dict[str, bool] = {}
valid_names = {c.name for c in CONFIGS}
if config_file_path.exists():
parser = configparser.ConfigParser()
try:
parser.read(str(config_file_path))
except (configparser.MissingSectionHeaderError, configparser.ParsingError) as e:
raise SNACError(f"Malformed config file: {config_file_path}", Errors.EX_USAGE)
if not parser.has_section("modules"):
logger.warning(
f"Config file {config_file_path} missing [modules] section. All modules will be enabled."
)
return enabled_modules
for name, value in parser.items("modules"):
key = name.strip().lower()
val = value.strip().lower()
if key not in valid_names:
raise SNACError(
f"Unknown module name '{key}' in config file: {config_file_path}. Valid names: {sorted(valid_names)}",
Errors.EX_USAGE,
)
if val not in ("enabled", "disabled"):
raise SNACError(
f"Invalid value for module '{key}' in config file: '{value}'. Must be 'enabled' or 'disabled'.",
Errors.EX_USAGE,
)
enabled_modules[key] = val == "enabled"
return enabled_modules


def _configure(args: argparse.Namespace) -> int:
"""Configure SNAC mode."""
logger.warning("!! Running this tool will irreversibly alter the state of your system. !!")
Expand All @@ -38,7 +69,15 @@ def _configure(args: argparse.Namespace) -> int:

print("Configuring SNAC mode.")
opkg_helper.update()

# Read /etc/snac/snac.conf for module enable/disable
enabled_modules = _get_enabled_modules()

for config in CONFIGS:
enabled = enabled_modules.get(config.name, True)
if not enabled:
logger.info(f"Skipping configuration for: {config.name} (disabled in config file)")
continue
config.configure(args)

print("!! A reboot is now required to affect your system configuration. !!")
Expand All @@ -51,7 +90,15 @@ def _verify(args: argparse.Namespace) -> int:
"""Configure SNAC mode."""
print("Validating SNAC mode.")
valid = True

# Read /etc/snac/snac.conf for module enable/disable
enabled_modules = _get_enabled_modules()

for config in CONFIGS:
enabled = enabled_modules.get(config.name, True)
if not enabled:
logger.info(f"Skipping verification for: {config.name} (disabled in config file)")
continue
new_valid = config.verify(args)
valid = valid and new_valid

Expand Down Expand Up @@ -139,7 +186,7 @@ def main( # noqa: D103 - Missing docstring in public function (auto-generated n
if args.cmd is None:
logger.error("Command required: {configure, verify}, see --help for more information.")
return Errors.EX_USAGE

try:
if not args.dry_run:
verify_prereqs()
Expand All @@ -150,5 +197,6 @@ def main( # noqa: D103 - Missing docstring in public function (auto-generated n

return ret_val


if __name__ == "__main__":
sys.exit(main(sys.argv))
41 changes: 26 additions & 15 deletions nilrt_snac/_configs/_auditd_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from nilrt_snac._configs._config_file import EqualsDelimitedConfigFile, _ConfigFile
from nilrt_snac.opkg import opkg_helper


def ensure_groups_exist(groups: List[str]) -> None:
"Ensures the specified groups exist on the system."
for group in groups:
Expand All @@ -21,8 +22,10 @@ def ensure_groups_exist(groups: List[str]) -> None:
_cmd("groupadd", group)
logger.info(f"Group {group} created.")


def format_email_template_text(audit_email: str) -> str:
return textwrap.dedent("""\
return textwrap.dedent(
"""\
#!/usr/bin/perl
use strict;
use warnings;
Expand Down Expand Up @@ -60,19 +63,22 @@ def format_email_template_text(audit_email: str) -> str:
$smtp->dataend()
or die "Error ending data: $!";
$smtp->quit;
""").format(audit_email=audit_email)
"""
).format(audit_email=audit_email)


def is_valid_email(email: str) -> bool:
"Validates an email address."
email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$'
email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$"
return re.match(email_regex, email) is not None


class _AuditdConfig(_BaseConfig):
def __init__(self):
super().__init__("auditd")
self._opkg_helper = opkg_helper
self.log_path = os.path.realpath('/var/log')
self.audit_config_path = '/etc/audit/auditd.conf'
self.log_path = os.path.realpath("/var/log")
self.audit_config_path = "/etc/audit/auditd.conf"

def configure(self, args: argparse.Namespace) -> None:
print("Configuring auditd...")
Expand All @@ -82,7 +88,7 @@ def configure(self, args: argparse.Namespace) -> None:
if not self._opkg_helper.is_installed("auditd"):
self._opkg_helper.install("auditd")

#Ensure proper groups exist
# Ensure proper groups exist
groups_required = ["adm", "sudo"]
ensure_groups_exist(groups_required)

Expand All @@ -100,7 +106,9 @@ def configure(self, args: argparse.Namespace) -> None:
audit_email = f"root@{socket.gethostname()}"

if is_valid_email(audit_email):
auditd_config_file.update(r'^action_mail_acct\s*=.*$', f'action_mail_acct = {audit_email}')
auditd_config_file.update(
r"^action_mail_acct\s*=.*$", f"action_mail_acct = {audit_email}"
)

# Install recommended SMTP package dependency
if not self._opkg_helper.is_installed("perl-module-net-smtp"):
Expand All @@ -111,7 +119,7 @@ def configure(self, args: argparse.Namespace) -> None:
self._opkg_helper.install("audispd-plugins")

# Create template audit rule script to send email alerts
audit_rule_script_path = '/etc/audit/audit_email_alert.pl'
audit_rule_script_path = "/etc/audit/audit_email_alert.pl"
if not os.path.exists(audit_rule_script_path):
audit_rule_script = format_email_template_text(audit_email)

Expand All @@ -121,14 +129,16 @@ def configure(self, args: argparse.Namespace) -> None:
# Set the appropriate permissions
_cmd("chmod", "700", audit_rule_script_path)

audit_email_conf_path = '/etc/audit/plugins.d/audit_email_alert.conf'
audit_email_conf_path = "/etc/audit/plugins.d/audit_email_alert.conf"
if not os.path.exists(audit_email_conf_path):
audit_email_config = textwrap.dedent("""\
audit_email_config = textwrap.dedent(
"""\
active = yes
direction = out
path = {audit_rule_script_path}
type = always
""").format(audit_rule_script_path=audit_rule_script_path)
"""
).format(audit_rule_script_path=audit_rule_script_path)

with open(audit_email_conf_path, "w") as file:
file.write(audit_email_config)
Expand All @@ -150,15 +160,17 @@ def configure(self, args: argparse.Namespace) -> None:
_cmd("/etc/init.d/auditd", "restart")

# Set the appropriate permissions to allow only root and the 'adm' group to write/read
init_log_permissions_path = '/etc/init.d/set_log_permissions.sh'
init_log_permissions_path = "/etc/init.d/set_log_permissions.sh"
if not os.path.exists(init_log_permissions_path) and not dry_run:
init_log_permissions_script = textwrap.dedent("""\
init_log_permissions_script = textwrap.dedent(
"""\
#!/bin/sh
chmod 770 {log_path}
chown root:adm {log_path}
setfacl -d -m g:adm:rwx {log_path}
setfacl -d -m o::0 {log_path}
""").format(log_path=self.log_path)
"""
).format(log_path=self.log_path)

with open(init_log_permissions_path, "w") as file:
file.write(init_log_permissions_script)
Expand All @@ -169,7 +181,6 @@ def configure(self, args: argparse.Namespace) -> None:
# Schedule the script to run at start
_cmd(*"update-rc.d set_log_permissions.sh start 3 S .".split())


def verify(self, args: argparse.Namespace) -> bool:
print("Verifying auditd configuration...")
valid: bool = True
Expand Down
3 changes: 3 additions & 0 deletions nilrt_snac/_configs/_base_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

class _BaseConfig(ABC):

def __init__(self, name: str):
self.name = name

@abstractmethod
def configure(self, args: argparse.Namespace) -> None:
raise NotImplementedError
Expand Down
2 changes: 1 addition & 1 deletion nilrt_snac/_configs/_console_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class _ConsoleConfig(_BaseConfig):
def __init__(self):
pass # Nothing to do for now
super().__init__("console")

def configure(self, args: argparse.Namespace) -> None:
print("Deconfiguring console access...")
Expand Down
1 change: 1 addition & 0 deletions nilrt_snac/_configs/_cryptsetup_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

class _CryptSetupConfig(_BaseConfig):
def __init__(self):
super().__init__("cryptsetup")
self._opkg_helper = opkg_helper

def configure(self, args: argparse.Namespace) -> None:
Expand Down
1 change: 1 addition & 0 deletions nilrt_snac/_configs/_faillock_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

class _FaillockConfig(_BaseConfig):
def __init__(self):
super().__init__("faillock")
self._opkg_helper = opkg_helper

def configure(self, args: argparse.Namespace) -> None:
Expand Down
Loading
Loading