Tags

Tags give the ability to mark specific points in history as being important
  • v0.16.1

    a683ff33 · tagged: v0.16.1 ·
    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 · tagged: v0.16.0 ·
      ## 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.4

    af341f70 · tagged: v0.15.4 ·
    v0.15.4
    
     ## Fixes
     - dropped server side access check for client side check
  • v0.15.3

    85a8d64e · tagged: v0.15.3 ·
    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 · tagged: v0.15.2 ·
    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 · tagged: v0.15.1 ·
    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 · tagged: v0.15.0 ·
    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 · tagged: v0.14.0 ·
     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.1

    b5bf2511 · tagged: v0.13.1 ·
    v0.13.1
    
        I forgot some fixes before pushing the main version tag :'(
  • v0.13.0

    b62998b1 · tagged: v0.13.0 ·
    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 · tagged: v0.12.1 ·
    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 · tagged: v0.12.0 ·
    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 · tagged: v0.11.0 ·
    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 · tagged: v0.10.1 ·
    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 · tagged: v0.10.0 ·
    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 · tagged: v0.9.0 ·
    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.5

    e5bd71ea · tagged: v0.8.5 ·
    v0.8.5
    
     ## Fixed
        - missing "unknown" colors from the other scoreboards
  • v0.8.4

    519192b9 · tagged: v0.8.4 ·
    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 · tagged: v0.8.3 ·
    v0.8.3
    
     ## Fixed
    - fixed sql query in edit user api
    - fixed promote latest job in gitlab ci
  • v0.8.2

    c9d94813 · tagged: v0.8.2 ·
    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