Skip to content

Lab Scenarios ​

The portal serves four scenarios. Students see them as Scenario 1 to 4; the backend knows them by their internal IDs 01, 06, 09 and 11, which are used for routing, provisioning and scoring (displayNumber vs id in cyberrange/portal/src/hooks/useScenarios.ts).

ExplainerHow scoring works1:26

Catalog ​

ScenarioInternal IDTypeDifficultyLab time limitMITRE (portal)Scored onTasksPoints
1. Network Reconnaissance & Exploitation01OffensiveEasy30 minT1046Kali4 auto + 1 flag275
2. SQL Injection & Reflected XSS06OffensiveMedium45 minT1190Browser (DVWA proxy), Kali fallback4 auto + 1 flag350
3. SIEM Alert Triage09DefensiveMedium45 minT1595metaunscored prep + 3 auto225
4. Vulnerability Hardening11DefensiveHard60 minT1548meta3 auto200
  • The time limit is real: the reaper ends the lab when it runs out. It comes from SCENARIO_TTL_MINUTES in cyberrange/src/provisioning/config.py and matches the card duration from the difficulty level in components/scenarios/DifficultyBadge.tsx (test_limits_match_portal_catalog_durations fails if they drift). An unknown scenario gets DEFAULT_TTL_MINUTES (60).
  • "Flag" tasks score on a correct flag typed into the lab page; "auto" tasks score from evidence in the lab (see below). A Scenario 0 orientation page (/scenario/orientation) explains the lab layout and scoring before Scenario 1; it starts no lab and has no score.
  • Every scenario runs in the same three-machine lab (Kali, meta, DVWA). What changes is the guide, which machine is scored and which checks run.
  • The catalog order is the recommended order: attack (1, 2), then defend (3, 4). Scenario 4 fixes the weakness Scenario 1 exploits.

Where a scenario is defined ​

There is no single scenario file. A scenario is the combination of these pieces, all keyed by the internal ID:

PieceFileWhat it holds
Catalog entrycyberrange/portal/src/hooks/useScenarios.tsName, type, difficulty, MITRE tag, milestone names, descriptions and points, guide file
Student guidecyberrange/portal/public/scenarios/scenario_0N_*.mdWalkthrough; the lab shows only the current task's ### Task N section, the PDF export has the whole guide
Lab surface and cueslabSurface, prepTask, cue, cueVariant in useScenarios.tsWhich right-hand pane the lab shows (terminal, dvwa or siem) and the per-task side cues
Prerequisites and hintscyberrange/portal/src/lib/scenarioPrereqs.ts, lib/scenarioHints.tsThe "Before you start" box on the scenario page and the per-task progressive hints (keyed by internal ID; kept in step with the guide by hand)
Scoring targetSCENARIO_TARGETS in cyberrange/src/provisioning/ssh_verifier.pyWhich container the check runs in (kali or meta)
Milestone checkscheck_scenario_<id>() in cyberrange/src/provisioning/scoring_checks.shThe PASS/FAIL logic for each milestone
Poller milestone countSCENARIO_MILESTONE_COUNTS in score_poller.pyHow many milestones the automatic poller checks (flag tasks are not polled)
Flag-only tasksPURE_FLAG_MILESTONES in hybrid_scoring.pyTasks that score on a correct flag alone: (1, 5) and (6, 5)
Time limitSCENARIO_TTL_MINUTES in config.pyMinutes before the reaper ends the lab
Browser signals (Scenario 2 only)cyberrange/portal/src/lib/dvwaScoreSignals.tsResponse patterns that prove each DVWA milestone
Rubricsschema/v5.sql and v10.sql seeds, and DEFAULT_RUBRICS in rubrics.pyCriteria, points, MITRE technique and NIST phase per milestone
SIEM detection evidenceDETECTION_RULES in wazuh_rule_map.pyOptional Wazuh rule that adds detection evidence to a check
Per-student flagsprepare_scenario_flags() and plant_scenario_flags() in flag_planting.pyWhere each milestone's flag is hidden in the lab; Scenario 1's Task 5 flag is planted on Kali and a failure there fails the provision
Failure diagnosticsREASON_CODES_BY_SCENARIO in scoring_diagnostics.py, the DIAG line in scoring_checks.shInstructor-only reason codes for a failed check (see below)

