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_guide.py 19675 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
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
#!/usr/bin/env python3
"""
Generates a branded, dark command-center themed Setup & Configuration Guide PDF
for the Airport Cyber Resilience Simulator V2. Palette mirrors the app.
"""
import os
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.colors import HexColor
from reportlab.lib.enums import TA_LEFT, TA_CENTER
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
    BaseDocTemplate, PageTemplate, Frame, Paragraph, Spacer, Table, TableStyle,
    KeepTogether, FrameBreak, NextPageTemplate, PageBreak,
)
from reportlab.lib.styles import ParagraphStyle

# ---- palette (from styles.css) ----
BG       = HexColor("#050b16")
PANEL    = HexColor("#0d1f33")
PANEL2   = HexColor("#0a1826")
LINE     = HexColor("#173355")
TEXT     = HexColor("#cfe4ff")
DIM      = HexColor("#6f8bad")
GREEN    = HexColor("#39ff9e")
CYAN     = HexColor("#38e6ff")
VIOLET   = HexColor("#a06bff")
AMBER    = HexColor("#ffb03a")
RED      = HexColor("#ff3b52")
WHITE    = HexColor("#eaf6ff")
CODEBG   = HexColor("#071019")

FONTS = r"C:\Windows\Fonts"
pdfmetrics.registerFont(TTFont("Bahn", os.path.join(FONTS, "bahnschrift.ttf")))
pdfmetrics.registerFont(TTFont("Segoe", os.path.join(FONTS, "segoeui.ttf")))
pdfmetrics.registerFont(TTFont("SegoeB", os.path.join(FONTS, "segoeuib.ttf")))
pdfmetrics.registerFont(TTFont("Mono", os.path.join(FONTS, "consola.ttf")))
pdfmetrics.registerFont(TTFont("MonoB", os.path.join(FONTS, "consolab.ttf")))

PAGE_W, PAGE_H = A4
MX = 16 * mm

# ---- styles ----
def S(name, **kw):
    base = dict(fontName="Segoe", fontSize=10.5, leading=15, textColor=TEXT)
    base.update(kw)
    return ParagraphStyle(name, **base)

st_body    = S("body")
st_dim     = S("dim", textColor=DIM, fontSize=9.5, leading=13)
st_h1       = S("h1", fontName="Bahn", fontSize=19, leading=23, textColor=WHITE, spaceBefore=2, spaceAfter=2)
st_kicker   = S("kick", fontName="Bahn", fontSize=10, leading=13, textColor=CYAN)
st_step_t   = S("stept", fontName="SegoeB", fontSize=12.5, leading=16, textColor=WHITE)
st_code     = S("code", fontName="Mono", fontSize=9, leading=13.5, textColor=CYAN)
st_code_c   = S("codec", fontName="Mono", fontSize=8.5, leading=12, textColor=DIM)
st_bullet   = S("bul", leftIndent=10, bulletIndent=0, spaceBefore=1, spaceAfter=1)
st_th       = S("th", fontName="SegoeB", fontSize=9.5, leading=12, textColor=WHITE)
st_td       = S("td", fontSize=9.5, leading=12.5, textColor=TEXT)
st_cover_t  = S("covt", fontName="Bahn", fontSize=34, leading=38, textColor=WHITE, alignment=TA_CENTER)
st_cover_s  = S("covs", fontName="Bahn", fontSize=13, leading=18, textColor=CYAN, alignment=TA_CENTER)
st_cover_d  = S("covd", fontSize=11, leading=17, textColor=DIM, alignment=TA_CENTER)

ACCENTS = {"green": GREEN, "cyan": CYAN, "violet": VIOLET, "amber": AMBER, "red": RED}


def code_block(lines):
    """A dark code/command box with a cyan monospace body."""
    body = "<br/>".join(l.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;") for l in lines)
    p = Paragraph(body, st_code)
    t = Table([[p]], colWidths=[PAGE_W - 2 * MX - 8 * mm])
    t.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), CODEBG),
        ("BOX", (0, 0), (-1, -1), 0.75, LINE),
        ("LEFTPADDING", (0, 0), (-1, -1), 10), ("RIGHTPADDING", (0, 0), (-1, -1), 10),
        ("TOPPADDING", (0, 0), (-1, -1), 8), ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
    ]))
    return t


