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 (Western Regional CCDC) and PRCCDC (Pacific Rim CCDC).
Ensure you have the following installed on your system:
- Docker
- Docker Compose
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 --detachThe 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 |
Create a .env file with the following required variables:
POSTGRES_USER=engineuser
POSTGRES_PASSWORD=<your-db-password>
POSTGRES_DB=engine
POSTGRES_HOST=quotient_database
REDIS_PASSWORD=<your-redis-password>
REDIS_HOST=quotient_redisOptional variables:
LDAP_BIND_PASSWORD- LDAP bind password (alternative to config file)
- Check logs:
docker-compose logsordocker-compose logs <service> - Verify
.envfile has all required variables set - Ensure
config/event.confexists and is valid TOML - For Redis memory warnings:
sudo sysctl vm.overcommit_memory=1(or addvm.overcommit_memory = 1to/etc/sysctl.conf) - Rebuild runners after modifying
Dockerfile.runner:docker-compose build runner && docker-compose up -d runner
After starting the engine:
- Log in as admin
- Navigate to the Admin UI
- Set the Identifier for each team (the unique part of target addresses, e.g.,
01for team 1) - Mark teams as Active to begin scoring
- How to Create Configuration File
- Configuration Sections
The config directory contains the configurations for the scoring engine. The primarily uses a TOML file to configure the engine. The configuration file is broken up into sections.
/quotient
└── config
├── certs/
├── credlists/
├── injects/
├── scoredfiles/
├── COOKIEKEY
└── event.conf
The configuration file is a TOML file that is used to configure the scoring engine. The configuration file is located in the ./config directory and is named event.conf. COOKIEKEY is auto-generated and is used to encrypt the session cookie. The certs directory is used to store any SSL certificates that are used by the scoring engine such as potential LDAPS certificates for the Docker container (since it won't inherit from the system). The injects directory is used to store any files that are uploaded for inject definitions (note: inject submissions will go in /submissions). The scoredfiles directory is used to store any files that are uploaded for scoring purposes (like SSH private keys).
The configuration file is broken up into sections. Only the RequiredSettings section is required. The other sections are optional and can be omitted if not needed.
Cred lists need to be CSVs specified in the ./config/credlists directory with a .credlist extension. When password change requests (PCRs) get processed, credlists will only be mutated by changing the password column of an existing user in the defined list. This means submitting a PCR with a user that does not exist will ignore that specific entry. Below is an example. See the below configuration examples to specify credlists for checks. You will have to map the files to a credlist name in a top-level config section for each credlist.
# example contents of a .credlist file
joe,s3cret
robby,mypass
johndoe,helloworld
# Example top-level credlist config
[CredlistSettings]
[[CredlistSettings.Credlist]]
CredlistName = "DomainUsersWindows"
CredlistPath = "domain_users_windows.credlist"
CredlistExplainText = "username,password"
[[CredlistSettings.Credlist]]
CredlistName = "DomainUsersLinux"
CredlistPath = "domain_users_linux.credlist"
CredlistExplainText = "username,password"
[[CredlistSettings.Credlist]]
CredlistName = "SQLUsers"
CredlistPath = "sql_users.credlist"
CredlistExplainText = "username,password"
They should be specified for each check that requires a credlist. The credlists field expects an array of strings of the exact file name of credlist to be used.
[[box]]
name = "example"
ip = "10.100.1_.2"
[[box.ssh]]
credlists = ["web01.credlist",]
[[box.custom]]
command = "/app/checks/example.sh ROUND TARGET TEAMIDENTIFIER USERNAME PASSWORD"
credlists = ["web01.credlist","users.credlist"]
regex = "example [Tt]ext"
[RequiredSettings]
EventName = "Name of my Competition"
EventType = "rvb" # Use "rvb" for Red vs Blue (CCDC-style)
DBConnectURL = "postgres://engineuser:password@quotient_database:5432/engine"
BindAddress = "0.0.0.0"EventType: Uservbfor Red vs Blue competitions (CCDC-style). Thekothoption exists but is not fully implemented.DBConnectURL: Can be omitted if using environment variables (POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_HOST,POSTGRES_DB).BindAddress: Use0.0.0.0when deploying in Docker.
[LdapSettings]
LdapConnectUrl = "ldap://ldap.yournet.org:389"
LdapBindDn = "CN=Scoring Engine Service Account,OU=service accounts,DC=yournet,DC=org"
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, add the certificate to ./config/certs so it gets added to the container's certificate store.
[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[SslSettings]
HttpsCert = "/app/config/certs/server.crt"
HttpsKey = "/app/config/certs/server.key"When SSL is configured, the default port changes to 443.
[MiscSettings]
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"
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)[UISettings]
EnablePublicGraphs = false # Allow unauthenticated graph viewing
DisableGraphsForBlueTeam = true # Hide graphs from teams
AllowNonAnonymizedGraphsForBlueTeam = false # Show team names on graphs
ShowAnnouncementsForRedTeam = true # Red team sees announcementsDefine local users for each role:
# Admin users - full engine control
[[admin]]
name = "admin"
pw = "password"
# Team users - blue team competitors
[[team]]
name = "Team01"
pw = "password"
[[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"The IP address of the target box should be the IP the scoring engine will use. To templatize the IP address, use an underscore _ in place of the part of the IP address that will be unique per team. This is the "Identifier" that you must specify through the Admin UI per team. The scoring engine will replace the underscore with the "Identifier" to create the unique target address for each team. If the target should use a DNS name, you can specify that by setting ip field to the DNS name (which will be used for all checks under the box) or using the target field at the individual check level. Template the DNS name with an underscore _ in place of the part of the DNS name that will be unique per team.
It is recommended to use Quotient with aweful-dns running on the same host.
[[box]]
name = "web01"
ip = "10.100.1_.2"
# ip = "team_.example.tld"Each service check is defined beneath a box.
[[box]]
name = "web01"
ip = "10.100.1_.2"
[[box.web]] # type of check you want
display = "web01" # name of the check that gets appended to the box name
target = "example.team_.tld" # e.g. this will resolve to example.team01.tld with aweful-dns
port = 8080
[[box.web.url]] # some checks have components you need to include
path = "/index.html"
[[box.web.url]]
path = "/admin"
status = 403Custom 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.
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 |
Simple ICMP ping check.
[[box.ping]]
display = "ping"
# No additional options requiredDefault port: N/A
Verify TCP port connectivity.
[[box.tcp]]
display = "ssh-port"
port = 22Default port: None (required)
Query DNS records and verify answers.
[[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
HTTP/HTTPS request with optional status code and content matching.
[[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 = 403Default port: 80 (http) or 443 (https) Default scheme: http
SSH login with optional command execution.
[[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 stringDefault port: 22
Windows Remote Management check with optional PowerShell commands.
[[box.winrm]]
display = "winrm"
port = 5985
credlists = ["windows_users.credlist"]
encrypted = false # Use HTTPS
badattempts = 2
[[box.winrm.command]]
command = "hostname"
output = "DC01"
useregex = falseDefault port: 80 (unencrypted) or 443 (encrypted)
Remote Desktop Protocol connectivity check.
[[box.rdp]]
display = "rdp"
port = 3389Default port: 3389
VNC connectivity check.
[[box.vnc]]
display = "vnc"
port = 5900Default port: 5900
SMB share access with optional file verification.
[[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 login with optional file retrieval.
[[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.
Send test email via SMTP.
[[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 advertisedDefault port: 25
IMAP mailbox access check.
[[box.imap]]
display = "imap"
port = 143
credlists = ["mail_users.credlist"]
encrypted = false # Use TLSDefault port: 143
POP3 mailbox access check.
[[box.pop3]]
display = "pop3"
port = 110
credlists = ["mail_users.credlist"]
encrypted = false # Use TLSDefault port: 110
LDAP authentication check.
[[box.ldap]]
display = "ldap"
port = 636
credlists = ["domain_users.credlist"]
domain = "example.com" # Domain for user@domain format
encrypted = true # Use LDAPSDefault port: 636
MySQL database connectivity and query verification.
[[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 = falseDefault port: 3306 Default kind: mysql
Execute custom scripts or binaries.
[[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
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.
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
For support or questions, please open a GitHub issue.