finalizing dynamic parameters and ui updates for building

This commit is contained in:
its-a-feature
2025-10-07 09:58:48 -05:00
parent fae956bec4
commit 4896194b0c
151 changed files with 2374 additions and 7421 deletions
+9 -9
View File
@@ -120,7 +120,7 @@ jobs:
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: postgres-docker
file: postgres-docker/.docker/Dockerfile
file: postgres-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_POSTGRES_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_POSTGRES_IMAGE_NAME }}:latest
@@ -162,7 +162,7 @@ jobs:
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: rabbitmq-docker
file: rabbitmq-docker/.docker/Dockerfile
file: rabbitmq-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_RABBITMQ_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_RABBITMQ_IMAGE_NAME }}:latest
@@ -204,7 +204,7 @@ jobs:
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: documentation-docker
file: documentation-docker/.docker/Dockerfile
file: documentation-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_DOCUMENTATION_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_DOCUMENTATION_IMAGE_NAME }}:latest
@@ -248,7 +248,7 @@ jobs:
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: jupyter-docker
file: jupyter-docker/.docker/Dockerfile
file: jupyter-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_JUPYTER_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_JUPYTER_IMAGE_NAME }}:latest
@@ -292,7 +292,7 @@ jobs:
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: hasura-docker
file: hasura-docker/.docker/Dockerfile
file: hasura-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_GRAPHQL_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_GRAPHQL_IMAGE_NAME }}:latest
@@ -336,7 +336,7 @@ jobs:
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: nginx-docker
file: nginx-docker/.docker/Dockerfile
file: nginx-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_NGINX_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_NGINX_IMAGE_NAME }}:latest
@@ -382,7 +382,7 @@ jobs:
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: mythic-react-docker
file: mythic-react-docker/.docker/Dockerfile
file: mythic-react-docker/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_REACT_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_REACT_IMAGE_NAME }}:latest
@@ -440,7 +440,7 @@ jobs:
uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images
with:
context: Mythic_CLI
file: Mythic_CLI/.docker/Dockerfile
file: Mythic_CLI/Dockerfile
tags: |
${{ env.REGISTRY }}/${{ env.MYTHIC_CLI_IMAGE_NAME }}:${{ env.VERSION }}
${{ env.REGISTRY }}/${{ env.MYTHIC_CLI_IMAGE_NAME }}:latest
@@ -552,7 +552,7 @@ jobs:
uses: EndBug/add-and-commit@v9 # ref: https://github.com/marketplace/actions/add-commit
with:
# Only add the Dockerfile changes. Nothing else should have been modified
add: "['mythic-docker/Dockerfile', 'Mythic_CLI/Dockerfile', 'Mythic_CLI/Makefile', 'Mythic_CLI/src/cmd/config/constants.go', 'postgres-docker/Dockerfile', 'rabbitmq-docker/Dockerfile', 'documentation-docker/Dockerfile', 'jupyter-docker/Dockerfile', 'hasura-docker/Dockerfile', 'nginx-docker/Dockerfile', 'mythic-react-docker/Dockerfile']"
add: "['Mythic_CLI/Makefile', 'Mythic_CLI/src/cmd/config/constants.go']"
# Use the Github actions bot for the commit author
default_author: github_actions
committer_email: github-actions[bot]@users.noreply.github.com
+23 -2
View File
@@ -4,13 +4,34 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.3.2.0] - 2025
## [3.4.0] - 2025
### Changed
- Added "selected_os" array option to some eventing triggers to additionally limit not just on payload type but OS as well
- this is the "os" you select when you build a payload
-
- Modified the UI to allow creating new eventing workflows based on existing ones with a single button
- Added group_name, supported_os, and hide_conditions to build parameters for more dynamic build experiences
- Added supports_multiple_c2_in_build, supports_multiple_c2_instances, c2_parameter_deviations to payload types
- This allows payload types to slightly tweak c2 profile configurations they support and indicating how many c2s are allowed per build
- c2 parameter deviations allow indicating parameters are not supported at all, that they have a different set of choices, and that the default value can be something else
- Added semver option for payload types to more accurately convey their current version outside of their description
- Updated the payload build workflow in the UI
- Added an option for payload types to hook into the `help` function and provide their own as part of their payload type definition
- Added a notification to callback tabs if there's new task changes while you're not on the tab
- Updated delegate tasks for the same target callback to be grouped together into a single message instead of a separate message per task
- Fixed a bug with MythicRPCCallbackSearch where all the timestamps would be 0
- Added support for C2 Profiles to include their own agent icons for the UI
- Added support for C2 Profiles to report their own SemVer as well
- Added support for TranslationContainers to report back their own SemVer
- Added support for all consuming containers (eventing, auth, logging, webhook) to report back their own SemVer
- Fixed bug in reporting where tasks weren't in order
- Updated the CredentialCreateWebhook to support empty credentials
- Added callback context menu option for triggering eventing workflows
- Also added option to bulk issue eventing workflows from callbacks
- Edited workflows so that all can be triggered manually
- Edited the server so manul execution can still provide arbitrary extra data, not just keyword execution
- Updated operationeventlog to have a dedicated `warning` field that's separate from the level
## [3.3.1-rc94] - 2025-09-05
+3 -3
View File
@@ -6778,9 +6778,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001715",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
"integrity": "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw==",
"version": "1.0.30001748",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz",
"integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==",
"funding": [
{
"type": "opencollective",
+1 -1
View File
@@ -36,7 +36,7 @@ export const operatorSettingDefaults = {
autoTaskLsOnEmptyDirectories: false,
hideBrowserTasking: false,
hideTaskingContext: false,
taskingContextFields: ["impersonation_context", "cwd", "extra_info"],
taskingContextFields: ["impersonation_context", "cwd"],
["experiment-responseStreamLimit"]: 50,
palette: {
primary: {
+113 -111
View File
@@ -39,6 +39,7 @@ import {InviteForm} from "./pages/Login/InviteForm";
import {snackActions} from "./utilities/Snackbar";
import {TopAppBarVertical} from "./TopAppBarVertical";
export const MeContext = createContext({});
export const userSettingsQuery = gql`
query getUserSettings {
getOperatorPreferences {
@@ -132,7 +133,7 @@ export function App(props) {
topAppBarColor: themeMode === 'dark' ? preferences?.palette?.navBarColor?.dark || operatorSettingDefaults.palette.navBarColor.dark :
preferences?.palette?.navBarColor?.light || operatorSettingDefaults.palette.navBarColor.light,
typography: {
fontSize: preferences?.fontSize,
fontSize: 12, //preferences?.fontSize,
fontFamily: preferences?.fontFamily
},
taskPromptTextColor: themeMode === 'dark' ? preferences?.palette?.taskPromptTextColor?.dark || operatorSettingDefaults.palette.taskPromptTextColor.dark :
@@ -223,7 +224,7 @@ export function App(props) {
topAppBarColor: themeMode === 'dark' ? operatorSettingDefaults.palette.navBarColor.dark :
operatorSettingDefaults.palette.navBarColor.light,
typography: {
fontSize: operatorSettingDefaults?.fontSize,
fontSize: 12,//operatorSettingDefaults?.fontSize,
fontFamily: operatorSettingDefaults?.fontFamily
},
})
@@ -295,122 +296,123 @@ export function App(props) {
<ThemeProvider theme={theme}>
<GlobalStyles theme={theme} />
<CssBaseline />
<Tooltip id={"my-tooltip"} style={{zIndex: 100000, wordBreak: "break-word", maxWidth: "80%", whiteSpace: "pre-wrap"}}/>
<ToastContainer limit={2} autoClose={3000}
theme={themeMode}
hideProgressBar={true}
newestOnTop={true}
stacked={false}
style={{maxWidth: "100%", minWidth: "40%", display: "flex", flexWrap: "wrap",
wordBreak: "break-all", flexDirection: "column", justifyContent: "center",}}
pauseOnFocusLoss={false} />
<div style={{ maxHeight: '100%', height: '100%', display: 'flex', flexDirection: 'row', maxWidth: "100%", width:"100%",
...background}}>
<MeContext.Provider value={me}>
<Tooltip id={"my-tooltip"} style={{zIndex: 100000, wordBreak: "break-word", maxWidth: "80%", whiteSpace: "pre-wrap"}}/>
<ToastContainer limit={2} autoClose={3000}
theme={themeMode}
hideProgressBar={true}
newestOnTop={true}
stacked={false}
style={{maxWidth: "100%", minWidth: "40%", display: "flex", flexWrap: "wrap",
wordBreak: "break-all", flexDirection: "column", justifyContent: "center",}}
pauseOnFocusLoss={false} />
<div style={{ maxHeight: '100%', height: '100%', display: 'flex', flexDirection: 'row', maxWidth: "100%", width:"100%",
...background}}>
{openRefreshDialog &&
<MythicDialog fullWidth={true} maxWidth="sm" open={openRefreshDialog}
onClose={()=>{setOpenRefreshDialog(false);}}
innerDialog={<RefreshTokenDialog
onClose={()=>{setOpenRefreshDialog(false);}} />}
/>
}
{me.loggedIn && me.user !== undefined && me.user !== null ? (
<TopAppBarVertical me={me} theme={themeMode} toggleTheme={themeToggler} />
) : null}
<div style={{
maxHeight: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
width: "100%"
}}>
{me.loggedIn && me?.user?.current_operation_banner_text !== "" &&
<Typography style={{
backgroundColor: me?.user?.current_operation_banner_color,
width: "100%",
textAlign: "center",
fontWeight: "600",
color: "white",
border: `1px solid ${theme.topAppBarColor || "grey"}`
}}>
{me?.user?.current_operation_banner_text}
</Typography>
}
{me.loggedIn && me?.badConnection
&&
<Typography style={{
backgroundColor: theme.palette.error.main,
width: "100%",
textAlign: "center",
fontWeight: "600",
color: "white",
border: `1px solid ${theme.topAppBarColor || "grey"}`
}}>
{"Can't connect to Mythic. Please check connection and refresh"}
</Typography>
{openRefreshDialog &&
<MythicDialog fullWidth={true} maxWidth="sm" open={openRefreshDialog}
onClose={()=>{setOpenRefreshDialog(false);}}
innerDialog={<RefreshTokenDialog
onClose={()=>{setOpenRefreshDialog(false);}} />}
/>
}
{me.loggedIn && me.user !== undefined && me.user !== null ? (
<TopAppBarVertical me={me} theme={themeMode} toggleTheme={themeToggler} />
) : null}
<div style={{
margin: '0px 0px 0px 0px',
flexGrow: 1,
overflowY: "hidden",
maxHeight: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
height: "100%",
width: "100%"
}}>
<Routes>
<Route path='/new/login' element={<LoginForm me={me}/>}/>
<Route path='/new/invite' element={<InviteForm me={me}/>}/>
<Route path='/' element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>}/>
<Route exact path='/new'
element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>}/>
<Route exact path='/new/settings'
element={<LoggedInRoute me={me}><Settings me={me}/></LoggedInRoute>}/>
<Route exact path='/new/payloadtypes'
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/eventfeed'
element={<LoggedInRoute me={me}><EventFeed me={me}/></LoggedInRoute>}/>
<Route exact path='/new/createpayload'
element={<LoggedInRoute me={me}><CreatePayload me={me}/></LoggedInRoute>}/>
<Route exact path='/new/createwrapper'
element={<LoggedInRoute me={me}><CreatePayloadWrapper
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/payloads'
element={<LoggedInRoute me={me}><Payloads me={me}/></LoggedInRoute>}/>
<Route exact path='/new/c2profiles'
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/services/'
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/callbacks'
element={<LoggedInRoute me={me}><Callbacks me={me}/></LoggedInRoute>}/>
<Route path='/new/search'
element={<LoggedInRoute me={me}><Search history={props.history}
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/browserscripts'
element={<LoggedInRoute me={me}><BrowserScripts me={me}/></LoggedInRoute>}/>
<Route exact path='/new/task/:taskId'
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
<Route exact path='/new/tasks/by_range'
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
<Route exact path='/new/operations'
element={<LoggedInRoute me={me}><Operations me={me}/></LoggedInRoute>}/>
<Route exact path='/new/callbacks/:callbackDisplayId'
element={<LoggedInRoute me={me}><ExpandedCallback
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/reporting'
element={<LoggedInRoute me={me}><Reporting me={me}/></LoggedInRoute>}/>
<Route exact path='/new/mitre'
element={<LoggedInRoute me={me}><MitreAttack me={me}/></LoggedInRoute>}/>
<Route exact path='/new/tagtypes'
element={<LoggedInRoute me={me}><Tags me={me}/></LoggedInRoute>}/>
<Route exact path='/new/eventing'
element={<LoggedInRoute me={me}><Eventing me={me}/></LoggedInRoute>}/>
</Routes>
{me.loggedIn && me?.user?.current_operation_banner_text !== "" &&
<Typography style={{
backgroundColor: me?.user?.current_operation_banner_color,
width: "100%",
textAlign: "center",
fontWeight: "600",
color: "white",
border: `1px solid ${theme.topAppBarColor || "grey"}`
}}>
{me?.user?.current_operation_banner_text}
</Typography>
}
{me.loggedIn && me?.badConnection
&&
<Typography style={{
backgroundColor: theme.palette.error.main,
width: "100%",
textAlign: "center",
fontWeight: "600",
color: "white",
border: `1px solid ${theme.topAppBarColor || "grey"}`
}}>
{"Can't connect to Mythic. Please check connection and refresh"}
</Typography>
}
<div style={{
margin: '0px 0px 0px 0px',
flexGrow: 1,
overflowY: "hidden",
flexDirection: 'column',
height: "100%",
}}>
<Routes>
<Route path='/new/login' element={<LoginForm me={me}/>}/>
<Route path='/new/invite' element={<InviteForm me={me}/>}/>
<Route path='/' element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>}/>
<Route exact path='/new'
element={<LoggedInRoute me={me}><Home me={me}/></LoggedInRoute>}/>
<Route exact path='/new/settings'
element={<LoggedInRoute me={me}><Settings me={me}/></LoggedInRoute>}/>
<Route exact path='/new/payloadtypes'
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/eventfeed'
element={<LoggedInRoute me={me}><EventFeed me={me}/></LoggedInRoute>}/>
<Route exact path='/new/createpayload'
element={<LoggedInRoute me={me}><CreatePayload me={me}/></LoggedInRoute>}/>
<Route exact path='/new/createwrapper'
element={<LoggedInRoute me={me}><CreatePayloadWrapper
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/payloads'
element={<LoggedInRoute me={me}><Payloads me={me}/></LoggedInRoute>}/>
<Route exact path='/new/c2profiles'
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/services/'
element={<LoggedInRoute me={me}><PayloadTypesC2Profiles
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/callbacks'
element={<LoggedInRoute me={me}><Callbacks me={me}/></LoggedInRoute>}/>
<Route path='/new/search'
element={<LoggedInRoute me={me}><Search history={props.history}
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/browserscripts'
element={<LoggedInRoute me={me}><BrowserScripts me={me}/></LoggedInRoute>}/>
<Route exact path='/new/task/:taskId'
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
<Route exact path='/new/tasks/by_range'
element={<LoggedInRoute me={me}><SingleTaskView me={me}/></LoggedInRoute>}/>
<Route exact path='/new/operations'
element={<LoggedInRoute me={me}><Operations me={me}/></LoggedInRoute>}/>
<Route exact path='/new/callbacks/:callbackDisplayId'
element={<LoggedInRoute me={me}><ExpandedCallback
me={me}/></LoggedInRoute>}/>
<Route exact path='/new/reporting'
element={<LoggedInRoute me={me}><Reporting me={me}/></LoggedInRoute>}/>
<Route exact path='/new/mitre'
element={<LoggedInRoute me={me}><MitreAttack me={me}/></LoggedInRoute>}/>
<Route exact path='/new/tagtypes'
element={<LoggedInRoute me={me}><Tags me={me}/></LoggedInRoute>}/>
<Route exact path='/new/eventing'
element={<LoggedInRoute me={me}><Eventing me={me}/></LoggedInRoute>}/>
</Routes>
</div>
</div>
</div>
</div>
</MeContext.Provider>
</ThemeProvider>
</StyledEngineProvider>
);
@@ -17,6 +17,7 @@ const subscribe_payloads = gql`
level
resolved
source
warning
}
}
`;
@@ -33,7 +34,10 @@ export function EventFeedNotifications(props) {
},
onData: ({data}) => {
if (data.data.operationeventlog_stream.length > 0){
if(data.data.operationeventlog_stream[0].level === "api" || data.data.operationeventlog_stream[0].level === "debug" ){
if(data.data.operationeventlog_stream[0].level === "api" ||
data.data.operationeventlog_stream[0].level === "debug" ||
data.data.operationeventlog_stream[0].level === "agent_message"
){
return;
}
if(data.data.operationeventlog_stream[0].resolved){
@@ -41,14 +45,14 @@ export function EventFeedNotifications(props) {
}
if(data.data.operationeventlog_stream[0].operator){
const message = data.data.operationeventlog_stream[0].operator.username + ":\n" + data.data.operationeventlog_stream[0].message;
if (data.data.operationeventlog_stream[0].level === "warning") {
if (data.data.operationeventlog_stream[0].warning) {
snackActions.warning(message, { autoClose: 2000});
} else {
snackActions.info(message, { autoClose: 2000});
}
}else {
if (data.data.operationeventlog_stream[0].level === "warning") {
if (data.data.operationeventlog_stream[0].warning) {
snackActions.warning(data.data.operationeventlog_stream[0].message, {autoClose: 3000});
} else {
snackActions.info(
@@ -1,16 +1,52 @@
import {useTheme} from '@mui/material/styles';
import {MythicStyledTooltip} from "./MythicStyledTooltip";
import WifiIcon from '@mui/icons-material/Wifi';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import { faLink } from '@fortawesome/free-solid-svg-icons';
export const MythicAgentSVGIcon = ({payload_type, style}) => {
export const MythicAgentSVGIcon = ({payload_type, style, is_p2p}) => {
const theme = useTheme();
if(payload_type === ""){
return;
}
return (
<MythicStyledTooltip title={payload_type}>
<MythicStyledTooltip title={payload_type} >
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"}
style={{...style}}/>
style={{...style}} />
{is_p2p === false &&
<WifiIcon style={{width: "20px", height: "20px", marginLeft: "-20px", marginBottom: "5px",
borderRadius: "10px", border: "1px solid",
backgroundColor: theme.palette.primary.main}}/>
}
{is_p2p === true &&
<FontAwesomeIcon icon={faLink} style={{width: "20px", height: "20px", marginLeft: "-20px", marginBottom: "5px",
borderRadius: "10px", border: "1px solid",
backgroundColor: theme.palette.primary.main}} />
}
</MythicStyledTooltip>
)
}
export const MythicAgentSVGIconNoTooltip = ({payload_type, is_p2p, className}) => {
const theme = useTheme();
if(payload_type === ""){
return;
}
return (
<div style={{display: "inline-block", left: "50%"}} className={className}>
<img src={"/static/" + payload_type + "_" + theme.palette.mode + ".svg"} style={{height: "20px", margin: 'auto'}}/>
{is_p2p === false &&
<WifiIcon style={{width: "10px", height: "10px", marginLeft: "-5px", marginBottom: "0px",
borderRadius: "10px", border: "1px solid",
backgroundColor: theme.palette.primary.main, color: "white" }} />
}
{is_p2p === true &&
<FontAwesomeIcon icon={faLink} style={{width: "20px", height: "20px", marginLeft: "-20px", marginBottom: "5px",
borderRadius: "10px", border: "1px solid",
backgroundColor: theme.palette.primary.main, color: "white"}} />
}
</div>
)
}
@@ -8,6 +8,7 @@ import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Paper from '@mui/material/Paper';
import Badge from '@mui/material/Badge';
import {
Draggable,
} from "@hello-pangea/dnd";
@@ -60,6 +61,8 @@ export function MythicTabLabel(props) {
getCallbackData,
onDragTab,
selectedIndex,
highlight,
newDataForTab,
...other
} = props;
const onClick = (e) => {
@@ -87,7 +90,7 @@ export function MythicTabLabel(props) {
setOpenContextMenu(false);
};
return (
<Draggable draggableId={`callbacks-tab-${label}`} index={index} disableInteractiveElementBlocking={true}>
<Draggable draggableId={`callbacks-tab-${tabInfo.tabID}`} index={index} disableInteractiveElementBlocking={true}>
{(provided, snapshot) => (
<div ref={provided.innerRef}
{...provided.draggableProps}>
@@ -95,7 +98,9 @@ export function MythicTabLabel(props) {
className={selectedIndex === index || snapshot.isDragging ? "selectedCallback" : "" }
label={
<span onContextMenu={handleContextClick} style={{}} ref={dropdownAnchorRef}>
{label}
<Badge color="success" variant="dot" invisible={!highlight} >
{label}
</Badge>
<IconButton component='div' size='small' onClick={onClick} {...other}>
<CloseIcon />
</IconButton>
@@ -11,7 +11,7 @@ import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import { Typography } from '@mui/material';
import { ReactFlow,
applyEdgeChanges,
EdgeLabelRenderer,getBezierPath, BaseEdge,
Handle, Position, useReactFlow, ReactFlowProvider, Panel,
MiniMap, Controls, ControlButton, useUpdateNodeInternals,
getConnectedEdges, useNodesState, useEdgesState
@@ -37,6 +37,7 @@ import SendIcon from '@mui/icons-material/Send';
import {getIconName} from "./ResponseDisplayTable";
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import { MythicAgentSVGIconNoTooltip} from "../../MythicComponents/MythicAgentSVGIcon";
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
@@ -459,6 +460,41 @@ export function GroupNode({data}) {
}
const nodeTypes = { "agentNode": AgentNode, "groupNode": GroupNode, "taskNode": TaskNode, "browserscriptNode": BrowserscriptNode };
export function C2LabelEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data,
labelStyle, markerStart, markerEnd, labelShowBg, labelBgStyle, style, label,
interactionWidth
}){
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
<BaseEdge id={id} path={edgePath} labelStyle={labelStyle} markerEnd={markerEnd} markerStart={markerStart}
labelShowBg={labelShowBg} labelBgStyle={labelBgStyle} style={style} label={label}
interactionWidth={interactionWidth} labelX={labelX} labelY={labelY}/>
<EdgeLabelRenderer>
<div
style={{
transform: `translate(-1%, -50%) translate(${labelX}px,${labelY}px)`,
}}
className="nodrag nopan"
>
{data.has_logo && <MythicAgentSVGIconNoTooltip payload_type={label}
is_p2p={data.is_p2p}
className={"circleImageNode"}/>}
</div>
</EdgeLabelRenderer>
</>
)
}
const edgeTypes = {
C2IconEdge: C2LabelEdge
};
const elk = new ELK();
const getWidth = (node) => {
if(node.type === "taskNode"){
@@ -854,9 +890,12 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
mythic_id: edge.id,
source: `${edge.source.id}`,
target: "Mythic",
label: edge.c2profile.name,
type: "C2IconEdge",
animated: true,
label: edge.c2profile.name,
data: {
has_logo: edge.c2profile.has_logo,
is_p2p: edge.c2profile.is_p2p,
source: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
target: {parentId: "Mythic"},
end_timestamp: edge.end_timestamp,
@@ -970,8 +1009,11 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
target: `${edge.source.id}`,
label: edge.c2profile.name,
animated: true,
type: "C2IconEdge",
data: {
end_timestamp: edge.end_timestamp,
has_logo: edge.c2profile.has_logo,
is_p2p: edge.c2profile.is_p2p,
source: {...edge.destination, parentId: getGroupBy(edge.destination, view_config)},
target: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
}
@@ -1004,8 +1046,11 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
target: `${edge.destination.id}`,
label: edge.c2profile.name,
animated: true,
type: "C2IconEdge",
data: {
end_timestamp: edge.end_timestamp,
has_logo: edge.c2profile.has_logo,
is_p2p: edge.c2profile.is_p2p,
source: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
target: {...edge.target, parentId: getGroupBy(edge.target, view_config)},
}
@@ -1040,8 +1085,11 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
target: `${edge.destination.id}`,
label: edge.c2profile.name,
animated: true,
type: "C2IconEdge",
data: {
end_timestamp: edge.end_timestamp,
has_logo: edge.c2profile.has_logo,
is_p2p: edge.c2profile.is_p2p,
source: {...edge.source, parentId: getGroupBy(edge.source, view_config)},
target: {...edge.target, parentId: getGroupBy(edge.destination, view_config)},
}
@@ -1239,7 +1287,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
tempEdges[i].markerEnd.type = "arrowclosed"
tempEdges[i].labelBgStyle = {
fill: theme.tableHover,
fillOpacity: 0.6,
fillOpacity: 0.8,
}
tempEdges[i].labelStyle = {
fill: theme.palette.background.contrast,
@@ -1270,6 +1318,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
target: tempEdges[j].data.target.parentId,
label: "",
hidden: true,
type: "C2IconEdge",
data: {
source: {parentId: parentIds[i].id},
target: tempEdges[j].data.target,
@@ -1289,6 +1338,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
target: tempEdges[j].data.target.parentId,
label: "",
hidden: true,
type: "C2IconEdge",
data: {
source: {parentId: parentIds[i].id},
target: tempEdges[j].data.target,
@@ -1368,6 +1418,7 @@ export const DrawC2PathElementsFlow = ({edges, panel, view_config, contextMenu,
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onPaneClick={onPaneClick}
onNodeContextMenu={onNodeContextMenu}
onNodeClick={onNodeSelected}
@@ -30,6 +30,7 @@ export const getCallbackIdFromClickedTab = (tabId) => {
export function Callbacks({me}) {
const [openCallbackImport, setOpenCallbackImport] = React.useState(false);
const [topDisplay, setTopDisplay] = React.useState('table');
const [newDataForTab, setNewDataForTab] = React.useState({});
const [openTabs, setOpenTabs] = React.useState([]);
const [clickedTabId, setClickedTabIdValue] = React.useState('');
const openTabRef = React.useRef([]);
@@ -46,6 +47,7 @@ export function Callbacks({me}) {
}
}
setClickedTabIdValue(tabID);
setNewDataForTab((prev) => {return {...prev, [tabID]: false}});
}
useEffect(() => {
const oldTabs = localStorage.getItem('openTabs');
@@ -257,6 +259,8 @@ export function Callbacks({me}) {
me={me}
onDragEnd={onDragEnd}
contextMenuOptions={contextMenuOptions}
newDataForTab={newDataForTab}
setNewDataForTab={setNewDataForTab}
/>
</div>
</Split>
@@ -20,7 +20,7 @@ import {
} from './CallbacksTableRow';
import MythicResizableGrid from '../../MythicComponents/MythicResizableGrid';
import {TableFilterDialog} from './TableFilterDialog';
import {CallbacksTabsHideMultipleDialog} from "./CallbacksTabsHideMultipleDialog";
import {CallbacksTabsHideMultipleDialog, CallbacksTabsSelectMultipleDialog} from "./CallbacksTabsHideMultipleDialog";
import {CallbacksTabsTaskMultipleDialog} from "./CallbacksTabsTaskMultipleDialog";
import ip6 from 'ip6';
import {CallbacksContext, OnOpenTabContext, OnOpenTabsContext} from "./CallbacksTop";
@@ -53,6 +53,9 @@ import NotificationsActiveTwoToneIcon from '@mui/icons-material/NotificationsAct
import NotificationsOffTwoToneIcon from '@mui/icons-material/NotificationsOffTwoTone';
import {CallbacksTableEditTriggerOnCheckinDialog} from "./CallbacksTableEditTriggerOnCheckinDialog";
import {CallbacksTableColumnsReorderDialog} from "./CallbacksTableColumnsReorderDialog";
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
import Typography from '@mui/material/Typography';
import {EventTriggerContextSelectDialog} from "../Eventing/EventTriggerContextSelect";
export const ipCompare = (a, b) => {
let aJSON = JSON.parse(a);
@@ -316,6 +319,9 @@ function CallbacksTablePreMemo(props){
})
const taskingData = React.useRef({"parameters": "", "ui_feature": "callback_table:exit"});
const [openTaskingButton, setOpenTaskingButton] = React.useState(false);
const eventingDataRef = React.useRef({});
const [openEventingDialog, setOpenEventingDialog] = React.useState(false);
const [openEventingMultipleDialog, setOpenEventingMultipleDialog] = React.useState(false);
const [updateSetting] = useSetMythicSetting();
const onUpdateTrigger = (newTriggerValue) => {
updateTrigger({variables: {callback_display_id: openTriggerDialog.display_id, trigger_on_checkin_after_time: newTriggerValue}})
@@ -331,7 +337,14 @@ function CallbacksTablePreMemo(props){
}
return [
{
name: "Callback: " + rowDataStatic.display_id,
name: <div style={{display: "flex", flexDirection: "column"}}>
<Typography>
{"Callback: " + rowDataStatic.display_id}
</Typography>
<Typography style={{fontSize: theme?.typography.pxToRem(12),}}>
{rowDataStatic.user + "@" + rowDataStatic.host}
</Typography>
</div>,
icon: null, click: ({event}) => {},
type: "item",
disabled: true
@@ -399,6 +412,21 @@ function CallbacksTablePreMemo(props){
setOpenTriggerDialog({open: true, trigger_on_checkin_after_time: rowDataStatic.trigger_on_checkin_after_time, display_id: rowDataStatic.display_id})
}
},
{
name: "Eventing", icon: <PlayCircleFilledTwoToneIcon />, click: (event) => {}, type: "menu",
menuItems: [
{
name: "Start Eventing Workflow", icon: <PlayCircleFilledTwoToneIcon style={{paddingRight: "5px"}} />,
click: ({event}) => {
eventingDataRef.current = {
name: "callback_id",
value: rowDataStatic.id
};
setOpenEventingDialog(true);
}
}
]
},
{
name: "Browsers", icon: null, click: () => {}, type: "menu",
menuItems: [
@@ -445,6 +473,12 @@ function CallbacksTablePreMemo(props){
setOpenTaskMultipleDialog({open: true, data: rowDataStatic});
}
},
{
name: "Start Multiple Eventing Workflows", icon: <PlayCircleFilledTwoToneIcon style={{paddingRight: "5px"}}/>,
click: ({event}) => {
setOpenEventingMultipleDialog(true);
}
}
]
},
{
@@ -904,6 +938,13 @@ function CallbacksTablePreMemo(props){
const onResetColumnReorder = () => {
onSubmitColumnReorder(callbackTableInitialColumns);
}
const onSubmitSelectMultiple = (callbacks) => {
eventingDataRef.current = {
trigger_context_type: "callback_id",
trigger_context_ids: callbacks.map(c => c.id)
};
setOpenEventingDialog(true);
}
return (
<div style={{width: '100%', height: '100%', position: "relative",}}>
<MythicResizableGrid
@@ -1081,6 +1122,31 @@ function CallbacksTablePreMemo(props){
}
/>
}
{openEventingDialog &&
<MythicDialog
fullWidth={true}
maxWidth="xl"
open={openEventingDialog}
onClose={() => {setOpenEventingDialog(false);}}
innerDialog={
<EventTriggerContextSelectDialog onClose={() => {setOpenEventingDialog(false);}}
triggerContext={eventingDataRef.current}
/>
}
/>
}
{openEventingMultipleDialog &&
<MythicDialog
fullWidth={true}
maxWidth="xl"
open={openEventingMultipleDialog}
onClose={() => {setOpenEventingMultipleDialog(false);}}
innerDialog={
<CallbacksTabsSelectMultipleDialog onClose={() => {setOpenEventingMultipleDialog(false);}}
onSubmit={onSubmitSelectMultiple}/>
}
/>
}
</div>
)
}
@@ -11,7 +11,7 @@ import { CallbacksTabsProcessBrowserLabel, CallbacksTabsProcessBrowserPanel } fr
import { CallbacksTabsTaskingSplitLabel, CallbacksTabsTaskingSplitPanel} from "./CallbacksTabsTaskingSplit";
import {CallbacksTabsTaskingConsoleLabel, CallbacksTabsTaskingConsolePanel} from "./CallbacksTabsTaskingConsole";
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clickedTabId, setClickedTabId, onEditTabDescription, contextMenuOptions, me}) {
export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clickedTabId, setClickedTabId, onEditTabDescription, contextMenuOptions, me, newDataForTab, setNewDataForTab}) {
const mountedRef = React.useRef(true);
const [value, setValue] = React.useState(0);
@@ -75,94 +75,103 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="callback-tabs-list" direction={"horizontal"}>
{(provided) => (
<Tabs
ref={provided.innerRef} {...provided.droppableProps}
value={value}
onChange={handleChange}
indicatorColor='primary'
textColor='primary'
variant='fullWidth'
scrollButtons={true}
style={{ }}
TabIndicatorProps={{style: {display: "none"}}}
aria-label='scrollable tabs'>
{openTabs.map((tab, index) => {
switch (tab.tabType) {
case 'interact':
return (
<CallbacksTabsTaskingLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={modifiedInteractContextMenuOptions}
/>
);
case 'interactSplit':
return (
<CallbacksTabsTaskingSplitLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'interactConsole':
return (
<CallbacksTabsTaskingConsoleLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'fileBrowser':
return (
<CallbacksTabsFileBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
case 'processBrowser':
return (
<CallbacksTabsProcessBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
default:
return null;
}
})}
</Tabs>
<>
<Tabs
ref={provided.innerRef} {...provided.droppableProps}
value={value}
onChange={handleChange}
indicatorColor='primary'
textColor='primary'
variant='fullWidth'
scrollButtons={true}
style={{ }}
TabIndicatorProps={{style: {display: "none"}}}
aria-label='scrollable tabs'>
{openTabs.map((tab, index) => {
switch (tab.tabType) {
case 'interact':
return (
<CallbacksTabsTaskingLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={modifiedInteractContextMenuOptions}
/>
);
case 'interactSplit':
return (
<CallbacksTabsTaskingSplitLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'interactConsole':
return (
<CallbacksTabsTaskingConsoleLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
)
case 'fileBrowser':
return (
<CallbacksTabsFileBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
case 'processBrowser':
return (
<CallbacksTabsProcessBrowserLabel
onEditTabDescription={onEditTabDescription}
onCloseTab={onCloseTabLocal}
key={'tablabel' + tab.tabID + tab.tabType}
tabInfo={tab}
index={index}
me={me}
newDataForTab={newDataForTab}
selectedIndex={value}
onDragTab={onDragTab}
contextMenuOptions={contextMenuOptions}
/>
);
default:
return null;
}
})}
{provided.placeholder}
</Tabs>
</>
)}
</Droppable>
</DragDropContext>
@@ -182,6 +191,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
key={'tabpanel' + tab.tabID + tab.tabType}
onCloseTab={onCloseTabLocal}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
@@ -201,6 +211,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
key={'tabpanel' + tab.tabID + tab.tabType}
onCloseTab={onCloseTabLocal}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
@@ -219,6 +230,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
key={'tabpanel' + tab.tabID + tab.tabType}
onCloseTab={onCloseTabLocal}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
@@ -237,6 +249,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
onCloseTab={onCloseTabLocal}
key={'tabpanel' + tab.tabID + tab.tabType}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
@@ -255,6 +268,7 @@ export function CallbacksTabs({ onCloseTab, openTabs, onDragTab, onDragEnd, clic
onCloseTab={onCloseTabLocal}
key={'tabpanel' + tab.tabID + tab.tabType}
tabInfo={tab}
setNewDataForTab={setNewDataForTab}
value={value}
index={index}
me={me}
@@ -173,7 +173,7 @@ export function CallbacksTabsFileBrowserLabel(props) {
]);
return (
<React.Fragment>
<MythicTabLabel label={description} onDragTab={props.onDragTab} {...props} contextMenuOptions={contextMenuOptions} />
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} {...props} contextMenuOptions={contextMenuOptions} />
{openEditDescriptionDialog && (
<MythicDialog
fullWidth={true}
@@ -196,7 +196,7 @@ export function CallbacksTabsFileBrowserLabel(props) {
</React.Fragment>
);
}
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) => {
export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNewDataForTab }) => {
const fromNow = React.useRef(getSkewedNow());
const [backdropOpen, setBackdropOpen] = React.useState(false);
const treeRootDataRef = React.useRef({}); // hold all of the actual data
@@ -360,7 +360,9 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me }) =>
return prev;
}, {...treeAdjMtx});
setTreeAdjMtx(newMatrix);
if(index !== value){
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
}
}
})
useSubscription(fileBrowserTaskSub, {
@@ -92,7 +92,7 @@ const columns = [
flex: 0.5,
}
];
const CustomSelectTable = ({initialData, selectedData}) => {
const CustomSelectTable = ({initialData, selectedData, sortModel}) => {
const [data, setData] = React.useState([]);
const [rowSelectionModel, setRowSelectionModel] = React.useState({
type: 'include',
@@ -120,7 +120,7 @@ const CustomSelectTable = ({initialData, selectedData}) => {
},
},
sorting: {
sortModel: [{ field: 'last_checkin', sort: 'asc' }],
sortModel: [sortModel],
},
}}
autoPageSize
@@ -176,7 +176,9 @@ export function CallbacksTabsHideMultipleDialog({onClose}) {
<React.Fragment>
<DialogTitle id="form-dialog-title">Hide Multiple Callbacks at Once</DialogTitle>
<CustomSelectTable initialData={initialData}
selectedData={selectedData} />
selectedData={selectedData}
sortModel={{ field: 'last_checkin', sort: 'asc' }}
/>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
Close
@@ -188,4 +190,46 @@ export function CallbacksTabsHideMultipleDialog({onClose}) {
</React.Fragment>
);
}
export function CallbacksTabsSelectMultipleDialog({onClose, onSubmit}) {
const selectedData = React.useRef([]);
const [initialData, setInitialData] = React.useState([]);
useQuery(callbacksAndFeaturesQuery,{
fetchPolicy: "no-cache",
onCompleted: (data) => {
const callbackData = data.callback.map( c => {
// for each callback, get a unique set of supported features
const display = `${c.display_id} - ${c.user}@${c.host} (${c.pid}) - ${c.description}`;
return {...c, display};
});
setInitialData(callbackData);
}
});
const submitTasking = () => {
if(selectedData.current.length === 0){
onClose();
return;
}
onSubmit(selectedData.current);
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Select Multiple Callbacks</DialogTitle>
<CustomSelectTable initialData={initialData}
selectedData={selectedData}
sortModel={{ field: 'display_id', sort: 'desc' }}
/>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
Close
</Button>
<Button onClick={submitTasking} variant="contained" color="warning">
Use Selection
</Button>
</DialogActions>
</React.Fragment>
);
}
@@ -132,7 +132,7 @@ export function CallbacksTabsProcessBrowserLabel(props){
}
return (
<React.Fragment>
<MythicTabLabel label={description} onDragTab={props.onDragTab} {...props} contextMenuOptions={contextMenuOptions}/>
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} {...props} contextMenuOptions={contextMenuOptions}/>
{openEditDescriptionDialog &&
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
innerDialog={
@@ -143,7 +143,7 @@ export function CallbacksTabsProcessBrowserLabel(props){
</React.Fragment>
)
}
export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>{
export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me, setNewDataForTab}) =>{
const fromNow = React.useRef((new Date()));
const [backdropOpen, setBackdropOpen] = React.useState(false);
const [expandOrCollapseAll, setExpandOrCollapseAll] = React.useState(false);
@@ -290,6 +290,9 @@ export const CallbacksTabsProcessBrowserPanel = ({index, value, tabInfo, me}) =>
}
const newMatrix = getNewMatrix();
setTreeAdjMtx(newMatrix);
if(index !== value){
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
}
}
});
const getHostProcessesQuerySuccess = (data) => {
@@ -48,7 +48,7 @@ export function CallbacksTabsTaskingLabel(props){
]);
return (
<>
<MythicTabLabel label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
<MythicTabLabel highlight={props.newDataForTab[props.tabInfo.tabID]} label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
{openEditDescriptionDialog &&
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
innerDialog={
@@ -82,7 +82,7 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
}
}
`;
export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me, collapseTaskRequest}) =>{
export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me, collapseTaskRequest, setNewDataForTab}) =>{
const [taskLimit, setTaskLimit] = React.useState(20);
const [scrollToBottom, setScrollToBottom] = React.useState(false);
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
@@ -175,6 +175,9 @@ export const CallbacksTabsTaskingPanel = ({tabInfo, index, value, onCloseTab, pa
if(!fetched){
setFetched(true);
}
if(index !== value && fetched){
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
}
//console.log("new subscription data in CallbacksTabsTasking", subscriptionData);
const oldLength = taskingDataRef.current.task.length;
const mergedData = data.data.task.reduce( (prev, cur) => {
@@ -48,7 +48,7 @@ export function CallbacksTabsTaskingConsoleLabel(props){
]);
return (
<React.Fragment>
<MythicTabLabel label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
{openEditDescriptionDialog &&
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
innerDialog={
@@ -82,7 +82,7 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
}
}
`;
export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me}) =>{
export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onCloseTab, parentMountedRef, me, setNewDataForTab}) =>{
const [taskLimit, setTaskLimit] = React.useState(10);
const [scrollToBottom, setScrollToBottom] = React.useState(false);
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
@@ -175,6 +175,9 @@ export const CallbacksTabsTaskingConsolePanel = ({tabInfo, index, value, onClose
if(!fetched){
setFetched(true);
}
if(index !== value && fetched){
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
}
//console.log("new subscription data in CallbacksTabsTasking", subscriptionData);
const oldLength = taskingDataRef.current.task.length;
const mergedData = data.data.task.reduce( (prev, cur) => {
@@ -50,7 +50,7 @@ export function CallbacksTabsTaskingSplitLabel(props){
]);
return (
<React.Fragment>
<MythicTabLabel label={description} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
<MythicTabLabel label={description} highlight={props.newDataForTab[props.tabInfo.tabID]} onDragTab={props.onDragTab} me={props.me} {...props} contextMenuOptions={contextMenuOptions}/>
{openEditDescriptionDialog &&
<MythicDialog fullWidth={true} open={openEditDescriptionDialog} onClose={() => {setOpenEditDescriptionDialog(false);}}
innerDialog={
@@ -82,7 +82,7 @@ query getBatchTasking($callback_id: Int!, $offset: Int!, $fetchLimit: Int!){
}
}
`;
export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTab, me}) =>{
export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTab, me, setNewDataForTab}) =>{
const [taskLimit, setTaskLimit] = React.useState(30);
const [scrollToBottom, setScrollToBottom] = React.useState(false);
const [openParametersDialog, setOpenParametersDialog] = React.useState(false);
@@ -171,6 +171,9 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
if(!fetched){
setFetched(true);
}
if(index !== value && fetched){
setNewDataForTab((prev) => {return {...prev, [tabInfo.tabID]: true}});
}
//console.log("new subscription data in CallbacksTabsTasking", subscriptionData);
const oldLength = taskingDataRef.current.task.length;
const mergedData = data.data.task.reduce( (prev, cur) => {
@@ -168,6 +168,8 @@ callback(where: {active: {_eq: true}}) {
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
@@ -201,6 +203,8 @@ callback(where: {active: {_eq: true}}) {
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
@@ -208,6 +212,7 @@ callback(where: {active: {_eq: true}}) {
id
is_p2p
name
has_logo
}
}
}
@@ -248,6 +253,8 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
@@ -282,6 +289,8 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
callbackc2profiles {
c2profile {
name
has_logo
is_p2p
}
}
}
@@ -289,6 +298,7 @@ callbackgraphedge_stream(batch_size: 100, cursor: {initial_value: {updated_at: $
id
is_p2p
name
has_logo
}
}
}
@@ -298,13 +298,14 @@ export const ResponseDisplayInteractive = (props) =>{
if(page.current === currentPage){
const pageCount = Math.max(1, Math.ceil(allData.length / pageSize.current));
if(page.current === pageCount -1){
console.log("updating pageSize");
// we just streamed more data and we're on the latest page, increase pageSize
//pageSize.current += Math.abs(allData.length - totalCount);
currentPage += 1;
}
}
setAllOutput(allData.slice((currentPage-1)*pageSize.current, currentPage*pageSize.current));
if(props.selectAllOutput){
setAllOutput(allData);
} else {
setAllOutput(allData.slice((currentPage-1)*pageSize.current, currentPage*pageSize.current));
}
setTotalCount(allData.length);
}else{
let allData = [...rawResponses, ...taskData];
@@ -337,6 +338,9 @@ export const ResponseDisplayInteractive = (props) =>{
React.useEffect( () => {
onSubmitPageChange(1);
}, [search]);
React.useEffect( () => {
onSubmitPageChange(1);
}, [props.selectAllOutput]);
const onSubmitSearch = React.useCallback( (newSearch) => {
setSearch(newSearch);
}, []);
@@ -431,7 +435,7 @@ export const ResponseDisplayInteractive = (props) =>{
<InteractivePaginationBar totalCount={totalCount} currentPage={page.current}
onSubmitPageChange={onSubmitPageChange} expand={props.expand}
pageSize={pageSize.current}/>
pageSize={pageSize.current} selectAllOutput={props.selectAllOutput}/>
</div>
)
@@ -633,12 +637,12 @@ const InteractiveTaskingBar = ({
</div>
)
}
const InteractivePaginationBar = ({totalCount, currentPage, onSubmitPageChange, pageSize, expand}) => {
const InteractivePaginationBar = ({totalCount, currentPage, onSubmitPageChange, pageSize, selectAllOutput}) => {
const onChangePage = (event, value) => {
onSubmitPageChange(value);
};
const pageCount = Math.max(1, Math.ceil(totalCount / pageSize));
if(pageCount < 2){
if(pageCount < 2 || selectAllOutput){
return null;
}
return (
@@ -21,6 +21,7 @@ import {faList, faTrashAlt, faSkullCrossbones, faCamera, faSyringe, faFolder, fa
import {Dropdown, DropdownMenuItem} from "../../MythicComponents/MythicNestedMenus";
import {GetComputedFontSize} from "../../MythicComponents/MythicSavedUserSetting";
import {TableFilterDialog} from "./TableFilterDialog";
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
const onCopyToClipboard = (data) => {
let result = copyStringToClipboard(data);
@@ -407,11 +408,12 @@ const ResponseDisplayTableActionCellButton = ({cellData, callback_id}) => {
table={taskingData?.value || {}} callback_id={callback_id} onClose={finishedViewingData} />}
/>
}
<Button size="small" variant={"contained"} color={"info"} ref={dropdownAnchorRef}
<Button size="small" variant={"contained"} ref={dropdownAnchorRef}
onClick={() => setOpenDropdownButton(true)} disabled={cellData?.button?.disabled || false}
startIcon={cellData?.button?.startIcon ? <FontAwesomeIcon icon={getIconName(cellData?.button?.startIcon)} style={{color: cellData?.button?.disabled ? "unset" : getIconColor(theme, cellData?.button?.startIconColor || "")}}/> : null}
style={{...actionCellButtonStyle}}
>{cellData?.button?.name || " "}</Button>
>{cellData?.button?.name || " "} <ArrowDropDownIcon />
</Button>
<ClickAwayListener onClickAway={handleClose} mouseEvent={"onMouseDown"}>
<Dropdown
isOpen={dropdownAnchorRef.current}
@@ -85,6 +85,17 @@ query getCredential($id: Int!){
}
`;
const arraysAreDifferent = (a, b) => {
if(a.length !== b.length){
return true
}
for(let i = 0; i < a.length; i++){
if(a[i] !== b[i]){
return true;
}
}
return false;
}
export function TaskParametersDialogRow(props){
const [value, setValue] = React.useState('');
const theme = useTheme();
@@ -364,7 +375,7 @@ export function TaskParametersDialogRow(props){
}
}
else if(props.type === "LinkInfo"){
if(props.choices.length > 0){
if(props.choices.length > 0 && value === ''){
setChoiceOptions([...props.choices]);
onChangeLinkInfo(0);
}
@@ -378,19 +389,29 @@ export function TaskParametersDialogRow(props){
}else{
setAgentConnectHost(0);
}
setAgentConnectHostOptions(props.choices);
if(arraysAreDifferent(props.choices, agentConnectHostOptions)){
setAgentConnectHostOptions(props.choices);
}
let payloadNum = 0;
if(agentConnectPayload < props.choices[hostNum]["payloads"].length){
payloadNum = agentConnectPayload;
}
setAgentConnectPayload(payloadNum);
setAgentConnectPayloadOptions(props.choices[hostNum]["payloads"]);
if(agentConnectPayload !== payloadNum){
setAgentConnectPayload(payloadNum);
}
if(arraysAreDifferent(props.choices[hostNum]['payloads'], agentConnectPayloadOptions)){
setAgentConnectPayloadOptions(props.choices[hostNum]["payloads"]);
}
if(props.choices[hostNum]["payloads"].length > 0){
//setAgentConnectPayload(0);
if(props.choices[hostNum]["payloads"][payloadNum]["c2info"].length > 0){
setAgentConnectC2ProfileOptions(props.choices[hostNum]["payloads"][payloadNum]["c2info"]);
if(arraysAreDifferent(props.choices[hostNum]["payloads"][payloadNum]["c2info"], agentConnectC2ProfileOptions)){
setAgentConnectC2ProfileOptions(props.choices[hostNum]["payloads"][payloadNum]["c2info"]);
}
//setAgentConnectC2Profile(0);
onChangeAgentConnect(agentConnectHost, payloadNum, 0);
if(agentConnectHost !== hostNum || agentConnectPayload !== payloadNum){
onChangeAgentConnect(hostNum, payloadNum, 0);
}
}
}else{
snackActions.warning("Mythic knows of no host with a P2P payload. Please add one.");
@@ -441,7 +462,9 @@ export function TaskParametersDialogRow(props){
}
}
}
}, [props.choices, props.default_value, props.type, props.value, setBoolValue, value]);
}, [props.choices, props.default_value, props.type, props.value, setBoolValue,
value, agentConnectHost, agentConnectPayload, agentConnectPayloadOptions, agentConnectHostOptions,
agentConnectC2ProfileOptions]);
const onChangeAgentConnect = (host_index, payload_index, c2_index) => {
const c2profileparameters = props.choices[host_index]["payloads"][payload_index]["c2info"][c2_index].parameters.reduce( (prev, opt) => {
return {...prev, [opt.name]: opt.value}
@@ -1007,7 +1030,7 @@ export function TaskParametersDialogRow(props){
</MythicStyledTableCell>
</TableRow>
<TableRow>
<MythicStyledTableCell>Payload</MythicStyledTableCell>
<MythicStyledTableCell>Payload / Callback</MythicStyledTableCell>
<MythicStyledTableCell>
<FormControl style={{width: "100%"}}>
<Select
@@ -16,6 +16,7 @@ import {ParseForDisplay} from "../Payloads/DetailedPayloadTable";
import Paper from '@mui/material/Paper';
import {useTheme} from '@mui/material/styles';
import {getModifiedC2Params} from "./Step4C2Profiles";
import { Backdrop, CircularProgress } from '@mui/material';
const GET_Payload_Types = gql`
@@ -37,11 +38,23 @@ query getPayloadTypesQuery {
export const GetPayloads = gql`
query payloads($payloadType: String!, $os: String!) {
payload(where: {auto_generated: {_eq: false}, os: {_eq: $os}, payloadtype: {name: {_eq: $payloadType}}, build_phase: {_eq: "success"}, deleted: {_eq: false}}, order_by: {id: desc}) {
payload(where: {auto_generated: {_eq: false}, os: {_eq: $os}, payloadtype: {name: {_eq: $payloadType}}, deleted: {_eq: false}}, order_by: {id: desc}) {
id
description
uuid
creation_time
build_phase
payload_build_steps(order_by: {step_number: asc}) {
step_name
step_number
step_success
step_skip
start_time
end_time
step_stdout
step_stderr
id
}
filemetum {
agent_file_id
filename_text
@@ -153,6 +166,7 @@ query getPayloadTypesBuildParametersQuery($payload_id: Int!) {
export function Step1SelectOS(props){
const [os, setOS] = React.useState('');
const [openBackdrop, setOpenBackdrop] = React.useState(true);
const [payloadtypeData, setPayloadtypeData] = React.useState({});
const [payloadtypesPerOS, setPayloadtypesPerOS] = React.useState({});
const [C2PerOS, setC2PerOS] = React.useState({});
@@ -219,11 +233,16 @@ export function Step1SelectOS(props){
.catch((data) => {console.log(data)})
}, [props.prevData, props.first])
React.useEffect( () => {
setOpenBackdrop(true);
getPayloads({variables: {payloadType: selectedPayloadType, os}})
.then(({data}) => {
setPayloadOptions(data.payload);
setOpenBackdrop(false);
})
.catch(({data}) => console.log(data));
.catch(({data}) => {
console.log(data);
setOpenBackdrop(false);
});
}, [selectedPayloadType, os]);
const finished = (clearNextPrevious) => {
@@ -437,13 +456,21 @@ export function Step1SelectOS(props){
overflow: "hidden"
}}>
{props.first &&
<StartFromExistingPayloadOrStartFresh first={props.first}
last={props.last}
canceled={canceled}
onSelectedPayload={onSelectedPayload}
payloadOptions={payloadOptions}
onStartFresh={onStartFresh}
/>
<div style={{flexGrow: 1, overflowY: "auto", position: "relative"}}>
{openBackdrop &&
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2000, position: "absolute"}}>
<CircularProgress color="inherit" disableShrink />
</Backdrop>
}
<StartFromExistingPayloadOrStartFresh first={props.first}
last={props.last}
canceled={canceled}
onSelectedPayload={onSelectedPayload}
payloadOptions={payloadOptions}
onStartFresh={onStartFresh}
/>
</div>
}
{!props.first &&
<ConfigureBuildParameters os={os} selectedPayloadType={selectedPayloadType}
@@ -659,7 +686,7 @@ export const GetGroupedParameters = ({buildParameters, os, c2_name}) => {
if(buildParameters[i].group_name === groupedData[j].name){
// only add the parameter if it doesn't meet a hide_condition
let should_hide = false;
if(buildParameters[i]?.supported_os?.length || 0 > 0){
if((buildParameters[i]?.supported_os?.length || 0) > 0){
if(!buildParameters[i]?.supported_os?.includes(os)){
should_hide = true;
}
@@ -669,17 +696,22 @@ export const GetGroupedParameters = ({buildParameters, os, c2_name}) => {
if(buildParameters[l].name === buildParameters[i].hide_conditions[k].name){
switch(buildParameters[i].hide_conditions[k].operand){
case "eq":
if(buildParameters[i].hide_conditions[k].value === buildParameters[l].value){
if(String(buildParameters[i].hide_conditions[k].value) === String(buildParameters[l].value)){
should_hide = true;
}
break;
case "neq":
if(buildParameters[i].hide_conditions[k].value !== buildParameters[l].value){
if(String(buildParameters[i].hide_conditions[k].value) !== String(buildParameters[l].value)){
should_hide = true;
}
break;
case "in":
if(buildParameters[i].hide_conditions[k].value.includes(buildParameters[l].value)){
if(buildParameters[i].hide_conditions[k].choices.includes(buildParameters[l].value)){
should_hide = true;
}
break;
case "nin":
if(!buildParameters[i].hide_conditions[k].choices.includes(buildParameters[l].value)){
should_hide = true;
}
break;
@@ -102,6 +102,9 @@ export const getModifiedC2Params = (c2, c2profileparameters, buildOptions, use_s
if(use_supplied_values){
configuredParam = {...param}
}
if(buildOptions.c2_parameter_deviations === null){
return [...prev, {...configuredParam}];
}
if(buildOptions.c2_parameter_deviations[c2.name] === undefined){
return [...prev, {...configuredParam}];
}
@@ -117,7 +120,12 @@ export const getModifiedC2Params = (c2, c2profileparameters, buildOptions, use_s
configuredParam.trackedValue = c2Config.default_value;
configuredParam.initialValue = c2Config.default_value;
}
if(c2Config.choices !== undefined){
if(c2Config.dictionary_choices !== undefined){
configuredParam.choices = c2Config.dictionary_choices;
configuredParam.value = configuredParam.choices;
configuredParam.trackedValue = configuredParam.choices;
configuredParam.initialValue = configuredParam.choices;
}else if(c2Config.choices !== undefined){
configuredParam.choices = c2Config.choices;
if(!configuredParam.choices.includes(configuredParam.default_value)){
if(configuredParam.choices.length > 0){
@@ -1,6 +1,5 @@
import React from 'react';
import {useQuery, gql} from '@apollo/client';
import CircularProgress from '@mui/material/CircularProgress';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import { CreatePayloadNavigationButtons} from './CreatePayloadNavigationButtons';
@@ -8,7 +7,7 @@ import Typography from '@mui/material/Typography';
import {snackActions} from '../../utilities/Snackbar';
import {useMythicLazyQuery} from "../../utilities/useMythicLazyQuery";
import {getDefaultChoices, getDefaultValueForType, getSavedToType} from "../CreatePayload/Step2SelectPayloadType";
import {getModifiedC2Params} from "../CreatePayload/Step4C2Profiles";
import { Backdrop, CircularProgress } from '@mui/material';
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {
ConfigureBuildParameters,
@@ -73,6 +72,7 @@ query getPayloadTypesBuildParametersQuery($payload_id: Int!) {
export function Step1SelectOS(props){
const [os, setOS] = React.useState('');
const [openBackdrop, setOpenBackdrop] = React.useState(true);
const [payloadtypeData, setPayloadtypeData] = React.useState({});
const [payloadtypesPerOS, setPayloadtypesPerOS] = React.useState({});
const [osOptions, setOSOptions] = React.useState([]);
@@ -125,11 +125,16 @@ export function Step1SelectOS(props){
.catch((data) => {console.log(data)})
}, [props.prevData, props.first])
React.useEffect( () => {
setOpenBackdrop(true);
getPayloads({variables: {payloadType: selectedPayloadType, os}})
.then(({data}) => {
setPayloadOptions(data.payload);
setOpenBackdrop(false);
})
.catch(({data}) => console.log(data));
.catch(({data}) => {
console.log(data);
setOpenBackdrop(false);
});
}, [selectedPayloadType, os]);
const finished = (clearNextPrevious) => {
@@ -296,13 +301,21 @@ export function Step1SelectOS(props){
overflow: "hidden"
}}>
{props.first &&
<StartFromExistingPayloadOrStartFresh first={props.first}
last={props.last}
canceled={canceled}
onSelectedPayload={onSelectedPayload}
payloadOptions={payloadOptions}
onStartFresh={onStartFresh}
/>
<div style={{flexGrow: 1, overflowY: "auto", position: "relative"}}>
{openBackdrop &&
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2000, position: "absolute"}}>
<CircularProgress color="inherit" disableShrink />
</Backdrop>
}
<StartFromExistingPayloadOrStartFresh first={props.first}
last={props.last}
canceled={canceled}
onSelectedPayload={onSelectedPayload}
payloadOptions={payloadOptions}
onStartFresh={onStartFresh}
/>
</div>
}
{!props.first &&
<ConfigureBuildParameters os={os} selectedPayloadType={selectedPayloadType}
@@ -19,8 +19,10 @@ import InfoIconOutline from '@mui/icons-material/InfoOutlined';
import IconButton from '@mui/material/IconButton';
import {b64DecodeUnicode} from '../Callbacks/ResponseDisplay';
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {ConfigureBuildParameters, StartFromExistingPayloadOrStartFresh} from "../CreatePayload/Step1SelectOS";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import { Backdrop, CircularProgress } from '@mui/material';
import {PayloadsTableRowBuildStatus} from "../Payloads/PayloadsTableRowBuildStatus";
import {PayloadsTableRowBuildProgress} from "../Payloads/PayloadsTableRowBuildProgress";
const PREFIX = 'Step3SelectPayload';
@@ -63,6 +65,18 @@ query getWrappablePayloads($payloadType: Int!) {
description
uuid
creation_time
build_phase
payload_build_steps(order_by: {step_number: asc}) {
step_name
step_number
step_success
step_skip
start_time
end_time
step_stdout
step_stderr
id
}
filemetum {
agent_file_id
filename_text
@@ -77,6 +91,7 @@ query getWrappablePayloads($payloadType: Int!) {
export function Step3SelectPayload(props){
const [payloadOptions, setPayloadOptions] = React.useState([]);
const [openBackdrop, setOpenBackdrop] = React.useState(true);
useQuery(GET_Payload_Types, {fetchPolicy: "network-only", variables: {payloadType: props.buildOptions["payload_type_id"]},
onCompleted: (data) => {
if(data.payloadtype_by_pk.wrap_these_payload_types.length > 0){
@@ -87,6 +102,7 @@ export function Step3SelectPayload(props){
}
}
setPayloadOptions(options);
setOpenBackdrop(false);
}else{
snackActions.warning("No supported payload for that wrapper");
}
@@ -155,7 +171,12 @@ export function Step3SelectPayload(props){
<Typography variant={"p"} style={{fontWeight: 600}}>
1. Select Payload to Include
</Typography>
<div style={{flexGrow: 1, overflowY: "auto"}}>
<div style={{flexGrow: 1, overflowY: "auto", position: "relative"}}>
{openBackdrop &&
<Backdrop open={openBackdrop} onClick={()=>{setOpenBackdrop(false);}} style={{zIndex: 2000, position: "absolute"}}>
<CircularProgress color="inherit" disableShrink />
</Backdrop>
}
<PayloadSelect payloadOptions={payloadOptions} first={props.first} last={props.last}
canceled={canceled} finished={finished}/>
</div>
@@ -184,6 +205,7 @@ export function PayloadSelect(props) {
<TableCell style={{width: "6rem"}}> Select</TableCell>
<TableCell style={{width: "15rem"}}>Timestamp</TableCell>
<TableCell>File</TableCell>
<TableCell>Status</TableCell>
<TableCell>Description</TableCell>
<TableCell style={{width: "5rem"}}>Details</TableCell>
</TableRow>
@@ -218,6 +240,10 @@ export function PayloadsTableRow(props){
</TableCell>
<TableCell>{toLocalTime(props.payload.creation_time, me.user.view_utc_time)}</TableCell>
<TableCell>{b64DecodeUnicode(props.payload.filemetum.filename_text)}</TableCell>
<TableCell>
<PayloadsTableRowBuildStatus {...props.payload} />
<PayloadsTableRowBuildProgress {...props.payload} />
</TableCell>
<TableCell>{props.payload.description}</TableCell>
<TableCell>
<IconButton size="small" color="info" onClick={() => setOpenDetailedView(true)}>
@@ -4,9 +4,9 @@ import {EventFeedTable} from './EventFeedTable';
import {snackActions} from '../../utilities/Snackbar';
import {alertCount} from "../../../cache";
const GET_Event_Feed = gql`
const GET_Event_Feed_Warnings = gql`
query GetOperationEventLogs($offset: Int!, $limit: Int!, $search: String!, $level: String!, $resolved: Boolean!) {
operationeventlog(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}, resolved: {_eq: $resolved}}, order_by: {id: desc}, limit: $limit, offset: $offset) {
operationeventlog(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}, resolved: {_eq: $resolved}, warning: {_eq: true}}, order_by: {id: desc}, limit: $limit, offset: $offset) {
id
level
message
@@ -14,17 +14,18 @@ query GetOperationEventLogs($offset: Int!, $limit: Int!, $search: String!, $leve
timestamp
count
source
warning
}
operationeventlog_aggregate(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}, resolved: {_eq: $resolved}}) {
operationeventlog_aggregate(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}, resolved: {_eq: $resolved}, warning: {_eq: true}}) {
aggregate {
count
}
}
}
`;
const GET_Event_Feed_No_Resolved = gql`
const GET_Event_Feed_No_Warnings = gql`
query GetOperationEventLogs($offset: Int!, $limit: Int!, $search: String!, $level: String!) {
operationeventlog(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}}, order_by: {id: desc}, limit: $limit, offset: $offset) {
operationeventlog(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_ilike: $level}}, order_by: {id: desc}, limit: $limit, offset: $offset) {
id
level
message
@@ -32,8 +33,9 @@ query GetOperationEventLogs($offset: Int!, $limit: Int!, $search: String!, $leve
timestamp
count
source
warning
}
operationeventlog_aggregate(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_like: $level}}) {
operationeventlog_aggregate(where: {deleted: {_eq: false}, message: {_ilike: $search}, level: {_ilike: $level}}) {
aggregate {
count
}
@@ -50,6 +52,7 @@ subscription GetOperationEventLogs($fromNow: timestamp!) {
timestamp
count
source
warning
}
}
`;
@@ -63,15 +66,16 @@ mutation UpdateResolutionOperationEventLog($id: Int!, $resolved: Boolean!) {
`;
const Update_Level = gql`
mutation UpdateLevelOperationEventLog($id: Int!) {
update_operationeventlog_by_pk(pk_columns: {id: $id}, _set: {level: "warning"}) {
update_operationeventlog_by_pk(pk_columns: {id: $id}, _set: {warning: true, resolved: false}) {
id
level
warning
resolved
}
}
`;
const Update_ResolveViewableErrors = gql`
mutation UpdateResolveViewableErrorsOperationEventLog($ids: [Int]!) {
update_operationeventlog(where:{id: {_in: $ids}}, _set: {resolved: true}) {
update_operationeventlog(where:{id: {_in: $ids}, warning: {_eq: true}}, _set: {resolved: true}) {
returning{
id
resolved
@@ -81,7 +85,7 @@ mutation UpdateLevelOperationEventLog($id: Int!) {
`;
const Update_ResolveAllErrors = gql`
mutation UpdateResolveAllErrorsOperationEventLog {
update_operationeventlog(where: {level: {_eq: "warning"}, resolved: {_eq: false}}, _set: {resolved: true}) {
update_operationeventlog(where: {resolved: {_eq: false}, warning: {_eq: true}}, _set: {resolved: true}) {
returning{
id
resolved
@@ -115,11 +119,11 @@ export function EventFeed({}){
}
});
const [getMoreTasking] = useLazyQuery(GET_Event_Feed, {
const [getMoreEventFeedWithWarning] = useLazyQuery(GET_Event_Feed_Warnings, {
onError: data => {
console.error(data)
},
fetchPolicy: "network-only",
fetchPolicy: "no-cache",
onCompleted: (data) => {
snackActions.dismiss();
let tempPageData = {...pageData};
@@ -130,11 +134,11 @@ export function EventFeed({}){
setOperationEventLog(newEventLog);
}
});
const [getMoreTaskingNoResolved] = useLazyQuery(GET_Event_Feed_No_Resolved, {
const [getMoreEventFeed] = useLazyQuery(GET_Event_Feed_No_Warnings, {
onError: data => {
console.error(data)
},
fetchPolicy: "network-only",
fetchPolicy: "no-cache",
onCompleted: (data) => {
snackActions.dismiss();
let tempPageData = {...pageData};
@@ -162,7 +166,7 @@ export function EventFeed({}){
const updatedMessage = data.update_operationeventlog_by_pk;
const updatedMessages = operationeventlog.map( (log) => {
if(log.id === updatedMessage.id){
return {...log, level: updatedMessage.level};
return {...log, warning: updatedMessage.warning, resolved: updatedMessage.resolved};
}
return log;
});
@@ -230,19 +234,19 @@ export function EventFeed({}){
localLevel = "%_%";
} else if(localLevel === "warning (unresolved)"){
localResolved = false;
localLevel = "warning";
localLevel = "%_%";
} else if(localLevel === "warning (resolved)"){
localResolved = true;
localLevel = "warning";
localLevel = "%_%";
}
if(localResolved === undefined){
getMoreTaskingNoResolved({variables: {offset: (value - 1) * pageData.fetchLimit,
getMoreEventFeed({variables: {offset: (value - 1) * pageData.fetchLimit,
limit: pageData.fetchLimit,
search: localSearch,
level: localLevel
}})
} else {
getMoreTasking({variables: {offset: (value - 1) * pageData.fetchLimit,
getMoreEventFeedWithWarning({variables: {offset: (value - 1) * pageData.fetchLimit,
limit: pageData.fetchLimit,
search: localSearch,
level: localLevel,
@@ -252,13 +256,9 @@ export function EventFeed({}){
}
React.useEffect( () => {
if( alertCount() > 0){
} else {
getMoreTaskingNoResolved({variables: {offset: 0,
limit: pageData.fetchLimit, search: "%_%", level: "%_%"
}});
if( alertCount() === 0){
getMoreEventFeed({variables: {offset: 0, limit: pageData.fetchLimit, search: "%_%", level: "%_%"}});
}
}, [])
const resolveViewableErrors = useCallback( () => {
snackActions.info("Resolving Errors...");
@@ -32,7 +32,7 @@ export function EventFeedTable(props){
const [search, setSearch] = React.useState("");
const [level, setLevel] = React.useState("info");
const levelOptions = [
"All Levels", "warning (unresolved)", "warning (resolved)", "info", "debug", "api"
"All Levels", "warning (unresolved)", "warning (resolved)", "info", "debug", "api", "auth", "agent_message"
];
const handleSearchValueChange = (name, value, error) => {
@@ -33,7 +33,7 @@ const StyledListItem = styled(ListItem)((
const GetPreAdornment = ({message}) => {
const theme = useTheme();
const getColor = React.useCallback(() => {
if(message.level === "warning"){
if(message.warning){
if(message.resolved){
return theme.palette.success.main;
} else {
@@ -41,9 +41,9 @@ const GetPreAdornment = ({message}) => {
}
}
return theme.palette.info.main;
}, [theme, message.level, message.resolved]);
}, [theme, message.level, message.resolved, message.warning]);
const getSymbol = React.useCallback(() => {
if(message.level === "warning"){
if(message.warning){
if(message.resolved){
return "+";
} else {
@@ -51,7 +51,7 @@ const GetPreAdornment = ({message}) => {
}
}
return "*";
}, [theme, message.level, message.resolved]);
}, [theme, message.level, message.resolved, message.warning]);
return (
<>
<p style={{display: "inline-block", fontWeight: "bold", color: getColor(), margin: "0 0 0 0"}}>[</p>
@@ -97,7 +97,7 @@ export function EventFeedTableEvents(props){
</div>
}
/>
<EventFeedTableEventsActions id={props.id} level={props.level}
<EventFeedTableEventsActions id={props.id} level={props.level} warning={props.warning}
onUpdateResolution={props.onUpdateResolution}
onUpdateLevel={props.onUpdateLevel}
resolved={props.resolved}/>
@@ -32,9 +32,9 @@ export function EventFeedTableEventsActions(props){
</IconButton>
<Menu elevation={5} id={"eventmenu" + props.id} anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleClose} >
{
props.level === "warning" && props.resolved ? (
props.warning && props.resolved ? (
<MenuItem onClick={onUpdateResolution}>Unresolve</MenuItem>
) : ( props.level === "warning" && !props.resolved ? (
) : ( props.warning && !props.resolved ? (
<MenuItem onClick={onUpdateResolution}>Resolve</MenuItem>
) : (
<MenuItem onClick={onUpdateLevel}>Make Warning</MenuItem>
@@ -9,6 +9,7 @@ import {ResponseDisplayPlaintext} from "../Callbacks/ResponseDisplayPlaintext";
import FormControl from '@mui/material/FormControl';
import MenuItem from '@mui/material/MenuItem';
import TextField from '@mui/material/TextField';
import {UploadEventFile} from "../../MythicComponents/MythicFileUpload";
const updateEventGroupMutation = gql(`
mutation updateEventGroupMutation($eventgroup_id: Int!, $updated_config: String) {
@@ -28,7 +29,7 @@ query exportWorkflow($eventgroup_id: Int!, $include_steps: Boolean!, $output_for
}
`)
const outputFormatOptions = ["yaml", "json", "toml"];
export function EventGroupTableEditDialog({onClose, selectedEventGroup}) {
export function EventGroupTableEditDialog({onClose, selectedEventGroup, includeSteps = false}) {
const workflowRef = React.useRef("");
const [outputFormat, setOutputFormat] = React.useState("yaml");
const [fetchedData, setFetchedData] = React.useState(false);
@@ -47,6 +48,18 @@ export function EventGroupTableEditDialog({onClose, selectedEventGroup}) {
snackActions.error("Failed to update");
}
});
const submitAsFile = async (evt) => {
let blob = new Blob([workflowRef.current], { type: 'text/plain' });
let file = new File([blob], "manual_eventing.yaml", {type: "text/plain"});
let uploadStatus = await UploadEventFile(file, "New Manual Eventing Workflow");
if(!uploadStatus){
snackActions.error("Failed to upload file");
}
if(uploadStatus.status === "error"){
snackActions.error(uploadStatus.error);
}
onClose();
}
const [getExportedWorkflow] = useLazyQuery(getExportWorkflow, {
fetchPolicy: "no-cache",
onCompleted: (data) => {
@@ -66,9 +79,9 @@ export function EventGroupTableEditDialog({onClose, selectedEventGroup}) {
React.useEffect( () => {
getExportedWorkflow({variables: {
eventgroup_id: selectedEventGroup.id,
include_steps: false,
include_steps: includeSteps,
output_format: outputFormat}})
}, [outputFormat, selectedEventGroup.id]);
}, [outputFormat, selectedEventGroup.id, includeSteps]);
const onUpdateClick = () => {
UpdateEventGroupMutation({variables: {eventgroup_id: selectedEventGroup.id, updated_config: workflowRef.current}})
}
@@ -104,8 +117,8 @@ export function EventGroupTableEditDialog({onClose, selectedEventGroup}) {
<Button onClick={onClose} variant="contained" color="primary">
Close
</Button>
<Button onClick={onUpdateClick} variant="contained" color="success">
Update
<Button onClick={includeSteps ? submitAsFile : onUpdateClick} variant="contained" color="success">
{includeSteps ? "Create New Workflow": "Update"}
</Button>
</DialogActions>
</React.Fragment>
@@ -17,8 +17,7 @@ import {
} from "./EventStepRender";
import OpenInNewTwoToneIcon from '@mui/icons-material/OpenInNewTwoTone';
import PermMediaTwoToneIcon from '@mui/icons-material/PermMediaTwoTone';
import {PreviewFileMediaDialog} from "../Search/PreviewFileMedia";
import {b64DecodeUnicode} from "../Callbacks/ResponseDisplay";
import AddCircleIcon from '@mui/icons-material/AddCircle';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
@@ -34,7 +33,7 @@ import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwo
import RuleTwoToneIcon from '@mui/icons-material/RuleTwoTone';
import ChecklistRtlTwoToneIcon from '@mui/icons-material/ChecklistRtlTwoTone';
import {EventGroupTableRunAsDialog} from "./EventApprovalDialog";
import AttachmentIcon from '@mui/icons-material/Attachment';
import AttachFileIcon from '@mui/icons-material/AttachFile';
import Badge from '@mui/material/Badge';
import {EventFileManageDialog} from "./EventFileManageDialog";
import SpellcheckIcon from '@mui/icons-material/Spellcheck';
@@ -67,7 +66,7 @@ mutation eventingManualTrigger($eventgroup_id: Int!){
}
}
`)
export function EventGroupTable({selectedEventGroup, me}) {
export function EventGroupTable({selectedEventGroup, me, showInstances, showGraph}) {
const [openEventStepRender, setOpenEventStepRender] = React.useState(false);
const [openEnvView, setOpenEnvView] = React.useState(false);
const [openTriggerDataView, setOpenTriggerDataView] = React.useState(false);
@@ -302,12 +301,12 @@ export function EventGroupTable({selectedEventGroup, me}) {
<OpenInNewTwoToneIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"View Original File"}>
<MythicStyledTooltip title={"Create New Workflow From This One"}>
<IconButton onClick={() => {
setOpenFileView(true);
}} size={"small"} color={"secondary"} style={{}}>
<PermMediaTwoToneIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
<AddCircleIcon color={"success"} style={{marginLeft: "-8px", marginTop: "7px", backgroundColor: "white", borderRadius: "10px"}} fontSize={"small"} />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Manage Associated Files"}>
@@ -315,7 +314,7 @@ export function EventGroupTable({selectedEventGroup, me}) {
setOpenFileManageView(true);
}} size={"small"} variant={"outlined"} color={"secondary"} style={{}}>
<Badge badgeContent={selectedEventGroup.filemeta.length} color="secondary">
<AttachmentIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
<AttachFileIcon color={"info"} style={{cursor: "pointer", marginRight: "5px"}}/>
</Badge>
</IconButton>
</MythicStyledTooltip>
@@ -347,11 +346,11 @@ export function EventGroupTable({selectedEventGroup, me}) {
</div>
}
<RenderSteps selectedInstanceID={selectedInstanceID} selectedEventGroup={selectedEventGroup} />
<EventGroupInstances setSelectedInstance={setSelectedInstanceID}
{showGraph && <RenderSteps selectedInstanceID={selectedInstanceID} selectedEventGroup={selectedEventGroup} />}
{showInstances && <EventGroupInstances setSelectedInstance={setSelectedInstanceID}
selectedInstanceID={selectedInstanceID}
foundQueryInstanceRef={foundQueryInstanceRef}
selectedEventGroup={selectedEventGroup} me={me}/>
selectedEventGroup={selectedEventGroup} me={me}/>}
{openEventStepRender &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openEventStepRender}
onClose={() => {
@@ -374,19 +373,16 @@ export function EventGroupTable({selectedEventGroup, me}) {
/>
}
{openFileView &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openFileView}
onClose={(e) => {
<MythicDialog fullWidth={true} maxWidth="lg" open={openFileView}
onClose={() => {
setOpenFileView(false);
}}
innerDialog={<PreviewFileMediaDialog
agent_file_id={selectedEventGroup.filemetum.agent_file_id}
filename={b64DecodeUnicode(selectedEventGroup.filemetum.filename_text)}
onClose={(e) => {
setOpenFileView(false);
}}/>}
innerDialog={<EventGroupTableEditDialog onClose={() => {
setOpenFileView(false);
}} me={me} selectedEventGroup={selectedEventGroup} includeSteps={true} />}
/>
}
{openEnvView && <MythicDialog fullWidth={true} maxWidth="md" open={openEnvView}
{openEnvView && <MythicDialog fullWidth={true} maxWidth="lg" open={openEnvView}
onClose={() => {
setOpenEnvView(false);
}}
@@ -398,7 +394,7 @@ export function EventGroupTable({selectedEventGroup, me}) {
}}/>}
/>
}
{openTriggerDataView && <MythicDialog fullWidth={true} maxWidth="md" open={openTriggerDataView}
{openTriggerDataView && <MythicDialog fullWidth={true} maxWidth="lg" open={openTriggerDataView}
onClose={() => {
setOpenTriggerDataView(false);
}}
@@ -412,7 +408,7 @@ export function EventGroupTable({selectedEventGroup, me}) {
/>
}
{openFileManageView &&
<MythicDialog fullWidth={true} maxWidth="md" open={openFileManageView}
<MythicDialog fullWidth={true} maxWidth="lg" open={openFileManageView}
onClose={() => {
setOpenFileManageView(false);
}}
@@ -432,7 +428,7 @@ export function EventGroupTable({selectedEventGroup, me}) {
/>
}
{openConsumingContainerDialog &&
<MythicDialog fullWidth={true} maxWidth="md" open={openConsumingContainerDialog}
<MythicDialog fullWidth={true} maxWidth="lg" open={openConsumingContainerDialog}
onClose={() => {
setOpenConsumingContainerDialog(false);
}}
@@ -1089,10 +1089,10 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) {
</div>
</DialogTitle>
<DialogContent dividers={true} style={{height: "calc(75vh)"}}>
<Typography style={{width: "100%", padding: "10px",
<DialogContent dividers={true} style={{height: "calc(75vh)", padding: "5px"}}>
<Typography style={{width: "100%", padding: "0px", paddingLeft: "10px",
backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main}}
variant={'h5'} component={Paper} >
variant={'h6'} component={Paper} >
Action: <b>{data.eventstepinstance_by_pk.eventstep.action}</b>
</Typography>
<Paper style={{marginTop: "5px"}}>
@@ -1100,10 +1100,8 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) {
TransitionProps={{ unmountOnExit: true }} defaultExpanded={false}
onChange={() => {setExpandStepTable(!expandStepTable)}} expanded={expandStepTable}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`panel1c-stdout`}
>Original and Instance Metadata
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls={`panel1c-stdout`}>
Original and Instance Metadata
</AccordionSummary>
<AccordionDetails style={{cursor: "default"}}>
<Table style={{width: "100%", overflow: "auto", tableLayout: "fixed", }}>
@@ -1170,6 +1168,7 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) {
</AccordionDetails>
</Accordion>
</Paper>
{ data.eventstepinstance_by_pk.stdout !== "" &&
<Paper style={{marginTop: "5px"}}>
<Accordion TransitionProps={{unmountOnExit: true}} defaultExpanded={false}
onChange={() => {
@@ -1201,6 +1200,8 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) {
</AccordionDetails>
</Accordion>
</Paper>
}
{data.eventstepinstance_by_pk.stderr !== "" &&
<Paper style={{marginTop: "5px"}}>
<Accordion TransitionProps={{ unmountOnExit: true }} defaultExpanded={false}
onChange={() => {setExpandStderr(!expandStderr)}} expanded={expandStderr}
@@ -1230,7 +1231,7 @@ function EventStepInstanceDetailDialog({selectedEventStepInstance, onClose}) {
</AccordionDetails>
</Accordion>
</Paper>
}
{data.eventstepinstance_by_pk.callbacks.length > 0 &&
<>
<Paper elevation={5} style={{backgroundColor: theme.pageHeader.main, color: theme.pageHeaderText.main,marginBottom: "5px", marginTop: "10px"}} variant={"elevation"}>
@@ -0,0 +1,283 @@
import React, {useContext} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import {useMutation,useQuery, gql} from '@apollo/client';
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 {IconButton} from '@mui/material';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import DeleteIcon from '@mui/icons-material/Delete';
import {TextField} from '@mui/material';
import {snackActions} from "../../utilities/Snackbar";
import {MeContext} from "../../App";
import {EventGroupTable} from "./EventGroupTable";
const triggerManualMutation = gql(`
mutation triggerManualMutation($eventgroup_id: Int!, $env_data: jsonb){
eventingTriggerManual(eventgroup_id: $eventgroup_id, env_data: $env_data){
status
error
}
}
`);
const triggerManualBulkMutation = gql(`
mutation triggerManualBulkMutation($eventgroup_id: Int!, $env_data: jsonb, $trigger_context_type: String!, $trigger_context_ids: [Int!]!){
eventingTriggerManualBulk(eventgroup_id: $eventgroup_id, env_data: $env_data, trigger_context_type: $trigger_context_type, trigger_context_ids: $trigger_context_ids){
status
error
}
}
`);
const getActiveWorkflows = gql(`
query getActiveWorkflows {
eventgroup(where: {deleted: {_eq: false}, active: {_eq: true}, approved_to_run: {_eq: true}}, order_by: {id: desc}){
id
operator {
username
}
filemetum {
agent_file_id
id
filename_text
}
filemeta(where: {deleted: {_eq: false}}) {
agent_file_id
id
filename_text
deleted
}
name
description
trigger
trigger_data
next_scheduled_run
keywords
environment
active
deleted
created_at
run_as
approved_to_run
eventgroupapprovals(order_by: {id: asc}) {
id
operator {
id
username
}
approved
created_at
updated_at
}
eventgroupconsumingcontainers {
id
consuming_container_name
all_functions_available
function_names
consuming_container {
container_running
subscriptions
}
}
}
}
`);
export function EventTriggerContextSelectDialog({onClose, triggerContext}) {
const me = useContext(MeContext);
const [workflowOptions, setWorkflowOptions] = React.useState([]);
const [selectedEventGroup, setSelectedEventGroup] = React.useState({id: 0});
useQuery(getActiveWorkflows, {
fetchPolicy: "no-cache",
onCompleted: (data) => {
setWorkflowOptions(data.eventgroup);
if(data.eventgroup.length > 0){
setSelectedEventGroup(data.eventgroup[0]);
}
},
onError: (error) => {
console.log(error);
}
})
const [dictionaryData, setDictionaryData] = React.useState([]);
const typeOptions = ["text", "number"];
const [triggerManual] = useMutation(triggerManualMutation, {
onCompleted: (data) => {
if(data.eventingTriggerManual.status === "success"){
snackActions.success("Successfully initiated trigger");
onClose();
} else {
snackActions.error(data.eventingTriggerManual.error);
}
},
onError: (data) => {
console.log(data);
}
})
const [triggerManualBulk] = useMutation(triggerManualBulkMutation, {
onCompleted: (data) => {
if(data.eventingTriggerManualBulk.status === "success"){
snackActions.success("Successfully initiated trigger");
onClose();
} else {
snackActions.error(data.eventingTriggerManualBulk.error);
}
},
onError: (data) => {
console.log(data);
}
})
const handleWorkflowChange = (event) => {
setSelectedEventGroup(event.target.value);
}
const addRow = () => {
setDictionaryData([...dictionaryData, {
"type": "text",
"key": "",
"value": "",
}])
}
const changeRowType = (event, index) => {
let newType = event.target.value;
const newDictionaryData = dictionaryData.map( (row, i) => {
if(index === i){
return {key: row["key"], type: newType, value: newType === "number" ? 0 : ""};
}
return {...row};
});
setDictionaryData(newDictionaryData);
}
const removeRow = (index) => {
let newDictionaryData = [...dictionaryData];
newDictionaryData.splice(index, 1);
setDictionaryData(newDictionaryData);
}
const onChangeRowKey = (value, index) => {
const newDictionaryData = dictionaryData.map( (e, i) => {
if(index === i){
return {...e, key: value};
}
return {...e};
});
setDictionaryData(newDictionaryData);
}
const onChangeRowValue = (value, index) => {
const newDictionaryData = dictionaryData.map( (e, i) => {
if(index === i){
return {...e, value: value};
}
return {...e};
});
setDictionaryData(newDictionaryData);
}
const onSubmit = () => {
let keywordEnvData = dictionaryData.reduce( (prev, cur) => {
try{
if(cur.type === "number"){
prev[cur.key] = parseInt(cur.value);
} else {
prev[cur.key] = cur.value;
}
}catch(error){
prev[cur.key] = cur.value;
}
return {...prev};
}, {});
if(triggerContext.name !== undefined){
keywordEnvData[triggerContext.name] = triggerContext.value;
triggerManual({variables: {eventgroup_id: selectedEventGroup.id, env_data:keywordEnvData}});
} else if(triggerContext.trigger_context_type !== undefined){
triggerManualBulk({variables: {eventgroup_id: selectedEventGroup.id, env_data: keywordEnvData,
trigger_context_type: triggerContext.trigger_context_type,
trigger_context_ids: triggerContext.trigger_context_ids}})
} else {
snackActions.error("No trigger context name or type, can't submit");
}
}
return (
<React.Fragment>
<DialogTitle id="form-dialog-title">Trigger a workflow</DialogTitle>
<DialogContent dividers={true} style={{maxHeight: "calc(70vh)"}}>
<DialogContentText>
Trigger a workflow with selected context and optional additional data.
</DialogContentText>
<Select
style={{marginBottom: "10px", width: "100%"}}
value={selectedEventGroup}
onChange={handleWorkflowChange}
>
{
workflowOptions.map((opt, i) => (
<MenuItem key={opt.id} value={opt}>{opt.name + " - " + opt.description}</MenuItem>
))
}
</Select>
{selectedEventGroup.id !== 0 &&
<EventGroupTable me={me} selectedEventGroup={selectedEventGroup} showInstances={false} showGraph={false}/>
}
<Table>
<TableHead>
<TableRow>
<TableCell style={{width: "50%"}}>Key</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{dictionaryData.map( (e, index) => (
<TableRow key={"dictionarydata" + index}>
<MythicStyledTableCell style={{display: "flex"}} >
<IconButton color={"error"} onClick={() => removeRow(index)} >
<DeleteIcon />
</IconButton>
<Select
style={{}}
value={e.type}
onChange={(evt) => changeRowType(evt, index)}
>
{
typeOptions.map((opt, i) => (
<MenuItem key={"type" + opt} value={opt}>{opt}</MenuItem>
))
}
</Select>
<TextField value={e.key}
style={{width: "100%"}}
onChange={(evt) => onChangeRowKey(evt.target.value, index)} />
</MythicStyledTableCell>
<MythicStyledTableCell>
<TextField value={e.value}
style={{width: "100%"}}
onChange={(evt) => onChangeRowValue(evt.target.value, index)} />
</MythicStyledTableCell>
</TableRow>
))}
<TableRow>
<MythicStyledTableCell>
<Button onClick={addRow} color={"success"}>
Add Entry
</Button>
</MythicStyledTableCell>
<MythicStyledTableCell></MythicStyledTableCell>
</TableRow>
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained" color="primary">
Close
</Button>
<Button onClick={onSubmit} variant="contained" color="success">
Submit
</Button>
</DialogActions>
</React.Fragment>
);
}
@@ -126,6 +126,8 @@ export function EventTriggerKeywordDialog({onClose, selectedEventGroup}) {
<TableCell style={{width: "50%"}}>Key</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{dictionaryData.map( (e, index) => (
<TableRow key={"dictionarydata" + index}>
<MythicStyledTableCell style={{display: "flex"}} >
@@ -154,8 +156,6 @@ export function EventTriggerKeywordDialog({onClose, selectedEventGroup}) {
</MythicStyledTableCell>
</TableRow>
))}
</TableHead>
<TableBody>
<TableRow>
<MythicStyledTableCell>
<Button onClick={addRow} color={"success"}>
@@ -21,6 +21,8 @@ import {TestEventGroupFileDialog} from "./CreateEventWorkflowDialog";
import AddCircleIcon from '@mui/icons-material/AddCircle';
import Split from 'react-split';
import Paper from '@mui/material/Paper';
import {CreateEventingStepper} from "./CreateEventingStepper";
import CategoryIcon from '@mui/icons-material/Category';
const get_eventgroups = gql`
query GetEventGroups {
@@ -165,6 +167,7 @@ steps:
export function Eventing({me}){
const theme = useTheme();
const [openTestModal, setOpenTestModal] = React.useState(false);
const [openCreateEventingStepper, setOpenCreateEventingStepper] = React.useState(false);
const [eventgroups, setEventgroups] = React.useState([]);
const [showDeleted, setShowDeleted] = React.useState(false);
const [selectedEventGroup, setSelectedEventGroup] = React.useState({id: 0});
@@ -233,6 +236,11 @@ export function Eventing({me}){
}
evt.target.value = null;
}
const onCloseStepper = (e, success) => {
if(success === true){
setOpenCreateEventingStepper(false);
}
}
React.useEffect( () => {
if( !foundQueryEvent.current ){
let queryParams = new URLSearchParams(window.location.search);
@@ -267,9 +275,18 @@ export function Eventing({me}){
<Button size={"small"} color={"success"}
style={{display: "inline-flex", marginRight: "10px", marginLeft: "10px", marginTop: "5px"}}
onClick={()=>setOpenTestModal(true)}
startIcon={<AddCircleIcon />}
startIcon={<AddCircleIcon style={{backgroundColor: "white", borderRadius: "10px"}} />}
>
Create New
Text
</Button>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Create Workflow with GUI Wizard"} tooltipStyle={{}} >
<Button size={"small"} color={"success"}
style={{display: "inline-flex", marginRight: "10px", marginLeft: "10px", marginTop: "5px"}}
onClick={()=>setOpenCreateEventingStepper(true)}
startIcon={<CategoryIcon />}
>
Wizard
</Button>
</MythicStyledTooltip>
</Paper>
@@ -286,6 +303,12 @@ export function Eventing({me}){
}}/>}
/>
}
{openCreateEventingStepper &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openCreateEventingStepper}
onClose={() => setOpenCreateEventingStepper(false)}
innerDialog={<CreateEventingStepper onClose={onCloseStepper} />}
/>
}
<ListItem onClick={() => setSelectedEventGroup({id: 0})}
style={selectedEventGroup.id === 0 ?
{paddingTop: 0, paddingBottom: 0, borderLeft: `5px solid ${theme.palette.info.main}`} :
@@ -330,7 +353,7 @@ export function Eventing({me}){
</div>
<div className="bg-gray-light" style={{display: "inline-flex"}}>
<div style={{width: "100%", height: "100%"}}>
<EventGroupTable selectedEventGroup={selectedEventGroup} me={me} />
<EventGroupTable selectedEventGroup={selectedEventGroup} me={me} showInstances={true} showGraph={true} />
</div>
</div>
</Split>
@@ -17,7 +17,7 @@ import { getDefaultValueForType } from '../CreatePayload/Step2SelectPayloadType'
const GET_C2_Details = gql`
query GetPC2Details($payload_name: String!) {
c2profile(where: {name: {_eq: $payload_name}}) {
c2profileparameters(where: {deleted: {_eq: false}}) {
c2profileparameters(where: {deleted: {_eq: false}}, order_by: {name: asc}) {
default_value
description
format_string
@@ -33,6 +33,7 @@ import AttachFileIcon from '@mui/icons-material/AttachFile';
import TableRow from '@mui/material/TableRow';
import MythicTableCell from "../../MythicComponents/MythicTableCell";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
const toggleDeleteStatus = gql`
@@ -162,11 +163,17 @@ export function C2ProfilesRow({service, showDeleted}) {
)}
</MythicTableCell>
<MythicTableCell>
{service.is_p2p ?
(<FontAwesomeIcon icon={faLink} style={{width: "80px", height: "80px", padding: "10px"}} />)
:
(<WifiIcon style={{width: "80px", height: "80px", padding: "10px"}}/>)
}
{service.has_logo ? (
<>
<MythicAgentSVGIcon payload_type={service.name} is_p2p={service.is_p2p} style={{width: "80px", padding: "5px", objectFit: "unset"}} />
</>
) : (
service.is_p2p ?
(<FontAwesomeIcon icon={faLink} style={{width: "80px", height: "80px", padding: "10px"}} />)
:
(<WifiIcon style={{width: "80px", height: "80px", padding: "10px"}}/>)
)}
</MythicTableCell>
<MythicTableCell>
{service.name}
@@ -176,6 +183,11 @@ export function C2ProfilesRow({service, showDeleted}) {
<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>
@@ -264,7 +276,6 @@ export function C2ProfilesRow({service, showDeleted}) {
<br/>
<MythicStyledTooltip title={"Documentation"}>
<IconButton
color={"secondary"}
href={"/docs/c2-profiles/" + service.name.toLowerCase()}
target="_blank"
size="medium">
@@ -273,7 +284,6 @@ export function C2ProfilesRow({service, showDeleted}) {
</MythicStyledTooltip>
<MythicStyledTooltip title={"Build Parameters"}>
<IconButton
color={"secondary"}
onClick={()=>{setOpenBuildingDialog(true);}}
size="medium">
<BuildIcon />
@@ -282,14 +292,12 @@ export function C2ProfilesRow({service, showDeleted}) {
<MythicStyledTooltip title={"Save/Edit Instances for Building"}>
<IconButton
onClick={() => {setOpenProfileSavedInstancesDialog(true);}}
color={"secondary"}
size="medium">
<SaveIcon />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={service.container_running ? "View Files" : "Unable to view files because container is offline"}>
<IconButton
color={"secondary"}
disabled={!service.container_running}
onClick={()=>{setOpenListFilesDialog(true);}}
size="medium">
@@ -22,7 +22,7 @@ import PermIdentityTwoToneIcon from '@mui/icons-material/PermIdentityTwoTone';
import {MythicDialog} from "../../MythicComponents/MythicDialog";
import AttachFileIcon from '@mui/icons-material/AttachFile';
import {ConsumingServicesGetIDPMetadataDialog} from "./ConsumingServicesGetIDPMetadataDialog";
import {C2ProfileListFilesDialog} from "../PayloadTypesC2Profiles/C2ProfileListFilesDialog";
import {C2ProfileListFilesDialog} from "./C2ProfileListFilesDialog";
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import VisibilityIcon from '@mui/icons-material/Visibility';
@@ -207,7 +207,6 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="medium">
@@ -276,7 +275,6 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="medium">
@@ -345,7 +343,6 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="medium">
@@ -419,7 +416,6 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
<MythicTableCell>
<MythicStyledTooltip title={w.container_running ? "View Files" : "Unable to view files since container is offline"}>
<IconButton
color={"secondary"}
disabled={!w.container_running}
onClick={()=>{onOpenListFilesDialog(w.name);}}
size="medium">
@@ -431,7 +427,8 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
{w.subscriptions.map(s => (
<Typography key={s.name + s.type + w.name} style={{display: "block"}}>
<MythicStyledTooltip title={"Fetch Container Metadata"} >
<IconButton onClick={() => getIDPMetadata(w.name, s.name)} >
<IconButton onClick={() => getIDPMetadata(w.name, s.name)}
disabled={!w.container_running}>
<PermIdentityTwoToneIcon />
</IconButton>
</MythicStyledTooltip>
@@ -17,7 +17,7 @@ import { getDefaultValueForType } from '../CreatePayload/Step2SelectPayloadType'
const GET_Payload_Details = gql`
query GetPayloadDetails($payload_name: String!) {
payloadtype(where: {name: {_eq: $payload_name}}) {
buildparameters (where: {deleted: {_eq: false} } ){
buildparameters (where: {deleted: {_eq: false}}, order_by: {name: asc} ){
description
name
id
@@ -97,6 +97,11 @@ export function PayloadTypeRow({service, showDeleted}){
<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}
@@ -114,7 +119,6 @@ export function PayloadTypeRow({service, showDeleted}){
<MythicTableCell>
<MythicStyledTooltip title={"Documentation"}>
<IconButton
color="secondary"
href={service.wrapper ? "/docs/wrappers/" + service.name : "/docs/agents/" + service.name}
target="_blank"
size="medium">
@@ -124,7 +128,6 @@ export function PayloadTypeRow({service, showDeleted}){
<MythicStyledTooltip title={"Build Parameters"}>
<IconButton
onClick={()=>{setOpenBuildingDialog(true);}}
color="secondary"
size="medium">
<BuildIcon />
</IconButton>
@@ -132,14 +135,12 @@ export function PayloadTypeRow({service, showDeleted}){
<MythicStyledTooltip title={"Commands"}>
<IconButton
onClick={()=>{setOpenCommandsDialog(true)}}
color={"secondary"}
size={"medium"}>
<SendTwoToneIcon />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={service.container_running ? "View Files" : "Unable to view files because container is offline"}>
<IconButton
color={"secondary"}
disabled={!service.container_running}
onClick={()=>{setOpenListFilesDialog(true);}}
size="medium">
@@ -79,7 +79,6 @@ export function PayloadTypeCommandDialog({service, payload_name, onClose}) {
<TableCell>{param.needs_admin ? "True" : "False"}</TableCell>
<TableCell>
<IconButton
color="secondary"
href={service.wrapper ? "/docs/wrappers/" + service.name : "/docs/agents/" + service.name}
target="_blank"
size="large">
@@ -12,7 +12,7 @@ import {TranslationContainerRow} from "./TranslationContainerCard";
import AppBar from '@mui/material/AppBar';
import Tabs from '@mui/material/Tabs';
import {MythicSearchTabLabel, MythicTabPanel} from "../../MythicComponents/MythicTabPanel";
import {ConsumingServicesTableRow} from '../ConsumingServices/ConsumingServicesTable';
import {ConsumingServicesTableRow} from './ConsumingServicesTable';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
@@ -33,6 +33,7 @@ const SUB_Payload_Types = gql`
supported_os
wrapper
agent_type
semver
translationcontainer {
id
name
@@ -58,6 +59,8 @@ subscription getPayloadTypesSubscription {
name
running
deleted
semver
has_logo
payloadtypec2profiles(order_by: {payloadtype: {name: asc}}) {
payloadtype {
name
@@ -102,7 +102,6 @@ export function TranslationContainerRow({service, showDeleted}) {
<MythicTableCell>
<MythicStyledTooltip title={"Documentation"}>
<IconButton
color={"secondary"}
href={"/docs/c2-profiles/" + service.name.toLowerCase()}
target="_blank"
size="medium">
@@ -111,7 +110,6 @@ export function TranslationContainerRow({service, showDeleted}) {
</MythicStyledTooltip>
<MythicStyledTooltip title={service.container_running ? "View Files" : "Unable to view files because container is offline"}>
<IconButton
color={"secondary"}
disabled={!service.container_running}
onClick={()=>{setOpenListFilesDialog(true);}}
size="medium">
@@ -134,6 +134,7 @@ export function Payloads(props){
"showAutogenerated": false,
});
const mountedRef = React.useRef(true);
const [openBackdrop, setOpenBackdrop] = React.useState(true);
useSubscription(SUB_Payloads, {
variables: {now: fromNow},
fetchPolicy: "no-cache",
@@ -171,6 +172,7 @@ export function Payloads(props){
tempPageData.totalCount = data.payload_aggregate.aggregate.count;
setPageData(tempPageData);
setPayloads(data.payload);
setOpenBackdrop(false);
},
onError: (data) => {
@@ -183,6 +185,7 @@ export function Payloads(props){
tempPageData.totalCount = data.payload_aggregate.aggregate.count;
setPageData(tempPageData);
setPayloads(data.payload);
setOpenBackdrop(false);
},
onError: (data) => {
@@ -190,6 +193,7 @@ export function Payloads(props){
})
const onChangePage = (event, value) => {
snackActions.info("Fetching page...");
setOpenBackdrop(true);
fetchNewPage({variables: {offset: (value - 1) * pageData.fetchLimit, limit: pageData.fetchLimit, showDeleted: pageData.showDeleted, showAutogenerated: pageData.showAutogenerated}})
}
const [deletePayload] = useMutation(payloadsDelete, {
@@ -319,8 +323,10 @@ export function Payloads(props){
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<div style={{display: "flex", height: "100%", flexDirection: "column"}}>
<PayloadsTable pageData={pageData} me={props.me}
<div style={{display: "flex", height: "100%", flexDirection: "column", position: "relative"}}>
<PayloadsTable pageData={pageData} me={props.me}
openBackdrop={openBackdrop}
onDeletePayload={onDeletePayload}
onUpdateCallbackAlert={onUpdateCallbackAlert}
payload={payloads}
@@ -20,11 +20,12 @@ import MenuList from '@mui/material/MenuList';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import {useNavigate} from 'react-router-dom';
import Pagination from '@mui/material/Pagination';
import { Backdrop, CircularProgress } from '@mui/material';
export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert, onRestorePayload, me,
pageData, onChangePage, onChangeShowDeleted, onChangeShowAutogenerated,
onCallbacksAllowedChanged}){
onCallbacksAllowedChanged, openBackdrop}){
const theme = useTheme();
const [showDeleted, setShowDeleted] = React.useState(false);
const [showAutogenerated, setShowAutogenerated] = React.useState(false);
@@ -118,8 +119,12 @@ export function PayloadsTable({payload, onDeletePayload, onUpdateCallbackAlert,
/>
}
</Paper>
<div style={{flexGrow: 1, overflowY: "auto", height: "100%"}}>
<div style={{flexGrow: 1, overflowY: "auto", height: "100%", position: "relative"}}>
{openBackdrop &&
<Backdrop open={openBackdrop} style={{zIndex: 2000, position: "absolute"}}>
<CircularProgress color="inherit" disableShrink />
</Backdrop>
}
<Table stickyHeader size="small" style={{ "maxWidth": "100%", "overflow": "auto", tableLayout: "fixed"}}>
<TableHead >
<TableRow>
@@ -238,14 +238,14 @@ export function FileMetaDownloadTable(props){
return (
<TableContainer className="mythicElement" style={{display: "flex", flexDirection: "column", height: "100%"}} >
<span style={{width: "100%", display: "flex"}}>
<Button size="small" onClick={onDownloadBulk} style={{float: "right", width: "100%"}}
color="primary" variant="contained"
<Button size="small" onClick={onDownloadBulk} style={{float: "right", width: "100%", marginLeft: "10px", marginRight: "10px"}}
color="primary" variant={disabled ? 'outlined': "contained"}
disabled={disabled}
>
<ArchiveIcon/>Zip & Download Selected
</Button>
<Button size="small" onClick={onDeleteBulk} style={{width: "100%"}}
color="error" variant="contained"
<Button size="small" onClick={onDeleteBulk} style={{width: "100%", marginLeft: "10px", marginRight: "10px"}}
color="error" variant={disabled ? 'outlined': "contained"}
disabled={disabled}
>
<DeleteIcon />Delete Selected
@@ -617,14 +617,14 @@ export function FileMetaUploadTable(props){
return (
<TableContainer className="mythicElement" style={{display: "flex", flexDirection: "column", height: "100%"}} >
<span style={{width: "100%", display: "flex"}}>
<Button size="small" onClick={onDownloadBulk} style={{float: "right", width: "100%"}}
color="primary" variant="contained"
<Button size="small" onClick={onDownloadBulk} style={{float: "right", width: "100%", marginLeft: "10px", marginRight: "10px"}}
color="primary" variant={disabled ? 'outlined': "contained"}
disabled={disabled}
>
<ArchiveIcon/>Zip & Download Selected
</Button>
<Button size="small" onClick={onDeleteBulk} style={{width: "100%"}}
color="error" variant="contained"
<Button size="small" onClick={onDeleteBulk} style={{width: "100%", marginLeft: "10px", marginRight: "10px"}}
color="error" variant={disabled ? 'outlined': "contained"}
disabled={disabled}
>
<DeleteIcon />Delete Selected
@@ -1195,14 +1195,14 @@ export function FileMetaEventingWorkflowsTable(props){
return (
<TableContainer className="mythicElement" style={{display: "flex", flexDirection: "column", height: "100%"}} >
<span style={{width: "100%", display: "flex"}}>
<Button size="small" onClick={onDownloadBulk} style={{float: "right", width: "100%"}}
color="primary" variant="contained"
<Button size="small" onClick={onDownloadBulk} style={{float: "right", width: "100%", marginLeft: "10px", marginRight: "10px"}}
color="primary" variant={disabled ? 'outlined': "contained"}
disabled={disabled}
>
<ArchiveIcon/>Zip & Download Selected
</Button>
<Button size="small" onClick={onDeleteBulk} style={{width: "100%"}}
color="error" variant="contained"
<Button size="small" onClick={onDeleteBulk} style={{width: "100%", marginLeft: "10px", marginRight: "10px"}}
color="error" variant={disabled ? 'outlined': "contained"}
disabled={disabled}
>
<DeleteIcon />Delete Selected
@@ -46,6 +46,7 @@ mutation createAPITokenMutation($operator_id: Int, $name: String){
operator_id
name
created_by
creation_time
}
}
`;
@@ -70,9 +71,10 @@ mutation toggleAPITokenActiveMutation($id: Int!, $active: Boolean!){
`;
const GetAPITokens = gql`
query getUserAPITokens($operator_id: Int!){
apitokens(where: {operator_id: {_eq: $operator_id}}) {
apitokens(where: {operator_id: {_eq: $operator_id}}, order_by: {id: desc}) {
token_value
token_type
creation_time
active
name
deleted
@@ -227,7 +229,7 @@ export function SettingsOperatorTableRow(props){
<MythicDialog open={openDelete}
onClose={()=>{setOpenDeleteDialog(false);}}
innerDialog={<SettingsOperatorDeleteDialog onClose={()=>{setOpenDeleteDialog(false);}} onAccept={onAcceptDelete} {...props} />}
/>
/>
}
</MythicStyledTableCell>
@@ -353,44 +355,43 @@ export function SettingsOperatorTableRow(props){
</MythicStyledTableCell>
</TableRow>
<TableRow>
{((props.id === me.user.id) || (props.userIsAdmin && props.account_type === "bot")) &&
<MythicStyledTableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={10}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box margin={1}>
<Typography variant="h6" gutterBottom component="div" style={{display: "inline-block"}}>
API Tokens
</Typography>
{showDeleted ? (
<MythicStyledTooltip title={"Hide API Tokens"} tooltipStyle={{float: "right"}}>
<IconButton size="small" style={{float: "right"}} variant="contained" onClick={() => setShowDeleted(!showDeleted)}><VisibilityIcon /></IconButton>
</MythicStyledTooltip>
{((props.id === me.user.id) || (props.userIsAdmin && props.account_type === "bot")) && open &&
<TableRow>
<MythicStyledTableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={10}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box margin={1}>
<Typography variant="h6" gutterBottom component="div" style={{display: "inline-block"}}>
API Tokens
</Typography>
{showDeleted ? (
<MythicStyledTooltip title={"Hide API Tokens"} tooltipStyle={{float: "right"}}>
<IconButton size="small" style={{float: "right"}} variant="contained" onClick={() => setShowDeleted(!showDeleted)}><VisibilityIcon /></IconButton>
</MythicStyledTooltip>
) : (
<MythicStyledTooltip title={"Show Deleted API Tokens"} tooltipStyle={{float: "right"}}>
<IconButton size="small" style={{float: "right"}} variant="contained" onClick={() => setShowDeleted(!showDeleted)} ><VisibilityOffIcon /></IconButton>
</MythicStyledTooltip>
)}
) : (
<MythicStyledTooltip title={"Show Deleted API Tokens"} tooltipStyle={{float: "right"}}>
<IconButton size="small" style={{float: "right"}} variant="contained" onClick={() => setShowDeleted(!showDeleted)} ><VisibilityOffIcon /></IconButton>
</MythicStyledTooltip>
)}
<Button size="small" onClick={() => {setOpenNewAPIToken(true)}} style={{marginRight: "20px", float: "right"}}
variant={"contained"}
startIcon={<AddCircleOutlineOutlinedIcon color="success" />} >
API Token
</Button>
{openNewAPIToken &&
<MythicDialog open={openNewAPIToken}
fullWidth={true}
onClose={()=>{setOpenNewAPIToken(false);}}
innerDialog={<SettingsAPITokenDialog title="New API Token Name" onAccept={onSubmitNewAPIToken} handleClose={()=>{setOpenNewAPIToken(false);}} {...props}/>}
/>
}
<APITokens apiTokens={apiTokens} onDeleteAPIToken={onDeleteAPIToken} onToggleActive={onToggleActive} showDeleted={showDeleted} />
</Box>
</Collapse>
</MythicStyledTableCell>
<Button size="small" onClick={() => {setOpenNewAPIToken(true)}} style={{marginRight: "20px", float: "right"}}
variant={"contained"}
startIcon={<AddCircleOutlineOutlinedIcon color="success" />} >
API Token
</Button>
{openNewAPIToken &&
<MythicDialog open={openNewAPIToken}
fullWidth={true}
onClose={()=>{setOpenNewAPIToken(false);}}
innerDialog={<SettingsAPITokenDialog title="New API Token Name" onAccept={onSubmitNewAPIToken} handleClose={()=>{setOpenNewAPIToken(false);}} {...props}/>}
/>
}
<APITokens me={me} apiTokens={apiTokens} onDeleteAPIToken={onDeleteAPIToken} onToggleActive={onToggleActive} showDeleted={showDeleted} />
</Box>
</Collapse>
</MythicStyledTableCell>
</TableRow>
}
</TableRow>
</React.Fragment>
)
}
@@ -429,6 +430,16 @@ const columns = [
headerName: 'Created By',
flex: 1,
valueGetter: (value, row) => row.created_by_operator?.username,
renderCell: (params) => (
<>
<Typography>
{params.row?.created_by_operator?.username}
</Typography>
<Typography style={{fontSize: params.row?.theme?.typography.pxToRem(12),}}>
Created at: {toLocalTime(params.row?.creation_time, params.row.me?.user?.view_utc_time )}
</Typography>
</>
)
},
{
field: 'token',
@@ -479,7 +490,7 @@ const columns = [
}
},
];
const APITokens = ({apiTokens, onDeleteAPIToken, onToggleActive, showDeleted}) => {
const APITokens = ({apiTokens, onDeleteAPIToken, onToggleActive, showDeleted, me}) => {
const onCopyTokenValue = (token_value) => {
let success = copyStringToClipboard(token_value);
if(success){
@@ -488,12 +499,14 @@ const APITokens = ({apiTokens, onDeleteAPIToken, onToggleActive, showDeleted}) =
snackActions.error("failed to copy token to clipboard");
}
}
const theme = useTheme();
const [data, setData] = React.useState([]);
React.useEffect( () => {
setData(apiTokens.reduce( (prev, c) => {
if(showDeleted || (!showDeleted && !c.deleted)){
return [...prev, {...c, onCopyTokenValue: onCopyTokenValue,
onDeleteAPIToken: onDeleteAPIToken, onToggleActive: onToggleActive,
me: me, theme: theme
}];
}
return [...prev];
@@ -425,12 +425,7 @@ export function SettingsOperatorUIConfigDialog(props) {
<TableContainer className="mythicElement" style={{paddingLeft: "10px", paddingRight: "10px"}}>
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
<TableBody>
<TableRow hover>
<MythicStyledTableCell style={{width: "30%"}}>Font Size</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicTextField type="number" value={fontSize} onChange={onChangeFontSize} showLabel={false} />
</MythicStyledTableCell>
</TableRow>
<TableRow hover>
<MythicStyledTableCell>Font Family</MythicStyledTableCell>
<MythicStyledTableCell>
@@ -130,7 +130,7 @@ const stillDoNotDisturb = () => {
}
const ToastComponent = ({msg, closeButton}) => {
return (
<div style={{display: "flex", justifyContent: "space-between", alignItems: "center"}}>
<div style={{display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%"}}>
{msg}
{closeButton}
</div>
-20
View File
@@ -1,20 +0,0 @@
#FROM itsafeaturemythic/mythic_go_base:latest
FROM golang:1.21-alpine
WORKDIR /usr/src/app
ARG GOPROXY=proxy.golang.org
ARG GO111MODULE
RUN go env -w GOPROXY=${GOPROXY}
RUN go env -w GO111MODULE=${GO111MODULE}
COPY ["src/", "."]
RUN apk add --no-cache make
RUN make build_all
FROM alpine
COPY --from=0 /usr/src/app/mythic-cli_linux /mythic-cli_linux
COPY --from=0 /usr/src/app/mythic-cli_macos /mythic-cli_macos
+6
View File
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## 0.3.18 - 2025-09-29
### Changed
- Updated uninstall to also remove associated .env values
## 0.3.17 - 2025-07-16
### Changed
+27 -2
View File
@@ -356,7 +356,7 @@ Setting this to "true" means that the local Mythic/hasura-docker/Dockerfile is u
mythicEnv.SetDefault("COMPOSE_BAKE", true)
mythicEnvInfo["COMPOSE_BAKE"] = `This is a newer option in Docker Compose to delegate builds to bake for better build performance`
mythicEnv.SetDefault("REBUILD_ON_START", false)
mythicEnvInfo["rebuild_on_start"] = `This identifies if a container's backing image should be re-built (or re-fetched) each time you start the container.
This can cause agent and c2 profile containers to have their volumes wiped on each start (and thus deleting any changes).
@@ -537,7 +537,11 @@ func writeMythicEnvironmentVariables() {
if len(mythicEnv.GetString(key)) == 0 {
_, err = f.WriteString(fmt.Sprintf("%s=\n", strings.ToUpper(key)))
} else {
_, err = f.WriteString(fmt.Sprintf("%s=\"%s\"\n", strings.ToUpper(key), mythicEnv.GetString(key)))
value := mythicEnv.GetString(key)
value = strings.ReplaceAll(value, "\\", "\\\\")
value = strings.ReplaceAll(value, "'", "\\'")
value = strings.ReplaceAll(value, "\"", "\\\"")
_, err = f.WriteString(fmt.Sprintf("%s=\"%s\"\n", strings.ToUpper(key), value))
}
if err != nil {
@@ -575,6 +579,27 @@ func GetConfigStrings(args []string) map[string]string {
}
return resultMap
}
func RemoveConfigStrings(args []string) error {
allSettings := mythicEnv.AllKeys()
mythicStruct := mythicEnv.AllSettings()
for i := 0; i < len(args[0:]); i++ {
searchRegex, err := regexp.Compile(args[i])
if err != nil {
log.Fatalf("[!] bad regex: %v", err)
}
for _, setting := range allSettings {
if searchRegex.MatchString(strings.ToUpper(setting)) || searchRegex.MatchString(strings.ToLower(setting)) {
mythicStruct[setting] = nil
}
}
}
err := mythicEnv.MergeConfigMap(mythicStruct)
if err != nil {
return err
}
writeMythicEnvironmentVariables()
return nil
}
func SetConfigStrings(key string, value string) {
allSettings := mythicEnv.AllKeys()
searchRegex, err := regexp.Compile(key)
+1 -1
View File
@@ -4,5 +4,5 @@ package config
var (
// Version Mythic CLI version
Version = "v0.3.18"
Version = "v0.3.19"
)
+12 -1
View File
@@ -488,7 +488,18 @@ func UninstallService(services []string) {
}
if found {
log.Printf("[+] Successfully Uninstalled %s\n", service)
log.Printf("[*] Removing associated .ENV settings...\n", service)
err := config.RemoveConfigStrings([]string{
fmt.Sprintf("%s_remote_image", service),
fmt.Sprintf("%s_use_volume", service),
fmt.Sprintf("%s_use_build_context", service),
fmt.Sprintf("%s_install_location", service),
})
if err != nil {
log.Printf("[-] Failed to remove .ENV settings: %v\n", err)
} else {
log.Printf("[+] Successfully Uninstalled %s\n", service)
}
if manager.GetManager().IsServiceRunning("mythic_documentation") {
log.Printf("[*] Restarting mythic_documentation container to pull in changes\n")
ServiceStop([]string{"mythic_documentation"}, true)
+2 -2
View File
@@ -27,7 +27,7 @@ func DatabaseReset(force bool) {
}
func RabbitmqReset(force bool, startAgain bool) {
if force {
manager.GetManager().StopServices([]string{"mythic_rabbitmq"}, config.GetMythicEnv().GetBool("REBUILD_ON_START"), false)
manager.GetManager().RemoveContainers([]string{"mythic_rabbitmq"}, false)
log.Printf("[*] Removing rabbitmq storage files\n")
manager.GetManager().ResetRabbitmq(config.GetMythicEnv().GetBool("rabbitmq_use_volume"))
if startAgain {
@@ -37,7 +37,7 @@ func RabbitmqReset(force bool, startAgain bool) {
}
confirm := config.AskConfirm("Are you sure you want to reset the rabbitmq storage? ")
if confirm {
manager.GetManager().StopServices([]string{"mythic_rabbitmq"}, config.GetMythicEnv().GetBool("REBUILD_ON_START"), false)
manager.GetManager().RemoveContainers([]string{"mythic_rabbitmq"}, false)
log.Printf("[*] Removing rabbitmq storage files\n")
manager.GetManager().ResetRabbitmq(config.GetMythicEnv().GetBool("rabbitmq_use_volume"))
if startAgain {
@@ -936,11 +936,14 @@ func (d *DockerComposeManager) ResetRabbitmq(useVolume bool) {
if !useVolume {
workingPath := utils.GetCwdFromExe()
err := os.RemoveAll(filepath.Join(workingPath, "rabbitmq-docker", "storage"))
if err != nil {
if err != nil && !os.IsNotExist(err) {
log.Fatalf("[-] Failed to remove rabbitmq storage files\n%v\n", err)
} else {
log.Printf("[+] Successfully reset rabbitmq storage files\n")
}
err = os.Remove(filepath.Join(workingPath, "rabbitmq-docker", "storage"))
if err != nil && !os.IsNotExist(err) {
log.Fatalf("[-] Failed to remove rabbitmq storage files\n%v\n", err)
}
log.Printf("[+] Successfully reset rabbitmq storage files\n")
} else {
_ = d.RemoveContainers([]string{"mythic_rabbitmq"}, false)
err := d.RemoveVolume("mythic_rabbitmq_volume")
+2 -2
View File
@@ -16,7 +16,6 @@ var rabbitmqResetCmd = &cobra.Command{
var startAgain bool
func init() {
rabbitmqCmd.AddCommand(rabbitmqResetCmd)
rabbitmqResetCmd.Flags().BoolVarP(
&force,
"force",
@@ -28,9 +27,10 @@ func init() {
&startAgain,
"start-again",
"s",
true,
false,
`Start the container again after making changes`,
)
rabbitmqCmd.AddCommand(rabbitmqResetCmd)
}
func rabbitmqReset(cmd *cobra.Command, args []string) {
+1 -1
View File
@@ -1 +1 @@
3.3.1-rc94
3.4.0
-9
View File
@@ -1,9 +0,0 @@
FROM klakegg/hugo:latest
COPY [".", "/src/"]
HEALTHCHECK --interval=30s --timeout=10s --retries=5 --start-period=10s \
CMD wget -nv -t1 -O /dev/null http://127.0.0.1:${DOCUMENTATION_PORT:-8090}/docs/ || exit 1
RUN hugo new site mythic_docs
ENTRYPOINT hugo server -p ${DOCUMENTATION_PORT:-8090}
+7 -1
View File
@@ -1,3 +1,9 @@
FROM klakegg/hugo:latest
FROM ghcr.io/its-a-feature/mythic_documentation:v3.3.0.136
COPY [".", "/src/"]
HEALTHCHECK --interval=30s --timeout=10s --retries=5 --start-period=10s \
CMD wget -nv -t1 -O /dev/null http://127.0.0.1:${DOCUMENTATION_PORT:-8090}/docs/ || exit 1
RUN hugo new site mythic_docs
ENTRYPOINT hugo server -p ${DOCUMENTATION_PORT:-8090}
-4
View File
@@ -1,4 +0,0 @@
FROM hasura/graphql-engine:latest.cli-migrations-v2
ENV HASURA_GRAPHQL_SERVER_PORT=8080
HEALTHCHECK CMD curl -f http://localhost:${HASURA_GRAPHQL_SERVER_PORT}/healthz || exit 1
COPY ["metadata/", "/metadata"]
+15
View File
@@ -304,9 +304,19 @@ type Mutation {
type Mutation {
eventingTriggerManual(
eventgroup_id: Int!
env_data: jsonb
): eventingTriggerManualOutput
}
type Mutation {
eventingTriggerManualBulk(
eventgroup_id: Int!
env_data: jsonb
trigger_context_type: String!
trigger_context_ids: [Int!]!
): eventingTriggerManualBulkOutput
}
type Mutation {
eventingTriggerRetry(
eventgroupinstance_id: Int!
@@ -1236,3 +1246,8 @@ type updatePayloadOutput {
payload_uuid: String
}
type eventingTriggerManualBulkOutput {
status: String!
error: String
}
+11
View File
@@ -406,6 +406,16 @@ actions:
- role: operation_admin
- role: mythic_admin
- role: developer
- name: eventingTriggerManualBulk
definition:
kind: synchronous
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/eventing_trigger_manual_bulk_webhook'
forward_client_headers: true
permissions:
- role: operator
- role: operation_admin
- role: mythic_admin
- role: developer
- name: eventingTriggerRetry
definition:
kind: synchronous
@@ -934,4 +944,5 @@ custom_types:
- name: dynamicQueryOutputChoice
- name: createArtifactOutput
- name: updatePayloadOutput
- name: eventingTriggerManualBulkOutput
scalars: []
+73 -31
View File
@@ -1185,6 +1185,7 @@
columns:
- container_running
- deleted
- has_logo
- is_p2p
- is_server_routed
- running
@@ -1192,27 +1193,31 @@
- author
- description
- name
- semver
- creation_time
filter: {}
- role: mythic_admin
permission:
columns:
- id
- name
- description
- creation_time
- running
- container_running
- author
- deleted
- has_logo
- is_p2p
- is_server_routed
- deleted
- running
- id
- author
- description
- name
- semver
- creation_time
filter: {}
- role: operation_admin
permission:
columns:
- container_running
- deleted
- has_logo
- is_p2p
- is_server_routed
- running
@@ -1220,6 +1225,7 @@
- author
- description
- name
- semver
- creation_time
filter: {}
- role: operator
@@ -1227,6 +1233,7 @@
columns:
- container_running
- deleted
- has_logo
- is_p2p
- is_server_routed
- running
@@ -1234,6 +1241,7 @@
- author
- description
- name
- semver
- creation_time
filter: {}
- role: spectator
@@ -1241,6 +1249,7 @@
columns:
- container_running
- deleted
- has_logo
- is_p2p
- is_server_routed
- running
@@ -1248,6 +1257,7 @@
- author
- description
- name
- semver
- creation_time
filter: {}
update_permissions:
@@ -2751,6 +2761,7 @@
- subscriptions
- description
- name
- semver
- type
- created_at
- updated_at
@@ -2765,6 +2776,7 @@
- subscriptions
- description
- name
- semver
- type
- created_at
- updated_at
@@ -2779,6 +2791,7 @@
- subscriptions
- description
- name
- semver
- type
- created_at
- updated_at
@@ -2793,6 +2806,7 @@
- subscriptions
- description
- name
- semver
- type
- created_at
- updated_at
@@ -2807,6 +2821,7 @@
- subscriptions
- description
- name
- semver
- type
- created_at
- updated_at
@@ -5383,6 +5398,7 @@
- operator_id
- resolved
- source
- warning
comment: ""
- role: mythic_admin
permission:
@@ -5399,6 +5415,7 @@
- operator_id
- resolved
- source
- warning
comment: ""
- role: operation_admin
permission:
@@ -5415,6 +5432,7 @@
- operator_id
- resolved
- source
- warning
comment: ""
- role: operator
permission:
@@ -5431,22 +5449,33 @@
- operator_id
- resolved
- source
- warning
comment: ""
- role: spectator
permission:
check: {}
set:
operation_id: x-hasura-current-operation-id
operator_id: x-hasura-user-id
columns:
- message
comment: ""
select_permissions:
- role: developer
permission:
columns:
- deleted
- resolved
- apitokens_id
- count
- deleted
- id
- operation_id
- operator_id
- level
- message
- operation_id
- operator_id
- resolved
- source
- timestamp
- warning
filter:
operation_id:
_eq: X-Hasura-current-operation-id
@@ -5456,6 +5485,7 @@
columns:
- deleted
- resolved
- warning
- apitokens_id
- count
- id
@@ -5474,6 +5504,7 @@
columns:
- deleted
- resolved
- warning
- apitokens_id
- count
- id
@@ -5492,6 +5523,7 @@
columns:
- deleted
- resolved
- warning
- apitokens_id
- count
- id
@@ -5510,6 +5542,7 @@
columns:
- deleted
- resolved
- warning
- apitokens_id
- count
- id
@@ -5530,6 +5563,7 @@
- deleted
- level
- resolved
- warning
filter:
operation_id:
_in: x-hasura-operations
@@ -5540,6 +5574,7 @@
- deleted
- level
- resolved
- warning
filter:
operation_id:
_in: x-hasura-operations
@@ -5550,6 +5585,7 @@
- deleted
- level
- resolved
- warning
filter:
operation_id:
_in: x-hasura-operations
@@ -5560,6 +5596,7 @@
- deleted
- level
- resolved
- warning
filter:
operation_id:
_in: x-hasura-operations
@@ -8998,52 +9035,57 @@
- role: developer
permission:
columns:
- author
- container_running
- deleted
- description
- id
- name
- deleted
- container_running
- description
- author
- semver
filter: {}
- role: mythic_admin
permission:
columns:
- author
- container_running
- deleted
- description
- id
- name
- deleted
- container_running
- description
- author
- semver
filter: {}
- role: operation_admin
permission:
columns:
- author
- container_running
- deleted
- description
- id
- name
- deleted
- container_running
- description
- author
- semver
filter: {}
- role: operator
permission:
columns:
- author
- container_running
- deleted
- description
- id
- name
- deleted
- container_running
- description
- author
- semver
filter: {}
- role: spectator
permission:
columns:
- author
- container_running
- deleted
- description
- id
- name
- deleted
- container_running
- description
- author
- semver
filter: {}
update_permissions:
- role: developer
-20
View File
@@ -1,20 +0,0 @@
FROM jupyter/minimal-notebook:python-3.11
# for offline builds, add ability for custom PIP repositories
ARG PIP_INDEX
ARG PIP_INDEX_URL
ARG PIP_TRUSTED_HOST
RUN pip3 install mythic==0.2.8
WORKDIR /projects
#CMD start.sh jupyter lab --ServerApp.open_browser=false --IdentityProvider.token='' --ServerApp.base_url="/jupyter" --ServerApp.default_url="/jupyter"
ENV JUPYTERHUB_SERVICE_PREFIX "/jupyter/"
ENV JUPYTER_TOKEN "mythic"
COPY ["jupyter/", "."]
CMD start.sh jupyter lab --ServerApp.open_browser=false --IdentityProvider.token=${JUPYTER_TOKEN:-mythic} --ServerApp.base_url="/jupyter" --ServerApp.default_url="/jupyter"
# sudo docker run -p 8888:8888 -v `pwd`/jupyter:/projects jupyter
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.23 AS builder
FROM golang:1.25 AS builder
WORKDIR /usr/src/app
+1 -1
View File
@@ -1,6 +1,6 @@
#FROM ghcr.io/its-a-feature/mythic_server:v0.0.3.8
FROM golang:1.23 AS builder
FROM golang:1.25 AS builder
WORKDIR /usr/src/app
@@ -23,7 +23,7 @@ func ValidateLogin(username string, password string, scriptingVersion string, fr
FROM operator
WHERE username=$1`, username)
if err != nil {
logging.LogError(err, "Failed to find username", "username", user.Username)
logging.LogError(err, "Failed to find username", "username", username)
return "", "", 0, err
}
if user.AccountType != databaseStructs.AccountTypeUser {
@@ -33,14 +33,14 @@ func ValidateLogin(username string, password string, scriptingVersion string, fr
err = errors.New("attempted log in with a deleted user")
logging.LogError(err, "username", user.Username)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Attempted to log in with deleted user, %s, from %s", user.Username, fromIP),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_AUTH, true)
return "", "", 0, err
}
if !user.Active {
err = errors.New("Attempted log in with an inactive user")
logging.LogError(err, "username", user.Username)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Attempted to log in with inactive user, %s, from %s", user.Username, fromIP),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_AUTH, true)
return "", "", 0, err
}
if user.ID == 1 &&
@@ -49,7 +49,7 @@ func ValidateLogin(username string, password string, scriptingVersion string, fr
err = errors.New("Throttling login attempts of default account")
logging.LogError(err, "Throttling login attempts of default account", "username", user.Username)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Throttling login attempts of account, %s, from %s", user.Username, fromIP),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_AUTH, true)
return "", "", 0, err
}
if !database.CheckUserPassword(user, password) {
@@ -62,7 +62,7 @@ func ValidateLogin(username string, password string, scriptingVersion string, fr
user.Active = false
logging.LogError(nil, "Deactivating user over too many failed login attempts", "username", user.Username)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deactivating account due to failed attempts, %s, from %s", user.Username, fromIP),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_AUTH, true)
}
}
updateUserLoginStatus(user)
@@ -73,9 +73,9 @@ func ValidateLogin(username string, password string, scriptingVersion string, fr
user.FailedLoginCount = 0
updateUserLoginStatus(user)
if scriptingVersion == "" {
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("%s logged in from %s", user.Username, fromIP), 0, "", database.MESSAGE_LEVEL_DEBUG)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("%s logged in from %s", user.Username, fromIP), 0, "", database.MESSAGE_LEVEL_AUTH, false)
} else {
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("%s connected via Mythic Scripting (v%s) from %s", user.Username, scriptingVersion, fromIP), 0, "", database.MESSAGE_LEVEL_DEBUG)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("%s connected via Mythic Scripting (v%s) from %s", user.Username, scriptingVersion, fromIP), 0, "", database.MESSAGE_LEVEL_AUTH, false)
}
return mythicjwt.GenerateJWT(user, mythicjwt.AUTH_METHOD_USER, 0, 0)
@@ -97,14 +97,14 @@ func ValidateCustomAuthProviderLogin(email string, authError string, validLogin
err = errors.New("attempted log in with a deleted user")
logging.LogError(err, "username", user.Username)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Attempted to log in with deleted user, %s, from %s", user.Username, fromIP),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_AUTH, true)
return "", "", 0, err
}
if !user.Active {
err = errors.New("Attempted log in with an inactive user")
logging.LogError(err, "username", user.Username)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Attempted to log in with inactive user, %s, from %s", user.Username, fromIP),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_AUTH, true)
return "", "", 0, err
}
if user.ID == 1 &&
@@ -113,7 +113,7 @@ func ValidateCustomAuthProviderLogin(email string, authError string, validLogin
err = errors.New("Throttling login attempts of default account")
logging.LogError(err, "Throttling login attempts of default account", "username", user.Username)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Throttling login attempts of account, %s, from %s", user.Username, fromIP),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_AUTH, true)
return "", "", 0, err
}
if !validLogin {
@@ -126,7 +126,7 @@ func ValidateCustomAuthProviderLogin(email string, authError string, validLogin
user.Active = false
logging.LogError(nil, "Deactivating user over too many failed login attempts", "username", user.Username)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deactivating account due to failed attempts, %s, from %s", user.Username, fromIP),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_AUTH, true)
}
}
updateUserLoginStatus(user)
@@ -136,7 +136,7 @@ func ValidateCustomAuthProviderLogin(email string, authError string, validLogin
user.LastLogin = sql.NullTime{Valid: true, Time: time.Now().UTC()}
user.FailedLoginCount = 0
updateUserLoginStatus(user)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("%s logged in from %s via %s's %s", user.Username, fromIP, containerName, IDPName), 0, "", database.MESSAGE_LEVEL_DEBUG)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("%s logged in from %s via %s's %s", user.Username, fromIP, containerName, IDPName), 0, "", database.MESSAGE_LEVEL_AUTH, false)
return mythicjwt.GenerateJWT(user, mythicjwt.AUTH_METHOD_USER, 0, 0)
}
+2 -2
View File
@@ -83,7 +83,7 @@ func TokenValid(c *gin.Context) error {
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deactivated APIToken, %s, for user %s (%s) attempted to be used",
token.Name, token.Operator.Username, token.Operator.AccountType),
int(token.Operator.CurrentOperationID.Int64), "apitoken usage",
database.MESSAGE_LEVEL_WARNING)
database.MESSAGE_LEVEL_API, true)
}(databaseApiToken)
return errors.New("Deactivated APIToken attempted to be used")
}
@@ -92,7 +92,7 @@ func TokenValid(c *gin.Context) error {
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Deleted APIToken, \"%s\", for user %s (%s) attempted to be used",
token.Name, token.Operator.Username, token.Operator.AccountType),
int(token.Operator.CurrentOperationID.Int64), token.Name+fmt.Sprintf("%d", token.ID)+token.Operator.Username,
database.MESSAGE_LEVEL_WARNING)
database.MESSAGE_LEVEL_API, true)
}(databaseApiToken)
return errors.New("Deleted APIToken attempted to be used")
}
@@ -59,7 +59,7 @@ func IPBlockMiddleware() gin.HandlerFunc {
logging.LogError(nil, "Client IP not in allowed IP blocks", "client_ip", ipAddr)
go rabbitmq.SendAllOperationsMessage(fmt.Sprintf("Client IP, %s, not in allowed IP blocks: %v", ipAddr.String(), utils.MythicConfig.AllowedIPBlocks),
0, ipAddr.String(), database.MESSAGE_LEVEL_WARNING)
0, ipAddr.String(), database.MESSAGE_LEVEL_AUTH, true)
c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
c.Abort()
}
+7 -5
View File
@@ -14,11 +14,13 @@ var (
OPERATOR_OPERATION_VIEW_MODE_OPERATOR = "operator"
)
type MESSAGE_LEVEL = string
type MESSAGE_TYPE = string
const (
MESSAGE_LEVEL_INFO MESSAGE_LEVEL = "info"
MESSAGE_LEVEL_WARNING = "warning"
MESSAGE_LEVEL_DEBUG = "debug"
MESSAGE_LEVEL_API = "api"
MESSAGE_LEVEL_INFO MESSAGE_TYPE = "info"
MESSAGE_LEVEL_AUTH = "auth"
MESSAGE_LEVEL_DEBUG = "debug"
MESSAGE_LEVEL_API = "api"
MESSAGE_LEVEL_AGENT_MESSGAGE = "agent"
MESSAGE_LEVEL_CHAT = "chat"
)
@@ -8,9 +8,32 @@ alter table "public"."payloadtype" add column IF NOT EXISTS "supported_c2" jsonb
alter table "public"."payloadtype" add column IF NOT EXISTS "semver" text not null default ''::text;
alter table "public"."payloadtype" add column IF NOT EXISTS "command_help_function" text not null default ''::text;
alter table "public"."c2profileparametersinstance" add column IF NOT EXISTS "count" integer not null default 0;
alter table "public"."c2profile" add column IF NOT EXISTS "semver" text not null default ''::text;
alter table "public"."c2profile" add column IF NOT EXISTS "has_logo" boolean not null default false;
alter table "public"."consuming_container" add column IF NOT EXISTS "semver" text not null default ''::text;
alter table "public"."operationeventlog" add column IF NOT EXISTS "warning" boolean not null default false;
drop index if exists "public"."c2profileparametersinstance_c2_profile_parameters_id_ins_93bb57";
drop index if exists "public"."c2profileparametersinstance_c2_profile_parameters_id_payload_id";
-- +migrate StatementBegin
CREATE OR REPLACE FUNCTION public.update_operation_alert_count() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
current_max integer;
BEGIN
SELECT COUNT(*)
INTO current_max
FROM operationeventlog
WHERE operation_id = NEW.operation_id AND resolved=false AND deleted=false AND warning=true;
UPDATE operation SET alert_count = current_max WHERE id = NEW.operation_id;
RETURN NEW;
END;
$$;
-- +migrate StatementEnd
-- +migrate Down
-- SQL in section 'Down' is executed when this migration is rolled back
@@ -15,4 +15,6 @@ type C2profile struct {
IsP2p bool `db:"is_p2p"`
IsServerRouted bool `db:"is_server_routed"`
Deleted bool `db:"deleted"`
SemVer string `db:"semver"`
HasLogo bool `db:"has_logo"`
}
@@ -12,4 +12,5 @@ type ConsumingContainer struct {
ContainerRunning bool `db:"container_running" json:"container_running" toml:"container_running" yaml:"container_running"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
SemVer string `json:"semver" toml:"semver" yaml:"semver"`
}
@@ -18,4 +18,5 @@ type Operationeventlog struct {
Source string `db:"source"`
Count int `db:"count"`
APITokensID structs.NullInt64 `db:"apitokens_id" json:"api_tokens_id" mapstructure:"apitokens_id"`
Warning bool `db:"warning"`
}
@@ -7,4 +7,5 @@ type Translationcontainer struct {
Author string `db:"author"`
Deleted bool `db:"deleted"`
ContainerRunning bool `db:"container_running"`
SemVer string `db:"semver"`
}
@@ -290,7 +290,7 @@ func CreateEventGroupInstance(eventGroupId int, trigger string, triggeringOperat
return 0, errors.New(fmt.Sprintf("event group \"%s\" is deactivated and will not be triggered", eventGroup.Name))
}
if trigger != TriggerKeyword {
if eventGroup.Trigger != trigger {
if eventGroup.Trigger != trigger && trigger != TriggerManual {
return 0, errors.New(fmt.Sprintf("event group \"%s\" doesn't have \"%s\" as a trigger", eventGroup.Name, trigger))
}
} else {
+2 -2
View File
@@ -8,7 +8,7 @@ type EventGroup struct {
Environment map[string]interface{} `db:"environment" json:"environment" toml:"environment" yaml:"environment"`
Keywords []string `db:"keywords" json:"keywords" toml:"keywords" yaml:"keywords"`
RunAs string `db:"run_as" json:"run_as" toml:"run_as" yaml:"run_as"`
Steps []EventStep `db:"eventstep" json:"steps" toml:"steps" yaml:"steps"`
Steps []EventStep `db:"eventstep" json:"steps,omitempty" toml:"steps,omitempty" yaml:"steps,omitempty"`
}
type EventStep struct {
@@ -20,6 +20,6 @@ type EventStep struct {
Environment map[string]interface{} `db:"environment" json:"environment" toml:"environment" yaml:"environment"`
Inputs map[string]interface{} `db:"inputs" json:"inputs" toml:"inputs" yaml:"inputs"`
Outputs map[string]interface{} `db:"outputs" json:"outputs" toml:"outputs" yaml:"outputs"`
Order int `db:"order" json:"order" toml:"order" yaml:"order"`
Order int `db:"order" json:"-" toml:"-" yaml:"-"`
ContinueOnError bool `db:"continue_on_error" json:"continue_on_error" toml:"continue_on_error" yaml:"continue_on_error"`
}
+33 -29
View File
@@ -1,12 +1,11 @@
module github.com/its-a-feature/Mythic
go 1.23.0
toolchain go1.23.4
go 1.25.1
require (
github.com/gin-gonic/gin v1.10.1
github.com/go-co-op/gocron/v2 v2.16.2
github.com/gin-gonic/gin v1.11.0
github.com/go-co-op/gocron/v2 v2.16.6
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.4.0
@@ -16,49 +15,54 @@ require (
github.com/rabbitmq/amqp091-go v1.10.0
github.com/rs/zerolog v1.34.0
github.com/rubenv/sql-migrate v1.8.0
github.com/spf13/viper v1.20.1
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
golang.org/x/mod v0.25.0
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
github.com/spf13/viper v1.21.0
golang.org/x/exp v0.0.0-20250911091902-df9299821621
golang.org/x/mod v0.28.0
google.golang.org/grpc v1.75.1
google.golang.org/protobuf v1.36.9
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.21.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
)
+80 -72
View File
@@ -1,13 +1,13 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -16,18 +16,18 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-co-op/gocron/v2 v2.16.2 h1:r08P663ikXiulLT9XaabkLypL/W9MoCIbqgQoAutyX4=
github.com/go-co-op/gocron/v2 v2.16.2/go.mod h1:4YTLGCCAH75A5RlQ6q+h+VacO7CgjkgP0EJ+BEOXRSI=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-co-op/gocron/v2 v2.16.6 h1:zI2Ya9sqvuLcgqJgV79LwoJXM8h20Z/drtB7ATbpRWo=
github.com/go-co-op/gocron/v2 v2.16.6/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -36,14 +36,16 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
@@ -60,10 +62,8 @@ github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbd
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -95,6 +95,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
@@ -106,28 +110,25 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o=
github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
@@ -136,47 +137,54 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
@@ -276,7 +276,7 @@ func checkContainerStatus() {
if !running {
SendAllOperationsMessage(
getDownContainerMessage(container),
0, fmt.Sprintf("%s_container_down", container), "warning")
0, fmt.Sprintf("%s_container_down", container), database.MESSAGE_LEVEL_INFO, true)
go updateDownContainerBuildingPayloads(container)
} else {
go database.ResolveAllOperationsMessage(getDownContainerMessage(container), 0)
@@ -309,7 +309,7 @@ func checkContainerStatus() {
UpdateC2ProfileRunningStatus(c2profilesToCheck[container], false)
SendAllOperationsMessage(
getDownContainerMessage(container),
0, fmt.Sprintf("%s_container_down", container), "warning")
0, fmt.Sprintf("%s_container_down", container), database.MESSAGE_LEVEL_INFO, true)
} else {
go database.ResolveAllOperationsMessage(getDownContainerMessage(container), 0)
go CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(container)
@@ -340,7 +340,7 @@ func checkContainerStatus() {
if !running {
SendAllOperationsMessage(
getDownContainerMessage(container),
0, fmt.Sprintf("%s_container_down", container), "warning")
0, fmt.Sprintf("%s_container_down", container), database.MESSAGE_LEVEL_INFO, true)
} else {
go database.ResolveAllOperationsMessage(getDownContainerMessage(container), 0)
go CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(container)
@@ -372,7 +372,7 @@ func checkContainerStatus() {
if !running {
SendAllOperationsMessage(
getDownContainerMessage(container),
0, fmt.Sprintf("%s_container_down", container), "warning")
0, fmt.Sprintf("%s_container_down", container), database.MESSAGE_LEVEL_INFO, true)
} else {
go database.ResolveAllOperationsMessage(getDownContainerMessage(container), 0)
go CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(container)
+1
View File
@@ -104,6 +104,7 @@ const (
PT_TASK_COMPLETION_FUNCTION = "pt_task_completion_function"
//
PT_TASK_PROCESS_RESPONSE = "pt_task_process_response"
PT_COMMAND_HELP_FUNCTION = "pt_command_help_function"
)
// Routes where container is consuming messages and responding back to Mythic
+16 -11
View File
@@ -137,14 +137,14 @@ func listenForCronEvents() {
if err != nil {
logging.LogError(err, "failed to create new cron job")
SendAllOperationsMessage(fmt.Sprintf("Failed to create new cron job\n%v", err),
event.OperationID, "eventing_cron_listen", database.MESSAGE_LEVEL_WARNING)
event.OperationID, "eventing_cron_listen", database.MESSAGE_LEVEL_INFO, true)
continue
}
nextTriggerDate, err := j.NextRun()
if err != nil {
logging.LogError(err, "failed to get next run")
SendAllOperationsMessage(fmt.Sprintf("Failed to get next cron job run\n%v", err),
event.OperationID, "eventing_cron_next", database.MESSAGE_LEVEL_WARNING)
event.OperationID, "eventing_cron_next", database.MESSAGE_LEVEL_INFO, true)
continue
}
_, err = database.DB.Exec(`UPDATE eventgroup SET next_scheduled_run=$1 WHERE id=$2`,
@@ -193,7 +193,7 @@ func listenForEvents() {
case eventing.TriggerManual:
// somebody manually triggered an event group to run
eventgroupinstanceID, err := eventing.CreateEventGroupInstance(event.EventGroupID,
eventing.TriggerManual, event.OperatorID, map[string]interface{}{})
eventing.TriggerManual, event.OperatorID, event.KeywordEnvData)
if err != nil {
logging.LogError(err, "Failed to create new event group instance")
errMsg := err.Error()
@@ -206,7 +206,7 @@ func listenForEvents() {
logging.LogError(err, "Failed to get event group information")
}
go SendAllOperationsMessage(fmt.Sprintf("Error triggering manual workflow: %s", errMsg),
event.OperationID, source, database.MESSAGE_LEVEL_WARNING)
event.OperationID, source, database.MESSAGE_LEVEL_INFO, true)
continue
}
// still need to do something to start processing steps
@@ -229,7 +229,7 @@ func listenForEvents() {
logging.LogError(err, "Failed to get event group information")
}
go SendAllOperationsMessage(fmt.Sprintf("Error retriggering workflow: %s", errMsg),
event.OperationID, source, database.MESSAGE_LEVEL_WARNING)
event.OperationID, source, database.MESSAGE_LEVEL_INFO, true)
continue
}
case eventing.TriggerRetryFromStep:
@@ -260,7 +260,7 @@ func listenForEvents() {
logging.LogError(err, "Failed to get event group information")
}
go SendAllOperationsMessage(fmt.Sprintf("Error triggering cron workflow: %s", errMsg),
event.OperationID, source, database.MESSAGE_LEVEL_WARNING)
event.OperationID, source, database.MESSAGE_LEVEL_INFO, true)
continue
}
_, err = database.DB.Exec(`UPDATE eventgroup SET next_scheduled_run=$1 WHERE id=$2`,
@@ -285,7 +285,7 @@ func listenForEvents() {
source = fmt.Sprintf("Cancel Event Group Trigger: %s", eventGroup.EventGroup.Name)
}
go SendAllOperationsMessage(fmt.Sprintf("Error cancelling workflow: %s", err.Error()),
event.OperationID, source, database.MESSAGE_LEVEL_WARNING)
event.OperationID, source, database.MESSAGE_LEVEL_INFO, true)
}
// payload starting to build triggers new things to start
case eventing.TriggerPayloadBuildStart:
@@ -412,7 +412,7 @@ func findEventGroupsToStart(eventNotification EventNotification) {
source := fmt.Sprintf("Keyword Event Group Trigger: %s", eventNotification.Trigger)
go SendAllOperationsMessage(fmt.Sprintf("Error triggering keyword workflow for %s: %s",
possibleEventGroups[i].Name, err.Error()),
eventNotification.OperationID, source, database.MESSAGE_LEVEL_WARNING)
eventNotification.OperationID, source, database.MESSAGE_LEVEL_INFO, true)
continue
}
// still need to do something to start processing steps
@@ -552,7 +552,7 @@ func findEventGroupsToStart(eventNotification EventNotification) {
source := fmt.Sprintf("Event Group Trigger: %s", eventNotification.Trigger)
go SendAllOperationsMessage(fmt.Sprintf("Error triggering workflow for %s: %s",
possibleEventGroups[i].Name, err.Error()),
eventNotification.OperationID, source, database.MESSAGE_LEVEL_WARNING)
eventNotification.OperationID, source, database.MESSAGE_LEVEL_INFO, true)
continue
}
// still need to do something to start processing steps
@@ -804,7 +804,7 @@ func startProcessingRunAgainGroupInstanceSteps(oldEventGroupInstanceID int, newO
source := fmt.Sprintf("RunAgain Event Group Trigger: %s", oldEventGroupInstance.EventGroup.Name)
go SendAllOperationsMessage(fmt.Sprintf("Error triggering keyword workflow for %s: %s",
oldEventGroupInstance.EventGroup.Name, err.Error()),
oldEventGroupInstance.OperationID, source, database.MESSAGE_LEVEL_WARNING)
oldEventGroupInstance.OperationID, source, database.MESSAGE_LEVEL_INFO, true)
return
}
oldEventGroupInstance.ID = eventgroupinstanceID
@@ -1264,7 +1264,12 @@ func startEventStepInstanceActionCreateAlert(eventStepInstance databaseStructs.E
logging.LogError(err, "failed to decode action data")
return err
}
go SendAllOperationsMessage(eventData.Alert, eventStepInstance.OperationID, eventData.Source, eventData.Level)
warning := false
if eventData.Level == "warning" {
warning = true
eventData.Level = database.MESSAGE_LEVEL_INFO
}
go SendAllOperationsMessage(eventData.Alert, eventStepInstance.OperationID, eventData.Source, eventData.Level, warning)
if eventData.SendWebhook {
databaseOperation := databaseStructs.Operation{ID: eventStepInstance.OperationID}
err = database.DB.Get(&databaseOperation, `SELECT "name", "webhook", "channel" FROM operation WHERE id=$1`, databaseOperation.ID)
+1
View File
@@ -86,6 +86,7 @@ var pushC2StreamingDisconnectNotification = make(chan int, 100)
func Initialize() {
RabbitMQConnection.channelMutexMap = make(map[string]*channelMutex)
go invalidateAllSpectatorAPITokens()
go InvalidateOperationEventLogCacheMap()
go listenForWriteDownloadChunkToLocalDisk()
go listenForAsyncAgentMessagePostResponseContent()
for {
+64 -14
View File
@@ -9,6 +9,8 @@ import (
"github.com/its-a-feature/Mythic/logging"
"github.com/its-a-feature/Mythic/utils"
amqp "github.com/rabbitmq/amqp091-go"
"os"
"path/filepath"
"strings"
)
@@ -27,11 +29,14 @@ type C2SyncMessage struct {
}
type C2Profile struct {
Name string `json:"name"`
Description string `json:"description"`
Author string `json:"author"`
IsP2p bool `json:"is_p2p"`
IsServerRouted bool `json:"is_server_routed"`
Name string `json:"name"`
Description string `json:"description"`
Author string `json:"author"`
IsP2p bool `json:"is_p2p"`
IsServerRouted bool `json:"is_server_routed"`
SemVer string `json:"semver"`
AgentIcon *[]byte `json:"agent_icon,omitempty"`
DarkModeAgentIcon *[]byte `json:"dark_mode_agent_icon,omitempty"`
}
const (
@@ -90,7 +95,7 @@ func processC2SyncMessages(msg amqp.Delivery) interface{} {
if err := json.Unmarshal(msg.Body, &c2SyncMsg); err != nil {
logging.LogError(err, "Failed to process c2 sync message")
go SendAllOperationsMessage(fmt.Sprintf("Failed to sync c2 profile %s", err.Error()), 0, "", database.MESSAGE_LEVEL_WARNING)
go SendAllOperationsMessage(fmt.Sprintf("Failed to sync c2 profile %s", err.Error()), 0, "", database.MESSAGE_LEVEL_INFO, true)
return C2SyncMessageResponse{Success: false, Error: err.Error()}
} else {
response := C2SyncMessageResponse{}
@@ -98,7 +103,7 @@ func processC2SyncMessages(msg amqp.Delivery) interface{} {
// failed to sync message
response.Success = false
response.Error = fmt.Sprintf("Error: %v", err)
go SendAllOperationsMessage(fmt.Sprintf("Failed to sync %s - %s", c2SyncMsg.Profile.Name, err.Error()), 0, c2SyncMsg.Profile.Name, database.MESSAGE_LEVEL_WARNING)
go SendAllOperationsMessage(fmt.Sprintf("Failed to sync %s - %s", c2SyncMsg.Profile.Name, err.Error()), 0, c2SyncMsg.Profile.Name, database.MESSAGE_LEVEL_INFO, true)
} else {
// successfully synced
response.Success = true
@@ -131,9 +136,13 @@ func c2Sync(in C2SyncMessage) error {
c2Profile.IsServerRouted = in.Profile.IsServerRouted
c2Profile.Description = in.Profile.Description
c2Profile.Deleted = false
c2Profile.SemVer = in.Profile.SemVer
if in.Profile.AgentIcon != nil || in.Profile.DarkModeAgentIcon != nil {
c2Profile.HasLogo = true
}
if statement, err := database.DB.PrepareNamed(`INSERT INTO c2profile
("name",author,container_running,is_p2p,is_server_routed,description, running, deleted)
VALUES (:name, :author, :container_running, :is_p2p, :is_server_routed, :description, :running, :deleted)
("name",author,container_running,is_p2p,is_server_routed,description, running, deleted, has_logo, semver)
VALUES (:name, :author, :container_running, :is_p2p, :is_server_routed, :description, :running, :deleted, :has_logo, :semver)
RETURNING id`,
); err != nil {
logging.LogError(err, "Failed to create new c2 profile statement")
@@ -156,9 +165,15 @@ func c2Sync(in C2SyncMessage) error {
c2Profile.IsServerRouted = in.Profile.IsServerRouted
c2Profile.Description = in.Profile.Description
c2Profile.Deleted = false
c2Profile.SemVer = in.Profile.SemVer
if in.Profile.AgentIcon != nil || in.Profile.DarkModeAgentIcon != nil {
c2Profile.HasLogo = true
} else {
c2Profile.HasLogo = false
}
_, err = database.DB.NamedExec(`UPDATE c2profile SET
author=:author, container_running=:container_running, is_p2p=:is_p2p, is_server_routed=:is_server_routed,
description=:description, running=:running, deleted=:deleted
description=:description, running=:running, deleted=:deleted, has_logo=:has_logo, semver=:semver
WHERE id=:id`, c2Profile,
)
if err != nil {
@@ -170,7 +185,7 @@ func c2Sync(in C2SyncMessage) error {
logging.LogError(err, "Failed to sync C2 profile")
return err
}
go SendAllOperationsMessage(fmt.Sprintf("Successfully synced %s with container version %s", c2Profile.Name, in.ContainerVersion), 0, "debug", database.MESSAGE_LEVEL_DEBUG)
go SendAllOperationsMessage(fmt.Sprintf("Successfully synced %s with container version %s", c2Profile.Name, in.ContainerVersion), 0, "debug", database.MESSAGE_LEVEL_DEBUG, false)
go database.ResolveAllOperationsMessage(getDownContainerMessage(c2Profile.Name), 0)
go autoStartC2Profile(c2Profile)
if newProfile {
@@ -178,6 +193,41 @@ func c2Sync(in C2SyncMessage) error {
}
checkContainerStatusAddC2Channel <- c2Profile
go CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(c2Profile.Name)
absPath, err := filepath.Abs(filepath.Join(".", "static", fmt.Sprintf("%s_light.svg", in.Profile.Name)))
if err != nil {
return err
}
file, err := os.Create(absPath)
if err != nil {
return err
}
if in.Profile.AgentIcon != nil {
if _, err = file.Write(*in.Profile.AgentIcon); err != nil {
return err
}
}
file.Close()
darkModeAbsPath, err := filepath.Abs(filepath.Join(".", "static", fmt.Sprintf("%s_dark.svg", in.Profile.Name)))
if err != nil {
return err
}
darkModeFile, err := os.Create(darkModeAbsPath)
if err != nil {
return err
}
if in.Profile.DarkModeAgentIcon != nil {
if _, err = darkModeFile.Write(*in.Profile.DarkModeAgentIcon); err != nil {
return err
}
} else {
if in.Profile.AgentIcon != nil {
if _, err = darkModeFile.Write(*in.Profile.AgentIcon); err != nil {
return err
}
}
}
darkModeFile.Close()
return nil
}
@@ -316,7 +366,7 @@ func autoStartC2Profile(c2Profile databaseStructs.C2profile) *C2StartServerMessa
} else {
UpdateC2ProfileRunningStatus(c2Profile, c2StartResp.InternalServerRunning)
if !c2StartResp.InternalServerRunning {
go SendAllOperationsMessage(fmt.Sprintf("Failed to start c2 profile %s:\n%s", c2Profile.Name, c2StartResp.Error), 0, "", database.MESSAGE_LEVEL_WARNING)
go SendAllOperationsMessage(fmt.Sprintf("Failed to start c2 profile %s:\n%s", c2Profile.Name, c2StartResp.Error), 0, "", database.MESSAGE_LEVEL_INFO, true)
}
}
}
@@ -354,14 +404,14 @@ func autoReHostFiles(c2Profile databaseStructs.C2profile) {
logging.LogError(err, "failed to send host file message to c2 profile")
go SendAllOperationsMessage(fmt.Sprintf(
"%s failed to start hosting file:\n%s", newTagMap["c2_profile"].(string),
err.Error()), tag.Operation, "", database.MESSAGE_LEVEL_WARNING)
err.Error()), tag.Operation, "", database.MESSAGE_LEVEL_INFO, true)
continue
}
if !c2HostFileResponse.Success {
logging.LogError(err, "c2 profile failed to start hosting file")
go SendAllOperationsMessage(fmt.Sprintf(
"%s failed to start hosting file:\n%s", newTagMap["c2_profile"].(string),
c2HostFileResponse.Error), tag.Operation, "", database.MESSAGE_LEVEL_WARNING)
c2HostFileResponse.Error), tag.Operation, "", database.MESSAGE_LEVEL_INFO, true)
continue
}
}
@@ -29,6 +29,7 @@ type ConsumingContainerDefinition struct {
Description string `json:"description"`
Type string `json:"type"`
Subscriptions []string `json:"subscriptions"`
SemVer string `json:"semver"`
}
const (
@@ -55,7 +56,7 @@ func processConsumingContainerSyncMessages(msg amqp.Delivery) interface{} {
if err != nil {
logging.LogError(err, "Failed to process consuming container sync message")
go SendAllOperationsMessage(fmt.Sprintf("Failed to sync consuming container profile %s", err.Error()),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_INFO, true)
return ConsumingContainerSyncMessageResponse{Success: false, Error: err.Error()}
}
response := ConsumingContainerSyncMessageResponse{}
@@ -65,7 +66,7 @@ func processConsumingContainerSyncMessages(msg amqp.Delivery) interface{} {
response.Error = fmt.Sprintf("Error: %v", err)
go SendAllOperationsMessage(fmt.Sprintf("Failed to sync %s - %s",
c2SyncMsg.ConsumingContainer.Name, err.Error()), 0, c2SyncMsg.ConsumingContainer.Name,
database.MESSAGE_LEVEL_WARNING)
database.MESSAGE_LEVEL_INFO, true)
} else {
// successfully synced
response.Success = true
@@ -94,10 +95,11 @@ func consumingServicesSync(in ConsumingContainerSyncMessage) error {
consumingContainer.Description = in.ConsumingContainer.Description
consumingContainer.Deleted = false
consumingContainer.Type = in.ConsumingContainer.Type
consumingContainer.SemVer = in.ConsumingContainer.SemVer
consumingContainer.Subscriptions = GetMythicJSONArrayFromStruct(in.ConsumingContainer.Subscriptions)
if statement, err := database.DB.PrepareNamed(`INSERT INTO consuming_container
("name",container_running,description,deleted,type,subscriptions)
VALUES (:name, :container_running, :description, :deleted, :type, :subscriptions)
("name",container_running,description,deleted,type,subscriptions, semver)
VALUES (:name, :container_running, :description, :deleted, :type, :subscriptions, :semver)
RETURNING id`,
); err != nil {
logging.LogError(err, "Failed to create new consuming_container statement")
@@ -117,10 +119,11 @@ func consumingServicesSync(in ConsumingContainerSyncMessage) error {
consumingContainer.Description = in.ConsumingContainer.Description
consumingContainer.Deleted = false
consumingContainer.Type = in.ConsumingContainer.Type
consumingContainer.SemVer = in.ConsumingContainer.SemVer
consumingContainer.Subscriptions = GetMythicJSONArrayFromStruct(in.ConsumingContainer.Subscriptions)
_, err = database.DB.NamedExec(`UPDATE consuming_container SET
container_running=:container_running, description=:description, deleted=:deleted,
type=:type, subscriptions=:subscriptions
type=:type, subscriptions=:subscriptions, semver=:semver
WHERE id=:id`, consumingContainer,
)
if err != nil {
@@ -129,7 +132,7 @@ func consumingServicesSync(in ConsumingContainerSyncMessage) error {
}
}
go SendAllOperationsMessage(fmt.Sprintf("Successfully synced %s with container version %s",
consumingContainer.Name, in.ContainerVersion), 0, "debug", database.MESSAGE_LEVEL_DEBUG)
consumingContainer.Name, in.ContainerVersion), 0, "debug", database.MESSAGE_LEVEL_DEBUG, false)
go database.ResolveAllOperationsMessage(getDownContainerMessage(consumingContainer.Name), 0)
checkContainerStatusAddConsumingContainerChannel <- consumingContainer
// update eventgroup consumingcontainer mappings
@@ -31,17 +31,17 @@ func processContainerOnStartMessageResponse(msg amqp.Delivery) {
if err != nil {
logging.LogError(err, "Failed to process container on start response message")
go SendAllOperationsMessage(fmt.Sprintf("Failed to process container on start response:\n %s", err.Error()),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_INFO, true)
return
}
if containerStartResponse.Stderr != "" {
go SendAllOperationsMessage(fmt.Sprintf("Failed to process %s on start response:\n %s",
containerStartResponse.ContainerName, containerStartResponse.Stderr),
0, fmt.Sprintf("%s_on_start", containerStartResponse.ContainerName), database.MESSAGE_LEVEL_WARNING)
0, fmt.Sprintf("%s_on_start", containerStartResponse.ContainerName), database.MESSAGE_LEVEL_INFO, true)
}
if containerStartResponse.Stdout != "" {
go SendAllOperationsMessage(fmt.Sprintf("Container %s on start response:\n %s",
containerStartResponse.ContainerName, containerStartResponse.Stdout),
0, fmt.Sprintf("%s_on_start", containerStartResponse.ContainerName), database.MESSAGE_LEVEL_INFO)
0, fmt.Sprintf("%s_on_start", containerStartResponse.ContainerName), database.MESSAGE_LEVEL_INFO, false)
}
}
@@ -35,7 +35,7 @@ func processEventingConditionalCheckResponse(msg amqp.Delivery) {
if err != nil {
logging.LogError(err, "Failed to process eventing custom function response message")
go SendAllOperationsMessage(fmt.Sprintf("Failed to process eventing conditional check response %s", err.Error()),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_INFO, true)
return
}
eventingConditionalCheckProcess(c2SyncMsg)
@@ -34,7 +34,7 @@ func processEventingCustomFunctionResponse(msg amqp.Delivery) {
if err != nil {
logging.LogError(err, "Failed to process eventing custom function response message")
go SendAllOperationsMessage(fmt.Sprintf("Failed to process eventing custom function response %s", err.Error()),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_INFO, true)
return
}
eventingCustomFunctionResponse(c2SyncMsg)
@@ -36,7 +36,7 @@ func processEventingResponseInterceptResponse(msg amqp.Delivery) {
if err != nil {
logging.LogError(err, "Failed to process eventing response intercept response message")
go SendAllOperationsMessage(fmt.Sprintf("Failed to process eventing response intercept response %s", err.Error()),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_INFO, true)
return
}
_, err = database.DB.Exec(`UPDATE response SET response=$1, eventstepinstance_id=$2, timestamp=$4 WHERE id=$3`,
@@ -44,7 +44,7 @@ func processEventingResponseInterceptResponse(msg amqp.Delivery) {
if err != nil {
logging.LogError(err, "Failed to process eventing response intercept response message")
go SendAllOperationsMessage(fmt.Sprintf("Failed to process eventing response intercept response %s", err.Error()),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_INFO, true)
}
taskID := 0
err = database.DB.Get(&taskID, `SELECT task.id
@@ -39,7 +39,7 @@ func processEventingTaskInterceptResponse(msg amqp.Delivery) {
if err != nil {
logging.LogError(err, "Failed to process eventing task intercept response message")
go SendAllOperationsMessage(fmt.Sprintf("Failed to process eventing task intercept response %s", err.Error()),
0, "", database.MESSAGE_LEVEL_WARNING)
0, "", database.MESSAGE_LEVEL_INFO, true)
return
}
task := databaseStructs.Task{ID: input.TaskID}
@@ -44,10 +44,10 @@ func MythicRPCC2UpdateStatus(input MythicRPCC2UpdateStatusMessage) MythicRPCC2Up
if input.Error != "" {
if strings.HasPrefix(input.Error, "Server already") {
go SendAllOperationsMessage(fmt.Sprintf("Update from C2 Profile %s:\n%s\n", input.C2Profile, input.Error),
0, fmt.Sprintf("%s_error", input.C2Profile), database.MESSAGE_LEVEL_DEBUG)
0, fmt.Sprintf("%s_error", input.C2Profile), database.MESSAGE_LEVEL_DEBUG, false)
} else {
go SendAllOperationsMessage(fmt.Sprintf("Error from C2 Profile %s:\n%s\n", input.C2Profile, input.Error),
0, fmt.Sprintf("%s_error", input.C2Profile), database.MESSAGE_LEVEL_WARNING)
0, fmt.Sprintf("%s_error", input.C2Profile), database.MESSAGE_LEVEL_INFO, true)
}
}
return response
@@ -124,7 +124,7 @@ func MythicRPCCallbackCreate(input MythicRPCCallbackCreateMessage) MythicRPCCall
logging.LogError(nil, "Operation is complete, but payload still trying to create callback", "payload_uuid", input.PayloadUUID)
go SendAllOperationsMessage(fmt.Sprintf(
"Operation %s is complete, but payload %s is still trying to create a callback via %s\n", payload.Operation.Name, payload.UuID, input.C2ProfileName),
0, fmt.Sprintf("complete_operation_%s", payload.UuID), database.MESSAGE_LEVEL_WARNING)
0, fmt.Sprintf("complete_operation_%s", payload.UuID), database.MESSAGE_LEVEL_INFO, true)
response.Error = "Operation is complete, but payload still trying to create callback"
return response
}
@@ -312,7 +312,7 @@ func MythicRPCCallbackCreate(input MythicRPCCallbackCreateMessage) MythicRPCCall
}
addCommandAugmentsToCallback(callback.ID, payload.Os, payload.Payloadtype.Name, callback.OperatorID)
operationsMsg := fmt.Sprintf("New Callback (%d) %s@%s with pid %d", callback.DisplayID, callback.User, callback.Host, callback.PID)
go SendAllOperationsMessage(operationsMsg, callback.OperationID, "", database.MESSAGE_LEVEL_INFO)
go SendAllOperationsMessage(operationsMsg, callback.OperationID, "", database.MESSAGE_LEVEL_INFO, false)
// prep data to send for log messages and webhook messages
webhookData := NewCallbackWebhookData{
User: callback.User,
@@ -201,6 +201,9 @@ func MythicRPCCallbackSearch(input MythicRPCCallbackSearchMessage) MythicRPCCall
result.RegisteredPayloadUUID = searchResults.Payload.UuID
result.LockedOperatorID = int(searchResults.LockedOperatorID.Int64)
result.PayloadType = searchResults.Payload.Payloadtype.Name
result.InitCallback = searchResults.InitCallback
result.Timestamp = searchResults.Timestamp
result.LastCheckin = searchResults.LastCheckin
response.Results = append(response.Results, result)
}
@@ -16,8 +16,9 @@ type MythicRPCOperationEventLogCreateMessage struct {
CallbackAgentId *string `json:"callback_agent_id"`
OperationId *int `json:"operation_id"`
// the data to store
Message string `json:"message"`
MessageLevel database.MESSAGE_LEVEL `json:"level"` //info or warning
Message string `json:"message"`
MessageLevel database.MESSAGE_TYPE `json:"level"` //info or warning
Warning bool `json:"warning"`
}
type MythicRPCOperationEventLogCreateMessageResponse struct {
Success bool `json:"success"`
@@ -69,7 +70,11 @@ func MythicRPCOperationEventLogCreate(input MythicRPCOperationEventLogCreateMess
} else if input.OperationId != nil {
operationId = *input.OperationId
}
SendAllOperationsMessage(input.Message, operationId, "", input.MessageLevel)
if input.MessageLevel == "warning" {
input.MessageLevel = database.MESSAGE_LEVEL_INFO
input.Warning = true
}
SendAllOperationsMessage(input.Message, operationId, "", input.MessageLevel, input.Warning)
response.Success = true
return response
}
@@ -265,9 +265,20 @@ func associateC2ProfilesWithPayload(databasePayload databaseStructs.Payload, c2P
}
// finalC2Profiles will look like {"http": {"callback_host": "val", "callback_port": val2}}
finalC2Profiles := make([]PayloadBuildC2Profile, 0)
payloadType := databasePayload.Payloadtype
err := database.DB.Get(&payloadType, `SELECT c2_parameter_deviations FROM payloadtype WHERE id=$1`, databasePayload.PayloadTypeID)
if err != nil {
return nil, err
}
deviations := make(map[string]map[string]C2ParameterDeviation)
err = mapstructure.Decode(payloadType.C2ParameterDeviations.StructValue(), &deviations)
if err != nil {
return nil, err
}
for count, suppliedC2Profile := range *c2Profiles {
databaseC2Profile := databaseStructs.C2profile{}
if err := database.DB.Get(&databaseC2Profile, "SELECT * FROM c2profile WHERE name=$1 and deleted=false", suppliedC2Profile.Name); err != nil {
err = database.DB.Get(&databaseC2Profile, "SELECT * FROM c2profile WHERE name=$1 and deleted=false", suppliedC2Profile.Name)
if err != nil {
logging.LogError(err, "Failed to find c2 profile")
return nil, err
}
@@ -277,18 +288,6 @@ func associateC2ProfilesWithPayload(databasePayload databaseStructs.Payload, c2P
ID: databaseC2Profile.ID,
Parameters: map[string]interface{}{},
}
/*
if !databaseC2Profile.ContainerRunning {
err := errors.New("C2 Profile container isn't running, can't task configuration checks")
logging.LogError(err, "C2 Profile container isn't running, can't task configuration checks")
return nil, err
}
if !databaseC2Profile.Running && !databaseC2Profile.IsP2p {
// the profile isn't running, and it's not a P2P container, so issue the start task
go autoStartC2Profile(databaseC2Profile)
}
*/
databaseC2ProfileParameter := databaseStructs.C2profileparameters{}
if rows, err := database.DB.NamedQuery(`SELECT
*
@@ -309,31 +308,55 @@ func associateC2ProfilesWithPayload(databasePayload databaseStructs.Payload, c2P
if suppliedParameterName == databaseC2ProfileParameter.Name {
// we have a supplied parameter that matches the one we're looking at from the database for this c2 profile
found = true
if strippedValue, err := GetFinalStringForDatabaseInstanceValueFromUserSuppliedValue(
strippedValue, err := GetFinalStringForDatabaseInstanceValueFromUserSuppliedValue(
databaseC2ProfileParameter.ParameterType, suppliedParameterValue,
); err != nil {
//if strippedValue, err := GetStrippedValueForC2Parameter(databaseC2ProfileParameter, suppliedParameterValue); err != nil {
)
if err != nil {
logging.LogError(err, "Failed to get stripped c2 profile parameter", "value", suppliedParameterValue)
return nil, err
} else {
paramStringVal = strippedValue
}
paramStringVal = strippedValue
break
}
}
if !found {
if strippedValue, err := getFinalStringForDatabaseInstanceValueFromDefaultDatabaseString(
databaseC2ProfileParameter.ParameterType, databaseC2ProfileParameter.DefaultValue,
databaseC2ProfileParameter.Choices.StructValue(),
databaseC2ProfileParameter.Randomize, databaseC2ProfileParameter.FormatString); err != nil {
//if strippedValue, err := GetDefaultValueForC2Parameter(databaseC2ProfileParameter); err != nil {
logging.LogError(err, "Failed to get default c2 profile parameter", "parameter", databaseC2ProfileParameter)
return nil, err
} else {
paramStringVal = strippedValue
if _, ok := deviations[suppliedC2Profile.Name]; ok {
if _, ok = deviations[suppliedC2Profile.Name][databaseC2ProfileParameter.Name]; ok {
if !deviations[suppliedC2Profile.Name][databaseC2ProfileParameter.Name].Supported {
continue
}
updateStringParam := false
if len(deviations[suppliedC2Profile.Name][databaseC2ProfileParameter.Name].Choices) > 0 {
if !slices.Contains(deviations[suppliedC2Profile.Name][databaseC2ProfileParameter.Name].Choices, paramStringVal) {
updateStringParam = true
}
} else if !found {
updateStringParam = true
}
found = true
if updateStringParam {
strippedValue, err := GetFinalStringForDatabaseInstanceValueFromUserSuppliedValue(
databaseC2ProfileParameter.ParameterType,
deviations[suppliedC2Profile.Name][databaseC2ProfileParameter.Name].DefaultValue,
)
if err != nil {
logging.LogError(err, "Failed to get stripped c2 profile parameter", "value", deviations[suppliedC2Profile.Name][databaseC2ProfileParameter.Name].DefaultValue)
return nil, err
}
paramStringVal = strippedValue
}
}
}
if !found {
strippedValue, err := getFinalStringForDatabaseInstanceValueFromDefaultDatabaseString(
databaseC2ProfileParameter.ParameterType, databaseC2ProfileParameter.DefaultValue,
databaseC2ProfileParameter.Choices.StructValue(),
databaseC2ProfileParameter.Randomize, databaseC2ProfileParameter.FormatString)
if err != nil {
logging.LogError(err, "Failed to get default c2 profile parameter", "parameter", databaseC2ProfileParameter)
return nil, err
}
paramStringVal = strippedValue
}
c2ParameterInstance := databaseStructs.C2profileparametersinstance{
PayloadID: sql.NullInt64{Valid: true, Int64: int64(databasePayload.ID)},
C2ProfileID: databaseC2Profile.ID,
@@ -49,6 +49,6 @@ func processOnNewCallbackResponse(msg amqp.Delivery) {
return
}
go SendAllOperationsMessage(fmt.Sprintf("Failed to handle onNewCallback processing for callback %d\n%s", databaseCallback.DisplayID, newCallbackResponse.Error),
databaseCallback.OperationID, "", database.MESSAGE_LEVEL_WARNING)
databaseCallback.OperationID, "", database.MESSAGE_LEVEL_INFO, true)
}
}

Some files were not shown because too many files have changed in this diff Show More