Phase 7 gave us path normalization, openat2 coverage, risk-based approval workflows, and an SQLite audit trail. But a security audit (docs/security/security-limitations.md) identified 2 CRITICAL, 3 HIGH, and 14 MEDIUM vulnerabilities remaining across six categories. Phase 8 implements 16 concrete fixes from docs/security/security-fixes.md, organized into four sub-phases by priority.
Problem 1: File manipulation bypasses. Guardian monitors open/openat/openat2 but not rename, unlink, or link. An agent can move a denied file to an allowed directory (os.rename("/etc/shadow", "/tmp/shadow")), read it there, and move it back. Hardlinks create same-inode copies in allowed paths. Unlink attacks can delete Guardian's own config or audit database.
Problem 2: io_uring and memfd_create bypasses. io_uring performs file I/O via ring buffers in shared memory, bypassing all syscall tracepoints entirely — Guardian sees nothing. memfd_create + execveat(AT_EMPTY_PATH) creates and executes in-memory binaries with no filesystem path for exec policy to match. Both are known blind spots in syscall-based security tools.
Problem 3: BPF map capacity and path truncation. With only 256 entries per map, complex policies silently drop rules. Paths longer than 256 bytes are silently truncated — policy evaluates against an incomplete path. An agent can craft paths designed to appear benign when truncated.
Problem 4: Exec evasion via dynamic linkers. An agent can invoke the dynamic linker directly (/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/bin/curl) — Guardian's bprm_check_security sees only the linker path (allowed), not the real binary (curl, which should be denied).
Problem 5: No fail-closed mode, no dashboard auth, no strict mode. Every eBPF error silently allows access (fail-open). The dashboard has zero authentication — anyone reaching the port can approve permissions, stop agents, or weaken policy. If LSM hooks fail to load in enforce mode, Guardian silently degrades to monitor-only with a log warning.
Problem 6: Approval workflow gaps. Fixed 120-second timeout for all risk levels. No CLI-based approval (dashboard required). No anomaly detection on approval patterns. No limit on accumulated grant duration — a patient agent can maintain permanent access through repeated renewals.
| File | Lines Changed | What |
|---|---|---|
guardian-ebpf/src/main.rs |
+180 | 6 new BPF maps, 3 tracepoints (rename/unlink/link), 3 LSM hooks (inode_rename/unlink/link), execveat tracepoint, dynamic linker detection, fail-closed map, path truncation flags |
guardian-common/src/lib.rs |
+25 | MAX_POLICY_RULES 64→1024, EVENT_FLAG_TRUNCATED constant, status_flags field on FileAccessEvent, 5 new map name constants |
guardian-launch/src/main.rs |
+55 | apply_seccomp_filter() blocking io_uring (syscalls 425-427) + memfd_create (319) with EPERM |
guardian-launch/Cargo.toml |
+1 | seccompiler = "0.4" dependency |
guardian/src/main.rs |
+90 | Load/attach new tracepoints and LSM hooks, populate_dynamic_linkers(), default /memfd: exec deny, truncation warning logging, strict mode bail |
| File | Lines Changed | What |
|---|---|---|
guardian-ebpf/src/main.rs |
(included above) | DYNAMIC_LINKERS map, argv[1] inspection in exec monitor, execveat tracepoint with AT_EMPTY_PATH detection |
guardian/src/config.rs |
+30 | auth_token in DashboardConfig, "strict" mode validation, fail_closed in AgentConfig |
guardian/src/dashboard/mod.rs |
+45 | auth_middleware() — Bearer token + ?token= query param, skips /metrics and /static/ |
guardian/src/dashboard/state.rs |
+1 | auth_token: Option<String> field |
| File | Lines Changed | What |
|---|---|---|
guardian/src/config.rs |
+40 | RiskTimeoutConfig struct (low/medium/high/critical), max_grant_total_secs field, timeouts on PermissionsConfig |
guardian/src/permissions.rs |
+120 | timeout_secs() on RiskLevel, GrantAccumulator struct, weighted SUSPICIOUS_PATTERNS, graduated justification_risk_bump(), AnomalyDetector struct |
guardian/src/ipc.rs |
+100 | Risk-based timeouts, grant accumulation check, handle_list_pending(), handle_approve_permission(), handle_deny_permission() |
guardian-common/src/lib.rs |
+30 | ListPending, ApprovePermission, DenyPermission IPC variants, PendingPermissions response, PendingPermissionInfo struct |
guardian-ctl/src/main.rs |
+45 | pending, approve, deny subcommands with formatted table output |
| File | Lines Changed | What |
|---|---|---|
guardian/src/dashboard/db.rs |
+70 | approval_rate_24h(), high_volume_agents_24h(), agents_with_deny_then_approve() SQL queries |
guardian/src/main.rs |
+20 | Hourly anomaly detection background task, SIGHUP reload comment |
Guardian already used the tracepoint-PENDING-LSM pattern for file open and exec. Phase 8 extends this to three additional inode operations:
Tracepoint BPF Map LSM Hook
───────────────── ──────────────── ─────────────────
sys_enter_renameat2 ──SET──> PENDING_RENAME_DENY ──CHK──> inode_rename
sys_enter_unlinkat ──SET──> PENDING_UNLINK_DENY ──CHK──> inode_unlink
sys_enter_linkat ──SET──> PENDING_LINK_DENY ──CHK──> inode_link
Each tracepoint reads the path from syscall arguments, evaluates it against the deny/allow policy maps, and sets the corresponding PENDING map entry if denied. The LSM hook fires later in the same syscall, checks the PENDING map, and returns -EACCES to block the operation.
New BPF maps (3):
| Map | Type | Max Entries | Purpose |
|---|---|---|---|
PENDING_RENAME_DENY |
HashMap<u64, u8> |
4096 | pid_tgid → deny flag for rename |
PENDING_UNLINK_DENY |
HashMap<u64, u8> |
4096 | pid_tgid → deny flag for unlink |
PENDING_LINK_DENY |
HashMap<u64, u8> |
4096 | pid_tgid → deny flag for hardlink |
New eBPF programs (6):
| Program | Hook Point | Function |
|---|---|---|
guardian_rename_monitor |
sys_enter_renameat2 |
Read source/dest paths, evaluate policy, set PENDING_RENAME_DENY |
guardian_unlink_monitor |
sys_enter_unlinkat |
Read path, evaluate deny policy, set PENDING_UNLINK_DENY |
guardian_link_monitor |
sys_enter_linkat |
Read source/dest paths, evaluate policy, set PENDING_LINK_DENY |
guardian_enforce_rename |
LSM inode_rename |
Check + consume PENDING_RENAME_DENY, return -EACCES if set |
guardian_enforce_unlink |
LSM inode_unlink |
Check + consume PENDING_UNLINK_DENY, return -EACCES if set |
guardian_enforce_link |
LSM inode_link |
Check + consume PENDING_LINK_DENY, return -EACCES if set |
All six programs use graceful fallback — if LSM attachment fails, a warning is logged and the daemon continues. In strict mode, failure to attach any LSM hook causes the daemon to exit.
Since io_uring operations bypass all syscall tracepoints and memfd_create enables fileless exec, Phase 8 blocks these at the seccomp level in guardian-launch:
guardian-launch startup flow:
┌──────────────────┐
│ 1. Parse CLI args│
│ 2. Create cgroup │
│ 3. Set resources │
│ 4. Register IPC │
│ 5. Move to cgroup│
└────────┬─────────┘
│
▼
┌──────────────────────────────────────┐
│ 6. apply_seccomp_filter() │
│ Block with EPERM: │
│ - io_uring_setup (syscall 425)│
│ - io_uring_enter (syscall 426)│
│ - io_uring_register (syscall 427)│
│ - memfd_create (syscall 319)│
│ Default: Allow everything else │
└────────┬─────────────────────────────┘
│
▼
┌──────────────────┐
│ 7. exec(agent) │
│ Agent inherits │
│ seccomp filter │
└──────────────────┘
The filter is applied after cgroup setup but before exec(), so the agent process and all its children inherit the restriction. The seccomp filter is compiled via seccompiler crate using BpfProgram with SeccompAction::Errno(EPERM) for matched syscalls and SeccompAction::Allow as the default.
Architecture portability: The filter uses std::env::consts::ARCH.try_into() for the target architecture instead of hardcoding x86_64. The seccomp application is best-effort — if it fails (e.g., seccomp not available), a warning is logged and the agent launches without the filter.
Map capacity increase (256 → 1024):
All rule and config maps were increased from 256 to 1024 entries:
// Before (Phase 7):
static DENY_PREFIXES: LpmTrie<...> = LpmTrie::with_max_entries(256, 0);
// After (Phase 8):
static DENY_PREFIXES: LpmTrie<...> = LpmTrie::with_max_entries(1024, 0);Affected maps: WATCHED_COMMS, ENFORCE_COMMS, WATCHED_CGROUPS, ENFORCE_CGROUPS, CGROUP_DEFAULT_ACTION, DEFAULT_ACTION, DENY_PREFIXES, DENY_EXACT, ALLOW_PREFIXES, ALLOW_EXACT, EXEC_DENY_EXACT, EXEC_DENY_PREFIXES, EXEC_ALLOW_EXACT, EXEC_ALLOW_PREFIXES, EXEC_DEFAULT_ACTION, EXEC_CGROUP_DEFAULT_ACTION.
The MAX_POLICY_RULES constant in guardian-common was updated from 64 to 1024 to match.
Path truncation detection:
A status_flags field was added to FileAccessEvent:
// guardian-common/src/lib.rs
pub const EVENT_FLAG_TRUNCATED: u32 = 1;
#[repr(C)]
pub struct FileAccessEvent {
// ... existing fields ...
pub filename_len: u32,
pub status_flags: u32, // NEW: bit 0 = path was truncated
pub filename: [u8; MAX_FILENAME_LEN],
}In eBPF tracepoints, if the path length equals or exceeds MAX_FILENAME_LEN - 1, the truncation flag is set. In enforcement mode, truncated paths are denied by default (inserted into PENDING_DENY). Userspace logs a warning:
[WARN] Truncated path detected (256 bytes): /very/long/path/... — denying by default
When bprm_check_security sees a known dynamic linker, Guardian inspects argv[1] to find the real binary being loaded:
Normal exec: execve("/usr/bin/curl", ...)
→ bprm_check_security sees "/usr/bin/curl" → DENY
Linker bypass: execve("/lib/.../ld-linux-x86-64.so.2", ["/usr/bin/curl", ...])
→ bprm_check_security sees "/lib/.../ld-linux-x86-64.so.2"
Phase 8 fix: → DYNAMIC_LINKERS map lookup → HIT
→ Read argv[1] → "/usr/bin/curl"
→ Evaluate exec policy against "/usr/bin/curl" → DENY
New BPF map:
| Map | Type | Max Entries | Purpose |
|---|---|---|---|
DYNAMIC_LINKERS |
HashMap<[u8; 256], u8> |
16 | Known linker paths → 1 |
Populated by populate_dynamic_linkers() in main.rs:
fn populate_dynamic_linkers(map: &mut HashMap<MapData, [u8; MAX_FILENAME_LEN], u8>) {
let linkers = [
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",
"/lib64/ld-linux-x86-64.so.2",
"/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",
"/lib/ld-linux-x86-64.so.2",
"/lib/ld-linux-aarch64.so.1",
"/lib/ld-linux.so.2",
];
// Insert each into the BPF map
}A new sys_enter_execveat tracepoint catches the execveat() syscall, which execve() does not cover:
if flags & AT_EMPTY_PATH (0x1000) != 0:
→ This is fd-based exec (memfd attack vector)
→ DENY unconditionally for enforced agents
else:
→ Read filename, evaluate exec policy normally
This closes the memfd_create + execveat(fd, "", ..., AT_EMPTY_PATH) attack vector at the kernel level, complementing the seccomp-level blocking in guardian-launch.
Phase 8 adds optional Bearer token authentication to the dashboard:
[dashboard]
enabled = true
listen = "127.0.0.1:8080"
auth_token = "your-secret-token-here" # NEW: optionalMiddleware behavior:
Request arrives
│
├── Path is /metrics or /static/* → SKIP AUTH (Prometheus scrapers need direct access)
│
├── auth_token is None → SKIP AUTH (backward compatible)
│
├── Authorization: Bearer <token> header matches → ALLOW
│
├── ?token=<token> query parameter matches → ALLOW
│
└── Otherwise → 401 Unauthorized
When no auth_token is configured, the dashboard behaves exactly as before (no auth). This preserves backward compatibility for existing deployments.
Phase 8 adds "strict" as a valid global mode alongside "monitor" and "enforce":
[global]
mode = "strict" # NEW: exit on LSM failure| Mode | LSM Hook Fails | Behavior |
|---|---|---|
monitor |
— | No LSM hooks loaded, monitoring only |
enforce |
warn!() |
Log warning, continue in monitor-only mode |
strict |
bail!() |
Exit immediately — refuse to run without enforcement |
In strict mode, if any LSM hook (file_open, bprm_check_security, inode_rename, inode_unlink, inode_link) fails to attach, the daemon exits with a clear error message. This prevents silent degradation to monitor-only mode.
Phase 8 unconditionally adds /memfd: as a prefix deny rule in the exec policy maps at startup:
// In main.rs, after loading exec maps:
let memfd_prefix = b"/memfd:";
let key = path_to_lpm_key(memfd_prefix);
exec_deny_prefixes.insert(&key, 1, 0)?;This blocks execution of any binary whose path starts with /memfd: — the pseudo-path that memfd_create() binaries have. Combined with the seccomp filter and execveat tracepoint, this provides three-layer defense against fileless execution.
Permission request timeouts are now configurable per risk level:
[permissions.timeouts]
low = 60 # 1 minute for low-risk requests
medium = 120 # 2 minutes for medium-risk
high = 180 # 3 minutes for high-risk
critical = 300 # 5 minutes for critical resourcesImplementation:
// guardian/src/config.rs
pub struct RiskTimeoutConfig {
pub low: u64, // default: 60
pub medium: u64, // default: 120
pub high: u64, // default: 180
pub critical: u64, // default: 300
}
// guardian/src/permissions.rs
impl RiskLevel {
pub fn timeout_secs(&self, config: Option<&RiskTimeoutConfig>) -> u64 {
match config {
Some(c) => match self {
RiskLevel::Low => c.low,
RiskLevel::Medium => c.medium,
RiskLevel::High => c.high,
RiskLevel::Critical => c.critical,
},
None => match self {
RiskLevel::Low => 60,
RiskLevel::Medium => 120,
RiskLevel::High => 180,
RiskLevel::Critical => 300,
},
}
}
}When no [permissions.timeouts] section is configured, the defaults above are used. This replaces the fixed PERMISSION_TIMEOUT_SECS = 120 constant.
Permission requests can now be managed from the command line without requiring the dashboard:
# List all pending permission requests
sudo guardian-ctl pending
# Output:
# ID AGENT TYPE RESOURCE RISK AGE
# ------------------------------------------------------------------------------------------
# 42 my-agent exec /usr/bin/curl medium 15s
# Justification: Need to fetch config from internal API
# Approve a request (grant for 5 minutes)
sudo guardian-ctl approve --id 42 --duration 300
# Deny a request with reason
sudo guardian-ctl deny --id 42 --reason "curl access not authorized for this agent"New IPC messages:
| Message | Direction | Purpose |
|---|---|---|
ListPending |
Request | List all pending permission requests |
PendingPermissions { requests } |
Response | Vector of PendingPermissionInfo structs |
ApprovePermission { request_id, duration_secs } |
Request | Approve by ID with grant duration |
DenyPermission { request_id, reason } |
Request | Deny by ID with optional reason |
The approve and deny commands reuse the existing resolve_permission() function, which handles grant creation, rate limiter updates, audit trail persistence, and SSE broadcast — identical behavior to dashboard approval.
A new GrantAccumulator tracks cumulative grant durations per agent per resource within a 24-hour sliding window:
T=0:00 Agent requests /etc/shadow → Approved for 3600s
T=0:30 Accumulator: agent=my-agent, resource=/etc/shadow, total=3600s
T=1:00 Agent requests /etc/shadow again → Approved for 3600s
T=1:00 Accumulator: total=7200s > limit=3600s → WARNING logged
Config:
[permissions]
max_grant_total_secs = 3600 # 1 hour max accumulated grant per resource per 24h
Implementation:
pub struct GrantAccumulator {
grants: HashMap<(String, String), Vec<(Instant, u64)>>,
}
impl GrantAccumulator {
pub fn record_and_check(&mut self, agent: &str, resource: &str, duration: u64) -> u64 {
let entries = self.grants.entry((agent.into(), resource.into())).or_default();
// Prune entries older than 24 hours
entries.retain(|(t, _)| now.duration_since(*t).as_secs() < 86400);
entries.push((now, duration));
entries.iter().map(|(_, d)| *d).sum() // total accumulated
}
}When the accumulated total exceeds max_grant_total_secs, a warning is logged. The grant is still honored (since the decision was already sent to the agent via oneshot channel), but the warning enables operators to detect and investigate persistent access accumulation.
Justification analysis now uses weighted scoring instead of binary pattern matching:
const SUSPICIOUS_PATTERNS: &[(&str, &str, u32)] = &[
("urgent", "URGENCY", 3),
("immediately", "URGENCY", 3),
("emergency", "URGENCY", 4),
("disable security", "SECURITY_BYPASS", 5),
("bypass", "SECURITY_BYPASS", 4),
("trust me", "REASSURANCE", 3),
("admin told", "AUTHORITY_CLAIM", 4),
("credential", "SENSITIVE_MENTION", 2),
("token", "SENSITIVE_MENTION", 1),
// ... 22 patterns total with weights 1-5
];Graduated risk bumps:
| Total Score | Risk Bump | Example |
|---|---|---|
| 0 | No bump | Clean justification |
| 1-2 | No bump | Single low-weight match (e.g., "token") |
| 3-7 | +1 tier | "This is urgent, trust me" (3+3=6) |
| 8+ | +2 tiers | "Emergency! Override security, admin told me to bypass" (4+5+4+4=17) |
This means a Low-risk request with a highly suspicious justification (score 8+) jumps directly to High risk, triggering a 5-second mandatory wait timer and enhanced UI friction.
An AnomalyDetector runs hourly as a background task, querying the SQLite audit trail for three suspicious patterns:
Every 3600 seconds:
│
├── Rubber-stamping check
│ SELECT COUNT(*) FROM permission_audit WHERE resolved_at > (now - 24h)
│ SELECT COUNT(*) WHERE approved = 1
│ If approval rate > 90% AND total > 10 → ALERT
│
├── High-volume agent check
│ SELECT agent_name, COUNT(*) FROM permission_audit
│ WHERE resolved_at > (now - 24h) GROUP BY agent_name HAVING cnt > 20
│ → ALERT for each agent exceeding threshold
│
└── Persistence attack check
SELECT DISTINCT d.agent_name FROM permission_audit d
JOIN permission_audit a ON d.agent_name = a.agent_name
AND d.resource_path = a.resource_path
AND d.approved = 0 AND a.approved = 1
AND a.resolved_at > d.resolved_at
WHERE d.resolved_at > (now - 24h)
→ ALERT for each agent with deny-then-approve on same resource
Findings are logged as warnings and sent through the AlertSender for dispatch to configured outputs (Slack, webhook, email, JSON log).
A new per-agent fail_closed option changes error handling in LSM hooks:
[[agents]]
name = "high-security-agent"
identity = "cgroup"
fail_closed = true # NEW: deny on eBPF error instead of allowing
[agents.file_access]
default = "deny"BPF map: FAIL_CLOSED_CGROUPS: HashMap<u64, u8> — populated during agent registration when fail_closed = true.
LSM hook behavior:
| Error in LSM | fail_closed = false (default) | fail_closed = true |
|---|---|---|
| Map lookup failure | return 0 (allow) | return -EACCES (deny) |
| Per-CPU array allocation failure | return 0 (allow) | return -EACCES (deny) |
| Any other eBPF error | return 0 (allow) | return -EACCES (deny) |
This is a per-cgroup setting, so agents with different security requirements can coexist on the same system.
[global]
mode = "strict" # NEW: "monitor", "enforce", or "strict"
[dashboard]
enabled = true
listen = "127.0.0.1:8080"
auth_token = "your-secret-token" # NEW: optional Bearer token
[permissions]
auto_deny = ["/etc/shadow", "/root/.ssh/**"]
auto_approve = [{ pattern = "/tmp/**", max_duration_secs = 300 }]
rate_limit_per_minute = 3
rate_limit_per_hour = 15
deny_cooldown_secs = 30
max_pending_per_agent = 2
max_grant_total_secs = 3600 # NEW: max accumulated grant duration per resource (24h window)
[permissions.timeouts] # NEW: risk-based timeout configuration
low = 60
medium = 120
high = 180
critical = 300
[[agents]]
name = "secure-agent"
identity = "cgroup"
fail_closed = true # NEW: deny on eBPF error
[agents.file_access]
default = "deny"
allow = ["/tmp/**"]
deny = ["/etc/shadow"]| Decision | Rationale |
|---|---|
| Seccomp for io_uring/memfd blocking | eBPF tracepoints can't intercept io_uring ring buffer operations. Seccomp operates at the syscall boundary before io_uring setup, blocking it completely. Applied in guardian-launch so agent inherits the filter. |
| Three-layer memfd defense | Seccomp blocks memfd_create, execveat tracepoint catches AT_EMPTY_PATH, and default /memfd: deny catches LSM-level exec. Any one layer is sufficient, but defense-in-depth is essential for security-critical code paths. |
| Separate PENDING maps per inode op | Reusing PENDING_DENY would cause races: if a rename triggers both file_open and inode_rename LSM hooks, the first to consume the pending entry would leave the second unprotected. |
| Warn-only for grant accumulation | The decision is already sent to the agent via oneshot channel before we can check accumulation. Moving the check before the oneshot send would require restructuring the entire approval flow. Warning + logging is sufficient for Phase 8. |
| auth_token in config (not generated) | Auto-generating tokens adds startup complexity and makes config non-reproducible. Operators can use any secret management tool to populate the token field. |
| Strict mode exits on LSM failure | Operator explicitly opted into "no monitoring without enforcement." A warning log is easily missed; an exit with a clear error message forces resolution before the system is considered operational. |
| Hourly anomaly detection interval | Frequent enough to catch patterns within a shift, infrequent enough to avoid query overhead. SQLite queries are indexed and sub-millisecond. |
| Weighted justification scoring | Binary detection (any match → bump) was too aggressive for benign mentions like "token" or "credential." Weights allow fine-grained tuning: "token" (weight 1) alone doesn't trigger a bump, but "emergency override bypass" (weight 4+4+3=11) jumps +2 tiers. |
| Per-cgroup fail-closed | System-wide fail-closed would break non-critical agents on transient eBPF errors. Per-cgroup allows security-critical agents to have strict error handling while development agents remain fail-open. |
| Section | Change |
|---|---|
| Maps (lines 19-188) | All rule maps: 256→1024 entries. 3 new PENDING maps for rename/unlink/link. DYNAMIC_LINKERS map (16 entries). FAIL_CLOSED_CGROUPS map (1024 entries). |
try_guardian_file_open() |
Set `event.status_flags |
try_guardian_exec_monitor() |
Check DYNAMIC_LINKERS map — if hit, read argv[1] pointer and evaluate exec policy against the real binary. |
NEW: guardian_execveat_monitor() |
Tracepoint on sys_enter_execveat. Detects AT_EMPTY_PATH flag for memfd-based exec. Denies unconditionally for enforced agents. |
NEW: guardian_rename_monitor() |
Tracepoint on sys_enter_renameat2. Reads source path, evaluates policy, sets PENDING_RENAME_DENY. |
NEW: guardian_unlink_monitor() |
Tracepoint on sys_enter_unlinkat. Reads path, evaluates deny policy, sets PENDING_UNLINK_DENY. |
NEW: guardian_link_monitor() |
Tracepoint on sys_enter_linkat. Reads source/dest paths, evaluates policy, sets PENDING_LINK_DENY. |
NEW: guardian_enforce_rename() |
LSM inode_rename. Checks PENDING_RENAME_DENY, returns -EACCES if set. Falls back based on FAIL_CLOSED_CGROUPS. |
NEW: guardian_enforce_unlink() |
LSM inode_unlink. Same pattern as rename. |
NEW: guardian_enforce_link() |
LSM inode_link. Same pattern as rename. |
| Change | Detail |
|---|---|
MAX_POLICY_RULES |
64 → 1024 |
FileAccessEvent |
Added status_flags: u32 field after filename_len |
EVENT_FLAG_TRUNCATED |
New constant: pub const EVENT_FLAG_TRUNCATED: u32 = 1; |
| Map name constants | Added MAP_PENDING_RENAME_DENY, MAP_PENDING_UNLINK_DENY, MAP_PENDING_LINK_DENY, MAP_DYNAMIC_LINKERS, MAP_FAIL_CLOSED_CGROUPS |
| IPC protocol | Added ListPending, ApprovePermission { request_id, duration_secs }, DenyPermission { request_id, reason } request variants |
| IPC responses | Added PendingPermissions { requests: Vec<PendingPermissionInfo> } response variant |
PendingPermissionInfo |
New struct: request_id, agent_name, resource_type, resource_path, justification, risk_level, elapsed_secs |
| Change | Detail |
|---|---|
DashboardConfig |
Added auth_token: Option<String> |
PermissionsConfig |
Added timeouts: Option<RiskTimeoutConfig>, max_grant_total_secs: u64 (default 3600) |
RiskTimeoutConfig |
New struct with low/medium/high/critical u64 fields, serde defaults 60/120/180/300 |
AgentConfig |
Added fail_closed: Option<bool> |
validate_config() |
"strict" added as valid mode |
| Change | Detail |
|---|---|
RiskLevel::timeout_secs() |
New method taking Option<&RiskTimeoutConfig>, returns per-risk-level timeout |
GrantAccumulator |
New struct tracking cumulative grants per (agent, resource) in 24h sliding window |
SUSPICIOUS_PATTERNS |
Changed from (&str, &str) to (&str, &str, u32) — added per-pattern weights |
analyze_justification() |
Returns (Vec<(String, String)>, u32) — findings + total score |
justification_risk_bump() |
Takes score parameter, returns 0/1/2 bumps instead of bool |
AnomalyDetector |
New struct with detect_anomalies() querying SQLite for rubber-stamping, persistence, floods |
| Change | Detail |
|---|---|
IpcState |
Added fail_closed_map, grant_accumulator fields |
process_request() |
Handle ListPending, ApprovePermission, DenyPermission |
handle_register() |
Populate FAIL_CLOSED_CGROUPS map for fail_closed agents |
handle_request_permission() |
Use risk_level.timeout_secs(timeout_config) instead of fixed 120s. Updated justification analysis to use weighted scoring and graduated bumps. |
resolve_permission() |
Added grant accumulation check — grant_accumulator.record_and_check() with warning log |
NEW: handle_list_pending() |
Returns Vec |
NEW: handle_approve_permission() |
Calls resolve_permission() with "Approved via CLI" reason |
NEW: handle_deny_permission() |
Calls resolve_permission() with custom or default reason |
| Change | Detail |
|---|---|
router() |
Conditionally applies auth middleware when auth_token is Some |
NEW: auth_middleware() |
Checks Bearer token header, ?token= query param. Skips /metrics and /static/. Returns 401 if not authenticated. |
| Change | Detail |
|---|---|
NEW: apply_seccomp_filter() |
Blocks io_uring (425-427) + memfd_create (319) via seccompiler. Uses MaskedEq(0) always-true condition. Applied after cgroup setup, before exec. Best-effort with warning on failure. |
| Change | Detail |
|---|---|
Commands enum |
Added Pending, Approve { id, duration }, Deny { id, reason } variants |
process_request() |
Maps new commands to ListPending, ApprovePermission, DenyPermission IPC messages |
| Response handling | Added PendingPermissions formatter with table output showing ID, agent, type, resource, risk level, age |
| Issue | Severity | Why Deferred |
|---|---|---|
Symlink resolution via bpf_d_path() |
CRITICAL | Requires Linux 5.11+ and complex raw eBPF helper calls not yet supported by aya |
| TOCTOU race condition | HIGH | Architectural limitation of tracepoint+LSM pattern; requires LSM-only enforcement (depends on bpf_d_path) |
cgroup/connect4/6 network enforcement |
CRITICAL | Requires per-cgroup BPF attachment which aya doesn't natively support well |
| DNS monitoring | MEDIUM | Requires port 53 packet inspection and periodic DNS resolution |
| BTF-portable tracepoint offsets | MEDIUM | Requires build-time vmlinux.h generation and BTF field access |
| Live BPF map sync on dashboard policy edit | MEDIUM | Requires refactoring map population into a reusable sync_bpf_maps() function |
| Full SIGHUP reload including alerting | LOW | Requires wrapping AlertSender in Arc<RwLock> for concurrent access |
- Phase 9 (planned): Symlink resolution via
bpf_d_path()(requires Linux 5.11+ kernel and aya raw helper support), network enforcement viacgroup/connect4/6, DNS monitoring, live BPF map sync on dashboard policy edit - Stretch goals: Content hashing via IMA (Integrity Measurement Architecture), BTF-portable tracepoint offsets,
mmap_fileLSM hook for memory-mapped file access control