├── run └── .gitkeep ├── .gitignore ├── data ├── base │ ├── python │ │ ├── .dockerignore │ │ ├── requirements.txt │ │ ├── Dockerfile │ │ └── exploitlib.py │ └── python-slim │ │ ├── .dockerignore │ │ ├── requirements.txt │ │ ├── Dockerfile │ │ └── exploitlib.py ├── templates │ ├── python │ │ ├── exploitlib.py │ │ ├── requirements.txt │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ └── exploit.py │ └── python-slim │ │ ├── exploitlib.py │ │ ├── requirements.txt │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ └── exploit.py ├── examples │ └── python-test │ │ ├── .dockerignore │ │ ├── exploit.toml │ │ ├── requirements.txt │ │ ├── Dockerfile │ │ ├── exploit.py │ │ └── exploitlib.py ├── test │ ├── k8s_registries.yaml │ ├── nats-seed.sh │ └── k8s_resources.yaml └── config │ ├── dev.toml │ ├── faust.toml │ ├── cini.toml │ └── enowars9.toml ├── frontend ├── .env.production ├── .dockerignore ├── .env.development ├── src │ ├── vite-env.d.ts │ ├── utils │ │ ├── cn.ts │ │ ├── constants.ts │ │ ├── types.ts │ │ └── enums.ts │ ├── index.css │ ├── routes │ │ ├── executions.lazy.tsx │ │ ├── index.lazy.tsx │ │ ├── __root.tsx │ │ ├── config.lazy.tsx │ │ └── submit.lazy.tsx │ ├── main.tsx │ ├── components │ │ ├── Switch.tsx │ │ ├── HoverCard.tsx │ │ ├── StatusCellCard.tsx │ │ ├── StatsCards.tsx │ │ ├── SimpleDisplay.tsx │ │ ├── StatusCell.tsx │ │ └── Dialog.tsx │ └── services │ │ ├── rest.ts │ │ └── webSocket.ts ├── tsconfig.json ├── postcss.config.js ├── .gitignore ├── vite.config.ts ├── tsconfig.node.json ├── index.html ├── Dockerfile ├── biome.json ├── tsconfig.app.json ├── tailwind.config.js ├── config │ └── nginx.conf └── package.json ├── crates ├── kriger_runner │ ├── README.md │ ├── Cargo.toml │ └── src │ │ ├── args.rs │ │ └── runner │ │ └── mod.rs ├── kriger_submitter │ ├── src │ │ ├── utils │ │ │ └── mod.rs │ │ ├── submitter │ │ │ ├── dummy.rs │ │ │ ├── mod.rs │ │ │ └── cini.rs │ │ └── metrics.rs │ └── Cargo.toml ├── kriger_common │ ├── src │ │ ├── server │ │ │ ├── mod.rs │ │ │ ├── args.rs │ │ │ └── runtime.rs │ │ ├── utils │ │ │ ├── mod.rs │ │ │ ├── time.rs │ │ │ └── data.rs │ │ ├── messaging │ │ │ ├── services │ │ │ │ ├── mod.rs │ │ │ │ ├── scheduling.rs │ │ │ │ ├── data.rs │ │ │ │ ├── executions.rs │ │ │ │ └── flags.rs │ │ │ ├── model.rs │ │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── models │ │ │ ├── requests.rs │ │ │ ├── responses.rs │ │ │ └── mod.rs │ │ └── client │ │ │ └── mod.rs │ └── Cargo.toml ├── kriger_rest │ ├── src │ │ ├── routes │ │ │ ├── mod.rs │ │ │ ├── config.rs │ │ │ ├── flags.rs │ │ │ ├── competition.rs │ │ │ └── exploits.rs │ │ ├── config.rs │ │ ├── support.rs │ │ └── lib.rs │ └── Cargo.toml ├── kriger_metrics │ ├── src │ │ ├── args.rs │ │ └── lib.rs │ └── Cargo.toml ├── kriger_ws │ ├── src │ │ └── config.rs │ └── Cargo.toml ├── kriger_scheduler │ ├── Cargo.toml │ └── src │ │ └── utils.rs ├── kriger_mock │ ├── Cargo.toml │ └── src │ │ └── util.rs ├── kriger │ ├── src │ │ ├── cli │ │ │ ├── emoji.rs │ │ │ ├── commands │ │ │ │ ├── mod.rs │ │ │ │ └── submit.rs │ │ │ ├── args.rs │ │ │ ├── mod.rs │ │ │ └── models.rs │ │ ├── args.rs │ │ ├── server │ │ │ ├── metrics.rs │ │ │ ├── args.rs │ │ │ └── mod.rs │ │ └── main.rs │ └── Cargo.toml ├── kriger_controller │ ├── Cargo.toml │ └── src │ │ ├── config.rs │ │ └── metrics.rs └── kriger_fetcher │ ├── src │ ├── fetcher │ │ ├── dummy.rs │ │ ├── mod.rs │ │ └── faust.rs │ ├── metrics.rs │ └── lib.rs │ └── Cargo.toml ├── .github ├── assets │ ├── banner.png │ └── dashboard_1.png └── workflows │ ├── release.yml │ ├── templates.yml │ └── build.yml ├── docs ├── assets │ └── exploits_simple_diagram.png ├── adrs │ ├── 003-scheduling.md │ ├── 004-exploit-containers.md │ ├── 000-adrs.md │ ├── 002-submitters.md │ └── 001-fetchers.md ├── emergency.md ├── debugging.md └── user.md ├── .devcontainer └── devcontainer.json ├── .dockerignore ├── Dockerfile ├── flake.nix ├── flake.lock ├── Cargo.toml ├── docker-compose.yml └── README.md /run/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | !/run/.gitkeep 4 | /run/* -------------------------------------------------------------------------------- /data/base/python/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile -------------------------------------------------------------------------------- /data/base/python-slim/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile -------------------------------------------------------------------------------- /data/templates/python/exploitlib.py: -------------------------------------------------------------------------------- 1 | ../../base/python/exploitlib.py -------------------------------------------------------------------------------- /data/templates/python/requirements.txt: -------------------------------------------------------------------------------- 1 | ../../base/python/requirements.txt -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | VITE_REST_URL="/api" 2 | VITE_WS_URL="/ws" 3 | -------------------------------------------------------------------------------- /data/templates/python-slim/exploitlib.py: -------------------------------------------------------------------------------- 1 | ../../base/python-slim/exploitlib.py -------------------------------------------------------------------------------- /data/templates/python-slim/requirements.txt: -------------------------------------------------------------------------------- 1 | ../../base/python-slim/requirements.txt -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .dockerignore 3 | .gitignore 4 | Dockerfile -------------------------------------------------------------------------------- /data/base/python-slim/requirements.txt: -------------------------------------------------------------------------------- 1 | # Add exploit-specific dependencies below: 2 | -------------------------------------------------------------------------------- /crates/kriger_runner/README.md: -------------------------------------------------------------------------------- 1 | # kriger_runner 2 | 3 | ## Anatomy of an Exploit 4 | 5 | TODO 6 | -------------------------------------------------------------------------------- /.github/assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyberlandslaget/kriger/HEAD/.github/assets/banner.png -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | VITE_REST_URL="http://localhost:8000" 2 | VITE_WS_URL="ws://localhost:8001" 3 | -------------------------------------------------------------------------------- /.github/assets/dashboard_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyberlandslaget/kriger/HEAD/.github/assets/dashboard_1.png -------------------------------------------------------------------------------- /crates/kriger_submitter/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | -------------------------------------------------------------------------------- /data/templates/python/.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .venv/ 3 | .dockerignore 4 | exploit.toml 5 | exploitlib.py 6 | Dockerfile -------------------------------------------------------------------------------- /data/examples/python-test/.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .venv/ 3 | .dockerignore 4 | exploit.toml 5 | exploitlib.py 6 | Dockerfile -------------------------------------------------------------------------------- /data/templates/python-slim/.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .venv/ 3 | .dockerignore 4 | exploit.toml 5 | exploitlib.py 6 | Dockerfile -------------------------------------------------------------------------------- /docs/assets/exploits_simple_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyberlandslaget/kriger/HEAD/docs/assets/exploits_simple_diagram.png -------------------------------------------------------------------------------- /docs/adrs/003-scheduling.md: -------------------------------------------------------------------------------- 1 | # Exploit containers 2 | 3 | ## Status 4 | 5 | Draft 6 | 7 | ## Decision 8 | 9 | ## Consequences 10 | -------------------------------------------------------------------------------- /docs/adrs/004-exploit-containers.md: -------------------------------------------------------------------------------- 1 | # Scheduling 2 | 3 | ## Status 4 | 5 | Draft 6 | 7 | ## Decision 8 | 9 | ## Consequences 10 | -------------------------------------------------------------------------------- /crates/kriger_common/src/server/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | pub mod runtime; 5 | pub mod args; -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | /// 5 | -------------------------------------------------------------------------------- /crates/kriger_common/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | pub(crate) mod data; 5 | pub mod time; 6 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "features": { 4 | "ghcr.io/devcontainers/features/rust:1": {} 5 | } 6 | } -------------------------------------------------------------------------------- /data/test/k8s_registries.yaml: -------------------------------------------------------------------------------- 1 | mirrors: 2 | r.o99.no: 3 | endpoint: 4 | - "http://registry:5000" 5 | localhost:5000: 6 | endpoint: 7 | - "http://registry:5000" 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.devcontainer 2 | /.git 3 | /.github 4 | /.idea 5 | /data 6 | /docs 7 | /frontend 8 | /run 9 | /target 10 | .dockerignore 11 | .gitignore 12 | docker-compose.yml 13 | Dockerfile 14 | README.md -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /crates/kriger_common/src/messaging/services/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | pub mod data; 5 | pub mod executions; 6 | pub mod flags; 7 | pub mod scheduling; 8 | -------------------------------------------------------------------------------- /docs/emergency.md: -------------------------------------------------------------------------------- 1 | # Emergency 2 | 3 | **First of all**: 4 | 5 | - Don't panic. 6 | - Ping @0xle if you haven't already. 7 | 8 | ## Running (Entirely) Locally 9 | 10 | TODO 11 | 12 | ## Recovery 13 | 14 | TODO 15 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | export default { 5 | plugins: { 6 | tailwindcss: {}, 7 | autoprefixer: {}, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /crates/kriger_rest/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | pub(crate) mod competition; 5 | pub(crate) mod config; 6 | pub(crate) mod exploits; 7 | pub(crate) mod flags; -------------------------------------------------------------------------------- /data/examples/python-test/exploit.toml: -------------------------------------------------------------------------------- 1 | [exploit] 2 | name = "test" 3 | service = "Service 1 Checker 1" 4 | replicas = 0 5 | enabled = true 6 | 7 | [exploit.resources] 8 | cpu_limit = "1" 9 | mem_limit = "512M" 10 | timeout = 10 11 | -------------------------------------------------------------------------------- /data/examples/python-test/requirements.txt: -------------------------------------------------------------------------------- 1 | # These dependencies are included in the base image to reduce the build time for exploits 2 | requests==2.32.3 3 | pwntools==4.12.0 4 | cryptography==42.0.8 5 | 6 | # Add exploit-specific dependencies below: 7 | -------------------------------------------------------------------------------- /crates/kriger_metrics/src/args.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use clap_derive::Parser; 5 | 6 | #[derive(Parser, Debug)] 7 | #[command(version, about)] 8 | #[group(skip)] 9 | pub struct Args {} 10 | -------------------------------------------------------------------------------- /data/base/python/requirements.txt: -------------------------------------------------------------------------------- 1 | # These dependencies are included in the base image to reduce the build time for exploits 2 | requests==2.32.3 3 | pwntools==4.14.1 4 | cryptography==44.0.2 5 | pycryptodome==3.22.0 6 | 7 | # Add exploit-specific dependencies below: 8 | -------------------------------------------------------------------------------- /data/templates/python/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG REPOSITORY="ghcr.io/cyberlandslaget/kriger-exploit-base" 2 | FROM $REPOSITORY:python 3 | 4 | COPY requirements.txt . 5 | RUN /usr/bin/uv pip install --system -r requirements.txt 6 | COPY . . 7 | 8 | CMD ["python3", "exploit.py"] 9 | -------------------------------------------------------------------------------- /data/examples/python-test/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG REPOSITORY="ghcr.io/cyberlandslaget/kriger-exploit-base" 2 | FROM $REPOSITORY:python 3 | 4 | COPY requirements.txt . 5 | RUN /usr/bin/uv pip install --system -r requirements.txt 6 | COPY . . 7 | 8 | CMD ["python3", "exploit.py"] 9 | -------------------------------------------------------------------------------- /data/templates/python-slim/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG REPOSITORY="ghcr.io/cyberlandslaget/kriger-exploit-base" 2 | FROM $REPOSITORY:python-slim 3 | 4 | COPY requirements.txt . 5 | RUN /usr/bin/uv pip install --system -r requirements.txt 6 | COPY . . 7 | 8 | CMD ["python3", "exploit.py"] 9 | -------------------------------------------------------------------------------- /data/config/dev.toml: -------------------------------------------------------------------------------- 1 | [competition] 2 | start = "2024-01-01T08:00:00Z" 3 | tick = 1 4 | tick_start = 0 5 | flag_validity = 5 6 | flag_format = "[A-Z0-9]{31}=" 7 | 8 | [submitter] 9 | type = "dummy" 10 | interval = 1 11 | 12 | [fetcher] 13 | type = "dummy" 14 | interval = 1 15 | -------------------------------------------------------------------------------- /crates/kriger_common/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | pub mod client; 5 | #[cfg(feature = "server")] 6 | pub mod messaging; 7 | pub mod models; 8 | #[cfg(feature = "server")] 9 | pub mod server; 10 | pub mod utils; 11 | -------------------------------------------------------------------------------- /frontend/src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import { clsx, type ClassValue } from "clsx"; 5 | import { twMerge } from "tailwind-merge"; 6 | 7 | export default function cn(...args: ClassValue[]) { 8 | return twMerge(clsx(args)); 9 | } -------------------------------------------------------------------------------- /data/templates/python/exploit.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-only 2 | # Copyright Authors of kriger 3 | 4 | import exploitlib 5 | from typing import Any 6 | 7 | 8 | def exploit(ip: str, hint: Any | None): 9 | print("RX5MCY7TGO5SJ75H37KU7KUQGP23UQY=") 10 | 11 | 12 | exploitlib.run(exploit) 13 | -------------------------------------------------------------------------------- /data/templates/python-slim/exploit.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-only 2 | # Copyright Authors of kriger 3 | 4 | import exploitlib 5 | from typing import Any 6 | 7 | 8 | def exploit(ip: str, hint: Any | None): 9 | print("RX5MCY7TGO5SJ75H37KU7KUQGP23UQY=") 10 | 11 | 12 | exploitlib.run(exploit) 13 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | *.gen.ts -------------------------------------------------------------------------------- /crates/kriger_common/src/models/requests.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | pub struct FlagHintQuery { 8 | pub service: String, 9 | } 10 | 11 | #[derive(Serialize, Deserialize)] 12 | pub struct FlagSubmitRequest { 13 | pub flags: Vec, 14 | } 15 | -------------------------------------------------------------------------------- /crates/kriger_ws/src/config.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use clap_derive::Parser; 5 | 6 | #[derive(Parser, Debug)] 7 | #[command(version, about)] 8 | #[group(skip)] 9 | pub struct Config { 10 | /// The socket address to listen to 11 | #[arg(env, long, default_value = "[::]:8001")] 12 | pub(crate) ws_listen: String, 13 | } 14 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import { defineConfig } from "vite"; 5 | import react from "@vitejs/plugin-react-swc"; 6 | import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [TanStackRouterVite(), react()], 11 | }); 12 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | export const CONFIG = { 5 | webSocketUrl: new URL( 6 | import.meta.env.VITE_WS_URL ?? "http://localhost:8001", 7 | location.origin, 8 | ), 9 | restUrl: new URL( 10 | import.meta.env.VITE_REST_URL ?? "http://localhost:8000", 11 | location.origin, 12 | ), 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /data/config/faust.toml: -------------------------------------------------------------------------------- 1 | [competition] 2 | start = "2024-08-11T20:34:47Z" 3 | tick = 60 4 | tick_start = 0 5 | flag_validity = 5 6 | flag_format = "CYBL_[A-Za-z0-9/+]{32}" 7 | 8 | [submitter] 9 | type = "faust" 10 | interval = 1 11 | host = "10.10.0.1:31337" 12 | 13 | [fetcher] 14 | type = "faust" 15 | interval = 10 # 6 req/min 16 | url = "http://10.10.0.1:5000/competition/teams.json" 17 | ip_format = "10.60.{}.1" 18 | -------------------------------------------------------------------------------- /data/test/nats-seed.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | # Copyright Authors of kriger 5 | 6 | # TODO: Remove once the fetcher is functional 7 | nats kv put services U2VydmljZSAxIENoZWNrZXIgMQ '{"name": "Service 1 Checker 1", "hasHint": false}' 8 | 9 | for i in `seq 0 9` 10 | do 11 | nats kv put teams "$i" "{\"name\": \"Team $i\", \"ipAddress\": \"10.60.$i.1\", \"services\":{}}" 12 | done 13 | -------------------------------------------------------------------------------- /data/config/cini.toml: -------------------------------------------------------------------------------- 1 | [competition] 2 | start = "2024-08-31T07:30:00.000Z" 3 | tick = 120 4 | tick_start = 0 5 | flag_validity = 5 6 | flag_format = "[A-Z0-9]{31}=" 7 | 8 | [submitter] 9 | type = "cini" 10 | interval = 3 # 20 req/min 11 | batch = 1000 # body size limit: 100 kB 12 | url = "http://10.10.0.1:8080/flags" 13 | token = "" 14 | 15 | [fetcher] 16 | type = "cini" 17 | interval = 10 # 6 req/min 18 | url = "http://10.10.0.1:8081" 19 | -------------------------------------------------------------------------------- /data/examples/python-test/exploit.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-only 2 | # Copyright Authors of kriger 3 | 4 | import exploitlib 5 | import requests 6 | from typing import Any 7 | 8 | 9 | def exploit(ip: str, hint: Any | None): 10 | team_id = ip.split(".")[2] 11 | res = requests.get(f"http://172.30.100.1:8080/getflag/{team_id}") 12 | res.raise_for_status() 13 | print(res.text) 14 | 15 | 16 | exploitlib.run(exploit) 17 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: AGPL-3.0-only */ 2 | /* Copyright Authors of kriger */ 3 | 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | html { 9 | @apply h-full; 10 | scroll-behavior: smooth; 11 | } 12 | 13 | body { 14 | @apply bg-primary-bg text-white h-full; 15 | font-family: "B612 Mono", monospace; 16 | } 17 | 18 | #root { 19 | @apply w-full h-full max-w-full max-h-full; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Kriger 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim AS build 2 | ENV PNPM_HOME="/pnpm" 3 | ENV PATH="$PNPM_HOME:$PATH" 4 | RUN corepack enable pnpm 5 | WORKDIR /app 6 | COPY package.json . 7 | COPY pnpm-lock.yaml . 8 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 9 | COPY . . 10 | RUN pnpm run build 11 | 12 | FROM cgr.dev/chainguard/nginx 13 | COPY --from=build /app/dist /usr/share/nginx/html 14 | COPY ./config/nginx.conf /etc/nginx/conf.d/nginx.default.conf 15 | -------------------------------------------------------------------------------- /crates/kriger_common/src/models/responses.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// The structure used to serialize consistent responses to the consumer. 7 | #[derive(Serialize, Deserialize, Debug)] 8 | #[serde(rename_all = "lowercase")] 9 | pub enum AppResponse { 10 | #[serde(rename = "data")] 11 | Ok(T), 12 | Error { 13 | message: String, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /frontend/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "formatter": { 7 | "indentStyle": "space" 8 | }, 9 | "linter": { 10 | "enabled": true, 11 | "rules": { 12 | "recommended": true 13 | } 14 | }, 15 | "json": { 16 | "parser": { 17 | "allowComments": true 18 | } 19 | }, 20 | "files": { 21 | "ignore": ["dist/**", "src/**/*.gen.ts"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/kriger_common/src/server/args.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | #[cfg(debug_assertions)] 5 | const DEFAULT_NATS_URL: &str = "nats://127.0.0.1:4222"; 6 | #[cfg(not(debug_assertions))] 7 | const DEFAULT_NATS_URL: &str = "nats://nats:4222"; 8 | 9 | #[derive(clap::Args, Debug)] 10 | #[group(skip)] 11 | pub struct RuntimeArgs { 12 | /// The URL to the NATS/JetStream server 13 | #[arg(env, long, default_value = DEFAULT_NATS_URL)] 14 | pub nats_url: String, 15 | } 16 | -------------------------------------------------------------------------------- /data/config/enowars9.toml: -------------------------------------------------------------------------------- 1 | [competition] 2 | start = "2025-07-19T12:00:00Z" 3 | tick = 60 4 | tick_start = 0 5 | flag_validity = 10 # Assuming it's same this year as 2023 https://github.com/Cyberlandslaget/angrepa/blob/105fee562830776f868f85ebfd112aeb6ac12f79/config/enowars7.toml 6 | flag_format = "ENO[A-Za-z0-9+\\/=]{48}" 7 | 8 | [submitter] 9 | type = "enowars" 10 | interval = 1 11 | host = "10.0.13.37:1337" 12 | 13 | [fetcher] 14 | type = "enowars" 15 | interval = 10 # 6 req/min 16 | url = "https://9.enowars.com/scoreboard/attack.json" -------------------------------------------------------------------------------- /crates/kriger_scheduler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kriger_scheduler" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | [features] 10 | 11 | [dependencies] 12 | base64.workspace = true 13 | chrono.workspace = true 14 | color-eyre.workspace = true 15 | dashmap.workspace = true 16 | futures.workspace = true 17 | kriger_common.path = "../kriger_common" 18 | prometheus-client.workspace = true 19 | tokio.workspace = true 20 | tracing.workspace = true 21 | -------------------------------------------------------------------------------- /frontend/src/routes/executions.lazy.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import { createLazyFileRoute } from "@tanstack/react-router"; 5 | import ExecutionDisplay from "../components/ExecutionDisplay"; 6 | 7 | function Executions() { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | 15 | export const Route = createLazyFileRoute("/executions")({ 16 | component: () => Executions(), 17 | }); 18 | -------------------------------------------------------------------------------- /crates/kriger_rest/src/routes/config.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use crate::AppState; 5 | use axum::extract::State; 6 | use axum::response::IntoResponse; 7 | use axum::Json; 8 | use kriger_common::models; 9 | use std::ops::Deref; 10 | use std::sync::Arc; 11 | 12 | pub(crate) async fn get_server_config(state: State>) -> impl IntoResponse { 13 | let config = state.runtime.config.deref().clone(); 14 | let config: models::AppConfig = config.into(); 15 | Json(models::responses::AppResponse::Ok(config)) 16 | } 17 | -------------------------------------------------------------------------------- /data/base/python-slim/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://github.com/astral-sh/uv/pkgs/container/uv 2 | FROM ghcr.io/astral-sh/uv:0.3.3 AS uv 3 | 4 | FROM ghcr.io/cyberlandslaget/kriger AS runner 5 | 6 | FROM python:3.12-slim-bookworm 7 | COPY --from=uv /uv /usr/bin/uv 8 | COPY --from=runner /usr/bin/kriger /usr/bin/kriger 9 | 10 | WORKDIR /exploit 11 | 12 | # Disable stdout/stderr buffering. See https://docs.python.org/3/using/cmdline.html#cmdoption-u 13 | ENV PYTHONUNBUFFERED 1 14 | 15 | COPY requirements.txt . 16 | RUN /usr/bin/uv pip install --system -r requirements.txt 17 | COPY . . 18 | 19 | ENTRYPOINT ["/usr/bin/kriger", "runner"] -------------------------------------------------------------------------------- /crates/kriger_mock/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kriger_mock" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | [dependencies] 10 | anyhow = "1.0.86" 11 | axum = { version = "0.8.3" } 12 | clap.workspace = true 13 | hyper = { version = "1.4.0", features = ["server", "http1"] } 14 | rand.workspace = true 15 | serde.workspace = true 16 | tokio = { workspace = true, features = [ 17 | "rt", 18 | "rt-multi-thread", 19 | "net", 20 | "macros", 21 | ] } 22 | tracing.workspace = true 23 | tracing-subscriber.workspace = true 24 | -------------------------------------------------------------------------------- /frontend/src/routes/index.lazy.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import { createLazyFileRoute } from "@tanstack/react-router"; 5 | import SimpleDisplay from "../components/SimpleDisplay"; 6 | import { StatsCards } from "../components/StatsCards"; 7 | 8 | function DashboardPage() { 9 | return ( 10 |
11 | 12 | 13 |
14 | ); 15 | } 16 | 17 | export const Route = createLazyFileRoute("/")({ 18 | component: DashboardPage, 19 | }); 20 | -------------------------------------------------------------------------------- /crates/kriger_rest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kriger_rest" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | [features] 10 | 11 | [dependencies] 12 | axum = { workspace = true, features = ["macros", "query"] } 13 | clap.workspace = true 14 | clap_derive.workspace = true 15 | color-eyre.workspace = true 16 | kriger_common = { path = "../kriger_common" } 17 | serde.workspace = true 18 | thiserror.workspace = true 19 | tokio.workspace = true 20 | tower-http = { workspace = true, features = ["cors", "trace"] } 21 | tracing.workspace = true 22 | -------------------------------------------------------------------------------- /docs/adrs/000-adrs.md: -------------------------------------------------------------------------------- 1 | # Architecture Decision Records (ADRs) 2 | 3 | ## Status 4 | 5 | Proposed 6 | 7 | ## Context 8 | 9 | Multiple decisions were made inside the codebase, many without any explanation as to why the decision was made. 10 | 11 | ## Decision 12 | 13 | Significant architectural decisions should be recorded as architectural architecture decision records (ADRs). 14 | 15 | ## Consequences 16 | 17 | Architecture decision records should be written as significant or relevant decisions are made. 18 | See [Documenting architecture decisions - Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 19 | -------------------------------------------------------------------------------- /crates/kriger/src/cli/emoji.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use console::Emoji; 5 | 6 | pub(crate) static ROCKET: Emoji = Emoji("🚀", ""); 7 | pub(crate) static PACKAGE: Emoji = Emoji("📦", ""); 8 | pub(crate) static HAMMER: Emoji = Emoji("🔨", ""); 9 | pub(crate) static SPARKLES: Emoji = Emoji("✨", ""); 10 | pub(crate) static CROSS_MARK: Emoji = Emoji("❌", ""); 11 | pub(crate) static INFORMATION: Emoji = Emoji("ℹ️", ""); 12 | pub(crate) static CHECK_MARK: Emoji = Emoji("✅", ""); 13 | pub(crate) static CHEQUERED_FLAG: Emoji = Emoji("🏁", ""); 14 | pub(crate) static TRIANGULAR_FLAG: Emoji = Emoji("🏁", ""); 15 | -------------------------------------------------------------------------------- /crates/kriger_rest/src/config.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use clap_derive::Parser; 5 | 6 | #[derive(Parser, Debug)] 7 | #[command(version, about)] 8 | #[group(skip)] 9 | pub struct Config { 10 | /// The socket address to listen to 11 | #[arg(env, long, default_value = "[::]:8000")] 12 | pub(crate) rest_listen: String, 13 | 14 | /// The origin(s) to allow CORS for 15 | #[arg( 16 | env, 17 | long, 18 | default_value = "https://kriger.o99.no,http://localhost:5173", 19 | value_delimiter = ',' 20 | )] 21 | pub(crate) rest_cors_origins: Vec, 22 | } 23 | -------------------------------------------------------------------------------- /data/test/k8s_resources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: kriger-exploits 5 | --- 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | name: nats 10 | namespace: kriger-exploits 11 | spec: 12 | type: ClusterIP 13 | ports: 14 | - port: 4222 15 | targetPort: nats 16 | --- 17 | # FIXME: For some reason Endpoints *work*, but not EndpointSlices. 18 | # EndpointSlices is the recommended replacement for Endpoints. 19 | apiVersion: v1 20 | kind: Endpoints 21 | metadata: 22 | name: nats 23 | namespace: kriger-exploits 24 | subsets: 25 | - addresses: 26 | - ip: 172.30.100.10 27 | ports: 28 | - port: 4222 29 | 30 | -------------------------------------------------------------------------------- /crates/kriger_metrics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kriger_metrics" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | [features] 10 | 11 | [dependencies] 12 | clap.workspace = true 13 | clap_derive.workspace = true 14 | color-eyre.workspace = true 15 | futures.workspace = true 16 | kriger_common.path = "../kriger_common" 17 | opentelemetry.workspace = true 18 | opentelemetry-semantic-conventions.workspace = true 19 | opentelemetry_sdk.workspace = true 20 | opentelemetry-otlp = { workspace = true, features = ["metrics"] } 21 | tokio.workspace = true 22 | tracing.workspace = true 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release kriger 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | release-ubuntu: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Build 21 | run: cargo build -p kriger --release && mv target/release/kriger target/release/kriger_amd64 22 | 23 | - name: Release 24 | uses: softprops/action-gh-release@v2 25 | if: startsWith(github.ref, 'refs/tags/') 26 | with: 27 | files: | 28 | target/release/kriger_amd64 29 | 30 | -------------------------------------------------------------------------------- /crates/kriger_controller/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kriger_controller" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | [features] 10 | 11 | [dependencies] 12 | async-nats.workspace = true 13 | clap.workspace = true 14 | clap_derive.workspace = true 15 | color-eyre.workspace = true 16 | futures.workspace = true 17 | k8s-openapi = { version = "0.24.0", features = ["latest"] } 18 | kriger_common = { path = "../kriger_common" } 19 | kube = "0.99.0" 20 | lazy_static.workspace = true 21 | prometheus-client.workspace = true 22 | tokio.workspace = true 23 | tracing.workspace = true 24 | -------------------------------------------------------------------------------- /crates/kriger_fetcher/src/fetcher/dummy.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use crate::fetcher::{CompetitionData, FetchOptions, Fetcher, FetcherError}; 5 | use async_trait::async_trait; 6 | use dashmap::DashMap; 7 | use kriger_common::models; 8 | 9 | pub(crate) struct DummyFetcher; 10 | 11 | #[async_trait] 12 | impl Fetcher for DummyFetcher { 13 | async fn fetch( 14 | &self, 15 | _options: &FetchOptions, 16 | _services: &DashMap, 17 | ) -> Result { 18 | Ok(CompetitionData { 19 | flag_hints: Some(vec![]), 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /data/base/python/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://github.com/astral-sh/uv/pkgs/container/uv 2 | FROM ghcr.io/astral-sh/uv:0.3.3 AS uv 3 | 4 | FROM ghcr.io/cyberlandslaget/kriger AS runner 5 | 6 | # Note that we SHOULD NOT use the slim variant due to some Python packages relying on native compilation (eg. gcc) 7 | FROM python:3.12-bookworm 8 | COPY --from=uv /uv /usr/bin/uv 9 | COPY --from=runner /usr/bin/kriger /usr/bin/kriger 10 | 11 | WORKDIR /exploit 12 | 13 | # Disable stdout/stderr buffering. See https://docs.python.org/3/using/cmdline.html#cmdoption-u 14 | ENV PYTHONUNBUFFERED 1 15 | 16 | COPY requirements.txt . 17 | RUN /usr/bin/uv pip install --system -r requirements.txt 18 | COPY . . 19 | 20 | ENTRYPOINT ["/usr/bin/kriger", "runner"] -------------------------------------------------------------------------------- /crates/kriger_common/src/utils/time.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use chrono::{DateTime, Utc}; 5 | use color_eyre::eyre; 6 | 7 | // Ported from angrepa 8 | pub fn get_instant_from_datetime(target: DateTime) -> eyre::Result { 9 | let time_since_start = Utc::now() - target; 10 | let instant = if time_since_start < chrono::Duration::seconds(0) { 11 | // The target time is in the future, we have to negate it 12 | tokio::time::Instant::now() + (-time_since_start).to_std()? 13 | } else { 14 | // The target time is in the past 15 | tokio::time::Instant::now() - time_since_start.to_std()? 16 | }; 17 | Ok(instant) 18 | } 19 | -------------------------------------------------------------------------------- /crates/kriger_submitter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kriger_submitter" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | [features] 10 | 11 | [dependencies] 12 | async-nats.workspace = true 13 | async-trait.workspace = true 14 | base64.workspace = true 15 | color-eyre.workspace = true 16 | futures.workspace = true 17 | kriger_common.path = "../kriger_common" 18 | prometheus-client.workspace = true 19 | rand.workspace = true 20 | reqwest.workspace = true 21 | serde.workspace = true 22 | serde_json.workspace = true 23 | thiserror.workspace = true 24 | tokio.workspace = true 25 | tokio-util.workspace = true 26 | tracing.workspace = true 27 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /crates/kriger_fetcher/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kriger_fetcher" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | [features] 10 | 11 | [dependencies] 12 | async-trait.workspace = true 13 | base64.workspace = true 14 | chrono.workspace = true 15 | color-eyre.workspace = true 16 | dashmap.workspace = true 17 | futures.workspace = true 18 | kriger_common = { path = "../kriger_common" } 19 | prometheus-client.workspace = true 20 | reqwest.workspace = true 21 | serde.workspace = true 22 | serde_json.workspace = true 23 | thiserror.workspace = true 24 | tokio.workspace = true 25 | tracing.workspace = true 26 | 27 | [dev-dependencies] 28 | tokio = { workspace = true, features = ["macros"] } 29 | warp = "0.3.7" 30 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import React from "react"; 5 | import ReactDOM from "react-dom/client"; 6 | import { RouterProvider, createRouter } from "@tanstack/react-router"; 7 | import { routeTree } from "./routeTree.gen"; 8 | import "./index.css"; 9 | 10 | // Create a new router instance 11 | const router = createRouter({ routeTree, defaultPreload: "intent" }); 12 | 13 | // Register the router instance for type safety 14 | declare module "@tanstack/react-router" { 15 | interface Register { 16 | router: typeof router; 17 | } 18 | } 19 | 20 | const root = document.getElementById("root"); 21 | if (root) { 22 | ReactDOM.createRoot(root).render( 23 | 24 | 25 | , 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /data/base/python/exploitlib.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-only 2 | # Copyright Authors of kriger 3 | 4 | ## FOR EXPLOITS: DO NOT EDIT ## 5 | 6 | # This file will be automatically included in the base image and thus, 7 | # changes are ignored by default in the exploit template. If you REALLY 8 | # need to edit this file, remove the exclusion from .dockerignore in the 9 | # exploit. 10 | 11 | import json 12 | import os 13 | 14 | from typing import Any, Callable 15 | 16 | 17 | # The exploit itself will be provided as a function to allow support for other execution models in the future 18 | def run(func: Callable[[str, Any | None], None]) -> None: 19 | ip = os.getenv("IP") 20 | hint = os.getenv("HINT") 21 | 22 | assert ip is not None 23 | if hint is not None: 24 | hint = json.loads(hint) 25 | 26 | func(ip, hint) 27 | -------------------------------------------------------------------------------- /data/base/python-slim/exploitlib.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-only 2 | # Copyright Authors of kriger 3 | 4 | ## FOR EXPLOITS: DO NOT EDIT ## 5 | 6 | # This file will be automatically included in the base image and thus, 7 | # changes are ignored by default in the exploit template. If you REALLY 8 | # need to edit this file, remove the exclusion from .dockerignore in the 9 | # exploit. 10 | 11 | import json 12 | import os 13 | 14 | from typing import Any, Callable 15 | 16 | 17 | # The exploit itself will be provided as a function to allow support for other execution models in the future 18 | def run(func: Callable[[str, Any | None], None]) -> None: 19 | ip = os.getenv("IP") 20 | hint = os.getenv("HINT") 21 | 22 | assert ip is not None 23 | if hint is not None: 24 | hint = json.loads(hint) 25 | 26 | func(ip, hint) 27 | -------------------------------------------------------------------------------- /data/examples/python-test/exploitlib.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: AGPL-3.0-only 2 | # Copyright Authors of kriger 3 | 4 | ## FOR EXPLOITS: DO NOT EDIT ## 5 | 6 | # This file will be automatically included in the base image and thus, 7 | # changes are ignored by default in the exploit template. If you REALLY 8 | # need to edit this file, remove the exclusion from .dockerignore in the 9 | # exploit. 10 | 11 | import json 12 | import os 13 | 14 | from typing import Any, Callable 15 | 16 | 17 | # The exploit itself will be provided as a function to allow support for other execution models in the future 18 | def run(func: Callable[[str, Any | None], None]) -> None: 19 | ip = os.getenv("IP") 20 | hint = os.getenv("HINT") 21 | 22 | assert ip is not None 23 | if hint is not None: 24 | hint = json.loads(hint) 25 | 26 | func(ip, hint) 27 | -------------------------------------------------------------------------------- /crates/kriger_runner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kriger_runner" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | [features] 10 | 11 | [dependencies] 12 | async-channel = "2.3.1" 13 | async-nats = "0.40.0" 14 | base64 = { workspace = true, features = ["alloc"] } 15 | clap.workspace = true 16 | clap_derive.workspace = true 17 | color-eyre.workspace = true 18 | flume.workspace = true 19 | futures.workspace = true 20 | kriger_common = { path = "../kriger_common" } 21 | num_cpus = "1.16.0" 22 | serde_json.workspace = true 23 | thiserror.workspace = true 24 | tokio = { workspace = true, features = ["process"] } 25 | tokio-util.workspace = true 26 | tokio-stream = { workspace = true, features = ["io-util"] } 27 | tracing.workspace = true 28 | regex.workspace = true 29 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 7 | theme: { 8 | extend: { 9 | colors: { 10 | "primary-bg": "#0f192e", 11 | background: "#0f192e", 12 | input: "rgb(148 163 184 / 0.4)", // bg-gray-200/70 13 | primary: "rgb(229 231 235 / 0.7)", // bg-slate-400/40 14 | red: { 15 | light: "#e8d7d7", 16 | DEFAULT: "#e78284", 17 | dark: "#7b2021", 18 | }, 19 | green: { 20 | light: "#dfe7db", 21 | DEFAULT: "#a6d189", 22 | dark: "#2d550f", 23 | }, 24 | yellow: { 25 | light: "#eae6df", 26 | DEFAULT: "#edda9b", 27 | dark: "#83611d", 28 | }, 29 | }, 30 | }, 31 | }, 32 | plugins: [require("tailwindcss-animate")], 33 | }; 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lukemathwalker/cargo-chef:latest-rust-1.86-slim-bookworm AS base 2 | RUN apt-get update && apt-get install -y sccache 3 | ENV RUSTC_WRAPPER=sccache SCCACHE_DIR=/sccache 4 | 5 | FROM base AS planner 6 | WORKDIR /app 7 | COPY . . 8 | RUN cargo chef prepare --recipe-path recipe.json 9 | 10 | FROM base AS builder 11 | WORKDIR /app 12 | COPY --from=planner /app/recipe.json recipe.json 13 | RUN --mount=type=cache,target=/usr/local/cargo/registry \ 14 | --mount=type=cache,target=/usr/local/cargo/git \ 15 | --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ 16 | cargo chef cook --release --recipe-path recipe.json 17 | COPY . . 18 | RUN --mount=type=cache,target=/usr/local/cargo/registry \ 19 | --mount=type=cache,target=/usr/local/cargo/git \ 20 | --mount=type=cache,target=$SCCACHE_DIR,sharing=locked \ 21 | cargo build --release 22 | 23 | FROM gcr.io/distroless/cc-debian12:nonroot 24 | COPY --from=builder /app/target/release/kriger /usr/bin/kriger 25 | ENTRYPOINT ["/usr/bin/kriger"] 26 | -------------------------------------------------------------------------------- /crates/kriger_ws/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kriger_ws" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | [features] 10 | 11 | [dependencies] 12 | async-nats.workspace = true 13 | clap.workspace = true 14 | clap_derive.workspace = true 15 | color-eyre.workspace = true 16 | fastwebsockets = { version = "0.10.0", features = [ 17 | "upgrade", 18 | "unstable-split", 19 | ] } 20 | flume.workspace = true 21 | futures.workspace = true 22 | http-body-util = "0.1.2" 23 | hyper = { workspace = true, features = ["server", "http1", "http2"] } 24 | hyper-util = { workspace = true, features = [ 25 | "tokio", 26 | "server", 27 | "http1", 28 | "http2", 29 | ] } 30 | kriger_common = { path = "../kriger_common" } 31 | time.workspace = true 32 | tokio = { workspace = true, features = ["rt", "net"] } 33 | serde.workspace = true 34 | serde_json.workspace = true 35 | tracing.workspace = true 36 | form_urlencoded = "1.2.1" 37 | -------------------------------------------------------------------------------- /frontend/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import type { ExecutionResultMessage } from "../services/models"; 5 | import type { FlagCode } from "./enums"; 6 | 7 | export type TeamFlagMap = { 8 | [teamId: string]: TeamServiceMap; 9 | }; 10 | 11 | export type TeamServiceMap = { 12 | [service: string]: TeamServiceFlags; 13 | }; 14 | 15 | export type TeamServiceFlags = { 16 | [flag: string]: TeamFlagStatus; 17 | }; 18 | 19 | export type TeamFlagStatus = { 20 | status?: FlagCode; 21 | published: number; 22 | exploit: string | null; 23 | }; 24 | 25 | export type TeamExecutionMap = { 26 | [teamId: string]: ExploitExecutionMap; 27 | }; 28 | 29 | export type ExploitExecutionMap = { 30 | [exploit: string]: TeamExploitExecutions; 31 | }; 32 | 33 | export type TeamExploitExecutions = { 34 | [sequence: number]: TeamExecutionStatus; 35 | }; 36 | 37 | export type TeamExecutionStatus = { 38 | published: number; 39 | result?: ExecutionResultMessage; 40 | }; 41 | -------------------------------------------------------------------------------- /crates/kriger_controller/src/config.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use clap_derive::Parser; 5 | 6 | #[derive(Parser, Debug)] 7 | #[command(version, about)] 8 | #[group(skip)] 9 | pub struct Config { 10 | /// The Kubernetes namespace to schedule exploits in 11 | #[arg(env, long, default_value = "kriger-exploits")] 12 | pub controller_exploit_namespace: String, 13 | 14 | /// The NATS service URL to pass to exploit runners 15 | #[arg(env, long, default_value = "nats://nats:4222")] 16 | pub controller_nats_svc_url: String, 17 | 18 | /// The OpenTelemetry OTLP endpoint to pass to exploit runners 19 | #[arg( 20 | env, 21 | long, 22 | default_value = "grpc://opentelemetry-collector.monitoring.svc.cluster.local:4317" 23 | )] 24 | pub controller_otlp_endpoint: String, 25 | 26 | /// Allow the controller to set resource limits 27 | #[arg(env, long, default_value_t = false)] 28 | pub controller_resource_limits: bool, 29 | } 30 | -------------------------------------------------------------------------------- /crates/kriger_rest/src/routes/flags.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use crate::support::{AppError, AppJson}; 5 | use crate::AppState; 6 | use axum::response::IntoResponse; 7 | use axum::{extract::State, Json}; 8 | use kriger_common::messaging::model; 9 | use kriger_common::models; 10 | use std::sync::Arc; 11 | 12 | pub(crate) async fn submit_flags( 13 | state: State>, 14 | AppJson(request): AppJson, 15 | ) -> Result { 16 | let flags_svc = state.runtime.messaging.flags(); 17 | 18 | // FIXME: Probably parallelize this, but whatever 19 | for flag in request.flags { 20 | flags_svc 21 | .submit_flag(&model::FlagSubmission { 22 | flag, 23 | team_id: None, 24 | service: None, 25 | exploit: None, 26 | }) 27 | .await?; 28 | } 29 | 30 | Ok(Json(models::responses::AppResponse::Ok(()))) 31 | } 32 | -------------------------------------------------------------------------------- /crates/kriger_common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kriger_common" 3 | version.workspace = true 4 | edition.workspace = true 5 | description.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | 9 | [features] 10 | default = ["server", "client"] 11 | server = ["dep:prometheus-client", "dep:toml"] 12 | client = ["dep:reqwest"] 13 | 14 | [dependencies] 15 | async-nats.workspace = true 16 | async-trait.workspace = true 17 | base64.workspace = true 18 | chrono = { workspace = true, features = ["serde"] } 19 | clap.workspace = true 20 | clap_derive.workspace = true 21 | color-eyre.workspace = true 22 | dashmap.workspace = true 23 | futures.workspace = true 24 | reqwest = { workspace = true, optional = true } 25 | prometheus-client = { workspace = true, optional = true } 26 | serde.workspace = true 27 | serde_repr.workspace = true 28 | serde_json.workspace = true 29 | thiserror.workspace = true 30 | time.workspace = true 31 | tokio = { workspace = true, features = ["signal"] } 32 | tokio-util.workspace = true 33 | toml = { workspace = true, optional = true } 34 | tracing.workspace = true 35 | -------------------------------------------------------------------------------- /crates/kriger/src/cli/commands/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use crate::cli; 5 | use crate::cli::emoji; 6 | use console::style; 7 | use tokio::fs; 8 | 9 | pub(crate) mod create; 10 | pub(crate) mod deploy; 11 | pub(crate) mod exploit; 12 | pub(crate) mod submit; 13 | 14 | pub(crate) async fn read_exploit_manifest() -> color_eyre::Result { 15 | let raw = fs::read("exploit.toml").await?; 16 | let toml = std::str::from_utf8(&raw)?; 17 | 18 | Ok(toml::from_str(toml)?) 19 | } 20 | 21 | pub(crate) async fn acquire_exploit_manifest() -> Option { 22 | match read_exploit_manifest().await { 23 | Ok(manifest) => Some(manifest), 24 | Err(err) => { 25 | eprintln!( 26 | " {} {}", 27 | emoji::CROSS_MARK, 28 | style("Unable to read the exploit manifest (exploit.toml)") 29 | .red() 30 | .bold() 31 | ); 32 | eprintln!("{err}"); 33 | None 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/kriger/src/args.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use clap_derive::{Parser, Subcommand}; 5 | 6 | /// An exploit farm for attack/defense CTFs 7 | #[derive(Parser, Debug)] 8 | #[command(version, about)] 9 | #[command(propagate_version = true)] 10 | pub(crate) struct Args { 11 | #[command(subcommand)] 12 | pub command: Commands, 13 | } 14 | 15 | #[derive(Subcommand, Debug)] 16 | pub(crate) enum Commands { 17 | /// Run the server components 18 | #[cfg(feature = "server")] 19 | Server(crate::server::args::Args), 20 | /// Run the runner component 21 | #[cfg(feature = "server")] 22 | Runner(kriger_runner::args::Args), 23 | /// Deploy an exploit 24 | #[cfg(feature = "cli")] 25 | Deploy(crate::cli::args::Deploy), 26 | /// Create an exploit 27 | #[cfg(feature = "cli")] 28 | Create(crate::cli::args::Create), 29 | /// Manually submit flag(s) 30 | #[cfg(feature = "cli")] 31 | Submit(crate::cli::args::Submit), 32 | /// Exploit-related commands 33 | #[cfg(feature = "cli")] 34 | #[command(subcommand)] 35 | Exploit(crate::cli::args::ExploitCommand), 36 | } 37 | -------------------------------------------------------------------------------- /crates/kriger_mock/src/util.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | pub fn encode(buf: &mut [u8]) { 5 | for ch in buf { 6 | let x = *ch % 36; 7 | if x < 26 { 8 | *ch = b'A' + x; 9 | } else { 10 | *ch = b'0' + x; 11 | } 12 | } 13 | } 14 | 15 | pub fn decode(buf: &mut [u8]) -> Option<()> { 16 | for ch in buf { 17 | if ch.is_ascii_alphanumeric() { 18 | if ch.is_ascii_uppercase() { 19 | *ch = *ch - b'A'; 20 | } else if ch.is_ascii_digit() { 21 | *ch = *ch - b'0' + 26; 22 | } else { 23 | return None; 24 | } 25 | } else { 26 | return None; 27 | } 28 | } 29 | 30 | Some(()) 31 | } 32 | 33 | pub fn encode_u32(buf: &mut [u8], mut n: u32) { 34 | for i in 0..7 { 35 | buf[i] = (n % 36) as u8; 36 | n /= 36; 37 | } 38 | encode(buf); 39 | } 40 | 41 | pub fn decode_u32(buf: &mut [u8]) -> Option { 42 | decode(buf)?; 43 | let mut n = 0; 44 | for i in 0..7 { 45 | n *= 36; 46 | n += buf[6 - i] as u32; 47 | } 48 | Some(n) 49 | } 50 | -------------------------------------------------------------------------------- /crates/kriger_runner/src/args.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use clap_derive::Parser; 5 | 6 | #[derive(Parser, Debug)] 7 | #[command(version, about)] 8 | #[group(skip)] 9 | pub struct Args { 10 | /// The URL to the NATS/JetStream server 11 | #[arg(env, long, default_value = "nats://127.0.0.1:4222")] 12 | pub nats_url: String, 13 | 14 | /// The name of the service that the runner will be exploiting 15 | #[arg(env, long)] 16 | pub service: Option, 17 | 18 | /// The name of the exploit that the runner will be responsible for 19 | #[arg(env, long)] 20 | pub exploit: String, 21 | 22 | /// The flag format 23 | #[arg(env, long)] 24 | pub flag_format: String, 25 | 26 | /// The maximum amount of workers/executions to handle at any given time. If omitted, the default worker count will be 2*cpu. 27 | #[arg(env, long)] 28 | pub workers: Option, 29 | 30 | /// The timeout, in seconds 31 | #[arg(env, long, default_value_t = 15)] 32 | pub timeout: u64, 33 | 34 | /// The command to execute 35 | pub command: String, 36 | 37 | /// The arguments to pass to the command 38 | #[arg(trailing_var_arg = true)] 39 | pub args: Vec, 40 | } 41 | -------------------------------------------------------------------------------- /crates/kriger_submitter/src/submitter/dummy.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use super::{SubmitError, Submitter}; 5 | use async_trait::async_trait; 6 | use kriger_common::models; 7 | use rand::Rng; 8 | use std::collections::HashMap; 9 | 10 | #[derive(Clone, Debug)] 11 | pub struct DummySubmitter; 12 | 13 | #[async_trait] 14 | impl Submitter for DummySubmitter { 15 | async fn submit( 16 | &self, 17 | flags: &[&str], 18 | ) -> Result, SubmitError> { 19 | Ok(flags 20 | .iter() 21 | .map(|&flag| (flag.to_owned(), gen_submission_status())) 22 | .collect()) 23 | } 24 | } 25 | 26 | fn gen_submission_status() -> models::FlagSubmissionStatus { 27 | let mut rng = rand::rng(); 28 | let r = rng.random_range(0..=99); 29 | match r { 30 | 0..=69 => models::FlagSubmissionStatus::Ok, 31 | 70..=74 => models::FlagSubmissionStatus::Duplicate, 32 | 75..=79 => models::FlagSubmissionStatus::Own, 33 | 80..=84 => models::FlagSubmissionStatus::Old, 34 | 85..=94 => models::FlagSubmissionStatus::Invalid, 35 | 95..=99 => models::FlagSubmissionStatus::Error, 36 | _ => unreachable!(), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 4 | 5 | rust-overlay.url = "github:oxalica/rust-overlay"; 6 | rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, rust-overlay }@inputs: 10 | let 11 | inherit (nixpkgs) lib; 12 | 13 | systems = [ 14 | "x86_64-linux" 15 | "aarch64-linux" 16 | "x86_64-darwin" 17 | "aarch64-darwin" 18 | "armv7l-linux" 19 | ]; 20 | 21 | forAllSystems = f: nixpkgs.lib.genAttrs systems (system: let 22 | pkgs = import nixpkgs { 23 | inherit system; 24 | overlays = [ 25 | (import rust-overlay) 26 | ]; 27 | }; 28 | 29 | rust-bin = rust-overlay.lib.mkRustBin { } pkgs.buildPackages; 30 | toolchain = rust-bin.stable.latest.default; 31 | in f system pkgs toolchain); 32 | in { 33 | 34 | devShell = forAllSystems (system: pkgs: toolchain: pkgs.mkShell { 35 | nativeBuildInputs = with pkgs; [ 36 | toolchain 37 | mysql-client 38 | cargo-nextest 39 | ] ++ lib.optionals stdenv.isDarwin [ 40 | darwin.apple_sdk.frameworks.Security 41 | ]; 42 | 43 | RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library"; 44 | }); 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1728241625, 6 | "narHash": "sha256-yumd4fBc/hi8a9QgA9IT8vlQuLZ2oqhkJXHPKxH/tRw=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "c31898adf5a8ed202ce5bea9f347b1c6871f32d1", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "rust-overlay": "rust-overlay" 23 | } 24 | }, 25 | "rust-overlay": { 26 | "inputs": { 27 | "nixpkgs": [ 28 | "nixpkgs" 29 | ] 30 | }, 31 | "locked": { 32 | "lastModified": 1728354625, 33 | "narHash": "sha256-r+Sa1NRRT7LXKzCaVaq75l1GdZcegODtF06uaxVVVbI=", 34 | "owner": "oxalica", 35 | "repo": "rust-overlay", 36 | "rev": "d216ade5a0091ce60076bf1f8bc816433a1fc5da", 37 | "type": "github" 38 | }, 39 | "original": { 40 | "owner": "oxalica", 41 | "repo": "rust-overlay", 42 | "type": "github" 43 | } 44 | } 45 | }, 46 | "root": "root", 47 | "version": 7 48 | } 49 | -------------------------------------------------------------------------------- /crates/kriger_common/src/utils/data.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use dashmap::DashMap; 5 | use std::borrow::Borrow; 6 | use std::collections::HashMap; 7 | use std::hash::Hash; 8 | use std::sync::Arc; 9 | 10 | pub(crate) trait MapWriter { 11 | fn insert(&mut self, k: K, v: V) -> Option 12 | where 13 | K: Hash + Eq; 14 | 15 | fn remove(&mut self, k: &Q) -> Option 16 | where 17 | K: Borrow, 18 | Q: Hash + Eq; 19 | } 20 | 21 | impl MapWriter for HashMap { 22 | fn insert(&mut self, k: K, v: V) -> Option { 23 | HashMap::::insert(self, k, v) 24 | } 25 | 26 | fn remove(&mut self, k: &Q) -> Option 27 | where 28 | K: Borrow, 29 | Q: Hash + Eq, 30 | { 31 | HashMap::::remove(self, k) 32 | } 33 | } 34 | 35 | impl MapWriter for Arc> { 36 | fn insert(&mut self, k: K, v: V) -> Option { 37 | DashMap::::insert(self, k, v) 38 | } 39 | 40 | fn remove(&mut self, k: &Q) -> Option 41 | where 42 | K: Borrow, 43 | Q: Hash + Eq, 44 | { 45 | DashMap::::remove(self, k).map(|(_, v)| v) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/components/Switch.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import * as React from "react"; 5 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 6 | import cn from "../utils/cn.ts"; 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )); 27 | Switch.displayName = SwitchPrimitives.Root.displayName; 28 | 29 | export { Switch }; 30 | -------------------------------------------------------------------------------- /crates/kriger/src/cli/commands/submit.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use crate::cli::{args, read_cli_config}; 5 | use color_eyre::eyre; 6 | use color_eyre::eyre::{bail, Context}; 7 | use console::style; 8 | use kriger_common::client::KrigerClient; 9 | use kriger_common::models; 10 | use regex::Regex; 11 | 12 | pub(crate) async fn main(args: args::Submit) -> eyre::Result<()> { 13 | let cli_config = read_cli_config().await?; 14 | let client = KrigerClient::new(cli_config.client.rest_url); 15 | 16 | let server_config = match client.get_server_config().await? { 17 | models::responses::AppResponse::Ok(team_map) => team_map, 18 | models::responses::AppResponse::Error { message } => bail!(message), 19 | }; 20 | let flag_format = 21 | Regex::new(&server_config.competition.flag_format).context("invalid regex")?; 22 | 23 | let flags: Vec = flag_format 24 | .find_iter(&args.input) 25 | .map(|flag| flag.as_str().to_string()) 26 | .collect(); 27 | let flag_count = flags.len(); 28 | 29 | client 30 | .submit_flags(flags) 31 | .await 32 | .context("unable to submit flags")?; 33 | 34 | eprintln!( 35 | "{}", 36 | style(format!("Queued {} flags for submission", flag_count)).green() 37 | ); 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/routes/__root.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import { createRootRoute, Outlet } from "@tanstack/react-router"; 5 | import NavigationBar from "../components/NavigationBar"; 6 | import { 7 | useWebSocketProvider, 8 | useConfigProvider, 9 | useCompetition, 10 | useExploits, 11 | } from "../utils/hooks"; 12 | import { CONFIG } from "../utils/constants"; 13 | import { Toaster } from "sonner"; 14 | 15 | export const RootComponent = () => { 16 | useWebSocketProvider(CONFIG.webSocketUrl.toString()); 17 | useConfigProvider(); 18 | useCompetition(); 19 | useExploits(); 20 | 21 | return ( 22 |
23 | 24 |
25 | 26 |
27 | 37 |
38 | ); 39 | }; 40 | 41 | export const Route = createRootRoute({ 42 | component: RootComponent, 43 | }); 44 | -------------------------------------------------------------------------------- /frontend/config/nginx.conf: -------------------------------------------------------------------------------- 1 | # https://hg.nginx.org/pkg-oss/file/fd9484abcae4/alpine/nginx.default.conf 2 | 3 | server { 4 | listen 8080; 5 | server_name localhost; 6 | 7 | #access_log /var/log/nginx/host.access.log main; 8 | 9 | location / { 10 | root /usr/share/nginx/html; 11 | index index.html index.htm; 12 | try_files $uri /index.html; 13 | } 14 | 15 | #error_page 404 /404.html; 16 | 17 | # redirect server error pages to the static page /50x.html 18 | # 19 | error_page 500 502 503 504 /50x.html; 20 | location = /50x.html { 21 | root /usr/share/nginx/html; 22 | } 23 | 24 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 25 | # 26 | #location ~ \.php$ { 27 | # proxy_pass http://127.0.0.1; 28 | #} 29 | 30 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 31 | # 32 | #location ~ \.php$ { 33 | # root html; 34 | # fastcgi_pass 127.0.0.1:9000; 35 | # fastcgi_index index.php; 36 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 37 | # include fastcgi_params; 38 | #} 39 | 40 | # deny access to .htaccess files, if Apache's document root 41 | # concurs with nginx's one 42 | # 43 | #location ~ /\.ht { 44 | # deny all; 45 | #} 46 | } 47 | 48 | -------------------------------------------------------------------------------- /crates/kriger_controller/src/metrics.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use prometheus_client::encoding::EncodeLabelSet; 5 | use prometheus_client::metrics::counter::Counter; 6 | use prometheus_client::metrics::family::Family; 7 | use prometheus_client::registry::Registry; 8 | 9 | #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] 10 | pub(crate) struct ExploitLabels { 11 | pub exploit: String, 12 | } 13 | 14 | #[derive(Default)] 15 | pub(crate) struct ControllerMetrics { 16 | pub requests: Family, 17 | pub complete: Family, 18 | pub error: Family, 19 | } 20 | 21 | impl ControllerMetrics { 22 | pub(crate) fn register(&self, registry: &mut Registry) { 23 | registry.register( 24 | "kriger_controller_reconciliation_requests", 25 | "The number of reconciliation requests", 26 | self.requests.clone(), 27 | ); 28 | registry.register( 29 | "kriger_controller_reconciliation_complete", 30 | "The number of completed reconciliations", 31 | self.complete.clone(), 32 | ); 33 | registry.register( 34 | "kriger_controller_reconciliation_error", 35 | "The number of errored reconciliations", 36 | self.error.clone(), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/templates.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ main ] 4 | paths: 5 | - ".github/workflows/templates.yml" 6 | - "data/templates/**" 7 | workflow_dispatch: 8 | 9 | name: Package templates 10 | 11 | jobs: 12 | package-templates: 13 | name: Package exploit template ${{ matrix.name }} 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - name: python 23 | - name: python-slim 24 | steps: 25 | - name: Check out the repo 26 | uses: actions/checkout@v4 27 | 28 | - name: Set up ORAS 29 | uses: oras-project/setup-oras@ca28077386065e263c03428f4ae0c09024817c93 # v1.2.0 30 | 31 | - name: Log in to the OCI registry 32 | run: | 33 | oras login -u "$REGISTRY_USER" --password-stdin ghcr.io <<<"$REGISTRY_PASS" 34 | env: 35 | REGISTRY_USER: ${{ github.actor }} 36 | REGISTRY_PASS: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Archive the template 39 | run: | 40 | tar czfh template.tar.gz -C "data/templates/${{ matrix.name }}" . 41 | 42 | - name: Push the template 43 | run: | 44 | oras push ghcr.io/cyberlandslaget/kriger-exploit-templates:${{ matrix.name }} template.tar.gz:application/vnd.kriger.exploit.template.v1.tar+gzip 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/*"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | version = "0.0.1" 7 | edition = "2021" 8 | description = "An exploit farm for attack/defense CTFs." 9 | repository = "https://github.com/Cyberlandslaget/kriger" 10 | license = "AGPL-3.0-only" 11 | 12 | [workspace.dependencies] 13 | async-nats = "0.40.0" 14 | async-trait = "0.1.81" 15 | axum = "0.8.3" 16 | base64 = "0.22.1" 17 | chrono = "0.4.38" 18 | clap = { version = "4.5.8", features = ["env", "derive"] } 19 | clap_derive = "4.5.8" 20 | color-eyre = "0.6.3" 21 | dashmap = "6.0.1" 22 | flume = { version = "0.11.0", features = ["async", "select"] } 23 | futures = "0.3.30" 24 | hyper = "1.4.1" 25 | hyper-util = "0.1.6" 26 | lazy_static = "1.5.0" 27 | opentelemetry = "0.25.0" 28 | opentelemetry-semantic-conventions = "0.25.0" 29 | opentelemetry_sdk = { version = "0.25.0", features = ["rt-tokio"] } 30 | opentelemetry-otlp = "0.25.0" 31 | prometheus-client = "0.23.1" 32 | rand = "0.9.1" 33 | reqwest = { version = "0.12.15", features = [ 34 | "rustls-tls", 35 | "charset", 36 | "http2", 37 | "json", 38 | ], default-features = false } 39 | regex = "1.10.5" 40 | serde = { version = "1.0.203", features = ["derive"] } 41 | serde_repr = "0.1.19" 42 | serde_json = "1.0.118" 43 | thiserror = "2.0.12" 44 | time = "0.3.36" 45 | tokio = "1.38.0" 46 | tokio-stream = "0.1.15" 47 | tokio-util = "0.7.11" 48 | toml = "0.8.19" 49 | tower-http = "0.6.2" 50 | tracing = "0.1.40" 51 | tracing-subscriber = "0.3.18" 52 | -------------------------------------------------------------------------------- /crates/kriger_runner/src/runner/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | pub mod simple; 5 | 6 | use std::future::Future; 7 | use std::time::Duration; 8 | 9 | #[derive(thiserror::Error, Debug)] 10 | pub enum RunnerError { 11 | #[error("serde json error: {0}")] 12 | SerdeJsonError(#[from] serde_json::Error), 13 | #[error("io error: {0}")] 14 | IoError(#[from] std::io::Error), 15 | #[error("send error: {0}")] 16 | FlumeSendError(#[from] flume::SendError), 17 | #[error("stdout unavailable")] 18 | StdoutUnavailable, 19 | #[error("stderr unavailable")] 20 | StderrUnavailable, 21 | #[error("execution timed out")] 22 | ExecutionTimeout, 23 | } 24 | 25 | pub trait Runner { 26 | fn run + Send + Sync>( 27 | &self, 28 | ip_address: S, 29 | flag_hint: &Option, 30 | ) -> impl Future> + Send; 31 | } 32 | 33 | pub trait RunnerExecution { 34 | fn complete(self) -> impl Future + Send; 35 | 36 | fn events(&self) -> flume::Receiver; 37 | } 38 | 39 | pub enum RunnerEvent { 40 | Stdout(String), 41 | Stderr(String), 42 | FlagMatch(String), 43 | } 44 | 45 | #[derive(Debug)] 46 | pub struct RunnerExecutionResult { 47 | pub time: Duration, 48 | pub exit_code: Option, 49 | pub error: Option, 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/components/HoverCard.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import * as React from "react"; 5 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; 6 | import cn from "../utils/cn"; 7 | 8 | const HoverCard = HoverCardPrimitive.Root; 9 | 10 | const HoverCardTrigger = HoverCardPrimitive.Trigger; 11 | 12 | const HoverCardContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; 30 | 31 | export { HoverCard, HoverCardTrigger, HoverCardContent }; 32 | -------------------------------------------------------------------------------- /frontend/src/utils/enums.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | export enum FlagCode { 5 | // Internal status code used by the frontend to represent "pending" flags 6 | Pending = -1, 7 | 8 | Ok = 1, // ACCEPTED: flag claimed 9 | Duplicate = 2, // DENIED: flag already claimed 10 | Own = 3, // DENIED: flag is your own 11 | Nop = 4, // DENIED: flag from nop team 12 | Old = 5, // DENIED: flag too old 13 | Invalid = 6, // DENIED: invalid flag 14 | 15 | /** 16 | * The server explicitly requests the flag to be resubmitted. 17 | * This can be due to the fact that the flag is not yet valid. 18 | * Submitters should retry this status. 19 | */ 20 | Resubmit = 7, 21 | 22 | /** 23 | * Server refused flag. Pre- or post-competition. 24 | * Submitters should retry this status. 25 | */ 26 | Error = 8, 27 | 28 | /** 29 | * The flag that was placed by the checker is stale and is invalid. 30 | * Submitters should not retry this status. 31 | */ 32 | Stale = 9, 33 | 34 | /** 35 | * Unknown response. Submitters should definitely retry this status. 36 | */ 37 | Unknown = 200, 38 | } 39 | export enum ServiceStatus { 40 | OK = 0, 41 | DOWN = 1, 42 | SYSTEM_ERROR = -1, 43 | } 44 | export enum ExecutionResultStatusCode { 45 | Success = 0, 46 | Timeout = 1, 47 | Error = 2, 48 | } 49 | 50 | export const flagCodeLookup = new Map( 51 | Object.entries(FlagCode).map(([k, v]) => [v, k]), 52 | ); 53 | -------------------------------------------------------------------------------- /crates/kriger_fetcher/src/metrics.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use prometheus_client::metrics::counter::Counter; 5 | use prometheus_client::metrics::histogram::{exponential_buckets, Histogram}; 6 | use prometheus_client::registry::Registry; 7 | 8 | pub(crate) struct FetcherMetrics { 9 | pub start: Counter, 10 | pub complete: Counter, 11 | pub error: Counter, 12 | pub duration: Histogram, 13 | } 14 | 15 | impl FetcherMetrics { 16 | pub(crate) fn register(&self, registry: &mut Registry) { 17 | registry.register( 18 | "kriger_fetcher_start", 19 | "The number of fetch start", 20 | self.start.clone(), 21 | ); 22 | registry.register( 23 | "kriger_fetcher_complete", 24 | "The number of fetch complete", 25 | self.complete.clone(), 26 | ); 27 | registry.register( 28 | "kriger_fetcher_error", 29 | "The number of fetch errors", 30 | self.error.clone(), 31 | ); 32 | registry.register( 33 | "kriger_fetcher_duration_seconds", 34 | "A histogram for the amount of time taken to fetch", 35 | self.duration.clone(), 36 | ); 37 | } 38 | } 39 | 40 | impl Default for FetcherMetrics { 41 | fn default() -> Self { 42 | Self { 43 | start: Default::default(), 44 | complete: Default::default(), 45 | error: Default::default(), 46 | duration: Histogram::new(exponential_buckets(0.001, 2.0, 18)), 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/kriger_rest/src/routes/competition.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use crate::support::{AppError, AppQuery}; 5 | use crate::AppState; 6 | use axum::response::IntoResponse; 7 | use axum::{extract::State, Json}; 8 | use kriger_common::messaging::Bucket; 9 | use kriger_common::models; 10 | use std::sync::Arc; 11 | 12 | pub(crate) async fn get_services( 13 | state: State>, 14 | ) -> Result { 15 | let services_bucket = state.runtime.messaging.services(); 16 | let services: Vec = services_bucket.list(None).await?.into_values().collect(); 17 | 18 | Ok(Json(models::responses::AppResponse::Ok(services))) 19 | } 20 | 21 | pub(crate) async fn get_teams(state: State>) -> Result { 22 | let teams_bucket = state.runtime.messaging.teams(); 23 | 24 | // We return a map of team network id to the team data 25 | let teams = teams_bucket.list(None).await?; 26 | 27 | Ok(Json(models::responses::AppResponse::Ok(teams))) 28 | } 29 | 30 | pub(crate) async fn get_flag_hints( 31 | state: State>, 32 | query: AppQuery, 33 | ) -> Result { 34 | let data_svc = state.runtime.messaging.data(); 35 | 36 | let flag_hints: Vec = data_svc 37 | .get_flag_hints(Some(&query.service)) 38 | .await? 39 | .into_iter() 40 | .map(|hint| models::FlagHint { 41 | team_id: hint.payload.team_id, 42 | service: hint.payload.service, 43 | hint: hint.payload.hint, 44 | }) 45 | .collect(); 46 | 47 | Ok(Json(models::responses::AppResponse::Ok(flag_hints))) 48 | } 49 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kriger_frontend", 3 | "description": "An exploit farm for attack/defense CTFs.", 4 | "license": "AGPL-3.0-only", 5 | "homepage": "https://github.com/Cyberlandslaget/kriger#readme", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Cyberlandslaget/kriger.git" 9 | }, 10 | "private": true, 11 | "version": "0.0.0", 12 | "type": "module", 13 | "scripts": { 14 | "dev": "vite", 15 | "build": "vite build", 16 | "lint": "biome lint", 17 | "preview": "vite preview" 18 | }, 19 | "dependencies": { 20 | "@fontsource/b612-mono": "^5.0.19", 21 | "@monaco-editor/react": "^4.6.0", 22 | "@radix-ui/react-dialog": "^1.1.1", 23 | "@radix-ui/react-hover-card": "^1.1.1", 24 | "@radix-ui/react-switch": "^1.1.0", 25 | "@tanstack/react-router": "^1.45.0", 26 | "clsx": "^2.1.1", 27 | "jotai": "^2.9.0", 28 | "lucide-react": "^0.427.0", 29 | "react": "^18.3.1", 30 | "react-dom": "^18.3.1", 31 | "react-virtualized-auto-sizer": "^1.0.24", 32 | "react-window": "^1.8.10", 33 | "shiki": "^1.10.3", 34 | "sonner": "^1.5.0", 35 | "swr": "^2.2.5", 36 | "tailwind-merge": "^2.5.2", 37 | "tailwindcss-animate": "^1.0.7", 38 | "usehooks-ts": "^3.1.0" 39 | }, 40 | "devDependencies": { 41 | "@biomejs/biome": "1.8.3", 42 | "@shikijs/monaco": "^1.10.3", 43 | "@tanstack/router-devtools": "^1.45.1", 44 | "@tanstack/router-plugin": "^1.45.0", 45 | "@types/react": "^18.3.3", 46 | "@types/react-dom": "^18.3.0", 47 | "@types/react-window": "^1.8.8", 48 | "@vitejs/plugin-react-swc": "^3.7.0", 49 | "autoprefixer": "^10.4.19", 50 | "postcss": "^8.4.39", 51 | "tailwindcss": "^3.4.4", 52 | "typescript": "^5.2.2", 53 | "vite": "^5.3.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/components/StatusCellCard.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import { useInterval } from "usehooks-ts"; 5 | import type { TeamServiceFlags } from "../utils/types"; 6 | import { Fragment, useState } from "react"; 7 | import { FlagCode, flagCodeLookup } from "../utils/enums"; 8 | 9 | type StatusCellCardProps = { 10 | flags: TeamServiceFlags; 11 | teamId: string; 12 | teamName: string | null; 13 | serviceName: string; 14 | }; 15 | 16 | export const StatusCellCard = ({ 17 | flags, 18 | teamId, 19 | teamName, 20 | serviceName, 21 | }: StatusCellCardProps) => { 22 | const [currentTime, setCurrentTime] = useState(Date.now()); 23 | 24 | useInterval(() => { 25 | setCurrentTime(Date.now()); 26 | }, 1000); 27 | 28 | return ( 29 |
30 |
31 |
32 | [{teamId}] {teamName} 33 |
34 |
{serviceName}
35 |
36 |
37 | {Object.keys(flags).length === 0 && ( 38 | No flags received 39 | )} 40 | {Object.entries(flags) 41 | .reverse() 42 | .map(([flag, status]) => ( 43 | 44 | 45 | [{flagCodeLookup.get(status.status ?? FlagCode.Unknown)}] 46 | 47 | {flag}{" "} 48 | 49 | {Math.floor((currentTime - status.published) / 1000)}s 50 | 51 | 52 | ))} 53 |
54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /frontend/src/components/StatsCards.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import { useAtomValue } from "jotai"; 5 | import { 6 | executionStatusAggregateAtom, 7 | exploitsAtom, 8 | flagStatusAggregateAtom, 9 | } from "../utils/atoms"; 10 | import { FlagCode } from "../utils/enums"; 11 | 12 | export const StatsCards = () => { 13 | const flagStatusAggregate = useAtomValue(flagStatusAggregateAtom); 14 | const executionStatusAggregate = useAtomValue(executionStatusAggregateAtom); 15 | const exploits = useAtomValue(exploitsAtom); 16 | 17 | const statCards = [ 18 | { 19 | title: "Pending executions", 20 | value: executionStatusAggregate.pendingCount, 21 | }, 22 | { title: "Exploits", value: exploits?.length }, 23 | { title: "Flags received", value: flagStatusAggregate.count }, 24 | { 25 | title: "Accepted flags", 26 | value: 27 | (flagStatusAggregate.statusMap.get(FlagCode.Ok) ?? 0) + 28 | (flagStatusAggregate.statusMap.get(FlagCode.Duplicate) ?? 0), 29 | }, 30 | { 31 | title: "Rejected flags", 32 | value: flagStatusAggregate.statusMap.get(FlagCode.Invalid) ?? 0, 33 | }, 34 | { 35 | title: "Pending flags", 36 | value: flagStatusAggregate.statusMap.get(FlagCode.Pending) ?? 0, 37 | }, 38 | ]; 39 | return ( 40 |
41 | {statCards.map((box) => ( 42 |
46 | {box.title} 47 | {box.value} 48 |
49 | ))} 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /crates/kriger_rest/src/routes/exploits.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use crate::support::{AppError, AppJson}; 5 | use crate::AppState; 6 | use axum::extract::Path; 7 | use axum::response::IntoResponse; 8 | use axum::{extract::State, Json}; 9 | use kriger_common::messaging::Bucket; 10 | use kriger_common::{messaging, models}; 11 | use std::sync::Arc; 12 | 13 | pub(crate) async fn get_exploits( 14 | state: State>, 15 | ) -> Result { 16 | let exploits_bucket = state.runtime.messaging.exploits(); 17 | let exploits: Vec = exploits_bucket.list(None).await?.into_values().collect(); 18 | 19 | Ok(Json(models::responses::AppResponse::Ok(exploits))) 20 | } 21 | 22 | pub(crate) async fn update_exploit( 23 | state: State>, 24 | Path(name): Path, 25 | AppJson(exploit): AppJson, 26 | ) -> Result { 27 | if name != exploit.manifest.name { 28 | return Err(AppError::BadInput( 29 | "the exploit manifest does not match the provided exploit name", 30 | )); 31 | } 32 | 33 | // TODO: Validate the exploit manifest thoroughly before completing the request 34 | let exploits_bucket = state.runtime.messaging.exploits(); 35 | 36 | exploits_bucket 37 | .put(&exploit.manifest.name, &exploit) 38 | .await?; 39 | 40 | Ok(Json(models::responses::AppResponse::Ok(()))) 41 | } 42 | 43 | pub(crate) async fn execute_exploit( 44 | state: State>, 45 | Path(name): Path, 46 | ) -> Result { 47 | let scheduling_svc = state.runtime.messaging.scheduling(); 48 | 49 | scheduling_svc 50 | .publish_request(&messaging::model::SchedulingRequest { exploit: name }) 51 | .await?; 52 | 53 | Ok(Json(models::responses::AppResponse::Ok(()))) 54 | } 55 | -------------------------------------------------------------------------------- /crates/kriger/src/server/metrics.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use crate::server::args::OpenMetricsConfig; 5 | use axum::http::{header, StatusCode}; 6 | use axum::response::IntoResponse; 7 | use axum::routing::get; 8 | use axum::Router; 9 | use color_eyre::eyre; 10 | use color_eyre::eyre::Context; 11 | use kriger_common::server::runtime::AppRuntime; 12 | use std::net::SocketAddr; 13 | use std::ops::Deref; 14 | use tokio::net::TcpListener; 15 | use tracing::info; 16 | 17 | pub(crate) async fn run_metrics_server( 18 | runtime: AppRuntime, 19 | args: OpenMetricsConfig, 20 | ) -> eyre::Result<()> { 21 | let cancellation_token = runtime.cancellation_token.clone(); 22 | let app = Router::new() 23 | .route("/metrics", get(metrics_handler)) 24 | .with_state(runtime); 25 | 26 | let addr: SocketAddr = args 27 | .openmetrics_listen 28 | .parse() 29 | .context("unable to parse the listening address")?; 30 | let listener = TcpListener::bind(addr) 31 | .await 32 | .context("unable to start the rest server, is the port taken?")?; 33 | 34 | info!("listening on {addr:?}"); 35 | axum::serve(listener, app) 36 | .with_graceful_shutdown(async move { 37 | cancellation_token.cancelled().await; 38 | }) 39 | .await 40 | .context("openmetrics server error")?; 41 | 42 | Ok(()) 43 | } 44 | 45 | async fn metrics_handler(runtime: axum::extract::State) -> impl IntoResponse { 46 | let mut buffer = String::new(); 47 | prometheus_client::encoding::text::encode( 48 | &mut buffer, 49 | runtime.metrics_registry.read().await.deref(), 50 | ) 51 | .unwrap(); 52 | ( 53 | StatusCode::OK, 54 | [( 55 | header::CONTENT_TYPE, 56 | "application/openmetrics-text; version=1.0.0; charset=utf-8", 57 | )], 58 | buffer, 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /docs/adrs/002-submitters.md: -------------------------------------------------------------------------------- 1 | # Submitters 2 | 3 | ## Status 4 | 5 | Draft 6 | 7 | ## Context 8 | 9 | There are varying standards and protocols for how flags should be submitted during an attack/defense CTF contest. 10 | 11 | Examples: 12 | 13 | - **CINI (ECSC 2024)**'s flag submission service is an HTTP service that accepts a list of flags. At the time of 14 | writing, CINI's submitter limits the request body to 100 kB and has a rate limit of 15 requests per minute. 15 | - **FAUST CTF**'s flag submission service accepts flag submissions via a raw TCP stream outlined in 16 | the [CTF Gameserver documentation](https://ctf-gameserver.org/submission/). The client may submit an arbitrary number 17 | of flags during a single connection. 18 | 19 | ## Decision 20 | 21 | Submitter implementations should have the flexibility required while maintaining an efficient system. 22 | 23 | ## Consequences 24 | 25 | The submitter trait should accept a vector of flags which are bound to be submitted in bulk. Regardless of whether the 26 | flags can be submitted in a stream, it is most likely more efficient to submit in bulk. By submitting the flags in bulk, 27 | the I/O round trip time is drastically reduced as the submitter does not have to wait for each response before 28 | submitting the next flag. Submitters that support "streaming" should submit in bulk with a short interval, e.g. 1s. See 29 | the figures below. 30 | 31 | **Fig. 1**: Streaming flag submission. 32 | 33 | ```mermaid 34 | gantt 35 | dateFormat ss 36 | axisFormat %S s 37 | tickInterval 1second 38 | Submit AAA...: 00, 1s 39 | Submit BBB...: 01, 2s 40 | Submit CCC...: 03, 1s 41 | Submit DDD...: 04, 1s 42 | ``` 43 | 44 | **Fig. 2**: Bulk flag submission. 45 | 46 | ```mermaid 47 | gantt 48 | dateFormat ss 49 | axisFormat %S s 50 | tickInterval 1second 51 | Submit AAA...: 00, 1s 52 | Submit BBB...: 00, 2s 53 | Submit CCC...: 02, 1s 54 | Submit DDD...: 02, 1s 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/adrs/001-fetchers.md: -------------------------------------------------------------------------------- 1 | # Fetchers 2 | 3 | ## Status 4 | 5 | Draft 6 | 7 | ## Context 8 | 9 | There are varying requirements for various attack/defense CTFs regarding the data needed to perform a successful attack 10 | against the services running on the target vulnbox. 11 | 12 | Examples: 13 | 14 | - **CINI (ECSC 2024)** provides a "flag ids" endpoint which must be queried by at least one of the following: service, 15 | team id. This means that the fetcher for CINI must fetch the list of teams and/or the list of services before querying 16 | the "flag ids". 17 | - **FAUST CTF** provides a "flag ids" endpoint which returns a list of teams, services, and the "flag ids" associated 18 | with them respectively. An example of a "flag id" is a username of a user that's registered on a service, which is 19 | required or highly recommended to use for an exploit execution. 20 | 21 | Furthermore, there are other considerations to make when fetching data from various attack/defense CTFs. The following 22 | should be considered (non-exhaustive): 23 | 24 | - A "team id" may refer to a persistent ID that's associated with a team. However, the association between the ID and 25 | the team identity may not be known. A "team id" may only be used to construct the target's IP address based on a 26 | format. Some attack/defense CTFs choose to anonymize this to avoid targeting. 27 | - Each team may have different IP addresses for each service. 28 | - Different kinds of data may have to be fetched separately. 29 | - Service IP addresses may not follow a specific format that's dependent on the team id. 30 | 31 | ## Decision 32 | 33 | Fetcher implementations should have the flexibility required while maintaining an efficient system. 34 | 35 | ## Consequences 36 | 37 | The fetcher trait is generalized and does not enforce a strongly opinionated pattern for the data that it must return. 38 | The fetcher is executed on an interval and is responsible for persisting the data that it retrieved. 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # This Docker Compose file is used for development ONLY 2 | 3 | services: 4 | nats: 5 | image: nats:2 6 | restart: always 7 | command: [ "-js" ] 8 | ports: 9 | - "127.0.0.1:4222:4222" 10 | networks: 11 | default: 12 | # Assign a static IP address for the Kubernetes "cluster" to access it 13 | ipv4_address: 172.30.100.10 14 | nats-init: 15 | image: natsio/nats-box 16 | restart: on-failure 17 | depends_on: 18 | - nats 19 | entrypoint: [ "/docker-entrypoint.sh" ] 20 | environment: 21 | NATS_URL: "nats://nats:4222" 22 | volumes: 23 | - "./data/test/nats-seed.sh:/docker-entrypoint.sh:roZ" 24 | k3s: 25 | # https://hub.docker.com/r/rancher/k3s/tags 26 | image: rancher/k3s:v1.30.2-k3s1 27 | restart: always 28 | command: [ "server", "--disable=traefik,servicelb,local-storage" ] 29 | privileged: true 30 | tmpfs: 31 | - /run 32 | - /var/run 33 | ulimits: 34 | nproc: 65535 35 | nofile: 36 | soft: 65535 37 | hard: 65535 38 | environment: 39 | K3S_KUBECONFIG_OUTPUT: "/output/kubeconfig" 40 | K3S_KUBECONFIG_MODE: "666" 41 | volumes: 42 | - "./run/k3s/:/output/:Z" 43 | - "./data/test/k8s_resources.yaml:/var/lib/rancher/k3s/server/manifests/kriger.yaml:roZ" 44 | - "./data/test/k8s_registries.yaml:/etc/rancher/k3s/registries.yaml:roZ" 45 | ports: 46 | - "127.0.0.1:6443:6443" 47 | registry: 48 | image: registry:2 49 | restart: always 50 | ports: 51 | - "127.0.0.1:5000:5000" 52 | volumes: 53 | - registry:/var/lib/registry 54 | jeager: 55 | image: jaegertracing/all-in-one:latest 56 | restart: always 57 | ports: 58 | - 127.0.0.1:4317:4317 59 | - 127.0.0.1:16686:16686 60 | 61 | volumes: 62 | registry: 63 | 64 | networks: 65 | default: 66 | driver: bridge 67 | ipam: 68 | config: 69 | - subnet: 172.30.100.0/24 70 | gateway: 172.30.100.1 71 | -------------------------------------------------------------------------------- /crates/kriger/src/cli/args.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use clap_derive::{Parser, Subcommand}; 5 | 6 | /// Deploy an exploit to the attack farm. 7 | #[derive(Parser, Debug)] 8 | #[command(version, about)] 9 | pub(crate) struct Deploy { 10 | /// Do not deploy the exploit. This will only build the exploit and push it to the registry. 11 | #[arg(long)] 12 | pub(crate) no_deploy: bool, 13 | 14 | /// Do not immediately execute the exploit. This will not immediately execute the exploit after deploying. 15 | #[arg(long)] 16 | pub(crate) no_execute: bool, 17 | } 18 | 19 | /// Create a new exploit based on a template. 20 | #[derive(Parser, Debug)] 21 | #[command(version, about)] 22 | pub(crate) struct Create { 23 | /// The exploit template repository 24 | #[arg(long, default_value = "cyberlandslaget/kriger-exploit-templates")] 25 | pub(crate) templates_repository: String, 26 | 27 | /// The service name that the exploit should target 28 | #[arg(long)] 29 | pub(crate) service: Option, 30 | 31 | /// The exploit's name 32 | pub(crate) name: Option, 33 | } 34 | 35 | /// Manually submit a flag 36 | #[derive(Parser, Debug)] 37 | #[command(version, about)] 38 | pub(crate) struct Submit { 39 | /// The input containing the flag(s) to submit 40 | pub(crate) input: String, 41 | } 42 | 43 | #[derive(Subcommand, Debug)] 44 | #[command(version, about)] 45 | pub(crate) enum ExploitCommand { 46 | /// Retrieve flag hints 47 | Hints(ExploitHints), 48 | /// Interactive exploit development 49 | #[cfg(feature = "runner")] 50 | Dev(ExploitDev), 51 | } 52 | 53 | #[derive(Parser, Debug)] 54 | #[command(version, about)] 55 | pub(crate) struct ExploitHints {} 56 | 57 | #[derive(Parser, Debug)] 58 | #[command(version, about)] 59 | #[cfg(feature = "runner")] 60 | pub(crate) struct ExploitDev { 61 | /// The team ID to run the exploit against. Defaults to nop. 62 | pub team_id: Option, 63 | } 64 | -------------------------------------------------------------------------------- /crates/kriger/src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use color_eyre::eyre::{self, Context, ContextCompat}; 5 | use futures::Future; 6 | use indicatif::ProgressBar; 7 | use models::CliConfig; 8 | use std::{path::PathBuf, time::Duration}; 9 | 10 | pub(crate) mod args; 11 | pub(crate) mod commands; 12 | mod emoji; 13 | mod models; 14 | 15 | #[cfg(not(debug_assertions))] 16 | const CONFIG_FILE_NAME: &str = "cli.toml"; 17 | #[cfg(debug_assertions)] 18 | const CONFIG_FILE_NAME: &str = "cli.dev.toml"; 19 | 20 | fn log(p: &ProgressBar, message: String) { 21 | p.suspend(|| { 22 | println!(" {message}"); 23 | }); 24 | } 25 | 26 | fn format_duration_secs(duration: &Duration) -> String { 27 | let secs_fractional = duration.as_millis() as f32 / 1000f32; 28 | format!("{secs_fractional:.2}s") 29 | } 30 | 31 | /// Displays a spinner in the console while the future is running. The caller is responsible for 32 | /// displaying a message signifying the completion. 33 | async fn with_spinner(message: &'static str, f: F) -> Result 34 | where 35 | F: FnOnce() -> Fut, 36 | Fut: Future>, 37 | { 38 | let pb = ProgressBar::new_spinner(); 39 | pb.enable_steady_tick(Duration::from_millis(130)); 40 | pb.set_message(message); 41 | 42 | let res = f().await; 43 | pb.finish_and_clear(); 44 | 45 | res 46 | } 47 | 48 | fn get_config_dir() -> Option { 49 | dirs::config_dir().map(|path| path.join("kriger")) 50 | } 51 | 52 | fn get_config_file() -> Option { 53 | get_config_dir().map(|path| path.join(CONFIG_FILE_NAME)) 54 | } 55 | 56 | async fn read_cli_config() -> eyre::Result { 57 | let path = get_config_file().context("unable to locate the config directory")?; 58 | let content = tokio::fs::read_to_string(path) 59 | .await 60 | .context("unable to read the config file")?; 61 | let config: CliConfig = toml::from_str(&content).context("unable to parse the config file")?; 62 | 63 | Ok(config) 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/services/rest.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import useSWR from "swr"; 5 | import { CONFIG } from "../utils/constants"; 6 | import type { 7 | APIErrorResponse, 8 | APISuccessResponse, 9 | Exploit, 10 | ServerConfig, 11 | Service, 12 | Team, 13 | } from "./models"; 14 | 15 | const fetcher = async (path: string): Promise> => { 16 | const res = await fetch(CONFIG.restUrl + path); 17 | if (!res.ok) { 18 | try { 19 | throw await res.json(); 20 | } catch (error) { 21 | throw { 22 | error: { 23 | message: `Parsing error: ${error}`, 24 | }, 25 | } as APIErrorResponse; 26 | } 27 | } 28 | 29 | try { 30 | return await res.json(); 31 | } catch (error) { 32 | throw { 33 | error: { 34 | message: `Parsing error: ${error}`, 35 | }, 36 | } as APIErrorResponse; 37 | } 38 | }; 39 | 40 | export const useServerConfig = () => 41 | useSWR, APIErrorResponse>( 42 | "/config/server", 43 | fetcher, 44 | ); 45 | 46 | export const useCompetitionServices = () => 47 | useSWR, APIErrorResponse>( 48 | "/competition/services", 49 | fetcher, 50 | ); 51 | 52 | export const useCompetitionTeams = () => 53 | useSWR>, APIErrorResponse>( 54 | "/competition/teams", 55 | fetcher, 56 | ); 57 | 58 | export const useExploitsData = () => 59 | useSWR, APIErrorResponse>( 60 | "/exploits", 61 | fetcher, 62 | ); 63 | 64 | export const executeExploit = async (exploit: Exploit): Promise => { 65 | const response = await fetch(`${CONFIG.restUrl}/exploits/${exploit.manifest.name}/execute`, { 66 | method: 'POST', 67 | }); 68 | return await response.json(); 69 | }; 70 | 71 | export const updateExploit = async (exploit: Exploit): Promise => { 72 | const response = await fetch(`${CONFIG.restUrl}/exploits/${exploit.manifest.name}`, { 73 | method: 'PUT', 74 | headers: { 75 | "Content-Type": "application/json" 76 | }, 77 | body: JSON.stringify(exploit) 78 | }); 79 | return await response.json(); 80 | }; -------------------------------------------------------------------------------- /frontend/src/services/webSocket.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import { mapWebSocketMessage } from "./models"; 5 | import type { WebSocketMessage } from "./models"; 6 | 7 | export class WebSocketService { 8 | readonly #url: string; 9 | readonly #fromProvider: () => number; 10 | readonly #messageHandler: (message: WebSocketMessage) => void; 11 | 12 | #ws: WebSocket | undefined; 13 | #closed = false; 14 | #timer: number | undefined; 15 | 16 | constructor( 17 | url: string, 18 | fromProvider: () => number, 19 | messageHandler: (message: WebSocketMessage) => void, 20 | ) { 21 | this.#url = url; 22 | this.#fromProvider = fromProvider; 23 | this.#messageHandler = messageHandler; 24 | this.connect(); 25 | } 26 | 27 | connect() { 28 | // Sanity check 29 | if (this.#closed) return; 30 | 31 | // Close any existing connections 32 | this.#ws?.close(); 33 | 34 | console.info("[ws] connecting"); 35 | 36 | const url = new URL(this.#url); 37 | url.searchParams.append("from", this.#fromProvider().toString()); 38 | 39 | this.#ws = new WebSocket(url); 40 | this.#ws.onopen = () => { 41 | console.info("[ws] connected"); 42 | }; 43 | this.#ws.onclose = () => { 44 | console.info("[ws] disconnected"); 45 | 46 | // Don't attempt to reconnect if `close()` has been explicitly called 47 | if (this.#closed) return; 48 | 49 | // Apply jitter to avoid a thundering herd 50 | const delay = 1000 + Math.floor(Math.random() * 100); 51 | this.#timer = setTimeout(() => { 52 | this.connect(); 53 | }, delay); 54 | }; 55 | this.#ws.onmessage = (event) => { 56 | let message: WebSocketMessage; 57 | try { 58 | message = mapWebSocketMessage(JSON.parse(event.data)); 59 | } catch (error) { 60 | console.warn("[ws] malformed data received", event.data, error); 61 | return; 62 | } 63 | try { 64 | this.#messageHandler(message); 65 | } catch (error) { 66 | console.warn("[ws] unable to handle message", message, error); 67 | } 68 | }; 69 | } 70 | 71 | close() { 72 | this.#closed = true; 73 | this.#ws?.close(); 74 | this.#ws = undefined; 75 | 76 | if (this.#timer) { 77 | clearTimeout(this.#timer); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /frontend/src/routes/config.lazy.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import { createLazyFileRoute } from "@tanstack/react-router"; 5 | import MonacoEditor, { type Monaco } from "@monaco-editor/react"; 6 | import { useCallback, useEffect, useRef } from "react"; 7 | import { createHighlighter } from "shiki"; 8 | import { shikiToMonaco } from "@shikijs/monaco"; 9 | import theme from "shiki/themes/catppuccin-mocha.mjs"; 10 | 11 | export const Route = createLazyFileRoute("/config")({ 12 | component: () => Configuration(), 13 | }); 14 | 15 | function Configuration() { 16 | const stringConfiguration = useRef(`hello = [ 17 | "world", 18 | "computer" 19 | ]`); 20 | 21 | const updateConfiguration = useCallback(() => { 22 | // TODO: post new configuration 23 | }, []); 24 | 25 | useEffect(() => { 26 | // TODO: fetch server configuration 27 | }, []); 28 | 29 | const monacoMount = useCallback(async (monaco: Monaco) => { 30 | const highlighter = await createHighlighter({ 31 | themes: [ 32 | { 33 | ...theme, 34 | name: "kriger", 35 | colors: { 36 | ...theme.colors, 37 | "editor.background": "#050a1b", 38 | }, 39 | }, 40 | ], 41 | langs: ["toml"], 42 | }); 43 | monaco.languages.register({ id: "toml" }); 44 | shikiToMonaco(highlighter, monaco); 45 | }, []); 46 | 47 | return ( 48 |
49 |
50 | { 56 | if (!value) return; 57 | stringConfiguration.current = value; 58 | }} 59 | options={{ 60 | minimap: { 61 | enabled: false, 62 | }, 63 | }} 64 | /> 65 |
66 | 67 | 74 |
75 | ); 76 | } 77 | export default Configuration; 78 | -------------------------------------------------------------------------------- /crates/kriger/src/cli/models.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | use kriger_common::models; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Deserialize)] 8 | pub(crate) struct CliConfig { 9 | pub client: CliClientConfig, 10 | pub registry: CliRegistryConfig, 11 | } 12 | 13 | #[derive(Debug, Deserialize)] 14 | pub(crate) struct CliClientConfig { 15 | pub rest_url: String, 16 | } 17 | 18 | #[derive(Debug, Deserialize)] 19 | pub(crate) struct CliRegistryConfig { 20 | pub secure: bool, 21 | pub registry: String, 22 | /// If true, the registry will be used for exploit templates too 23 | pub custom_templates: bool, 24 | pub username: String, 25 | pub password: String, 26 | } 27 | 28 | #[derive(Debug, Deserialize, Serialize)] 29 | pub(crate) struct ExploitManifest { 30 | /// If specified, the CLI will skip the building step 31 | pub image: Option, 32 | pub exploit: InnerExploitManifest, 33 | } 34 | 35 | #[derive(Debug, Deserialize, Serialize)] 36 | pub(crate) struct InnerExploitManifest { 37 | pub name: String, 38 | pub service: String, 39 | pub replicas: i32, 40 | pub workers: Option, 41 | pub enabled: bool, 42 | pub resources: ExploitResources, 43 | } 44 | 45 | #[derive(Debug, Deserialize, Serialize)] 46 | pub(crate) struct ExploitResources { 47 | pub cpu_request: Option, 48 | pub mem_request: Option, 49 | pub cpu_limit: String, 50 | pub mem_limit: String, 51 | pub timeout: u32, 52 | } 53 | 54 | impl Into for InnerExploitManifest { 55 | fn into(self) -> models::ExploitManifest { 56 | models::ExploitManifest { 57 | name: self.name, 58 | service: self.service, 59 | replicas: self.replicas, 60 | workers: self.workers, 61 | enabled: self.enabled, 62 | resources: self.resources.into(), 63 | } 64 | } 65 | } 66 | 67 | impl Into for ExploitResources { 68 | fn into(self) -> models::ExploitResources { 69 | models::ExploitResources { 70 | cpu_request: self.cpu_request, 71 | mem_request: self.mem_request, 72 | cpu_limit: self.cpu_limit, 73 | mem_limit: self.mem_limit, 74 | timeout: self.timeout, 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /frontend/src/routes/submit.lazy.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | // Copyright Authors of kriger 3 | 4 | import { createLazyFileRoute } from "@tanstack/react-router"; 5 | import { FlagCode } from "../utils/enums"; 6 | 7 | export const Route = createLazyFileRoute("/submit")({ 8 | component: () => ( 9 |
10 | {/* Textarea to dump texts containing one or many flags */} 11 |