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,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from boltclam.quarantine import QuarantineError, QuarantineService, delete_file, hash_file
|
||||
from boltclam.storage import Storage
|
||||
|
||||
|
||||
def _service(tmp_path) -> QuarantineService:
|
||||
storage = Storage(tmp_path / "history.db")
|
||||
return QuarantineService(storage, tmp_path / "quarantine")
|
||||
|
||||
|
||||
def test_quarantine_moves_records_and_writes_metadata(tmp_path):
|
||||
service = _service(tmp_path)
|
||||
source = tmp_path / "malware.exe"
|
||||
source.write_bytes(b"malware bytes")
|
||||
digest = hash_file(source)
|
||||
item = service.quarantine_file(source, "Win.Test", scan_id=7, reason="Unit test")
|
||||
|
||||
assert not source.exists()
|
||||
destination = Path(item.quarantine_path)
|
||||
assert destination.is_file()
|
||||
assert destination.name.endswith(".quarantine")
|
||||
assert item.sha256 == digest
|
||||
assert service.storage.get_quarantine(item.id).signature == "Win.Test"
|
||||
metadata = json.loads((service.root / f"{item.id}.json").read_text())
|
||||
assert metadata["original_path"] == str(source)
|
||||
assert metadata["reason"] == "Unit test"
|
||||
|
||||
|
||||
def test_restore_to_original_or_chosen_path(tmp_path):
|
||||
service = _service(tmp_path)
|
||||
source = tmp_path / "bad.exe"
|
||||
source.write_bytes(b"x")
|
||||
item = service.quarantine_file(source, "Sig")
|
||||
target = service.restore(item.id)
|
||||
assert target.read_bytes() == b"x"
|
||||
assert service.storage.get_quarantine(item.id).status == "restored"
|
||||
|
||||
source.write_bytes(b"y")
|
||||
item2 = service.quarantine_file(source, "Sig")
|
||||
chosen = tmp_path / "restored" / "safe.exe"
|
||||
restored = service.restore(item2.id, chosen)
|
||||
assert restored == chosen
|
||||
assert chosen.read_bytes() == b"y"
|
||||
|
||||
|
||||
def test_delete_quarantined_item_and_missing_original(tmp_path):
|
||||
service = _service(tmp_path)
|
||||
source = tmp_path / "bad.exe"
|
||||
source.write_bytes(b"x")
|
||||
item = service.quarantine_file(source, "Sig")
|
||||
service.delete(item.id)
|
||||
assert not Path(item.quarantine_path).exists()
|
||||
assert service.storage.get_quarantine(item.id).status == "deleted"
|
||||
with pytest.raises(QuarantineError):
|
||||
service.restore(item.id)
|
||||
|
||||
|
||||
def test_refuses_symlink_and_directory(tmp_path):
|
||||
service = _service(tmp_path)
|
||||
directory = tmp_path / "folder"
|
||||
directory.mkdir()
|
||||
with pytest.raises(QuarantineError):
|
||||
service.quarantine_file(directory, "Sig")
|
||||
real = tmp_path / "real.exe"
|
||||
real.write_bytes(b"x")
|
||||
link = tmp_path / "link.exe"
|
||||
try:
|
||||
link.symlink_to(real)
|
||||
except OSError:
|
||||
pytest.skip("symlinks unavailable")
|
||||
with pytest.raises(QuarantineError):
|
||||
service.quarantine_file(link, "Sig")
|
||||
with pytest.raises(QuarantineError):
|
||||
delete_file(directory)
|
||||
|
||||
|
||||
def test_delete_file_clears_readonly_bit(tmp_path):
|
||||
target = tmp_path / "readonly.exe"
|
||||
target.write_bytes(b"x")
|
||||
os.chmod(target, stat.S_IREAD)
|
||||
delete_file(target)
|
||||
assert not target.exists()
|
||||
|
||||
|
||||
def test_restore_refuses_to_overwrite(tmp_path):
|
||||
service = _service(tmp_path)
|
||||
source = tmp_path / "bad.exe"
|
||||
source.write_bytes(b"x")
|
||||
item = service.quarantine_file(source, "Sig")
|
||||
source.write_bytes(b"replacement")
|
||||
with pytest.raises(QuarantineError):
|
||||
service.restore(item.id)
|
||||
Reference in New Issue
Block a user