Skip to content

Commit 2696466

Browse files
committed
Merge branch 'master' into dev/texasaggie97/log-progress
2 parents 65fb175 + 7a64fcb commit 2696466

File tree

5 files changed

+252
-0
lines changed

5 files changed

+252
-0
lines changed

docs/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
### Added
1212

1313
* Add support for `/etc/snac/snac.conf` to control which modules are configured
14+
* Add `ClamAV` antivirus verification support (#77)
15+
* When ClamAV packages are installed, `nilrt-snac verify` validates configuration files (`clamd.conf`, `freshclam.conf`) and virus signature databases (`.cvd`, `.cld` files)
1416
* Add logging of all output to `/var/log/nilrt-snac`
1517

1618
## [3.0.0] - 2025-09-18

nilrt_snac/_configs/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from nilrt_snac._configs._auditd_config import _AuditdConfig
44
from nilrt_snac._configs._base_config import _BaseConfig
5+
from nilrt_snac._configs._clamav_config import _ClamAVConfig
56
from nilrt_snac._configs._console_config import _ConsoleConfig
67
from nilrt_snac._configs._cryptsetup_config import _CryptSetupConfig
78
from nilrt_snac._configs._faillock_config import _FaillockConfig
@@ -39,4 +40,5 @@
3940
_AuditdConfig(),
4041
_SyslogConfig(),
4142
_USBGuardConfig(),
43+
_ClamAVConfig(),
4244
]
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import argparse
2+
import pathlib
3+
4+
from nilrt_snac._configs._base_config import _BaseConfig
5+
from nilrt_snac._configs._config_file import _ConfigFile
6+
7+
from nilrt_snac import logger
8+
from nilrt_snac.opkg import opkg_helper
9+
10+
11+
class _ClamAVConfig(_BaseConfig):
12+
"""ClamAV configuration handler."""
13+
14+
def __init__(self):
15+
super().__init__("clamav")
16+
self.clamd_config_path = "/etc/clamav/clamd.conf"
17+
self.freshclam_config_path = "/etc/clamav/freshclam.conf"
18+
self.virus_db_path = "/var/lib/clamav/"
19+
self.package_names = ["clamav", "clamav-daemon", "clamav-freshclam"]
20+
self._opkg_helper = opkg_helper
21+
22+
def configure(self, args: argparse.Namespace) -> None:
23+
"""ClamAV must be installed manually by the user."""
24+
# Check if any ClamAV package is installed
25+
installed_packages = [pkg for pkg in self.package_names if self._opkg_helper.is_installed(pkg)]
26+
if not installed_packages:
27+
print("ClamAV configuration: Manual installation required")
28+
29+
def verify(self, args: argparse.Namespace) -> bool:
30+
"""Verify ClamAV configuration if any ClamAV package is installed."""
31+
# Check if any ClamAV package is installed
32+
installed_packages = [pkg for pkg in self.package_names if self._opkg_helper.is_installed(pkg)]
33+
34+
if installed_packages:
35+
print("Verifying clamav configuration...")
36+
valid = True
37+
38+
# Check clamd configuration file
39+
clamd_config = _ConfigFile(self.clamd_config_path)
40+
if not clamd_config.exists():
41+
logger.error(f"ClamAV daemon config file missing: {self.clamd_config_path}")
42+
valid = False
43+
elif pathlib.Path(self.clamd_config_path).stat().st_size == 0:
44+
logger.error(f"ClamAV daemon config file is empty: {self.clamd_config_path}")
45+
valid = False
46+
47+
# Check freshclam configuration file
48+
freshclam_config = _ConfigFile(self.freshclam_config_path)
49+
if not freshclam_config.exists():
50+
logger.error(f"ClamAV freshclam config file missing: {self.freshclam_config_path}")
51+
valid = False
52+
elif pathlib.Path(self.freshclam_config_path).stat().st_size == 0:
53+
logger.error(f"ClamAV freshclam config file is empty: {self.freshclam_config_path}")
54+
valid = False
55+
56+
# Check virus database directory and that signatures have been downloaded
57+
virus_db_dir = pathlib.Path(self.virus_db_path)
58+
if not virus_db_dir.exists():
59+
logger.error(f"ClamAV virus database directory missing: {self.virus_db_path}")
60+
valid = False
61+
else:
62+
# Check for signature files (typically .cvd or .cld files)
63+
signature_files = list(virus_db_dir.glob("*.cvd")) + list(virus_db_dir.glob("*.cld"))
64+
if not signature_files:
65+
logger.error(f"No ClamAV signature files found in {self.virus_db_path}")
66+
logger.error("Run 'freshclam' to download virus signatures")
67+
valid = False
68+
else:
69+
# Check that at least one signature file is not empty
70+
valid_signatures = [f for f in signature_files if f.stat().st_size > 0]
71+
if not valid_signatures:
72+
logger.error("All ClamAV signature files are empty")
73+
logger.error("Run 'freshclam' to download virus signatures")
74+
valid = False
75+
76+
if valid:
77+
logger.info(f"ClamAV verification passed. Found packages: {', '.join(installed_packages)}")
78+
79+
return valid
80+
else:
81+
print("ClamAV is not installed; skipping verification.")
82+
return True

