Skip to content

Commit

Permalink
add: stats api, chart and general graphics improvements on frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
domysh committed Jun 16, 2024
1 parent 7a5eaf5 commit 567e39a
Show file tree
Hide file tree
Showing 27 changed files with 1,992 additions and 1,196 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@
- Upload tar of exploit to share it with other players or run it remotely
- exploit versioning

Dev Notes:
```
npx openapi-typescript http://localhost:5050/openapi.json -o backendtypes.ts
```
9 changes: 8 additions & 1 deletion backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import time, jwt
from fastapi.middleware.cors import CORSMiddleware
from submitter import run_submitter_daemon
from stats import run_stats_daemon
from utils import *
from env import DEBUG, CORS_ALLOW, JWT_ALGORITHM, JWT_EXPIRE_H
from fastapi.responses import FileResponse
Expand Down Expand Up @@ -162,7 +163,8 @@ async def get_status(loggined: None|bool = Depends(is_loggined)):
""" This will return the application status, and the configuration if the user is allowed to see it """
config = await Configuration.get_from_db()
if config.PASSWORD_HASH: config.PASSWORD_HASH = "********"
messages = await get_messages_array()
messages = await get_messages_array()

return StatusAPI(
status=config.SETUP_STATUS,
loggined=bool(loggined),
Expand All @@ -173,6 +175,8 @@ async def get_status(loggined: None|bool = Depends(is_loggined)):
submitter=None if not loggined or config.SUBMITTER is None else json_like(await Submitter.objects.get(id=config.SUBMITTER)),
messages= messages if loggined else None,
services = json_like(await Service.objects.all()) if loggined else None,
start_time=config.start_time,
end_time=config.end_time
)

