Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ libc = "0.2"
pelite = "0.10"
memmap2 = "0.9"
ferrisetw = "1.2"
windows = { version = "0.62.2", features = ["Win32_Foundation", "Win32_System_Diagnostics_Etw", "Win32_Security", "Win32_Security_Authorization", "Win32_System_Threading", "Win32_System_Diagnostics_ToolHelp", "Win32_System_ProcessStatus", "Win32_Storage_FileSystem"] }
windows = { version = "0.62.2", features = ["Win32_Foundation", "Win32_System_Diagnostics_Etw", "Win32_Security", "Win32_Security_Authorization", "Win32_System_Threading", "Win32_System_Diagnostics_ToolHelp", "Win32_System_ProcessStatus", "Win32_Storage_FileSystem", "Win32_System_Memory", "Win32_System_Diagnostics_Debug"] }
windows-service = "0.8.0"

[dev-dependencies]
Expand Down
17 changes: 17 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ sigma_rules_path = "rules/sigma"
yara_enabled = true
yara_rules_path = "rules/yara"

# Memory scanning is off by default.
# yara_memory_enabled = false
# yara_memory_delay_ms = 750
# yara_memory_max_process_mb = 64
# yara_memory_max_region_mb = 8
# yara_memory_include_private = true
# yara_memory_include_image = false
# yara_memory_include_mapped = false

[reload]
enabled = true
debounce_ms = 2000
Expand Down Expand Up @@ -124,6 +133,14 @@ These defaults feed `allowlist.paths`, which then propagate to active response,
| `yara_enabled` | `true` | Enable YARA scanning |
| `yara_rules_path` | `rules/yara` | YARA rules directory |
| `yara_allowlist_paths` | inherits `allowlist.paths` | Prefix paths skipped by YARA queueing and scanning |
| `yara_memory_enabled` | `false` | Enable YARA memory scanning (requires `yara_enabled = true`) |
| `yara_memory_queue_capacity` | `64` | Maximum pending memory scan jobs before new ones are dropped |
| `yara_memory_delay_ms` | `750` | Milliseconds to wait after process start before reading memory |
| `yara_memory_max_process_mb` | `64` | Stop reading a process once this many MB have been accumulated |
| `yara_memory_max_region_mb` | `8` | Clamp each region read to this many MB |
| `yara_memory_include_private` | `true` | Scan private (anonymous) memory regions |
| `yara_memory_include_image` | `false` | Scan image-backed regions (loaded executables/DLLs) |
| `yara_memory_include_mapped` | `false` | Scan file-mapped regions |

### Reload

Expand Down
12 changes: 12 additions & 0 deletions docs/detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@ YARA scanning is shared across both supported platforms.

Every YARA match is emitted as a `critical` alert.

### YARA memory scanning

YARA memory scanning is optional and disabled by default (`scanner.yara_memory_enabled = false`).

When enabled, Rustinel queues process IDs from process-start events to a bounded background worker. The worker waits a configurable delay (`yara_memory_delay_ms`, default 750 ms) to allow packers or loaders to finish unpacking, then reads a limited amount of selected process memory and scans it with the active YARA ruleset.

Default behavior scans private readable memory only and avoids mapped or image-backed regions to reduce overhead and false positives. Each matching YARA rule emits its own `critical` alert. The alert `provider` field is set to `yara-memory` to distinguish memory hits from file hits (`etw` or `ebpf`).

Memory scanning follows the same allowlist as file YARA: process paths allowlisted via `scanner.yara_allowlist_paths` are not queued for memory scanning either.

The worker uses `try_send` so a full queue drops jobs rather than blocking the sensor event path.

## IOC

