admin / AirportCyberSimulator
publicAirport Cyber Attack Simulation Application
AirportCyberSimulator / airport-cyber-sim-v2 / docs / build_playguide.py
17481 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | #!/usr/bin/env python3 """ Generates the branded "Player & Facilitator Guide" PDF for the Airport Cyber Resilience Simulator V2 — how to play, strategy, and what it communicates. Reuses the shared design system from build_guide.py. """ import os from build_guide import ( # shared palette, fonts, styles and flowable helpers A4, mm, HexColor, TA_CENTER, ParagraphStyle, BaseDocTemplate, PageTemplate, Frame, Paragraph, Spacer, Table, TableStyle, KeepTogether, NextPageTemplate, PageBreak, BG, PANEL, PANEL2, LINE, TEXT, DIM, GREEN, CYAN, VIOLET, AMBER, RED, WHITE, CODEBG, PAGE_W, PAGE_H, MX, st_body, st_dim, st_h1, st_kicker, st_cover_t, st_cover_s, st_cover_d, st_th, st_td, code_block, step, bullets, info_table, section, S, ACCENTS, ) HEADER_RIGHT = "V2 · PLAYER & FACILITATOR GUIDE" st_lead = S("lead", fontSize=11.5, leading=17, textColor=TEXT) st_quote = S("quote", fontName="Bahn", fontSize=13, leading=18, textColor=CYAN, alignment=TA_CENTER) def callout(title, body, accent=CYAN): inner = [Paragraph(title, S("ct", fontName="SegoeB", fontSize=11, leading=15, textColor=WHITE)), Spacer(1, 3), Paragraph(body, st_body)] t = Table([[inner]], colWidths=[PAGE_W - 2 * MX]) t.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, -1), HexColor("#0a1c26")), ("LINEBEFORE", (0, 0), (0, -1), 3, accent), ("BOX", (0, 0), (-1, -1), 0.5, LINE), ("LEFTPADDING", (0, 0), (-1, -1), 12), ("RIGHTPADDING", (0, 0), (-1, -1), 12), ("TOPPADDING", (0, 0), (-1, -1), 10), ("BOTTOMPADDING", (0, 0), (-1, -1), 10), ])) return KeepTogether([t, Spacer(1, 8)]) def paint_bg(c, doc): c.setFillColor(BG); c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1) c.setFillColor(HexColor("#081726")); c.rect(0, PAGE_H - 26 * mm, PAGE_W, 26 * mm, stroke=0, fill=1) c.setFillColor(GREEN); c.circle(MX + 3, PAGE_H - 15 * mm, 3, stroke=0, fill=1) c.setFont("Bahn", 10); c.setFillColor(WHITE) c.drawString(MX + 10, PAGE_H - 16 * mm, "AIRPORT CYBER RESILIENCE SIMULATOR") c.setFillColor(CYAN); c.drawRightString(PAGE_W - MX, PAGE_H - 16 * mm, HEADER_RIGHT) c.setStrokeColor(LINE); c.setLineWidth(0.6) c.line(MX, PAGE_H - 19 * mm, PAGE_W - MX, PAGE_H - 19 * mm) c.line(MX, 14 * mm, PAGE_W - MX, 14 * mm) c.setFont("Segoe", 8); c.setFillColor(DIM) c.drawString(MX, 10 * mm, "A decision game about resilience, trade-offs and consequence") c.setFont("MonoB", 8); c.setFillColor(GREEN) c.drawRightString(PAGE_W - MX, 10 * mm, f"{doc.page:02d}") def paint_cover(c, doc): c.setFillColor(BG); c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1) c.setStrokeColor(HexColor("#0c2033")); c.setLineWidth(0.5) for x in range(0, int(PAGE_W), 26): c.line(x, 0, x, PAGE_H) for y in range(0, int(PAGE_H), 26): c.line(0, y, PAGE_W, y) cx, cy = PAGE_W / 2, PAGE_H - 82 * mm r = 20 * mm c.setStrokeColor(CYAN); c.setLineWidth(2.4); c.setFillColor(HexColor("#08131f")) p = c.beginPath() p.moveTo(cx, cy + r); p.lineTo(cx + r * 0.8, cy + r * 0.5) p.lineTo(cx + r * 0.8, cy - r * 0.35); p.lineTo(cx, cy - r) p.lineTo(cx - r * 0.8, cy - r * 0.35); p.lineTo(cx - r * 0.8, cy + r * 0.5); p.close() c.drawPath(p, stroke=1, fill=1) # play triangle inside the emblem c.setFillColor(GREEN); c.setStrokeColor(GREEN) pt = c.beginPath() pt.moveTo(cx - 5 * mm, cy + 7 * mm); pt.lineTo(cx - 5 * mm, cy - 7 * mm); pt.lineTo(cx + 8 * mm, cy); pt.close() c.drawPath(pt, stroke=0, fill=1) sw = 18 * mm x0 = cx - (sw * 5) / 2 for i, col in enumerate([GREEN, CYAN, VIOLET, AMBER, RED]): c.setFillColor(col); c.rect(x0 + i * sw, 40 * mm, sw - 2, 4 * mm, stroke=0, fill=1) def build(path): doc = BaseDocTemplate(path, pagesize=A4, leftMargin=MX, rightMargin=MX, topMargin=24 * mm, bottomMargin=18 * mm, title="Airport Cyber Resilience Simulator V2 — Player & Facilitator Guide", author="Airport Cyber Resilience Simulator") frame = Frame(MX, 18 * mm, PAGE_W - 2 * MX, PAGE_H - 24 * mm - 18 * mm, id="main", leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0) cover_frame = Frame(MX, 20 * mm, PAGE_W - 2 * MX, 98 * mm, id="cover") doc.addPageTemplates([ PageTemplate(id="cover", frames=[cover_frame], onPage=paint_cover), PageTemplate(id="content", frames=[frame], onPage=paint_bg), ]) story = [] # ---------- COVER ---------- story.append(Spacer(1, 2 * mm)) story.append(Paragraph("AIRPORT CYBER RESILIENCE SIMULATOR", st_cover_t)) story.append(Spacer(1, 3 * mm)) story.append(Paragraph("VERSION 2 · PLAYER & FACILITATOR GUIDE", st_cover_s)) story.append(Spacer(1, 6 * mm)) story.append(Paragraph("How to play, how to think, and what the experience is designed to " "teach — a companion for visitors at the kiosk and for the facilitators " "running the stand.", st_cover_d)) story.append(NextPageTemplate("content")) story.append(PageBreak()) # ---------- WHAT IT COMMUNICATES ---------- story += section("What this simulation is about", "The message") story.append(Paragraph( "The Airport Cyber Resilience Simulator is not a quiz with a right answer — it is a " "<b>decision game about consequence</b>. You take the seat of an airport's incident " "commander during a live cyber attack and discover, in ninety seconds, that every option " "on the table costs something. The goal is to make the trade-offs <i>felt</i>, not just " "explained.", st_lead)) story.append(Spacer(1, 8)) story.append(Paragraph("Five ideas it is designed to land:", st_dim)) story.append(Spacer(1, 5)) story.append(info_table( [["1 · Small door, big house", "A single unglamorous entry point — a USB stick, a default password, a phishing email — cascades across critical infrastructure."], ["2 · There are no free wins", "Every mitigation trades security against operational capacity and cash. Doing nothing also has a price, ticking up every second."], ["3 · Consequence scales", "Heathrow and a regional airport face the same attack but very different financial and regulatory exposure."], ["4 · Comms is response", "How you talk to the public moves the loss line as sharply as any technical control."], ["5 · Regulation is real", "Each node maps to an NCSC CAF objective and a UK Cyber Security and Resilience Bill duty — the fines are not invented."]], ["Idea", "What the player experiences"], [46 * mm, PAGE_W - 2 * MX - 46 * mm])) # ---------- HOW TO PLAY ---------- story.append(Spacer(1, 10)) story += section("How to play", "The approach — one round, about 2-3 minutes") story.append(step(1, "Set the stakes — choose an airport", "green", [ Paragraph("Pick the airport on the configuration screen. This is a difficulty dial: a " "Tier-1 hub bleeds cash fast and carries maximum statutory fines; a regional " "airport is a gentler first game. Everything downstream scales to this choice.", st_body)])) story.append(step(2, "Choose a threat and start", "cyan", [ Paragraph("Select one of the nine playbooks and press <b>START SIMULATION</b>. Use the " "5-second countdown to orient: find the glowing ingress node and trace the dotted " "links leading away from it — that is the likely blast radius.", st_body)])) story.append(step(3, "Read the board", "violet", [ Paragraph("At zero, the method-of-ingress icon pulses over the entry node, then it turns " "red. Red particles race down the links to the next target on a timer. Watch the " "sidebar: the financial odometer starts climbing white → amber → red, and the " "Live Incident Log narrates every event.", st_body)])) story.append(step(4, "Decide under pressure", "amber", [ Paragraph("Tap a compromised node and pick a mitigation. You cannot save everything — the " "spread timer is running, so choose which node to defend and how. The Regulatory " "Insights tab tells you why a node matters.", st_body)])) story.append(step(5, "Brief the media and the regulators", "red", [ Paragraph("Two duties run in parallel. The Crisis Communications panel opens a 15-second " "proactive window — handle it, silence is expensive. At the same time the " "<b>Notify CAA</b> and <b>Notify DfT</b> buttons light amber if this incident type " "carries a statutory reporting duty; press the ones that apply. Failing to notify a " "required regulator adds an enforcement penalty at the end.", st_body)])) story.append(step(6, "Reach containment and close out", "green", [ Paragraph("Keep isolating or patching until the spread can no longer advance. Once no new " "node is compromised for five seconds the simulation ends with <b>ATTACK " "CONTAINED</b> and the clock and financial bleed freeze. Read the final numbers, " "then press RESET to CONFIG for the next player.", st_body)])) story.append(Spacer(1, 6)) # ---------- STRATEGY ---------- story += section("Strategy", "How to think about the trade-offs") story.append(Paragraph("There is no single winning line, but there is skilful play. Strong " "players triage: they decide what to sacrifice <i>before</i> the spread " "forces their hand.", st_body)) story.append(Spacer(1, 8)) story.append(Paragraph("THE MITIGATION DECISION MATRIX", st_kicker)) story.append(Spacer(1, 5)) story.append(info_table( [["Isolate Node", "The next hop is a crown jewel (identity, border, power, ATC) and you cannot afford the spread.", "Stops spread dead. Accept 0% capacity and maximum operational bleed on that node."], ["Reduce / Throttle", "You want to keep partial service running and buy time to think or to patch elsewhere.", "Slows the spread 80% and holds 40% capacity — a hedge, not a cure."], ["Stop Attack (Patch)", "You can start the 5-second patch early enough to beat the attack to the next node — you will see it closing in as the patch counts down.", "Restores the node to normal — but £50k is spent whether or not you win the race."]], ["Move", "Reach for it when…", "What you are buying"], [30 * mm, (PAGE_W - 2 * MX) * 0.38, None])) story.append(Spacer(1, 10)) story.append(callout("Protect the data-sensitive nodes first", "Identity Store, Passenger Services, Frequent Flyer DB, Border Control API and Ticketing DB " "trigger huge one-off regulatory fines the instant they are breached — scaled by your " "airport. If one of these is the next hop, isolating or patching upstream before it falls is " "usually worth the operational hit.", RED)) story.append(callout("Win the comms game every time", "Releasing a proactive statement in the first 15 seconds halves the reputational multiplier " "(×0.5), which slows the entire financial bleed. Miss it and answer the flashing phone/email " "to hold the baseline (×1.0). Ignore the barrage and a blackout <b>triples</b> it (×3.0) — " "the single most expensive mistake available.", AMBER)) story.append(callout("Mind the clock — and the tempo", "Every scenario has its own spread speed, and it shifts slightly each run. Fast attacks — " "ransomware, signal spoofing, the perimeter breach — usually outrun a patch entirely, so " "isolate instead. Slower, stealthier ones give a patch time to land. Throttling also " "multiplies the remaining delay ×5, which can turn a patch you would have lost into one you " "win. Read the tempo before you commit £50k to a patch.", CYAN)) story.append(callout("Notify the right regulator — the duty matches the breach", "Attacks that touch air traffic, runway or aircraft systems must be reported to the <b>Civil " "Aviation Authority</b>; disruption to critical transport infrastructure and passenger data " "goes to the <b>Department for Transport</b>; the most serious incidents require <b>both</b>. " "The sidebar buttons light up only for the duties this scenario actually triggers — press " "them, or take a per-regulator enforcement penalty at the end. The deck below shows who to " "call for each playbook.", VIOLET)) # ---------- SCENARIO DECK ---------- story.append(Spacer(1, 10)) story += section("The scenario deck", "Nine playbooks, nine lessons") story.append(Paragraph("Each playbook is a self-contained talking point. Rotate them to keep a " "stand fresh and to show how varied the attack surface really is.", st_body)) story.append(Spacer(1, 7)) story.append(info_table( [["1", "The OT Gridlock", "USB drive", "DfT", "Removable media walks straight past the perimeter into OT."], ["2", "The Silent Choke", "Default password", "CAA + DfT", "Cheap smart-building IoT bridges into safety-critical systems."], ["3", "The Third-Party Siphon", "Phishing email", "DfT", "Your suppliers' security is your security."], ["4", "The Rogue Insider", "Rogue smartphone", "CAA", "Stolen legitimate access defeats the perimeter entirely."], ["5", "The Blind Spot", "VPN breach", "CAA + DfT", "An unmanaged path can reach kinetic, life-safety systems."], ["6", "The Information Hijack", "Password crack", "DfT", "Public-facing displays and PA are an integrity and panic risk."], ["7", "The Perimeter Ghost", "Rogue router", "DfT", "Physical and cyber security are one problem, not two."], ["8", "The Data Hemorrhage", "Cloud leak", "CAA + DfT", "A misconfiguration can leak PII at scale and stack fines."], ["9", "The Grid Overload", "Malware update", "DfT", "Energy and EV infrastructure is now part of the attack surface."]], ["#", "Scenario", "Ingress", "Notify", "The lesson it teaches"], [7 * mm, 37 * mm, 27 * mm, 21 * mm, None])) # ---------- DEBRIEF ---------- story.append(Spacer(1, 10)) story += section("Reading the result", "What good looks like") story.append(Paragraph("The best possible ending is <b>ATTACK CONTAINED</b> — you stopped the " "spread before it ran its course. Whether contained or fully spread, the " "debrief lives in a handful of numbers and the story behind them. Use " "these as discussion prompts:", st_body)) story.append(Spacer(1, 7)) story.append(info_table( [["Outcome", "ATTACK CONTAINED before full spread", "Which mitigation finally halted it — and could it have come sooner?"], ["Total Financial Loss", "As low as the situation allowed", "What drove it — bleed, fines, comms failure, or a missed notification?"], ["Security Integrity", "Kept as high as possible", "Which nodes were sacrificed, and were they the right ones?"], ["Operational Capacity", "Traded knowingly, not accidentally", "Did isolating for safety visibly close the runway or stop bags?"], ["Reputation multiplier", "×0.5 (proactive)", "Was the public told early, or did the story get away?"], ["Statutory notifications", "CAA / DfT discharged as required", "Did you match the notification to the type of breach?"]], ["Metric", "A good outcome", "Debrief question"], [40 * mm, 46 * mm, None], head_color=GREEN)) story.append(Spacer(1, 10)) story += section("Running the stand", "Facilitator notes") story += bullets([ "<b>Session length:</b> one round is 2-3 minutes. Let a visitor lose badly once — the " "trade-off lands harder than a clean win.", "<b>Audio:</b> set it on the configuration screen to match the hall. The countdown ticks " "and ingress thud are strong attention-grabbers on a busy floor.", "<b>Kiosk mode:</b> full-screen the browser (F11). After 90 seconds idle the app returns " "to the attract loop by itself.", "<b>Reset ritual:</b> press RESET TO CONFIG between visitors so each person sets their own " "airport and owns the outcome.", "<b>Great opening question:</b> \"You have one attack incoming and you cannot save every " "system — what do you protect first?\"", ], GREEN) story.append(Spacer(1, 6)) story.append(Paragraph("Ultimately the simulator argues one thing: resilience is not the " "absence of incidents, it is the quality of the decisions you make " "during one.", st_quote)) doc.build(story) print("wrote", path) if __name__ == "__main__": here = os.path.dirname(os.path.abspath(__file__)) build(os.path.join(here, "Airport-Cyber-Sim-V2-Player-Guide.pdf")) |