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/// The `--` matches the sudoers grant, which requires it so a direct caller
53/// cannot add `-d <dir>` and load profiles from a directory it owns.
54///
55/// Risk: High. Activating enforcement can immediately block operations that
56/// the application relies on, potentially causing it to fail or lose data.
57/// Always test in complain mode first.
58pub fn apparmor_enforce(profile_path: &str) -> ActionSpec {
59    ActionSpec {
60        action_name: "AppArmorEnforce",
61        mechanism: command_mechanism("sudo", ["aa-enforce", "--", profile_path]),
62        risk_level: RiskLevel::High,
63        reboot_required: false,
64        rollback_available: false,
65    }
66}
67
68/// Put an AppArmor profile into complain (learning) mode
69/// (`sudo aa-complain -- <profile_path>`; see [`apparmor_enforce`] for the `--`).
70///
71/// Risk: High. Complain mode stops a profile from *blocking* violations — it
72/// disables mandatory-access-control enforcement for that profile (violations
73/// are logged only). Applied to an enforcing security profile this neutralizes
74/// the control (T1562.001 Impair Defenses), the inverse of `AppArmorEnforce`.
75/// Gated at Admin to match `AppArmorEnforce`. The audit-before-enforce workflow
76/// (complain first, then enforce) is still recommended, but both directions
77/// alter enforcement of a security control and carry the same privilege bar.
78pub fn apparmor_complain(profile_path: &str) -> ActionSpec {
79    ActionSpec {
80        action_name: "AppArmorComplain",
81        mechanism: command_mechanism("sudo", ["aa-complain", "--", profile_path]),
82        risk_level: RiskLevel::High,
83        reboot_required: false,
84        rollback_available: false,
85    }
86}
87
88// ---------------------------------------------------------------------------
89// Tests
90// ---------------------------------------------------------------------------
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::actions::ActionMechanism;
96
97    fn extract_args(spec: &ActionSpec) -> (&'static str, Vec<String>) {
98        match &spec.mechanism {
99            ActionMechanism::Command { program, args } => (*program, args.clone()),
100            _ => panic!("expected Command mechanism"),
101        }
102    }
103
104    // ── apparmor_status ──────────────────────────────────────────────────────
105
106    #[test]
107    fn apparmor_status_action_name() {
108        assert_eq!(apparmor_status().action_name, "AppArmorStatus");
109    }
110
111    #[test]
112    fn apparmor_status_risk_is_low() {
113        assert_eq!(apparmor_status().risk_level, RiskLevel::Low);
114    }
115
116    #[test]
117    fn apparmor_status_argv() {
118        let spec = apparmor_status();
119        let (prog, args) = extract_args(&spec);
120        assert_eq!(prog, "sudo");
121        let a: Vec<&str> = args.iter().map(String::as_str).collect();
122        assert_eq!(a, vec!["aa-status"]);
123    }
124
125    // ── apparmor_enforce ─────────────────────────────────────────────────────
126
127    #[test]
128    fn apparmor_enforce_action_name() {
129        assert_eq!(
130            apparmor_enforce("/etc/apparmor.d/usr.bin.firefox").action_name,
131            "AppArmorEnforce"
132        );
133    }
134
135    #[test]
136    fn apparmor_enforce_risk_is_high() {
137        assert_eq!(
138            apparmor_enforce("/etc/apparmor.d/usr.bin.firefox").risk_level,
139            RiskLevel::High
140        );
141    }
142
143    #[test]
144    fn apparmor_enforce_argv() {
145        let profile = "/etc/apparmor.d/usr.bin.firefox";
146        let spec = apparmor_enforce(profile);
147        let (prog, args) = extract_args(&spec);
148        assert_eq!(prog, "sudo");
149        let a: Vec<&str> = args.iter().map(String::as_str).collect();
150        assert_eq!(a, vec!["aa-enforce", "--", profile]);
151    }
152
153    // ── apparmor_complain ────────────────────────────────────────────────────
154
155    #[test]
156    fn apparmor_complain_action_name() {
157        assert_eq!(
158            apparmor_complain("/etc/apparmor.d/usr.bin.firefox").action_name,
159            "AppArmorComplain"
160        );
161    }
162
163    #[test]
164    fn apparmor_complain_risk_is_high() {
165        // Complain mode disables MAC enforcement for the profile — parity with
166        // AppArmorEnforce, which also alters enforcement of a security control.
167        assert_eq!(
168            apparmor_complain("/etc/apparmor.d/usr.bin.firefox").risk_level,
169            RiskLevel::High
170        );
171    }
172
173    #[test]
174    fn apparmor_complain_argv() {
175        let profile = "/etc/apparmor.d/usr.bin.nginx";
176        let spec = apparmor_complain(profile);
177        let (prog, args) = extract_args(&spec);
178        assert_eq!(prog, "sudo");
179        let a: Vec<&str> = args.iter().map(String::as_str).collect();
180        assert_eq!(a, vec!["aa-complain", "--", profile]);
181    }
182
183    #[test]
184    fn enforce_uses_aa_enforce_not_aa_complain() {
185        let spec = apparmor_enforce("/etc/apparmor.d/test");
186        let (_, args) = extract_args(&spec);
187        let a: Vec<&str> = args.iter().map(String::as_str).collect();
188        assert!(a.contains(&"aa-enforce"));
189        assert!(!a.contains(&"aa-complain"));
190    }
191
192    #[test]
193    fn complain_uses_aa_complain_not_aa_enforce() {
194        let spec = apparmor_complain("/etc/apparmor.d/test");
195        let (_, args) = extract_args(&spec);
196        let a: Vec<&str> = args.iter().map(String::as_str).collect();
197        assert!(a.contains(&"aa-complain"));
198        assert!(!a.contains(&"aa-enforce"));
199    }
200
201    // ── specs() completeness ─────────────────────────────────────────────────
202
203    #[test]
204    fn specs_covers_all_action_names() {
205        let expected = ["AppArmorStatus", "AppArmorEnforce", "AppArmorComplain"];
206        let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
207        for name in &expected {
208            assert!(spec_names.contains(name), "specs() missing {name}");
209        }
210    }
211}