LCOV - code coverage report
Current view: top level - pkg/selinux - setroubleshoot-view.jsx Coverage Total Hit
Test: cockpit Lines: 85.5 % 344 294
Test Date: 2026-07-13 10:00:01

            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            3 : import React from "react";
       9              : import { Alert, AlertActionCloseButton, AlertGroup } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
      10              : import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js";
      11              : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
      12              : import { Divider } from "@patternfly/react-core/dist/esm/components/Divider/index.js";
      13              : import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
      14              : import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
      15              : import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
      16              : import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
      17              : import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
      18              : import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
      19              : import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
      20              : import { ExclamationCircleIcon, ExclamationTriangleIcon, InfoCircleIcon } from "@patternfly/react-icons";
      21              : import { Icon } from "@patternfly/react-core/dist/esm/components/Icon/index.js";
      22              : import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js";
      23              : import { CodeBlock, CodeBlockCode } from "@patternfly/react-core/dist/esm/components/CodeBlock/index.js";
      24              : 
      25              : import { Modifications } from "cockpit-components-modifications";
      26              : import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
      27              : import { ListingTable } from "cockpit-components-table.jsx";
      28              : import { ListingPanel } from 'cockpit-components-listing-panel.jsx';
      29              : import * as timeformat from 'timeformat';
      30              : 
      31            3 : const _ = cockpit.gettext;
      32              : 
      33              : /* Show details for an alert, including possible solutions
      34              :  * Props correspond to an item in the setroubleshoot dataStore
      35              :  */
      36            3 : class SELinuxEventDetails extends React.Component {
      37            0 :     runFix(itmIdx, runCommand) {
      38            0 :         const localId = this.props.details.localId;
      39            0 :         const analysisId = this.props.details.pluginAnalysis[itmIdx].analysisId;
      40            0 :         this.props.runFix(localId, analysisId, itmIdx, runCommand);
      41            0 :     }
      42              : 
      43            2 :     render() {
      44            1 :         if (!this.props.details) {
      45              :             // details should be requested by default, so we just need to wait for them
      46            1 :             if (this.props.details === undefined)
      47            1 :                 return <EmptyStatePanel loading title={ _("Waiting for details...") } />;
      48              :             else
      49            1 :                 return <EmptyStatePanel icon={ExclamationCircleIcon} title={ _("Unable to get alert details.") } />;
      50            1 :         }
      51              : 
      52            2 :         const self = this;
      53            2 :         const fixEntries = this.props.details.pluginAnalysis.map(function(itm, itmIdx) {
      54            2 :             let fixit = null;
      55            2 :             let fixit_command = null;
      56            2 :             let msg = null;
      57              : 
      58              :             /* some plugins like catchall_sebool don't report fixable as they offer multiple solutions;
      59              :              * we can offer to run a single setsebool command for convenience */
      60            2 :             let fixable = itm.fixable;
      61            2 :             if (!fixable && itm.doText && itm.doText.startsWith("setsebool") && itm.doText.indexOf("\n") < 0) {
      62            2 :                 fixable = true;
      63            2 :                 fixit_command = itm.doText;
      64            2 :             }
      65              : 
      66            2 :             if (fixable) {
      67            1 :                 if ((itm.fix) && (itm.fix.plugin == itm.analysisId)) {
      68            1 :                     if (itm.fix.running) {
      69            1 :                         msg = (
      70            1 :                             <div>
      71            1 :                                 <Spinner size="sm" className="setroubleshoot-progress-spinner" />
      72            1 :                                 <span className="setroubleshoot-progress-message"> { _("Applying solution...") }</span>
      73            1 :                             </div>
      74              :                         );
      75            1 :                     } else {
      76            1 :                         if (itm.fix.success) {
      77            1 :                             msg = (
      78            1 :                                 <Alert isInline variant="success" title={ _("Solution applied successfully") }>
      79            1 :                                     {itm.fix.result}
      80            1 :                                 </Alert>
      81              :                             );
      82            1 :                         } else {
      83            1 :                             msg = (
      84            1 :                                 <Alert isInline variant="danger" title={ _("Solution failed") }>
      85            1 :                                     {itm.fix.result}
      86            1 :                                 </Alert>
      87              :                             );
      88            1 :                         }
      89            1 :                     }
      90            1 :                 }
      91            2 :                 if (!itm.fix) {
      92            2 :                     fixit = (
      93            2 :                         <div className="setroubleshoot-listing-action">
      94            2 :                             <Button variant="secondary" onClick={ self.runFix.bind(self, itmIdx, fixit_command) }>
      95            2 :                                 { _("Apply this solution") }
      96            2 :                             </Button>
      97            2 :                         </div>
      98              :                     );
      99            2 :                 }
     100            2 :             } else {
     101            2 :                 fixit = (
     102            2 :                     <div className="setroubleshoot-listing-action">
     103            2 :                         <span>{ _("Unable to apply this solution automatically") }</span>
     104            2 :                     </div>
     105              :                 );
     106            2 :             }
     107              : 
     108            2 :             function codeBlock(text, key) {
     109            2 :                 return (
     110            2 :                     <CodeBlock key={key} aria-label={_("solution")}>
     111            2 :                         <CodeBlockCode>{text}</CodeBlockCode>
     112            2 :                     </CodeBlock>
     113              :                 );
     114            2 :             }
     115              : 
     116            2 :             let doElement = "";
     117              : 
     118              :             // One line usually means one command
     119            2 :             if (itm.doText && itm.doText.indexOf("\n") < 0)
     120            2 :                 doElement = codeBlock(itm.doText);
     121              : 
     122              :             // There can be text with commands. Command always starts on a new line with '#'
     123              :             // Group subsequent commands into one `<CodeBlock>` element.
     124            2 :             if (itm.doText && itm.doText.indexOf("\n") >= 0) {
     125            2 :                 const parts = [];
     126            2 :                 const lines = itm.doText.split("\n");
     127            2 :                 let lastCommand = false;
     128            2 :                 lines.forEach(l => {
     129            2 :                     if (l[0] == "#") { // command
     130            2 :                         if (lastCommand) // When appending command remove "# ". Only the first command keeps it and it is removed later on
     131            2 :                             parts[parts.length - 1] += ("\n" + l.substring(2));
     132              :                         else
     133            2 :                             parts.push(l);
     134            2 :                         lastCommand = true;
     135            2 :                     } else {
     136            2 :                         parts.push(l);
     137            2 :                         lastCommand = false;
     138            2 :                     }
     139            2 :                 });
     140            2 :                 doElement = parts.map((p, index) => p[0] == "#"
     141            2 :                     ? codeBlock(p.substring(2), index)
     142            2 :                     : <span key={p}>{p}</span>);
     143            2 :             }
     144              : 
     145            2 :             return (
     146            1 :                 <StackItem key={itm.analysisId + (itm.ifText || "") + (itm.doText || "")}>
     147            2 :                     <div className="selinux-details" data-solution-id={itmIdx}>
     148            2 :                         <div>
     149            2 :                             <div>
     150            2 :                                 <span>{itm.ifText}</span>
     151            2 :                             </div>
     152            2 :                             <div>
     153            2 :                                 {itm.thenText}
     154            2 :                             </div>
     155            2 :                             <ExpandableSection toggleText={_("solution details")}>
     156            2 :                                 {doElement}
     157            2 :                             </ExpandableSection>
     158            2 :                             {msg}
     159            2 :                         </div>
     160            2 :                         {fixit}
     161            2 :                     </div>
     162            2 :                     {itmIdx != self.props.details.pluginAnalysis.length - 1 && <Divider />}
     163            2 :                 </StackItem>
     164              :             );
     165            2 :         });
     166            2 :         return <Stack hasGutter>{fixEntries}</Stack>;
     167            2 :     }
     168            3 : }
     169              : 
     170              : /* Show the audit log events for an alert */
     171            0 : const SELinuxEventLog = ({ details }) => {
     172            0 :     if (!details) {
     173              :         // details should be requested by default, so we just need to wait for them
     174            0 :         if (details === undefined)
     175            0 :             return <EmptyStatePanel loading title={ _("Waiting for details...") } />;
     176              :         else
     177            0 :             return <EmptyStatePanel icon={ExclamationCircleIcon} title={ _("Unable to get alert details.") } />;
     178            0 :     }
     179              : 
     180            0 :     const logEntries = details.auditEvent.map((itm, idx) => {
     181              :         // use the alert id and index in the event log array as the data key for react
     182              :         // if the log becomes dynamic, the entire log line might need to be considered as the key
     183            0 :         return <div key={ details.localId + "." + idx }>{itm}</div>;
     184            0 :     });
     185            0 :     return <div className="setroubleshoot-log">{logEntries}</div>;
     186            0 : };
     187              : 
     188              : /* Component to show a dismissable error, message as child text
     189              :  * dismissError callback function triggered when the close button is pressed
     190              :  */
     191            3 : class DismissableError extends React.Component {
     192            0 :     constructor(props) {
     193            0 :         super(props);
     194            0 :         this.handleDismissError = this.handleDismissError.bind(this);
     195            0 :     }
     196              : 
     197            0 :     handleDismissError(e) {
     198              :         // only consider primary mouse button
     199            0 :         if (!e || e.button !== 0)
     200            0 :             return;
     201            0 :         if (this.props.dismissError)
     202            0 :             this.props.dismissError();
     203            0 :         e.stopPropagation();
     204            0 :     }
     205              : 
     206            0 :     render() {
     207            0 :         return (
     208            0 :             <Alert isInline
     209            0 :                 variant='danger' title={this.props.children}
     210            0 :                 actionClose={<AlertActionCloseButton onClose={this.handleDismissError} />} />
     211              :         );
     212            0 :     }
     213            3 : }
     214              : 
     215              : /* Component to show selinux status and offer an option to change it
     216              :  * selinuxStatus      status of selinux on the system, properties as defined in selinux-client.js
     217              :  * selinuxStatusError error message from reading or setting selinux status/mode
     218              :  * changeSelinuxMode  function to use for changing the selinux enforcing mode
     219              :  * dismissError       function to dismiss the error message
     220              :  */
     221            3 : class SELinuxStatus extends React.Component {
     222            3 :     render() {
     223            3 :         const errorMessage = this.props.selinuxStatusError
     224            1 :             ? <DismissableError dismissError={this.props.dismissError}>{this.props.selinuxStatusError}</DismissableError>
     225            3 :             : null;
     226              : 
     227            3 :         if (this.props.selinuxStatus.enabled === undefined) {
     228              :             // we don't know the current state
     229            3 :             return (
     230            3 :                 <div>
     231            3 :                     {errorMessage}
     232            3 :                     <h3>{_("SELinux system status is unknown.")}</h3>
     233            3 :                 </div>
     234              :             );
     235            1 :         } else if (!this.props.selinuxStatus.enabled) {
     236              :             // selinux is disabled on the system, not much we can do
     237            1 :             return (
     238            1 :                 <div>
     239            1 :                     {errorMessage}
     240            1 :                     <h3>{_("SELinux is disabled on the system.")}</h3>
     241            1 :                 </div>
     242              :             );
     243            1 :         }
     244            3 :         const configUnknown = (this.props.selinuxStatus.configEnforcing === undefined);
     245            3 :         let note = null;
     246            3 :         if (configUnknown)
     247            1 :             note = _("The configured state is unknown, it might change on the next boot.");
     248            3 :         else if (!configUnknown && this.props.selinuxStatus.enforcing !== this.props.selinuxStatus.configEnforcing)
     249            2 :             note = _("Setting deviates from the configured state and will revert on the next boot.");
     250              : 
     251              :         // note = _("Setting deviates from the configured state and will revert on the next boot.");
     252              : 
     253            3 :         return (
     254            3 :             <Stack hasGutter className="selinux-policy-ct">
     255            3 :                 <Flex spaceItems={{ default: 'spaceItemsMd' }} alignItems={{ default: 'alignItemsCenter' }}>
     256            3 :                     <h2>{_("SELinux policy")}</h2>
     257            3 :                     <Switch isChecked={this.props.selinuxStatus.enforcing}
     258            3 :                             label={_("Enforcing")}
     259            3 :                             onChange={this.props.changeSelinuxMode} />
     260            3 :                 </Flex>
     261            3 :                 { note !== null &&
     262            2 :                     <Content component={ContentVariants.p}>
     263            2 :                         <Icon isInline status="info"><InfoCircleIcon /></Icon>
     264            2 :                         { "\n" }
     265            2 :                         { note }
     266            2 :                     </Content>
     267              :                 }
     268            3 :                 {errorMessage}
     269            3 :             </Stack>
     270              :         );
     271            3 :     }
     272            3 : }
     273              : 
     274              : /* The listing only shows if we have a connection to the dbus API
     275              :  * Otherwise we have blank slate: trying to connect, error
     276              :  * Expected properties:
     277              :  * connected    true if the client is connected to setroubleshoot-server via dbus
     278              :  * error        error message to show (in EmptyState if not connected, as a dismissable alert otherwise
     279              :  * dismissError callback, triggered for the dismissable error in connected state
     280              :  * deleteAlert  callback, triggered with an alert id as parameter to trigger deletion
     281              :  * entries   setroubleshoot entries
     282              :  *  - runFix      function to run fix
     283              :  *  - details     fix details as provided by the setroubleshoot client
     284              :  *  - description brief description of the error
     285              :  *  - count       how many times (>= 1) this alert occurred
     286              :  * selinuxStatus      status of selinux on the system, properties as defined in selinux-client.js
     287              :  * selinuxStatusError error message from reading or setting selinux status/mode
     288              :  * changeSelinuxMode  function to use for changing the selinux enforcing mode
     289              :  * dismissStatusError function that is triggered to dismiss the selinux status error
     290              :  */
     291            3 : export class SETroubleshootPage extends React.Component {
     292            3 :     constructor(props) {
     293            3 :         super(props);
     294            3 :         this.state = { selected: {} };
     295            3 :         this.handleDismissError = this.handleDismissError.bind(this);
     296            3 :         this.onSelect = this.onSelect.bind(this);
     297            3 :     }
     298              : 
     299            0 :     handleDismissError(e) {
     300              :         // only consider primary mouse button
     301            0 :         if (!e || e.button !== 0)
     302            0 :             return;
     303            0 :         if (this.props.dismissError)
     304            0 :             this.props.dismissError();
     305            0 :         e.stopPropagation();
     306            0 :     }
     307              : 
     308            0 :     onSelect(_, isSelected, rowId) {
     309            0 :         this.setState(prevState => ({
     310            0 :             selected: { ...prevState.selected, [this.props.entries[rowId].key]: isSelected }
     311            0 :         }));
     312            0 :     }
     313              : 
     314            3 :     render() {
     315              :         // if selinux is disabled, we only show EmptyState
     316            1 :         if (this.props.selinuxStatus.enabled === false) {
     317            1 :             return <EmptyStatePanel icon={ ExclamationCircleIcon } title={ _("SELinux is disabled on the system") } />;
     318            1 :         }
     319            3 :         const self = this;
     320            3 :         const title = _("SELinux access control errors");
     321            3 :         const emptyCaption = _("No SELinux alerts.");
     322            3 :         let emptyState;
     323            3 :         let entries;
     324            3 :         if (!this.props.connected) {
     325            3 :             if (this.props.connecting) {
     326            3 :                 emptyState = <EmptyStatePanel paragraph={ _("Connecting to SETroubleshoot daemon...") } loading />;
     327            1 :             } else {
     328              :                 // if we don't have setroubleshoot-server, be more subtle about saying that
     329            1 :                 emptyState = <EmptyStatePanel icon={ InfoCircleIcon }
     330            1 :                                               paragraph={_("Install setroubleshoot-server to troubleshoot SELinux events.")} />;
     331            1 :             }
     332            2 :         } else {
     333            2 :             entries = this.props.entries.map(function(itm, index) {
     334            2 :                 itm.runFix = self.props.runFix;
     335            2 :                 let listingDetail;
     336            2 :                 if (itm.details && 'firstSeen' in itm.details) {
     337            2 :                     if (itm.details.reportCount >= 2) {
     338            2 :                         listingDetail = cockpit.format(_("Occurred between $0 and $1"),
     339            2 :                                                        timeformat.dateTime(itm.details.firstSeen),
     340            2 :                                                        timeformat.dateTime(itm.details.lastSeen)
     341            2 :                         );
     342            1 :                     } else {
     343            1 :                         listingDetail = cockpit.format(_("Occurred $0"), timeformat.dateTime(itm.details.firstSeen));
     344            1 :                     }
     345            2 :                 }
     346            2 :                 const tabRenderers = [
     347            2 :                     {
     348            2 :                         name: _("Solutions"),
     349            2 :                         renderer: SELinuxEventDetails,
     350            2 :                         data: itm,
     351            2 :                     },
     352            2 :                     {
     353            2 :                         name: _("Audit log"),
     354            2 :                         renderer: SELinuxEventLog,
     355            2 :                         data: itm,
     356            2 :                     },
     357            2 :                 ];
     358              :                 // if the alert has level "red", it's critical
     359            2 :                 const criticalAlert = (itm.details && 'level' in itm.details && itm.details.level == "red")
     360            1 :                     ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle pf-v6-c-icon pf-m-lg" />
     361            2 :                     : null;
     362            2 :                 const columns = [
     363            2 :                     { title: criticalAlert },
     364            2 :                     { title: itm.description }
     365            2 :                 ];
     366            2 :                 if (itm.count > 1) {
     367            2 :                     columns.push({ title: <Badge isRead>{itm.count}</Badge>, props: { className: "pf-v6-c-table__action" } });
     368            1 :                 } else {
     369            1 :                     columns.push({ title: <span />, props: { className: "pf-v6-c-table__action" } });
     370            1 :                 }
     371            1 :                 const rowId = itm.details ? itm.details.localId : index;
     372            2 :                 return ({
     373            2 :                     props: { key: rowId, "data-row-id": rowId },
     374            1 :                     selected: self.state.selected[itm.details ? itm.details.localId : index],
     375            2 :                     disableSelection: !itm.details,
     376            2 :                     columns,
     377            2 :                     expandedContent: <ListingPanel tabRenderers={tabRenderers}
     378            2 :                                                    listingDetail={listingDetail} />
     379            2 :                 });
     380            2 :             });
     381            2 :         }
     382            3 :         let selectedCnt = 0;
     383            1 :         for (const k in this.state.selected) if (this.state.selected[k]) selectedCnt++;
     384            0 :         const onDeleteClick = () => {
     385            0 :             for (const k in this.state.selected)
     386            0 :                 if (this.state.selected[k])
     387            0 :                     this.props.deleteAlert(k).then(() => this.setState(prevState => ({ selected: { ...prevState.selected, [k]: false } })));
     388            0 :         };
     389            3 :         const actions = (
     390            3 :             !emptyState
     391            2 :                 ? <Button className="selinux-alert-dismiss"
     392            2 :                 variant="danger"
     393            2 :                 onClick={onDeleteClick}
     394            2 :                 isDisabled={ !this.props.deleteAlert || !selectedCnt}>
     395            1 :                     {selectedCnt ? cockpit.format(cockpit.ngettext("Dismiss $0 alert", "Dismiss $0 alerts", selectedCnt), selectedCnt) : _("Dismiss selected alerts")}
     396            2 :                 </Button>
     397            3 :                 : null
     398              :         );
     399            3 :         const troubleshooting = (
     400            3 :             <Card isPlain>
     401            3 :                 <CardHeader actions={{ actions }}>
     402            3 :                     <CardTitle component="h2">{title}</CardTitle>
     403            3 :                 </CardHeader>
     404            3 :                 <CardBody className="contains-list">
     405            3 :                     {!emptyState
     406            2 :                         ? <ListingTable aria-label={ title }
     407            2 :                                   id="selinux-alerts"
     408            2 :                                   onSelect={this.onSelect}
     409            2 :                                   gridBreakPoint=''
     410            2 :                                   emptyCaption={ emptyCaption }
     411            2 :                                   columns={[{ title: _("Alert") }, { title: _("Error message"), header: true }, { title: _("Occurrences") }]}
     412            2 :                                   showHeader={false}
     413            2 :                                   variant="compact"
     414            2 :                                   rows={entries} />
     415            3 :                         : emptyState}
     416            3 :                 </CardBody>
     417            3 :             </Card>
     418              :         );
     419              : 
     420            3 :         const modifications = (
     421            3 :             <Modifications
     422            3 :                 title={ _("System modifications") }
     423            3 :                 permitted={ this.props.selinuxStatus.permitted }
     424            3 :                 shell={ "semanage import <<EOF\n" + this.props.selinuxStatus.shell.trim() + "\nEOF" }
     425            3 :                 ansible={ this.props.selinuxStatus.ansible }
     426            3 :                 entries={ this.props.selinuxStatus.modifications }
     427            1 :                 failed={this.props.selinuxStatus.failed ? _("Error running semanage to discover system modifications") : null}
     428            3 :             />
     429              :         );
     430              : 
     431            3 :         let errorMessage;
     432            1 :         if (this.props.error) {
     433            1 :             errorMessage = (
     434            1 :                 <AlertGroup isToast>
     435            1 :                     <Alert
     436            1 :                         isLiveRegion
     437            1 :                         variant='danger' title={this.props.error}
     438            1 :                         actionClose={<AlertActionCloseButton onClose={this.handleDismissError} />} />
     439            1 :                 </AlertGroup>
     440              :             );
     441            1 :         }
     442              : 
     443            3 :         return (
     444            3 :             <>
     445            3 :                 {errorMessage}
     446            3 :                 <Page className="pf-m-no-sidebar">
     447            3 :                     <PageSection hasBodyWrapper={false} padding={{ default: "padding" }}>
     448            3 :                         <SELinuxStatus
     449            3 :                             selinuxStatus={this.props.selinuxStatus}
     450            3 :                             selinuxStatusError={this.props.selinuxStatusError}
     451            3 :                             changeSelinuxMode={this.props.changeSelinuxMode}
     452            3 :                             dismissError={this.props.dismissStatusError}
     453            3 :                         />
     454            3 :                     </PageSection>
     455            3 :                     <PageSection hasBodyWrapper={false}>
     456            3 :                         <Stack hasGutter>
     457            3 :                             <StackItem>{modifications}</StackItem>
     458            3 :                             <StackItem>{troubleshooting}</StackItem>
     459            3 :                         </Stack>
     460            3 :                     </PageSection>
     461            3 :                 </Page>
     462            3 :             </>
     463              :         );
     464            3 :     }
     465            3 : }
        

Generated by: LCOV version 2.0-1