LCOV - code coverage report
Current view: top level - pkg/selinux - selinux-client.js Coverage Total Hit
Test: cockpit Lines: 79.0 % 143 113
Test Date: 2026-07-03 07:31:16

            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 : }
        

Generated by: LCOV version 2.0-1