updated installed services table

This commit is contained in:
its-a-feature
2026-05-12 16:51:36 -06:00
parent b8000b9630
commit 81126e65db
8 changed files with 821 additions and 360 deletions
@@ -1,5 +1,4 @@
import React from 'react';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import {C2ProfileBuildDialog} from './C2ProfileBuildDialog';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
@@ -34,6 +33,10 @@ import MythicTableCell from "../../MythicComponents/MythicTableCell";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {C2ProfileStatusSummary} from "./InstalledServiceStatus";
import {
InstalledServiceIdentity,
InstalledServiceMetadataSummary
} from "./InstalledServiceTableComponents";
const toggleDeleteStatus = gql`
@@ -144,6 +147,8 @@ export function C2ProfilesRow({service, showDeleted}) {
updateDeleted({variables: {c2profile_id: service.id, deleted: !service.deleted}})
setOpenDeleteDialog(false);
}
const typeLabel = service.is_p2p ? "P2P" : "Egress";
const supportedAgents = React.useMemo(() => (service.payloadtypec2profiles || []).filter((pt) => !pt.payloadtype.deleted).map((c) => c.payloadtype.name), [service.payloadtypec2profiles]);
if(service.deleted && !showDeleted){
return null;
}
@@ -175,27 +180,22 @@ export function C2ProfilesRow({service, showDeleted}) {
</MythicTableCell>
<MythicTableCell>
<div className="mythic-installed-service-identity">
<span className="mythic-installed-service-name">{service.name}</span>
<C2ProfileStatusSummary service={service} />
</div>
<InstalledServiceIdentity
name={service.name}
typeLabel={typeLabel}
deleted={service.deleted}
status={<C2ProfileStatusSummary service={service} />}
/>
</MythicTableCell>
<MythicTableCell>{service.is_p2p ? "P2P" : "Egress"}</MythicTableCell>
<MythicTableCell>
<Typography variant="body1" component="p">
<b>Author:</b> {service.author}
</Typography>
{service.semver !== "" &&
<Typography variant="body1" component="p">
<b>Version:</b> {service.semver}
</Typography>
}
<Typography variant="body1" component="p">
<b>Supported Agents:</b> {service.payloadtypec2profiles.filter( (pt) => !pt.payloadtype.deleted ).map(c => c.payloadtype.name).join(", ")}
</Typography>
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Description: </b>{service.description}
</Typography>
<InstalledServiceMetadataSummary
items={[
{label: "Author", value: service.author},
{label: "Version", value: service.semver, chip: true},
{label: "Supported Agents", value: supportedAgents},
]}
description={service.description}
/>
</MythicTableCell>
<MythicTableCell>
{service.container_running ? (
@@ -312,6 +312,7 @@ export function C2ProfilesRow({service, showDeleted}) {
)}
</Popper>
</MythicTableCell>
</TableRow>
{openBuildingDialog &&
<MythicDialog fullWidth={true} maxWidth="lg" open={openBuildingDialog}
onClose={()=>{setOpenBuildingDialog(false);}}
@@ -364,7 +365,6 @@ export function C2ProfilesRow({service, showDeleted}) {
innerDialog={<C2ProfileListFilesDialog container_name={service.name} {...service} onClose={()=>{setOpenListFilesDialog(false);}} />}
/>
}
</TableRow>
</>
);
@@ -1,10 +1,5 @@
import React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import PublicIcon from '@mui/icons-material/Public';
import {IconButton} from '@mui/material';
import {gql, useMutation} from '@apollo/client';
@@ -21,6 +16,14 @@ import AttachFileIcon from '@mui/icons-material/AttachFile';
import {ConsumingServicesGetIDPMetadataDialog} from "./ConsumingServicesGetIDPMetadataDialog";
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
import {InstalledServiceContainerStatus} from "./InstalledServiceStatus";
import {
InstalledServiceDefinitionList,
InstalledServiceDetailRow,
InstalledServiceDetailSection,
InstalledServiceDetailToggle,
InstalledServiceIdentity,
InstalledServiceMetadataSummary
} from "./InstalledServiceTableComponents";
const testWebhookMutation = gql`
mutation testWebhookWorks($service_type: String!){
@@ -85,6 +88,7 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
testLog({variables: {service_type: service_type}});
}
const [openIDPMetadata, setOpenIDPMetadata] = React.useState(false);
const [openDetails, setOpenDetails] = React.useState(false);
const IDPMetadataRef = React.useRef({"container": "", "idp": ""});
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
const [openDelete, setOpenDeleteDialog] = React.useState(false);
@@ -165,9 +169,6 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
</IconButton>
</MythicStyledTooltip>
);
const renderContainerStatus = (w) => (
<InstalledServiceContainerStatus isOnline={w.container_running} />
);
const renderFileButton = (w) => (
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
@@ -180,179 +181,161 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
</IconButton>
</MythicStyledTooltip>
);
const renderSubscriptionTestButtons = (w, events, icon, onClick, prefix) => (
events.map(s => (
<MythicStyledTooltip title={`${prefix} ${s}`} key={`${w.id}-${prefix}-${s}`}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!getSubscriptionNames(w).includes(s) || !w.container_running}
onClick={() => onClick(s)}
size="small">
{icon}
</IconButton>
</MythicStyledTooltip>
))
);
const getSubscriptionNames = (w) => {
if(!Array.isArray(w.subscriptions)){return []}
return w.subscriptions.map((subscription) => subscription?.name || subscription).filter(Boolean);
};
const renderIdentityProviderMetadata = (w) => {
const subscriptions = Array.isArray(w.subscriptions) ? w.subscriptions : [];
if(subscriptions.length === 0){
return <span className="mythic-installed-service-empty-value">Not set</span>;
}
return (
<span className="mythic-installed-service-action-chip-list">
{subscriptions.map((subscription) => {
const providerName = subscription?.name || subscription;
return (
<MythicStyledTooltip title={w.container_running ? "Fetch container metadata" : "Container is offline"} key={`${w.name}-${providerName}`}>
<button
className="mythic-installed-service-action-chip"
disabled={!w.container_running}
onClick={() => getIDPMetadata(w.name, providerName)}
type="button"
>
<span>{providerName}</span>
<PermIdentityTwoToneIcon fontSize="small" />
</button>
</MythicStyledTooltip>
);
})}
</span>
);
};
const renderBaseRow = ({w, typeLabel, metadataItems, actions, hasDetails = false}) => (
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{renderDeleteButton(w)}
</MythicTableCell>
<MythicTableCell>
<InstalledServiceIdentity
name={w.name}
typeLabel={typeLabel}
deleted={w.deleted}
status={<InstalledServiceContainerStatus isOnline={w.container_running} />}
/>
</MythicTableCell>
<MythicTableCell>
<InstalledServiceMetadataSummary
items={metadataItems}
description={w.description}
/>
</MythicTableCell>
<MythicTableCell>
<div className="mythic-table-row-actions mythic-service-actions">
{renderFileButton(w)}
{actions}
{hasDetails &&
<InstalledServiceDetailToggle open={openDetails} onClick={() => setOpenDetails((current) => !current)} />
}
</div>
</MythicTableCell>
</TableRow>
);
const getTableRow = (w) => {
switch(service.type){
case "webhook":
return (
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{renderDeleteButton(w)}
</MythicTableCell>
<MythicTableCell>
<Typography variant={"h5"}>
{w.name}
</Typography>
{renderContainerStatus(w)}
<Typography variant={"body"}>
<b>Type: </b>{w.type}
</Typography>
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
</MythicTableCell>
<MythicTableCell>
{renderFileButton(w)}
</MythicTableCell>
<MythicTableCell>
<div className="mythic-table-row-actions mythic-service-actions">
{webhook_events.map(s => (
<MythicStyledTooltip title={"test webhook " + s} key={w.id + "webhook_" + s}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!w.subscriptions.includes(s) || !w.container_running}
onClick={() => {
issueTestWebhook(s)
}}
size="small">
<PublicIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
))}
</div>
</MythicTableCell>
</TableRow>
)
return renderBaseRow({
w,
typeLabel: "Webhook",
metadataItems: [
{label: "Type", value: w.type},
{label: "Version", value: w.semver, chip: true},
{label: "Subscriptions", value: getSubscriptionNames(w)},
],
actions: renderSubscriptionTestButtons(w, webhook_events, <PublicIcon fontSize="small" />, issueTestWebhook, "test webhook"),
});
case "logging":
return (
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{renderDeleteButton(w)}
</MythicTableCell>
<MythicTableCell>
<Typography variant={"h5"}>
{w.name}
</Typography>
{renderContainerStatus(w)}
<Typography variant={"body"}>
<b>Type: </b>{w.type}
</Typography>
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
</MythicTableCell>
<MythicTableCell>
{renderFileButton(w)}
</MythicTableCell>
<MythicTableCell>
<div className="mythic-table-row-actions mythic-service-actions">
{logging_events.map(s => (
<MythicStyledTooltip title={"test logging " + s} key={w.id + "logging_" + s}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!w.subscriptions.includes(s) || !w.container_running}
onClick={() => {
issueTestLog(s)
}}
size="small">
<SyncAltIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
))}
</div>
</MythicTableCell>
</TableRow>
)
return renderBaseRow({
w,
typeLabel: "Logger",
metadataItems: [
{label: "Type", value: w.type},
{label: "Version", value: w.semver, chip: true},
{label: "Subscriptions", value: getSubscriptionNames(w)},
],
actions: renderSubscriptionTestButtons(w, logging_events, <SyncAltIcon fontSize="small" />, issueTestLog, "test logging"),
});
case "eventing":
return (
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{renderDeleteButton(w)}
</MythicTableCell>
<MythicTableCell>
<Typography variant={"h5"}>
{w.name}
</Typography>
{renderContainerStatus(w)}
<Typography variant={"body"}>
<b>Type: </b>{w.type}
</Typography>
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
</MythicTableCell>
<MythicTableCell>
{renderFileButton(w)}
</MythicTableCell>
<MythicTableCell>
<Table>
<TableHead>
<TableRow>
<TableCell>Function</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{w.subscriptions.map(s => (
<TableRow key={s.name} hover>
<MythicTableCell><b>{s.name}</b></MythicTableCell>
<MythicTableCell>{s.description}</MythicTableCell>
</TableRow>
))}
</TableBody>
</Table>
</MythicTableCell>
</TableRow>
)
return renderBaseRow({
w,
typeLabel: "Eventing",
metadataItems: [
{label: "Type", value: w.type},
{label: "Version", value: w.semver, chip: true},
{label: "Functions", value: getSubscriptionNames(w)},
],
hasDetails: true,
});
case "auth":
return (
(showDeleted || !w.deleted) &&
<TableRow key={w.id} hover>
<MythicTableCell>
{renderDeleteButton(w)}
</MythicTableCell>
<MythicTableCell>
<Typography variant={"h5"}>
{w.name}
</Typography>
{renderContainerStatus(w)}
<Typography variant={"body"}>
<b>Type: </b>{w.type}
</Typography>
<Typography variant={"body2"}>
<b>Description: </b>{w.description}
</Typography>
</MythicTableCell>
<MythicTableCell>
{renderFileButton(w)}
</MythicTableCell>
<MythicTableCell>
{w.subscriptions.map(s => (
<Typography key={s.name + s.type + w.name} style={{display: "block"}}>
<MythicStyledTooltip title={"Fetch Container Metadata"} >
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={() => getIDPMetadata(w.name, s.name)}
disabled={!w.container_running}
size="small"
>
<PermIdentityTwoToneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
{s.name}
</Typography>
))}
</MythicTableCell>
</TableRow>
)
return renderBaseRow({
w,
typeLabel: "Auth",
metadataItems: [
{label: "Type", value: w.type},
{label: "Version", value: w.semver, chip: true},
{label: "Identity Providers", value: getSubscriptionNames(w), render: renderIdentityProviderMetadata(w)},
],
});
default:
return null;
}
}
const getDetailContent = (w) => {
switch(service.type){
case "eventing":
return (
<InstalledServiceDetailSection title="Eventing functions" count={(w.subscriptions || []).length}>
<InstalledServiceDefinitionList
items={(w.subscriptions || []).map((subscription) => ({
title: subscription.name,
description: subscription.description,
}))}
emptyText="No eventing functions registered."
/>
</InstalledServiceDetailSection>
);
case "auth":
return null;
default:
return null;
}
};
if(localData.deleted && !showDeleted){
return null;
}
const detailContent = getDetailContent(localData);
return (
<>
{getTableRow(localData)}
{detailContent &&
<InstalledServiceDetailRow open={openDetails} colSpan={4}>
{detailContent}
</InstalledServiceDetailRow>
}
{openDelete &&
<MythicConfirmDialog onClose={() => { setOpenDeleteDialog(false); }}
onSubmit={onAcceptDelete}
@@ -1,6 +1,5 @@
import React from 'react';
import AttachFileIcon from '@mui/icons-material/AttachFile';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import {useMutation, gql} from '@apollo/client';
import {snackActions} from '../../utilities/Snackbar';
@@ -14,11 +13,15 @@ import MythicTableCell from "../../MythicComponents/MythicTableCell";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import {InstalledServiceContainerStatus} from "./InstalledServiceStatus";
import {
InstalledServiceDefinitionList,
InstalledServiceDetailRow,
InstalledServiceDetailSection,
InstalledServiceDetailToggle,
InstalledServiceIdentity,
InstalledServiceListValue
} from "./InstalledServiceTableComponents";
const toggleDeleteStatus = gql`
mutation toggleCustomBrowserDeleteStatus($custombrowser_id: Int!, $deleted: Boolean!){
@@ -31,6 +34,7 @@ mutation toggleCustomBrowserDeleteStatus($custombrowser_id: Int!, $deleted: Bool
export function CustomBrowserRow({service, showDeleted}) {
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
const [openDelete, setOpenDeleteDialog] = React.useState(false);
const [openDetails, setOpenDetails] = React.useState(false);
const [updateDeleted] = useMutation(toggleDeleteStatus, {
onCompleted: data => {
},
@@ -50,8 +54,8 @@ export function CustomBrowserRow({service, showDeleted}) {
return null;
}
return (
<TableRow >
<>
<TableRow hover>
<MythicTableCell>
{service.deleted ? (
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" size="small" onClick={()=>{setOpenDeleteDialog(true);}}><RestoreFromTrashOutlinedIcon fontSize="small" /></IconButton>
@@ -69,108 +73,104 @@ export function CustomBrowserRow({service, showDeleted}) {
<FontAwesomeIcon icon={faFolderOpen} style={{width: "60px", height: "60px"}} />
</MythicTableCell>
<MythicTableCell>
<div className="mythic-installed-service-identity">
<span className="mythic-installed-service-name">{service.name}</span>
<InstalledServiceContainerStatus isOnline={service.container_running} />
<InstalledServiceIdentity
name={service.name}
typeLabel={service.type}
deleted={service.deleted}
status={<InstalledServiceContainerStatus isOnline={service.container_running} />}
/>
</MythicTableCell>
<MythicTableCell>
<div className="mythic-installed-service-browser-metadata">
{service.author &&
<div className="mythic-installed-service-browser-author" title={service.author}>
{service.author}
</div>
}
<div className="mythic-installed-service-browser-metrics">
{service.semver &&
<span className="mythic-installed-service-browser-metric">
<span>Version</span>
<InstalledServiceListValue value={[service.semver]} limit={1} />
</span>
}
<span className="mythic-installed-service-browser-metric">
<span>Export</span>
<strong>{service.export_function === "" ? "False" : "True"}</strong>
</span>
<span className="mythic-installed-service-browser-metric">
<span>Row actions</span>
<strong>{(service.row_actions || []).length}</strong>
</span>
<span className="mythic-installed-service-browser-metric">
<span>Columns</span>
<strong>{(service.columns || []).length}</strong>
</span>
<span className="mythic-installed-service-browser-metric">
<span>Inputs</span>
<strong>{(service.extra_table_inputs || []).length}</strong>
</span>
</div>
{service.description &&
<div className="mythic-installed-service-description" title={service.description}>
<span>Description</span>
<p>{service.description}</p>
</div>
}
</div>
</MythicTableCell>
<MythicTableCell>
{service.type}
<div className="mythic-table-row-actions">
<MythicStyledTooltip title={service.container_running ? "View Files" : "Unable to view files because container is offline"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!service.container_running}
onClick={()=>{setOpenListFilesDialog(true);}}
size="small">
<AttachFileIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<InstalledServiceDetailToggle open={openDetails} onClick={() => setOpenDetails((current) => !current)} />
</div>
</MythicTableCell>
<MythicTableCell>
<Typography variant="body1" component="p">
<b>Author:</b> {service.author}
</Typography>
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Description: </b>{service.description}
</Typography>
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Version: </b>{service.semver}
</Typography>
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Export Capabilities: </b>{service.export_function === "" ? "False" : "True"}
</Typography>
</MythicTableCell>
<MythicTableCell>
<MythicStyledTooltip title={service.container_running ? "View Files" : "Unable to view files because container is offline"}>
<IconButton
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
disabled={!service.container_running}
onClick={()=>{setOpenListFilesDialog(true);}}
size="small">
<AttachFileIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
</MythicTableCell>
<MythicTableCell style={{padding: 0}}>
<TableContainer className="mythicElement">
<Table>
<TableHead>
<TableRow hover>
<MythicTableCell>{"Custom Row Action"}</MythicTableCell>
<MythicTableCell>{"Needed UI Feature"}</MythicTableCell>
</TableRow>
</TableHead>
<TableBody>
{service.row_actions.map(c => (
<TableRow key={c.name} hover>
<MythicTableCell>{c.name}</MythicTableCell>
<MythicTableCell>{c.ui_feature}</MythicTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TableContainer className="mythicElement">
<Table>
<TableHead>
<TableRow hover>
<MythicTableCell>{"Display Table Column"}</MythicTableCell>
<MythicTableCell>{"Metadata Key"}</MythicTableCell>
</TableRow>
</TableHead>
<TableBody>
{service.columns.map(c => (
<TableRow key={c.name} hover>
<MythicTableCell>{c.name}</MythicTableCell>
<MythicTableCell>{c.key}</MythicTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TableContainer className="mythicElement">
<Table>
<TableHead>
<TableRow hover>
<MythicTableCell>{"Extra Table Task Parameter"}</MythicTableCell>
<MythicTableCell>{"Parameter Name"}</MythicTableCell>
</TableRow>
</TableHead>
<TableBody>
{service.extra_table_inputs.map(c => (
<TableRow key={c.name} hover>
<MythicTableCell>
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>{c.display_name}: </b> {c.description}
</Typography>
</MythicTableCell>
<MythicTableCell>{c.name}</MythicTableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</MythicTableCell>
</TableRow>
<InstalledServiceDetailRow open={openDetails} colSpan={5}>
<InstalledServiceDetailSection title="Custom row actions" count={(service.row_actions || []).length}>
<InstalledServiceDefinitionList
items={(service.row_actions || []).map((action) => ({
title: action.name,
subtitle: action.ui_feature,
}))}
emptyText="No custom row actions."
/>
</InstalledServiceDetailSection>
<InstalledServiceDetailSection title="Display columns" count={(service.columns || []).length}>
<InstalledServiceDefinitionList
items={(service.columns || []).map((column) => ({
title: column.name,
subtitle: column.key,
}))}
emptyText="No display columns."
/>
</InstalledServiceDetailSection>
<InstalledServiceDetailSection title="Extra task inputs" count={(service.extra_table_inputs || []).length}>
<InstalledServiceDefinitionList
items={(service.extra_table_inputs || []).map((input) => ({
title: input.display_name,
subtitle: input.name,
description: input.description,
}))}
emptyText="No extra task inputs."
/>
</InstalledServiceDetailSection>
</InstalledServiceDetailRow>
{openListFilesDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openListFilesDialog}
onClose={()=>{setOpenListFilesDialog(false);}}
innerDialog={<C2ProfileListFilesDialog container_name={service.name} {...service} onClose={()=>{setOpenListFilesDialog(false);}} />}
/>
}
</TableRow>
</>
);
}
@@ -0,0 +1,190 @@
import React from 'react';
import Collapse from '@mui/material/Collapse';
import IconButton from '@mui/material/IconButton';
import TableRow from '@mui/material/TableRow';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import MythicTableCell from "../../MythicComponents/MythicTableCell";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
const normalizeValueList = (value) => {
if(Array.isArray(value)){
return value.filter((entry) => entry !== undefined && entry !== null && `${entry}`.length > 0);
}
if(value === undefined || value === null || value === ""){return []}
return [`${value}`];
};
const hasValue = (value) => normalizeValueList(value).length > 0;
export const getInstalledServiceListTitle = (value) => normalizeValueList(value).join(", ");
export function InstalledServiceIdentity({name, typeLabel, status, deleted}) {
return (
<div className="mythic-installed-service-identity">
<div className="mythic-installed-service-name-row">
<span className="mythic-installed-service-name">{name}</span>
{typeLabel &&
<MythicStatusChip label={typeLabel} status={deleted ? "deleted" : "neutral"} showIcon={deleted} />
}
</div>
{status}
</div>
);
}
export function InstalledServiceListValue({value, limit = 4}) {
const values = normalizeValueList(value);
if(values.length === 0){
return <span className="mythic-installed-service-empty-value">Not set</span>;
}
const visibleValues = values.slice(0, limit);
const hiddenCount = values.length - visibleValues.length;
return (
<span className="mythic-installed-service-chip-list" title={getInstalledServiceListTitle(values)}>
{visibleValues.map((entry, index) => (
<span className="mythic-installed-service-chip" key={`${entry}-${index}`}>{entry}</span>
))}
{hiddenCount > 0 &&
<span className="mythic-installed-service-chip mythic-installed-service-chip-more">+{hiddenCount}</span>
}
</span>
);
}
export function InstalledServiceMetadataSummary({items = [], description}) {
const visibleItems = items.filter((item) => hasValue(item.value) || item.render);
const renderValue = (item) => {
if(item.render){
return (
<span className="mythic-installed-service-metadata-custom-value">
{item.render}
</span>
);
}
if(item.chip){
return <InstalledServiceListValue value={[item.value]} limit={1} />;
}
if(Array.isArray(item.value)){
return <InstalledServiceListValue value={item.value} limit={item.limit} />;
}
return (
<span className={item.code ? "mythic-installed-service-metadata-code" : "mythic-installed-service-metadata-value"} title={`${item.value}`}>
{`${item.value}`}
</span>
);
};
return (
<div className="mythic-installed-service-metadata-summary">
{visibleItems.length > 0 &&
<div className="mythic-installed-service-metadata-grid">
{visibleItems.map((item) => (
<div className="mythic-installed-service-metadata-item" key={item.label}>
<span className="mythic-installed-service-metadata-label">{item.label}</span>
{renderValue(item)}
</div>
))}
</div>
}
{description &&
<div className="mythic-installed-service-description" title={description}>
<span>Description</span>
<p>{description}</p>
</div>
}
</div>
);
}
export function InstalledServiceDetailToggle({open, onClick, label = "details"}) {
return (
<MythicStyledTooltip title={open ? `Hide ${label}` : `Show ${label}`}>
<IconButton
aria-label={open ? `hide ${label}` : `show ${label}`}
aria-expanded={open}
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
onClick={onClick}
size="small"
>
{open ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
</IconButton>
</MythicStyledTooltip>
);
}
export function InstalledServiceDetailRow({open, colSpan, children}) {
return (
<TableRow className="mythic-installed-service-detail-row">
<MythicTableCell className="mythic-installed-service-detail-cell" colSpan={colSpan}>
<Collapse in={open} timeout="auto" unmountOnExit>
<div className="mythic-installed-service-detail-panel">
{children}
</div>
</Collapse>
</MythicTableCell>
</TableRow>
);
}
export function InstalledServiceDetailSection({title, count, children}) {
return (
<div className="mythic-installed-service-detail-section">
<div className="mythic-installed-service-detail-section-header">
<span>{title}</span>
{count !== undefined &&
<MythicStatusChip label={`${count}`} status={count > 0 ? "info" : "neutral"} showIcon={false} />
}
</div>
<div className="mythic-installed-service-detail-section-body">
{children}
</div>
</div>
);
}
export function InstalledServiceDetailList({items = []}) {
const visibleItems = items.filter((item) => hasValue(item.value));
if(visibleItems.length === 0){
return <div className="mythic-installed-service-empty-value">No additional details.</div>;
}
return (
<div className="mythic-installed-service-detail-list">
{visibleItems.map((item) => (
<div className="mythic-installed-service-detail-list-item" key={item.label}>
<span className="mythic-installed-service-detail-label">{item.label}</span>
{Array.isArray(item.value) ?
<InstalledServiceListValue value={item.value} limit={item.limit || 12} /> :
<span className={item.code ? "mythic-installed-service-metadata-code" : "mythic-installed-service-detail-value"}>{`${item.value}`}</span>
}
</div>
))}
</div>
);
}
export function InstalledServiceDefinitionList({items = [], emptyText = "No entries."}) {
if(items.length === 0){
return <div className="mythic-installed-service-empty-value">{emptyText}</div>;
}
return (
<div className="mythic-installed-service-definition-list">
{items.map((item, index) => (
<div className="mythic-installed-service-definition-row" key={`${item.title || item.label || "item"}-${index}`}>
<div className="mythic-installed-service-definition-main">
<span className="mythic-installed-service-definition-title">{item.title || item.label}</span>
{item.subtitle &&
<span className="mythic-installed-service-definition-subtitle">{item.subtitle}</span>
}
{item.description &&
<span className="mythic-installed-service-definition-description">{item.description}</span>
}
</div>
{item.action &&
<div className="mythic-installed-service-definition-action">{item.action}</div>
}
</div>
))}
</div>
);
}
@@ -1,5 +1,4 @@
import React, { useEffect } from 'react';
import Typography from '@mui/material/Typography';
import React from 'react';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {PayloadTypeBuildDialog} from './PayloadTypeBuildDialog';
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
@@ -19,6 +18,10 @@ import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
import AttachFileIcon from '@mui/icons-material/AttachFile';
import {InstalledServiceContainerStatus} from "./InstalledServiceStatus";
import {
InstalledServiceIdentity,
InstalledServiceMetadataSummary
} from "./InstalledServiceTableComponents";
const toggleDeleteStatus = gql`
mutation togglePayloadTypeDeleteStatus($payloadtype_id: Int!, $deleted: Boolean!){
@@ -29,9 +32,7 @@ mutation togglePayloadTypeDeleteStatus($payloadtype_id: Int!, $deleted: Boolean!
`;
export function PayloadTypeRow({service, showDeleted}){
const [wrappedPayloads, setWrappedPayloads] = React.useState("");
const [openBuildingDialog, setOpenBuildingDialog] = React.useState(false);
const [supportedOS, setSupportedOS] = React.useState("");
const [openDelete, setOpenDeleteDialog] = React.useState(false);
const [updateDeleted] = useMutation(toggleDeleteStatus, {
onCompleted: data => {
@@ -47,22 +48,13 @@ export function PayloadTypeRow({service, showDeleted}){
});
const [openCommandsDialog, setOpenCommandsDialog] = React.useState(false);
const [openListFilesDialog, setOpenListFilesDialog] = React.useState(false);
const typeLabel = service.wrapper ? "Wrapper" : service.agent_type === "agent" ? "Agent" : service.agent_type === "service" ? "3rd Party Service" : "Command Augmentation";
const wrappedPayloadNames = React.useMemo(() => (service.wrap_these_payload_types || []).map((cur) => cur.wrapped.name), [service.wrap_these_payload_types]);
const supportedOS = React.useMemo(() => service.supported_os || [], [service.supported_os]);
const onAcceptDelete = () => {
updateDeleted({variables: {payloadtype_id: service.id, deleted: !service.deleted}})
setOpenDeleteDialog(false);
}
useEffect( () => {
if( service.wrap_these_payload_types.length > 0){
const wrapped = service.wrap_these_payload_types.map( (cur) => {
return cur.wrapped.name;
});
setWrappedPayloads(wrapped.join(", "));
}
else{
setWrappedPayloads("");
}
setSupportedOS(service.supported_os.join(", "));
}, [service.wrap_these_payload_types, service.supported_os]);
if(service.deleted && !showDeleted){
return null;
}
@@ -84,34 +76,23 @@ export function PayloadTypeRow({service, showDeleted}){
<MythicAgentSVGIcon payload_type={service.name} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
</MythicTableCell>
<MythicTableCell>
<div className="mythic-installed-service-identity">
<span className="mythic-installed-service-name">{service.name}</span>
<InstalledServiceContainerStatus isOnline={service.container_running} />
</div>
<InstalledServiceIdentity
name={service.name}
typeLabel={typeLabel}
deleted={service.deleted}
status={<InstalledServiceContainerStatus isOnline={service.container_running} />}
/>
</MythicTableCell>
<MythicTableCell>
{service.wrapper ? "Wrapper" : service.agent_type === "agent" ? "Agent" : service.agent_type === "service" ? "3rd Party Service" : "Command Augmentation"}
</MythicTableCell>
<MythicTableCell>
<Typography variant="body1" component="p">
<b>Author:</b> {service.author}
</Typography>
<Typography variant="body1" component="p">
<b>Supported Operating Systems:</b> {supportedOS}
</Typography>
{service.semver !== "" &&
<Typography variant="body1" component="p">
<b>Version:</b> {service.semver}
</Typography>
}
{service.wrap_these_payload_types.length === 0 ? null : (
<Typography variant="body1" component="p">
<b>Wrapped Payload Types:</b> {wrappedPayloads}
</Typography>
)}
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Description: </b>{service.note}
</Typography>
<InstalledServiceMetadataSummary
items={[
{label: "Author", value: service.author},
{label: "Version", value: service.semver, chip: true},
{label: "Supported OS", value: supportedOS},
{label: "Wraps", value: wrappedPayloadNames},
]}
description={service.note}
/>
</MythicTableCell>
<MythicTableCell>
<div className="mythic-table-row-actions">
@@ -474,10 +474,9 @@ A few examples are github.com/MythicAgents/bloodhound and github.com/MythicAgent
<TableRow>
<MythicTableCell style={{width: "4rem"}}>Delete</MythicTableCell>
<MythicTableCell style={{width: "90px"}}></MythicTableCell>
<MythicTableCell>Service</MythicTableCell>
<MythicTableCell style={{width: "4rem"}}>Type</MythicTableCell>
<MythicTableCell style={{width: "22%"}}>Service</MythicTableCell>
<MythicTableCell>Metadata</MythicTableCell>
<MythicTableCell style={{width: "12rem"}}>Actions</MythicTableCell>
<MythicTableCell style={{width: "13rem"}}>Actions</MythicTableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -542,8 +541,8 @@ You can extend this auth capability to support your own LDAP, SSO, or otherwise
<TableHead>
<TableRow>
<MythicTableCell style={{width: "3rem"}}></MythicTableCell>
<MythicTableCell style={{width: "30%"}}>Name</MythicTableCell>
<MythicTableCell style={{width: "5rem"}}>Manage</MythicTableCell>
<MythicTableCell style={{width: "24%"}}>Service</MythicTableCell>
<MythicTableCell>Metadata</MythicTableCell>
<MythicTableCell>Actions</MythicTableCell>
</TableRow>
</TableHead>
@@ -594,11 +593,9 @@ const ContainersTabCustomBrowsersPanel = (props) => {
<TableRow>
<MythicTableCell style={{width: "4rem"}}>Delete</MythicTableCell>
<MythicTableCell style={{width: "90px"}}></MythicTableCell>
<MythicTableCell>Service</MythicTableCell>
<MythicTableCell style={{width: "4rem"}}>Type</MythicTableCell>
<MythicTableCell style={{width: "22%"}}>Service</MythicTableCell>
<MythicTableCell>Metadata</MythicTableCell>
<MythicTableCell>Manage</MythicTableCell>
<MythicTableCell></MythicTableCell>
<MythicTableCell style={{width: "8rem"}}>Actions</MythicTableCell>
</TableRow>
</TableHead>
@@ -1,6 +1,5 @@
import React from 'react';
import AttachFileIcon from '@mui/icons-material/AttachFile';
import Typography from '@mui/material/Typography';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import { faLanguage } from '@fortawesome/free-solid-svg-icons';
import IconButton from '@mui/material/IconButton';
@@ -16,6 +15,10 @@ import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
import {InstalledServiceContainerStatus} from "./InstalledServiceStatus";
import {
InstalledServiceIdentity,
InstalledServiceMetadataSummary
} from "./InstalledServiceTableComponents";
const toggleDeleteStatus = gql`
mutation toggleC2ProfileDeleteStatus($translationcontainer_id: Int!, $deleted: Boolean!){
@@ -46,8 +49,9 @@ export function TranslationContainerRow({service, showDeleted}) {
if(service.deleted && !showDeleted){
return null;
}
const supportedAgents = (service.payloadtypes || []).filter(pt => !pt.deleted).map((pt) => pt.name);
return (
<>
<TableRow hover>
<MythicTableCell>
{service.deleted ? (
@@ -66,24 +70,22 @@ export function TranslationContainerRow({service, showDeleted}) {
<FontAwesomeIcon icon={faLanguage} style={{width: "80px", height: "80px"}} />
</MythicTableCell>
<MythicTableCell>
<div className="mythic-installed-service-identity">
<span className="mythic-installed-service-name">{service.name}</span>
<InstalledServiceContainerStatus isOnline={service.container_running} />
</div>
<InstalledServiceIdentity
name={service.name}
typeLabel="Translation"
deleted={service.deleted}
status={<InstalledServiceContainerStatus isOnline={service.container_running} />}
/>
</MythicTableCell>
<MythicTableCell>
Translation
</MythicTableCell>
<MythicTableCell>
<Typography variant="body1" component="p">
<b>Author:</b> {service.author}
</Typography>
<Typography variant="body1" component="p">
<b>Supported Agents:</b> {service.payloadtypes.filter(pt => !pt.deleted).map( (pt) => pt.name).join(", ")}
</Typography>
<Typography variant="body2" component="p" style={{whiteSpace: "pre-wrap"}}>
<b>Description: </b>{service.description}
</Typography>
<InstalledServiceMetadataSummary
items={[
{label: "Author", value: service.author},
{label: "Version", value: service.semver, chip: true},
{label: "Supported Agents", value: supportedAgents},
]}
description={service.description}
/>
</MythicTableCell>
<MythicTableCell>
<div className="mythic-table-row-actions">
@@ -107,13 +109,14 @@ export function TranslationContainerRow({service, showDeleted}) {
</MythicStyledTooltip>
</div>
</MythicTableCell>
</TableRow>
{openListFilesDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openListFilesDialog}
onClose={()=>{setOpenListFilesDialog(false);}}
innerDialog={<C2ProfileListFilesDialog container_name={service.name} {...service} onClose={()=>{setOpenListFilesDialog(false);}} />}
/>
}
</TableRow>
</>
);
}
+307
View File
@@ -10081,6 +10081,313 @@ tspan {
line-height: 1.25;
overflow-wrap: anywhere;
}
.mythic-installed-service-name-row {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
min-width: 0;
}
.mythic-installed-service-metadata-summary {
display: flex;
flex-direction: column;
gap: 0.55rem;
min-width: 0;
}
.mythic-installed-service-metadata-grid {
display: grid;
gap: 0.32rem 0.7rem;
grid-template-columns: 7rem minmax(0, 1fr);
min-width: 0;
}
.mythic-installed-service-metadata-item {
display: contents;
}
.mythic-installed-service-metadata-label,
.mythic-installed-service-detail-label {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.68rem;
font-weight: 750;
line-height: 1.2;
}
.mythic-installed-service-metadata-label {
align-self: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-installed-service-metadata-value,
.mythic-installed-service-detail-value {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.8rem;
font-weight: 500;
line-height: 1.25;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.mythic-installed-service-metadata-grid .mythic-installed-service-chip-list {
flex-wrap: nowrap;
overflow: hidden;
}
.mythic-installed-service-metadata-custom-value {
min-width: 0;
}
.mythic-installed-service-metadata-code {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.055) : alpha(props.theme.palette.common.black, 0.035)};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.primary};
display: inline-block;
font-family: ${(props) => props.theme.typography.fontFamilyMono || "monospace"};
font-size: 0.75rem;
line-height: 1.25;
max-width: 100%;
overflow: hidden;
padding: 0.12rem 0.32rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-installed-service-chip-list {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
min-width: 0;
}
.mythic-installed-service-chip {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.055) : alpha(props.theme.palette.common.black, 0.04)};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.primary};
display: inline-flex;
font-size: 0.72rem;
font-weight: 750;
line-height: 1.2;
max-width: 9rem;
min-width: 0;
overflow: hidden;
padding: 0.16rem 0.42rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-installed-service-chip-more {
color: ${(props) => props.theme.palette.text.secondary};
}
.mythic-installed-service-action-chip-list {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
min-width: 0;
}
.mythic-installed-service-action-chip {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.055) : alpha(props.theme.palette.common.black, 0.04)};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.primary};
cursor: pointer;
display: inline-flex;
font-size: 0.72rem;
font-weight: 700;
gap: 0.3rem;
line-height: 1.2;
max-width: 11rem;
min-height: 24px;
min-width: 0;
overflow: hidden;
padding: 0.12rem 0.38rem 0.12rem 0.48rem;
transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
}
.mythic-installed-service-action-chip > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-installed-service-action-chip .MuiSvgIcon-root {
color: ${(props) => props.theme.palette.text.secondary};
flex: 0 0 auto;
font-size: 0.92rem;
}
.mythic-installed-service-action-chip:hover {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.54 : 0.36)};
color: ${(props) => props.theme.palette.info.main};
}
.mythic-installed-service-action-chip:hover .MuiSvgIcon-root {
color: ${(props) => props.theme.palette.info.main};
}
.mythic-installed-service-action-chip:disabled {
color: ${(props) => props.theme.palette.text.disabled};
cursor: default;
opacity: 0.72;
}
.mythic-installed-service-action-chip:disabled:hover {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.055) : alpha(props.theme.palette.common.black, 0.04)};
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
color: ${(props) => props.theme.palette.text.disabled};
}
.mythic-installed-service-empty-value {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.78rem;
font-weight: 650;
}
.mythic-installed-service-description {
color: ${(props) => props.theme.palette.text.secondary};
min-width: 0;
}
.mythic-installed-service-description > span {
display: block;
font-size: 0.68rem;
font-weight: 750;
line-height: 1.2;
margin-bottom: 0.18rem;
}
.mythic-installed-service-description > p {
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
color: ${(props) => props.theme.palette.text.primary};
display: -webkit-box;
font-size: 0.8rem;
font-weight: 500;
line-height: 1.35;
margin: 0;
overflow: hidden;
white-space: normal;
}
.mythic-installed-service-browser-metadata {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-width: 0;
}
.mythic-installed-service-browser-author {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.82rem;
font-weight: 500;
line-height: 1.3;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-installed-service-browser-metrics {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.35rem 0.6rem;
min-width: 0;
}
.mythic-installed-service-browser-metric {
align-items: center;
display: inline-flex;
gap: 0.3rem;
min-width: 0;
}
.mythic-installed-service-browser-metric > span:first-child {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.68rem;
font-weight: 750;
line-height: 1.2;
}
.mythic-installed-service-browser-metric > strong {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.78rem;
font-weight: 500;
line-height: 1.2;
}
.mythic-installed-service-detail-row .mythic-installed-service-detail-cell {
border-bottom: 0 !important;
padding: 0 !important;
}
.mythic-installed-service-detail-panel {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.035) : alpha(props.theme.palette.common.black, 0.022)};
border-top: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
margin: 0;
padding: 0.85rem 1rem 1rem;
}
.mythic-installed-service-detail-section {
background-color: ${(props) => props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
min-width: 0;
overflow: hidden;
}
.mythic-installed-service-detail-section-header {
align-items: center;
background-color: ${(props) => props.theme.pageHeader?.main || props.theme.palette.background.contrast};
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary};
display: flex;
font-size: 0.76rem;
font-weight: 850;
justify-content: space-between;
letter-spacing: 0;
min-height: 2rem;
padding: 0.35rem 0.6rem;
}
.mythic-installed-service-detail-section-body {
padding: 0.65rem;
}
.mythic-installed-service-detail-list {
display: grid;
gap: 0.5rem 0.85rem;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
}
.mythic-installed-service-detail-list-item {
display: flex;
flex-direction: column;
gap: 0.22rem;
min-width: 0;
}
.mythic-installed-service-definition-list {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.mythic-installed-service-definition-row {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.035) : alpha(props.theme.palette.common.black, 0.022)};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
gap: 0.75rem;
justify-content: space-between;
min-width: 0;
padding: 0.5rem 0.6rem;
}
.mythic-installed-service-definition-main {
display: flex;
flex-direction: column;
gap: 0.15rem;
min-width: 0;
}
.mythic-installed-service-definition-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.82rem;
font-weight: 800;
line-height: 1.25;
overflow-wrap: anywhere;
}
.mythic-installed-service-definition-subtitle,
.mythic-installed-service-definition-description {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.75rem;
font-weight: 650;
line-height: 1.35;
overflow-wrap: anywhere;
}
.mythic-installed-service-definition-action {
align-items: center;
display: flex;
flex: 0 0 auto;
}
.mythic-service-status-summary {
align-items: flex-start;
display: flex;