@api.post("/setup", response_model=MessageResponse[Configuration], tags=["Status"])
Expand Down Expand Up @@ -228,6 +232,7 @@ async def catch_all(full_path:str):
time.tzset()
init_db()
submitter = run_submitter_daemon()
stats = run_stats_daemon()
try:
uvicorn.run(
"app:app",
Expand All @@ -241,4 +246,6 @@ async def catch_all(full_path:str):
pass
finally:
submitter.terminate()
stats.terminate()
stats.join()
submitter.join()
6 changes: 6 additions & 0 deletions backend/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ class Env(ormar.Model):

key: EnvKey = ormar.String(max_length=1024, primary_key=True)
value: str|None = ormar.String(max_length=1024*1024, nullable=True)

class BinEnv(ormar.Model):
ormar_config = dbconf.copy(tablename="bin_envs")

key: EnvKey = ormar.String(max_length=1024, primary_key=True)
value: str|None = ormar.LargeBinary(max_length=1024*1024, nullable=True)

MANUAL_CLIENT_ID = "manual"

Expand Down
2 changes: 1 addition & 1 deletion backend/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
DB_PORT = os.getenv("DB_PORT", "5432")
POSTGRES_URL = os.getenv("POSTGRES_URL", f"postgresql+psycopg2://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{DB_HOST}:{DB_PORT}/{POSTGRES_DB}")

SUBMITTER_POLLING = 5
FLAG_UPDATE_POLLING = 5
41 changes: 37 additions & 4 deletions backend/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Configuration(BaseModel):
TICK_DURATION: PositiveInt = 120

ATTACK_MODE: AttackMode = AttackMode.TICK_DELAY
LOOP_ATTACK_DELAY: NonNegativeInt = 0
LOOP_ATTACK_DELAY: NonNegativeInt = 60
ATTACK_TIME_TICK_DELAY: NonNegativeInt = 0

FLAG_TIMEOUT: PositiveInt|None = None
Expand All @@ -38,6 +38,9 @@ class Configuration(BaseModel):

SERVER_ID: UUID = uuid4()

__start_time = None
__end_time = None

@property
def login_enabled(self):
return self.AUTHENTICATION_REQUIRED and self.SETUP_STATUS != SetupStatus.SETUP
Expand Down Expand Up @@ -85,7 +88,36 @@ async def get_from_db(cls) -> Self:
keys = Configuration.keys()
result = await Env.objects.filter(Env.key << keys).all()
result = {ele.key:ele.value for ele in result}
return cls(**result)
res = cls(**result)
await res.__get_times()
return res

@property
def start_time(self):
return self.__start_time

@property
def end_time(self):
return self.__end_time

async def __get_times(self):
now = datetime_now()
start_time = self.START_TIME
end_time = self.END_TIME if not self.END_TIME is None and self.END_TIME > now else None
done_query = AttackExecution.objects.filter(AttackExecution.status == AttackExecutionStatus.done.value)
if not start_time:
try:
start_time = (await done_query.order_by(AttackExecution.recieved_at.asc()).first()).recieved_at #We take the first flag time as start time
except ormar.NoMatch:
start_time = None
if start_time and not end_time:
try:
end_time = (await done_query.order_by(AttackExecution.recieved_at.desc()).first()).recieved_at #We take the last flag time as end time
except ormar.NoMatch:
end_time = None
self.__start_time = start_time
self.__end_time = end_time
return start_time, end_time

class StatusAPI(BaseModel):
status: SetupStatus
Expand All @@ -97,7 +129,8 @@ class StatusAPI(BaseModel):
teams: List[TeamDTO]|None = None
messages: List[MessageInfo]|None
services: List[ServiceDTO]|None
start_time: AwareDatetime|None = None
end_time: AwareDatetime|None = None
version: str = env.VERSION
whoami: str = "exploitfarm"



27 changes: 23 additions & 4 deletions backend/models/flags.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
from pydantic import BaseModel
from models.enums import FlagStatus, AttackExecutionStatus
from pydantic import AwareDatetime
from uuid import UUID
from db import FlagID, AttackExecutionID, TeamID, ExploitID, ClientID, FkType

class FlagDTOAttackDetails(BaseModel):
id: AttackExecutionID
start_time: AwareDatetime|None = None
end_time: AwareDatetime|None = None
status: AttackExecutionStatus
recieved_at: AwareDatetime
target: FkType[TeamID]|None = None,
exploit: FkType[ExploitID]|None = None
executed_by: FkType[ClientID]|None = None

class AttackExecutionDTO(BaseModel):
id: AttackExecutionID
start_time: AwareDatetime|None = None
end_time: AwareDatetime|None = None
Expand All @@ -23,8 +34,16 @@ class FlagDTO(BaseModel):
submit_attempts: int = 0
attack: FlagDTOAttackDetails

class TickStats(BaseModel):
tick: int
start_time: AwareDatetime
end_time: AwareDatetime
flags: dict = {}
exploits: dict = {}
services: dict = {}
teams: dict = {}
clients: dict = {}

class FlagStats(BaseModel):
timeout_flags: int
wait_flags: int
invalid_flags: int
ok_flags: int
ticks: list[TickStats] = []
globals: dict = {}
2 changes: 1 addition & 1 deletion backend/models/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ class TeamEditForm(BaseModel):
id: TeamID
name: str|None = None
short_name: str|None = None
host: str|None = None
host: str|None = None
75 changes: 56 additions & 19 deletions backend/routes/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from models.response import *
from utils import *
from models.flags import *
from models.config import *
from db import Flag, UnHashedClientID
from fastapi import APIRouter
from fastapi_pagination import Page, add_pagination, paginate
Expand All @@ -18,7 +19,7 @@
from fastapi_pagination.bases import AbstractParams, BasePage, RawParams
from fastapi_pagination.types import GreaterEqualOne, GreaterEqualZero
from fastapi_pagination.utils import create_pydantic_model
import asyncio
from utils import get_db_stats

T = TypeVar("T")

Expand Down Expand Up @@ -79,7 +80,8 @@ async def get_flags(
attack_status: AttackExecutionStatus|None = Query(None, description="Attack status"),
target: TeamID|None = Query(None, description="Target team"),
exploit: ExploitID|None = Query(None, description="Exploit"),
executed_by: UnHashedClientID|ClientID|None = Query(None, description="Client")
executed_by: UnHashedClientID|ClientID|None = Query(None, description="Client"),
reversed: bool = Query(False, description="Reverse order"),
):
equalities = [
[flag_status.value if flag_status is not None else None, Flag.status],
Expand All @@ -89,32 +91,67 @@ async def get_flags(
[executed_by, Flag.attack.executed_by.id],
]
filter = None

for ele in equalities:
if ele[0] is not None:
equality = ele[0] == ele[1]
if filter is None: filter = equality
else: filter = filter & equality
query = Flag.objects

if filter:
query = query.filter(filter)
return await paginate(
query.select_related(Flag.attack)
.order_by(Flag.attack.recieved_at.desc())
.order_by(Flag.id.desc())
)

query = query.select_related(Flag.attack).exclude_fields("attack__error")

if reversed:
query = query.order_by(Flag.attack.recieved_at.asc()).order_by(Flag.id.asc())
else:
query = query.order_by(Flag.attack.recieved_at.desc()).order_by(Flag.id.desc())

return await paginate(query)

@router.get("/attacks", response_model=CustomPage[AttackExecutionDTO] )
async def get_attacks(
status: AttackExecutionStatus|None = Query(None, description="Attack status"),
target: TeamID|None = Query(None, description="Target team"),
exploit: ExploitID|None = Query(None, description="Exploit"),
executed_by: UnHashedClientID|ClientID|None = Query(None, description="Client"),
reversed: bool = Query(False, description="Reverse order"),
):
equalities = [
[status.value if status is not None else None, AttackExecution.status],
[target, AttackExecution.target.id],
[exploit, AttackExecution.exploit.id],
[executed_by, AttackExecution.executed_by.id],
]
filter = None

for ele in equalities:
if ele[0] is not None:
equality = ele[0] == ele[1]
if filter is None: filter = equality
else: filter = filter & equality
query = AttackExecution.objects

if filter:
query = query.filter(filter)

if reversed:
query = query.order_by(AttackExecution.recieved_at.asc()).order_by(AttackExecution.id.asc())
else:
query = query.order_by(AttackExecution.recieved_at.desc()).order_by(AttackExecution.id.desc())

return await paginate(query)


@router.get("/stats", response_model=FlagStats)
#@cache(expire=5)
async def get_flags():
results = await asyncio.gather(
Flag.objects.filter(Flag.status == FlagStatus.timeout.value).count(),
Flag.objects.filter(Flag.status == FlagStatus.wait.value).count(),
Flag.objects.filter(Flag.status == FlagStatus.invalid.value).count(),
Flag.objects.filter(Flag.status == FlagStatus.ok.value).count(),
)
return FlagStats(
timeout_flags=results[0],
wait_flags=results[1],
invalid_flags=results[2],
ok_flags=results[3]
)
data = await get_db_stats()
if not data:
from stats import complete_stats
return {"ticks":[], "globals":complete_stats()}
return data

add_pagination(router)
Loading

0 comments on commit 567e39a

Please sign in to comment.