Adding a scenario

A new scenario needs an entry in every row above that applies, a golden-image change if it needs new software, and tests. test_sec01_rbac_matrix.py is not affected because scenarios do not add routes.

Scenario lifecycle ​

StageWhat happensSource
DefinitionThe pieces in the table above ship with the code releasesee above
ProvisioningAdmission checks, then the background build; the lab row stores scenario_idProvisioning & Teardown
InteractionTwo-pane lab page: the exercise panel (task outline, goals, score status, walkthrough) on the left; the terminal tabs, embedded DVWA pane or terminal plus SIEM modal on the rightLab Experience
ScoringBehavioral checks run in the scored container; results are rows in milestone_verificationbelow
CompletionThe portal marks the scenario complete when every task has a PASS; points are summed once per passed milestone (earned_points() in pods_router.py)pods_router.py, useScenarioMilestones.ts
TeardownContainers and network are deleted; scores are notProvisioning & Teardown

Scoring workflow ​

Scoring paths ​

PathWho triggers itWhat decides PASSWritesStatus
Automatic pollerAPI background task every SCORE_POLL_INTERVAL_SECONDS (3)scoring_checks.sh in the scored containera row per check, including FAILImplemented
Manual CheckStudent, from the exercise panelsame script, run immediatelya row per check; a definitive PASS or FAIL also records the one attempt in manual_check_attemptsImplemented, one per task: after a failed check the task is locked to instructor review
Browser evidencePortal DVWA proxy (Scenario 2 only)response patterns in dvwaScoreSignals.tsone PASS row per milestoneImplemented
Flag-only taskFlagSubmission in the exercise panel → POST /progress/{scenario}/flaga correct per-student flag alone (PURE_FLAG_MILESTONES: Scenario 1 Task 5, Scenario 2 Task 5)one PASS row marked pure_flag_capture; a wrong flag scores nothing and opens no caseImplemented
Hybrid flag (other milestones)POST /progress/{scenario}/flagflag and lab state must agree; disagreement escalates a staff-only SCORING_CONFLICTPASS row when both agreeAPI only: the portal shows the flag box only on flag-only tasks
Instructor approvalInstructor or admin resolving a milestone-scoped case (conflict, written report or manual review) as APPROVEDhuman judgmentPASS row marked instructor_approved_conflict or instructor_approved_review (deduplicated); changing the decision to REJECTED or RETRY deletes that instructor-granted row againImplemented
Written report or manual reviewStudent ("Ask an instructor", or automatically after a failed Manual Check), graded by staffhuman judgmentreview status, 0 to 100 score and feedback; approval credits the milestone as aboveImplemented

Sources: score_poller.py, scoring.py, ssh_verifier.py, scoring_checks.sh, hybrid_scoring.py, pods_router.py (browser_score, resolve_student_review, verify_milestone_route, manual_check_locked_ids), schema/v10.sql, cyberrange/portal/src/components/scenario/FlagSubmission.tsx, cyberrange/portal/src/app/lab/dvwa/[[...path]]/route.ts.

