BoltClam 1.0.0: ClamAV desktop protection with Bolt sign-in
Adds the PyQt6 Windows client (Homepage, Scan & results, About & updates, Settings), ClamAV scanning and quarantine, browser-download watch with an exclusive scan gate, Bolt loopback/PKCE sign-in with DPAPI key storage, Gitea release feed with checksum verification, opt-in diagnostics, and Windows/portable build tooling. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import stat
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from boltclam.clamav import (
|
||||
ClamAVService,
|
||||
build_freshclam_args,
|
||||
build_scan_args,
|
||||
database_age,
|
||||
database_files,
|
||||
find_clamscan,
|
||||
parse_clamscan_line,
|
||||
parse_extra_options,
|
||||
parse_version_output,
|
||||
scan_targets,
|
||||
)
|
||||
|
||||
|
||||
def _script(path: Path, body: str = "") -> Path:
|
||||
path.write_text("#!/bin/sh\n" + body, encoding="utf-8")
|
||||
path.chmod(path.stat().st_mode | stat.S_IXUSR)
|
||||
return path
|
||||
|
||||
|
||||
def test_parse_version_output():
|
||||
assert parse_version_output("ClamAV 1.4.3/27452/Wed Oct 22 07:17:23 2026") == (
|
||||
"1.4.3", "27452", "Wed Oct 22 07:17:23 2026")
|
||||
assert parse_version_output("ClamAV 1.4.0") == ("1.4.0", "", "")
|
||||
assert parse_version_output("nonsense") == ("", "", "")
|
||||
|
||||
|
||||
def test_parse_extra_options_handles_windows_quotes():
|
||||
options = parse_extra_options(
|
||||
'--max-filesize=200M --exclude-dir="C:\\Program Files\\Skip" '
|
||||
'--database="C:\\My DB"')
|
||||
assert options == [
|
||||
"--max-filesize=200M",
|
||||
"--exclude-dir=C:\\Program Files\\Skip",
|
||||
"--database=C:\\My DB",
|
||||
]
|
||||
|
||||
|
||||
def test_parse_extra_options_refuses_destructive_flags():
|
||||
for text in ("--remove=yes", "--move=C:\\Quarantine", "--copy=/tmp"):
|
||||
with pytest.raises(ValueError):
|
||||
parse_extra_options(text)
|
||||
|
||||
|
||||
def test_parse_extra_options_rejects_malformed_quoting():
|
||||
with pytest.raises(ValueError):
|
||||
parse_extra_options('--exclude-dir="unterminated')
|
||||
|
||||
|
||||
def test_build_scan_args_uses_managed_database_and_excludes(tmp_path):
|
||||
db = tmp_path / "db"
|
||||
db.mkdir()
|
||||
(db / "main.cvd").write_bytes(b"x")
|
||||
quarantine = tmp_path / "quarantine"
|
||||
temp = tmp_path / "temp"
|
||||
args = build_scan_args(
|
||||
Path("/usr/bin/clamscan"), [tmp_path / "target"],
|
||||
database_dir=db, quarantine_dir=quarantine, temp_dir=temp,
|
||||
extra_options=["--max-filesize=200M"])
|
||||
assert args[0].endswith("clamscan")
|
||||
assert "--recursive=yes" in args
|
||||
assert "--infected" in args
|
||||
assert f"--database={db}" in args
|
||||
assert any(arg.startswith("--exclude-dir=") for arg in args)
|
||||
assert "--max-filesize=200M" in args
|
||||
assert args[-1] == str(tmp_path / "target")
|
||||
|
||||
|
||||
def test_build_scan_args_omits_database_when_no_signatures(tmp_path):
|
||||
args = build_scan_args(Path("/usr/bin/clamscan"), [tmp_path])
|
||||
assert not any(arg.startswith("--database=") for arg in args)
|
||||
|
||||
|
||||
def test_parse_clamscan_line_detection_windows_path():
|
||||
event = parse_clamscan_line(
|
||||
r"C:\Users\aaron\Downloads\invoice.pdf.exe: Win.Trojan.Agent-123 FOUND")
|
||||
assert event == {
|
||||
"kind": "detection",
|
||||
"path": r"C:\Users\aaron\Downloads\invoice.pdf.exe",
|
||||
"signature": "Win.Trojan.Agent-123",
|
||||
}
|
||||
|
||||
|
||||
def test_parse_clamscan_line_summary_progress_and_errors():
|
||||
assert parse_clamscan_line("Scanned files: 18234")["key"] == "scanned_files"
|
||||
assert parse_clamscan_line("Scanned files: 18234")["value"] == "18234"
|
||||
assert parse_clamscan_line("Engine version: 1.4.3")["value"] == "1.4.3"
|
||||
assert parse_clamscan_line("Scanning /home/a/file")["kind"] == "progress"
|
||||
assert parse_clamscan_line("/home/a/file: Access denied")["kind"] == "error"
|
||||
assert parse_clamscan_line("/home/a/file: Size limit exceeded")["kind"] == "skipped"
|
||||
assert parse_clamscan_line("") is None
|
||||
|
||||
|
||||
def test_scan_targets_deduplicates_injected_profiles(tmp_path):
|
||||
one = tmp_path / "one"
|
||||
two = tmp_path / "two"
|
||||
one.mkdir()
|
||||
two.mkdir()
|
||||
targets = scan_targets("homes", profiles=[one, two, one, Path("/does/not/exist")])
|
||||
assert targets == [one.resolve(), two.resolve()]
|
||||
|
||||
|
||||
def test_scan_targets_full_uses_injected_drives(tmp_path):
|
||||
drive = tmp_path / "drive"
|
||||
drive.mkdir()
|
||||
assert scan_targets("full", drives=[drive]) == [drive.resolve()]
|
||||
|
||||
|
||||
def test_database_detection_and_age(tmp_path):
|
||||
db = tmp_path / "db"
|
||||
db.mkdir()
|
||||
assert database_files(db) == []
|
||||
main = db / "main.cvd"
|
||||
main.write_bytes(b"signatures")
|
||||
old = time.time() - 3 * 86400
|
||||
os.utime(main, (old, old))
|
||||
assert database_files(db) == [main]
|
||||
date, age = database_age(db)
|
||||
assert date
|
||||
assert 2.8 < age < 3.2
|
||||
|
||||
|
||||
def test_clamav_service_reads_a_fake_engine(tmp_path):
|
||||
fake = _script(tmp_path / "clamscan", 'echo "ClamAV 1.4.3/27452/Wed Oct 22 07:17:23 2026"\n')
|
||||
service = ClamAVService(str(fake))
|
||||
info = service.info(force=True)
|
||||
assert info.available is True
|
||||
assert info.engine_version == "1.4.3"
|
||||
assert info.db_version == "27452"
|
||||
assert info.path == fake
|
||||
assert service.info(force=True).summary.startswith("1.4.3")
|
||||
|
||||
|
||||
def test_find_clamscan_accepts_a_file_or_directory(tmp_path):
|
||||
fake = _script(tmp_path / "clamscan")
|
||||
found, error = find_clamscan(str(fake))
|
||||
assert found == fake and error == ""
|
||||
found, error = find_clamscan(str(tmp_path))
|
||||
assert found == fake and error == ""
|
||||
missing, error = find_clamscan(str(tmp_path / "missing"))
|
||||
assert missing is None and "does not exist" in error
|
||||
|
||||
|
||||
def test_freshclam_arguments_target_managed_database(tmp_path):
|
||||
args = build_freshclam_args(Path("/usr/bin/freshclam"), tmp_path / "db")
|
||||
assert "--foreground" in args
|
||||
assert "--stdout" in args
|
||||
assert f"--datadir={tmp_path / 'db'}" in args
|
||||
Reference in New Issue
Block a user