The IOC engine hot reloads indicator files and splits work between inline event checks and a background hash worker.
Expand Down
31 changes: 31 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,37 @@ Typical symptom in logs:
YARA queue full; dropping scan job
```

### YARA memory scan produces no alerts

Check these first:

- `scanner.yara_memory_enabled` must be `true` and `scanner.yara_enabled` must also be `true`
- the process path may be allowlisted via `scanner.yara_allowlist_paths`
- the memory scan queue may have been full and the job dropped (look for `YARA memory queue full; dropping scan job`)
- the process may have exited before the scan ran (the worker waits `yara_memory_delay_ms` first)
- per-region or per-process byte caps may have prevented reading the region containing the match
- insufficient privileges may prevent reading process memory (see below)

#### Linux memory scanning privileges

On Linux, reading `/proc/<pid>/mem` typically requires root or the `CAP_SYS_PTRACE` capability. You may also need to set a permissive ptrace scope:

```bash
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
```

Without adequate privileges, region reads will fail silently (logged at `trace`).

#### Windows memory scanning privileges

On Windows, `OpenProcess` with `PROCESS_VM_READ` may fail for:

- protected processes (`PROTECTED_PROCESS_LIGHT` or `PROTECTED_PROCESS`)
- system processes with elevated integrity levels
- some anti-tamper or security software

These failures are logged at `trace` and do not affect other detection paths.

### IOC hash matching did not fire

Hash matching is more selective than inline IOC checks.
Expand Down
37 changes: 37 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ pub struct ScannerConfig {
pub yara_enabled: bool,
pub yara_rules_path: PathBuf,
pub yara_allowlist_paths: Vec<String>,
pub yara_memory_enabled: bool,
pub yara_memory_queue_capacity: usize,
pub yara_memory_delay_ms: u64,
pub yara_memory_max_process_mb: u64,
pub yara_memory_max_region_mb: u64,
pub yara_memory_include_private: bool,
pub yara_memory_include_image: bool,
pub yara_memory_include_mapped: bool,
}

/// Global allowlist configuration shared across modules
Expand Down Expand Up @@ -145,6 +153,14 @@ impl AppConfig {
.set_default("scanner.yara_enabled", true)?
.set_default("scanner.yara_rules_path", "rules/yara")?
.set_default("scanner.yara_allowlist_paths", Vec::<String>::new())?
.set_default("scanner.yara_memory_enabled", false)?
.set_default("scanner.yara_memory_queue_capacity", 64i64)?
.set_default("scanner.yara_memory_delay_ms", 750i64)?
.set_default("scanner.yara_memory_max_process_mb", 64i64)?
.set_default("scanner.yara_memory_max_region_mb", 8i64)?
.set_default("scanner.yara_memory_include_private", true)?
.set_default("scanner.yara_memory_include_image", false)?
.set_default("scanner.yara_memory_include_mapped", false)?
// Logging
.set_default("logging.level", "info")?
.set_default("logging.directory", "logs")?
Expand Down Expand Up @@ -215,6 +231,14 @@ impl Default for AppConfig {
yara_enabled: true,
yara_rules_path: PathBuf::from("rules/yara"),
yara_allowlist_paths: Vec::new(),
yara_memory_enabled: false,
yara_memory_queue_capacity: 64,
yara_memory_delay_ms: 750,
yara_memory_max_process_mb: 64,
yara_memory_max_region_mb: 8,
yara_memory_include_private: true,
yara_memory_include_image: false,
yara_memory_include_mapped: false,
},
logging: LogConfig {
level: "info".to_string(),
Expand Down Expand Up @@ -307,6 +331,19 @@ mod tests {
assert_eq!(cfg.scanner.yara_allowlist_paths, cfg.allowlist.paths);
}

#[test]
fn test_yara_memory_defaults_disabled() {
let cfg = AppConfig::default();
assert!(!cfg.scanner.yara_memory_enabled);
assert_eq!(cfg.scanner.yara_memory_queue_capacity, 64);
assert_eq!(cfg.scanner.yara_memory_max_process_mb, 64);
assert_eq!(cfg.scanner.yara_memory_max_region_mb, 8);
assert_eq!(cfg.scanner.yara_memory_delay_ms, 750);
assert!(cfg.scanner.yara_memory_include_private);
assert!(!cfg.scanner.yara_memory_include_image);
assert!(!cfg.scanner.yara_memory_include_mapped);
}

#[test]
fn test_module_specific_allowlist_not_overwritten() {
let mut cfg = AppConfig::default();
Expand Down
Loading
Loading