Skip to content

Commit

Permalink
fixes + frontend UI to manage groups
Browse files Browse the repository at this point in the history
  • Loading branch information
domysh committed Dec 14, 2024
1 parent b85e94e commit c9f8c3c
Show file tree
Hide file tree
Showing 18 changed files with 476 additions and 19 deletions.
2 changes: 1 addition & 1 deletion backend/routes/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async def result(result: sqla.Row[Tuple[AttackGroup, AttackExecution]]):
members = await redis_conn.smembers(f"group:{group.id}:members")
status = GroupStatus.active
if members is None or len(members) == 0:
status = GroupStatus.unactive
status = GroupStatus.inactive
members = set()
return GroupDTO(
**json_like(group, mode="python", unset=True),
Expand Down
10 changes: 8 additions & 2 deletions backend/workers/group_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ def current_tick_calc():
raise Exception("Attack not started yet")
return math.floor((this_time - start_time).total_seconds() / g.configuration.TICK_DURATION)

async def set_exploit_stopped(exploit_id: str):
redis_key = f"exploit:{exploit_id}:stopped"
await redis_conn.set(redis_key, pickle.dumps(datetime_now() + timedelta(seconds=5))) # 5 seconds for submitting the remaining flags
await redis_conn.publish(redis_channels.exploit, "update")

def calc_round_time_available():
#Needed to calculate the initial time based on attack schedule logic
this_time = datetime_now()
Expand Down Expand Up @@ -282,6 +287,7 @@ async def attack_run_actions(self):

if len(client_status) == 0:
if self.running:
await set_exploit_stopped(self.group.exploit_id)
self.running = False
return # No client available, need someone to join

Expand Down Expand Up @@ -336,8 +342,8 @@ def trigger_next_loop(self, data="trigger"):
async def handle_request(self, request: GroupResponseEvent):
match request.event:
case GroupEventResponseType.SET_RUNNING_STATUS:
if request.data["running"] is False:
await redis_conn.set(f"exploit:{self.group.exploit_id}:stopped", pickle.dumps(datetime_now()))
if not request.data["running"]:
await set_exploit_stopped(self.group.exploit_id)
self.running = request.data["running"]
await self.send_running_status()
await self.loop_reset()
Expand Down
2 changes: 1 addition & 1 deletion client/exploitfarm/models/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class AuthStatus(Enum):

class GroupStatus(Enum):
active = 'active'
unactive = 'unactive'
inactive = 'inactive'

class FlagStatus(Enum):
ok = 'ok'
Expand Down
3 changes: 3 additions & 0 deletions client/exploitfarm/models/flags.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from exploitfarm.models.enums import FlagStatus, AttackExecutionStatus
from pydantic import AwareDatetime
from exploitfarm.models.dbtypes import FlagID, AttackExecutionID, TeamID, ExploitID, ClientID, ExploitSourceID
from exploitfarm.models.dbtypes import AttackGroupID
from pydantic import BaseModel, Field, AliasChoices

class FlagDTOSmall(BaseModel):
Expand All @@ -18,6 +19,7 @@ class AttackExecutionDTO(BaseModel):
target: TeamID|None = Field(None, validation_alias=AliasChoices('target_id'))
exploit: ExploitID|None = Field(None, validation_alias=AliasChoices('exploit_id'))
executed_by: ClientID|None = Field(None, validation_alias=AliasChoices('executed_by_id'))
executed_by_group: AttackGroupID|None = Field(None, validation_alias=AliasChoices('executed_by_group_id'))
flags: list[FlagDTOSmall]
exploit_source: ExploitSourceID|None = Field(None, validation_alias=AliasChoices('exploit_source_id'))

Expand All @@ -30,6 +32,7 @@ class FlagDTOAttackDetails(BaseModel):
target: TeamID|None = Field(None, validation_alias=AliasChoices('target_id'))
exploit: ExploitID|None = Field(None, validation_alias=AliasChoices('exploit_id'))
executed_by: ClientID|None = Field(None, validation_alias=AliasChoices('executed_by_id'))
executed_by_group: AttackGroupID|None = Field(None, validation_alias=AliasChoices('executed_by_group_id'))
exploit_source: ExploitSourceID|None = Field(None, validation_alias=AliasChoices('exploit_source_id'))

class FlagDTO(BaseModel):
Expand Down
2 changes: 1 addition & 1 deletion client/exploitfarm/models/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class GroupDTO(BaseModel):
members: list[str] = []
exploit: ExploitID = Field(validation_alias=AliasChoices('exploit_id'))
last_attack_at: AwareDatetime|None = None
status: GroupStatus = GroupStatus.unactive
status: GroupStatus = GroupStatus.inactive
commit: UUID|Literal["latest"] = Field("latest", validation_alias=AliasChoices('commit_id'))

class Config:
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/components/elements/ExploitsBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { exploitsQuery, useClientSolver, useExtendedExploitSolver } from "@/utils/queries"
import { exploitsQuery, useClientSolver, useExtendedExploitSolver, useGroupSolver } from "@/utils/queries"
import { Box, Divider, Flex, Paper, ScrollArea, Space, Tooltip } from "@mantine/core"
import { useState } from "react"
import { GoDotFill } from "react-icons/go"
Expand All @@ -9,6 +9,7 @@ export const ExploitBar = () => {
const exploits = exploitsQuery()
const extendedExploitSolver = useExtendedExploitSolver()
const clientSolver = useClientSolver()
const groupSolver = useGroupSolver()
const [modalExploitId, setModalExploitId] = useState<string|null>(null)

return <Box style={{width:"100%", fontSize:"90%"}} >
Expand All @@ -26,7 +27,7 @@ export const ExploitBar = () => {
else
return b.name.localeCompare(a.name)
} ).map((expl) => <Box style={{fontSize:"90%", cursor:"pointer"}} onClick={()=>setModalExploitId(expl.id)} className="transparency-on-hover" key={expl.id}>
<Paper shadow="md" radius="xl" withBorder px={20} py={5} bg="gray" mx="xs">
<Paper shadow="md" radius="xl" withBorder px={10} py={5} bg="gray" mx="xs">
<Box className="center-flex">
<StatusPoint status={expl.status} />
<Space w={3} />
Expand All @@ -35,7 +36,7 @@ export const ExploitBar = () => {
{extendedExploitSolver(expl.id)}
</Box>
<Box style={{ fontSize: "70%", whiteSpace: "nowrap"}}>
{expl.status=="active"?"running":"ran"} by: {clientSolver(expl.last_execution_by)}
{expl.status=="active"?"running":"ran"} by: {expl.last_execution_group_by?`${groupSolver(expl.last_execution_group_by)}`:clientSolver(expl.last_execution_by)}
</Box>
</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { attacksQuery, exploitsSourcesQuery, useClientSolver, useExtendedExploitSolver, useTeamSolver } from "@/utils/queries";
import { attacksQuery, exploitsSourcesQuery, useClientSolver, useExtendedExploitSolver, useGroupSolver, useTeamSolver } from "@/utils/queries";
import { useGlobalStore } from "@/utils/stores";
import { Alert, Box, Modal, ScrollArea, Space, Title } from "@mantine/core"
import { showNotification } from "@mantine/notifications";
Expand Down Expand Up @@ -27,6 +27,7 @@ export const AttackExecutionDetailsModal = (props:{ opened:boolean, close:()=>vo
const isUsedSourceLatest = sourceQuery.data?.length??0 > 0 ? sourceQuery.data?.[0]?.id == usedSource?.id : false
const setLoading = useGlobalStore((store) => store.setLoader)
const clientSolver = useClientSolver()
const groupSolver = useGroupSolver()
const teamSolver = useTeamSolver()
const extendedExploitSolver = useExtendedExploitSolver()
const scollRef = useRef<any>()
Expand All @@ -51,6 +52,12 @@ export const AttackExecutionDetailsModal = (props:{ opened:boolean, close:()=>vo
<Box display="flex" style={{alignItems: "center", width: boxWidth}}><FaUser /><Space w="xs" />Executed by<Space w="xs" /></Box>
<b>{clientSolver(attack.executed_by)}</b>
</Box>
{attack.executed_by_group?<Box display="flex">
<Box display="flex" style={{alignItems: "center", width: boxWidth}}><FaUser /><Space w="xs" />Executed by group<Space w="xs" /></Box>
<b>{groupSolver(attack.executed_by_group)}</b>
</Box>:null

}
<Box display="flex">
<Box display="flex" style={{alignItems: "center", width: boxWidth}}><ImTarget /><Space w="xs" />Target Team<Space w="xs" /></Box>
<b>{teamSolver(attack.target)}</b>
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/components/modals/DeleteGroupModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useQueryClient } from "@tanstack/react-query"
import { YesOrNoModal } from "./YesOrNoModal"
import { Title } from "@mantine/core"
import { deleteGroup } from "@/utils/queries"
import { notifications } from "@mantine/notifications"
import { AttackGroup } from "@/utils/types"


export const DeleteGroupModal = ({ onClose, group }:{ onClose: () => void, group?: AttackGroup }) => {
const queryClient = useQueryClient()

return <YesOrNoModal
open={ group != null }
onClose={onClose}
title={<Title order={3}>Deleting an attack group</Title>}
onConfirm={()=>{
if (group == null){
onClose?.()
return
}
deleteGroup(group?.id).then(()=>{
notifications.show({ title: "Group deleted", message: `The group ${group?.name} has been deleted successfully`, color: "green" })
queryClient.invalidateQueries({ queryKey: ["groups"] })

}).catch((err)=>{
notifications.show({ title: "Error deleting the group", message: `An error occurred while deleting the group ${group?.name}: ${err.message}`, color: "red" })
}).finally(()=>{ onClose() })
}}
size="xl"
message={
<>
<span>Are you sure you want to delete the group <b>{group?.name}</b>?</span><br />
<span style={{ color: "yellow" }}>This action will stop the group if it is running!</span>
</>
}/>
}
2 changes: 1 addition & 1 deletion frontend/src/components/modals/EditExploitModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const EditExploitModal = ({ onClose, exploit }:{ onClose: ()=>void, explo
})}>
<TextInput
label="Name"
placeholder="Service name"
placeholder="Exploit name"
withAsterisk
{...form.getInputProps("name")}
/>
Expand Down
70 changes: 70 additions & 0 deletions frontend/src/components/modals/EditGroupModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { editGroup } from "@/utils/queries"
import { AttackGroup } from "@/utils/types"
import { Button, Group, Modal, TextInput } from "@mantine/core"
import { useForm } from "@mantine/form"
import { notifications } from "@mantine/notifications"
import { useQueryClient } from "@tanstack/react-query"
import { useEffect } from "react"


export const EditGroupModal = ({ onClose, group }:{ onClose: ()=>void, group?:AttackGroup }) => {
const form = useForm({
initialValues: {
name: group?.name??"",
},
validate: {
name: (value) => value == ""?"Name is required":undefined
}
})

const queryClient = useQueryClient()

useEffect(() => {
form.setInitialValues({
name: group?.name??""
})
form.reset()
}, [group])

return <Modal
opened={ group != null }
onClose={onClose}
title="Edit group"
size="xl"
centered
>
<form onSubmit={form.onSubmit((data) => {
if (group == null){
onClose()
return
}
editGroup(group.id, data)
.then(()=>{
notifications.show({
title: `${group.name} Group edited!`,
message: "Group has been edited successfully!",
color: "green",
})
queryClient.invalidateQueries({ queryKey: ["groups"] })
}).catch((err) => {
notifications.show({
title: "Error during editing!",
message: err.message??err??"Unknown error",
color: "red",
})
}).finally(()=>{ onClose() })
})}>
<TextInput
label="Name"
placeholder="Group name"
withAsterisk
{...form.getInputProps("name")}
/>
<Group mt="xl" justify="flex-end">
<Button onClick={form.reset} color="gray" disabled={!form.isDirty()}>Reset</Button>
<Button type="submit" color="blue" disabled={!form.isValid() || !form.isDirty()}>Edit</Button>
</Group>
</form>

</Modal>
}
11 changes: 9 additions & 2 deletions frontend/src/components/modals/ExploitDetailModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { exploitsQuery, statsQuery, useClientSolver, useServiceSolverByExploitId } from "@/utils/queries";
import { exploitsQuery, statsQuery, useClientSolver, useGroupMapping, useServiceSolverByExploitId } from "@/utils/queries";
import { useGlobalStore } from "@/utils/stores";
import { Box, Modal, Space, Title } from "@mantine/core"
import { showNotification } from "@mantine/notifications";
Expand All @@ -20,6 +20,7 @@ export const ExploitDetailModal = (props:{ opened:boolean, close:()=>void, explo
const stats = statsQuery()
const setLoading = useGlobalStore((store) => store.setLoader)
const clientSolver = useClientSolver()
const groupsMapping = useGroupMapping()
const serviceSolver = useServiceSolverByExploitId()

useEffect(() => {
Expand All @@ -45,6 +46,8 @@ export const ExploitDetailModal = (props:{ opened:boolean, close:()=>void, explo
const noFlagsAttacks = exploit?(stats.data?.ticks.reduce((a, b) => a+(b.exploits[exploit.id]?.attacks?.noflags??0), 0)??0):0
const totAttacks = exploit?(stats.data?.ticks.reduce((a, b) => a+(b.exploits[exploit.id]?.attacks?.tot??0), 0)??0):0

const group = groupsMapping[exploit?.last_execution_group_by??""]

return <Modal opened={props.opened} onClose={props.close} title={<Box className="center-flex">
<StatusPoint status={exploit?.status} />
<Space w="xs" />
Expand All @@ -58,8 +61,12 @@ export const ExploitDetailModal = (props:{ opened:boolean, close:()=>void, explo
</Box>
<Box display="flex">
<Box display="flex" style={{alignItems: "center", width: boxWidth}}><FaUser /><Space w="xs" />Last execution<Space w="xs" /></Box>
<Box>by <b>{exploit.last_execution_by?clientSolver(exploit.last_execution_by):"unknown"}</b> at <b>{exploit.last_update?getDateFormatted(exploit.last_update):"unknown"}</b> </Box>
<Box>by <b>{exploit.last_execution_by?group?group.name:clientSolver(exploit.last_execution_by):"unknown"}</b> at <b>{exploit.last_update?getDateFormatted(exploit.last_update):"unknown"}</b></Box>
</Box>
{group && <Box display="flex">
<Box display="flex" style={{alignItems: "center", width: boxWidth}}><FaUser /><Space w="xs" />Group Members<Space w="xs" /></Box>
<b>{group.members.map(ele => clientSolver(ele)).join(", ")} ({group.members.length})</b>
</Box>}
<Space h="xs" />
<Box display="flex">
<Box display="flex" style={{alignItems: "center", width: boxWidth}}><flagStatusTable.ok.icon /><Space w="xs" />Valid Flags<Space w="xs" /></Box>
Expand Down
12 changes: 9 additions & 3 deletions frontend/src/components/screens/OptionScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AddEditServiceModal } from "../modals/AddEditServiceModal";
import { useState } from "react";
import { useSettingsStore } from "@/utils/stores";
import { notifications } from "@mantine/notifications";
import { GroupTable } from "../tables/GroupTable";

export const OptionScreen = () => {

Expand All @@ -18,19 +19,24 @@ export const OptionScreen = () => {
</Title>
<Space h="md" />
<ExploitTable />
<Space h="xl" />
<Title order={2}>
Attack Groups
</Title>
<Space h="md" />
<GroupTable />
<Space h="md" />
<Title order={2} display="flex" style={{alignItems: "center"}}>
<span>Services</span><Box style={{flexGrow:1}} /><AddButton onClick={()=>setAddNewServiceModalOpen(true)} />
</Title>
<Space h="md" />
<ServiceTable />
<Space h="xl" />
<Space h="md" />
<Title order={2}>
Clients
</Title>
<Space h="md" />
<ClientTable />
<Space h="xl" />
<Space h="md" />
<ViewOptionEditor />
<AddEditServiceModal open={addNewServiceModalOpen} onClose={()=>setAddNewServiceModalOpen(false)} />
</Container>
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/tables/ExploitTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { exploitsQuery, useClientSolver, useServiceSolver } from "@/utils/queries";
import { exploitsQuery, useClientSolver, useGroupSolver, useServiceSolver } from "@/utils/queries";
import { getDateSmallFormatted } from "@/utils/time";
import { Box, Table, Title } from "@mantine/core";
import { DeleteButton, EditButton } from "@/components/inputs/Buttons";
Expand All @@ -14,6 +14,7 @@ export const ExploitTable = () => {
const exploits = exploitsQuery()
const getServiceName = useServiceSolver()
const getClientName = useClientSolver()
const getGroupName = useGroupSolver()

const [deleteExploit, setDeleteExploit] = useState<Exploit|undefined>()
const [editExploit, setEditExploit] = useState<Exploit|undefined>()
Expand All @@ -24,7 +25,7 @@ export const ExploitTable = () => {
<Table.Td style={{width:"100%"}}><Box>{expl.name}</Box></Table.Td>
<Table.Td><Box>{getServiceName(expl.service)}</Box></Table.Td>
<Table.Td><Box>{expl.language}</Box></Table.Td>
<Table.Td><Box>{getClientName(expl.last_execution_by)}</Box></Table.Td>
<Table.Td><Box>{expl.last_execution_group_by?`${getGroupName(expl.last_execution_group_by)} (group)`:getClientName(expl.last_execution_by)}</Box></Table.Td>
<Table.Td><Box>{expl.last_update?getDateSmallFormatted(expl.last_update):"Not started"}</Box></Table.Td>
<Table.Td><EditButton onClick={()=>setEditExploit(expl)} /></Table.Td>
<Table.Td><DeleteButton onClick={()=>setDeleteExploit(expl)} /></Table.Td>
Expand Down
Loading

0 comments on commit c9f8c3c

Please sign in to comment.