1. Overview
The Anti-Ransomware realtime daemon implements BastionGuard’s filesystem-monitoring and rapid-response service for suspicious file activity. It combines inotify-based realtime monitoring, YARA scanning, delayed verification, archive inspection, and local authenticated alerting to detect ransomware-like behaviors during file creation, modification, rename, and late-stage write completion.
The service is designed for continuous execution on Linux desktop systems, with explicit support for cross-distro runtime behavior, safe recursive monitoring, best-effort process attribution, structured logging, and integration with local BastionGuard UI or companion components through a localhost TCP channel protected by a shared token.
The daemon integrates with:
- inotify – realtime recursive filesystem event monitoring
- YARA – signature/rule-based detection on files and suspected writer executables
- ArchiveInspector – sandboxed archive inspection for suspicious archive outputs
- DelayedScanQueue – deferred secondary verification after a fixed delay
- Local TCP alerting – authenticated localhost notifications on port
1025 - loginctl – active desktop-session user detection
- glib i18n – translated runtime logging via
_()
2. Global Runtime Controls and Logging
2.1 Lifecycle Flag and Signal Handling
The daemon uses a global atomic lifecycle flag:
static std::atomic<bool> running(true);
Termination is handled through standard process signals:
SIGINT→signal_handler()setsrunning = falseSIGTERM→signal_handler()setsrunning = false
The shutdown flow joins the realtime and delayed worker threads, then calls clean_shutdown(), which is currently a placeholder hook for future cleanup logic.
2.2 Log File Management
Operational logs are appended to:
/var/log/BastionGuard/antiransom_inotify.log
The helper ensure_log_dir() guarantees:
- Creation of
/var/log/BastionGuardif missing - Directory permissions
0755 - Creation of the log file if missing
- Log file permissions
0644
Messages are written through log_msg(), which prefixes each entry with a local-time timestamp in the format:
[YYYY-MM-DD HH:MM:SS] message
2.3 PID Attribution Rate Limiting
The current implementation introduces a best-effort PID attribution rate limiter using:
g_pid_rl_mtx– mutex protecting attribution timing stateg_pid_last_ms– last attribution attempt timestamp per path
The helper should_guess_pid(path, min_interval_ms) prevents repeated PID lookups on the same path within a short interval. The current usage applies a default interval of about 600 ms for writer-related events.
3. Desktop User Detection
3.1 Purpose
The daemon targets an active desktop-user context in order to:
- Resolve the correct home directory to monitor
- Copy the shared ransomware token into the user runtime area
- Build a desktop-relevant watch list
3.2 Detection Strategy
The function detect_desktop_user() identifies the active graphical user via loginctl. The detection flow is:
- Enumerate users through:
loginctl list-users --no-legend | awk '{print $2}'
- For each candidate user, query:
loginctl show-user <user> --property=State --property=Display
- Select the first user where:
State=activeDisplayis not empty
When a valid graphical user is found, the daemon logs the selected identity and uses it as the desktop security context.
4. Security Token Management and Local Alerting
4.1 Token Purpose and Paths
A shared security token is used to authenticate local alerts sent over TCP to a localhost listener. The token is stored at two levels:
- Root token:
/etc/BastionGuard/ransomware.token
- User copy:
~/.local/share/BastionGuard/ransomware.token
This allows the realtime scanner and UI-side components to share a trust secret for local event delivery.
4.2 Load-or-Create Logic
The function load_or_create_token() implements the following logic:
- If
/etc/BastionGuard/ransomware.tokenexists:- Read the first line into
SECURITY_TOKEN - Log successful load
- Copy the token to the active desktop user
- Read the first line into
- If the file is missing:
- Create
/etc/BastionGuardif needed - Read 32 random bytes from
/dev/urandom - Encode them into a 44-character Base64-like token
- Write the token to
/etc/BastionGuard/ransomware.token - Apply permissions
0640and ownershiproot:root - Copy the token to the active desktop user
- Create
4.3 Token Copy to User Profile
The helper copy_token_to_user():
- Detects the active desktop user
- Resolves the user home directory via
getpwnam() - Creates:
~/.local/share/BastionGuard
- Writes:
~/.local/share/BastionGuard/ransomware.token
- Applies user ownership and permissions
0644
4.4 Local Socket Alert Protocol
Realtime detections are reported over a localhost TCP socket:
- Destination:
127.0.0.1:1025 - Transport:
AF_INET/SOCK_STREAM
The generic YARA alert sender send_socket_alert(file, family) transmits messages in the format:
<TOKEN>|<file_or_exe_path>|<family>
Common family values emitted by the current implementation include:
YARA_PROCESS_MATCHYARA_FILE_MATCH
Archive-related detections are sent through a dedicated helper, send_archive_alert(), which formats the family field as:
ARCHIVE_<reason_code>
5. Process Attribution and File Classification
5.1 Best-Effort Writer PID Detection
The daemon attempts to attribute a file event to a process through guess_writer_pid(path). The function scans:
/proc/<pid>/fdfor all numeric PIDs- Each file-descriptor symlink via
readlink()
If any open descriptor resolves exactly to the affected file path, the corresponding PID is returned.
This is a best-effort heuristic and is not guaranteed in all timing or race conditions.
5.2 Process Executable Resolution
The helper get_process_exe(pid) resolves:
/proc/<pid>/exe
This executable path is then passed to YARA for process-level scanning.
5.3 Safe Regular-File Check
The helper is_regular_file_safe(path) uses std::filesystem::is_regular_file(...) with an error code guard to avoid exceptions and unsafe path assumptions during archive inspection routing.
5.4 Archive Path Heuristics
The current implementation adds a dedicated archive-path heuristic through looks_like_archive_path(path). It lowercases the path and recognizes these suffixes:
.zip.7z.rar.tar.gz.tgz.bz2.xz
This heuristic is used only to decide whether archive inspection should be attempted; it is not a trusted magic-byte format validator.
6. YARA Engine Integration
6.1 Rule Loading
The embedded YaraEngine class encapsulates YARA initialization, compilation, scanning, and teardown.
Rule loading occurs through load_rules(dirpath), which:
- Calls
yr_initialize() - Creates a compiler with
yr_compiler_create() - Iterates the rule directory
- Loads files ending in
.yara - Compiles them through
yr_compiler_add_file() - Finalizes a consolidated
YR_RULESobject throughyr_compiler_get_rules()
The current implementation loads only .yara files, not .yar.
6.2 Scan Callback and Match Logging
The static callback used by YARA sets a match flag when it receives CALLBACK_MSG_RULE_MATCHING and logs the matched rule identifier in the format:
MATCH → <rule_identifier>
6.3 Supported Scan Targets
The engine supports two scan targets:
scan_pid(pid)– resolves and scans the executable behind/proc/<pid>/exescan_file(path)– scans the affected file path directly
Both scan methods use:
yr_rules_scan_file(..., SCAN_FLAGS_FAST_MODE, ...)
When a match is found, the engine sends a local alert through send_socket_alert().
6.4 Thread Safety
The YaraEngine protects its shared YR_RULES state through an internal mutex, ensuring that concurrent scans from the realtime thread and delayed queue do not race the engine state.
7. Immediate and Delayed Scanning Strategy
7.1 Reliable Immediate Scan Helper
The code still contains a stabilization helper, reliable_scan(yara, path), intended to reduce false negatives when events fire before writes are fully committed.
Its behavior is:
- Up to
20attempts 50 msdelay between attempts- Checks that the file exists and has size greater than zero
- Applies an additional
20 msstabilization delay before scanning - Falls back to a final scan if the file still exists
In the current code path, realtime event handling calls yara.scan_file(full) directly rather than routing through reliable_scan(), so the helper exists but is not currently used in the main event loop.
7.2 DelayedScanQueue
The DelayedScanQueue performs secondary verification after a fixed delay. It is designed to catch behaviors such as:
- Multi-stage writes
- Late file replacement
- Atomic rename after encrypted output generation
- Post-write transformations
Each queue item stores:
pathwhen– a steady-clock execution timestamp
The default enqueue delay is:
30000 ms
which corresponds to 30 seconds.
7.3 Delayed Worker Loop
The delayed queue runs in its own thread. Every 500 ms, it wakes up, collects expired queue items, and rescans each path through yara.scan_file().
Lifecycle control is handled by:
std::atomic<bool> active{true}
Calling stop() sets active = false, allowing a clean thread shutdown.
8. Archive Inspection Integration
8.1 ArchiveInspector Initialization
The current daemon integrates the archive analysis subsystem through:
ArchiveInspector archive_inspector(
"/usr/libexec/bastionguard/archive_worker",
10
);
This means archive analysis is delegated to a sandboxed worker executable with a timeout of 10 seconds.
8.2 Archive Event Routing
The helper maybe_inspect_archive(full) is invoked for writer-related file events. Its flow is:
- Check whether the path looks like an archive via
looks_like_archive_path() - Ensure the target is a regular file through
is_regular_file_safe() - Run:
archive_inspector.inspect(full)
- Log the structured archive result through
log_archive_result() - If analysis failed, log the failure and stop
- If the worker result does not classify the file as ZIP, stop
- If the risk is not
CLEAN, emit an authenticated archive alert
8.3 Archive Alert Semantics
Archive alerts are emitted only when the inspection result indicates a non-clean archive risk. The alert family is built as:
ARCHIVE_<reason_code>
The archive result is also logged with fields such as:
successis_zipriskscorereasondetail
This is the main functional addition compared to the earlier daemon design.
9. Inotify Engine
9.1 Initialization
The InotifyEngine owns the realtime monitoring layer. It initializes an inotify instance using:
inotify_init1(IN_NONBLOCK)
Watch descriptors are stored in:
std::map<int, std::string> watch_map
and protected by map_mutex.
9.2 Watch Registration
Directory watches are added with the following broad mask:
IN_CREATE | IN_CLOSE_WRITE | IN_MODIFY |
IN_MOVED_TO | IN_MOVED_FROM | IN_ATTRIB |
IN_DELETE | IN_DELETE_SELF | IN_ISDIR
This covers the most relevant file lifecycle operations for ransomware-style write behavior, overwrite workflows, and post-write metadata changes.
9.3 Recursive Monitoring
The method add_recursive(root) traverses the directory tree with:
std::filesystem::recursive_directory_iterator(
root,
std::filesystem::directory_options::skip_permission_denied,
ec)
This ensures broad cross-distro compatibility while avoiding failures on protected or inaccessible subtrees.
9.4 Event Decoding
For diagnostic transparency, the engine decodes event masks into human-readable labels through decode_mask(). Logged names include:
CREATECLOSE_WRITEMODIFYMOVED_TOMOVED_FROMATTRIBDELETEDELETE_SELFISDIR
9.5 New Directory Handling
If an inotify event is flagged with IN_ISDIR, the engine immediately adds a watch for the new directory. This preserves recursive coverage for directories created after startup.
9.6 Relevant File Events
The engine currently treats the following mask group as relevant file events:
IN_CREATE | IN_CLOSE_WRITE | IN_MODIFY |
IN_MOVED_TO | IN_DELETE_SELF | IN_ATTRIB
Events outside this set are ignored for scanning purposes.
9.7 Atomic Rename Detection
An “atomic rename” condition is logged when:
IN_MOVED_TOis presentIN_CREATEis not present
This targets a common ransomware behavior where encrypted output is written to a temporary file and then moved into place.
9.8 Per-Event Detection Workflow
For writer-related events, currently defined as:
IN_CLOSE_WRITE or IN_MOVED_TO
the engine executes the following workflow:
- Throttle PID attribution through
should_guess_pid(full, 600) - If allowed, try to resolve the writer PID using
guess_writer_pid(full) - If a PID is found, scan the writer executable through
yara.scan_pid(pid) - Attempt sandboxed archive inspection through
maybe_inspect_archive(full) - Scan the file directly through
yara.scan_file(full) - Queue a delayed secondary scan through
delayed.enqueue(full)
This is an updated three-plus-one stage workflow: process scan, archive inspection when applicable, immediate file scan, and delayed file scan.
10. Monitored Directories
10.1 User-Scope Directories
After detecting the active desktop user and resolving the user home directory, the daemon constructs a safe-mode watch list. The current user-scope directories are:
$HOME$HOME/.cache$HOME/.local/share$HOME/.local/share/kate/swap$HOME/.config$HOME/.config/Code/Backups$HOME/.PlayOnLinux
10.2 Runtime-Scope Directories
The daemon also attempts to monitor runtime-user directories:
/run/user/<uid>/run/user/<uid>/doc
If these paths are inaccessible or protected, they are skipped and logged accordingly.
10.3 System Temporary Directories
The watch list also includes system temporary areas commonly abused during staging or encryption workflows:
/tmp/var/tmp
All selected directories are logged explicitly at startup for operational transparency.
11. Main Execution Flow
11.1 Startup Sequence
The daemon startup sequence is currently:
- Ensure the log directory and log startup banner
- Register signal handlers for
SIGINTandSIGTERM - Load or create the shared ransomware token
- Detect the active desktop user
- Resolve the user home directory via
getpwnam() - Validate that the home directory is accessible
- Build the monitored directory list
WATCH_DIRS - Load YARA rules from:
/usr/share/BastionGuard/data/yara/
- Initialize
ArchiveInspectorusing:
/usr/libexec/bastionguard/archive_worker
- Start the delayed scan thread
- Initialize the inotify engine
- Start the realtime thread
- Enter the main wait loop until
runningbecomes false
11.2 Shutdown Sequence
On termination request, the daemon performs the following shutdown flow:
- Log termination request
- Join the realtime inotify thread
- Stop the delayed queue
- Join the delayed queue thread
- Invoke
clean_shutdown() - Log successful daemon termination
12. Runtime and Security Considerations
- Authenticated local alerting: every outbound alert to the localhost receiver includes a shared token, reducing the risk of unauthenticated local event injection
- Layered detection: scanning both the file and the suspected writer process increases the chance of detecting active ransomware tooling
- Archive-aware detection: suspicious archives are now analyzed through a dedicated sandboxed worker, extending coverage beyond plain file-content YARA matches
- Delayed verification: delayed rescanning improves resilience against multi-stage writes, atomic replacement, and late content mutation
- Best-effort PID attribution: writer attribution improves context but is heuristic and intentionally rate-limited to avoid excessive /proc scanning overhead
- Cross-distro safety: recursive monitoring uses standard filesystem iterators with permission-denied skipping to avoid brittle distro-specific behavior
- Operational load: broad recursive watch coverage can generate a high event rate; tuning
WATCH_DIRSmay be required on low-end systems or I/O-heavy desktops - YARA rule scope: the current loader accepts only
.yarafiles;.yarsupport would require an explicit extension to the loader - Logging visibility: the daemon emits detailed operational logs useful for forensics and debugging, but production deployments should pair this with a log rotation policy
- Unused stabilization helper: although
reliable_scan()exists to reduce race-related false negatives, the current realtime path callsyara.scan_file()directly, so stabilization is not yet applied in the main hot path