ClamGuardian is a Linux-first security framework built around ClamAV. It provides an asynchronous AV abstraction, encrypted quarantine, scan profiles, a persisted scan history, and native GObject event signalling, suitable for GTK4/Libadwaita applications and headless integrations.
v1.0.0 released (2026-10-16, tag
v1.0.0@745e1e7): the release gate is closed — both Flatpak manifests build offline againstorg.gnome.Platform//51, AppStream validates, real ClamAV scans (clean + EICAR viaclamscanandclamd) and the GTK smoke test pass in CI. Seedocs/RELEASE-GATE-1.0.mdfor the evidence anddocs/ROADMAP.mdfor what comes after 1.0.
GTK4 / Libadwaita UI
|
v
ShieldTaskController (GObject signals)
|
v
ScanTaskRunner (bounded asyncio tasks)
|
v
BaseAVEngine <---- ClamAVEngine
| |
/run/clamd.ctl clamscan/freshclam
|
Flatpak: flatpak-spawn --host
Security storage
|
QuarantineVault ---> AES-256-GCM encrypted records
- Hybrid ClamAV execution: prefers
/run/clamd.ctl, then falls back toclamscan/freshclamthroughasyncio.subprocess. - CLI / headless interface (M3):
clamguardian scan|profiles|version— a thin front-end over the same Core used by the GTK UI (seedocs/design/cli.md), with human and--jsonoutput, centralized exit codes and M0.5-grade Ctrl+C cancellation. - Scan profiles (M2): immutable, validated
ScanProfilepresets (quick,home,full,custom) shared by every front-end; the UI never touches ClamAV CLI flags (seedocs/design/scan-profiles.md). - Flatpak-aware execution: optionally routes host commands through
flatpak-spawn --host. - Reactive event API:
ShieldTaskControlleremitsscan-started,scan-finished,threat-detected, andscan-cancelledas native GObject signals. - Encrypted quarantine: AES-256-GCM authenticated encryption, owner-only vault permissions, atomic writes, and integrity-protected metadata.
- Async concurrency: bounded multi-slot scan runner with cancellation support.
- Python 3.11+: typed, packaged with Hatchling, and tested by pytest.
Download the installable bundle from the Release Gate run on the v1.0.0 tag
(ClamGuardian-v1.0.0-linux-x86_64.flatpak, ~4.7 MB):
flatpak install --user ClamGuardian-v1.0.0-linux-x86_64.flatpakPrerequisites on the host: a working ClamAV installation (clamscan,
freshclam, optionally the clamd daemon) — the sandbox reaches it through
flatpak-spawn --host and the clamd UNIX socket — and the GNOME Platform 51
runtime (installed automatically from Flathub on first run).
Core dependencies are intentionally minimal (cryptography, aiohttp): PyGObject lives in the optional [gtk] extra, so headless machines can install and test the framework without building GTK bindings. A uv.lock is provided for reproducible environments.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install '.[test]' # headless (core framework + tests)
python -m pip install '.[test,gtk]' # full desktop (adds PyGObject >= 3.46)With uv the locked environments are:
uvx --with '.[test]' pytest # headless CI path
uvx --with '.[test,gtk]' pytest # full GTK pathOn Debian/Ubuntu systems, install PyGObject build prerequisites if pip needs to compile it. The gtk extra allows PyGObject >= 3.46; versions 3.53 and newer require the girepository-2.0 bindings (libgirepository-2.0-dev):
sudo apt-get update
sudo apt-get install -y libgirepository-2.0-dev gir1.2-gtk-4.0 gir1.2-adw-1 libcairo2-dev pkg-config python3-dev meson ninja-buildA working ClamAV installation is required for real scans. The framework itself does not install or configure the antivirus daemon.
Installing the package provides the clamguardian console command (also available as python -m clamguardian.cli):
uv run clamguardian --help
# or, after pip install:
clamguardian --helpThe CLI shares the exact Core of the GTK UI: profiles come from ScanProfileRegistry, scans run through ScanTaskRunner and ClamAVEngine. It never spawns its own subprocesses and never duplicates scan logic.
clamguardian scan PATH # scan a file or directory
clamguardian scan PATH --profile quick # quick / home / full / custom
clamguardian scan PATH --json # machine-readable report on stdout
clamguardian profiles [--json] # list profiles from the Core registry
clamguardian versionWithout --profile the Core's default profile is used (id default, Standard Scan). Profiles are resolved exclusively from clamguardian.core.profiles.DEFAULT_REGISTRY.
Example session:
$ clamguardian scan ~/Downloads --profile quick
ClamGuardian
Profile: Quick Scan
Target: /home/user/Downloads
Scanning...
Scan completed
Status: CLEAN
Engine: clamscan
Duration: 2.41s
scan --json prints exactly one JSON object on stdout (diagnostics go to stderr). Guaranteed, deterministic keys:
{
"status": "clean",
"profile": "quick",
"target": "/home/user/Downloads",
"engine": "clamscan",
"threats": [],
"files_scanned": 0,
"duration_seconds": 2.41,
"error": null,
"metadata": {"profile_id": "quick"}
}| Code | Meaning |
|---|---|
| 0 | scan completed successfully / clean |
| 1 | threats detected |
| 2 | usage / CLI argument error |
| 3 | engine or runtime error |
| 4 | scan timeout |
| 5 | scan cancelled (Ctrl+C) |
Pressing Ctrl+C during a scan triggers the M0.5 cancellation lifecycle: the ClamAV subprocess group receives SIGTERM (then SIGKILL after a grace period), the process is reaped, and the CLI exits with code 5. No zombies, no orphans.
import asyncio
from pathlib import Path
from clamguardian.engines import ClamAVEngine
async def main() -> None:
engine = ClamAVEngine()
result = await engine.scan(Path.home() / "Downloads")
print(result.clean, result.threats, result.engine)
asyncio.run(main())The vault key must be supplied by the application. For production deployments, persist the key in an OS-backed secret store rather than a plaintext configuration file.
from pathlib import Path
from clamguardian.core import QuarantineVault
key = QuarantineVault.generate_key()
vault = QuarantineVault(Path("~/.local/share/clamguardian/quarantine").expanduser(), key)
entry = vault.quarantine(Path("/path/to/suspicious-file"))
vault.restore(entry.item_id)ClamAV's official signatures, updated by freshclam, do not cover every threat feed. Security researchers and communities publish additional ClamAV-compatible signature databases, and ClamGuardian lets you manage them alongside the official one:
- Official vs third-party: the official database is owned by the ClamAV engine/freshclam and is never touched by ClamGuardian's database manager. Third-party sources are fully managed — downloaded, verified, installed, updated and removed by
DatabaseManager. - Add: from the UI (
Antivirus Databases → Third-party → Add database) or programmatically viaDatabaseManager.add_source(). A source requires an ID, a name, an HTTPS URL, a safe plain filename, and optionally a SHA-256 digest. - Update: each row has an Update button (
await manager.update(source_id)) and the page header offers Update all for every enabled, auto-update source (await manager.update_enabled()). - SHA-256 verification: if a digest is provided, the downloaded artifact is hashed before installation; a mismatch aborts the update.
- HTTPS only: artifact URLs must use
https://; plain HTTP is rejected byDatabaseSourcevalidation. - Atomic install & rollback: downloads go to a staging directory and are moved into place only after verification. If a step fails, the previously installed database files are restored from a backup, so scanning never loses its current signatures.
- Enable / auto-update: each source can be disabled individually or excluded from bulk updates, with the choice persisted in the manager state file and restored on the next launch.
- Remove: deleting a source also deletes its managed database files after an explicit confirmation dialog.
The database management flow is a real GTK4/Libadwaita navigation:
Antivirus Databases
|
+-- ClamAV Official (managed by freshclam)
|
+-- Third-party
|
+-- Database rows (status, enable, auto-update, update, remove)
+-- Add database (Adw.Dialog)
AntivirusDatabasesPageis aAdw.PreferencesPageembedded in anAdw.NavigationView; the Third-party row pushesThirdPartyDatabasesPage.ThirdPartyDatabasesPagelists oneAdw.ActionRowper source with live status (Available / Installed / Disabled / Updating… / Update error), enable and auto-update switches, per-row update, and a confirmed remove viaAdw.AlertDialog. An empty state (Adw.StatusPage) opens the add dialog directly.- All operations run through an asyncio/GTK bridge (
run_async) that never blocks the main loop; the UI always renders the realDatabaseManagerstate. - The UI module is import-safe on headless systems: without PyGObject the page classes fail only when instantiated, so
pytestworks on CI without GTK.
from clamguardian.ui import build_database_navigation
from clamguardian.databases import DatabaseManager
manager = DatabaseManager(db_dir, state_file)
view = build_database_navigation(manager) # Adw.NavigationView, ready to embed- Quarantine data is authenticated and encrypted with AES-256-GCM.
- Each record uses a fresh 96-bit nonce.
- Metadata is authenticated as AES-GCM associated data, preventing undetected metadata tampering.
- Vault directories are created with
0700; record and restored-file writes use0600. - External commands are executed without a shell, reducing command-injection exposure.
Run the local test suite with:
pytestLint and type-check with:
ruff check .
mypy clamguardianGitHub Actions (.github/workflows/ci.yml) triggers on every pull request and on pushes to main, feature and development. The test job installs the GTK/PyGObject system dependencies the package needs and runs ruff check ., mypy clamguardian and pytest on Python 3.11, 3.12 and 3.13. A separate audit job verifies that uv.lock matches pyproject.toml (uv lock --check) and runs pip-audit --strict against the pinned runtime dependency set exported from the lockfile; see docs/M6-CI-GATE-REPORT.md.
A second workflow, .github/workflows/release-gate.yml, is the automated 1.0
release gate. It runs on pushes to feature and on v* tags and has 7 jobs:
preflight (branch/HEAD/clean-tree check), appstream (appstreamcli validate), clamav (fresh DB, clean + EICAR via clamscan and clamdscan),
gtk-smoke (UI construction + database pages under Xvfb), flatpak-local
(real offline build of the working-tree manifest against org.gnome.Platform//51
- installable
.flatpakbundle),flatpak-release(same for the tag-pinned release manifest, only on tags), andgate(fails unless every required job succeeded, and uploadsgate-evidence.txt). The local bundle is kept 14 days, the release bundle 90 days. Evidence:docs/RELEASE-GATE-1.0.md.
Development follows the flow feature → testing → development → main:
| Branch | Role | Protection |
|---|---|---|
feature |
Active development of new functionality | open |
testing |
Integration and QA of features | PR-only, no force push/delete |
development |
Stable pre-release integration | PR-only, no force push/delete |
main |
Production; releases are cut from here | PR-only, 1 approving review, stale-review dismissal, conversations must be resolved |
main and development (and testing) are protected: changes land exclusively through pull requests targeting the next branch in the chain.
This repository is the core framework. A complete desktop product can build its GTK4/Libadwaita presentation layer, portal permissions, service integration, USB monitoring, real-time/on-access monitoring, and operational policy on top of these primitives.
GPL-3.0-or-later. See LICENSE.