Properties worth knowing ​

  • Checks read evidence, not intent. Most checks look for a matching command in shell history (flushed after every command by enable_history_flush.sh), a file with the right content, or live system state. A typed command counts only where the check also confirms something real (for example a working route to the lab subnet for Scenario 1, or the HTTP response code for Scenario 4).
  • Checks are tied to the student's own lab. Scenario 1 derives the lab subnet from the container's own eth0 address, so scanning another lab's subnet does not count.
  • Placeholders do not score. Scenario 2's Kali fallback checks for Milestones 1 and 3 ignore history lines containing angle-bracket placeholders or echo/printf (history_has_real_line()); Scenario 3 rejects unedited PASTE… skeleton text, literal HH:MM and the guide's fictional example, and its Task 1 and Task 2 timestamps must match a real invalid-user SSH event in meta's auth.log (±5 minutes).
  • An automatic PASS is permanent. Once a milestone has a PASS row the poller stops checking it, and later changes in the lab cannot remove it. The only PASS rows that can be taken back are instructor-granted ones, when the instructor changes the decision. Students can erase their own rows for a scenario with the reset (DELETE /progress/{scenario}), which is offered once every task is complete; the reset also clears their used Manual Check attempts for that scenario.
  • The Manual Check lock is per student, not per lab. It is keyed by student, scenario and milestone, so it survives a page refresh, a new lab and a new sign-in. An infrastructure ERROR or UNKNOWN does not use up the attempt.
  • Growth. Every automatic check of an unpassed milestone writes one milestone_verification row and one audit_log row, and GET /pods/{id}/milestones returns the full history. See Findings.
  • Inputs are shape-checked. Since pull request 277 the flag submission body and the portal-to-API browser scoring bodies reject unknown fields, and the browser bodies also bound student_id (same pattern as the student-facing models), the IDs and the label length before any handler runs (FlagSubmissionRequest in models.py; BrowserScoreIn, BrowserDiagnosticIn in pods_router.py).

Scoring diagnostics (instructor only) ​

Pull requests 242, 260, 261, 262 and 267 added a way for staff to see why a check failed without changing how it scores. Students still see only the generic result.

  • For Scenarios 1, 3 and 4 (check_scenario_1, _9, _11) the check prints one line, DIAG reason=<CODE> location=<LOC> copies=<N>, before its PASS/FAIL token. The verifier still reads only the last line, and PASS is printed only when the reason is NONE. For Scenario 2 the portal DVWA proxy sends a near-miss code instead (browserNearMiss() in dvwaScoreSignals.ts).
  • Codes come from a fixed list per scenario (REASON_CODES_BY_SCENARIO), for example COMMAND_NOT_FOUND, SESSION_NOT_LIVE (Scenario 1), AGENT_MISMATCH, TIMESTAMP_NOT_MATCHED (Scenario 3), DEFAULT_PASSWORD_PRESENT, OLD_CREDS_ACCEPTED (Scenario 4) and SECURITY_NOT_LOW, UNION_NO_HASH (Scenario 2). Anything outside the list is dropped; no file contents, commands or payloads are stored.
  • Rows go to the scoring_diagnostics table (schema/v14.sql): one row per student, lab and milestone, updated on repeat failures and replaced when the milestone passes, so polling does not grow it. The score poller purges rows older than the retention period (default 14 days) at most hourly.
  • It is off by default and switched on per scorer ID with SCORING_DIAGNOSTICS_SCENARIOS (for example 9 or 1,6,9,11; S3_DIAGNOSTICS_ENABLED=1 still turns on 9), with SCORING_DIAGNOSTICS_RETENTION_DAYS for retention (cyberrange/env/oci-12gib.env.example). Recording is best effort and never blocks a score.
  • Staff read them with GET /instructor/students/{student_id}/diagnostics (instructor or admin only), shown on the instructor's student page (components/instructor/ScoringDiagnostics.tsx).

Sources: scoring_diagnostics.py, scoring.py, ssh_verifier.py (verify_milestone_detailed), score_poller.py, pods_router.py, scoring_checks.sh.

Safety model shared by all scenarios ​

  • Offensive tools run only inside the student's own lab network; labs are not peered with each other (see Containers & Networks).
  • Vulnerable services (Tomcat Manager with a default credential, DVWA at security level Low) exist only in lab containers created from golden images and are destroyed with the lab.
  • DVWA is reachable only through the session-gated portal proxy, which allows only the pages the scenario needs.
  • Students have sudo inside their own unprivileged containers, never on the host.
  • Labs expire when their scenario's time limit runs out (30 to 60 minutes).

Cyber Range capstone documentation (Capstone 2 · Group 2 · A4102). Documents main at commit 6971373.