Tags give the ability to mark specific points in history as being important
-
v0.16.1
a683ff33 · ·v0.16.1 ## CHANGES - We now pass error messages from checks to blue team users in check stats view ## FIXES - Fixed possible deadlock between new validation scheduler and uptime scheduler when switching between Dry Run modes -
v0.16.0
8cab62a9 · ·## Upgrade notes - **Start from a fresh database.** The schema changed a lot (scoring criteria, `Raw_Scores` in place of `Team_Score_Current`, validation runs, dry-run rounds, the IR tables dropped) and there are no migrations. - **Re-check old settings exports before importing.** A v0.15.x export won't import unchanged: - `ir.toml` is no longer read. - `settings.toml` now carries the scoring criteria. - Every inject in `inject.toml` needs a `scoring_target` (a criterion name). - `round_interval` must be at least 15. Export from a fresh v0.16.0 install and use it as the reference format. - **Traefik certificates are now stored on the host.** Run `just setup-environment` before `just reset-swarm` or a fresh deploy. It creates `/opt/comp/hermes/traefik`, the deploy refuses to start without it, and `reset-swarm` now leaves it alone. Let's Encrypt only issues five certificates a week per domain, and the old named volume was wiped on every reset. - **New minion env var `MINION_MAX_PARALLEL_VALIDATION`.** It sets the size of a separate pool of validation slots and defaults to 2. The prod compose file sets 8, and `just setup-environment` writes 16 for dev. - **New RabbitMQ exchange `validation`**, with queues `validation.execute` and `validation.results`. `EnsureTopology` declares them at startup, so nothing has to be done by hand. - **The minimum round interval is 15 seconds (was 5).** Every round keeps its last 10 seconds for minions to report back and for saving results, so at 15 s checks get 5 s to run. ## Added - **Scoring criteria.** A team's score is now a weighted percentage over the enabled criteria. Each criterion has a kind (Uptime, Injects, KOTH or Custom), a weight (all weights add up to 1) and a total possible. - A criterion's score is weight × (earned + adjustments) / total possible. - Staff manage criteria on a new Scores subpage. - Disabling a criterion locks every write to what it scores, staff included, and shows a banner on the scoreboard and team pages. Disabling Uptime also pauses the Uptime Engine. - **IR reports are now injects.** An inject template targets a scoring criterion, so IR is an Injects criterion its templates score into. - **Dry-run mode** (Settings). Rounds keep running against real targets and their results show on the scoreboard and team pages, but no points are awarded. The scoreboard shows "Dry run". Results from dry-run rounds stop showing once the mode is turned off. A change takes effect from the next round. - **Validation tab** (Checks → Validation, black team and admin). Runs selected checks, or every active check, against one team or all teams right now, outside any round and without scoring. - A check you pick runs even if it's inactive. - Results are grouped by check, with pass/down/engine-error counts, timings, a filter (failures, no response) and per-step output for chain checks. - Runs are stored. The tab opens on the newest run with its time, and a paged list of past runs lets you reopen any of them. - The check editor has a Validate panel that runs the saved check. - Validation has its own RabbitMQ exchange and its own minion slots, so it never takes a scoring slot or gets purged at round start. Each validation check is capped at 50 seconds. - API: `POST /api/validation-runs`, `GET /api/validation-runs` (paged), `GET /api/validation-runs/:run_id`. - **Variables can be renamed.** Renaming a variable rewrites the `{vars.<name>}` references in its check, and team overrides stay attached. - **Staff can view any team's check page**, with results, stats and variables, from the scoreboard (white team and up). - **Team check stats graphs** have a set range. - **The LDAP check can search, not just bind.** Pass on bind, on an entry existing, on an entry count, or on an attribute value (equals, contains, regex). It supports LDAPS, base DN, scope, a filter with a substituted value, and a returned attribute. - **The settings page** shows how many seconds checks get per round and warns at the minimum interval. ## Changed - "Engine" is now **Uptime Engine** everywhere, and the engine reset is now **Reset Competition**. - White team and above accounts no longer need a team number. - Team check pages refetch when the scoreboard announces a new round, so the two pages move together. A round only appears on the team page once its end time has passed, as on the scoreboard. - The telnet check works with Windows telnet servers: CMD and PowerShell prompts, and KpyM's ANSI screen updates. - ICMP execution times are more accurate. - Check variables are readable by white team (editing still needs black team). - A check that times out now reports "check exceeded its deadline" (was "…round deadline"), which is also correct for validation runs. - `just update-swarm` passes `--force`, so redeploying the same tag restarts the services. ## Removed - The old IR system: its pages, the `/api/ir/*` routes, and `ir.toml` in settings export and import. IR is now an inject criterion (see Added). ## Fixed - Minion heartbeats and registrations were being dropped. The round-start purge now clears only the check queues, not the minion queues. - Down targets held minion slots until the round deadline. Connect timeouts were added to the FTP, HTTP, LDAP, MongoDB, MSSQL, MySQL, SMB, telnet and WinRM checks. - Missing logging was added across the minion checks. - NULL bytes are stripped from check output before it is stored. - The ipvlan network setup no longer gives a minion an address ending in `.0` or `.255`. - Scoring rounds survive switching dry-run mode on or off. ## Internal - Frontend tests moved from Jest to Vitest, and several were simplified. - The CI dind services stream their logs into the job (`CI_DEBUG_SERVICES`) and have a TCP health check on 2376. That check only waits for the port to open, not for dockerd to serve, so integration jobs can still fail on a slow runner node. - `build_ci_tools_image` no longer runs on tag pipelines. - Table migrations in `setup_db.go` were removed. -
-
v0.15.3
85a8d64e · ·v0.15.3 ## Added - A "Public scoreboard" setting, off by default. While it's off, `/scoreboard`, `/api/round/info`, `/api/checks/limited` and `/api/users/limited` need a valid session or bearer token of any role, and the frontend sends a logged-out viewer to the login page. Admins and black team change it from the settings page, `PATCH /api/settings` (`scoreboard_public`), or `scoreboard_public` under `[settings]` in settings.toml. An import that leaves the key out keeps the current value - `GET /api/scoreboard/access` returns 204 if the caller may view the scoreboard, 401 if not ## Changed - Labels on read-only values (settings, user and check cards, minion stats, rubric rows, check details) are now lime captions, so they stand apart from their values. Form input labels are unchanged - The settings page shows each setting's current value, with a divider between settings - Toggles take an on and an off label and keep their width when the label changes, so the switch no longer moves on the checks and users cards - Rubric descriptions on inject and IR rubrics are edited in a text area that grows with the text - The "Public Scoreboard" button is now "Scoreboard" - The SMB check's domain field is labelled "Windows AD Domain", and its placeholders no longer use names from a past competition ## Fixed - The toggle knob overflowed its track by 2px at the bottom, and by 2px on the right when on -
v0.15.2
0a1cf7fe · ·v0.15.2 ## Fixed - Frontend was failing to build because it didn't like the upgrade to TS 7 :( - Updated frontend containers to node 26
-
v0.15.1
1bcd21c3 · ·v0.15.1 ## Changed - `GET /api/health` returns 503 until minion registration, the round info cache and the scheduler have each come up once. While starting, the body lists what it is still waiting on. After that it returns 503 with `Status: Degraded` whenever Postgres is unreachable or RabbitMQ is disconnected. The body now includes `Database` and `RabbitMQ` lines - `/api/health` no longer goes through the transaction middleware, so a database outage shows up as a 503 from the health check instead of a 500 - `/minion/health` returns the version, `Status` and `RabbitMQ`, and returns 503 while the minion is unregistered or has no RabbitMQ connection. Docker's healthcheck retries cover a short reconnect - The backend, frontend and minion images no longer have the `entrypoint.sh` that waited on Postgres and RabbitMQ. The processes already retry both on their own - In the compose override, the minion waits for a healthy backend instead of the database, and the frontend waits for a healthy backend instead of the database and RabbitMQ - Frontend is on TypeScript 7, and npm dependencies are updated ## Fixed - A check or team deleted mid-round (a database reset, for example) made that round's ledger insert fail, and every team lost the round's results. The ledger write now skips results for deleted checks and teams, logs a warning with their ids, and writes the rest. A delete that lands during the write waits for it to finish, then cascades over the new rows - The scheduler published checks in the same order every round, and RabbitMQ's round-robin gave each team the same minion, and so the same source IP, every round. Checks are now shuffled before publishing - `just check-nodes` showed no IP range on Docker 29, which reports no IPAM config for the config-only `minion_lan_config` network. It reads the range from `minion_lan` now. That network only exists on a node while a minion runs there, so a node without one shows "range shows once a minion runs here"" -
v0.15.0
068ced4f · ·v0.15.0 ## Added - Minions register with the backend on startup and take no checks until the backend has a row for them. They send a heartbeat every 30 s. A minion the backend hasn't heard from in 5 minutes is marked expired, and it registers again on its next heartbeat - Minions report their own shutdown (with the signal) or panic (with the stack) when they deregister. The backend records registrations and expiries, and keeps every event in a history - Each uptime result records which minion ran it - The admin Minions page lists registered minions and the latest minion events, refreshed every 30 s - `GET /api/minions/registered` and `GET /api/minions/events?limit=` (default 100, max 500), both WHITE or higher. `GET /api/minions/basic-stats` is a stub that returns 501 - A Manage Graders button on the IR reports management page - Check forms hide settings that don't apply. Skip TLS verification only shows for an `https://` target, and `continue_on_failure` hides on optional chain steps - The HTTP check's URL must start with `http://` or `https://`. The form flags a bad URL once you leave the field ## Changed - `Round_Ledger` and `Round_Scores` are now `Uptime_Round_Ledger` and `Uptime_Round_Scores` - The backend, scheduler and minions retry Postgres and RabbitMQ with backoff (capped at 16 s) forever instead of exiting. A scheduler that loses its lock connection reconnects and competes for the lock again - Log names and log files are `<service>-<machine>-<instance>`. The instance is `HERMES_INSTANCE` (the Swarm task slot in prod), else the port the process holds, else the pid, so a replica keeps its log file across restarts - Swarm runs 10 minion replicas by default and updates them one at a time, stop-first. A minion's healthcheck only passes once it is registered and consuming checks - The database container creates the `hermes` database through `POSTGRES_DB`. The backend no longer creates it, and `just redeploy-ephemeral-postgres` sets `POSTGRES_DB` too - A minion waits for in-flight checks before exiting on a signal. A second signal exits right away - Default IR rubric is now 30 points for Professionalism and 70 for Accuracy of attack (fresh databases only) ## Fixed - Two processes starting together could be given the same port, because the port was probed and released before use. The process now keeps the listener it found - Editing a check swallowed every error except a 401, and creating a check didn't show the server's validation message. Both now show it - References to the old setup scripts now point at `just setup-environment` -
v0.14.0
e6b34c6d · ·v0.14.0 ## Added - Checks can be reordered by dragging their cards on the admin checks page. Dragging is off while a search or type filter is active, because the server takes the whole list. The admin checks page, the blue team checks page and the scoreboard all list checks in this order, and a new check goes last - `POST /api/checks/reorder` takes `check_ids`, every check id in the new order, and needs BLACK or higher. A list with a duplicate, an unknown id or a missing check is a 400 - A Delete button on the check edit page, behind a confirmation - `/api/health` says whether this process's scheduler holds the lock (`leader`) or is waiting (`standby`) - Login is rate limited per client IP to 1 request a second with a burst of 10. Over the limit it returns a 429, and the login page tells the user to wait. The limiter is in memory per replica, so N replicas allow N times that rate - Cross-origin protection on every API route, using Go's `http.CrossOriginProtection` with the allowed URLs as trusted origins. It refuses cross-site form posts, which CORS and SameSite=Lax both let through. Clients that send no `Origin` or `Sec-Fetch-Site`, such as curl and Bruno, are unaffected - Security headers. The backend sends `X-Content-Type-Options: nosniff` and a CSP whose `frame-ancestors` lists only the trusted origins, so the frontend can still frame inject PDFs. The frontend sends `nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: same-origin` and a CSP that blocks framing, plugins and `<base>` changes, and drops `X-Powered-By`. The prod swarm override adds a one-year HSTS header on the HTTPS routers ## Changed - The JWT cookie is HttpOnly and SameSite=Lax, so script on the page can't read the token. A second cookie, `hermes_session`, holds the token's claims as unsigned base64url JSON for the frontend to read. The backend never reads it, and every access check still runs on the JWT. Anyone signed in before the upgrade has no `hermes_session` cookie and has to sign in again - The backend accepts only HS256 tokens that carry an expiry. It moved from the unmaintained `dgrijalva/jwt-go` to `golang-jwt/jwt/v5` - Every backend process runs both the scheduler and the API. `HERMES_ROLE` is gone, and `just setup-environment` no longer writes it. Replicas that lose the scheduler lock wait as standbys. The scheduler's separate health server is gone, since `/api/health` covers it. One database pool, opened at startup, serves both - Pausing, resuming and resetting the engine take effect right away. Every write of the engine state sends `NOTIFY engine_state`, which wakes a paused scheduler. A reset also sends `NOTIFY engine_reset`, which cancels the round in flight and makes every API reload its cached scoreboard. Each API checks the engine state every second, so clients see a pause or resume made through any replica - The scheduler's round-info message carries only the round id. Each API reads that round back from the database, so a message that arrives after a reset can't bring a deleted round back - `/api/round/info` sends each check's `check_id`, `user_id`, `status` and `success` and nothing else. Check names, output, error text and timings are gone from it, and the scoreboard cell tooltips no longer show output or errors - `checks.toml` has a `sort_order` on every check. Import refuses a check with no `sort_order`, a negative one, or one an earlier check in the file already uses. A merge import puts the file's checks after the existing ones and keeps their relative order. Exports from v0.13.1 and earlier have no `sort_order`, so add one to each check before importing them - A destructive users import is refused with a 400 unless the importing account is in `users.toml`. Once it commits, the importer's session ends and they sign in again with the password from the export - Importing a `checks.toml` with no checks, a `users.toml` with no users or a `settings.toml` with nothing in it is no longer an error. A destructive checks import still needs a `checks` key, so `checks = []` clears every check - Bulk import caps the zip at 512 MB, both as uploaded and as decompressed. Each PDF inside is held to the PDF size limit and every other file to 10 MB, counted on the decompressed bytes rather than the sizes the zip claims. The zip is read from the upload instead of being copied into memory first - `docker-compose.yml` publishes Traefik's ports 80 and 443 in host mode. Ingress mode NATs every client to one gateway address, which would put every user in the same login rate-limit bucket. The backend takes the client IP from `X-Forwarded-For`, which Traefik overwrites - echo package bumped to 5.4.0 - Existing databases need the new `Checks.sort_order` column added by hand. A fresh database gets it from setup: ALTER TABLE Checks ADD COLUMN sort_order INTEGER; UPDATE Checks c SET sort_order = s.n - 1 FROM (SELECT id, row_number() OVER (ORDER BY id) AS n FROM Checks) s WHERE c.id = s.id; ALTER TABLE Checks ALTER COLUMN sort_order SET NOT NULL, ADD CONSTRAINT checks_sort_order_key UNIQUE (sort_order); ## Fixed - The public scoreboard showed check output and error text in its tooltips, and network errors in that text could include a minion's IP. Minions now send their own non-loopback addresses with each result, and the scheduler replaces them with `[minion]` in output and errors, chain steps included, before anything is stored - A scoreboard refreshed from the database could include points from a round that was still running. It now shows the latest round past its `ends_at`, with scores as they stood when that round ended - After an engine reset, the scheduler kept collecting results for a round that no longer existed, then backed off - Each new scoreboard connection re-sent the current round, engine state and remaining time to every connected client. Now only the new client gets them - A bulk import with destructive users and injects together failed, because the injects were credited to the importer's old account id, which the users import had just deleted - A bulk import zip had no size limit, so a small upload could decompress into gigabytes - A username containing a NUL byte made login return a 500 instead of the usual 401 - Confirm dialogs could be confirmed twice while the first request was still running. Their buttons are now disabled until it finishes - The Service and Score labels on the default scoreboard drew over the mobile nav bar -
-
v0.13.0
b62998b1 · ·v0.13.0 ## Added - A working Users page. Accounts show as cards grouped by role, in the order ADMIN, BLACK, WHITE, BLUE, with search and role and status filters. Create and edit open in a modal, and each card has an activation switch. BLACK and up can change accounts. WHITE can only view - Bulk account actions. "Add teams" creates a numbered range, such as team01 to team20, in one request, up to 100 at a time. Selecting cards gives a bar to activate, deactivate, set the role of, or delete every selected account. A bulk action only reaches selected accounts the current filters still show - `PATCH /api/users?user_id=1&user_id=2` sets `activated`, `role` or both on several accounts at once - A time window for the scoring page graph: the last 30 minutes (the default), 1, 2, 6 or 12 hours, or the whole competition. `GET /api/scores/history` takes an optional `window` in seconds, at least 60. Each team's line starts at its value when the window opens, and the x-axis always spans the full window - Signed-in sessions now end when an account's role, activation or password changes, or the account is deleted. The next request gets a 401 and the auth cookie is cleared. Triggers on `Users` keep a `session_epoch` and announce changes with Postgres `LISTEN/NOTIFY`. Each API process keeps the epochs in memory, so no request queries the database for this - An `insecure` option on LDAP checks that skips LDAPS certificate verification. It is off by default - `just reset-swarm` returns every node to the state `just setup-node` left it in, using the new `docker/scripts/reset_node.sh`. It removes leftover containers, volumes and orphaned stack networks, and restarts dockerd when only a restart clears a network. `delete-swarm` runs it first - Integration tests against real services for HTTP (nginx), LDAP (OpenLDAP) and telnet (busybox telnetd). The SSH and SMB tests gain stalled-server and oversized-reply cases - `FuzzValidate` and `FuzzRunners` in `minion/fuzz_test.go`. They feed malformed configs to the check schema and to every check runner ## Changed - Minion networks use ipvlan (L2 mode) instead of macvlan. Each minion keeps its own IP but shares the host NIC's MAC. The script is now `docker/scripts/create_docker_ipvlan_network.sh` and refuses to run on a host that isn't an active swarm member - `just setup-node <host> <ip-range>` joins the node to the swarm if it isn't already a member. It stops if the node belongs to a different swarm. `show-swarm-join-info` is only needed for joining a node by hand - Swarm update and rollback `monitor` is 30s instead of 90s for the backend and minion services - Go 1.26 is the minimum. Every module's `go.mod` says `go 1.26.0`, and the CI jobs, the CI tools image and the backend and minion Dockerfiles build on `golang:1.26-alpine3.23`. Dependencies moved with it, including pgx 5.9.2, amqp091-go 1.13.0, x/crypto 0.56.0 and go-ntlmssp v0.1.1 - The integration test jobs pull Docker Hub images, the dind service and testcontainers' own Ryuk image included, through the group's GitLab Dependency Proxy. GitLab fetches each image from Docker Hub once and serves its cached copy after that, so CI no longer runs into Docker Hub's rate limit. The SSH tests pull `ghcr.io/linuxserver/openssh-server` directly - The scoreboard lists services as rows and teams as columns - User writes need BLACK or higher. Nobody can give a role above their own, change an account above their own role, or change the role or activation of their own account, or delete it - `POST /api/users` takes a `users` field holding a JSON array of accounts and returns 201. The whole set is validated before anything is written, and every bad entry is named. Taken usernames are a 409 instead of a 200. `DELETE /api/users` takes `user_id` once per account and returns 204. `PUT /api/users` refuses a missing field instead of saving a zero value - Login refuses a deactivated account with a 403, checked only after the password matches. An existing session no longer stops a new login - Blue team numbers must be above 0 and unique among blue teams. `Users.team_number` and `created_at` are NOT NULL - Deleting a team also deletes its round results and score adjustments. A staff account that other records still name as their author can't be deleted, and should be deactivated instead - Engine reset restarts the round, ledger and score ids at 1 - A list of accepted outputs can't contain a blank entry. Every string contains "", so a blank entry used to make any "contains one of" check pass - Checks stop reading a reply at 16 MiB and fail. This covers HTTP, SSH, SMB, FTP and telnet - An SSH check gives the connection and the SSH handshake 5 seconds together. Before, only the connection had a limit - Existing databases need the schema changes above applied by hand: the cascades, the `Users` constraints and `session_epoch` column, and the two session triggers. A fresh database gets them from setup ## Fixed - A chain check started after its deadline reported "all steps passed" without running a step. It now fails. The minion also reported a pass or a timeout at random in that case, and a late result could overwrite a timeout result - A telnet check passed on the prompt or the command's own echo when the command printed nothing. Reading large telnet output was slow enough to hit the deadline - A team-set MSSQL password containing `;` could change where the check connected - MSSQL checks starting at the same time could read a half-written Kerberos config - LDAPS checks always verified the certificate, despite the `insecure=1` in their URL - A MongoDB check passed without running a query when given a query type it doesn't handle - Editing a user without a new password set that account's password to an empty string, and a new password typed into the form was ignored - A password over 72 bytes or a taken username returned a 500 instead of a clear error, and deleting any team that had played a round failed - A failed refresh no longer blanks the Users page. The list stays, with an alert - The search box and the filters on the Users page are the same height - TanStack Charts logged a key warning on the scoring page graph - One blue team inject hook still used `fetch` instead of `apiFetch` - With amqp091-go 1.13.0, which refuses to publish on an expired context, a minion dropped the "check exceeded round deadline" result and requeued the check. The check then bounced between minions until the next round. Results now publish on the minion's own context - The Go unit test jobs could start before a rebuild of the CI tools image finished, and run on the old image. They now wait for `build_ci_tools_image` whenever it's in the pipeline -
v0.12.1
5a96c05b · ·v0.12.1 ## Added - The scoreboard shows the engine state next to the round timer. It reads "Running", "Paused", or "Final Round" when the engine is paused but the current round is still counting down. The backend sends a `current_engine_state` SSE event when a client connects and each time the scheduler reads the settings. Only the default theme displays it. The template and irsec-2026 themes receive the value but don't render it yet - Every log line carries `service`, `host` and `pid` fields, plus `node` when `HERMES_NODE` is set. The Swarm compose file sets `HERMES_NODE` to the node's hostname, so container logs say which machine they came from ## Changed - A fresh database creates the default admin account as `admin` instead of `default`. Existing databases keep their account as it is - Log files are named `<service>-<host>-<pid>.log` in `/opt/comp/hermes/logs`, replacing `hermes_backend_log` and `minion-<host>-<random>_log`. Minions and KOTH minions use the same `<service>-<host>-<pid>` string as their name. A replaced container has a new hostname, so it starts a new log file and the old one stays on disk ## Fixed - N/A -
v0.12.0
61ae38fc · ·v0.12.0 - A working Settings page. It shows the current engine settings and lets BLACK and up start or pause the engine, manage competition placeholders, and export or import the competition's configuration. WHITE can view it but not change anything. Import and Reset only show for ADMIN - Export to a ZIP. Staff pick any of checks, graders, injects, IR, users and settings, and `POST /api/settings/export` returns `hermes-settings.zip` with one TOML file per section. Inject and IR exports include their PDFs. Submissions are never exported. Requires BLACK - Import from that ZIP. `POST /api/settings/bulk-import` now reads `graders.toml`, `inject.toml` and `ir.toml` along with checks, users and settings, and picks up the inject and IR PDFs from their `document_path` entries. Each section has its own destructive flag (`destructiveChecks`, `destructiveGraders`, `destructiveInjects`, `destructiveIR`, `destructiveUsers`, `destructiveSettings`) that replaces existing data instead of adding to it. Replacing graders fails with a 409 while reviews or submissions still reference them. Replacing injects deletes their submissions and reviews. Replacing IR deletes all IR reports and reviews so a rubric that has already been graded against can change - The users export carries each account's bcrypt hash, and import restores it as is. An import rejects a user with a missing or invalid `hashed_password` - Engine reset. `POST /api/settings/reset` deletes score adjustments, current team scores, team variable overrides, inject submissions, rounds, the round ledger and round scores, then sets the engine to PAUSED. It asks for confirmation on the page. Requires ADMIN - List variables. A check variable can now hold a list of values instead of one. Each round it hands the check one item, walking the list in order, or a random item when `randomize` is set. Teams can override a list with their own items from the blue team check page. When a list variable is secret, every item in it is scrubbed from round output - `variable_type`, `default_items` and `randomize` on the check variable endpoints, and `items` on a team's own variable writes. `POST /api/checks` takes a `variables` JSON array, so a check and its variables are created in one request and no round can run the check before its variables exist - Multiple accepted outputs. HTTP, FTP, SMB, SSH and WinRM checks gain two match types, "equals one of" (`exactListMatch`) and "contains one of" (`substringListMatch`), which pass when the output matches any entry in the new `accepted_outputs` list. Telnet gains only "contains one of". SSH, Telnet and WinRM checks get a `match_type` field for this, defaulting to the comparison they used before - A reusable list editor in the frontend for entering list items one at a time - Bruno requests for settings export, list variables, randomized list variables, a team's list overrides, and a chain check - Icons on several admin buttons - Almost every frontend API hook now goes through `apiFetch`, replacing each hook's own fetch and error handling - The checks export and import round-trip variables, including their type, list items and `randomize`. A `checks.toml` from before this release imports its variables as strings - The round number comes from the latest round in the database instead of a counter the scheduler keeps in memory, so an engine reset starts back at round 1. An ABORTED round is retried under its own number - Importing checks no longer fails with "conn busy", and chain checks import correctly - A bulk import without a zip file returns a 400 instead of panicking on the missing upload - The scoreboard fits every team on screen. Rows and columns share the available space down to a minimum size, and the panel scrolls past that, in the default, template and irsec-2026 themes
-
v0.11.0
74d3cd8c · ·v0.11.0 ## Added - A Scoring page for staff, replacing the empty placeholder. It shows the top three teams, a score graph, and a breakdown table with each team's uptime, inject, IR report and adjustment totals and their sum. The table sorts on any column and the page refreshes every 30 seconds. If a refresh fails, the last loaded scores stay on screen with the error above them - A score graph over time, switchable between total, uptime, inject, IR report and adjustment scores, on a linear or symmetric log axis. Lines step instead of sloping, since a score holds until the next event changes it. Dots mark each grade and adjustment, and hovering names the team, the event, the score and the time - The graph starts on the top eight teams, each in its own colour. Teams can be toggled, all selected or cleared. Past eight, teams draw in grey underneath, and a team keeps its colour while others are added or removed - The graph only asks for as much uptime history as it can draw. `GET /api/scores/history` takes the chart's width in points (50 to 2000) and returns at most that many uptime events per team, keeping the last round in each time bucket. A pause between rounds stays a gap. The browser fetches and shapes the history in a web worker, so a long competition does not block the page - Score adjustments. Staff add a bonus or penalty to a blue team with a reason, and edit or delete it later. WHITE can read them, BLACK and up can change them. An adjustment cannot be moved to another team or round, since that would rewrite the other team's history. Deleting one keeps its history - An audit log of every adjustment change, recording who made it, when, the adjustment before and after, and the team's total before and after. Each adjustment has a History button, and the adjustments page lists recent changes across all teams, including deleted adjustments. Both page back through older entries - `GET /api/scores/dashboard`, `GET /api/scores/history`, and `GET`, `POST`, `PATCH` and `DELETE` on `/api/scores/adjustments`, plus `GET /api/scores/adjustments/audit` - Score requests in the Bruno collection, under "admin actions/scores" ## Changed - A team's inject total counts only the latest graded submission for each inject. A graded resubmission replaces the earlier grade, and an ungraded one leaves it standing. Before, every graded submission was added, so a regraded resubmission counted twice. Existing cached totals keep the old sum until `POST /api/scores/recalculate` runs or the team is graded again - The admin nav's "Scores" link is nashboard is titled "Engine Dashboard" - The frontend depends on `d3-scale` scales - Schema changes for an existing database, which `setup_db.go` does not touch: the new `Score_Adjustment_Audit` table and)` index
-
v0.10.1
7c324bf4 · ·v0.10.1 ## Added - An FTP check. It logs in with a username and password, or anonymously when auth is left empty, retrieves one file in passive mode, and passes when the file exists, equals some text, contains it, matches a regex, or has a given SHA-256, MD5 or SHA-1 digest. The file contents are kept as result data even when the match fails - The FTP check fails a file over 16 MiB or a control conversation over 64 KiB, and times out after 30 seconds when the round gives it no deadline. A malformed passive reply fails the check instead of crashing the minion ## Changed - N/A ## Fixed - Closing the "Report graded." message after grading an IR report went to `/admin-panel/ir-reports/{team_id}/submissions`, which does not exist. It now goes back to the submissions list - A check's saved settings showed a nested group, auth credentials or each repeated item with its own numbered section headings. They now render as plain rows under their parent - A check's saved settings show an enum's label, such as "File contains", instead of its raw value `substringMatch` - A field marked hidden in a check schema lost the flag when the schema was built, so it still showed -
v0.10.0
457e8127 · ·v0.10.0 ## Added - IR reports. Blue teams upload incident response reports as PDFs to a dropbox, staff grade them against a rubric, and each team's IR total is kept alongside its uptime and inject scores - An IR settings page for staff. The dropbox opens on a timed window or by hand (hidden, open, closed), can allow one report per team or several, and can carry an example report that blue teams can download while the dropbox is open. A window whose close comes before its open is refused - One IR rubric shared by every report, seeded with Professionalism (40) and Accuracy of attack (60). Changing a line's max points rescales every report already scored against it, and removing a line removes the scores given against it - A blue team IR reports page. A team sees the dropbox state, uploads one PDF per request so a failed file is named on its own, and can retract a report while the dropbox is open and before anyone has graded it. A team sees what it uploaded and when, never its score. While the dropbox is hidden the page learns only that it is hidden, so the schedule is not published early - IR grading for staff. Reports are listed per team with whether each has been reviewed, open beside a PDF preview, and are scored line by line with feedback. A score over a line's max points, or from a grader not on the roster, is refused. A line left out of a save keeps its previous score - IR scores for staff, per report and per team total. They are staff only, like inject scores. No blue team or public route returns them - `POST /api/scores/recalculate` rebuilds every team's cached uptime, inject and IR totals from the rows behind them. The cache has several writers, and this puts it right without working out which one missed - Uploads are checked to be PDFs, and every route except bulk import stops reading a body past 26 MB instead of buffering it whole. The 25 MB document cap is now enforced before the upload is read, not after - The frontend sends you to the login page when any API call returns 401 - IR requests in the Bruno collection, under "blue team actions/ir" and "admin actions/ir" ## Changed - Graders are shared between injects and IR reports. The routes moved from `/api/injects/graders` to `/api/graders`, which breaks any existing caller. The Bruno requests moved to "admin actions/graders" - The inject submission review page and the IR review page use one shared review form - `Team_Score_Current` holds `uptime_score`, `injects_score` and `ir_reports_score`. The cumulative `passed_checks` and `failed_checks` columns are gone, since nothing read them and `Round_Scores` keeps the per-round counts. Each scoring category writes only its own column, so finishing a round can no longer reset a graded score - The round broadcast's scores use `uptime_score` in place of `score`. The bundled scoreboards are updated, and any other consumer has to follow - Creating an inject template requires `sort_order`, and a negative one is refused - Bulk settings import requires ADMIN instead of BLACK - Schema changes for an existing database, which `setup_db.go` does not touch: `Inject_Graders` renamed to `Graders`, the `inject_access_type` enum renamed to `access_type`, the `Team_Score_Current` columns above, and the new `IR_Config`, `IR_Rubric_Items`, `IR_Submissions`, `IR_Document_Submissions`, `IR_Reviewed_Submissions` and `IR_Submission_Scored_Items` tables ## Fixed - `Inject_Template_Documents.template_uuid` and `Inject_Document_Submissions.submission_id` are now unique, so a template or submission cannot end up with two documents - The timed and manual inject access tables were created without an error check, so a failure there went unreported during setup
-
v0.9.0
455dda75 · ·v0.9.0 - A blue team check page. Each check a team is scored on now has its own page, showing the fields that team controls, how the check has been going for them, and the recent rounds with the reason for each failure - Check names on the scoreboard are links. A blue teamer lands on their own team's view of that check, staff land on the check's settings in the admin panel, and an anonymous viewer gets no link, since the scoreboard is public and the destinations are not - Blue teams can set their own values for a check's declared variables. Everything on the card saves in one request, and a rejected variable takes the whole batch with it, so the form can never half-apply. A secret stays write-only, so its box starts empty and a badge says whether the team has already set one - Per-check statistics for a team: passed, failed, uptime, the points the check has earned, and average and most recent response time - Two charts on the check page, drawn with @tanstack/charts. An uptime strip with one bar per round, and a response time line. Hovering either names the round, the result and the points, or the latency - A paginated table of recent rounds, with page sizes, a Refresh button and a last-updated time. It also polls on the round interval, so a team watching a fix land sees it without pressing anything - Chain checks show which step failed and which never ran. The runner stops at the first required failure and records nothing for the steps after it, so the page reads the chain's declared steps and marks those as never run rather than leaving them off the list - `GET /api/checks/{id}/results/mine` and `GET /api/checks/{id}/stats/mine`, both blue team gated, both scoped to the calling team by its token rather than by anything in the request - Check fields can be hidden by a condition. An http request body is now hidden until a content type is chosen, instead of sitting there asking to be filled in for a request that will not carry it - The browser tab reads "(DEV) Hermes" when the frontend runs in development, so a dev tab is not mistaken for the live one ## Changed - `PUT /api/checks/{id}/variables/mine` takes the whole set of variables in one `variables` JSON array instead of one `variable_name` and `variable_value` per request. This breaks any existing caller. The saved Bruno requests are updated - A check's status on the scoreboard is now derived from whether the check succeeded, with its status text asked only whether the check reached a verdict at all. The scoreboard and the blue team check page read the same four states with the same wording, so a round cannot be labelled one way in one place and another way in the other. Each theme keeps its own colours - Blue team check code gathered into one `blue_checks.go` per package, and the blue team requests moved out of the Bruno collection's "admin actions" folder into their own - The blue team check page split into a page plus three components, in line with how the admin check pages are laid out - A new index on `Round_Ledger (user_id, check_id, round_id DESC)`. A team's history for one check used to scan the whole ledger, since the existing unique constraint leads with `round_id`. `setup_db.go` only runs against a fresh database, so an existing deployment needs the statement applied by hand ## Fixed - The scoreboard logged `retrieved unknown check status` to the console on every cell, every render, for any status outside a fixed list it matched against. That list had fallen behind the engine: a cancelled check was never in it, and neither were rows left over from an earlier version. Both now resolve without the noise -
-
v0.8.4
519192b9 · ·v0.8.4 ## Added - A date and time picker for inject open and expiry times and for access overrides, replacing `<input type="datetime-local">`, whose Firefox picker closes after the day is picked and leaves the time to be typed. One popover holds the month and a quarter-hour list, and any other time can be typed as "10:07 PM", "1007p" or "22:07" - An Unknown scoreboard state, shown in gray, for a cell with no result, such as when the engine starts paused or is paused before the page has seen a round. A check status the frontend doesn't recognize also shows as Unknown instead of Engine Error ## Changed - New injects default to unlimited resubmissions (-1) instead of none - The injects overview shows a graded result as points (`7/10`) instead of a percentage - Production minions run as 4 replicas of 16 parallel checks, instead of 2 of 64, and stay off swarm manager nodes - CI artifacts expire after 7 days - Removed one-line wrapper helpers, `cn` among them, from the check editor, inject pages and shared components ## Fixed - The API's CORS allowlist only accepted `HERMES_PUBLIC_DOMAIN` with a scheme, while the traefik rules need it bare. It now takes the bare host, allows both its http and https origins, and skips a value that includes a scheme
-
v0.8.3
e010b33f · ·v0.8.3 ## Fixed - fixed sql query in edit user api - fixed promote latest job in gitlab ci
-
v0.8.2
c9d94813 · ·v0.8.2 ## Added - Limited users API (`GET /api/users/limited`): the activated blue teams with just the fields the scoreboard needs, unauthenticated like the limited checks API - `just setup-node <host> <range>` and `just check-nodes`, which prepare a swarm node's host directories and macvlan config from the manager over ssh, and report which nodes are missing a config network or sharing a range - `HERMES_PUBLIC_IP`, prompted for by `just setup-environment` and used in the traefik host rules ## Changed - The scoreboard builds its team rows from the roster API instead of the live round feed, so teams are listed before the first round lands and stay listed through a round with no results - Round info reads the scores and the ledger separately instead of joining them - Traefik routing split per router: plain HTTP over the public IP, HTTPS with a redirect over the domain, replacing the global entrypoint redirect - `NEXT_PUBLIC_API_URL` is empty in production, so requests go to `/api` on whatever host served the page and one image works at any address - Container registry moved to port 4999 - The traefik swarm provider network is stack-prefixed, matching the name `docker stack deploy` actually creates - `just deploy-prod-swarm` sources `.internal.env` itself and refuses to deploy when the public host variables are unset, rather than deploying green with empty traefik rules ## Fixed - Login behind a TLS-terminating proxy: the session cookie was always marked Secure, so a browser on plain HTTP dropped it with no error anywhere. Login and logout now build the cookie through one constructor, so logout clears what login set - A team with no ledger rows in the current round reported a score of 0 instead of its standing score - The macvlan script takes the IP range as an argument so it can be piped over ssh, removes the two networks in dependency order, and explains what to do when the config network is still in use - `just setup-environment` chowned the hermes path before creating its subdirectories, leaving them root-owned - The `promote_latest` CI job no longer requires the privileged runner tag