src/snac.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# Example: To disable a module, uncomment the line and set to 'disabled'.
77
#
88
# auditd = disabled # Auditd system auditing
9+
# clamav = disabled # ClamAV antivirus (verification only)
910
# console = disabled # Console access
1011
# cryptsetup = disabled # Disk encryption (cryptsetup)
1112
# faillock = disabled # PAM faillock for login security
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""Test ClamAV configuration verification."""
2+
3+
import argparse
4+
import io
5+
import pathlib
6+
import tempfile
7+
from contextlib import redirect_stdout
8+
from unittest.mock import patch
9+
10+
from nilrt_snac._configs._clamav_config import _ClamAVConfig
11+
12+
13+
class TestClamAVConfig:
14+
"""Test cases for ClamAV configuration verification."""
15+
16+
def test_configure_not_installed(self):
17+
"""Test configure method when ClamAV is not installed."""
18+
config = _ClamAVConfig()
19+
20+
# Mock opkg_helper to return False for all packages
21+
with patch.object(config._opkg_helper, "is_installed", return_value=False):
22+
args = argparse.Namespace(dry_run=False)
23+
24+
# This should not raise an exception
25+
config.configure(args)
26+
27+
def test_verify_not_installed(self):
28+
"""Test verify method when ClamAV is not installed - should pass."""
29+
config = _ClamAVConfig()
30+
31+
# Mock opkg_helper to return False for all packages
32+
with patch.object(config._opkg_helper, "is_installed", return_value=False):
33+
# Capture stdout to verify the skip message
34+
captured_output = io.StringIO()
35+
args = argparse.Namespace()
36+
37+
with redirect_stdout(captured_output):
38+
result = config.verify(args)
39+
40+
# Verify it returns True for the right reason
41+
assert result is True
42+
output = captured_output.getvalue()
43+
assert "ClamAV is not installed; skipping verification." in output
44+
45+
def test_verify_installed_missing_config_files(self):
46+
"""Test verify when ClamAV is installed but config files missing."""
47+
config = _ClamAVConfig()
48+
49+
# Mock opkg_helper to return True for one ClamAV package
50+
with patch.object(config._opkg_helper, "is_installed") as mock_installed:
51+
mock_installed.side_effect = lambda pkg: pkg == "clamav"
52+
53+
args = argparse.Namespace()
54+
result = config.verify(args)
55+
56+
# Should fail because config files don't exist
57+
assert result is False
58+
59+
def test_verify_installed_with_valid_config(self):
60+
"""Test verify method when ClamAV installed with valid config."""
61+
config = _ClamAVConfig()
62+
63+
with tempfile.TemporaryDirectory() as tmpdir:
64+
# Create temporary config files and database directory
65+
clamd_config = pathlib.Path(tmpdir) / "clamd.conf"
66+
freshclam_config = pathlib.Path(tmpdir) / "freshclam.conf"
67+
virus_db_dir = pathlib.Path(tmpdir) / "db"
68+
virus_db_dir.mkdir()
69+
70+
# Create non-empty config files
71+
clamd_config.write_text(
72+
"# ClamAV daemon config\n" "LogFile /var/log/clamav/clamd.log\n"
73+
)
74+
freshclam_config.write_text(
75+
"# FreshClam config\n" "UpdateLogFile /var/log/clamav/freshclam.log\n"
76+
)
77+
78+
# Create a signature file
79+
signature_file = virus_db_dir / "main.cvd"
80+
signature_file.write_bytes(b"fake signature data")
81+
82+
# Update config paths to use temporary files
83+
config.clamd_config_path = str(clamd_config)
84+
config.freshclam_config_path = str(freshclam_config)
85+
config.virus_db_path = str(virus_db_dir)
86+
87+
# Mock opkg_helper to return True for one ClamAV package
88+
with patch.object(config._opkg_helper, "is_installed") as mock_installed:
89+
mock_installed.side_effect = lambda pkg: pkg == "clamav"
90+
91+
args = argparse.Namespace()
92+
result = config.verify(args)
93+
94+
# Should pass because all required files exist and not empty
95+
assert result is True
96+
97+
def test_verify_installed_empty_config_files(self):
98+
"""Test verify method when ClamAV config files exist but empty."""
99+
config = _ClamAVConfig()
100+
101+
with tempfile.TemporaryDirectory() as tmpdir:
102+
# Create temporary empty config files
103+
clamd_config = pathlib.Path(tmpdir) / "clamd.conf"
104+
freshclam_config = pathlib.Path(tmpdir) / "freshclam.conf"
105+
virus_db_dir = pathlib.Path(tmpdir) / "db"
106+
virus_db_dir.mkdir()
107+
108+
# Create empty config files
109+
clamd_config.write_text("")
110+
freshclam_config.write_text("")
111+
112+
# Create a signature file
113+
signature_file = virus_db_dir / "main.cvd"
114+
signature_file.write_bytes(b"fake signature data")
115+
116+
# Update config paths to use temporary files
117+
config.clamd_config_path = str(clamd_config)
118+
config.freshclam_config_path = str(freshclam_config)
119+
config.virus_db_path = str(virus_db_dir)
120+
121+
# Mock opkg_helper to return True for one ClamAV package
122+
with patch.object(config._opkg_helper, "is_installed") as mock_installed:
123+
mock_installed.side_effect = lambda pkg: pkg == "clamav"
124+
125+
args = argparse.Namespace()
126+
result = config.verify(args)
127+
128+
# Should fail because config files are empty
129+
assert result is False
130+
131+
def test_verify_installed_no_signatures(self):
132+
"""Test verify when ClamAV installed but no signature files exist."""
133+
config = _ClamAVConfig()
134+
135+
with tempfile.TemporaryDirectory() as tmpdir:
136+
# Create temporary config files and database directory
137+
clamd_config = pathlib.Path(tmpdir) / "clamd.conf"
138+
freshclam_config = pathlib.Path(tmpdir) / "freshclam.conf"
139+
virus_db_dir = pathlib.Path(tmpdir) / "db"
140+
virus_db_dir.mkdir()
141+
142+
# Create non-empty config files
143+
clamd_config.write_text(
144+
"# ClamAV daemon config\n" "LogFile /var/log/clamav/clamd.log\n"
145+
)
146+
freshclam_config.write_text(
147+
"# FreshClam config\n" "UpdateLogFile /var/log/clamav/freshclam.log\n"
148+
)
149+
150+
# Don't create any signature files
151+
152+
# Update config paths to use temporary files
153+
config.clamd_config_path = str(clamd_config)
154+
config.freshclam_config_path = str(freshclam_config)
155+
config.virus_db_path = str(virus_db_dir)
156+
157+
# Mock opkg_helper to return True for one ClamAV package
158+
with patch.object(config._opkg_helper, "is_installed") as mock_installed:
159+
mock_installed.side_effect = lambda pkg: pkg == "clamav"
160+
161+
args = argparse.Namespace()
162+
result = config.verify(args)
163+
164+
# Should fail because no signature files exist
165+
assert result is False

0 commit comments

Comments
 (0)