mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
updated file browser for container files
This commit is contained in:
@@ -6,6 +6,7 @@ import 'ace-builds/src-noconflict/mode-json';
|
||||
import 'ace-builds/src-noconflict/theme-monokai';
|
||||
import 'ace-builds/src-noconflict/theme-xcode';
|
||||
import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext";
|
||||
import {b64DecodeUnicode} from "../Callbacks/ResponseDisplay";
|
||||
import {textExtensionTypesToSyntax} from "../Callbacks/ResponseDisplayMedia";
|
||||
import {MythicDialogButton, MythicDialogFooter} from "../../MythicComponents/MythicDialogLayout";
|
||||
import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
|
||||
@@ -34,17 +35,33 @@ const getInitialMode = (filename) => {
|
||||
}
|
||||
return "html";
|
||||
}
|
||||
const normalizeBase64 = (data) => {
|
||||
const value = data || "";
|
||||
const paddingLength = (4 - (value.length % 4)) % 4;
|
||||
return `${value}${"=".repeat(paddingLength)}`;
|
||||
}
|
||||
const encodeBase64Unicode = (data) => {
|
||||
const bytes = new TextEncoder().encode(data);
|
||||
let binary = "";
|
||||
bytes.forEach((byte) => {
|
||||
binary += String.fromCharCode(byte);
|
||||
});
|
||||
return btoa(binary);
|
||||
}
|
||||
export function C2ProfileConfigDialog(props) {
|
||||
const config = React.useRef("");
|
||||
const [configText, setConfigText] = React.useState("");
|
||||
const { loading, error } = useQuery(getProfileConfigQuery, {
|
||||
variables: {container_name: props.container_name, filename: props.filename},
|
||||
onCompleted: data => {
|
||||
let newConfig = "";
|
||||
if(data.containerDownloadFile.status === "error"){
|
||||
config.current = "Errored trying to read file from container\n" + data.containerDownloadFile.error;
|
||||
newConfig = "Errored trying to read file from container\n" + data.containerDownloadFile.error;
|
||||
}else{
|
||||
//console.log(data);
|
||||
config.current = atob(data.containerDownloadFile.data);
|
||||
newConfig = b64DecodeUnicode(normalizeBase64(data.containerDownloadFile.data));
|
||||
}
|
||||
config.current = newConfig;
|
||||
setConfigText(newConfig);
|
||||
},
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
@@ -72,10 +89,10 @@ export function C2ProfileConfigDialog(props) {
|
||||
);
|
||||
}
|
||||
const onConfigSubmit = () => {
|
||||
props.onConfigSubmit(btoa(config.current));
|
||||
props.onConfigSubmit(encodeBase64Unicode(config.current));
|
||||
props.onClose();
|
||||
}
|
||||
const setConfig = (newData) => {
|
||||
const onEditorChange = (newData) => {
|
||||
config.current = newData;
|
||||
}
|
||||
|
||||
@@ -85,9 +102,9 @@ export function C2ProfileConfigDialog(props) {
|
||||
<DialogContent dividers={true} style={{padding: 0}}>
|
||||
<div style={{height: "calc(80vh)"}}>
|
||||
<ResponseDisplayPlaintext
|
||||
onChangeContent={setConfig}
|
||||
onChangeContent={onEditorChange}
|
||||
initial_mode={initialMode.current}
|
||||
plaintext={config.current}
|
||||
plaintext={configText}
|
||||
expand={true} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
+352
-148
@@ -1,22 +1,32 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Breadcrumbs from '@mui/material/Breadcrumbs';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import {useQuery, gql, useMutation, useLazyQuery} from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import Link from '@mui/material/Link';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {useQuery, gql, useMutation, useLazyQuery} from '@apollo/client';
|
||||
import { snackActions } from '../../utilities/Snackbar';
|
||||
import {C2ProfileConfigDialog} from './C2ProfileConfigDialog';
|
||||
import { MythicDialog } from '../../MythicComponents/MythicDialog';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import HomeIcon from '@mui/icons-material/Home';
|
||||
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import {MythicConfirmDialog} from '../../MythicComponents/MythicConfirmDialog';
|
||||
import {downloadFileFromMemory} from '../../utilities/Clipboard';
|
||||
import {MythicDialogBody, MythicDialogButton, MythicDialogFooter, MythicDialogSection} from "../../MythicComponents/MythicDialogLayout";
|
||||
@@ -24,11 +34,12 @@ import {MythicErrorState, MythicLoadingState} from "../../MythicComponents/Mythi
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
|
||||
export const containerListFilesQuery = gql`
|
||||
query listC2ProfileFilesQuery($container_name: String!){
|
||||
containerListFiles(container_name: $container_name){
|
||||
query listC2ProfileFilesQuery($container_name: String!, $path: String){
|
||||
containerListFiles(container_name: $container_name, path: $path){
|
||||
status
|
||||
error
|
||||
files
|
||||
folders
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -60,160 +71,353 @@ query getProfileConfigOutput($container_name: String!, $filename: String!) {
|
||||
}
|
||||
`;
|
||||
|
||||
const normalizePath = (path) => {
|
||||
if (!path) {
|
||||
return "";
|
||||
}
|
||||
return path.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
|
||||
};
|
||||
|
||||
const joinBrowserPath = (basePath, name) => {
|
||||
const base = normalizePath(basePath);
|
||||
const child = normalizePath(name);
|
||||
return base ? `${base}/${child}` : child;
|
||||
};
|
||||
|
||||
const parentPath = (path) => {
|
||||
const parts = normalizePath(path).split("/").filter(Boolean);
|
||||
parts.pop();
|
||||
return parts.join("/");
|
||||
};
|
||||
|
||||
const sortNames = (names) => [...new Set(names || [])].sort((a, b) => a.localeCompare(b, undefined, {sensitivity: "base"}));
|
||||
|
||||
const decodeBase64Binary = (data) => {
|
||||
const value = data || "";
|
||||
const paddingLength = (4 - (value.length % 4)) % 4;
|
||||
return atob(`${value}${"=".repeat(paddingLength)}`);
|
||||
};
|
||||
|
||||
const fileBrowserContentSx = {
|
||||
height: {xs: "60vh", md: "64vh"},
|
||||
minHeight: {sm: "28rem"},
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
const fileBrowserBodySx = {
|
||||
flex: "1 1 auto",
|
||||
minHeight: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
};
|
||||
|
||||
const fileBrowserSectionSx = {
|
||||
flex: "1 1 auto",
|
||||
minHeight: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
};
|
||||
|
||||
const fileBrowserTableSx = {
|
||||
flex: "1 1 auto",
|
||||
minHeight: 0,
|
||||
overflow: "auto",
|
||||
};
|
||||
|
||||
const BrowserPath = ({path, onChangePath}) => {
|
||||
const parts = normalizePath(path).split("/").filter(Boolean);
|
||||
return (
|
||||
<Box sx={{display: "flex", alignItems: "center", gap: 1, minWidth: 0, flex: 1}}>
|
||||
<Breadcrumbs aria-label="container file path" maxItems={5} sx={{minWidth: 0}}>
|
||||
<Link
|
||||
component="button"
|
||||
underline="hover"
|
||||
color={parts.length === 0 ? "text.primary" : "inherit"}
|
||||
onClick={() => onChangePath("")}
|
||||
sx={{display: "inline-flex", alignItems: "center", gap: 0.5, font: "inherit"}}
|
||||
>
|
||||
<HomeIcon fontSize="small" />
|
||||
root
|
||||
</Link>
|
||||
{parts.map((part, index) => {
|
||||
const targetPath = parts.slice(0, index + 1).join("/");
|
||||
const isLast = index === parts.length - 1;
|
||||
if (isLast) {
|
||||
return (
|
||||
<Typography key={targetPath} color="text.primary" sx={{display: "inline-flex", alignItems: "center", minWidth: 0}}>
|
||||
{part}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
component="button"
|
||||
key={targetPath}
|
||||
underline="hover"
|
||||
color="inherit"
|
||||
onClick={() => onChangePath(targetPath)}
|
||||
sx={{font: "inherit"}}
|
||||
>
|
||||
{part}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Breadcrumbs>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export function C2ProfileListFilesDialog(props) {
|
||||
|
||||
const { loading, error, data } = useQuery(containerListFilesQuery, {
|
||||
variables: {container_name: props.container_name},
|
||||
onCompleted: data => {
|
||||
|
||||
},
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
const [configSubmit] = useMutation(containerWriteFileMutation, {
|
||||
update: (cache, {data}) => {
|
||||
|
||||
},
|
||||
onError: data => {
|
||||
console.error(data);
|
||||
},
|
||||
onCompleted: data => {
|
||||
//console.log(data);
|
||||
if(data.containerWriteFile.status === "success"){
|
||||
snackActions.success("Updated file");
|
||||
}else{
|
||||
snackActions.error("Error updating: " + data.containerWriteFile.error );
|
||||
}
|
||||
const [currentPath, setCurrentPath] = React.useState("");
|
||||
const normalizedCurrentPath = normalizePath(currentPath);
|
||||
const { loading, error, data, refetch } = useQuery(containerListFilesQuery, {
|
||||
variables: {container_name: props.container_name, path: normalizedCurrentPath},
|
||||
fetchPolicy: "network-only",
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
const [configSubmit] = useMutation(containerWriteFileMutation, {
|
||||
onError: data => {
|
||||
console.error(data);
|
||||
},
|
||||
onCompleted: data => {
|
||||
if(data.containerWriteFile.status === "success"){
|
||||
snackActions.success("Updated file");
|
||||
refetch();
|
||||
}else{
|
||||
snackActions.error("Error updating: " + data.containerWriteFile.error );
|
||||
}
|
||||
}
|
||||
});
|
||||
const onFileChange = (evt) => {
|
||||
Array.from(evt.target.files).forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const contents = e.target.result;
|
||||
configSubmit({
|
||||
variables: {
|
||||
container_name: props.container_name,
|
||||
file_path: joinBrowserPath(normalizedCurrentPath, file.name),
|
||||
data: btoa(contents),
|
||||
}
|
||||
});
|
||||
};
|
||||
reader.readAsBinaryString(file);
|
||||
});
|
||||
const onFileChange = (evt) => {
|
||||
for(let i = 0; i < evt.target.files.length; i++){
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const contents = e.target.result;
|
||||
configSubmit({variables: {container_name: props.container_name, file_path: evt.target.files[i].name, data: btoa(contents)}});
|
||||
}
|
||||
reader.readAsBinaryString(evt.target.files[i]);
|
||||
}
|
||||
evt.target.value = "";
|
||||
};
|
||||
const onChangePath = React.useCallback((path) => {
|
||||
setCurrentPath(normalizePath(path));
|
||||
}, []);
|
||||
const onRefresh = React.useCallback(() => {
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
const listResponse = data?.containerListFiles;
|
||||
const folders = React.useMemo(() => sortNames(listResponse?.folders), [listResponse?.folders]);
|
||||
const files = React.useMemo(() => {
|
||||
const folderSet = new Set(folders);
|
||||
return sortNames(listResponse?.files).filter((name) => !folderSet.has(name));
|
||||
}, [folders, listResponse?.files]);
|
||||
const isInitialLoading = loading && !data;
|
||||
|
||||
if (isInitialLoading) {
|
||||
return (
|
||||
<>
|
||||
<DialogTitle id="form-dialog-title">{props.container_name}'s Current Files</DialogTitle>
|
||||
<DialogContent dividers={true} sx={fileBrowserContentSx}>
|
||||
<Box sx={{flex: "1 1 auto", display: "flex", alignItems: "center"}}>
|
||||
<MythicLoadingState title="Loading container files" description="Fetching files from the service container." minHeight={180} />
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<DialogTitle id="form-dialog-title">{props.container_name}'s Current Files</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<MythicLoadingState title="Loading container files" description="Fetching files from the service container." minHeight={180} />
|
||||
</DialogContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<>
|
||||
<DialogTitle id="form-dialog-title">{props.container_name}'s Current Files</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<MythicErrorState title="Unable to load container files" description={error.message} minHeight={180} />
|
||||
</DialogContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<>
|
||||
<DialogTitle id="form-dialog-title">{props.container_name}'s Current Files</DialogTitle>
|
||||
<DialogContent dividers={true} sx={fileBrowserContentSx}>
|
||||
<Box sx={{flex: "1 1 auto", display: "flex", alignItems: "center"}}>
|
||||
<MythicErrorState title="Unable to load container files" description={error.message} minHeight={180} />
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
<div className="mythic-dialog-title-row">
|
||||
<span>{props.container_name}'s Current Files</span>
|
||||
<Button
|
||||
className="mythic-dialog-title-action mythic-dialog-button-success"
|
||||
component="label"
|
||||
size="small"
|
||||
startIcon={<FileUploadIcon fontSize="small" />}
|
||||
variant="outlined"
|
||||
>
|
||||
Upload File
|
||||
<input onChange={onFileChange} type="file" multiple hidden />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<MythicDialogBody compact>
|
||||
<MythicDialogSection title="Container Files" description="Download, edit, upload, or remove files from this installed service.">
|
||||
<TableContainer className="mythicElement">
|
||||
<Table stickyHeader size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableHead>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
<div className="mythic-dialog-title-row">
|
||||
<span>{props.container_name}'s Current Files</span>
|
||||
<Button
|
||||
className="mythic-dialog-title-action mythic-dialog-button-success"
|
||||
component="label"
|
||||
size="small"
|
||||
startIcon={<FileUploadIcon fontSize="small" />}
|
||||
variant="outlined"
|
||||
>
|
||||
Upload File
|
||||
<input onChange={onFileChange} type="file" multiple hidden />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers={true} sx={fileBrowserContentSx}>
|
||||
<MythicDialogBody compact sx={fileBrowserBodySx}>
|
||||
<MythicDialogSection
|
||||
title="Container Files"
|
||||
sx={fileBrowserSectionSx}
|
||||
actions={
|
||||
<Box sx={{display: "flex", alignItems: "center", gap: 0.5}}>
|
||||
<MythicStyledTooltip title="Parent folder">
|
||||
<span>
|
||||
<IconButton
|
||||
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
|
||||
disabled={normalizedCurrentPath === ""}
|
||||
onClick={() => onChangePath(parentPath(normalizedCurrentPath))}
|
||||
size="small"
|
||||
>
|
||||
<ArrowUpwardIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title="Refresh folder">
|
||||
<IconButton
|
||||
className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info"
|
||||
onClick={onRefresh}
|
||||
size="small"
|
||||
>
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box sx={{display: "flex", alignItems: "center", gap: 1, minWidth: 0, mb: 1}}>
|
||||
<BrowserPath path={normalizedCurrentPath} onChangePath={onChangePath} />
|
||||
</Box>
|
||||
{listResponse?.status === "error" ? (
|
||||
<Box sx={{flex: "1 1 auto", minHeight: 0, display: "flex", alignItems: "center"}}>
|
||||
<MythicErrorState title="Unable to load folder" description={listResponse.error} minHeight={180} />
|
||||
</Box>
|
||||
) : (
|
||||
<TableContainer className="mythicElement" sx={fileBrowserTableSx}>
|
||||
<Table stickyHeader size="small" style={{maxWidth: "100%"}}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{width: "4rem"}}>Delete</TableCell>
|
||||
<TableCell style={{width: "4rem"}}>Download</TableCell>
|
||||
<TableCell style={{width: "4rem"}}>Edit</TableCell>
|
||||
<TableCell>File</TableCell>
|
||||
<TableCell style={{width: "3rem"}} />
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell style={{width: "6rem"}}>Type</TableCell>
|
||||
<TableCell align="right" style={{width: "9rem"}}>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.containerListFiles.files && data.containerListFiles.files.map( f => (
|
||||
<C2ProfileListFilesDialogTableRow key={"file" + f} id={props.id} container_name={props.container_name} filename={f} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</MythicDialogSection>
|
||||
</MythicDialogBody>
|
||||
</DialogContent>
|
||||
<MythicDialogFooter>
|
||||
<MythicDialogButton onClick={props.onClose}>
|
||||
Close
|
||||
</MythicDialogButton>
|
||||
</MythicDialogFooter>
|
||||
</React.Fragment>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{folders.map((folder) => (
|
||||
<ContainerFolderRow
|
||||
key={`folder-${normalizedCurrentPath}-${folder}`}
|
||||
folder={folder}
|
||||
onOpen={() => onChangePath(joinBrowserPath(normalizedCurrentPath, folder))}
|
||||
/>
|
||||
))}
|
||||
{files.map((file) => (
|
||||
<C2ProfileListFilesDialogTableRow
|
||||
key={`file-${normalizedCurrentPath}-${file}`}
|
||||
id={props.id}
|
||||
container_name={props.container_name}
|
||||
displayName={file}
|
||||
filename={joinBrowserPath(normalizedCurrentPath, file)}
|
||||
onRefresh={onRefresh}
|
||||
/>
|
||||
))}
|
||||
{folders.length === 0 && files.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4}>
|
||||
<Typography color="text.secondary" sx={{py: 3, textAlign: "center"}}>
|
||||
Empty folder
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</MythicDialogSection>
|
||||
</MythicDialogBody>
|
||||
</DialogContent>
|
||||
<MythicDialogFooter>
|
||||
<MythicDialogButton onClick={props.onClose}>
|
||||
Close
|
||||
</MythicDialogButton>
|
||||
</MythicDialogFooter>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const C2ProfileListFilesDialogTableRow = ({container_name, id, filename}) => {
|
||||
const ContainerFolderRow = ({folder, onOpen}) => {
|
||||
return (
|
||||
<TableRow hover onDoubleClick={onOpen} sx={{cursor: "pointer"}}>
|
||||
<TableCell>
|
||||
<FolderIcon color="primary" fontSize="small" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
endIcon={<KeyboardArrowRightIcon fontSize="small" />}
|
||||
onClick={onOpen}
|
||||
size="small"
|
||||
sx={{justifyContent: "flex-start", minWidth: 0, px: 0.5, textTransform: "none"}}
|
||||
variant="text"
|
||||
>
|
||||
{folder}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>Folder</TableCell>
|
||||
<TableCell align="right" />
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
const C2ProfileListFilesDialogTableRow = ({container_name, id, filename, displayName, onRefresh}) => {
|
||||
const [openProfileConfigDialog, setOpenProfileConfigDialog] = React.useState(false);
|
||||
const [openDelete, setOpenDeleteDialog] = React.useState(false);
|
||||
const [configSubmit] = useMutation(containerWriteFileMutation, {
|
||||
update: (cache, {data}) => {
|
||||
|
||||
},
|
||||
onError: data => {
|
||||
console.error(data);
|
||||
console.error(data);
|
||||
},
|
||||
onCompleted: data => {
|
||||
//console.log(data);
|
||||
if(data.containerWriteFile.status === "success"){
|
||||
snackActions.success("Updated file");
|
||||
}else{
|
||||
snackActions.error("Error updating: " + data.containerWriteFile.error );
|
||||
}
|
||||
if(data.containerWriteFile.status === "success"){
|
||||
snackActions.success("Updated file");
|
||||
onRefresh();
|
||||
}else{
|
||||
snackActions.error("Error updating: " + data.containerWriteFile.error );
|
||||
}
|
||||
}
|
||||
});
|
||||
const onConfigSubmit = (content) => {
|
||||
//console.log(content)
|
||||
configSubmit({variables: {container_name: container_name, file_path: filename, data: content}});
|
||||
}
|
||||
};
|
||||
const [removeFile] = useMutation(containerRemoveFileMutation, {
|
||||
update: (cache, {data}) => {
|
||||
|
||||
},
|
||||
onError: data => {
|
||||
console.error(data);
|
||||
console.error(data);
|
||||
},
|
||||
onCompleted: data => {
|
||||
//console.log(data);
|
||||
if(data.containerRemoveFile.status === "success"){
|
||||
snackActions.success("Removed file");
|
||||
}else{
|
||||
snackActions.error("Error removing: " + data.containerRemoveFile.error );
|
||||
}
|
||||
if(data.containerRemoveFile.status === "success"){
|
||||
snackActions.success("Removed file");
|
||||
onRefresh();
|
||||
}else{
|
||||
snackActions.error("Error removing: " + data.containerRemoveFile.error );
|
||||
}
|
||||
}
|
||||
});
|
||||
const onAcceptDelete = () => {
|
||||
removeFile({variables: {container_name: container_name, filename: filename}})
|
||||
}
|
||||
removeFile({variables: {container_name: container_name, filename: filename}});
|
||||
};
|
||||
const [getContainerFile] = useLazyQuery(containerDownloadFileQuery, {
|
||||
onCompleted: data => {
|
||||
if(data.containerDownloadFile.status === "success"){
|
||||
downloadFileFromMemory(atob(data.containerDownloadFile.data), filename);
|
||||
downloadFileFromMemory(decodeBase64Binary(data.containerDownloadFile.data), displayName || filename);
|
||||
} else {
|
||||
snackActions.error(data.containerDownloadFile.error);
|
||||
}
|
||||
@@ -222,48 +426,48 @@ const C2ProfileListFilesDialogTableRow = ({container_name, id, filename}) => {
|
||||
console.log(error);
|
||||
snackActions.error(error.message);
|
||||
}
|
||||
})
|
||||
});
|
||||
const downloadFile = () => {
|
||||
getContainerFile({variables: {container_name: container_name, filename: filename}})
|
||||
}
|
||||
getContainerFile({variables: {container_name: container_name, filename: filename}});
|
||||
};
|
||||
return (
|
||||
<React.Fragment>
|
||||
<TableRow hover>
|
||||
<TableCell>
|
||||
<InsertDriveFileIcon color="action" fontSize="small" />
|
||||
</TableCell>
|
||||
<TableCell>{displayName || filename}</TableCell>
|
||||
<TableCell>File</TableCell>
|
||||
<TableCell align="right">
|
||||
<MythicStyledTooltip title="Remove file">
|
||||
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-danger" size="small" onClick={()=>{setOpenDeleteDialog(true);}} >
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTooltip title="Download file">
|
||||
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-success" size="small" onClick={downloadFile}>
|
||||
<DownloadIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MythicStyledTooltip title="Edit file">
|
||||
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={() => {setOpenProfileConfigDialog(true);}}>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</TableCell>
|
||||
<TableCell>{filename}</TableCell>
|
||||
</TableRow>
|
||||
{openProfileConfigDialog &&
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openProfileConfigDialog}
|
||||
onClose={()=>{setOpenProfileConfigDialog(false);}}
|
||||
<MythicDialog fullWidth={true} maxWidth="lg" open={openProfileConfigDialog}
|
||||
onClose={()=>{setOpenProfileConfigDialog(false);}}
|
||||
innerDialog={<C2ProfileConfigDialog filename={filename} onConfigSubmit={onConfigSubmit} container_name={container_name} onClose={()=>{setOpenProfileConfigDialog(false);}} profile_id={id} />}
|
||||
/>
|
||||
}
|
||||
{openDelete &&
|
||||
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete}
|
||||
open={openDelete}
|
||||
acceptText={"Remove"}
|
||||
acceptColor={"error"} />
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
{openDelete &&
|
||||
<MythicConfirmDialog onClose={() => {setOpenDeleteDialog(false);}} onSubmit={onAcceptDelete}
|
||||
open={openDelete}
|
||||
acceptText={"Remove"}
|
||||
acceptColor={"error"} />
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -73,6 +73,7 @@ type Query {
|
||||
type Query {
|
||||
containerListFiles(
|
||||
container_name: String!
|
||||
path: String
|
||||
): containerListFilesOutput
|
||||
}
|
||||
|
||||
@@ -1219,6 +1220,7 @@ type containerListFilesOutput {
|
||||
status: String!
|
||||
error: String
|
||||
files: [String]
|
||||
folders: [String]
|
||||
}
|
||||
|
||||
type containerUploadFileOutput {
|
||||
@@ -1324,4 +1326,3 @@ type custombrowserExportFunctionOutput {
|
||||
status: String!
|
||||
error: String
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
|
||||
type ContainerRPCListFileMessage struct {
|
||||
Name string `json:"container_name"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
type ContianerRPCListFileMessageResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error"`
|
||||
Files []string `json:"files"`
|
||||
Folders []string `json:"folders"`
|
||||
}
|
||||
|
||||
func (r *rabbitMQConnection) SendContainerRPCListFile(getFile ContainerRPCListFileMessage) (*ContianerRPCListFileMessageResponse, error) {
|
||||
|
||||
@@ -57,7 +57,7 @@ func ContainerDownloadFileWebhook(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
base64Contents := base64.RawStdEncoding.EncodeToString(c2ProfileResponse.Message)
|
||||
base64Contents := base64.StdEncoding.EncodeToString(c2ProfileResponse.Message)
|
||||
c.JSON(http.StatusOK, GetContainerFileResponse{
|
||||
Status: "success",
|
||||
Filename: input.Input.Filename,
|
||||
|
||||
@@ -14,12 +14,14 @@ type GetContainerFileListInput struct {
|
||||
|
||||
type GetContainerFileList struct {
|
||||
ContainerName string `json:"container_name" binding:"required"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type GetContainerFileListResponse struct {
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
Files []string `json:"files"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
Files []string `json:"files"`
|
||||
Folders []string `json:"folders"`
|
||||
}
|
||||
|
||||
func ContainerListFilesWebhook(c *gin.Context) {
|
||||
@@ -34,6 +36,7 @@ func ContainerListFilesWebhook(c *gin.Context) {
|
||||
}
|
||||
c2ProfileResponse, err := rabbitmq.RabbitMQConnection.SendContainerRPCListFile(rabbitmq.ContainerRPCListFileMessage{
|
||||
Name: input.Input.ContainerName,
|
||||
Path: input.Input.Path,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, GetContainerFileListResponse{
|
||||
@@ -50,7 +53,11 @@ func ContainerListFilesWebhook(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, GetContainerFileListResponse{Status: "success", Files: c2ProfileResponse.Files})
|
||||
c.JSON(http.StatusOK, GetContainerFileListResponse{
|
||||
Status: "success",
|
||||
Files: c2ProfileResponse.Files,
|
||||
Folders: c2ProfileResponse.Folders,
|
||||
})
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user