def step(num, title, accent, flowables):
    """A numbered step card with a colored accent bar on the left."""
    col = ACCENTS[accent]
    badge = Table([[Paragraph(f"{num}", ParagraphStyle("b", fontName="Bahn", fontSize=15,
                   textColor=BG, alignment=TA_CENTER))]], colWidths=[9 * mm], rowHeights=[9 * mm])
    badge.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), col), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (-1, -1), 0),
        ("TOPPADDING", (0, 0), (-1, -1), 0), ("BOTTOMPADDING", (0, 0), (-1, -1), 0),
    ]))
    header = Table([[badge, Paragraph(title, st_step_t)]], colWidths=[11 * mm, None])
    header.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("LEFTPADDING", (0, 0), (-1, -1), 0), ("BOTTOMPADDING", (0, 0), (-1, -1), 4)]))
    inner = [header] + flowables
    card = Table([[inner]], colWidths=[PAGE_W - 2 * MX])
    card.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), PANEL2),
        ("LINEBEFORE", (0, 0), (0, -1), 3, col),
        ("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), 11), ("BOTTOMPADDING", (0, 0), (-1, -1), 11),
    ]))
    return KeepTogether([card, Spacer(1, 8)])


def bullets(items, color=GREEN):
    out = []
    for it in items:
        out.append(Paragraph(f'<font color="#{color.hexval()[2:]}">&#8226;</font>&nbsp;&nbsp;{it}', st_bullet))
    return out


def info_table(rows, header, widths, head_color=CYAN):
    data = [[Paragraph(h, st_th) for h in header]]
    for r in rows:
        data.append([Paragraph(str(c), st_td) for c in r])
    t = Table(data, colWidths=widths, repeatRows=1)
    t.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), PANEL),
        ("LINEBELOW", (0, 0), (-1, 0), 1, head_color),
        ("ROWBACKGROUNDS", (0, 1), (-1, -1), [PANEL2, HexColor("#08131f")]),
        ("GRID", (0, 0), (-1, -1), 0.4, LINE),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("LEFTPADDING", (0, 0), (-1, -1), 7), ("RIGHTPADDING", (0, 0), (-1, -1), 7),
        ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
    ]))
    return t


def section(title, kicker=None):
    out = []
    if kicker:
        out.append(Paragraph(kicker.upper(), st_kicker))
    out.append(Paragraph(title, st_h1))
    rule = Table([[""]], colWidths=[PAGE_W - 2 * MX], rowHeights=[2])
    rule.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), LINE)]))
    out.append(Spacer(1, 3)); out.append(rule); out.append(Spacer(1, 9))
    return out


# ---------- page painters ----------
def paint_bg(c, doc):
    c.setFillColor(BG); c.rect(0, 0, PAGE_W, PAGE_H, stroke=0, fill=1)
    # subtle top glow band
    c.setFillColor(HexColor("#081726")); c.rect(0, PAGE_H - 26 * mm, PAGE_W, 26 * mm, stroke=0, fill=1)
    # header
    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, "V2 · SETUP & CONFIGURATION")
    c.setStrokeColor(LINE); c.setLineWidth(0.6)
    c.line(MX, PAGE_H - 19 * mm, PAGE_W - MX, PAGE_H - 19 * mm)
    # footer
    c.line(MX, 14 * mm, PAGE_W - MX, 14 * mm)
    c.setFont("Segoe", 8); c.setFillColor(DIM)
    c.drawString(MX, 10 * mm, "NCSC CAF · UK Cyber Security and Resilience Bill aligned")
    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)
    # grid pattern
    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)
    # central emblem
    cx, cy = PAGE_W / 2, PAGE_H - 82 * mm
    c.setStrokeColor(GREEN); c.setLineWidth(2.4)
    r = 20 * mm
    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)
    c.setStrokeColor(GREEN); c.setLineWidth(3)
    c.line(cx - 8 * mm, cy, cx - 2 * mm, cy - 6 * mm)
    c.line(cx - 2 * mm, cy - 6 * mm, cx + 9 * mm, cy + 7 * mm)
    # palette bar
    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)


