Line data Source code
1 : /*
2 : * Copyright (C) 2016 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 3 : import cockpit from 'cockpit';
7 :
8 : /* until we have a good dbus interface to get selinux status updates,
9 : * we resort to polling
10 : */
11 :
12 : // how often to check the status [milliseconds]
13 3 : const pollingInterval = 10000;
14 :
15 3 : const statusCommand = "sestatus";
16 :
17 : // currentStatus reflects the status of SELinux on the system
18 3 : const status = {
19 3 : enabled: undefined,
20 3 : enforcing: false,
21 3 : configEnforcing: false, // configured mode at boot time
22 3 : shell: "",
23 3 : ansible: "",
24 3 : modifications: null,
25 3 : permitted: true,
26 3 : failed: false,
27 3 : };
28 :
29 : /* initializes the selinux status updating, returns initial status
30 : * statusChangedCallback parameters (status, errorMessage)
31 : * status with the following properties:
32 : * - enabled: undefined (couldn't get info), true (enabled) or false (disabled)
33 : * cannot be changed without a reboot
34 : * - enforcing: boolean (current selinux mode of the system, false if permissive or selinux disabled)
35 : * - configEnforcing: boolean (mode the system is configured to boot in, may differ from current mode)
36 : * - shell: Output of `semanage export`
37 : * - ansible: Ansible script for setting up local modifications
38 : * - modifications: List of all local modifications in selinux policy
39 : * - permitted: Set to false if user is not allowed to see local modifications
40 : * - failed: Reading of modifications failed in unexpected way
41 : * errorMessage: optional, if getting the status failed, here will be additional info
42 : *
43 : * Since we're screenscraping we need to run this in LC_ALL=C mode
44 : */
45 3 : export function init(statusChangedCallback) {
46 3 : const refreshInfo = function() {
47 3 : cockpit.spawn(statusCommand, { err: 'message', environ: ["LC_ALL=C"], superuser: "try" }).then(
48 3 : function(output) {
49 : /* parse output that looks like this:
50 : * SELinux status: enabled
51 : * SELinuxfs mount: /sys/fs/selinux
52 : * SELinux root directory: /etc/selinux
53 : * Loaded policy name: targeted
54 : * Current mode: enforcing
55 : * Mode from config file: enforcing
56 : * Policy MLS status: enabled
57 : * Policy deny_unknown status: allowed
58 : * Max kernel policy version: 30
59 : * We want the lines 'SELinux status', 'Current mode' and 'Mode from config file'
60 : */
61 :
62 3 : const lines = output.split("\n");
63 3 : lines.forEach(function(itm) {
64 3 : const items = itm.trim().split(":");
65 3 : if (items.length !== 2)
66 3 : return;
67 3 : const key = items[0].trim();
68 3 : const value = items[1].trim();
69 3 : if (key == "SELinux status") {
70 3 : status.enabled = (value == "enabled");
71 3 : } else if (key == "Current mode") {
72 3 : status.enforcing = (value == "enforcing");
73 3 : } else if (key == "Mode from config file") {
74 1 : if (value == 'error (Permission denied)') {
75 1 : status.configEnforcing = undefined;
76 1 : } else {
77 3 : status.configEnforcing = (value == "enforcing");
78 3 : }
79 3 : }
80 3 : });
81 3 : if (statusChangedCallback)
82 3 : statusChangedCallback(status, undefined);
83 3 : },
84 0 : function(error) {
85 0 : if (status === undefined)
86 0 : return;
87 0 : if (statusChangedCallback) {
88 0 : status.enabled = undefined;
89 0 : statusChangedCallback(status, error.message);
90 0 : }
91 0 : }
92 3 : );
93 3 : };
94 :
95 3 : let polling = null;
96 :
97 3 : function setupPolling() {
98 3 : if (cockpit.hidden) {
99 3 : window.clearInterval(polling);
100 3 : polling = null;
101 3 : } else if (polling === null) {
102 3 : polling = window.setInterval(refreshInfo, pollingInterval);
103 3 : refreshInfo();
104 3 : getModifications(statusChangedCallback);
105 3 : }
106 3 : }
107 :
108 3 : cockpit.addEventListener("visibilitychange", setupPolling);
109 3 : setupPolling();
110 :
111 : /* The first time */
112 3 : if (polling === null)
113 1 : refreshInfo();
114 :
115 3 : return status;
116 3 : }
117 :
118 1 : function parseBoolean(result, item) {
119 : // Example:
120 : // authlogin_nsswitch_use_ldap (on , on) Allow authlogin to nsswitch use ldap
121 : // Split by first ')', as the name cannot contain ')'
122 1 : if (item) {
123 1 : const match = item.match(/(\S*)\s*\((\S*)\s*,.*\)\s*(.*)/);
124 1 : if (match) {
125 1 : let description = match[3];
126 1 : let enable_val = "--on";
127 0 : if (match[2] !== "on") {
128 0 : enable_val = "--off";
129 0 : description = description.replace("Allow", "Disallow");
130 0 : }
131 : // We want to support Ansible Core, the `seboolean:` module is not a builtin
132 1 : const ansible = `
133 1 : - name: ${description}
134 1 : command: semanage boolean -m ${enable_val} ${match[1]}
135 : `;
136 1 : result.push({ description, ansible });
137 1 : }
138 1 : }
139 1 : return result;
140 1 : }
141 :
142 3 : export function getModifications(statusChangedCallback) {
143 : // List of items we know how to parse
144 3 : const manageditems_callbacks = [["boolean", parseBoolean]];
145 3 : const manageditems = manageditems_callbacks.map(item => item[0]);
146 :
147 : // Building a query to get information from semanage
148 : // Use `semanage export` to show shell script (and parse types we yet don't parse explicitly)
149 : // Use `semanage <type> --list -C` to get better readable and parsable local changes
150 : // Use `echo '~~~~~'` as separator, so we don't need to execute multiple commands
151 3 : let script = "semanage export";
152 3 : manageditems.forEach(item => { script += " && echo '~~~~~' && semanage " + item + " --list -C --noheading" });
153 3 : cockpit.script(script, [], { err: 'message', environ: ["LC_MESSAGES=C"], superuser: "try" })
154 1 : .then(output => {
155 1 : output = output.split("~~~~~");
156 1 : status.shell = output[0];
157 1 : status.modifications = [];
158 1 : status.ansible = "";
159 :
160 1 : for (let i = 1; i < output.length; i++) {
161 1 : const parsed = output[i].trim().split("\n")
162 1 : .reduce(manageditems_callbacks[i - 1][1], []);
163 1 : parsed.forEach(p => {
164 1 : status.modifications.push(p.description);
165 1 : status.ansible += p.ansible;
166 1 : });
167 1 : }
168 :
169 1 : const shell_rules = {};
170 : // As long as we don't parse all items, we need to get some from general export
171 : // Once we can parse all types, this can be dropped
172 1 : status.modifications.push(...(output[0].split("\n").reduce(function (result, mod) {
173 1 : mod = mod.trim();
174 1 : if (mod === "")
175 1 : return result;
176 :
177 1 : const items = mod.split(" ");
178 :
179 : // Skip enumeration of types, e.g. 'boolean -D'
180 1 : if (items.length === 2 && items[1] == "-D")
181 1 : return result;
182 :
183 0 : if (manageditems.indexOf(items[0]) < 0) {
184 0 : if (items[0] in shell_rules)
185 0 : shell_rules[items[0]].push(" semanage " + mod);
186 : else
187 0 : shell_rules[items[0]] = [" semanage " + mod];
188 0 : result.push(mod);
189 0 : }
190 1 : return result;
191 1 : }, [])));
192 :
193 : // Create shell rule for every ansible group separately
194 0 : Object.keys(shell_rules).forEach(t => {
195 0 : const rules = shell_rules[t].join("\n");
196 0 : status.ansible += `
197 0 : - name: Set up ${t} customizations
198 : shell: |
199 0 : semanage ${t} -D
200 0 : ${rules}
201 : `;
202 0 : });
203 :
204 1 : statusChangedCallback(status, undefined);
205 1 : })
206 1 : .catch(e => {
207 1 : status.modifications = [];
208 1 : if (e.message.indexOf("ValueError:") >= 0) {
209 1 : status.permitted = false;
210 1 : statusChangedCallback(status, undefined);
211 0 : } else {
212 0 : status.failed = true;
213 0 : statusChangedCallback(status, e.message);
214 0 : }
215 1 : });
216 3 : }
217 :
218 : // returns a promise of the command used to set enforcing mode
219 1 : export function setEnforcing(enforcingMode) {
220 0 : const command = ["setenforce", (enforcingMode ? "1" : "0")];
221 1 : return cockpit.spawn(command, { superuser: "require", err: "message" });
222 1 : }
|