mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
initial chat feature
This commit is contained in:
Generated
+1463
-1
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,7 @@
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-draggable": "^4.4.6",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-moment": "^1.1.3",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"react-scrollbar-size": "^5.0.0",
|
||||
@@ -59,6 +60,7 @@
|
||||
"react-virtualized": "^9.22.6",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "^1.8.11",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"save-svg-as-png": "^1.4.17",
|
||||
"semver": "^7.6.2",
|
||||
"sql.js": "^1.13.0",
|
||||
|
||||
@@ -77,7 +77,7 @@ export const taskingContextFieldsOptions = ["impersonation_context", "cwd", "use
|
||||
export const defaultShortcuts = [
|
||||
"ActiveCallbacks", "Payloads", "PayloadTypesAndC2",
|
||||
"Operations", "SearchFiles", "SearchProxies",
|
||||
"CreatePayload", "Eventing",
|
||||
"CreatePayload", "Eventing", "Chat",
|
||||
].sort();
|
||||
export const operatorSettingDefaults = {
|
||||
fontSize: 13,
|
||||
@@ -154,6 +154,30 @@ export const operatorSettingDefaults = {
|
||||
dark: "#e5e7eb",
|
||||
light: "#111827",
|
||||
},
|
||||
chatMessageOperatorBackground: {
|
||||
dark: "#18191c",
|
||||
light: "#ffffff",
|
||||
},
|
||||
chatMessageSelfBackground: {
|
||||
dark: "#202123",
|
||||
light: "#f2f3f2",
|
||||
},
|
||||
chatMessageAIBackground: {
|
||||
dark: "#232220",
|
||||
light: "#f3f2ef",
|
||||
},
|
||||
chatMessageSystemBackground: {
|
||||
dark: "#252320",
|
||||
light: "#f7f3eb",
|
||||
},
|
||||
chatMarkdownSurfaceBackground: {
|
||||
dark: "#17181a",
|
||||
light: "#f3f4f5",
|
||||
},
|
||||
chatMarkdownSurfaceStrongBackground: {
|
||||
dark: "#2b2c2f",
|
||||
light: "#e8e9eb",
|
||||
},
|
||||
sectionHeaderAccent: {
|
||||
dark: "#8ab4f8",
|
||||
light: "#2563eb",
|
||||
@@ -182,10 +206,6 @@ export const operatorSettingDefaults = {
|
||||
dark: "#394c5d",
|
||||
light: "#d3d7e8",
|
||||
},
|
||||
speedDialAction: {
|
||||
dark: "#495054",
|
||||
light: "#ffffff",
|
||||
},
|
||||
chartSeries1: {
|
||||
dark: "#09bdff",
|
||||
light: "#09bdff",
|
||||
@@ -338,7 +358,7 @@ export const successfulRefresh = (data) => {
|
||||
let now = new Date();
|
||||
let serverNow = new Date(data.user.current_utc_time);
|
||||
const difference = (serverNow.getTime() - now.getTime()) ;
|
||||
let me = {...meState().user};
|
||||
let me = {...meState().user, ...(data.user || {})};
|
||||
me.server_skew = difference;
|
||||
me.login_time = now;
|
||||
meState({
|
||||
|
||||
@@ -46,6 +46,7 @@ const Reporting = lazyNamed(() => import('./pages/Reporting/Reporting'), 'Report
|
||||
const MitreAttack = lazyNamed(() => import('./pages/MITRE_ATTACK/MitreAttack'), 'MitreAttack');
|
||||
const Tags = lazyNamed(() => import('./pages/Tags/Tags'), 'Tags');
|
||||
const Eventing = lazyNamed(() => import('./pages/Eventing/Eventing'), 'Eventing');
|
||||
const Chat = lazyNamed(() => import('./pages/Chat/Chat'), 'Chat');
|
||||
const Jupyter = lazyNamed(() => import('./pages/Jupyter/Jupyter'), 'Jupyter');
|
||||
const Hasura = lazyNamed(() => import('./pages/Hasura/Hasura'), 'Hasura');
|
||||
|
||||
@@ -163,6 +164,16 @@ const getModernThemeAdditions = (themeMode, preferences = operatorSettingDefault
|
||||
hover: tableRowHoverColor,
|
||||
selected: tableSelectedColor,
|
||||
},
|
||||
chat: {
|
||||
message: {
|
||||
operatorBackground: getColor("chatMessageOperatorBackground"),
|
||||
selfBackground: getColor("chatMessageSelfBackground"),
|
||||
aiBackground: getColor("chatMessageAIBackground"),
|
||||
systemBackground: getColor("chatMessageSystemBackground"),
|
||||
markdownSurface: getColor("chatMarkdownSurfaceBackground"),
|
||||
markdownSurfaceStrong: getColor("chatMarkdownSurfaceStrongBackground"),
|
||||
},
|
||||
},
|
||||
table: {
|
||||
header: tableHeaderColor,
|
||||
headerHover: tableRowHoverColor,
|
||||
@@ -709,8 +720,6 @@ export function App(props) {
|
||||
preferences?.palette?.borderColor?.light || operatorSettingDefaults.palette.borderColor.light,
|
||||
graphGroupRGBA: themeMode === 'dark' ? `${preferences?.palette?.graphGroupColor?.dark || operatorSettingDefaults.palette.graphGroupColor.dark}80` :
|
||||
`${preferences?.palette?.graphGroupColor?.light || operatorSettingDefaults.palette.graphGroupColor.light}80`,
|
||||
speedDialAction: themeMode === 'dark' ? preferences?.palette?.speedDialAction?.dark || operatorSettingDefaults.palette.speedDialAction.dark :
|
||||
preferences?.palette?.speedDialAction?.light || operatorSettingDefaults.palette.speedDialAction.light,
|
||||
},
|
||||
folderColor: themeMode === 'dark' ? preferences?.palette?.folderColor?.dark || operatorSettingDefaults.palette.folderColor.dark :
|
||||
preferences?.palette?.folderColor?.light || operatorSettingDefaults.palette.folderColor.light,
|
||||
@@ -829,8 +838,6 @@ export function App(props) {
|
||||
operatorSettingDefaults.palette.borderColor.light,
|
||||
graphGroupRGBA: themeMode === 'dark' ? `${operatorSettingDefaults.palette.graphGroupColor.dark}80` :
|
||||
`${operatorSettingDefaults.palette.graphGroupColor.light}80`,
|
||||
speedDialAction: themeMode === 'dark' ? operatorSettingDefaults.palette.speedDialAction.dark :
|
||||
operatorSettingDefaults.palette.speedDialAction.light,
|
||||
},
|
||||
folderColor: themeMode === 'dark' ? operatorSettingDefaults.palette.folderColor.dark :
|
||||
operatorSettingDefaults.palette.folderColor.light,
|
||||
@@ -1071,6 +1078,8 @@ export function App(props) {
|
||||
element={<LoggedInRoute me={me}><Tags me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/eventing'
|
||||
element={<LoggedInRoute me={me}><Eventing me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/chat'
|
||||
element={<LoggedInRoute me={me}><Chat me={me}/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/jupyter' element={<LoggedInRoute me={me}><Jupyter/></LoggedInRoute>}/>
|
||||
<Route exact path='/new/hasura' element={<LoggedInRoute me={me}><Hasura/></LoggedInRoute>}/>
|
||||
</Routes>
|
||||
|
||||
@@ -10,6 +10,7 @@ import ManageAccountsTwoToneIcon from '@mui/icons-material/ManageAccountsTwoTone
|
||||
import { Link } from 'react-router-dom';
|
||||
import List from '@mui/material/List';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import SportsScoreIcon from '@mui/icons-material/SportsScore';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
@@ -47,8 +48,9 @@ import LocalOfferTwoToneIcon from '@mui/icons-material/LocalOfferTwoTone';
|
||||
import LightModeTwoToneIcon from '@mui/icons-material/LightModeTwoTone';
|
||||
import DarkModeTwoToneIcon from '@mui/icons-material/DarkModeTwoTone';
|
||||
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
|
||||
import ForumTwoToneIcon from '@mui/icons-material/ForumTwoTone';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import {useQuery, gql} from '@apollo/client';
|
||||
import {useQuery, useSubscription, gql} from '@apollo/client';
|
||||
import ConfirmationNumberIcon from '@mui/icons-material/ConfirmationNumber';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import AssignmentIcon from '@mui/icons-material/Assignment';
|
||||
@@ -74,6 +76,7 @@ import {reorder} from "./MythicComponents/MythicDraggableList";
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import {getSkewedNow} from "./utilities/Time";
|
||||
import {
|
||||
MythicDialogBody,
|
||||
MythicDialogButton,
|
||||
@@ -241,6 +244,79 @@ query getGlobalSettings {
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CHAT_UNREAD_STATUS_QUERY = gql`
|
||||
query ChatUnreadStatus {
|
||||
chat_channel(where: {last_message_id: {_is_null: false}}) {
|
||||
id
|
||||
archived
|
||||
last_message_id
|
||||
updated_at
|
||||
}
|
||||
chat_read_state {
|
||||
channel_id
|
||||
last_read_message_id
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CHAT_UNREAD_CHANNELS_STREAM_SUBSCRIPTION = gql`
|
||||
subscription ChatUnreadChannelsStream($now: timestamp!) {
|
||||
chat_channel_stream(batch_size: 50, cursor: {initial_value: {updated_at: $now}, ordering: ASC}, where: {last_message_id: {_is_null: false}}) {
|
||||
id
|
||||
archived
|
||||
last_message_id
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CHAT_UNREAD_READ_STATE_STREAM_SUBSCRIPTION = gql`
|
||||
subscription ChatUnreadReadStateStream($now: timestamp!) {
|
||||
chat_read_state_stream(batch_size: 50, cursor: {initial_value: {updated_at: $now}, ordering: ASC}) {
|
||||
channel_id
|
||||
last_read_message_id
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const unreadTimestampValue = (timestamp) => {
|
||||
if(!timestamp){ return 0; }
|
||||
const value = new Date(timestamp).getTime();
|
||||
return Number.isNaN(value) ? 0 : value;
|
||||
};
|
||||
|
||||
const mergeUnreadChannels = (current, incoming) => {
|
||||
if(!incoming || incoming.length === 0){ return current; }
|
||||
const rowsByID = new Map((current || []).map((channel) => [channel.id, channel]));
|
||||
incoming.forEach((channel) => {
|
||||
const existing = rowsByID.get(channel.id);
|
||||
if(!existing || unreadTimestampValue(channel.updated_at) >= unreadTimestampValue(existing.updated_at)){
|
||||
rowsByID.set(channel.id, {...existing, ...channel});
|
||||
}
|
||||
});
|
||||
return [...rowsByID.values()];
|
||||
};
|
||||
|
||||
const mergeUnreadReadStates = (current, incoming) => {
|
||||
if(!incoming || incoming.length === 0){ return current; }
|
||||
return incoming.reduce((prev, readState) => ({
|
||||
...prev,
|
||||
[readState.channel_id]: Math.max(prev[readState.channel_id] || 0, readState.last_read_message_id || 0),
|
||||
}), current);
|
||||
};
|
||||
|
||||
const getUnreadChatCount = (channels, readState) => {
|
||||
return (channels || []).reduce((count, channel) => {
|
||||
if(channel.archived){ return count; }
|
||||
const latestMessageID = channel.last_message_id || 0;
|
||||
const lastReadMessageID = readState[channel.id] || 0;
|
||||
return latestMessageID > lastReadMessageID ? count + 1 : count;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const Dashboard = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
@@ -492,6 +568,80 @@ const Eventing = () => {
|
||||
</StyledListItem>
|
||||
)
|
||||
}
|
||||
const Chat = ({me}) => {
|
||||
const theme = useTheme();
|
||||
const streamStart = React.useRef(getSkewedNow().toISOString());
|
||||
const [channels, setChannels] = React.useState([]);
|
||||
const [readState, setReadState] = React.useState({});
|
||||
const {data: initialData, error: initialError} = useQuery(CHAT_UNREAD_STATUS_QUERY, {
|
||||
skip: !me?.user?.current_operation_id,
|
||||
fetchPolicy: "no-cache",
|
||||
});
|
||||
React.useEffect(() => {
|
||||
streamStart.current = getSkewedNow().toISOString();
|
||||
setChannels([]);
|
||||
setReadState({});
|
||||
}, [me?.user?.current_operation_id]);
|
||||
React.useEffect(() => {
|
||||
if(initialData?.chat_channel){
|
||||
setChannels((prev) => mergeUnreadChannels(prev, initialData.chat_channel));
|
||||
}
|
||||
if(initialData?.chat_read_state){
|
||||
setReadState((prev) => mergeUnreadReadStates(prev, initialData.chat_read_state));
|
||||
}
|
||||
}, [initialData]);
|
||||
const {error: channelStreamError} = useSubscription(CHAT_UNREAD_CHANNELS_STREAM_SUBSCRIPTION, {
|
||||
variables: {now: streamStart.current},
|
||||
skip: !me?.user?.current_operation_id,
|
||||
fetchPolicy: "no-cache",
|
||||
onData: ({data}) => {
|
||||
const updates = data.data?.chat_channel_stream || [];
|
||||
if(updates.length > 0){
|
||||
setChannels((prev) => mergeUnreadChannels(prev, updates));
|
||||
}
|
||||
},
|
||||
onError: (errorData) => {
|
||||
console.log("chat unread channel stream error");
|
||||
console.error(errorData);
|
||||
},
|
||||
});
|
||||
const {error: readStateStreamError} = useSubscription(CHAT_UNREAD_READ_STATE_STREAM_SUBSCRIPTION, {
|
||||
variables: {now: streamStart.current},
|
||||
skip: !me?.user?.current_operation_id,
|
||||
fetchPolicy: "no-cache",
|
||||
onData: ({data}) => {
|
||||
const updates = data.data?.chat_read_state_stream || [];
|
||||
if(updates.length > 0){
|
||||
setReadState((prev) => mergeUnreadReadStates(prev, updates));
|
||||
}
|
||||
},
|
||||
onError: (errorData) => {
|
||||
console.log("chat unread status error");
|
||||
console.error(errorData);
|
||||
},
|
||||
});
|
||||
const error = initialError || channelStreamError || readStateStreamError;
|
||||
const unreadCount = React.useMemo(() => getUnreadChatCount(channels, readState), [channels, readState]);
|
||||
const tooltipTitle = error ? "Operation Chat unread status unavailable" :
|
||||
unreadCount > 0 ? `Operation Chat (${unreadCount} unread ${unreadCount === 1 ? "chat" : "chats"})` : "Operation Chat";
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} component={Link} to='/new/chat' >
|
||||
<StyledListItemIcon>
|
||||
<MythicStyledTooltip title={tooltipTitle} tooltipStyle={{display: "inline-flex"}}>
|
||||
<Badge
|
||||
badgeContent={error ? "X" : unreadCount}
|
||||
color={error ? "secondary" : "error"}
|
||||
invisible={!error && unreadCount === 0}
|
||||
max={99}
|
||||
>
|
||||
<ForumTwoToneIcon style={{color: theme.navBarTextIconColor}} fontSize={"medium"} className="mythicElement"/>
|
||||
</Badge>
|
||||
</MythicStyledTooltip>
|
||||
</StyledListItemIcon>
|
||||
<ListItemText primary={"Operation Chat"} />
|
||||
</StyledListItem>
|
||||
)
|
||||
}
|
||||
const JupyterNotebook = () => {
|
||||
return (
|
||||
<StyledListItem className={classes.listSubHeader} target="_blank" component={Link} to='/jupyter' key={"jupyter"} >
|
||||
@@ -586,7 +736,7 @@ const BrowserScripts = () => {
|
||||
const AllSettingOptions = [
|
||||
"Dashboard", "ActiveCallbacks", "Payloads", "SearchCallbacks", "SearchTasks", "SearchPayloads",
|
||||
"SearchFiles", "SearchScreenshots", "SearchCredentials", "SearchKeylogs", "SearchArtifacts", "SearchTokens", "SearchProxies",
|
||||
"SearchProcesses", "SearchTags", "Mitre", "Reporting", "Tags", "Eventing", "JupyterNotebook",
|
||||
"SearchProcesses", "SearchTags", "Mitre", "Reporting", "Tags", "Eventing", "Chat", "JupyterNotebook",
|
||||
"GraphQL", "CreatePayload", "CreateWrapper", "PayloadTypesAndC2", "Operations",
|
||||
"BrowserScripts"
|
||||
].sort();
|
||||
@@ -789,6 +939,8 @@ export function TopAppBarVertical(props) {
|
||||
return <Tags key={c + i} />
|
||||
case "Eventing":
|
||||
return <Eventing key={c + i} />
|
||||
case "Chat":
|
||||
return <Chat key={c + i} me={me} />
|
||||
case "JupyterNotebook":
|
||||
return <JupyterNotebook key={c + i} />
|
||||
case "GraphQL":
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -141,6 +141,7 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
|
||||
}
|
||||
break;
|
||||
case "auth":
|
||||
case "chat":
|
||||
try{
|
||||
const newSubs = service.subscriptions.map( s => {
|
||||
try{
|
||||
@@ -300,6 +301,17 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
|
||||
{label: "Identity Providers", value: getSubscriptionNames(w), render: renderIdentityProviderMetadata(w)},
|
||||
],
|
||||
});
|
||||
case "chat":
|
||||
return renderBaseRow({
|
||||
w,
|
||||
typeLabel: "Chat",
|
||||
metadataItems: [
|
||||
{label: "Type", value: w.type},
|
||||
{label: "Version", value: w.semver, chip: true},
|
||||
{label: "Models", value: getSubscriptionNames(w)},
|
||||
],
|
||||
hasDetails: true,
|
||||
});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -320,6 +332,18 @@ export const ConsumingServicesTableRow = ({service, showDeleted}) => {
|
||||
);
|
||||
case "auth":
|
||||
return null;
|
||||
case "chat":
|
||||
return (
|
||||
<InstalledServiceDetailSection title="Chat models" count={(w.subscriptions || []).length}>
|
||||
<InstalledServiceDefinitionList
|
||||
items={(w.subscriptions || []).map((subscription) => ({
|
||||
title: subscription.name || subscription,
|
||||
description: subscription.description || subscription.type || "",
|
||||
}))}
|
||||
emptyText="No chat models registered."
|
||||
/>
|
||||
</InstalledServiceDetailSection>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
+19
-1
@@ -135,7 +135,7 @@ function useCustomSubscription({customSubscription, dataKey}){
|
||||
return customData
|
||||
}
|
||||
|
||||
const tabTypes = ["Payload Types", "C2 Profiles", "Translators", "Command Augmentation", "3rd Party", "Webhooks", "Loggers", "Eventing", "Auth", "Browsers"];
|
||||
const tabTypes = ["Payload Types", "C2 Profiles", "Translators", "Command Augmentation", "3rd Party", "Webhooks", "Loggers", "Eventing", "Auth", "Chat", "Browsers"];
|
||||
|
||||
const filterDeleted = (c, showDeleted) => {
|
||||
if(showDeleted){
|
||||
@@ -197,6 +197,8 @@ export function PayloadTypesC2Profiles({me}){
|
||||
return visibleData.filter(c => c.__typename === "consuming_container" && c.type === "eventing");
|
||||
case "Auth":
|
||||
return visibleData.filter(c => c.__typename === "consuming_container" && c.type === "auth");
|
||||
case "Chat":
|
||||
return visibleData.filter(c => c.__typename === "consuming_container" && c.type === "chat");
|
||||
case "Browsers":
|
||||
return visibleData.filter(c => c.__typename === "custombrowser");
|
||||
default:
|
||||
@@ -235,6 +237,9 @@ export function PayloadTypesC2Profiles({me}){
|
||||
return <ContainersTabConsumingServicesPanel key={"Auth"} type={"Auth"} index={value} value={value} showDeleted={showDeleted}
|
||||
containers={getContainersForTab("Auth")} />
|
||||
case 9:
|
||||
return <ContainersTabConsumingServicesPanel key={"Chat"} type={"Chat"} index={value} value={value} showDeleted={showDeleted}
|
||||
containers={getContainersForTab("Chat")} />
|
||||
case 10:
|
||||
return <ContainersTabCustomBrowsersPanel key={"Browsers"} type={"Browsers"} index={value} value={value} showDeleted={showDeleted}
|
||||
containers={getContainersForTab("Browsers")} />
|
||||
default:
|
||||
@@ -314,6 +319,9 @@ export function PayloadTypesC2Profiles({me}){
|
||||
case "Auth":
|
||||
return <ContainersTabAuthLabel key={"auth"}
|
||||
containers={getContainersForTab("Auth")}/>;
|
||||
case "Chat":
|
||||
return <ContainersTabChatLabel key={"chat"}
|
||||
containers={getContainersForTab("Chat")}/>;
|
||||
case "Browsers":
|
||||
return <ContainersTabBrowsersLabel key={"browsers"}
|
||||
containers={getContainersForTab("Browsers")}/>;
|
||||
@@ -399,6 +407,13 @@ const ContainersTabAuthLabel = (props) => {
|
||||
} {...props}/>
|
||||
)
|
||||
}
|
||||
const ContainersTabChatLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"Chat" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
<></>
|
||||
} {...props}/>
|
||||
)
|
||||
}
|
||||
const ContainersTabCommandAugmentLabel = (props) => {
|
||||
return (
|
||||
<MythicSearchTabLabel label={"Command Augments" + (props.containers.length > 0 ? " (" + props.containers.length + ") " : "")} iconComponent={
|
||||
@@ -515,6 +530,9 @@ You can easily create your own containers, or you can use github.com/MythicAgent
|
||||
message = `Mythic supports basic username/password authentication to users within its database.
|
||||
You can extend this auth capability to support your own LDAP, SSO, or otherwise customized authentication flow with auth containers.`;
|
||||
break;
|
||||
case "Chat":
|
||||
message = `Chat containers connect operation chat channels to AI chat services.`;
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<div style={{overflowY: "hidden", flexGrow: 1}}>
|
||||
|
||||
@@ -294,11 +294,22 @@ const COLOR_EDITOR_SECTIONS = [
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Graphs and Floating Controls",
|
||||
description: "Graph grouping surfaces and legacy floating action controls.",
|
||||
title: "Chat",
|
||||
description: "Chat message bubbles and markdown block surfaces.",
|
||||
colors: [
|
||||
{name: "chatMessageOperatorBackground", display: "Other Messages", description: "Message bubble background for operator messages from other users.", preview: "chat"},
|
||||
{name: "chatMessageSelfBackground", display: "My Messages", description: "Message bubble background for messages sent by the current operator.", preview: "chat"},
|
||||
{name: "chatMessageAIBackground", display: "AI Messages", description: "Message bubble background for AI-generated messages.", preview: "chat"},
|
||||
{name: "chatMessageSystemBackground", display: "System Messages", description: "Message bubble background for chat system messages.", preview: "chat"},
|
||||
{name: "chatMarkdownSurfaceBackground", display: "Markdown Surface", description: "Background for inline code, fenced code blocks, blockquotes, and markdown table headers.", preview: "chat"},
|
||||
{name: "chatMarkdownSurfaceStrongBackground", display: "Markdown Label Surface", description: "Background for compact markdown labels such as fenced-code language tags.", preview: "chat"},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Graphs",
|
||||
description: "Graph grouping surfaces.",
|
||||
colors: [
|
||||
{name: "graphGroupColor", display: "Graph Group", description: "Grouped node backgrounds in graph-style views.", preview: "graph"},
|
||||
{name: "speedDialAction", display: "Floating Action", description: "Floating action buttons where SpeedDial controls are still used.", preview: "floatingAction"},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -517,7 +528,6 @@ const ColorUsagePreview = ({option, palette, mode}) => {
|
||||
const subtleAccentGradientStart = getPaletteValue(palette, "subtleAccentGradientStart", mode);
|
||||
const subtleAccentGradientEnd = getPaletteValue(palette, "subtleAccentGradientEnd", mode);
|
||||
const graphGroup = getPaletteValue(palette, "graphGroupColor", mode);
|
||||
const speedDialAction = getPaletteValue(palette, "speedDialAction", mode);
|
||||
const chartSeriesColors = Array.from({length: 10}, (_, index) => getPaletteValue(palette, `chartSeries${index + 1}`, mode));
|
||||
const navTop = getPaletteValue(palette, "navBarColor", mode);
|
||||
const navBottom = getPaletteValue(palette, "navBarBottomColor", mode);
|
||||
@@ -536,6 +546,12 @@ const ColorUsagePreview = ({option, palette, mode}) => {
|
||||
const extra = getPaletteValue(palette, "taskContextExtraColor", mode);
|
||||
const folder = getPaletteValue(palette, "folderColor", mode);
|
||||
const emptyFolder = getPaletteValue(palette, "emptyFolderColor", mode);
|
||||
const chatOperatorBackground = getPaletteValue(palette, "chatMessageOperatorBackground", mode);
|
||||
const chatSelfBackground = getPaletteValue(palette, "chatMessageSelfBackground", mode);
|
||||
const chatAIBackground = getPaletteValue(palette, "chatMessageAIBackground", mode);
|
||||
const chatSystemBackground = getPaletteValue(palette, "chatMessageSystemBackground", mode);
|
||||
const chatMarkdownSurface = getPaletteValue(palette, "chatMarkdownSurfaceBackground", mode);
|
||||
const chatMarkdownSurfaceStrong = getPaletteValue(palette, "chatMarkdownSurfaceStrongBackground", mode);
|
||||
const previewColor = getPaletteValue(palette, option.name, mode);
|
||||
const shellSx = {
|
||||
border: `1px solid ${addAlpha(border, "99")}`,
|
||||
@@ -637,12 +653,6 @@ const ColorUsagePreview = ({option, palette, mode}) => {
|
||||
<PreviewLabel color={textSecondary}>Grouped graph node</PreviewLabel>
|
||||
</Box>
|
||||
);
|
||||
case "floatingAction":
|
||||
return (
|
||||
<Box sx={{...shellSx, p: 0.75, backgroundColor: surfaceMuted, display: "flex", alignItems: "center", justifyContent: "center"}}>
|
||||
<Box sx={{height: 34, width: 34, borderRadius: "50%", backgroundColor: speedDialAction, border: `1px solid ${addAlpha(border, "99")}`, boxShadow: `0 4px 10px ${addAlpha(text, "33")}`}} />
|
||||
</Box>
|
||||
);
|
||||
case "chart":
|
||||
return (
|
||||
<Box sx={{...shellSx, p: 0.75, backgroundColor: paper}}>
|
||||
@@ -661,6 +671,33 @@ const ColorUsagePreview = ({option, palette, mode}) => {
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
case "chat":
|
||||
return (
|
||||
<Box sx={{...shellSx, p: 0.75, backgroundColor: background}}>
|
||||
<Box sx={{display: "grid", gap: 0.45}}>
|
||||
<Box sx={{justifySelf: "start", maxWidth: "82%", border: `1px solid ${addAlpha(border, "99")}`, borderRadius: "6px", backgroundColor: chatOperatorBackground, color: text, px: 0.75, py: 0.4}}>
|
||||
<PreviewLabel color={text}>Other operator</PreviewLabel>
|
||||
</Box>
|
||||
<Box sx={{justifySelf: "end", maxWidth: "82%", border: `1px solid ${addAlpha(border, "99")}`, borderRadius: "6px", backgroundColor: chatSelfBackground, color: text, px: 0.75, py: 0.4}}>
|
||||
<PreviewLabel color={text}>My message</PreviewLabel>
|
||||
</Box>
|
||||
<Box sx={{justifySelf: "start", maxWidth: "82%", border: `1px solid ${addAlpha(border, "99")}`, borderRadius: "6px", backgroundColor: chatAIBackground, color: text, px: 0.75, py: 0.4}}>
|
||||
<PreviewLabel color={text}>AI response</PreviewLabel>
|
||||
<Box sx={{mt: 0.35, backgroundColor: chatMarkdownSurface, border: `1px solid ${addAlpha(border, "99")}`, borderRadius: "4px", px: 0.5, py: 0.25}}>
|
||||
<Typography variant="caption" sx={{color: text, display: "block", fontFamily: "monospace", lineHeight: 1.2}}>
|
||||
markdown block
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{justifySelf: "center", maxWidth: "82%", border: `1px solid ${addAlpha(border, "99")}`, borderRadius: "6px", backgroundColor: chatSystemBackground, color: text, px: 0.75, py: 0.35}}>
|
||||
<Box sx={{display: "inline-flex", alignItems: "center", gap: 0.4}}>
|
||||
<PreviewLabel color={text}>System</PreviewLabel>
|
||||
<Box sx={{height: 12, width: 34, borderRadius: "3px", backgroundColor: chatMarkdownSurfaceStrong, border: `1px solid ${addAlpha(border, "66")}`}} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
case "task":
|
||||
return (
|
||||
<Box sx={{...shellSx, p: 1, backgroundColor: paper}}>
|
||||
|
||||
@@ -13,6 +13,7 @@ const getSubtleAccentGradient = (props) => props.theme.gradients?.subtleAccent |
|
||||
`linear-gradient(135deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.13 : 0.075)} 0%, ${alpha(props.theme.palette.background.paper, 0)} 62%)`;
|
||||
const getSubtleAccentHorizontalGradient = (props) => props.theme.gradients?.subtleAccentHorizontal ||
|
||||
`linear-gradient(90deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.12 : 0.07)} 0%, ${alpha(props.theme.palette.background.paper, 0)} 100%)`;
|
||||
const getSoftBorderColor = (props) => props.theme.table?.borderSoft || props.theme.borderColor;
|
||||
|
||||
export const GlobalStyles = createGlobalStyle`
|
||||
body {
|
||||
@@ -131,13 +132,6 @@ tspan {
|
||||
min-height: unset;
|
||||
max-width: unset;
|
||||
}
|
||||
.MuiSpeedDialAction-staticTooltipLabel {
|
||||
white-space: nowrap;
|
||||
max-width: none;
|
||||
}
|
||||
.MuiSpeedDialAction-fab {
|
||||
background-color: ${(props) => props.theme.palette.speedDialAction};
|
||||
}
|
||||
.MuiTooltip-tooltip {
|
||||
background-color: ${(props) => props.theme.palette.background.contrast};
|
||||
color: ${(props) => props.theme.palette.text.contrast};
|
||||
@@ -4655,6 +4649,7 @@ tspan {
|
||||
line-height: 1.4;
|
||||
margin-top: 0.25rem;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.mythic-metadata-grid {
|
||||
display: grid;
|
||||
@@ -9985,7 +9980,7 @@ tspan {
|
||||
line-height: 1.35;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.mythic-installed-service-browser-metadata {
|
||||
display: flex;
|
||||
@@ -10111,6 +10106,7 @@ tspan {
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.mythic-installed-service-definition-action {
|
||||
align-items: center;
|
||||
@@ -10479,6 +10475,488 @@ tspan {
|
||||
color: inherit;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.mythic-chat-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
border-radius: 8px;
|
||||
overflow: visible;
|
||||
}
|
||||
.mythic-chat-sidebar {
|
||||
background-color: ${(props) => alpha(props.theme.palette.background.paper, props.theme.palette.mode === "dark" ? 0.72 : 0.86)};
|
||||
border: 1px solid ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.18)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: inset 0 1px 0 ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.12)};
|
||||
overflow: hidden;
|
||||
}
|
||||
.mythic-chat-sidebar-toolbar {
|
||||
background-color: ${(props) => props.theme.pageHeader?.main || props.theme.palette.background.paper};
|
||||
border-bottom: 1px solid ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.18)};
|
||||
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary};
|
||||
min-height: 48px;
|
||||
}
|
||||
.mythic-chat-sidebar-toolbar,
|
||||
.mythic-chat-conversation-header,
|
||||
.mythic-chat-composer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.mythic-chat-sidebar-heading {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-chat-sidebar-heading .MuiTypography-root {
|
||||
font-weight: 750;
|
||||
}
|
||||
.mythic-chat-sidebar-count {
|
||||
background-color: ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.1)} !important;
|
||||
border: 1px solid ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.24)} !important;
|
||||
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary} !important;
|
||||
font-size: 0.68rem !important;
|
||||
font-weight: 800 !important;
|
||||
height: 22px !important;
|
||||
}
|
||||
.mythic-chat-conversation-header {
|
||||
background-color: ${(props) => props.theme.pageHeader?.main || props.theme.palette.background.paper};
|
||||
border-bottom: 1px solid ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.18)};
|
||||
box-shadow: inset 0 1px 0 ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.12)};
|
||||
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary};
|
||||
min-height: 54px;
|
||||
}
|
||||
.mythic-chat-composer {
|
||||
box-shadow: inset 0 1px 0 ${getSoftBorderColor};
|
||||
align-items: center;
|
||||
}
|
||||
.mythic-chat-channel-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.mythic-chat-channel-section > .MuiTypography-caption {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.mythic-chat-channel-button {
|
||||
align-items: center;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: 26px minmax(0, 1fr) auto;
|
||||
min-height: 42px;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
transition: background-color 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
.mythic-chat-channel-button:hover {
|
||||
background-color: ${(props) => props.theme.surfaces?.hover || props.theme.palette.action.hover} !important;
|
||||
border-color: ${getSoftBorderColor} !important;
|
||||
}
|
||||
.mythic-chat-channel-button-selected {
|
||||
box-shadow: inset 3px 0 0 var(--mythic-chat-channel-accent);
|
||||
}
|
||||
.mythic-chat-channel-icon,
|
||||
.mythic-chat-author,
|
||||
.mythic-chat-search-channel {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: 5px;
|
||||
}
|
||||
.mythic-chat-channel-icon {
|
||||
color: var(--mythic-chat-channel-accent);
|
||||
justify-content: center;
|
||||
}
|
||||
.mythic-chat-channel-button-archived .mythic-chat-channel-icon {
|
||||
color: var(--mythic-chat-channel-warning);
|
||||
}
|
||||
.mythic-chat-channel-button-archived .mythic-chat-channel-name,
|
||||
.mythic-chat-channel-button-archived .mythic-chat-channel-meta {
|
||||
color: var(--mythic-chat-channel-muted);
|
||||
}
|
||||
.mythic-chat-channel-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-chat-channel-name {
|
||||
font-weight: 650;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-chat-channel-meta {
|
||||
font-size: 0.74rem;
|
||||
opacity: 0.72;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-chat-channel-states {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
.mythic-chat-channel-state,
|
||||
.mythic-chat-unread-badge {
|
||||
align-items: center;
|
||||
background: ${(props) => props.theme.palette.action.selected};
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
padding: 2px 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-chat-channel-state-archived {
|
||||
color: var(--mythic-chat-channel-warning);
|
||||
box-shadow: inset 0 0 0 1px var(--mythic-chat-channel-warning);
|
||||
}
|
||||
.mythic-chat-channel-state-locked {
|
||||
color: var(--mythic-chat-channel-warning);
|
||||
}
|
||||
.mythic-chat-channel-state-offline {
|
||||
color: var(--mythic-chat-channel-error);
|
||||
}
|
||||
.mythic-chat-unread-badge {
|
||||
align-self: center;
|
||||
color: var(--mythic-chat-channel-error);
|
||||
box-shadow: inset 0 0 0 1px var(--mythic-chat-channel-error);
|
||||
}
|
||||
.mythic-chat-main {
|
||||
background-color: ${(props) => props.theme.palette.background.paper};
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mythic-chat-conversation-icon {
|
||||
align-items: center;
|
||||
border: 1px solid;
|
||||
border-radius: 7px;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
}
|
||||
.mythic-chat-conversation-title {
|
||||
font-weight: 800 !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
.mythic-chat-conversation-subtitle {
|
||||
opacity: 0.82;
|
||||
}
|
||||
.mythic-chat-header-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
.mythic-chat-messages {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
.mythic-chat-message-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.mythic-chat-message-row-mine {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.mythic-chat-message {
|
||||
border: 1px solid var(--mythic-chat-markdown-border);
|
||||
border-radius: 8px;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
width: fit-content;
|
||||
}
|
||||
.mythic-chat-message-header {
|
||||
align-items: center;
|
||||
background: var(--mythic-chat-markdown-surface);
|
||||
border-bottom: 1px solid var(--mythic-chat-markdown-border);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
.mythic-chat-message-system .mythic-chat-message-header {
|
||||
background: ${(props) => `linear-gradient(135deg, ${alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.34 : 0.24)} 0%, ${alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.18 : 0.12)} 58%, ${alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.08 : 0.06)} 100%)`};
|
||||
border-bottom-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.36 : 0.28)};
|
||||
}
|
||||
.mythic-chat-author {
|
||||
font-weight: 750;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-chat-author > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-chat-message-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
white-space: nowrap;
|
||||
|
||||
}
|
||||
.mythic-chat-message-actions .MuiTypography-caption {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
.mythic-chat-message-actions .MuiIconButton-root,
|
||||
.mythic-chat-header-actions .MuiIconButton-root {
|
||||
border-radius: 6px;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
.mythic-chat-paragraph {
|
||||
margin: 0 0 6px 0;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
.mythic-chat-paragraph:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.mythic-chat-markdown {
|
||||
overflow-wrap: anywhere;
|
||||
padding: 8px 10px 9px;
|
||||
}
|
||||
.mythic-chat-edit-box {
|
||||
padding: 8px 10px 9px;
|
||||
}
|
||||
.mythic-chat-message > .MuiTypography-caption {
|
||||
padding: 0 10px 8px;
|
||||
}
|
||||
.mythic-chat-heading {
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
.mythic-chat-heading-1,
|
||||
.mythic-chat-heading-2 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.mythic-chat-heading-3,
|
||||
.mythic-chat-heading-4,
|
||||
.mythic-chat-heading-5,
|
||||
.mythic-chat-heading-6 {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.mythic-chat-list {
|
||||
margin: 4px 0 8px 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
.mythic-chat-list li {
|
||||
margin: 2px 0;
|
||||
padding-left: 0.15rem;
|
||||
}
|
||||
.mythic-chat-blockquote {
|
||||
background: var(--mythic-chat-markdown-surface);
|
||||
border: 1px solid var(--mythic-chat-markdown-border);
|
||||
border-left: 3px solid var(--mythic-chat-markdown-border);
|
||||
border-radius: 7px;
|
||||
color: inherit;
|
||||
margin: 6px 0;
|
||||
padding: 4px 8px 4px 10px;
|
||||
}
|
||||
.mythic-chat-rule {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--mythic-chat-markdown-border);
|
||||
margin: 8px 0;
|
||||
}
|
||||
.mythic-chat-inline-code {
|
||||
background: var(--mythic-chat-markdown-surface);
|
||||
border: 1px solid var(--mythic-chat-markdown-border);
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 0.88em;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
.mythic-chat-code-block {
|
||||
margin: 6px 0;
|
||||
position: relative;
|
||||
}
|
||||
.mythic-chat-code-block pre {
|
||||
background: var(--mythic-chat-markdown-surface);
|
||||
border: 1px solid var(--mythic-chat-markdown-border);
|
||||
border-radius: 7px;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
.mythic-chat-code-block code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
.mythic-chat-code-language {
|
||||
background: var(--mythic-chat-markdown-surface-strong);
|
||||
border: 1px solid var(--mythic-chat-markdown-border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
padding: 1px 5px;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 5px;
|
||||
}
|
||||
.mythic-chat-table-wrap {
|
||||
margin: 6px 0;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
width: fit-content;
|
||||
}
|
||||
.mythic-chat-table {
|
||||
font-size: 0.82rem;
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-chat-table .MuiTableCell-root {
|
||||
font-size: 0.82rem;
|
||||
vertical-align: top;
|
||||
}
|
||||
.mythic-chat-edit-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.mythic-chat-edit-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.mythic-chat-dialog-content {
|
||||
padding-top: 20px !important;
|
||||
}
|
||||
.mythic-chat-composer .MuiFormControl-root {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.mythic-chat-composer .MuiOutlinedInput-root {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
padding-left: 0;
|
||||
}
|
||||
.mythic-chat-composer .MuiOutlinedInput-notchedOutline {
|
||||
border: 0 !important;
|
||||
}
|
||||
.mythic-chat-composer .MuiInputBase-input {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.mythic-chat-send-button .MuiSvgIcon-root {
|
||||
display: block;
|
||||
}
|
||||
.mythic-chat-system-button.MuiIconButton-root {
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
flex: 0 0 auto;
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
}
|
||||
.mythic-chat-system-destination {
|
||||
align-items: center;
|
||||
background-color: ${(props) => props.theme.surfaces?.hover || props.theme.palette.action.hover};
|
||||
border: 1px solid ${getSoftBorderColor};
|
||||
border-radius: 7px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.mythic-chat-system-destination .MuiSvgIcon-root {
|
||||
color: ${(props) => props.theme.palette.warning.main};
|
||||
}
|
||||
.mythic-chat-system-options {
|
||||
background-color: ${(props) => props.theme.surfaces?.hover || props.theme.palette.action.hover};
|
||||
border: 1px solid ${getSoftBorderColor};
|
||||
border-radius: 7px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
.mythic-chat-dialog-content .MuiFormControl-root {
|
||||
margin-top: 4px;
|
||||
}
|
||||
.mythic-chat-empty-state {
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
min-height: 12rem;
|
||||
opacity: 0.72;
|
||||
text-align: center;
|
||||
}
|
||||
.mythic-chat-empty-list {
|
||||
background: ${(props) => props.theme.palette.action.hover};
|
||||
border-radius: 7px;
|
||||
font-size: 0.76rem;
|
||||
opacity: 0.72;
|
||||
padding: 9px;
|
||||
}
|
||||
.mythic-chat-search-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 55vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.mythic-chat-search-result {
|
||||
background: transparent;
|
||||
border: 1px solid ${getSoftBorderColor};
|
||||
border-radius: 7px;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
.mythic-chat-search-message {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-chat-search-meta {
|
||||
font-size: 0.74rem;
|
||||
opacity: 0.72;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.mythic-chat-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.mythic-chat-sidebar {
|
||||
max-height: 220px;
|
||||
}
|
||||
.mythic-chat-message {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
.ace_editor{
|
||||
background-color: ${(props) => props.theme.outputBackgroundColor + "20"};
|
||||
}
|
||||
|
||||
@@ -51,6 +51,80 @@ type Query {
|
||||
): ConfigCheckOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
chatCancelRequest(
|
||||
request_id: Int!
|
||||
): ChatActionOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
chatCreateChannel(
|
||||
name: String!
|
||||
description: String
|
||||
channel_type: String
|
||||
chat_container_id: Int
|
||||
chat_model: String
|
||||
locked: Boolean
|
||||
ai_metadata: jsonb
|
||||
): ChatActionOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
chatCreateMessage(
|
||||
channel_id: Int!
|
||||
message: String!
|
||||
system_message: Boolean
|
||||
all_operations: Boolean
|
||||
): ChatActionOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
chatDeleteMessage(
|
||||
message_id: Int!
|
||||
): ChatActionOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
chatEditMessage(
|
||||
message_id: Int!
|
||||
message: String!
|
||||
): ChatActionOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
chatMarkRead(
|
||||
channel_id: Int!
|
||||
last_read_message_id: Int
|
||||
): ChatActionOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
chatRetryRequest(
|
||||
request_id: Int!
|
||||
): ChatActionOutput
|
||||
}
|
||||
|
||||
type Query {
|
||||
chatSearch(
|
||||
query: String!
|
||||
channel_id: Int
|
||||
limit: Int
|
||||
offset: Int
|
||||
): ChatActionOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
chatUpdateChannel(
|
||||
channel_id: Int!
|
||||
name: String
|
||||
description: String
|
||||
archived: Boolean
|
||||
locked: Boolean
|
||||
chat_model: String
|
||||
ai_metadata: jsonb
|
||||
): ChatActionOutput
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
consumingServicesTestLog(
|
||||
service_type: String!
|
||||
@@ -1327,3 +1401,28 @@ type custombrowserExportFunctionOutput {
|
||||
error: String
|
||||
}
|
||||
|
||||
type ChatActionOutput {
|
||||
status: String!
|
||||
error: String
|
||||
id: Int
|
||||
channel_id: Int
|
||||
message_id: Int
|
||||
request_id: Int
|
||||
response_message_id: Int
|
||||
results: [ChatSearchResult]
|
||||
}
|
||||
|
||||
type ChatSearchResult {
|
||||
id: Int!
|
||||
channel_id: Int!
|
||||
channel_name: String!
|
||||
channel_slug: String!
|
||||
channel_type: String!
|
||||
author_type: String!
|
||||
sender_display_name: String!
|
||||
message: String!
|
||||
edited: Boolean!
|
||||
status: String!
|
||||
created_at: String!
|
||||
rank: Float!
|
||||
}
|
||||
|
||||
@@ -91,6 +91,107 @@ actions:
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
comment: Using the UUID of a payload, ask's that payload's C2 Profiles to check if the Payload's configuration matches with the configuration for the profile itself.
|
||||
- name: chatCancelRequest
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/chat_cancel_request_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
comment: Cancel an in-flight AI chat request
|
||||
- name: chatCreateChannel
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/chat_create_channel_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
comment: Create a standard or AI chat channel in the current operation
|
||||
- name: chatCreateMessage
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/chat_create_message_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
comment: Create a chat message, submit an AI chat prompt, or submit an admin-only system message across one or all operations
|
||||
- name: chatDeleteMessage
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/chat_delete_message_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
comment: Delete a chat message visible in the current operation
|
||||
- name: chatEditMessage
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/chat_edit_message_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
comment: Edit an operator-authored chat message
|
||||
- name: chatMarkRead
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/chat_mark_read_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: spectator
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
comment: Mark a chat channel as read for the current operator
|
||||
- name: chatRetryRequest
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/chat_retry_request_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
comment: Retry an AI chat request in the same AI channel
|
||||
- name: chatSearch
|
||||
definition:
|
||||
kind: ""
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/chat_search_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: spectator
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
comment: Search operation chat messages visible to the current token
|
||||
- name: chatUpdateChannel
|
||||
definition:
|
||||
kind: synchronous
|
||||
handler: '{{MYTHIC_ACTIONS_URL_BASE}}/chat_update_channel_webhook'
|
||||
forward_client_headers: true
|
||||
permissions:
|
||||
- role: operator
|
||||
- role: operation_admin
|
||||
- role: mythic_admin
|
||||
- role: developer
|
||||
comment: Update, archive, or lock a chat channel
|
||||
- name: consumingServicesTestLog
|
||||
definition:
|
||||
kind: synchronous
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
table:
|
||||
name: chat_channel
|
||||
schema: public
|
||||
object_relationships:
|
||||
- name: archived_operator
|
||||
using:
|
||||
foreign_key_constraint_on: archived_by
|
||||
- name: chat_container
|
||||
using:
|
||||
foreign_key_constraint_on: chat_container_id
|
||||
- name: creator
|
||||
using:
|
||||
foreign_key_constraint_on: created_by
|
||||
- name: locked_operator
|
||||
using:
|
||||
foreign_key_constraint_on: locked_by
|
||||
- name: operation
|
||||
using:
|
||||
foreign_key_constraint_on: operation_id
|
||||
array_relationships:
|
||||
- name: messages
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: channel_id
|
||||
table:
|
||||
name: chat_message
|
||||
schema: public
|
||||
- name: read_states
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: channel_id
|
||||
table:
|
||||
name: chat_read_state
|
||||
schema: public
|
||||
- name: requests
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: channel_id
|
||||
table:
|
||||
name: chat_request
|
||||
schema: public
|
||||
select_permissions:
|
||||
- role: mythic_admin
|
||||
permission:
|
||||
columns:
|
||||
- ai_metadata
|
||||
- archived
|
||||
- archived_at
|
||||
- archived_by
|
||||
- channel_type
|
||||
- chat_container_id
|
||||
- chat_model
|
||||
- created_at
|
||||
- created_by
|
||||
- description
|
||||
- id
|
||||
- locked
|
||||
- locked_at
|
||||
- locked_by
|
||||
- last_message_id
|
||||
- name
|
||||
- operation_id
|
||||
- slug
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- _or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: operation_admin
|
||||
permission:
|
||||
columns:
|
||||
- ai_metadata
|
||||
- archived
|
||||
- archived_at
|
||||
- archived_by
|
||||
- channel_type
|
||||
- chat_container_id
|
||||
- chat_model
|
||||
- created_at
|
||||
- created_by
|
||||
- description
|
||||
- id
|
||||
- locked
|
||||
- locked_at
|
||||
- locked_by
|
||||
- last_message_id
|
||||
- name
|
||||
- operation_id
|
||||
- slug
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- _or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: operator
|
||||
permission:
|
||||
columns:
|
||||
- ai_metadata
|
||||
- archived
|
||||
- archived_at
|
||||
- archived_by
|
||||
- channel_type
|
||||
- chat_container_id
|
||||
- chat_model
|
||||
- created_at
|
||||
- created_by
|
||||
- description
|
||||
- id
|
||||
- locked
|
||||
- locked_at
|
||||
- locked_by
|
||||
- last_message_id
|
||||
- name
|
||||
- operation_id
|
||||
- slug
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- _or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: spectator
|
||||
permission:
|
||||
columns:
|
||||
- ai_metadata
|
||||
- archived
|
||||
- archived_at
|
||||
- archived_by
|
||||
- channel_type
|
||||
- chat_container_id
|
||||
- chat_model
|
||||
- created_at
|
||||
- created_by
|
||||
- description
|
||||
- id
|
||||
- locked
|
||||
- locked_at
|
||||
- locked_by
|
||||
- last_message_id
|
||||
- name
|
||||
- operation_id
|
||||
- slug
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- _or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
@@ -0,0 +1,197 @@
|
||||
table:
|
||||
name: chat_message
|
||||
schema: public
|
||||
object_relationships:
|
||||
- name: apitoken
|
||||
using:
|
||||
foreign_key_constraint_on: apitokens_id
|
||||
- name: channel
|
||||
using:
|
||||
foreign_key_constraint_on: channel_id
|
||||
- name: chat_container
|
||||
using:
|
||||
foreign_key_constraint_on: chat_container_id
|
||||
- name: deleted_operator
|
||||
using:
|
||||
foreign_key_constraint_on: deleted_by
|
||||
- name: operation
|
||||
using:
|
||||
foreign_key_constraint_on: operation_id
|
||||
- name: operator
|
||||
using:
|
||||
foreign_key_constraint_on: operator_id
|
||||
array_relationships:
|
||||
- name: requests_as_prompt
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: request_message_id
|
||||
table:
|
||||
name: chat_request
|
||||
schema: public
|
||||
- name: requests_as_response
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: response_message_id
|
||||
table:
|
||||
name: chat_request
|
||||
schema: public
|
||||
- name: read_states
|
||||
using:
|
||||
foreign_key_constraint_on:
|
||||
column: last_read_message_id
|
||||
table:
|
||||
name: chat_read_state
|
||||
schema: public
|
||||
select_permissions:
|
||||
- role: mythic_admin
|
||||
permission:
|
||||
columns:
|
||||
- apitokens_id
|
||||
- author_type
|
||||
- channel_id
|
||||
- chat_container_id
|
||||
- created_at
|
||||
- deleted
|
||||
- deleted_at
|
||||
- deleted_by
|
||||
- edited
|
||||
- edited_at
|
||||
- id
|
||||
- message
|
||||
- metadata
|
||||
- operation_id
|
||||
- operator_id
|
||||
- sender_display_name
|
||||
- status
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- channel:
|
||||
_or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: operation_admin
|
||||
permission:
|
||||
columns:
|
||||
- apitokens_id
|
||||
- author_type
|
||||
- channel_id
|
||||
- chat_container_id
|
||||
- created_at
|
||||
- deleted
|
||||
- deleted_at
|
||||
- deleted_by
|
||||
- edited
|
||||
- edited_at
|
||||
- id
|
||||
- message
|
||||
- metadata
|
||||
- operation_id
|
||||
- operator_id
|
||||
- sender_display_name
|
||||
- status
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- channel:
|
||||
_or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: operator
|
||||
permission:
|
||||
columns:
|
||||
- apitokens_id
|
||||
- author_type
|
||||
- channel_id
|
||||
- chat_container_id
|
||||
- created_at
|
||||
- deleted
|
||||
- deleted_at
|
||||
- deleted_by
|
||||
- edited
|
||||
- edited_at
|
||||
- id
|
||||
- message
|
||||
- metadata
|
||||
- operation_id
|
||||
- operator_id
|
||||
- sender_display_name
|
||||
- status
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- channel:
|
||||
_or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: spectator
|
||||
permission:
|
||||
columns:
|
||||
- apitokens_id
|
||||
- author_type
|
||||
- channel_id
|
||||
- chat_container_id
|
||||
- created_at
|
||||
- deleted
|
||||
- deleted_at
|
||||
- deleted_by
|
||||
- edited
|
||||
- edited_at
|
||||
- id
|
||||
- message
|
||||
- metadata
|
||||
- operation_id
|
||||
- operator_id
|
||||
- sender_display_name
|
||||
- status
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- channel:
|
||||
_or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
@@ -0,0 +1,125 @@
|
||||
table:
|
||||
name: chat_read_state
|
||||
schema: public
|
||||
object_relationships:
|
||||
- name: channel
|
||||
using:
|
||||
foreign_key_constraint_on: channel_id
|
||||
- name: last_read_message
|
||||
using:
|
||||
foreign_key_constraint_on: last_read_message_id
|
||||
- name: operation
|
||||
using:
|
||||
foreign_key_constraint_on: operation_id
|
||||
- name: operator
|
||||
using:
|
||||
foreign_key_constraint_on: operator_id
|
||||
select_permissions:
|
||||
- role: mythic_admin
|
||||
permission:
|
||||
columns:
|
||||
- channel_id
|
||||
- last_read_message_id
|
||||
- operation_id
|
||||
- operator_id
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- operator_id:
|
||||
_eq: X-Hasura-User-Id
|
||||
- channel:
|
||||
_or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: operation_admin
|
||||
permission:
|
||||
columns:
|
||||
- channel_id
|
||||
- last_read_message_id
|
||||
- operation_id
|
||||
- operator_id
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- operator_id:
|
||||
_eq: X-Hasura-User-Id
|
||||
- channel:
|
||||
_or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: operator
|
||||
permission:
|
||||
columns:
|
||||
- channel_id
|
||||
- last_read_message_id
|
||||
- operation_id
|
||||
- operator_id
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- operator_id:
|
||||
_eq: X-Hasura-User-Id
|
||||
- channel:
|
||||
_or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: spectator
|
||||
permission:
|
||||
columns:
|
||||
- channel_id
|
||||
- last_read_message_id
|
||||
- operation_id
|
||||
- operator_id
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- operator_id:
|
||||
_eq: X-Hasura-User-Id
|
||||
- channel:
|
||||
_or:
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: standard
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-read-operation
|
||||
- _and:
|
||||
- channel_type:
|
||||
_eq: ai
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
@@ -0,0 +1,130 @@
|
||||
table:
|
||||
name: chat_request
|
||||
schema: public
|
||||
object_relationships:
|
||||
- name: channel
|
||||
using:
|
||||
foreign_key_constraint_on: channel_id
|
||||
- name: chat_container
|
||||
using:
|
||||
foreign_key_constraint_on: chat_container_id
|
||||
- name: creator
|
||||
using:
|
||||
foreign_key_constraint_on: created_by
|
||||
- name: operation
|
||||
using:
|
||||
foreign_key_constraint_on: operation_id
|
||||
- name: request_message
|
||||
using:
|
||||
foreign_key_constraint_on: request_message_id
|
||||
- name: response_message
|
||||
using:
|
||||
foreign_key_constraint_on: response_message_id
|
||||
- name: retry_of
|
||||
using:
|
||||
foreign_key_constraint_on: retry_of_id
|
||||
select_permissions:
|
||||
- role: mythic_admin
|
||||
permission:
|
||||
columns:
|
||||
- cancelled_at
|
||||
- channel_id
|
||||
- chat_container_id
|
||||
- completed_at
|
||||
- context_snapshot
|
||||
- created_at
|
||||
- created_by
|
||||
- error
|
||||
- id
|
||||
- model
|
||||
- operation_id
|
||||
- request_message_id
|
||||
- response_message_id
|
||||
- retry_of_id
|
||||
- status
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: operation_admin
|
||||
permission:
|
||||
columns:
|
||||
- cancelled_at
|
||||
- channel_id
|
||||
- chat_container_id
|
||||
- completed_at
|
||||
- context_snapshot
|
||||
- created_at
|
||||
- created_by
|
||||
- error
|
||||
- id
|
||||
- model
|
||||
- operation_id
|
||||
- request_message_id
|
||||
- response_message_id
|
||||
- retry_of_id
|
||||
- status
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: operator
|
||||
permission:
|
||||
columns:
|
||||
- cancelled_at
|
||||
- channel_id
|
||||
- chat_container_id
|
||||
- completed_at
|
||||
- context_snapshot
|
||||
- created_at
|
||||
- created_by
|
||||
- error
|
||||
- id
|
||||
- model
|
||||
- operation_id
|
||||
- request_message_id
|
||||
- response_message_id
|
||||
- retry_of_id
|
||||
- status
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
- role: spectator
|
||||
permission:
|
||||
columns:
|
||||
- cancelled_at
|
||||
- channel_id
|
||||
- chat_container_id
|
||||
- completed_at
|
||||
- context_snapshot
|
||||
- created_at
|
||||
- created_by
|
||||
- error
|
||||
- id
|
||||
- model
|
||||
- operation_id
|
||||
- request_message_id
|
||||
- response_message_id
|
||||
- retry_of_id
|
||||
- status
|
||||
- updated_at
|
||||
filter:
|
||||
_and:
|
||||
- operation_id:
|
||||
_eq: X-Hasura-current-operation-id
|
||||
- operation_id:
|
||||
_lte: X-Hasura-Scope-chat-ai-read-operation
|
||||
allow_aggregations: true
|
||||
@@ -14,6 +14,10 @@
|
||||
- "!include public_callbackgraphedge.yaml"
|
||||
- "!include public_callbackport.yaml"
|
||||
- "!include public_callbacktoken.yaml"
|
||||
- "!include public_chat_channel.yaml"
|
||||
- "!include public_chat_message.yaml"
|
||||
- "!include public_chat_read_state.yaml"
|
||||
- "!include public_chat_request.yaml"
|
||||
- "!include public_command.yaml"
|
||||
- "!include public_commandparameters.yaml"
|
||||
- "!include public_consuming_container.yaml"
|
||||
|
||||
@@ -18,6 +18,12 @@ const (
|
||||
SCOPE_CALLBACK_READ = "callback.read"
|
||||
SCOPE_CALLBACK_WRITE = "callback.write"
|
||||
|
||||
SCOPE_CHAT_READ = "chat.read"
|
||||
SCOPE_CHAT_WRITE = "chat.write"
|
||||
|
||||
SCOPE_CHAT_AI_READ = "chat-ai.read"
|
||||
SCOPE_CHAT_AI_WRITE = "chat-ai.write"
|
||||
|
||||
SCOPE_CONTAINER_FILE_READ = "container_file.read"
|
||||
SCOPE_CONTAINER_FILE_WRITE = "container_file.write"
|
||||
|
||||
@@ -85,6 +91,10 @@ var scopeDefinitions = []ScopeDefinition{
|
||||
{Name: SCOPE_C2_WRITE, DisplayName: "Write C2 profiles", Description: "Start, stop, update, or interact with C2 profile containers. Includes read access.", Resource: "c2", Access: "write", Includes: []string{SCOPE_C2_READ}},
|
||||
{Name: SCOPE_CALLBACK_READ, DisplayName: "Read callbacks", Description: "View callbacks and callback metadata for accessible operations.", Resource: "callback", Access: "read"},
|
||||
{Name: SCOPE_CALLBACK_WRITE, DisplayName: "Write callbacks", Description: "Create or update callback state. Includes read access.", Resource: "callback", Access: "write", Includes: []string{SCOPE_CALLBACK_READ}},
|
||||
{Name: SCOPE_CHAT_READ, DisplayName: "Read operation chat", Description: "View and search standard chat channels and messages for accessible operations.", Resource: "chat", Access: "read"},
|
||||
{Name: SCOPE_CHAT_WRITE, DisplayName: "Write operation chat", Description: "Create standard chat channels and post, edit, or delete standard chat messages. Includes read access.", Resource: "chat", Access: "write", Includes: []string{SCOPE_CHAT_READ}},
|
||||
{Name: SCOPE_CHAT_AI_READ, DisplayName: "Read AI chat", Description: "View and search AI-backed chat sessions and messages for accessible operations.", Resource: "chat-ai", Access: "read"},
|
||||
{Name: SCOPE_CHAT_AI_WRITE, DisplayName: "Write AI chat", Description: "Create AI chat sessions, post prompts, and receive AI chat responses. Includes read access.", Resource: "chat-ai", Access: "write", Includes: []string{SCOPE_CHAT_AI_READ}},
|
||||
{Name: SCOPE_CONTAINER_FILE_READ, DisplayName: "Read container files", Description: "List and download files exposed by Mythic service containers.", Resource: "container_file", Access: "read"},
|
||||
{Name: SCOPE_CONTAINER_FILE_WRITE, DisplayName: "Write container files", Description: "Write or remove files exposed by Mythic service containers. Includes read access.", Resource: "container_file", Access: "write", Includes: []string{SCOPE_CONTAINER_FILE_READ}},
|
||||
{Name: SCOPE_CREDENTIAL_READ, DisplayName: "Read credentials", Description: "View credential records for accessible operations.", Resource: "credential", Access: "read"},
|
||||
|
||||
@@ -103,6 +103,18 @@ func TestAllowsScope(t *testing.T) {
|
||||
required: SCOPE_FILE_WRITE,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "hyphenated resource wildcard grants ai chat scope",
|
||||
granted: []string{"chat-ai.*"},
|
||||
required: SCOPE_CHAT_AI_WRITE,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "chat write does not grant ai chat write",
|
||||
granted: []string{SCOPE_CHAT_WRITE},
|
||||
required: SCOPE_CHAT_AI_WRITE,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "file write grants upload",
|
||||
granted: []string{SCOPE_FILE_WRITE},
|
||||
@@ -232,6 +244,16 @@ func TestHasuraScopeRequirementsUseScalarClaims(t *testing.T) {
|
||||
wantAnchor: "operation",
|
||||
wantClaim: "x-hasura-scope-operation-write-operation",
|
||||
},
|
||||
{
|
||||
scope: SCOPE_CHAT_READ,
|
||||
wantAnchor: "operation",
|
||||
wantClaim: "x-hasura-scope-chat-read-operation",
|
||||
},
|
||||
{
|
||||
scope: SCOPE_CHAT_AI_WRITE,
|
||||
wantAnchor: "operation",
|
||||
wantClaim: "x-hasura-scope-chat-ai-write-operation",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -92,6 +92,225 @@ create index if not exists operationeventlog_unresolved_warning_source_operation
|
||||
on "public"."operationeventlog" using btree (source, operation_id, "level")
|
||||
where warning=true and resolved=false and deleted=false;
|
||||
|
||||
create table if not exists "public"."chat_channel" (
|
||||
id integer generated by default as identity primary key,
|
||||
operation_id integer not null references "public"."operation"(id) on delete cascade,
|
||||
name text not null,
|
||||
slug text not null,
|
||||
description text not null default '',
|
||||
channel_type text not null default 'standard',
|
||||
created_by integer not null references "public"."operator"(id) on delete restrict,
|
||||
archived boolean not null default false,
|
||||
archived_by integer references "public"."operator"(id) on delete set null,
|
||||
archived_at timestamp without time zone,
|
||||
locked boolean not null default false,
|
||||
locked_by integer references "public"."operator"(id) on delete set null,
|
||||
locked_at timestamp without time zone,
|
||||
chat_container_id integer references "public"."consuming_container"(id) on delete set null,
|
||||
chat_model text not null default '',
|
||||
ai_metadata jsonb not null default jsonb_build_object(),
|
||||
created_at timestamp without time zone not null default now(),
|
||||
updated_at timestamp without time zone not null default now(),
|
||||
constraint chat_channel_type_check check (channel_type in ('standard', 'ai')),
|
||||
constraint chat_channel_ai_container_check check (channel_type <> 'ai' or chat_container_id is not null)
|
||||
);
|
||||
|
||||
create unique index if not exists chat_channel_operation_slug_unique
|
||||
on "public"."chat_channel" using btree (operation_id, lower(slug));
|
||||
|
||||
create index if not exists chat_channel_operation_type_archived_idx
|
||||
on "public"."chat_channel" using btree (operation_id, channel_type, archived, name);
|
||||
|
||||
create or replace trigger set_public_chat_channel_updated_at
|
||||
before update on "public"."chat_channel"
|
||||
for each row execute function public.set_current_timestamp_updated_at();
|
||||
|
||||
create table if not exists "public"."chat_message" (
|
||||
id integer generated by default as identity primary key,
|
||||
operation_id integer not null references "public"."operation"(id) on delete cascade,
|
||||
channel_id integer not null references "public"."chat_channel"(id) on delete cascade,
|
||||
operator_id integer references "public"."operator"(id) on delete set null,
|
||||
apitokens_id integer references "public"."apitokens"(id) on delete set null,
|
||||
author_type text not null default 'operator',
|
||||
chat_container_id integer references "public"."consuming_container"(id) on delete set null,
|
||||
sender_display_name text not null default '',
|
||||
message text not null default '',
|
||||
deleted boolean not null default false,
|
||||
search_vector tsvector generated always as (
|
||||
to_tsvector('simple', case when deleted then '' else coalesce(message, '') end)
|
||||
) stored,
|
||||
edited boolean not null default false,
|
||||
edited_at timestamp without time zone,
|
||||
deleted_by integer references "public"."operator"(id) on delete set null,
|
||||
deleted_at timestamp without time zone,
|
||||
status text not null default 'complete',
|
||||
metadata jsonb not null default jsonb_build_object(),
|
||||
created_at timestamp without time zone not null default now(),
|
||||
updated_at timestamp without time zone not null default now(),
|
||||
constraint chat_message_author_type_check check (author_type in ('operator', 'ai', 'system')),
|
||||
constraint chat_message_status_check check (status in ('pending', 'streaming', 'complete', 'error', 'cancelled'))
|
||||
);
|
||||
|
||||
create index if not exists chat_message_operation_channel_id_idx
|
||||
on "public"."chat_message" using btree (operation_id, channel_id, id desc);
|
||||
|
||||
create index if not exists chat_message_operation_updated_idx
|
||||
on "public"."chat_message" using btree (operation_id, updated_at desc);
|
||||
|
||||
create index if not exists chat_message_operator_idx
|
||||
on "public"."chat_message" using btree (operator_id);
|
||||
|
||||
create index if not exists chat_message_search_vector_idx
|
||||
on "public"."chat_message" using gin (search_vector);
|
||||
|
||||
create or replace trigger set_public_chat_message_updated_at
|
||||
before update on "public"."chat_message"
|
||||
for each row execute function public.set_current_timestamp_updated_at();
|
||||
|
||||
alter table "public"."chat_channel"
|
||||
add column if not exists last_message_id integer;
|
||||
|
||||
update "public"."chat_channel" channel
|
||||
set last_message_id = latest.id
|
||||
from (
|
||||
select distinct on (channel_id)
|
||||
channel_id,
|
||||
id
|
||||
from "public"."chat_message"
|
||||
order by channel_id, id desc
|
||||
) latest
|
||||
where channel.id = latest.channel_id
|
||||
and channel.last_message_id is distinct from latest.id;
|
||||
|
||||
create index if not exists chat_channel_operation_unread_idx
|
||||
on "public"."chat_channel" using btree (operation_id, archived, last_message_id);
|
||||
|
||||
-- +migrate StatementBegin
|
||||
create or replace function public.update_chat_channel_last_message_id() returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
update "public"."chat_channel"
|
||||
set last_message_id = new.id,
|
||||
updated_at = now()
|
||||
where id = new.channel_id
|
||||
and (last_message_id is null or last_message_id < new.id);
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
-- +migrate StatementEnd
|
||||
|
||||
create or replace trigger update_chat_channel_last_message_id_trigger
|
||||
after insert on "public"."chat_message"
|
||||
for each row execute function public.update_chat_channel_last_message_id();
|
||||
|
||||
create table if not exists "public"."chat_request" (
|
||||
id integer generated by default as identity primary key,
|
||||
operation_id integer not null references "public"."operation"(id) on delete cascade,
|
||||
channel_id integer not null references "public"."chat_channel"(id) on delete cascade,
|
||||
request_message_id integer not null references "public"."chat_message"(id) on delete cascade,
|
||||
response_message_id integer not null references "public"."chat_message"(id) on delete cascade,
|
||||
chat_container_id integer not null references "public"."consuming_container"(id) on delete restrict,
|
||||
model text not null default '',
|
||||
status text not null default 'pending',
|
||||
error text not null default '',
|
||||
context_snapshot jsonb not null default jsonb_build_object(),
|
||||
retry_of_id integer references "public"."chat_request"(id) on delete set null,
|
||||
created_by integer not null references "public"."operator"(id) on delete restrict,
|
||||
created_at timestamp without time zone not null default now(),
|
||||
updated_at timestamp without time zone not null default now(),
|
||||
completed_at timestamp without time zone,
|
||||
cancelled_at timestamp without time zone,
|
||||
constraint chat_request_status_check check (status in ('pending', 'streaming', 'complete', 'error', 'cancelled'))
|
||||
);
|
||||
|
||||
create index if not exists chat_request_operation_channel_idx
|
||||
on "public"."chat_request" using btree (operation_id, channel_id, id desc);
|
||||
|
||||
create index if not exists chat_request_response_message_id_idx
|
||||
on "public"."chat_request" using btree (response_message_id);
|
||||
|
||||
create or replace trigger set_public_chat_request_updated_at
|
||||
before update on "public"."chat_request"
|
||||
for each row execute function public.set_current_timestamp_updated_at();
|
||||
|
||||
create table if not exists "public"."chat_read_state" (
|
||||
operation_id integer not null references "public"."operation"(id) on delete cascade,
|
||||
channel_id integer not null references "public"."chat_channel"(id) on delete cascade,
|
||||
operator_id integer not null references "public"."operator"(id) on delete cascade,
|
||||
last_read_message_id integer references "public"."chat_message"(id) on delete set null,
|
||||
updated_at timestamp without time zone not null default now(),
|
||||
primary key (operator_id, channel_id)
|
||||
);
|
||||
|
||||
create index if not exists chat_read_state_operation_operator_idx
|
||||
on "public"."chat_read_state" using btree (operation_id, operator_id);
|
||||
|
||||
create or replace trigger set_public_chat_read_state_updated_at
|
||||
before update on "public"."chat_read_state"
|
||||
for each row execute function public.set_current_timestamp_updated_at();
|
||||
|
||||
-- +migrate StatementBegin
|
||||
create or replace function public.create_default_chat_channel_for_operation() returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
insert into "public"."chat_channel" (
|
||||
operation_id,
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
channel_type,
|
||||
created_by
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
'general',
|
||||
'general',
|
||||
'Default operation chat channel',
|
||||
'standard',
|
||||
new.admin_id
|
||||
)
|
||||
on conflict do nothing;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
-- +migrate StatementEnd
|
||||
|
||||
create or replace trigger create_default_chat_channel_for_operation_trigger
|
||||
after insert on "public"."operation"
|
||||
for each row execute function public.create_default_chat_channel_for_operation();
|
||||
|
||||
insert into "public"."chat_channel" (
|
||||
operation_id,
|
||||
name,
|
||||
slug,
|
||||
description,
|
||||
channel_type,
|
||||
created_by
|
||||
)
|
||||
select
|
||||
operation.id,
|
||||
'general',
|
||||
'general',
|
||||
'Default operation chat channel',
|
||||
'standard',
|
||||
operation.admin_id
|
||||
from "public"."operation"
|
||||
on conflict do nothing;
|
||||
|
||||
update "public"."chat_channel"
|
||||
set
|
||||
name = 'general',
|
||||
archived = false,
|
||||
archived_by = null,
|
||||
archived_at = null
|
||||
where channel_type = 'standard'
|
||||
and lower(slug) = 'general'
|
||||
and (name <> 'general' or archived = true or archived_by is not null or archived_at is not null);
|
||||
|
||||
alter table "public"."task"
|
||||
add column if not exists subtask_callback_function_started boolean not null default false,
|
||||
add column if not exists group_callback_function_started boolean not null default false,
|
||||
@@ -227,6 +446,15 @@ drop index if exists "public"."mythictree_operation_tree_timestamp_idx";
|
||||
drop index if exists "public"."mythictree_operation_tree_host_full_callback_idx";
|
||||
drop index if exists "public"."mythictree_operation_tree_host_parent_callback_idx";
|
||||
drop index if exists "public"."operationeventlog_unresolved_warning_source_operation_level_idx";
|
||||
drop index if exists "public"."chat_channel_operation_unread_idx";
|
||||
drop trigger if exists update_chat_channel_last_message_id_trigger on "public"."chat_message";
|
||||
drop function if exists public.update_chat_channel_last_message_id();
|
||||
drop trigger if exists create_default_chat_channel_for_operation_trigger on "public"."operation";
|
||||
drop function if exists public.create_default_chat_channel_for_operation();
|
||||
drop table if exists "public"."chat_read_state";
|
||||
drop table if exists "public"."chat_request";
|
||||
drop table if exists "public"."chat_message";
|
||||
drop table if exists "public"."chat_channel";
|
||||
alter table "public"."task"
|
||||
drop column if exists completed_callback_function_started,
|
||||
drop column if exists group_callback_function_started,
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package databaseStructs
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/its-a-feature/Mythic/utils/structs"
|
||||
)
|
||||
|
||||
const (
|
||||
ChatChannelTypeStandard = "standard"
|
||||
ChatChannelTypeAI = "ai"
|
||||
|
||||
ChatMessageAuthorOperator = "operator"
|
||||
ChatMessageAuthorAI = "ai"
|
||||
ChatMessageAuthorSystem = "system"
|
||||
|
||||
ChatMessageStatusPending = "pending"
|
||||
ChatMessageStatusStreaming = "streaming"
|
||||
ChatMessageStatusComplete = "complete"
|
||||
ChatMessageStatusError = "error"
|
||||
ChatMessageStatusCancelled = "cancelled"
|
||||
)
|
||||
|
||||
type ChatChannel struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
OperationID int `db:"operation_id" json:"operation_id"`
|
||||
Operation Operation `db:"operation" json:"operation,omitempty"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Slug string `db:"slug" json:"slug"`
|
||||
Description string `db:"description" json:"description"`
|
||||
ChannelType string `db:"channel_type" json:"channel_type"`
|
||||
CreatedBy int `db:"created_by" json:"created_by"`
|
||||
Creator Operator `db:"creator" json:"creator,omitempty"`
|
||||
Archived bool `db:"archived" json:"archived"`
|
||||
ArchivedBy structs.NullInt64 `db:"archived_by" json:"archived_by"`
|
||||
ArchivedAt sql.NullTime `db:"archived_at" json:"archived_at"`
|
||||
Locked bool `db:"locked" json:"locked"`
|
||||
LockedBy structs.NullInt64 `db:"locked_by" json:"locked_by"`
|
||||
LockedAt sql.NullTime `db:"locked_at" json:"locked_at"`
|
||||
LastMessageID structs.NullInt64 `db:"last_message_id" json:"last_message_id"`
|
||||
ChatContainerID structs.NullInt64 `db:"chat_container_id" json:"chat_container_id"`
|
||||
ChatContainer ConsumingContainer `db:"chat_container" json:"chat_container,omitempty"`
|
||||
ChatModel string `db:"chat_model" json:"chat_model"`
|
||||
AIMetadata MythicJSONText `db:"ai_metadata" json:"ai_metadata"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type ChatMessage struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
OperationID int `db:"operation_id" json:"operation_id"`
|
||||
Operation Operation `db:"operation" json:"operation,omitempty"`
|
||||
ChannelID int `db:"channel_id" json:"channel_id"`
|
||||
Channel ChatChannel `db:"channel" json:"channel,omitempty"`
|
||||
OperatorID structs.NullInt64 `db:"operator_id" json:"operator_id"`
|
||||
Operator Operator `db:"operator" json:"operator,omitempty"`
|
||||
APITokensID structs.NullInt64 `db:"apitokens_id" json:"api_tokens_id" mapstructure:"apitokens_id"`
|
||||
APIToken Apitokens `db:"apitoken" json:"apitoken,omitempty"`
|
||||
AuthorType string `db:"author_type" json:"author_type"`
|
||||
ChatContainerID structs.NullInt64 `db:"chat_container_id" json:"chat_container_id"`
|
||||
ChatContainer ConsumingContainer `db:"chat_container" json:"chat_container,omitempty"`
|
||||
SenderDisplayName string `db:"sender_display_name" json:"sender_display_name"`
|
||||
Message string `db:"message" json:"message"`
|
||||
Edited bool `db:"edited" json:"edited"`
|
||||
EditedAt sql.NullTime `db:"edited_at" json:"edited_at"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
DeletedBy structs.NullInt64 `db:"deleted_by" json:"deleted_by"`
|
||||
DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"`
|
||||
Status string `db:"status" json:"status"`
|
||||
Metadata MythicJSONText `db:"metadata" json:"metadata"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type ChatRequest struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
OperationID int `db:"operation_id" json:"operation_id"`
|
||||
Operation Operation `db:"operation" json:"operation,omitempty"`
|
||||
ChannelID int `db:"channel_id" json:"channel_id"`
|
||||
Channel ChatChannel `db:"channel" json:"channel,omitempty"`
|
||||
RequestMessageID int `db:"request_message_id" json:"request_message_id"`
|
||||
RequestMessage ChatMessage `db:"request_message" json:"request_message,omitempty"`
|
||||
ResponseMessageID int `db:"response_message_id" json:"response_message_id"`
|
||||
ResponseMessage ChatMessage `db:"response_message" json:"response_message,omitempty"`
|
||||
ChatContainerID int `db:"chat_container_id" json:"chat_container_id"`
|
||||
ChatContainer ConsumingContainer `db:"chat_container" json:"chat_container,omitempty"`
|
||||
Model string `db:"model" json:"model"`
|
||||
Status string `db:"status" json:"status"`
|
||||
Error string `db:"error" json:"error"`
|
||||
ContextSnapshot MythicJSONText `db:"context_snapshot" json:"context_snapshot"`
|
||||
RetryOfID structs.NullInt64 `db:"retry_of_id" json:"retry_of_id"`
|
||||
CreatedBy int `db:"created_by" json:"created_by"`
|
||||
Creator Operator `db:"creator" json:"creator,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
CompletedAt sql.NullTime `db:"completed_at" json:"completed_at"`
|
||||
CancelledAt sql.NullTime `db:"cancelled_at" json:"cancelled_at"`
|
||||
}
|
||||
|
||||
type ChatReadState struct {
|
||||
OperationID int `db:"operation_id" json:"operation_id"`
|
||||
Operation Operation `db:"operation" json:"operation,omitempty"`
|
||||
ChannelID int `db:"channel_id" json:"channel_id"`
|
||||
Channel ChatChannel `db:"channel" json:"channel,omitempty"`
|
||||
OperatorID int `db:"operator_id" json:"operator_id"`
|
||||
Operator Operator `db:"operator" json:"operator,omitempty"`
|
||||
LastReadMessageID structs.NullInt64 `db:"last_read_message_id" json:"last_read_message_id"`
|
||||
LastReadMessage ChatMessage `db:"last_read_message" json:"last_read_message,omitempty"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
@@ -205,6 +205,8 @@ func CreateGraphQLSpectatorAPITokenAndSendOnStartMessage(containerName string) {
|
||||
mythicjwt.SCOPE_FILE_WRITE,
|
||||
mythicjwt.SCOPE_TAG_WRITE,
|
||||
mythicjwt.SCOPE_CALLBACK_WRITE,
|
||||
mythicjwt.SCOPE_CHAT_AI_READ,
|
||||
mythicjwt.SCOPE_CHAT_AI_WRITE,
|
||||
}
|
||||
statement, err := database.DB.PrepareNamed(`INSERT INTO apitokens
|
||||
(token_value, operator_id, token_type, active, "name", created_by, task_id, callback_id, scopes)
|
||||
|
||||
@@ -89,6 +89,9 @@ const (
|
||||
CONTAINER_ON_START = "container_on_start"
|
||||
CONTAINER_ON_START_RESPONSE = "container_on_start_response"
|
||||
|
||||
// CHAT
|
||||
CHAT_RESPONSE_ROUTING_KEY = "chat_response"
|
||||
|
||||
// CUSTOM_BROWSER routes
|
||||
CUSTOMBROWSER_SYNC_ROUTING_KEY = "custombrowser_sync"
|
||||
CUSTOMBROWSER_EXPORT_FUNCTION = "custombrowser_exportfunction"
|
||||
@@ -124,6 +127,7 @@ const (
|
||||
//
|
||||
PT_TASK_PROCESS_RESPONSE = "pt_task_process_response"
|
||||
PT_COMMAND_HELP_FUNCTION = "pt_command_help_function"
|
||||
CHAT_REQUEST = "chat_request"
|
||||
)
|
||||
|
||||
// Routes where container is consuming messages and responding back to Mythic
|
||||
|
||||
@@ -31,6 +31,7 @@ type DirectQueueStruct struct {
|
||||
RoutingKey string
|
||||
Handler QueueHandler
|
||||
Scopes []string
|
||||
Sequential bool
|
||||
}
|
||||
|
||||
type channelMutex struct {
|
||||
@@ -90,6 +91,7 @@ func (r *rabbitMQConnection) startListeners() {
|
||||
directQueue.Queue,
|
||||
directQueue.RoutingKey,
|
||||
directQueue.Handler,
|
||||
directQueue.Sequential,
|
||||
exclusiveQueue)
|
||||
}
|
||||
go checkContainerStatus()
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/its-a-feature/Mythic/authentication/mythicjwt"
|
||||
"github.com/its-a-feature/Mythic/database"
|
||||
databaseStructs "github.com/its-a-feature/Mythic/database/structs"
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
const chatStreamFlushInterval = 500 * time.Millisecond
|
||||
|
||||
type ChatContainerResponseMessage struct {
|
||||
OperationID int `json:"operation_id" mapstructure:"operation_id"`
|
||||
RequestID int `json:"request_id" mapstructure:"request_id"`
|
||||
ResponseMessageID int `json:"response_message_id" mapstructure:"response_message_id"`
|
||||
Content string `json:"content" mapstructure:"content"`
|
||||
IsDelta bool `json:"is_delta" mapstructure:"is_delta"`
|
||||
Complete bool `json:"complete" mapstructure:"complete"`
|
||||
Status string `json:"status" mapstructure:"status"`
|
||||
Error string `json:"error" mapstructure:"error"`
|
||||
Metadata map[string]interface{} `json:"metadata" mapstructure:"metadata"`
|
||||
}
|
||||
|
||||
type chatResponseRequest struct {
|
||||
ID int `db:"id"`
|
||||
OperationID int `db:"operation_id"`
|
||||
ResponseMessageID int `db:"response_message_id"`
|
||||
Status string `db:"status"`
|
||||
}
|
||||
|
||||
type chatStreamBuffer struct {
|
||||
OperationID int
|
||||
Pending string
|
||||
Timer *time.Timer
|
||||
}
|
||||
|
||||
var (
|
||||
chatStreamBuffers = map[int]*chatStreamBuffer{}
|
||||
chatStreamBuffersLock sync.Mutex
|
||||
chatResponseLocks sync.Map
|
||||
)
|
||||
|
||||
func init() {
|
||||
RabbitMQConnection.AddDirectQueue(DirectQueueStruct{
|
||||
Exchange: MYTHIC_EXCHANGE,
|
||||
Queue: CHAT_RESPONSE_ROUTING_KEY,
|
||||
RoutingKey: CHAT_RESPONSE_ROUTING_KEY,
|
||||
Handler: processChatContainerResponse,
|
||||
Scopes: []string{mythicjwt.SCOPE_CHAT_AI_WRITE},
|
||||
Sequential: true,
|
||||
})
|
||||
}
|
||||
|
||||
func processChatContainerResponse(msg amqp.Delivery) {
|
||||
incomingMessage := ChatContainerResponseMessage{}
|
||||
if err := json.Unmarshal(msg.Body, &incomingMessage); err != nil {
|
||||
logging.LogError(err, "Failed to process chat container response message")
|
||||
return
|
||||
}
|
||||
authContext, err := GetRabbitMQAuthContextFromHeaders(msg.Headers)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to get chat response auth headers")
|
||||
return
|
||||
}
|
||||
if err = applyChatContainerResponse(incomingMessage, authContext); err != nil {
|
||||
logging.LogError(err, "Failed to apply chat container response",
|
||||
"request_id", incomingMessage.RequestID, "response_message_id", incomingMessage.ResponseMessageID)
|
||||
}
|
||||
}
|
||||
|
||||
func applyChatContainerResponse(incomingMessage ChatContainerResponseMessage, authContext RabbitMQAuthContext) error {
|
||||
request, err := getChatResponseRequest(incomingMessage, authContext.OperationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if request.Status == databaseStructs.ChatMessageStatusCancelled {
|
||||
return nil
|
||||
}
|
||||
if incomingMessage.OperationID > 0 && incomingMessage.OperationID != request.OperationID {
|
||||
return fmt.Errorf("chat response operation %d does not match request operation %d",
|
||||
incomingMessage.OperationID, request.OperationID)
|
||||
}
|
||||
if authContext.OperationID > 0 && authContext.OperationID != request.OperationID {
|
||||
return fmt.Errorf("chat response auth operation %d does not match request operation %d",
|
||||
authContext.OperationID, request.OperationID)
|
||||
}
|
||||
if incomingMessage.ResponseMessageID > 0 && incomingMessage.ResponseMessageID != request.ResponseMessageID {
|
||||
return fmt.Errorf("chat response message %d does not match request response message %d",
|
||||
incomingMessage.ResponseMessageID, request.ResponseMessageID)
|
||||
}
|
||||
|
||||
chatResponseLock := getChatResponseLock(request.ResponseMessageID)
|
||||
chatResponseLock.Lock()
|
||||
defer chatResponseLock.Unlock()
|
||||
|
||||
if request, err = getChatResponseRequest(ChatContainerResponseMessage{RequestID: request.ID}, request.OperationID); err != nil {
|
||||
return err
|
||||
}
|
||||
if isTerminalChatResponseStatus(request.Status) {
|
||||
return nil
|
||||
}
|
||||
|
||||
status := normalizeChatContainerResponseStatus(incomingMessage)
|
||||
if incomingMessage.Content != "" {
|
||||
if incomingMessage.IsDelta {
|
||||
if err = queueChatResponseDelta(request.ResponseMessageID, request.OperationID, incomingMessage.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = flushChatResponseMessageLocked(request.ResponseMessageID, request.OperationID, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = setChatResponseMessageContent(request.ResponseMessageID, request.OperationID, incomingMessage.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if status == "" {
|
||||
status = databaseStructs.ChatMessageStatusStreaming
|
||||
}
|
||||
}
|
||||
|
||||
if status == databaseStructs.ChatMessageStatusComplete ||
|
||||
status == databaseStructs.ChatMessageStatusError ||
|
||||
status == databaseStructs.ChatMessageStatusCancelled {
|
||||
if err = flushChatResponseMessageLocked(request.ResponseMessageID, request.OperationID, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if status == "" && len(incomingMessage.Metadata) > 0 {
|
||||
status = request.Status
|
||||
}
|
||||
if status != "" {
|
||||
if err = setChatResponseStatus(request, status, incomingMessage.Error, incomingMessage.Metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
if isTerminalChatResponseStatus(status) {
|
||||
chatResponseLocks.Delete(request.ResponseMessageID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getChatResponseRequest(incomingMessage ChatContainerResponseMessage, operationID int) (chatResponseRequest, error) {
|
||||
request := chatResponseRequest{}
|
||||
if incomingMessage.RequestID <= 0 && incomingMessage.ResponseMessageID <= 0 {
|
||||
return request, errors.New("chat response requires request_id or response_message_id")
|
||||
}
|
||||
whereClause := "id=$1"
|
||||
arg := incomingMessage.RequestID
|
||||
if incomingMessage.RequestID <= 0 {
|
||||
whereClause = "response_message_id=$1"
|
||||
arg = incomingMessage.ResponseMessageID
|
||||
}
|
||||
sqlStatement := fmt.Sprintf(`SELECT id, operation_id, response_message_id, status
|
||||
FROM chat_request
|
||||
WHERE %s`, whereClause)
|
||||
args := []interface{}{arg}
|
||||
if operationID > 0 {
|
||||
sqlStatement += " AND operation_id=$2"
|
||||
args = append(args, operationID)
|
||||
}
|
||||
if err := database.DB.Get(&request, sqlStatement, args...); err != nil {
|
||||
return request, err
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func normalizeChatContainerResponseStatus(incomingMessage ChatContainerResponseMessage) string {
|
||||
status := strings.ToLower(strings.TrimSpace(incomingMessage.Status))
|
||||
switch status {
|
||||
case databaseStructs.ChatMessageStatusStreaming,
|
||||
databaseStructs.ChatMessageStatusComplete,
|
||||
databaseStructs.ChatMessageStatusError,
|
||||
databaseStructs.ChatMessageStatusCancelled:
|
||||
return status
|
||||
}
|
||||
if incomingMessage.Error != "" {
|
||||
return databaseStructs.ChatMessageStatusError
|
||||
}
|
||||
if incomingMessage.Complete {
|
||||
return databaseStructs.ChatMessageStatusComplete
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isTerminalChatResponseStatus(status string) bool {
|
||||
return status == databaseStructs.ChatMessageStatusComplete ||
|
||||
status == databaseStructs.ChatMessageStatusError ||
|
||||
status == databaseStructs.ChatMessageStatusCancelled
|
||||
}
|
||||
|
||||
func getChatResponseLock(responseMessageID int) *sync.Mutex {
|
||||
lock, _ := chatResponseLocks.LoadOrStore(responseMessageID, &sync.Mutex{})
|
||||
return lock.(*sync.Mutex)
|
||||
}
|
||||
|
||||
func queueChatResponseDelta(responseMessageID int, operationID int, delta string) error {
|
||||
chatStreamBuffersLock.Lock()
|
||||
defer chatStreamBuffersLock.Unlock()
|
||||
buffer, ok := chatStreamBuffers[responseMessageID]
|
||||
if !ok {
|
||||
buffer = &chatStreamBuffer{OperationID: operationID}
|
||||
chatStreamBuffers[responseMessageID] = buffer
|
||||
}
|
||||
if buffer.OperationID > 0 && operationID > 0 && buffer.OperationID != operationID {
|
||||
return fmt.Errorf("chat response buffer operation %d does not match incoming operation %d for response message %d",
|
||||
buffer.OperationID, operationID, responseMessageID)
|
||||
}
|
||||
if buffer.OperationID == 0 {
|
||||
buffer.OperationID = operationID
|
||||
}
|
||||
buffer.Pending += delta
|
||||
if buffer.Timer == nil {
|
||||
buffer.Timer = time.AfterFunc(chatStreamFlushInterval, func() {
|
||||
if err := flushChatResponseMessage(responseMessageID, operationID, false); err != nil {
|
||||
logging.LogError(err, "Failed to flush chat response buffer", "response_message_id", responseMessageID)
|
||||
}
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func flushChatResponseMessage(responseMessageID int, operationID int, final bool) error {
|
||||
chatResponseLock := getChatResponseLock(responseMessageID)
|
||||
chatResponseLock.Lock()
|
||||
defer chatResponseLock.Unlock()
|
||||
return flushChatResponseMessageLocked(responseMessageID, operationID, final)
|
||||
}
|
||||
|
||||
func flushChatResponseMessageLocked(responseMessageID int, operationID int, final bool) error {
|
||||
chatStreamBuffersLock.Lock()
|
||||
buffer, ok := chatStreamBuffers[responseMessageID]
|
||||
if !ok {
|
||||
chatStreamBuffersLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
if buffer.OperationID > 0 && operationID > 0 && buffer.OperationID != operationID {
|
||||
chatStreamBuffersLock.Unlock()
|
||||
return fmt.Errorf("chat response buffer operation %d does not match flush operation %d for response message %d",
|
||||
buffer.OperationID, operationID, responseMessageID)
|
||||
}
|
||||
pending := buffer.Pending
|
||||
buffer.Pending = ""
|
||||
if buffer.Timer != nil {
|
||||
buffer.Timer.Stop()
|
||||
buffer.Timer = nil
|
||||
}
|
||||
if final {
|
||||
delete(chatStreamBuffers, responseMessageID)
|
||||
}
|
||||
chatStreamBuffersLock.Unlock()
|
||||
|
||||
if pending == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := database.DB.Exec(`UPDATE chat_message
|
||||
SET message = message || $2,
|
||||
status = CASE WHEN status IN ('pending', 'streaming') THEN 'streaming' ELSE status END
|
||||
WHERE id=$1 AND operation_id=$3 AND deleted=false`, responseMessageID, pending, operationID)
|
||||
return err
|
||||
}
|
||||
|
||||
func setChatResponseMessageContent(responseMessageID int, operationID int, content string) error {
|
||||
_, err := database.DB.Exec(`UPDATE chat_message
|
||||
SET message=$3,
|
||||
status = CASE WHEN status IN ('pending', 'streaming') THEN 'streaming' ELSE status END
|
||||
WHERE id=$1 AND operation_id=$2 AND deleted=false`, responseMessageID, operationID, content)
|
||||
return err
|
||||
}
|
||||
|
||||
func setChatResponseStatus(request chatResponseRequest, status string, responseError string, metadata map[string]interface{}) error {
|
||||
if len(metadata) == 0 {
|
||||
metadata = map[string]interface{}{}
|
||||
}
|
||||
metadataJSON := GetMythicJSONTextFromStruct(metadata)
|
||||
if responseError != "" {
|
||||
metadataJSON = GetMythicJSONTextFromStruct(map[string]interface{}{
|
||||
"container_metadata": metadata,
|
||||
"error": responseError,
|
||||
})
|
||||
}
|
||||
_, err := database.DB.Exec(`UPDATE chat_message
|
||||
SET status=$3,
|
||||
metadata = metadata || $4::jsonb
|
||||
WHERE id=$1 AND operation_id=$2 AND deleted=false`, request.ResponseMessageID, request.OperationID, status, metadataJSON.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = database.DB.Exec(`UPDATE chat_request
|
||||
SET status=$2,
|
||||
error=$3,
|
||||
completed_at=CASE WHEN $2='complete' THEN now() ELSE completed_at END,
|
||||
cancelled_at=CASE WHEN $2='cancelled' THEN now() ELSE cancelled_at END
|
||||
WHERE id=$1 AND operation_id=$4 AND status <> 'cancelled'`, request.ID, status, responseError, request.OperationID)
|
||||
return err
|
||||
}
|
||||
@@ -38,6 +38,7 @@ const (
|
||||
CONSUMING_SERVICES_TYPE_WEBHOOK = "webhook"
|
||||
CONSUMING_SERVICES_TYPE_EVENTING = "eventing"
|
||||
CONSUMING_SERVICES_TYPE_SCRIPTING = "scripting"
|
||||
CONSUMING_SERVICES_TYPE_CHAT = "chat"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/its-a-feature/Mythic/logging"
|
||||
)
|
||||
|
||||
type ChatContainerContextMessage struct {
|
||||
ID int `db:"id" json:"id" mapstructure:"id"`
|
||||
AuthorType string `db:"author_type" json:"author_type" mapstructure:"author_type"`
|
||||
SenderDisplayName string `db:"sender_display_name" json:"sender_display_name" mapstructure:"sender_display_name"`
|
||||
Message string `db:"message" json:"message" mapstructure:"message"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at" mapstructure:"created_at"`
|
||||
}
|
||||
|
||||
type ChatContainerRequestMessage struct {
|
||||
ContainerName string `json:"container_name" mapstructure:"container_name"`
|
||||
OperationID int `json:"operation_id" mapstructure:"operation_id"`
|
||||
ChannelID int `json:"channel_id" mapstructure:"channel_id"`
|
||||
ChannelName string `json:"channel_name" mapstructure:"channel_name"`
|
||||
ChannelSlug string `json:"channel_slug" mapstructure:"channel_slug"`
|
||||
RequestID int `json:"request_id" mapstructure:"request_id"`
|
||||
RequestMessageID int `json:"request_message_id" mapstructure:"request_message_id"`
|
||||
ResponseMessageID int `json:"response_message_id" mapstructure:"response_message_id"`
|
||||
Model string `json:"model" mapstructure:"model"`
|
||||
Prompt string `json:"prompt" mapstructure:"prompt"`
|
||||
Context []ChatContainerContextMessage `json:"context" mapstructure:"context"`
|
||||
Secrets map[string]interface{} `json:"secrets" mapstructure:"secrets"`
|
||||
}
|
||||
|
||||
func (r *rabbitMQConnection) SendChatContainerRequest(containerName string, chatMessage ChatContainerRequestMessage, authContext RabbitMQAuthContext) error {
|
||||
chatMessage.ContainerName = containerName
|
||||
headers, err := GenerateRabbitMQAuthTokenHeader(authContext)
|
||||
if err != nil {
|
||||
logging.LogError(err, "Failed to generate auth context for chat request")
|
||||
return err
|
||||
}
|
||||
if err = r.SendStructMessage(
|
||||
MYTHIC_EXCHANGE,
|
||||
GetChatContainerRequestRoutingKey(containerName),
|
||||
"",
|
||||
chatMessage,
|
||||
false,
|
||||
headers,
|
||||
); err != nil {
|
||||
logging.LogError(err, "Failed to send chat request", "container", containerName, "request_id", chatMessage.RequestID)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -59,6 +59,9 @@ func GetPtTaskProcessResponseRoutingKey(container string) string {
|
||||
func GetPtCommandHelpRoutingKey(container string) string {
|
||||
return fmt.Sprintf("%s_%s", container, PT_COMMAND_HELP_FUNCTION)
|
||||
}
|
||||
func GetChatContainerRequestRoutingKey(container string) string {
|
||||
return fmt.Sprintf("%s_%s", container, CHAT_REQUEST)
|
||||
}
|
||||
|
||||
// c2 rpc routing key functions
|
||||
func GetC2RPCOpsecChecksRoutingKey(container string) string {
|
||||
@@ -519,7 +522,7 @@ func (r *rabbitMQConnection) SendRPCMessage(exchange string, queue string, body
|
||||
logging.LogError(finalError, "failed 3 times")
|
||||
return nil, finalError
|
||||
}
|
||||
func (r *rabbitMQConnection) ReceiveFromMythicDirectExchange(exchange string, queue string, routingKey string, handler QueueHandler, exclusiveQueue bool) {
|
||||
func (r *rabbitMQConnection) ReceiveFromMythicDirectExchange(exchange string, queue string, routingKey string, handler QueueHandler, sequential bool, exclusiveQueue bool) {
|
||||
// exchange is a direct exchange
|
||||
// queue is where the messages get sent to (local name)
|
||||
// routingKey is the specific direct topic we're interested in for the exchange
|
||||
@@ -603,7 +606,11 @@ func (r *rabbitMQConnection) ReceiveFromMythicDirectExchange(exchange string, qu
|
||||
logging.LogError(err, "RabbitMQ direct exchange auth check failed", "queue", queue)
|
||||
continue
|
||||
}
|
||||
go handler(d)
|
||||
if sequential {
|
||||
handler(d)
|
||||
} else {
|
||||
go handler(d)
|
||||
}
|
||||
}
|
||||
forever <- true
|
||||
}()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,8 @@ func Login(c *gin.Context) {
|
||||
"username": currentOperation.CurrentOperator.Username,
|
||||
"id": currentOperation.CurrentOperator.ID,
|
||||
"user_id": currentOperation.CurrentOperator.ID,
|
||||
"admin": currentOperation.CurrentOperator.Admin,
|
||||
"view_mode": currentOperation.ViewMode,
|
||||
"view_utc_time": currentOperation.CurrentOperator.ViewUtcTime,
|
||||
"current_utc_time": time.Now().UTC(),
|
||||
}
|
||||
@@ -75,8 +77,12 @@ func GetMeWebhook(c *gin.Context) {
|
||||
"current_operation_banner_color": currentOperation.CurrentOperation.BannerColor,
|
||||
"current_operation_complete": currentOperation.CurrentOperation.Complete,
|
||||
"current_operation_id": currentOperation.CurrentOperation.ID,
|
||||
"username": currentOperation.CurrentOperator.Username,
|
||||
"user_id": currentOperation.CurrentOperator.ID,
|
||||
"id": currentOperation.CurrentOperator.ID,
|
||||
"admin": currentOperation.CurrentOperator.Admin,
|
||||
"view_mode": currentOperation.ViewMode,
|
||||
"view_utc_time": currentOperation.CurrentOperator.ViewUtcTime,
|
||||
"current_utc_time": time.Now().UTC(),
|
||||
"scope_info": buildScopeIntrospectionResponse(c, currentClaimsOrNil(c)),
|
||||
})
|
||||
@@ -162,6 +168,8 @@ func RefreshJWT(c *gin.Context) {
|
||||
"username": currentOperation.CurrentOperator.Username,
|
||||
"id": currentOperation.CurrentOperator.ID,
|
||||
"user_id": currentOperation.CurrentOperator.ID,
|
||||
"admin": currentOperation.CurrentOperator.Admin,
|
||||
"view_mode": currentOperation.ViewMode,
|
||||
"view_utc_time": currentOperation.CurrentOperator.ViewUtcTime,
|
||||
"current_utc_time": time.Now().UTC(),
|
||||
}
|
||||
|
||||
@@ -313,6 +313,10 @@ func setRoutes(r *gin.Engine) {
|
||||
mythicjwt.SCOPE_FILE_READ,
|
||||
}),
|
||||
webcontroller.DownloadBulkFilesWebhook)
|
||||
allOperationMembers.POST("chat_search_webhook",
|
||||
webcontroller.ChatSearchWebhook)
|
||||
allOperationMembers.POST("chat_mark_read_webhook",
|
||||
webcontroller.MarkChatReadWebhook)
|
||||
allOperationMembers.POST("preview_file_webhook",
|
||||
authentication.TokenScopeMiddleware([]string{
|
||||
mythicjwt.SCOPE_FILE_READ,
|
||||
@@ -354,6 +358,21 @@ func setRoutes(r *gin.Engine) {
|
||||
webcontroller.ConsumingServicesTestWebhook)
|
||||
noSpectators.POST("consuming_services_test_log",
|
||||
webcontroller.ConsumingServicesTestLog)
|
||||
// chat
|
||||
noSpectators.POST("chat_create_channel_webhook",
|
||||
webcontroller.CreateChatChannelWebhook)
|
||||
noSpectators.POST("chat_update_channel_webhook",
|
||||
webcontroller.UpdateChatChannelWebhook)
|
||||
noSpectators.POST("chat_create_message_webhook",
|
||||
webcontroller.CreateChatMessageWebhook)
|
||||
noSpectators.POST("chat_edit_message_webhook",
|
||||
webcontroller.EditChatMessageWebhook)
|
||||
noSpectators.POST("chat_delete_message_webhook",
|
||||
webcontroller.DeleteChatMessageWebhook)
|
||||
noSpectators.POST("chat_cancel_request_webhook",
|
||||
webcontroller.CancelChatRequestWebhook)
|
||||
noSpectators.POST("chat_retry_request_webhook",
|
||||
webcontroller.RetryChatRequestWebhook)
|
||||
// creating a payload
|
||||
noSpectators.POST("createpayload_webhook",
|
||||
authentication.TokenScopeMiddleware([]string{
|
||||
|
||||
Reference in New Issue
Block a user