# ---------- build ----------
def build(path):
    doc = BaseDocTemplate(path, pagesize=A4, leftMargin=MX, rightMargin=MX,
                          topMargin=24 * mm, bottomMargin=18 * mm,
                          title="Airport Cyber Resilience Simulator V2 — Setup & Configuration 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 · SETUP &amp; CONFIGURATION GUIDE", st_cover_s))
    story.append(Spacer(1, 6 * mm))
    story.append(Paragraph("A step-by-step guide to deploying, configuring and operating the "
                           "containerised kiosk simulator — from Docker Compose to your first "
                           "live attack scenario.", st_cover_d))
    story.append(NextPageTemplate("content"))
    story.append(PageBreak())

    # ---------- 1. OVERVIEW ----------
    story += section("What you are deploying", "Overview")
    story.append(Paragraph(
        "The Airport Cyber Resilience Simulator is a self-contained, kiosk-grade training and "
        "exhibition tool. A Python (FastAPI) engine runs a per-second simulation loop and streams "
        "live state over WebSockets to an immersive React command-center dashboard. Everything is "
        "data-driven and fully containerised — one command brings the whole stack up.", st_body))
    story.append(Spacer(1, 6))
    story.append(info_table(
        [["Backend", "Python 3.11 · FastAPI · asyncio · WebSockets"],
         ["Frontend", "React + GSAP + Web Audio (all vendored, no CDN needed)"],
         ["Delivery", "Docker + Docker Compose (single service, port 8000)"],
         ["Data", "29 UK airports · 26 nodes · 9 attack playbooks (JSON)"],
         ["Compliance", "NCSC CAF objectives + UK Cyber Security and Resilience Bill"]],
        ["Layer", "Technology"], [40 * mm, PAGE_W - 2 * MX - 40 * mm]))
    story.append(Spacer(1, 12))

    story += section("Setup", "Deployment")

    story.append(step(1, "Confirm prerequisites", "green", [
        Paragraph("Install <b>Docker Desktop</b> (Windows/macOS) or Docker Engine + Compose "
                  "(Linux). This is the only requirement — Python and all frontend libraries ship "
                  "inside the image.", st_body),
        Spacer(1, 5),
        Paragraph("Verify the toolchain is present and the daemon is running:", st_dim),
        Spacer(1, 4),
        code_block(["docker --version", "docker compose version", "docker info"]),
        Spacer(1, 4),
        Paragraph("No Docker? You can also run it directly with Python 3.11+: "
                  "<font name='Mono' color='#38e6ff'>pip install -r backend/requirements.txt</font> "
                  "then <font name='Mono' color='#38e6ff'>uvicorn main:app --port 8000</font>.", st_dim),
    ]))

    story.append(step(2, "Get the project files", "cyan", [
        Paragraph("Place the <font name='Mono'>airport-cyber-sim-v2/</font> folder anywhere on the "
                  "host. The structure you should see:", st_body),
        Spacer(1, 5),
        code_block([
            "airport-cyber-sim-v2/",
            "  docker-compose.yml        sim.ps1 / Makefile   README.md",
            "  backend/",
            "    Dockerfile  main.py  requirements.txt",
            "    data/    airports.json  infrastructure.json  playbooks.json",
            "    static/  index.html  app.js  styles.css  vendor/",
        ]),
    ]))

    story.append(step(3, "Launch with Docker Compose", "violet", [
        Paragraph("From the project root, build and start the container:", st_body),
        Spacer(1, 4),
        code_block(["cd airport-cyber-sim-v2", "docker compose up --build"]),
        Spacer(1, 5),
        Paragraph("The first build downloads the Python base image and installs dependencies "
                  "(about a minute). When you see <font name='Mono' color='#39ff9e'>Uvicorn running "
                  "on 0.0.0.0:8000</font>, the simulator is live.", st_dim),
        Spacer(1, 5),
        Paragraph("Prefer shorthand? Use the bundled helpers:", st_dim),
        Spacer(1, 4),
        code_block([".\\sim.ps1 up          # Windows PowerShell",
                    "make up               # macOS / Linux / WSL"]),
    ]))

    story.append(step(4, "Open the kiosk", "amber", [
        Paragraph("Browse to the address below on the kiosk display. For a true full-screen "
                  "exhibition kiosk, press <b>F11</b> (or launch Chrome/Edge with "
                  "<font name='Mono'>--kiosk http://localhost:8000</font>).", st_body),
        Spacer(1, 4),
        code_block(["http://localhost:8000"]),
        Spacer(1, 4),
        Paragraph("The stage is locked to a 1920×1080 layout and auto-scales to fit any screen, "
                  "so it looks correct on portrait tablets and 4K panels alike.", st_dim),
    ]))

    story.append(PageBreak())

    # ---------- CONFIGURATION ----------
    story += section("Configuration &amp; first run", "Operating the kiosk")

    story.append(step(5, "Profile the airport", "green", [
        Paragraph("On the <b>Initial Configuration Screen</b>, choose one of the 29 UK passenger "
                  "airports. Your selection scales the entire economic model — Heathrow drives the "
                  "highest revenue-per-second and maximum statutory fines; regional airports scale "
                  "down proportionally.", st_body),
        Spacer(1, 4),
        Paragraph("Toggle <b>Enable Simulation Audio</b> to match your stand's noise environment, "
                  "then press <b>INITIALIZE SIMULATOR</b> to enter the dashboard.", st_dim),
    ]))

    story.append(step(6, "Arm and launch a playbook", "cyan", [
        Paragraph("In the left sidebar, pick one of the 9 attack playbooks, then press "
                  "<b>START SIMULATION</b>. A dramatic 5-second countdown plays; the simulation "
                  "clock, financial bleed and the point-of-ingress indicator only begin when it "
                  "hits zero.", st_body),
    ]))

    story.append(step(7, "Respond to the incident", "violet", [
        Paragraph("Tap any node to open its overlay — service detail, live capacity, threat vector, "
                  "mitigation options and a <b>Regulatory Insights</b> tab mapping the node to its "
                  "CAF objective and statutory reporting duty. Choose a mitigation deliberately; "
                  "each carries an operational trade-off:", st_body),
        Spacer(1, 6),
        info_table(
            [["1 · Isolate Node", "Severs links, halts spread", "→ 0% (runway lights go dark)", "Max operational bleed"],
             ["2 · Reduce / Throttle", "Slows spread by 80%", "→ 40% (lights dim &amp; flicker)", "Moderate"],
             ["3 · Stop Attack (Patch)", "5s patch race — reverts to normal only if it beats the attack to the next node", "Unchanged", "£50k flat fee"]],
            ["Mitigation", "Effect on spread", "Capacity", "Cost"],
            [28 * mm, 43 * mm, 44 * mm, (PAGE_W - 2 * MX - 24) - 115 * mm]),
    ]))

    story.append(step(8, "Work the Crisis Communications module", "amber", [
        Paragraph("Once an attack begins you have a 15-second window to <b>Release a Proactive "
                  "Media Statement</b>, halving the reputational-loss multiplier. Miss it and the "
                  "phone and email icons flash red — answer them to hold the baseline penalty. "
                  "Ignore the barrage and a communications blackout <b>triples</b> the reputational "
                  "bleed.", st_body),
    ]))

    story.append(step(9, "Reset between visitors", "red", [
        Paragraph("Press <b>RESET TO CONFIG</b> to clear all state and return to airport selection. "
                  "If the kiosk is idle for 90 seconds it auto-resets to an animated attract loop, "
                  "ready for the next visitor.", st_body),
    ]))

    story.append(Spacer(1, 4))
    story += section("The math engine", "Understanding the metrics")
    story.append(Paragraph("Every second the backend recomputes total loss and streams it to the "
                           "odometer, which shifts colour white → amber → neon-red as it climbs:", st_body))
    story.append(Spacer(1, 6))
    story.append(code_block([
        "Total_Loss = Operational_Cost + Regulatory_Fines + Mitigation_Costs",
        "",
        "Operational_Cost += Revenue_Per_Sec x Node_Impact x Downtime_Penalty",
        "                    x Reputational_Multiplier      (every second)",
        "Regulatory_Fines += Node_Flat_Fine x Airport_Fine_Multiplier",
        "                    (on breach of a data-sensitive node)",
        "Mitigation_Costs += 50,000            (per emergency patch deployed)",
    ]))
    story.append(Spacer(1, 6))
    story += bullets([
        "<b>Downtime penalties:</b> Normal 0.0 · Throttled 0.6 · Isolated 1.0.",
        "<b>Impact factors</b> are per-node (ATC ≈ 0.85, Power ≈ 0.88, Retail POS ≈ 0.10).",
        "<b>Reputational multiplier:</b> 0.5 proactive · 1.0 baseline · 3.0 blackout.",
    ], GREEN)

    story.append(Spacer(1, 10))
    story += section("Customise &amp; troubleshoot", "Reference")
    story.append(Paragraph("The simulator is fully data-driven — extend it by editing the JSON in "
                           "<font name='Mono'>backend/data/</font>; the compose file mounts that "
                           "folder read-only so changes hot-reload without a rebuild.", st_body))
    story.append(Spacer(1, 6))
    story.append(info_table(
        [["Add an airport / change scalars", "backend/data/airports.json"],
         ["Add nodes, links or impact factors", "backend/data/infrastructure.json"],
         ["Add or edit attack playbooks", "backend/data/playbooks.json"],
         ["Port 8000 already in use", "Edit the ports mapping in docker-compose.yml"],
         ["Check container health / logs", "sim.ps1 status  ·  sim.ps1 logs"],
         ["Stop everything", "docker compose down  ·  sim.ps1 down"]],
        ["Task", "Where"], [78 * mm, PAGE_W - 2 * MX - 78 * mm], head_color=AMBER))

    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-Setup-Guide.pdf"))