mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
updated installed services table
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
|
||||
);
|
||||
|
||||
+153
-170
@@ -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>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
+190
@@ -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>
|
||||
|
||||
+22
-19
@@ -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>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user