From 745c07b57fc304e73418d0d2954d89fb437c254e Mon Sep 17 00:00:00 2001 From: Tirefire <84106878+tire-fire@users.noreply.github.com> Date: Sun, 1 Feb 2026 23:50:00 +0000 Subject: [PATCH 1/2] Improve documentation and expand config examples --- .gitignore | 1 + README.md | 446 ++++++++++++++++++++-- config/credlists/linux.credlist.example | 3 + config/credlists/windows.credlist.example | 3 + config/event.conf.example | 219 +++++++++-- custom-checks/example.sh | 13 +- docs/custom-checks.md | 122 +++++- 7 files changed, 714 insertions(+), 93 deletions(-) create mode 100644 config/credlists/linux.credlist.example create mode 100644 config/credlists/windows.credlist.example diff --git a/.gitignore b/.gitignore index 467ed097..dff79cea 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ runner/runner /config/* !/config/**/ /config/*/** +!/config/credlists/*.example /custom-checks/* !/custom-checks/.gitkeep !/custom-checks/example.sh diff --git a/README.md b/README.md index 883b670c..5e19162c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Quotient +Quotient is a cybersecurity competition scoring platform designed for CCDC-style events. It automatically scores defensive service checks while providing infrastructure for teams to submit inject solutions and make password change requests (PCRs). + +Used by [WRCCDC](https://wrccdc.org) (Western Regional CCDC) and [PRCCDC](https://prccdc.com) (Pacific Rim CCDC). + ## Prerequisites Ensure you have the following installed on your system: @@ -11,31 +15,56 @@ Ensure you have the following installed on your system: ```bash git clone --recurse-submodules https://github.com/dbaseqp/Quotient cd Quotient +# Edit .env with your database and Redis passwords +cp config/event.conf.example config/event.conf +# Edit config/event.conf with your competition settings docker-compose up --build --detach ``` ## Architecture -The system is designed as a group of docker components using docker compose in docker-compose.yml. These components are: -1. Server - this is the scoring engine, the web frontend and API, configuration parser, and coordinator of scoring checks. -2. Database - PostgreSQL database that keeps state for each of the checks and round information. -3. Redis - This passes data between the runners and the scoring engine as a queue. -4. Runner - This alpine container has go code that runs the service check after retrieving the task from redis. It needs to be customize if custom checks require additionally installed software like python modules (automatically installed from requirements.txt) or alpine packages. This is managed by Dockerfile.runner and can be rebuilt with `docker-compose build runner`. -5. Divisor - Docker container with elevated privileges that randomly selects an IP address from a configured subnet, and assigns from a configurable pool size to the docker runner containers. This needs to be able to query docker to determine the docker hosts and to be able to manage host network iptables. This is a separate git repo and a submodule of this repo under ./divisor. +The system is designed as a group of Docker components using Docker Compose: + +| Component | Description | +|-----------|-------------| +| **Server** | Scoring engine, web frontend/API, configuration parser, and check coordinator | +| **Database** | PostgreSQL database for persisting checks, rounds, scores, and submissions | +| **Redis** | Message queue passing tasks between the server and runners | +| **Runner** | Alpine containers (5 replicas by default) that execute service checks. Customize via `Dockerfile.runner` for additional packages | +| **Divisor** | Optional IP rotation container - assigns unique source IPs from a subnet pool to runners, preventing target systems from blocking based on IP. See [Divisor](https://github.com/dbaseqp/Divisor) | + +## Environment Variables + +Create a `.env` file with the following required variables: + +```bash +POSTGRES_USER=engineuser +POSTGRES_PASSWORD= +POSTGRES_DB=engine +POSTGRES_HOST=quotient_database +REDIS_PASSWORD= +REDIS_HOST=quotient_redis +``` + +Optional variables: +- `LDAP_BIND_PASSWORD` - LDAP bind password (alternative to config file) ## Troubleshooting -If you encounter any issues during setup or operation, consider the following: -- Check the logs for any error messages using `docker-compose logs`. -- Verify that all environment variables are correctly set in the `.env` file. -- Make sure that config values are set in `event.conf` before running the engine. -- Set `vm.overcommit_memory=1` on the host to avoid Redis warnings. Run `sudo sysctl vm.overcommit_memory=1` or add `vm.overcommit_memory = 1` to `/etc/sysctl.conf` and reboot. +- Check logs: `docker-compose logs` or `docker-compose logs ` +- Verify `.env` file has all required variables set +- Ensure `config/event.conf` exists and is valid TOML +- For Redis memory warnings: `sudo sysctl vm.overcommit_memory=1` (or add `vm.overcommit_memory = 1` to `/etc/sysctl.conf`) +- Rebuild runners after modifying `Dockerfile.runner`: `docker-compose build runner && docker-compose up -d runner` -## Web setup +## Web Setup -Through the Admin UI you will have to specify the "Identifier" for each team. This is the unique part of the target address. Also, you will need to mark the team as "Active" so that the team can start scoring. +After starting the engine: -If you want to rotate IPs, configure [Divisor](https://github.com/dbaseqp/Divisor). +1. Log in as admin +2. Navigate to the Admin UI +3. Set the **Identifier** for each team (the unique part of target addresses, e.g., `01` for team 1) +4. Mark teams as **Active** to begin scoring ## Configuration @@ -117,77 +146,131 @@ ip = "10.100.1_.2" ```toml [RequiredSettings] EventName = "Name of my Competition" -EventType = "rvb" +EventType = "rvb" # Use "rvb" for Red vs Blue (CCDC-style) DBConnectURL = "postgres://engineuser:password@quotient_database:5432/engine" BindAddress = "0.0.0.0" ``` -The "DBConnectURL" will use values you populate in the `.env` file. The `BindAddress` is the IP address the scoring engine will bind to. If you are deploying in the Docker container, this can be set to `0.0.0.0`. +- `EventType`: Use `rvb` for Red vs Blue competitions (CCDC-style). The `koth` option exists but is not fully implemented. +- `DBConnectURL`: Can be omitted if using environment variables (`POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_HOST`, `POSTGRES_DB`). +- `BindAddress`: Use `0.0.0.0` when deploying in Docker. -#### Ldap Settings +#### LDAP Settings ```toml [LdapSettings] LdapConnectUrl = "ldap://ldap.yournet.org:389" LdapBindDn = "CN=Scoring Engine Service Account,OU=service accounts,DC=yournet,DC=org" -LdapBindPassword = "password" +LdapBindPassword = "password" # Can also use LDAP_BIND_PASSWORD env var LdapSearchBaseDn = "OU=Users,DC=yournet,DC=org" LdapAdminGroupDn = "CN=YourAdmins,OU=Groups,DC=yournet,DC=org" LdapTeamGroupDn = "CN=YourBlueTeam,OU=Groups,DC=yournet,DC=org" LdapRedGroupDn = "CN=YourRedTeam,OU=Groups,DC=yournet,DC=org" +LdapInjectGroupDn = "CN=YourInjectManagers,OU=Groups,DC=yournet,DC=org" ``` -If using LDAPS for the Docker deployment, make sure you add the cert to the `./config/certs` so that it gets added to the certificate store. +If using LDAPS for the Docker deployment, add the certificate to `./config/certs` so it gets added to the container's certificate store. + +#### OIDC Settings + +```toml +[OIDCSettings] +OIDCEnabled = true +OIDCIssuerURL = "https://your-idp.example.com" +OIDCClientID = "quotient" +OIDCClientSecret = "your-client-secret" +OIDCRedirectURL = "https://quotient.example.com/auth/oidc/callback" + +# Optional settings with defaults shown +OIDCScopes = ["openid", "profile", "email", "groups", "offline_access"] +OIDCGroupClaim = "groups" + +# Group mappings +OIDCAdminGroups = ["quotient-admins"] +OIDCRedGroups = ["quotient-red"] +OIDCTeamGroups = ["quotient-teams"] +OIDCInjectGroups = ["quotient-inject-managers"] + +# Token expiry in seconds (defaults shown) +OIDCRefreshTokenExpiryTeam = 86400 # 1 day +OIDCRefreshTokenExpiryAdmin = 2592000 # 30 days +OIDCRefreshTokenExpiryRed = 172800 # 2 days +OIDCRefreshTokenExpiryInject = 86400 # 1 day + +# UI option +OIDCDisableLocalLogin = false +``` #### SSL Settings ```toml [SslSettings] -HttpsCert = "/path/to/https/cert" -HttpsKey = "/path/to/https/key" +HttpsCert = "/app/config/certs/server.crt" +HttpsKey = "/app/config/certs/server.key" ``` +When SSL is configured, the default port changes to 443. + #### Misc Settings ```toml [MiscSettings] -EasyPCR = true -Port = 80 +EasyPCR = true # Simplified PCR interface +ShowDebugToBlueTeam = false # Show check debug info to teams +Port = 80 # Server port (443 default with SSL) LogoImage = "/static/assets/quotient.svg" -StartPaused = true - -Delay = 60 -Jitter = 10 - -Points = 5 -Timeout = 30 -SlaThreshold = 5 -SlaPenalty = 50 +LogFile = "" # Optional log file path +StartPaused = true # Start with scoring paused + +# Round timing +Delay = 60 # Seconds between rounds (default: 60) +Jitter = 10 # Random jitter in seconds (default: 5, must be < Delay) + +# Scoring defaults (can be overridden per-check) +Points = 5 # Points per successful check (default: 1) +Timeout = 30 # Check timeout in seconds (default: Delay/2) +SlaThreshold = 5 # Consecutive failures before SLA penalty (default: 5) +SlaPenalty = 50 # Points deducted for SLA violation (default: SlaThreshold * Points) ``` #### UI Settings ```toml [UISettings] -DisableInfoPage = true -DisableGraphsForBlueTeam = true -ShowAnnouncementsForRedTeam = true +EnablePublicGraphs = false # Allow unauthenticated graph viewing +DisableGraphsForBlueTeam = true # Hide graphs from teams +AllowNonAnonymizedGraphsForBlueTeam = false # Show team names on graphs +ShowAnnouncementsForRedTeam = true # Red team sees announcements ``` #### Local Auth +Define local users for each role: + ```toml +# Admin users - full engine control +[[admin]] +name = "admin" +pw = "password" + +# Team users - blue team competitors [[team]] name = "Team01" pw = "password" -[[admin]] -name = "admin" +[[team]] +name = "Team02" pw = "password" +# Red team users - vulnerability tracking [[red]] name = "red01" pw = "password" + +# Inject managers - create injects and announcements +[[inject]] +name = "inject01" +pw = "password" ``` #### Environment Configuration @@ -223,10 +306,297 @@ ip = "10.100.1_.2" status = 403 ``` -Custom checks can be added to the `./custom-checks/` directory. It is very common to make the custom check simply run some other script that you have written that has the necessary logic to check the service. The script should return a 0 if the service is up and anything else if it is down. The script should be executable. The script will be mounted in the `/app/checks/` directory of the runner. If the script invokes external dependencies or needs to have a specific run time, this should be added to the Dockerfile.runner and the runner rebuilt and redeployed. +Custom checks can be added to the `./custom-checks/` directory. The script should exit with code 0 if the service is up and non-zero if down. Scripts are mounted at `/app/checks/` in the runner container. For a detailed walkthrough of writing custom checks, see [docs/custom-checks.md](docs/custom-checks.md). +### Service Check Reference + +All checks support these common properties: + +| Property | Description | Default | +|----------|-------------|---------| +| `display` | Check name suffix (e.g., "web" creates "boxname-web") | Check type | +| `target` | Override the box IP/hostname for this check | Box IP | +| `port` | Service port | Type-specific | +| `points` | Points awarded for success | Global default | +| `timeout` | Check timeout in seconds | Global default | +| `slathreshold` | Failures before SLA penalty | Global default | +| `slapenalty` | Points deducted on SLA violation | Global default | +| `credlists` | Array of credlist names for authentication | None | +| `disabled` | Disable this check | false | +| `launchtime` | Start checking at this time | Immediate | +| `stoptime` | Stop checking at this time | Never | + +#### Ping Check + +Simple ICMP ping check. + +```toml +[[box.ping]] +display = "ping" +# No additional options required +``` + +**Default port:** N/A + +#### TCP Check + +Verify TCP port connectivity. + +```toml +[[box.tcp]] +display = "ssh-port" +port = 22 +``` + +**Default port:** None (required) + +#### DNS Check + +Query DNS records and verify answers. + +```toml +[[box.dns]] +display = "dns" +port = 53 + + [[box.dns.record]] + kind = "A" + domain = "www.team_.example.com" + answer = ["10.100.1_.10"] + + [[box.dns.record]] + kind = "MX" + domain = "team_.example.com" + answer = ["mail.team_.example.com"] +``` + +**Default port:** 53 +**Supported record types:** A, MX + +#### Web Check + +HTTP/HTTPS request with optional status code and content matching. + +```toml +[[box.web]] +display = "web" +port = 8080 +scheme = "https" # "http" or "https" + + [[box.web.url]] + path = "/index.html" + status = 200 # Expected status code (optional) + regex = "Welcome" # Content regex (optional) + + [[box.web.url]] + path = "/admin" + status = 403 +``` + +**Default port:** 80 (http) or 443 (https) +**Default scheme:** http + +#### SSH Check + +SSH login with optional command execution. + +```toml +[[box.ssh]] +display = "ssh" +port = 22 +credlists = ["linux_users.credlist"] +privkey = "id_rsa" # Private key file in config/scoredfiles/ (optional) +badattempts = 3 # Failed login attempts before real attempt (optional) + + [[box.ssh.command]] + command = "whoami" + output = "root" # Exact match (optional) + useregex = false + contains = false # Check if output contains the string +``` + +**Default port:** 22 + +#### WinRM Check + +Windows Remote Management check with optional PowerShell commands. + +```toml +[[box.winrm]] +display = "winrm" +port = 5985 +credlists = ["windows_users.credlist"] +encrypted = false # Use HTTPS +badattempts = 2 + + [[box.winrm.command]] + command = "hostname" + output = "DC01" + useregex = false +``` + +**Default port:** 80 (unencrypted) or 443 (encrypted) + +#### RDP Check + +Remote Desktop Protocol connectivity check. + +```toml +[[box.rdp]] +display = "rdp" +port = 3389 +``` + +**Default port:** 3389 + +#### VNC Check + +VNC connectivity check. + +```toml +[[box.vnc]] +display = "vnc" +port = 5900 +``` + +**Default port:** 5900 + +#### SMB Check + +SMB share access with optional file verification. + +```toml +[[box.smb]] +display = "smb" +port = 445 +credlists = ["domain_users.credlist"] +domain = "MYDOMAIN" +share = "\\\\server\\share" + + [[box.smb.file]] + name = "important.txt" + regex = "secret data" # Content regex (optional) + hash = "abc123..." # SHA256 hash (optional, mutually exclusive with regex) +``` + +**Default port:** 445 +**Note:** If no credlists specified, uses guest authentication. + +#### FTP Check + +FTP login with optional file retrieval. + +```toml +[[box.ftp]] +display = "ftp" +port = 21 +credlists = ["ftp_users.credlist"] + + [[box.ftp.file]] + name = "/pub/readme.txt" + regex = "Welcome" # Content regex (optional) + hash = "abc123..." # SHA256 hash (optional) +``` + +**Default port:** 21 +**Note:** If no credlists specified, uses anonymous login. + +#### SMTP Check + +Send test email via SMTP. + +```toml +[[box.smtp]] +display = "smtp" +port = 25 +credlists = ["mail_users.credlist"] +domain = "@example.com" # Appended to usernames +encrypted = false # Use TLS +requireauth = false # Force authentication even if not advertised +``` + +**Default port:** 25 + +#### IMAP Check + +IMAP mailbox access check. + +```toml +[[box.imap]] +display = "imap" +port = 143 +credlists = ["mail_users.credlist"] +encrypted = false # Use TLS +``` + +**Default port:** 143 + +#### POP3 Check + +POP3 mailbox access check. + +```toml +[[box.pop3]] +display = "pop3" +port = 110 +credlists = ["mail_users.credlist"] +encrypted = false # Use TLS +``` + +**Default port:** 110 + +#### LDAP Check + +LDAP authentication check. + +```toml +[[box.ldap]] +display = "ldap" +port = 636 +credlists = ["domain_users.credlist"] +domain = "example.com" # Domain for user@domain format +encrypted = true # Use LDAPS +``` + +**Default port:** 636 + +#### SQL Check + +MySQL database connectivity and query verification. + +```toml +[[box.sql]] +display = "mysql" +port = 3306 +credlists = ["db_users.credlist"] +kind = "mysql" # Database type + + [[box.sql.query]] + database = "production" + command = "SELECT version()" + output = "8.0" # Expected output (optional) + useregex = false +``` + +**Default port:** 3306 +**Default kind:** mysql + +#### Custom Check + +Execute custom scripts or binaries. + +```toml +[[box.custom]] +display = "mycheck" +command = "/app/checks/mycheck.sh ROUND TARGET TEAMIDENTIFIER USERNAME PASSWORD" +credlists = ["users.credlist"] +regex = "SUCCESS" # Output regex for success (optional) +``` + +**Placeholders:** ROUND, TARGET, TEAMIDENTIFIER, USERNAME, PASSWORD + ## Contributing Please fork the repository and submit a pull request. For major changes, please open an issue first to discuss what you would like to change. diff --git a/config/credlists/linux.credlist.example b/config/credlists/linux.credlist.example new file mode 100644 index 00000000..f81493f4 --- /dev/null +++ b/config/credlists/linux.credlist.example @@ -0,0 +1,3 @@ +admin,changeme123 +user1,password1 +user2,password2 diff --git a/config/credlists/windows.credlist.example b/config/credlists/windows.credlist.example new file mode 100644 index 00000000..6ef6ed9b --- /dev/null +++ b/config/credlists/windows.credlist.example @@ -0,0 +1,3 @@ +administrator,changeme123 +domainuser1,password1 +domainuser2,password2 diff --git a/config/event.conf.example b/config/event.conf.example index a3b11e1c..99caf38b 100644 --- a/config/event.conf.example +++ b/config/event.conf.example @@ -1,7 +1,20 @@ +# =========================================== +# EXAMPLE CONFIGURATION - REFERENCE ONLY +# =========================================== +# This is a reference example showing all available check types. +# To use this config, you must: +# 1. Create credlist files in config/credlists/ (e.g., linux.credlist, windows.credlist) +# 2. Set team identifiers in the admin UI to replace "_" in IPs +# 3. Have actual infrastructure running at the configured IPs +# 4. Create any custom check scripts referenced in Box.Custom +# +# See README.md for full documentation. +# =========================================== + [RequiredSettings] - EventName = "Example" + EventName = "Example Competition" EventType = "rvb" - DBConnectURL = "postgres://engineuser:postgres_password@quotient_database:5432/engine" + DBConnectURL = "postgres://engineuser:your_password@quotient_database:5432/engine" BindAddress = "0.0.0.0" [MiscSettings] @@ -9,64 +22,196 @@ ShowDebugToBlueTeam = false LogoImage = "/static/assets/quotient.svg" StartPaused = true - Delay = 30 - Jitter = 5 - Points = 1 - Timeout = 15 + + Delay = 60 + Jitter = 10 + + Points = 5 + Timeout = 30 SlaThreshold = 5 - SlaPenalty = 5 + SlaPenalty = 25 [CredlistSettings] [[CredlistSettings.Credlist]] - CredlistName = "Users" - CredlistPath = "users.credlist" + CredlistName = "LinuxUsers" + CredlistPath = "linux.credlist" CredlistExplainText = "username,password" + [[CredlistSettings.Credlist]] - CredlistName = "Admins" - CredlistPath = "admins.credlist" + CredlistName = "WindowsUsers" + CredlistPath = "windows.credlist" CredlistExplainText = "username,password" +# =========================================== +# USER ACCOUNTS +# =========================================== [[Admin]] Name = "admin" - Pw = "admin" + Pw = "changeme" [[Team]] - Name = "team01" - Pw = "password" + Name = "team1" + Pw = "changeme" [[Team]] - Name = "team02" - Pw = "password" + Name = "team2" + Pw = "changeme" -[[Team]] - Name = "team03" - Pw = "password" +[[Red]] + Name = "redteam" + Pw = "changeme" -[[Box]] - Name = "leto" - IP = "172.19.12_.22" +[[Inject]] + Name = "inject" + Pw = "changeme" - [[Box.Tcp]] - Display = "cyclopes" - Port = 4444 +# =========================================== +# BOX DEFINITIONS +# =========================================== +# ---- Linux Web Server ---- [[Box]] - Name = "atlas" - IP = "172.19.12_.25" + Name = "web-server" + IP = "10.100._.10" - [[Box.Custom]] - Display = "cassandra" - CredLists = ["Users", "Admins"] - Command = "/app/checks/example.sh ROUND TARGET TEAMIDENTIFIER USERNAME PASSWORD" + [[Box.Ping]] + Display = "ping" + [[Box.Web]] + Display = "https" + Scheme = "https" + Port = 443 + [[Box.Web.Url]] + Path = "/" + Status = 200 + Regex = "[^<]*Dashboard[^<]*" + [[Box.Web.Url]] + Path = "/api/health" + Status = 200 + Regex = "\"status\":\\s*\"(ok|healthy)\"" + + [[Box.Ssh]] + Display = "ssh" + CredLists = ["LinuxUsers"] + [[Box.Ssh.Command]] + Command = "cat /etc/hostname" + Contains = true + Output = "web" + +# ---- Mail Server ---- [[Box]] - Name = "titan" - IP = "172.19.12_.27" + Name = "mail-server" + IP = "10.100._.11" [[Box.Smtp]] - Display = "marpessa" - CredLists = ["Users"] + Display = "smtp" + Port = 25 + CredLists = ["LinuxUsers"] + Domain = "@example.local" + RequireAuth = true + + [[Box.Imap]] + Display = "imap" + Port = 143 + CredLists = ["LinuxUsers"] + + [[Box.Pop3]] + Display = "pop3" + Port = 110 + CredLists = ["LinuxUsers"] + +# ---- DNS Server ---- +[[Box]] + Name = "dns-server" + IP = "10.100._.2" + + [[Box.Dns]] + Display = "dns" + Port = 53 + [[Box.Dns.Record]] + Kind = "A" + Domain = "www.team_.example.local" + Answer = ["10.100._.10"] + [[Box.Dns.Record]] + Kind = "MX" + Domain = "team_.example.local" + Answer = ["mail.team_.example.local"] + +# ---- Database Server ---- +[[Box]] + Name = "db-server" + IP = "10.100._.12" [[Box.Tcp]] - Port = 4444 - Display = "daphne" + Display = "mysql-port" + Port = 3306 + + [[Box.Sql]] + Display = "mysql" + Port = 3306 + Kind = "mysql" + CredLists = ["LinuxUsers"] + [[Box.Sql.Query]] + Database = "production" + Command = "SELECT 1" + Output = "1" + +# ---- Windows Domain Controller ---- +[[Box]] + Name = "dc01" + IP = "10.100._.1" + + [[Box.Ldap]] + Display = "ldap" + Port = 636 + CredLists = ["WindowsUsers"] + Domain = "example.local" + Encrypted = true + + [[Box.WinRM]] + Display = "winrm" + Port = 5985 + CredLists = ["WindowsUsers"] + [[Box.WinRM.Command]] + Command = "hostname" + Output = "DC01" + + [[Box.Rdp]] + Display = "rdp" + Port = 3389 + +# ---- File Server ---- +[[Box]] + Name = "file-server" + IP = "10.100._.13" + + [[Box.Ftp]] + Display = "ftp" + Port = 21 + CredLists = ["LinuxUsers"] + [[Box.Ftp.File]] + Name = "/pub/readme.txt" + Regex = "Welcome" + + [[Box.Smb]] + Display = "smb" + Port = 445 + CredLists = ["WindowsUsers"] + Share = "shared" + [[Box.Smb.File]] + Name = "important.txt" + +# ---- Custom Check Example ---- +[[Box]] + Name = "app-server" + IP = "10.100._.14" + + [[Box.Custom]] + Display = "api-health" + CredLists = ["LinuxUsers"] + Command = "/app/checks/api-check.sh ROUND TARGET TEAMIDENTIFIER USERNAME PASSWORD" + Regex = "SUCCESS" + + [[Box.Vnc]] + Display = "vnc" + Port = 5900 diff --git a/custom-checks/example.sh b/custom-checks/example.sh index f5f0d1f2..db8e0992 100755 --- a/custom-checks/example.sh +++ b/custom-checks/example.sh @@ -1,6 +1,7 @@ #!/bin/sh # Usage: ./example.sh
+# Exit 0 for success, non-zero for failure ROUND="$1" ADDRESS="$2" @@ -8,8 +9,14 @@ TEAM_ID="$3" USERNAME="$4" PASSWORD="$5" -echo "$ROUND" "$ADDRESS" "$TEAM_ID" "$USERNAME" "$PASSWORD" +# Log inputs for debugging (visible in admin interface) +echo "Round: $ROUND, Target: $ADDRESS, Team: $TEAM_ID" -ping -c2 -W2 -w2 "$ADDRESS" && return 0 +# Perform the check +if ping -c2 -W2 -w2 "$ADDRESS" > /dev/null 2>&1; then + echo "SUCCESS: Host $ADDRESS is reachable" + exit 0 +fi -return 1 +echo "FAILED: Host $ADDRESS is not reachable" +exit 1 diff --git a/docs/custom-checks.md b/docs/custom-checks.md index a97bfee3..60d57625 100644 --- a/docs/custom-checks.md +++ b/docs/custom-checks.md @@ -1,27 +1,41 @@ # Writing Custom Score Checks -This guide explains how to create and run your own service checks. Custom checks are shell scripts or binaries executed by the runner container. They return an exit status of `0` when the service is considered up and non–zero when it is down. +This guide explains how to create and run your own service checks. Custom checks are shell scripts or binaries executed by the runner container. They return an exit status of `0` when the service is considered up and non-zero when it is down. ## Command Format Custom checks are defined under a `[[box.custom]]` section in `event.conf`. The `command` field contains the command to run. The runner replaces placeholders before executing the command: -- `ROUND` – the current round number -- `TARGET` – hostname or IP address for the service -- `TEAMIDENTIFIER` – the team's unique identifier -- `USERNAME` – a username pulled from any configured credlists -- `PASSWORD` – the corresponding password +| Placeholder | Description | +|-------------|-------------| +| `ROUND` | Current round number | +| `TARGET` | Hostname or IP address (with team identifier substituted) | +| `TEAMIDENTIFIER` | The team's unique identifier (e.g., "01") | +| `USERNAME` | A username from the configured credlists | +| `PASSWORD` | The corresponding password | Example configuration: ```toml [[box.custom]] +display = "myservice" command = "/app/checks/example.sh ROUND TARGET TEAMIDENTIFIER USERNAME PASSWORD" credlists = ["web01.credlist", "users.credlist"] -regex = "example [Tt]ext" +regex = "SUCCESS" ``` -The runner mounts the contents of `./custom-checks` at `/app/checks/` inside the container. Place your script in that directory and ensure it is executable. +### Configuration Options + +| Option | Description | +|--------|-------------| +| `command` | The command to execute (required) | +| `credlists` | Array of credlist names for USERNAME/PASSWORD substitution | +| `regex` | Regular expression to match against output for success (optional) | +| `display` | Check name suffix (default: "custom") | +| `points` | Points for this check (inherits from global if not set) | +| `timeout` | Check timeout in seconds (inherits from global if not set) | + +The runner mounts the contents of `./custom-checks` at `/app/checks/` inside the container. Place your script in that directory and ensure it is executable (`chmod +x`). ## Writing the Script @@ -37,20 +51,98 @@ USERNAME="$4" PASSWORD="$5" # Implement your logic here -ping -c2 -W2 -w2 "$ADDRESS" && exit 0 +if ping -c2 -W2 -w2 "$ADDRESS" > /dev/null 2>&1; then + echo "SUCCESS: Host $ADDRESS is reachable" + exit 0 +fi + +echo "FAILED: Host $ADDRESS is not reachable" exit 1 ``` -Your script can print output to stdout or stderr to help with debugging. This output is captured and visible from the admin interface when a check fails. +### Output Matching + +If the `regex` option is set, the check only passes if: +1. The script exits with code 0, AND +2. The output matches the regular expression + +Without `regex`, only the exit code matters. + +Your script can print output to stdout or stderr for debugging. This output is captured and visible from the admin interface. ## Environment and Dependencies -Runner containers are built from `Dockerfile.runner`. Python dependencies can be listed in `custom-checks/requirements.txt`. Additional packages may be installed by editing `Dockerfile.runner` and rebuilding the runner with `docker-compose build runner`. +Runner containers are built from `Dockerfile.runner`. + +### Python Dependencies + +List Python packages in `custom-checks/requirements.txt`: + +``` +requests +paramiko +``` + +These are automatically installed when the runner container builds. + +### System Packages + +For additional Alpine packages, edit `Dockerfile.runner`: + +```dockerfile +RUN apk add --no-cache curl netcat-openbsd nmap +``` + +Then rebuild the runner: + +```bash +docker-compose build runner +docker-compose up -d runner +``` ## Recommendations -- Keep the check fast. Rounds may have short timeouts (default is 15 s). -- Avoid infinite loops. The runner will forcibly stop the command when the timeout is reached. -- Log helpful details on failure to ease troubleshooting. +- **Keep checks fast.** The default timeout is half the round delay (e.g., 30s for a 60s delay). Checks that exceed the timeout are marked as failed. +- **Avoid infinite loops.** The runner forcibly kills the command when the timeout is reached. +- **Use meaningful output.** Print context on both success and failure to help with debugging. +- **Handle escaped credentials.** USERNAME and PASSWORD are shell-escaped by the runner. If your script passes them to other tools, ensure those tools handle quoted strings correctly. +- **Test locally first.** Run your script manually before deploying to catch errors early. + +## Examples + +### HTTP Check with curl + +```sh +#!/bin/sh +ADDRESS="$2" +USERNAME="$4" +PASSWORD="$5" + +response=$(curl -s -o /dev/null -w "%{http_code}" -u "$USERNAME:$PASSWORD" "http://$ADDRESS/api/health") +if [ "$response" = "200" ]; then + echo "SUCCESS: API returned 200" + exit 0 +fi + +echo "FAILED: API returned $response" +exit 1 +``` + +### Database Check + +```sh +#!/bin/sh +ADDRESS="$2" +USERNAME="$4" +PASSWORD="$5" + +if mysql -h "$ADDRESS" -u "$USERNAME" -p"$PASSWORD" -e "SELECT 1" > /dev/null 2>&1; then + echo "SUCCESS: Database connection successful" + exit 0 +fi + +echo "FAILED: Database connection failed" +exit 1 +``` -For more advanced examples, see `custom-checks/example.sh` in this repository. +For more examples, see `custom-checks/example.sh` in this repository. From beb3826998157ff6a236f34871a6b00c16471769 Mon Sep 17 00:00:00 2001 From: tirefire <84106878+tire-fire@users.noreply.github.com> Date: Sun, 1 Feb 2026 17:58:11 -0600 Subject: [PATCH 2/2] Fix link format for PRCCDC in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e19162c..2bcf1691 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Quotient is a cybersecurity competition scoring platform designed for CCDC-style events. It automatically scores defensive service checks while providing infrastructure for teams to submit inject solutions and make password change requests (PCRs). -Used by [WRCCDC](https://wrccdc.org) (Western Regional CCDC) and [PRCCDC](https://prccdc.com) (Pacific Rim CCDC). +Used by [WRCCDC](https://wrccdc.org) (Western Regional CCDC) and [PRCCDC](http://prccdc.com) (Pacific Rim CCDC). ## Prerequisites