Catalyst / admin/AirportCyberSimulator 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / AirportCyberSimulator

public

Airport Cyber Attack Simulation Application

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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"))