Skip to main content

sysknife_daemon/actions/
apparmor.rs

1//! AppArmor profile management actions (Ubuntu / Debian).
2//!
3//! AppArmor is the default MAC (Mandatory Access Control) system on Ubuntu.
4//! These actions invoke `aa-status` (provided by the `apparmor` base package)
5//! and `aa-enforce` / `aa-complain` (provided by `apparmor-utils`, which may
6//! need a separate `apt install`). On a minimal server install the base
7//! package is present but `apparmor-utils` is not — `AppArmorEnforce` and
8//! `AppArmorComplain` will report `command not found` until it is added.
9//!
10//! ## Profile modes
11//!
12//! - **enforce** — the profile is active; violations are blocked and logged.
13//! - **complain** — the profile is in learning mode; violations are logged but
14//!   not blocked. Use complain to diagnose an over-restrictive profile before
15//!   deciding to fix or disable it.
16
17use super::{command_mechanism, ActionSpec};
18use sysknife_types::RiskLevel;
19
20// ---------------------------------------------------------------------------
21// specs() — for action_consistency tests
22// ---------------------------------------------------------------------------
23
24/// Return one representative `ActionSpec` per AppArmor action name.
25pub fn specs() -> Vec<ActionSpec> {
26    vec![
27        apparmor_status(),
28        apparmor_enforce("/etc/apparmor.d/usr.bin.firefox"),
29        apparmor_complain("/etc/apparmor.d/usr.bin.firefox"),
30    ]
31}
32
33// ---------------------------------------------------------------------------
34// Action constructors
35// ---------------------------------------------------------------------------
36
37/// Show the status of all loaded AppArmor profiles (`sudo aa-status`).
38///
39/// Risk: Low. Read-only; lists every profile and its current mode.
40pub fn apparmor_status() -> ActionSpec {
41    ActionSpec {
42        action_name: "AppArmorStatus",
43        mechanism: command_mechanism("sudo", ["aa-status"]),
44        risk_level: RiskLevel::Low,
45        reboot_required: false,
46        rollback_available: false,
47    }
48}
49
50/// Put an AppArmor profile into enforce mode (`sudo aa-enforce <profile_path>`).
51///
52/// Risk: High. Activating enforcement can immediately block operations that
53/// the application relies on, potentially causing it to fail or lose data.
54/// Always test in complain mode first.
55pub fn apparmor_enforce(profile_path: &str) -> ActionSpec {
56    ActionSpec {
57        action_name: "AppArmorEnforce",
58        mechanism: command_mechanism("sudo", ["aa-enforce", profile_path]),
59        risk_level: RiskLevel::High,
60        reboot_required: false,
61        rollback_available: false,
62    }
63}
64
65/// Put an AppArmor profile into complain (learning) mode (`sudo aa-complain <profile_path>`).
66///
67/// Risk: High. Complain mode stops a profile from *blocking* violations — it
68/// disables mandatory-access-control enforcement for that profile (violations
69/// are logged only). Applied to an enforcing security profile this neutralizes
70/// the control (T1562.001 Impair Defenses), the inverse of `AppArmorEnforce`.
71/// Gated at Admin to match `AppArmorEnforce`. The audit-before-enforce workflow
72/// (complain first, then enforce) is still recommended, but both directions
73/// alter enforcement of a security control and carry the same privilege bar.
74pub fn apparmor_complain(profile_path: &str) -> ActionSpec {
75    ActionSpec {
76        action_name: "AppArmorComplain",
77        mechanism: command_mechanism("sudo", ["aa-complain", profile_path]),
78        risk_level: RiskLevel::High,
79        reboot_required: false,
80        rollback_available: false,
81    }
82}
83
84// ---------------------------------------------------------------------------
85// Tests
86// ---------------------------------------------------------------------------
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::actions::ActionMechanism;
92
93    fn extract_args(spec: &ActionSpec) -> (&'static str, Vec<String>) {
94        match &spec.mechanism {
95            ActionMechanism::Command { program, args } => (*program, args.clone()),
96            _ => panic!("expected Command mechanism"),
97        }
98    }
99
100    // ── apparmor_status ──────────────────────────────────────────────────────
101
102    #[test]
103    fn apparmor_status_action_name() {
104        assert_eq!(apparmor_status().action_name, "AppArmorStatus");
105    }
106
107    #[test]
108    fn apparmor_status_risk_is_low() {
109        assert_eq!(apparmor_status().risk_level, RiskLevel::Low);
110    }
111
112    #[test]
113    fn apparmor_status_argv() {
114        let spec = apparmor_status();
115        let (prog, args) = extract_args(&spec);
116        assert_eq!(prog, "sudo");
117        let a: Vec<&str> = args.iter().map(String::as_str).collect();
118        assert_eq!(a, vec!["aa-status"]);
119    }
120
121    // ── apparmor_enforce ─────────────────────────────────────────────────────
122
123    #[test]
124    fn apparmor_enforce_action_name() {
125        assert_eq!(
126            apparmor_enforce("/etc/apparmor.d/usr.bin.firefox").action_name,
127            "AppArmorEnforce"
128        );
129    }
130
131    #[test]
132    fn apparmor_enforce_risk_is_high() {
133        assert_eq!(
134            apparmor_enforce("/etc/apparmor.d/usr.bin.firefox").risk_level,
135            RiskLevel::High
136        );
137    }
138
139    #[test]
140    fn apparmor_enforce_argv() {
141        let profile = "/etc/apparmor.d/usr.bin.firefox";
142        let spec = apparmor_enforce(profile);
143        let (prog, args) = extract_args(&spec);
144        assert_eq!(prog, "sudo");
145        let a: Vec<&str> = args.iter().map(String::as_str).collect();
146        assert_eq!(a[0], "aa-enforce");
147        assert_eq!(a[1], profile);
148    }
149
150    // ── apparmor_complain ────────────────────────────────────────────────────
151
152    #[test]
153    fn apparmor_complain_action_name() {
154        assert_eq!(
155            apparmor_complain("/etc/apparmor.d/usr.bin.firefox").action_name,
156            "AppArmorComplain"
157        );
158    }
159
160    #[test]
161    fn apparmor_complain_risk_is_high() {
162        // Complain mode disables MAC enforcement for the profile — parity with
163        // AppArmorEnforce, which also alters enforcement of a security control.
164        assert_eq!(
165            apparmor_complain("/etc/apparmor.d/usr.bin.firefox").risk_level,
166            RiskLevel::High
167        );
168    }
169
170    #[test]
171    fn apparmor_complain_argv() {
172        let profile = "/etc/apparmor.d/usr.bin.nginx";
173        let spec = apparmor_complain(profile);
174        let (prog, args) = extract_args(&spec);
175        assert_eq!(prog, "sudo");
176        let a: Vec<&str> = args.iter().map(String::as_str).collect();
177        assert_eq!(a[0], "aa-complain");
178        assert_eq!(a[1], profile);
179    }
180
181    #[test]
182    fn enforce_uses_aa_enforce_not_aa_complain() {
183        let spec = apparmor_enforce("/etc/apparmor.d/test");
184        let (_, args) = extract_args(&spec);
185        let a: Vec<&str> = args.iter().map(String::as_str).collect();
186        assert!(a.contains(&"aa-enforce"));
187        assert!(!a.contains(&"aa-complain"));
188    }
189
190    #[test]
191    fn complain_uses_aa_complain_not_aa_enforce() {
192        let spec = apparmor_complain("/etc/apparmor.d/test");
193        let (_, args) = extract_args(&spec);
194        let a: Vec<&str> = args.iter().map(String::as_str).collect();
195        assert!(a.contains(&"aa-complain"));
196        assert!(!a.contains(&"aa-enforce"));
197    }
198
199    // ── specs() completeness ─────────────────────────────────────────────────
200
201    #[test]
202    fn specs_covers_all_action_names() {
203        let expected = ["AppArmorStatus", "AppArmorEnforce", "AppArmorComplain"];
204        let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
205        for name in &expected {
206            assert!(spec_names.contains(name), "specs() missing {name}");
207        }
208    }
209}