* Copyright (C) 2016 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import React from "react";
import { Alert, AlertActionCloseButton, AlertGroup } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Divider } from "@patternfly/react-core/dist/esm/components/Divider/index.js";
import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
import { ExclamationCircleIcon, ExclamationTriangleIcon, InfoCircleIcon } from "@patternfly/react-icons";
import { Icon } from "@patternfly/react-core/dist/esm/components/Icon/index.js";
import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js";
import { CodeBlock, CodeBlockCode } from "@patternfly/react-core/dist/esm/components/CodeBlock/index.js";
import { Modifications } from "cockpit-components-modifications";
import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
import { ListingTable } from "cockpit-components-table.jsx";
import { ListingPanel } from 'cockpit-components-listing-panel.jsx';
import * as timeformat from 'timeformat';
import { Title, TitleSizes } from "@patternfly/react-core/dist/esm/components/Title";
const _ = cockpit.gettext;
* Props correspond to an item in the setroubleshoot dataStore
*/
class SELinuxEventDetails extends React.Component {
runFix(itmIdx, runCommand) {
const localId = this.props.details.localId;
const analysisId = this.props.details.pluginAnalysis[itmIdx].analysisId;
this.props.runFix(localId, analysisId, itmIdx, runCommand);
}
render() {
if (!this.props.details) {
if (this.props.details === undefined)
return <EmptyStatePanel loading title={ _("Waiting for details...") } />;
else
return <EmptyStatePanel icon={ExclamationCircleIcon} title={ _("Unable to get alert details.") } />;
}
const self = this;
const fixEntries = this.props.details.pluginAnalysis.map(function(itm, itmIdx) {
let fixit = null;
let fixit_command = null;
let msg = null;
* we can offer to run a single setsebool command for convenience */
let fixable = itm.fixable;
if (!fixable && itm.doText && itm.doText.startsWith("setsebool") && itm.doText.indexOf("\n") < 0) {
fixable = true;
fixit_command = itm.doText;
}
if (fixable) {
if ((itm.fix) && (itm.fix.plugin === itm.analysisId)) {
if (itm.fix.running) {
msg = (
<div>
<Spinner size="sm" className="setroubleshoot-progress-spinner" />
<span className="setroubleshoot-progress-message"> { _("Applying solution...") }</span>
</div>
);
} else {
if (itm.fix.success) {
msg = (
<Alert isInline variant="success" title={ _("Solution applied successfully") }>
{itm.fix.result}
</Alert>
);
} else {
msg = (
<Alert isInline variant="danger" title={ _("Solution failed") }>
{itm.fix.result}
</Alert>
);
}
}
}
if (!itm.fix) {
fixit = (
<div className="setroubleshoot-listing-action">
<Button variant="secondary" onClick={ self.runFix.bind(self, itmIdx, fixit_command) }>
{ _("Apply this solution") }
</Button>
</div>
);
}
} else {
fixit = (
<div className="setroubleshoot-listing-action">
<span>{ _("Unable to apply this solution automatically") }</span>
</div>
);
}
function codeBlock(text, key) {
return (
<CodeBlock key={key} aria-label={_("solution")}>
<CodeBlockCode>{text}</CodeBlockCode>
</CodeBlock>
);
}
let doElement = "";
if (itm.doText && itm.doText.indexOf("\n") < 0)
doElement = codeBlock(itm.doText);
if (itm.doText && itm.doText.indexOf("\n") >= 0) {
const parts = [];
const lines = itm.doText.split("\n");
let lastCommand = false;
lines.forEach(l => {
if (l[0] === "#") {
if (lastCommand)
parts[parts.length - 1] += ("\n" + l.substring(2));
else
parts.push(l);
lastCommand = true;
} else {
parts.push(l);
lastCommand = false;
}
});
doElement = parts.map((p, index) => p[0] === "#"
? codeBlock(p.substring(2), index)
: <span key={p}>{p}</span>);
}
return (
<StackItem key={itm.analysisId + (itm.ifText || "") + (itm.doText || "")}>
<div className="selinux-details" data-solution-id={itmIdx}>
<div>
<div>
<span>{itm.ifText}</span>
</div>
<div>
{itm.thenText}
</div>
<ExpandableSection toggleText={_("solution details")}>
{doElement}
</ExpandableSection>
{msg}
</div>
{fixit}
</div>
{itmIdx !== self.props.details.pluginAnalysis.length - 1 && <Divider />}
</StackItem>
);
});
return <Stack hasGutter>{fixEntries}</Stack>;
}
}
const SELinuxEventLog = ({ details }) => {
if (!details) {
if (details === undefined)
return <EmptyStatePanel loading title={ _("Waiting for details...") } />;
else
return <EmptyStatePanel icon={ExclamationCircleIcon} title={ _("Unable to get alert details.") } />;
}
const logEntries = details.auditEvent.map((itm, idx) => {
return <div key={ details.localId + "." + idx }>{itm}</div>;
});
return <div className="setroubleshoot-log">{logEntries}</div>;
};
* dismissError callback function triggered when the close button is pressed
*/
class DismissableError extends React.Component {
constructor(props) {
super(props);
this.handleDismissError = this.handleDismissError.bind(this);
}
handleDismissError(e) {
if (!e || e.button !== 0)
return;
if (this.props.dismissError)
this.props.dismissError();
e.stopPropagation();
}
render() {
return (
<Alert isInline
variant='danger' title={this.props.children}
actionClose={<AlertActionCloseButton onClose={this.handleDismissError} />} />
);
}
}
* selinuxStatus status of selinux on the system, properties as defined in selinux-client.js
* selinuxStatusError error message from reading or setting selinux status/mode
* changeSelinuxMode function to use for changing the selinux enforcing mode
* dismissError function to dismiss the error message
*/
const SELinuxStatus = ({ selinuxStatus, selinuxStatusError, changeSelinuxMode, dismissError }) => {
const errorMessage = selinuxStatusError
? <DismissableError dismissError={dismissError}>{selinuxStatusError}</DismissableError>
: null;
if (selinuxStatus.enabled === undefined) {
return (
<div>
{errorMessage}
<h3>{_("SELinux system status is unknown.")}</h3>
</div>
);
} else if (!selinuxStatus.enabled) {
return (
<div>
{errorMessage}
<h3>{_("SELinux is disabled on the system.")}</h3>
</div>
);
}
const configUnknown = (selinuxStatus.configEnforcing === undefined);
let note = null;
if (configUnknown)
note = _("The configured state is unknown, it might change on the next boot.");
else if (!configUnknown && selinuxStatus.enforcing !== selinuxStatus.configEnforcing)
note = _("Setting deviates from the configured state and will revert on the next boot.");
return (
<Stack hasGutter className="selinux-policy-ct">
<Flex spaceItems={{ default: 'spaceItemsMd' }} alignItems={{ default: 'alignItemsCenter' }}>
<Title headingLevel="h2" size={TitleSizes['3xl']}>
{_("SELinux policy")}
</Title>
<Switch isChecked={selinuxStatus.enforcing}
label={_("Enforcing")}
onChange={changeSelinuxMode} />
</Flex>
{ note !== null &&
<Content component={ContentVariants.p}>
<Icon isInline status="info"><InfoCircleIcon /></Icon>
{ "\n" }
{ note }
</Content>
}
{errorMessage}
</Stack>
);
};
* Otherwise we have blank slate: trying to connect, error
* Expected properties:
* connected true if the client is connected to setroubleshoot-server via dbus
* error error message to show (in EmptyState if not connected, as a dismissable alert otherwise
* dismissError callback, triggered for the dismissable error in connected state
* deleteAlert callback, triggered with an alert id as parameter to trigger deletion
* entries setroubleshoot entries
* - runFix function to run fix
* - details fix details as provided by the setroubleshoot client
* - description brief description of the error
* - count how many times (>= 1) this alert occurred
* selinuxStatus status of selinux on the system, properties as defined in selinux-client.js
* selinuxStatusError error message from reading or setting selinux status/mode
* changeSelinuxMode function to use for changing the selinux enforcing mode
* dismissStatusError function that is triggered to dismiss the selinux status error
*/
export class SETroubleshootPage extends React.Component {
constructor(props) {
super(props);
this.state = { selected: {} };
this.handleDismissError = this.handleDismissError.bind(this);
this.onSelect = this.onSelect.bind(this);
}
handleDismissError(e) {
if (!e || e.button !== 0)
return;
if (this.props.dismissError)
this.props.dismissError();
e.stopPropagation();
}
onSelect(_, isSelected, rowId) {
this.setState(prevState => ({
selected: { ...prevState.selected, [this.props.entries[rowId].key]: isSelected }
}));
}
render() {
if (this.props.selinuxStatus.enabled === false) {
return <EmptyStatePanel icon={ ExclamationCircleIcon } title={ _("SELinux is disabled on the system") } />;
}
const self = this;
const title = _("SELinux access control errors");
const emptyCaption = _("No SELinux alerts.");
let emptyState;
let entries;
if (!this.props.connected) {
if (this.props.connecting) {
emptyState = <EmptyStatePanel paragraph={ _("Connecting to SETroubleshoot daemon...") } loading />;
} else {
emptyState = <EmptyStatePanel icon={ InfoCircleIcon }
paragraph={_("Install setroubleshoot-server to troubleshoot SELinux events.")} />;
}
} else {
entries = this.props.entries.map(function(itm, index) {
itm.runFix = self.props.runFix;
let listingDetail;
if (itm.details && 'firstSeen' in itm.details) {
if (itm.details.reportCount >= 2) {
listingDetail = cockpit.format(_("Occurred between $0 and $1"),
timeformat.dateTime(itm.details.firstSeen),
timeformat.dateTime(itm.details.lastSeen)
);
} else {
listingDetail = cockpit.format(_("Occurred $0"), timeformat.dateTime(itm.details.firstSeen));
}
}
const tabRenderers = [
{
name: _("Solutions"),
renderer: SELinuxEventDetails,
data: itm,
},
{
name: _("Audit log"),
renderer: SELinuxEventLog,
data: itm,
},
];
const criticalAlert = (itm.details && 'level' in itm.details && itm.details.level === "red")
? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle pf-v6-c-icon pf-m-lg" />
: null;
const columns = [
{ title: criticalAlert },
{ title: itm.description }
];
if (itm.count > 1) {
columns.push({ title: <Badge isRead>{itm.count}</Badge>, props: { className: "pf-v6-c-table__action" } });
} else {
columns.push({ title: <span />, props: { className: "pf-v6-c-table__action" } });
}
const rowId = itm.details ? itm.details.localId : index;
return ({
props: { key: rowId, "data-row-id": rowId },
selected: self.state.selected[itm.details ? itm.details.localId : index],
disableSelection: !itm.details,
columns,
expandedContent: <ListingPanel tabRenderers={tabRenderers}
listingDetail={listingDetail} />
});
});
}
let selectedCnt = 0;
for (const k in this.state.selected) if (this.state.selected[k]) selectedCnt++;
const onDeleteClick = () => {
for (const k in this.state.selected)
if (this.state.selected[k])
this.props.deleteAlert(k).then(() => this.setState(prevState => ({ selected: { ...prevState.selected, [k]: false } })));
};
const actions = (
!emptyState
? <Button className="selinux-alert-dismiss"
variant="danger"
onClick={onDeleteClick}
isDisabled={ !this.props.deleteAlert || !selectedCnt}>
{selectedCnt ? cockpit.format(cockpit.ngettext("Dismiss $0 alert", "Dismiss $0 alerts", selectedCnt), selectedCnt) : _("Dismiss selected alerts")}
</Button>
: null
);
const troubleshooting = (
<Card isPlain>
<CardHeader actions={{ actions }}>
<CardTitle component="h2">{title}</CardTitle>
</CardHeader>
<CardBody className="contains-list">
{!emptyState
? <ListingTable aria-label={ title }
id="selinux-alerts"
onSelect={this.onSelect}
gridBreakPoint=''
emptyCaption={ emptyCaption }
columns={[{ title: _("Alert") }, { title: _("Error message"), header: true }, { title: _("Occurrences") }]}
showHeader={false}
variant="compact"
rows={entries} />
: emptyState}
</CardBody>
</Card>
);
const modifications = (
<Modifications
title={ _("System modifications") }
permitted={ this.props.selinuxStatus.permitted }
shell={ "semanage import <<EOF\n" + this.props.selinuxStatus.shell.trim() + "\nEOF" }
ansible={ this.props.selinuxStatus.ansible }
entries={ this.props.selinuxStatus.modifications }
failed={this.props.selinuxStatus.failed ? _("Error running semanage to discover system modifications") : null}
/>
);
let errorMessage;
if (this.props.error) {
errorMessage = (
<AlertGroup isToast>
<Alert
isLiveRegion
variant='danger' title={this.props.error}
actionClose={<AlertActionCloseButton onClose={this.handleDismissError} />} />
</AlertGroup>
);
}
return (
<>
{errorMessage}
<Page className="pf-m-no-sidebar">
<PageSection hasBodyWrapper={false} padding={{ default: "padding" }}>
<SELinuxStatus
selinuxStatus={this.props.selinuxStatus}
selinuxStatusError={this.props.selinuxStatusError}
changeSelinuxMode={this.props.changeSelinuxMode}
dismissError={this.props.dismissStatusError}
/>
</PageSection>
<PageSection hasBodyWrapper={false}>
<Stack hasGutter>
<StackItem>{modifications}</StackItem>
<StackItem>{troubleshooting}</StackItem>
</Stack>
</PageSection>
</Page>
</>
);
}
}