diff --git a/.gitignore b/.gitignore
index 9193811..879add9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,8 @@ dist/enhanced/
# public/cards/hacking/ are the committed record; these are intermediates.
# node scripts/hacking-legends.mjs all
dist/hacking/
+
+# Security Professionals (Series Three) working dirs. Same deal — the published
+# faces under public/cards/security/ are the committed record.
+# node scripts/security-pros.mjs all
+dist/security/
diff --git a/README.md b/README.md
index 01053de..f04580e 100644
--- a/README.md
+++ b/README.md
@@ -78,13 +78,30 @@ Chromium — set `CHROME_PATH` if it is not on the usual paths.
`dist/hacking/` is gitignored, so the full-resolution faces are local only.
Archive them before wiping the directory if you want print masters.
-**Series Three — Security Professionals** (`src/data/security.ts`, roster only):
+**Series Three — Security Professionals** (`src/data/security.ts`, 50 cards):
-50 hand-curated cards for the defensive side of the field. Copy and sources are
-written; no art pipeline exists yet, so the set renders as a data-only roster at
-`/security-professionals` and its cards have no `front`/`back` and no card pages.
-When art starts, copy `scripts/hacking-legends.mjs` — the data module already
-carries every field that pipeline reads.
+```bash
+pnpm security validate # roster checks, reports what art is missing
+pnpm security all # portraits -> render -> publish
+pnpm security all 1 5 12 # ...only these card numbers
+pnpm security render # re-render faces from the template, no API calls
+```
+
+Same stages and the same resumability as Series Two, with one deliberate
+difference: **`all` does not run `enhance`.** On Series Two that image-to-image
+finish pass rewrote text on the card faces despite the prompt forbidding it — it
+fabricated quotes attributed to living people, invented stat panels that were not
+on the card, and misspelled a name. Nothing in the pipeline catches that, because
+there is no OCR step. Since this set's whole premise is documented, sourced
+history, an invented quote is the worst defect it can carry, so the pass is
+opt-in: it refuses to run unless `ENHANCE_I_WILL_CHECK_EVERY_FACE=1` is set, and
+running it means checking every face by eye before publish.
+
+Two template changes follow from the same finding. The curator's note renders as a
+labelled panel rather than wrapped in quote marks, because on Series Two backs the
+quoted note reads as something the subject said — the field is editorial voice.
+And the front's portrait window is taller, so the caption band is sized to what it
+holds instead of leaving a third of the card empty.
Nobody appears in more than one series; `src/data/roster.ts` holds the shared card
vocabulary that Series Two and Three both render through.
diff --git a/assets/portraits/security-pros/card_001.png b/assets/portraits/security-pros/card_001.png
new file mode 100644
index 0000000..a984533
Binary files /dev/null and b/assets/portraits/security-pros/card_001.png differ
diff --git a/assets/portraits/security-pros/card_002.png b/assets/portraits/security-pros/card_002.png
new file mode 100644
index 0000000..33e3bf0
Binary files /dev/null and b/assets/portraits/security-pros/card_002.png differ
diff --git a/assets/portraits/security-pros/card_003.png b/assets/portraits/security-pros/card_003.png
new file mode 100644
index 0000000..99951f1
Binary files /dev/null and b/assets/portraits/security-pros/card_003.png differ
diff --git a/assets/portraits/security-pros/card_004.png b/assets/portraits/security-pros/card_004.png
new file mode 100644
index 0000000..43e7367
Binary files /dev/null and b/assets/portraits/security-pros/card_004.png differ
diff --git a/assets/portraits/security-pros/card_005.png b/assets/portraits/security-pros/card_005.png
new file mode 100644
index 0000000..f470845
Binary files /dev/null and b/assets/portraits/security-pros/card_005.png differ
diff --git a/assets/portraits/security-pros/card_006.png b/assets/portraits/security-pros/card_006.png
new file mode 100644
index 0000000..7ddc4c6
Binary files /dev/null and b/assets/portraits/security-pros/card_006.png differ
diff --git a/assets/portraits/security-pros/card_007.png b/assets/portraits/security-pros/card_007.png
new file mode 100644
index 0000000..d790e1f
Binary files /dev/null and b/assets/portraits/security-pros/card_007.png differ
diff --git a/assets/portraits/security-pros/card_008.png b/assets/portraits/security-pros/card_008.png
new file mode 100644
index 0000000..d9ab256
Binary files /dev/null and b/assets/portraits/security-pros/card_008.png differ
diff --git a/assets/portraits/security-pros/card_009.png b/assets/portraits/security-pros/card_009.png
new file mode 100644
index 0000000..debf88d
Binary files /dev/null and b/assets/portraits/security-pros/card_009.png differ
diff --git a/assets/portraits/security-pros/card_010.png b/assets/portraits/security-pros/card_010.png
new file mode 100644
index 0000000..795a602
Binary files /dev/null and b/assets/portraits/security-pros/card_010.png differ
diff --git a/assets/portraits/security-pros/card_011.png b/assets/portraits/security-pros/card_011.png
new file mode 100644
index 0000000..e2787a2
Binary files /dev/null and b/assets/portraits/security-pros/card_011.png differ
diff --git a/assets/portraits/security-pros/card_012.png b/assets/portraits/security-pros/card_012.png
new file mode 100644
index 0000000..22054c3
Binary files /dev/null and b/assets/portraits/security-pros/card_012.png differ
diff --git a/assets/portraits/security-pros/card_013.png b/assets/portraits/security-pros/card_013.png
new file mode 100644
index 0000000..76ce6dc
Binary files /dev/null and b/assets/portraits/security-pros/card_013.png differ
diff --git a/assets/portraits/security-pros/card_014.png b/assets/portraits/security-pros/card_014.png
new file mode 100644
index 0000000..2770667
Binary files /dev/null and b/assets/portraits/security-pros/card_014.png differ
diff --git a/assets/portraits/security-pros/card_015.png b/assets/portraits/security-pros/card_015.png
new file mode 100644
index 0000000..584188f
Binary files /dev/null and b/assets/portraits/security-pros/card_015.png differ
diff --git a/assets/portraits/security-pros/card_016.png b/assets/portraits/security-pros/card_016.png
new file mode 100644
index 0000000..bca299f
Binary files /dev/null and b/assets/portraits/security-pros/card_016.png differ
diff --git a/assets/portraits/security-pros/card_017.png b/assets/portraits/security-pros/card_017.png
new file mode 100644
index 0000000..5d282fd
Binary files /dev/null and b/assets/portraits/security-pros/card_017.png differ
diff --git a/assets/portraits/security-pros/card_018.png b/assets/portraits/security-pros/card_018.png
new file mode 100644
index 0000000..dbc2054
Binary files /dev/null and b/assets/portraits/security-pros/card_018.png differ
diff --git a/assets/portraits/security-pros/card_019.png b/assets/portraits/security-pros/card_019.png
new file mode 100644
index 0000000..4a15018
Binary files /dev/null and b/assets/portraits/security-pros/card_019.png differ
diff --git a/assets/portraits/security-pros/card_020.png b/assets/portraits/security-pros/card_020.png
new file mode 100644
index 0000000..8c946de
Binary files /dev/null and b/assets/portraits/security-pros/card_020.png differ
diff --git a/assets/portraits/security-pros/card_021.png b/assets/portraits/security-pros/card_021.png
new file mode 100644
index 0000000..2735548
Binary files /dev/null and b/assets/portraits/security-pros/card_021.png differ
diff --git a/assets/portraits/security-pros/card_022.png b/assets/portraits/security-pros/card_022.png
new file mode 100644
index 0000000..7b5ffa0
Binary files /dev/null and b/assets/portraits/security-pros/card_022.png differ
diff --git a/assets/portraits/security-pros/card_023.png b/assets/portraits/security-pros/card_023.png
new file mode 100644
index 0000000..d8867a9
Binary files /dev/null and b/assets/portraits/security-pros/card_023.png differ
diff --git a/assets/portraits/security-pros/card_024.png b/assets/portraits/security-pros/card_024.png
new file mode 100644
index 0000000..c2edea6
Binary files /dev/null and b/assets/portraits/security-pros/card_024.png differ
diff --git a/assets/portraits/security-pros/card_025.png b/assets/portraits/security-pros/card_025.png
new file mode 100644
index 0000000..5d858e1
Binary files /dev/null and b/assets/portraits/security-pros/card_025.png differ
diff --git a/assets/portraits/security-pros/card_026.png b/assets/portraits/security-pros/card_026.png
new file mode 100644
index 0000000..02d3847
Binary files /dev/null and b/assets/portraits/security-pros/card_026.png differ
diff --git a/assets/portraits/security-pros/card_027.png b/assets/portraits/security-pros/card_027.png
new file mode 100644
index 0000000..1ced391
Binary files /dev/null and b/assets/portraits/security-pros/card_027.png differ
diff --git a/assets/portraits/security-pros/card_028.png b/assets/portraits/security-pros/card_028.png
new file mode 100644
index 0000000..73c6a6f
Binary files /dev/null and b/assets/portraits/security-pros/card_028.png differ
diff --git a/assets/portraits/security-pros/card_029.png b/assets/portraits/security-pros/card_029.png
new file mode 100644
index 0000000..63011ec
Binary files /dev/null and b/assets/portraits/security-pros/card_029.png differ
diff --git a/assets/portraits/security-pros/card_030.png b/assets/portraits/security-pros/card_030.png
new file mode 100644
index 0000000..e076d03
Binary files /dev/null and b/assets/portraits/security-pros/card_030.png differ
diff --git a/assets/portraits/security-pros/card_031.png b/assets/portraits/security-pros/card_031.png
new file mode 100644
index 0000000..b798931
Binary files /dev/null and b/assets/portraits/security-pros/card_031.png differ
diff --git a/assets/portraits/security-pros/card_032.png b/assets/portraits/security-pros/card_032.png
new file mode 100644
index 0000000..ed0648a
Binary files /dev/null and b/assets/portraits/security-pros/card_032.png differ
diff --git a/assets/portraits/security-pros/card_033.png b/assets/portraits/security-pros/card_033.png
new file mode 100644
index 0000000..c28a487
Binary files /dev/null and b/assets/portraits/security-pros/card_033.png differ
diff --git a/assets/portraits/security-pros/card_034.png b/assets/portraits/security-pros/card_034.png
new file mode 100644
index 0000000..2a92e9c
Binary files /dev/null and b/assets/portraits/security-pros/card_034.png differ
diff --git a/assets/portraits/security-pros/card_035.png b/assets/portraits/security-pros/card_035.png
new file mode 100644
index 0000000..eaf43ec
Binary files /dev/null and b/assets/portraits/security-pros/card_035.png differ
diff --git a/assets/portraits/security-pros/card_036.png b/assets/portraits/security-pros/card_036.png
new file mode 100644
index 0000000..d0b980e
Binary files /dev/null and b/assets/portraits/security-pros/card_036.png differ
diff --git a/assets/portraits/security-pros/card_037.png b/assets/portraits/security-pros/card_037.png
new file mode 100644
index 0000000..d93595f
Binary files /dev/null and b/assets/portraits/security-pros/card_037.png differ
diff --git a/assets/portraits/security-pros/card_038.png b/assets/portraits/security-pros/card_038.png
new file mode 100644
index 0000000..32d741c
Binary files /dev/null and b/assets/portraits/security-pros/card_038.png differ
diff --git a/assets/portraits/security-pros/card_039.png b/assets/portraits/security-pros/card_039.png
new file mode 100644
index 0000000..99709be
Binary files /dev/null and b/assets/portraits/security-pros/card_039.png differ
diff --git a/assets/portraits/security-pros/card_040.png b/assets/portraits/security-pros/card_040.png
new file mode 100644
index 0000000..8395c29
Binary files /dev/null and b/assets/portraits/security-pros/card_040.png differ
diff --git a/assets/portraits/security-pros/card_041.png b/assets/portraits/security-pros/card_041.png
new file mode 100644
index 0000000..f19528a
Binary files /dev/null and b/assets/portraits/security-pros/card_041.png differ
diff --git a/assets/portraits/security-pros/card_042.png b/assets/portraits/security-pros/card_042.png
new file mode 100644
index 0000000..47a8fa2
Binary files /dev/null and b/assets/portraits/security-pros/card_042.png differ
diff --git a/assets/portraits/security-pros/card_043.png b/assets/portraits/security-pros/card_043.png
new file mode 100644
index 0000000..b4ddc87
Binary files /dev/null and b/assets/portraits/security-pros/card_043.png differ
diff --git a/assets/portraits/security-pros/card_044.png b/assets/portraits/security-pros/card_044.png
new file mode 100644
index 0000000..4586780
Binary files /dev/null and b/assets/portraits/security-pros/card_044.png differ
diff --git a/assets/portraits/security-pros/card_045.png b/assets/portraits/security-pros/card_045.png
new file mode 100644
index 0000000..54611ab
Binary files /dev/null and b/assets/portraits/security-pros/card_045.png differ
diff --git a/assets/portraits/security-pros/card_046.png b/assets/portraits/security-pros/card_046.png
new file mode 100644
index 0000000..b778eb1
Binary files /dev/null and b/assets/portraits/security-pros/card_046.png differ
diff --git a/assets/portraits/security-pros/card_047.png b/assets/portraits/security-pros/card_047.png
new file mode 100644
index 0000000..093ccf8
Binary files /dev/null and b/assets/portraits/security-pros/card_047.png differ
diff --git a/assets/portraits/security-pros/card_048.png b/assets/portraits/security-pros/card_048.png
new file mode 100644
index 0000000..d804524
Binary files /dev/null and b/assets/portraits/security-pros/card_048.png differ
diff --git a/assets/portraits/security-pros/card_049.png b/assets/portraits/security-pros/card_049.png
new file mode 100644
index 0000000..bb4eb64
Binary files /dev/null and b/assets/portraits/security-pros/card_049.png differ
diff --git a/assets/portraits/security-pros/card_050.png b/assets/portraits/security-pros/card_050.png
new file mode 100644
index 0000000..d872803
Binary files /dev/null and b/assets/portraits/security-pros/card_050.png differ
diff --git a/package.json b/package.json
index 7185428..cceed96 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,10 @@
"db:migrate": "node scripts/migrate.mjs",
"hacking": "node scripts/hacking-legends.mjs",
"hacking:validate": "node scripts/hacking-legends.mjs validate",
- "hacking:render": "node scripts/hacking-legends.mjs render"
+ "hacking:render": "node scripts/hacking-legends.mjs render",
+ "security": "node scripts/security-pros.mjs",
+ "security:validate": "node scripts/security-pros.mjs validate",
+ "security:render": "node scripts/security-pros.mjs render"
},
"dependencies": {
"@libsql/client": "^0.17.4",
diff --git a/public/cards/security/001-whitfield-diffie-back.png b/public/cards/security/001-whitfield-diffie-back.png
new file mode 100644
index 0000000..915e9f9
Binary files /dev/null and b/public/cards/security/001-whitfield-diffie-back.png differ
diff --git a/public/cards/security/001-whitfield-diffie-front.png b/public/cards/security/001-whitfield-diffie-front.png
new file mode 100644
index 0000000..a94cd2a
Binary files /dev/null and b/public/cards/security/001-whitfield-diffie-front.png differ
diff --git a/public/cards/security/002-martin-hellman-back.png b/public/cards/security/002-martin-hellman-back.png
new file mode 100644
index 0000000..13e8958
Binary files /dev/null and b/public/cards/security/002-martin-hellman-back.png differ
diff --git a/public/cards/security/002-martin-hellman-front.png b/public/cards/security/002-martin-hellman-front.png
new file mode 100644
index 0000000..4c44c51
Binary files /dev/null and b/public/cards/security/002-martin-hellman-front.png differ
diff --git a/public/cards/security/003-ron-rivest-back.png b/public/cards/security/003-ron-rivest-back.png
new file mode 100644
index 0000000..065bb24
Binary files /dev/null and b/public/cards/security/003-ron-rivest-back.png differ
diff --git a/public/cards/security/003-ron-rivest-front.png b/public/cards/security/003-ron-rivest-front.png
new file mode 100644
index 0000000..9e84ffd
Binary files /dev/null and b/public/cards/security/003-ron-rivest-front.png differ
diff --git a/public/cards/security/004-adi-shamir-back.png b/public/cards/security/004-adi-shamir-back.png
new file mode 100644
index 0000000..5a55fd0
Binary files /dev/null and b/public/cards/security/004-adi-shamir-back.png differ
diff --git a/public/cards/security/004-adi-shamir-front.png b/public/cards/security/004-adi-shamir-front.png
new file mode 100644
index 0000000..19d8ce3
Binary files /dev/null and b/public/cards/security/004-adi-shamir-front.png differ
diff --git a/public/cards/security/005-leonard-adleman-back.png b/public/cards/security/005-leonard-adleman-back.png
new file mode 100644
index 0000000..ee6ecde
Binary files /dev/null and b/public/cards/security/005-leonard-adleman-back.png differ
diff --git a/public/cards/security/005-leonard-adleman-front.png b/public/cards/security/005-leonard-adleman-front.png
new file mode 100644
index 0000000..1e72d2f
Binary files /dev/null and b/public/cards/security/005-leonard-adleman-front.png differ
diff --git a/public/cards/security/006-phil-zimmermann-back.png b/public/cards/security/006-phil-zimmermann-back.png
new file mode 100644
index 0000000..013dcb2
Binary files /dev/null and b/public/cards/security/006-phil-zimmermann-back.png differ
diff --git a/public/cards/security/006-phil-zimmermann-front.png b/public/cards/security/006-phil-zimmermann-front.png
new file mode 100644
index 0000000..88e40f7
Binary files /dev/null and b/public/cards/security/006-phil-zimmermann-front.png differ
diff --git a/public/cards/security/007-bruce-schneier-back.png b/public/cards/security/007-bruce-schneier-back.png
new file mode 100644
index 0000000..0aa4626
Binary files /dev/null and b/public/cards/security/007-bruce-schneier-back.png differ
diff --git a/public/cards/security/007-bruce-schneier-front.png b/public/cards/security/007-bruce-schneier-front.png
new file mode 100644
index 0000000..09ee26d
Binary files /dev/null and b/public/cards/security/007-bruce-schneier-front.png differ
diff --git a/public/cards/security/008-paul-kocher-back.png b/public/cards/security/008-paul-kocher-back.png
new file mode 100644
index 0000000..f59ff81
Binary files /dev/null and b/public/cards/security/008-paul-kocher-back.png differ
diff --git a/public/cards/security/008-paul-kocher-front.png b/public/cards/security/008-paul-kocher-front.png
new file mode 100644
index 0000000..1ad6909
Binary files /dev/null and b/public/cards/security/008-paul-kocher-front.png differ
diff --git a/public/cards/security/009-dorothy-denning-back.png b/public/cards/security/009-dorothy-denning-back.png
new file mode 100644
index 0000000..316ee33
Binary files /dev/null and b/public/cards/security/009-dorothy-denning-back.png differ
diff --git a/public/cards/security/009-dorothy-denning-front.png b/public/cards/security/009-dorothy-denning-front.png
new file mode 100644
index 0000000..7cc90d9
Binary files /dev/null and b/public/cards/security/009-dorothy-denning-front.png differ
diff --git a/public/cards/security/010-gene-spafford-back.png b/public/cards/security/010-gene-spafford-back.png
new file mode 100644
index 0000000..0e7499e
Binary files /dev/null and b/public/cards/security/010-gene-spafford-back.png differ
diff --git a/public/cards/security/010-gene-spafford-front.png b/public/cards/security/010-gene-spafford-front.png
new file mode 100644
index 0000000..53deabb
Binary files /dev/null and b/public/cards/security/010-gene-spafford-front.png differ
diff --git a/public/cards/security/011-ross-anderson-back.png b/public/cards/security/011-ross-anderson-back.png
new file mode 100644
index 0000000..5b828bd
Binary files /dev/null and b/public/cards/security/011-ross-anderson-back.png differ
diff --git a/public/cards/security/011-ross-anderson-front.png b/public/cards/security/011-ross-anderson-front.png
new file mode 100644
index 0000000..3e2d83e
Binary files /dev/null and b/public/cards/security/011-ross-anderson-front.png differ
diff --git a/public/cards/security/012-matt-blaze-back.png b/public/cards/security/012-matt-blaze-back.png
new file mode 100644
index 0000000..f62ff8a
Binary files /dev/null and b/public/cards/security/012-matt-blaze-back.png differ
diff --git a/public/cards/security/012-matt-blaze-front.png b/public/cards/security/012-matt-blaze-front.png
new file mode 100644
index 0000000..7a04bb5
Binary files /dev/null and b/public/cards/security/012-matt-blaze-front.png differ
diff --git a/public/cards/security/013-radia-perlman-back.png b/public/cards/security/013-radia-perlman-back.png
new file mode 100644
index 0000000..1ec4b05
Binary files /dev/null and b/public/cards/security/013-radia-perlman-back.png differ
diff --git a/public/cards/security/013-radia-perlman-front.png b/public/cards/security/013-radia-perlman-front.png
new file mode 100644
index 0000000..52e33b4
Binary files /dev/null and b/public/cards/security/013-radia-perlman-front.png differ
diff --git a/public/cards/security/014-steven-bellovin-back.png b/public/cards/security/014-steven-bellovin-back.png
new file mode 100644
index 0000000..1b0a1bf
Binary files /dev/null and b/public/cards/security/014-steven-bellovin-back.png differ
diff --git a/public/cards/security/014-steven-bellovin-front.png b/public/cards/security/014-steven-bellovin-front.png
new file mode 100644
index 0000000..a6c48d4
Binary files /dev/null and b/public/cards/security/014-steven-bellovin-front.png differ
diff --git a/public/cards/security/015-william-cheswick-back.png b/public/cards/security/015-william-cheswick-back.png
new file mode 100644
index 0000000..1b2d5b6
Binary files /dev/null and b/public/cards/security/015-william-cheswick-back.png differ
diff --git a/public/cards/security/015-william-cheswick-front.png b/public/cards/security/015-william-cheswick-front.png
new file mode 100644
index 0000000..f96a23e
Binary files /dev/null and b/public/cards/security/015-william-cheswick-front.png differ
diff --git a/public/cards/security/016-marcus-ranum-back.png b/public/cards/security/016-marcus-ranum-back.png
new file mode 100644
index 0000000..ce73574
Binary files /dev/null and b/public/cards/security/016-marcus-ranum-back.png differ
diff --git a/public/cards/security/016-marcus-ranum-front.png b/public/cards/security/016-marcus-ranum-front.png
new file mode 100644
index 0000000..c877732
Binary files /dev/null and b/public/cards/security/016-marcus-ranum-front.png differ
diff --git a/public/cards/security/017-wietse-venema-back.png b/public/cards/security/017-wietse-venema-back.png
new file mode 100644
index 0000000..e667238
Binary files /dev/null and b/public/cards/security/017-wietse-venema-back.png differ
diff --git a/public/cards/security/017-wietse-venema-front.png b/public/cards/security/017-wietse-venema-front.png
new file mode 100644
index 0000000..b40d480
Binary files /dev/null and b/public/cards/security/017-wietse-venema-front.png differ
diff --git a/public/cards/security/018-dan-farmer-back.png b/public/cards/security/018-dan-farmer-back.png
new file mode 100644
index 0000000..5cb3d65
Binary files /dev/null and b/public/cards/security/018-dan-farmer-back.png differ
diff --git a/public/cards/security/018-dan-farmer-front.png b/public/cards/security/018-dan-farmer-front.png
new file mode 100644
index 0000000..473abf7
Binary files /dev/null and b/public/cards/security/018-dan-farmer-front.png differ
diff --git a/public/cards/security/019-gordon-lyon-back.png b/public/cards/security/019-gordon-lyon-back.png
new file mode 100644
index 0000000..cd2b3f9
Binary files /dev/null and b/public/cards/security/019-gordon-lyon-back.png differ
diff --git a/public/cards/security/019-gordon-lyon-front.png b/public/cards/security/019-gordon-lyon-front.png
new file mode 100644
index 0000000..9ec0efb
Binary files /dev/null and b/public/cards/security/019-gordon-lyon-front.png differ
diff --git a/public/cards/security/020-martin-roesch-back.png b/public/cards/security/020-martin-roesch-back.png
new file mode 100644
index 0000000..02d4a65
Binary files /dev/null and b/public/cards/security/020-martin-roesch-back.png differ
diff --git a/public/cards/security/020-martin-roesch-front.png b/public/cards/security/020-martin-roesch-front.png
new file mode 100644
index 0000000..8c832f0
Binary files /dev/null and b/public/cards/security/020-martin-roesch-front.png differ
diff --git a/public/cards/security/021-gerald-combs-back.png b/public/cards/security/021-gerald-combs-back.png
new file mode 100644
index 0000000..b8cc12d
Binary files /dev/null and b/public/cards/security/021-gerald-combs-back.png differ
diff --git a/public/cards/security/021-gerald-combs-front.png b/public/cards/security/021-gerald-combs-front.png
new file mode 100644
index 0000000..e1a455e
Binary files /dev/null and b/public/cards/security/021-gerald-combs-front.png differ
diff --git a/public/cards/security/022-renaud-deraison-back.png b/public/cards/security/022-renaud-deraison-back.png
new file mode 100644
index 0000000..0217894
Binary files /dev/null and b/public/cards/security/022-renaud-deraison-back.png differ
diff --git a/public/cards/security/022-renaud-deraison-front.png b/public/cards/security/022-renaud-deraison-front.png
new file mode 100644
index 0000000..ce87913
Binary files /dev/null and b/public/cards/security/022-renaud-deraison-front.png differ
diff --git a/public/cards/security/023-paul-vixie-back.png b/public/cards/security/023-paul-vixie-back.png
new file mode 100644
index 0000000..e6c6456
Binary files /dev/null and b/public/cards/security/023-paul-vixie-back.png differ
diff --git a/public/cards/security/023-paul-vixie-front.png b/public/cards/security/023-paul-vixie-front.png
new file mode 100644
index 0000000..821a620
Binary files /dev/null and b/public/cards/security/023-paul-vixie-front.png differ
diff --git a/public/cards/security/024-niels-provos-back.png b/public/cards/security/024-niels-provos-back.png
new file mode 100644
index 0000000..f89601b
Binary files /dev/null and b/public/cards/security/024-niels-provos-back.png differ
diff --git a/public/cards/security/024-niels-provos-front.png b/public/cards/security/024-niels-provos-front.png
new file mode 100644
index 0000000..cf9a1aa
Binary files /dev/null and b/public/cards/security/024-niels-provos-front.png differ
diff --git a/public/cards/security/025-jeremiah-grossman-back.png b/public/cards/security/025-jeremiah-grossman-back.png
new file mode 100644
index 0000000..d047bb6
Binary files /dev/null and b/public/cards/security/025-jeremiah-grossman-back.png differ
diff --git a/public/cards/security/025-jeremiah-grossman-front.png b/public/cards/security/025-jeremiah-grossman-front.png
new file mode 100644
index 0000000..ad6925a
Binary files /dev/null and b/public/cards/security/025-jeremiah-grossman-front.png differ
diff --git a/public/cards/security/026-robert-hansen-back.png b/public/cards/security/026-robert-hansen-back.png
new file mode 100644
index 0000000..0f23d79
Binary files /dev/null and b/public/cards/security/026-robert-hansen-back.png differ
diff --git a/public/cards/security/026-robert-hansen-front.png b/public/cards/security/026-robert-hansen-front.png
new file mode 100644
index 0000000..cb5823c
Binary files /dev/null and b/public/cards/security/026-robert-hansen-front.png differ
diff --git a/public/cards/security/027-mark-curphey-back.png b/public/cards/security/027-mark-curphey-back.png
new file mode 100644
index 0000000..bb236cb
Binary files /dev/null and b/public/cards/security/027-mark-curphey-back.png differ
diff --git a/public/cards/security/027-mark-curphey-front.png b/public/cards/security/027-mark-curphey-front.png
new file mode 100644
index 0000000..11ec5d1
Binary files /dev/null and b/public/cards/security/027-mark-curphey-front.png differ
diff --git a/public/cards/security/028-jeff-williams-back.png b/public/cards/security/028-jeff-williams-back.png
new file mode 100644
index 0000000..f432ca9
Binary files /dev/null and b/public/cards/security/028-jeff-williams-back.png differ
diff --git a/public/cards/security/028-jeff-williams-front.png b/public/cards/security/028-jeff-williams-front.png
new file mode 100644
index 0000000..d5c7f86
Binary files /dev/null and b/public/cards/security/028-jeff-williams-front.png differ
diff --git a/public/cards/security/029-gary-mcgraw-back.png b/public/cards/security/029-gary-mcgraw-back.png
new file mode 100644
index 0000000..c7086da
Binary files /dev/null and b/public/cards/security/029-gary-mcgraw-back.png differ
diff --git a/public/cards/security/029-gary-mcgraw-front.png b/public/cards/security/029-gary-mcgraw-front.png
new file mode 100644
index 0000000..0605914
Binary files /dev/null and b/public/cards/security/029-gary-mcgraw-front.png differ
diff --git a/public/cards/security/030-michael-howard-back.png b/public/cards/security/030-michael-howard-back.png
new file mode 100644
index 0000000..05e5226
Binary files /dev/null and b/public/cards/security/030-michael-howard-back.png differ
diff --git a/public/cards/security/030-michael-howard-front.png b/public/cards/security/030-michael-howard-front.png
new file mode 100644
index 0000000..8ecbb8d
Binary files /dev/null and b/public/cards/security/030-michael-howard-front.png differ
diff --git a/public/cards/security/031-adam-shostack-back.png b/public/cards/security/031-adam-shostack-back.png
new file mode 100644
index 0000000..2fba100
Binary files /dev/null and b/public/cards/security/031-adam-shostack-back.png differ
diff --git a/public/cards/security/031-adam-shostack-front.png b/public/cards/security/031-adam-shostack-front.png
new file mode 100644
index 0000000..2120988
Binary files /dev/null and b/public/cards/security/031-adam-shostack-front.png differ
diff --git a/public/cards/security/032-ivan-ristic-back.png b/public/cards/security/032-ivan-ristic-back.png
new file mode 100644
index 0000000..33dd533
Binary files /dev/null and b/public/cards/security/032-ivan-ristic-back.png differ
diff --git a/public/cards/security/032-ivan-ristic-front.png b/public/cards/security/032-ivan-ristic-front.png
new file mode 100644
index 0000000..8b9f45d
Binary files /dev/null and b/public/cards/security/032-ivan-ristic-front.png differ
diff --git a/public/cards/security/033-mark-dowd-back.png b/public/cards/security/033-mark-dowd-back.png
new file mode 100644
index 0000000..f231912
Binary files /dev/null and b/public/cards/security/033-mark-dowd-back.png differ
diff --git a/public/cards/security/033-mark-dowd-front.png b/public/cards/security/033-mark-dowd-front.png
new file mode 100644
index 0000000..8700220
Binary files /dev/null and b/public/cards/security/033-mark-dowd-front.png differ
diff --git a/public/cards/security/034-halvar-flake-back.png b/public/cards/security/034-halvar-flake-back.png
new file mode 100644
index 0000000..6a0d853
Binary files /dev/null and b/public/cards/security/034-halvar-flake-back.png differ
diff --git a/public/cards/security/034-halvar-flake-front.png b/public/cards/security/034-halvar-flake-front.png
new file mode 100644
index 0000000..86064ce
Binary files /dev/null and b/public/cards/security/034-halvar-flake-front.png differ
diff --git a/public/cards/security/035-alex-sotirov-back.png b/public/cards/security/035-alex-sotirov-back.png
new file mode 100644
index 0000000..fd1cb3f
Binary files /dev/null and b/public/cards/security/035-alex-sotirov-back.png differ
diff --git a/public/cards/security/035-alex-sotirov-front.png b/public/cards/security/035-alex-sotirov-front.png
new file mode 100644
index 0000000..94dba9a
Binary files /dev/null and b/public/cards/security/035-alex-sotirov-front.png differ
diff --git a/public/cards/security/036-thomas-ptacek-back.png b/public/cards/security/036-thomas-ptacek-back.png
new file mode 100644
index 0000000..8112421
Binary files /dev/null and b/public/cards/security/036-thomas-ptacek-back.png differ
diff --git a/public/cards/security/036-thomas-ptacek-front.png b/public/cards/security/036-thomas-ptacek-front.png
new file mode 100644
index 0000000..d1bed8f
Binary files /dev/null and b/public/cards/security/036-thomas-ptacek-front.png differ
diff --git a/public/cards/security/037-matthew-green-back.png b/public/cards/security/037-matthew-green-back.png
new file mode 100644
index 0000000..605ad9a
Binary files /dev/null and b/public/cards/security/037-matthew-green-back.png differ
diff --git a/public/cards/security/037-matthew-green-front.png b/public/cards/security/037-matthew-green-front.png
new file mode 100644
index 0000000..a7a100c
Binary files /dev/null and b/public/cards/security/037-matthew-green-front.png differ
diff --git a/public/cards/security/038-j-alex-halderman-back.png b/public/cards/security/038-j-alex-halderman-back.png
new file mode 100644
index 0000000..68721d7
Binary files /dev/null and b/public/cards/security/038-j-alex-halderman-back.png differ
diff --git a/public/cards/security/038-j-alex-halderman-front.png b/public/cards/security/038-j-alex-halderman-front.png
new file mode 100644
index 0000000..9074d70
Binary files /dev/null and b/public/cards/security/038-j-alex-halderman-front.png differ
diff --git a/public/cards/security/039-nadia-heninger-back.png b/public/cards/security/039-nadia-heninger-back.png
new file mode 100644
index 0000000..fb9eaa0
Binary files /dev/null and b/public/cards/security/039-nadia-heninger-back.png differ
diff --git a/public/cards/security/039-nadia-heninger-front.png b/public/cards/security/039-nadia-heninger-front.png
new file mode 100644
index 0000000..8a47094
Binary files /dev/null and b/public/cards/security/039-nadia-heninger-front.png differ
diff --git a/public/cards/security/040-troy-hunt-back.png b/public/cards/security/040-troy-hunt-back.png
new file mode 100644
index 0000000..d24a191
Binary files /dev/null and b/public/cards/security/040-troy-hunt-back.png differ
diff --git a/public/cards/security/040-troy-hunt-front.png b/public/cards/security/040-troy-hunt-front.png
new file mode 100644
index 0000000..5572fad
Binary files /dev/null and b/public/cards/security/040-troy-hunt-front.png differ
diff --git a/public/cards/security/041-window-snyder-back.png b/public/cards/security/041-window-snyder-back.png
new file mode 100644
index 0000000..984b62d
Binary files /dev/null and b/public/cards/security/041-window-snyder-back.png differ
diff --git a/public/cards/security/041-window-snyder-front.png b/public/cards/security/041-window-snyder-front.png
new file mode 100644
index 0000000..9d4d275
Binary files /dev/null and b/public/cards/security/041-window-snyder-front.png differ
diff --git a/public/cards/security/042-heather-adkins-back.png b/public/cards/security/042-heather-adkins-back.png
new file mode 100644
index 0000000..7a13a8b
Binary files /dev/null and b/public/cards/security/042-heather-adkins-back.png differ
diff --git a/public/cards/security/042-heather-adkins-front.png b/public/cards/security/042-heather-adkins-front.png
new file mode 100644
index 0000000..1a6b554
Binary files /dev/null and b/public/cards/security/042-heather-adkins-front.png differ
diff --git a/public/cards/security/043-parisa-tabriz-back.png b/public/cards/security/043-parisa-tabriz-back.png
new file mode 100644
index 0000000..1f8a070
Binary files /dev/null and b/public/cards/security/043-parisa-tabriz-back.png differ
diff --git a/public/cards/security/043-parisa-tabriz-front.png b/public/cards/security/043-parisa-tabriz-front.png
new file mode 100644
index 0000000..950dff9
Binary files /dev/null and b/public/cards/security/043-parisa-tabriz-front.png differ
diff --git a/public/cards/security/044-alex-stamos-back.png b/public/cards/security/044-alex-stamos-back.png
new file mode 100644
index 0000000..7357be2
Binary files /dev/null and b/public/cards/security/044-alex-stamos-back.png differ
diff --git a/public/cards/security/044-alex-stamos-front.png b/public/cards/security/044-alex-stamos-front.png
new file mode 100644
index 0000000..2b3c68e
Binary files /dev/null and b/public/cards/security/044-alex-stamos-front.png differ
diff --git a/public/cards/security/045-dan-geer-back.png b/public/cards/security/045-dan-geer-back.png
new file mode 100644
index 0000000..9afe5c4
Binary files /dev/null and b/public/cards/security/045-dan-geer-back.png differ
diff --git a/public/cards/security/045-dan-geer-front.png b/public/cards/security/045-dan-geer-front.png
new file mode 100644
index 0000000..98f7dd9
Binary files /dev/null and b/public/cards/security/045-dan-geer-front.png differ
diff --git a/public/cards/security/046-kevin-mandia-back.png b/public/cards/security/046-kevin-mandia-back.png
new file mode 100644
index 0000000..0d7eadf
Binary files /dev/null and b/public/cards/security/046-kevin-mandia-back.png differ
diff --git a/public/cards/security/046-kevin-mandia-front.png b/public/cards/security/046-kevin-mandia-front.png
new file mode 100644
index 0000000..e4e68ae
Binary files /dev/null and b/public/cards/security/046-kevin-mandia-front.png differ
diff --git a/public/cards/security/047-mikko-hypponen-back.png b/public/cards/security/047-mikko-hypponen-back.png
new file mode 100644
index 0000000..49aefc2
Binary files /dev/null and b/public/cards/security/047-mikko-hypponen-back.png differ
diff --git a/public/cards/security/047-mikko-hypponen-front.png b/public/cards/security/047-mikko-hypponen-front.png
new file mode 100644
index 0000000..d2aa740
Binary files /dev/null and b/public/cards/security/047-mikko-hypponen-front.png differ
diff --git a/public/cards/security/048-robert-m-lee-back.png b/public/cards/security/048-robert-m-lee-back.png
new file mode 100644
index 0000000..cb39e91
Binary files /dev/null and b/public/cards/security/048-robert-m-lee-back.png differ
diff --git a/public/cards/security/048-robert-m-lee-front.png b/public/cards/security/048-robert-m-lee-front.png
new file mode 100644
index 0000000..71b9c74
Binary files /dev/null and b/public/cards/security/048-robert-m-lee-front.png differ
diff --git a/public/cards/security/049-lesley-carhart-back.png b/public/cards/security/049-lesley-carhart-back.png
new file mode 100644
index 0000000..39ae333
Binary files /dev/null and b/public/cards/security/049-lesley-carhart-back.png differ
diff --git a/public/cards/security/049-lesley-carhart-front.png b/public/cards/security/049-lesley-carhart-front.png
new file mode 100644
index 0000000..62a29c6
Binary files /dev/null and b/public/cards/security/049-lesley-carhart-front.png differ
diff --git a/public/cards/security/050-katie-nickels-back.png b/public/cards/security/050-katie-nickels-back.png
new file mode 100644
index 0000000..8fea037
Binary files /dev/null and b/public/cards/security/050-katie-nickels-back.png differ
diff --git a/public/cards/security/050-katie-nickels-front.png b/public/cards/security/050-katie-nickels-front.png
new file mode 100644
index 0000000..118e8db
Binary files /dev/null and b/public/cards/security/050-katie-nickels-front.png differ
diff --git a/scripts/security-pros.mjs b/scripts/security-pros.mjs
new file mode 100644
index 0000000..50bdfde
--- /dev/null
+++ b/scripts/security-pros.mjs
@@ -0,0 +1,606 @@
+#!/usr/bin/env node
+/*
+ Security Professionals (Series Three) — end-to-end card pipeline.
+
+ Same shape as scripts/hacking-legends.mjs. Every stage skips work that already
+ exists, so re-running is cheap and a failed batch can just be run again.
+
+ portraits AI portrait art, one per professional -> assets/portraits-art/security-pros/
+ render HTML template -> exact card PNGs -> dist/security/html/
+ enhance image-to-image premium finish pass -> dist/security/enhanced/ (OPT-IN, see below)
+ publish pick the best face available -> public/cards/security/
+ validate roster + asset sanity checks
+ all portraits -> render -> publish
+
+ WHY `all` DOES NOT RUN `enhance`
+ --------------------------------
+ On Series Two the image-to-image finish pass rewrote text on the card faces
+ despite the prompt forbidding it: it fabricated quotes attributed to living
+ people, invented stat panels that were not on the card, and misspelled a name.
+ Nothing in the pipeline catches that, because there is no OCR step — it was
+ found by eye, after the art shipped.
+
+ This set's whole premise is documented, sourced history, so an invented quote
+ attributed to a real person is the worst defect it can have. `enhance` is
+ therefore opt-in only and never part of `all`. Running it means accepting that
+ every affected face must be checked by eye before publish.
+
+ The roster is src/data/security.ts, imported directly (Node strips the types),
+ so there is no second copy of the data to keep in sync.
+
+ Portrait resolution order, highest authority first:
+ 1. assets/portraits/security-pros/card_NNN.png (human-approved, committed)
+ 2. assets/portraits-art/security-pros/card_NNN.png (model output, gitignored)
+ 3. generate a new one
+
+ Env:
+ OPENAI_API_KEY / GEMINI_API_KEY at least one, picks the provider
+ IMAGE_PROVIDER=openai|gemini override auto-detection
+ OPENAI_IMAGE_MODEL default gpt-image-2
+ GEMINI_IMAGE_MODEL default gemini-3-pro-image-preview
+ CHROME_PATH override browser discovery
+
+ Run:
+ node scripts/security-pros.mjs all # whole set
+ node scripts/security-pros.mjs all 1 5 12 # only these numbers
+ node scripts/security-pros.mjs render # no API calls, no spend
+*/
+import fs from 'node:fs/promises';
+import fssync from 'node:fs';
+import path from 'node:path';
+import process from 'node:process';
+import { fileURLToPath } from 'node:url';
+import { chromium } from 'playwright-core';
+import sharp from 'sharp';
+import { buildFront, buildBack, CARD_W, CARD_H } from './security-template.mjs';
+
+// What the site serves. Series One publishes at exactly this size and all three
+// decks appear in the same grids, so Series Three has to match.
+const WEB_W = 500;
+const WEB_H = 745;
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+
+// Importing src/data/security.ts makes Node warn that package.json has no "type".
+// Setting it would change module resolution for the Next app, so mute just this
+// one warning rather than 30 lines of noise on every run.
+const emitWarning = process.emitWarning.bind(process);
+process.emitWarning = (warning, ...rest) => {
+ const code = rest.find((r) => typeof r === 'string' && r.startsWith('MODULE_')) ??
+ rest.find((r) => r && typeof r === 'object')?.code;
+ if (code === 'MODULE_TYPELESS_PACKAGE_JSON') return;
+ return emitWarning(warning, ...rest);
+};
+
+const DIRS = {
+ approvedArt: path.join(ROOT, 'assets', 'portraits', 'security-pros'),
+ workingArt: path.join(ROOT, 'assets', 'portraits-art', 'security-pros'),
+ html: path.join(ROOT, 'dist', 'security', 'html'),
+ enhanced: path.join(ROOT, 'dist', 'security', 'enhanced'),
+ published: path.join(ROOT, 'public', 'cards', 'security'),
+};
+
+const CONCURRENCY = 3;
+const cardId = (n) => `card_${String(n).padStart(3, '0')}`;
+
+// ---------------------------------------------------------------- roster ----
+
+async function loadRoster(only = []) {
+ const mod = await import(path.join(ROOT, 'src', 'data', 'security.ts'));
+ const all = mod.pros;
+ if (!only.length) return all;
+ const found = all.filter((h) => only.includes(h.number));
+ const missing = only.filter((n) => !all.some((h) => h.number === n));
+ if (missing.length) throw new Error(`no such card number: ${missing.join(', ')}`);
+ return found;
+}
+
+// ------------------------------------------------------------- providers ----
+
+function provider() {
+ const forced = process.env.IMAGE_PROVIDER;
+ if (forced) {
+ if (!['openai', 'gemini'].includes(forced)) throw new Error(`unknown IMAGE_PROVIDER: ${forced}`);
+ return forced;
+ }
+ if (process.env.OPENAI_API_KEY) return 'openai';
+ if (process.env.GEMINI_API_KEY) return 'gemini';
+ throw new Error('set OPENAI_API_KEY or GEMINI_API_KEY to generate art');
+}
+
+const OPENAI_MODEL = () => process.env.OPENAI_IMAGE_MODEL || 'gpt-image-2';
+const GEMINI_MODEL = () => process.env.GEMINI_IMAGE_MODEL || 'gemini-3-pro-image-preview';
+
+const RETRYABLE = /quota|rate|429|500|502|503|timeout|unavailable|internal|overloaded/i;
+
+async function withRetry(label, fn, attempts = 3) {
+ let lastErr;
+ for (let attempt = 1; attempt <= attempts; attempt++) {
+ try {
+ return await fn();
+ } catch (err) {
+ lastErr = err;
+ if (attempt === attempts || !RETRYABLE.test(String(err.message))) break;
+ const wait = 4000 * attempt;
+ process.stdout.write(` … ${label} retry ${attempt}/${attempts - 1} in ${wait / 1000}s\n`);
+ await new Promise((r) => setTimeout(r, wait));
+ }
+ }
+ throw lastErr;
+}
+
+/** Text prompt -> PNG buffer. */
+async function generateImage(prompt, { size = '1024x1024' } = {}) {
+ if (provider() === 'gemini') {
+ const res = await fetch(
+ `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL()}:generateContent?key=${process.env.GEMINI_API_KEY}`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ contents: [{ parts: [{ text: prompt }] }],
+ generationConfig: { responseModalities: ['IMAGE'] },
+ }),
+ }
+ );
+ const d = await res.json();
+ if (!d.candidates) throw new Error(JSON.stringify(d.error || d).slice(0, 200));
+ const part = d.candidates[0].content.parts.find((p) => p.inlineData || p.inline_data);
+ if (!part) throw new Error('response contained no image');
+ return Buffer.from((part.inlineData || part.inline_data).data, 'base64');
+ }
+
+ const res = await fetch('https://api.openai.com/v1/images/generations', {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ model: OPENAI_MODEL(), prompt, size, n: 1 }),
+ });
+ const d = await res.json();
+ if (d.error) throw new Error(d.error.message?.slice(0, 200) || 'openai error');
+ if (!d.data?.[0]?.b64_json) throw new Error('response contained no image');
+ return Buffer.from(d.data[0].b64_json, 'base64');
+}
+
+/** Source PNG + instruction -> revised PNG buffer. */
+async function editImage(pngBuffer, prompt, { size = '1024x1536' } = {}) {
+ if (provider() === 'gemini') {
+ const res = await fetch(
+ `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL()}:generateContent?key=${process.env.GEMINI_API_KEY}`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ contents: [
+ {
+ parts: [
+ { inline_data: { mime_type: 'image/png', data: pngBuffer.toString('base64') } },
+ { text: prompt },
+ ],
+ },
+ ],
+ generationConfig: { responseModalities: ['IMAGE'] },
+ }),
+ }
+ );
+ const d = await res.json();
+ if (!d.candidates) throw new Error(JSON.stringify(d.error || d).slice(0, 200));
+ const part = d.candidates[0].content.parts.find((p) => p.inlineData || p.inline_data);
+ if (!part) throw new Error('response contained no image');
+ return Buffer.from((part.inlineData || part.inline_data).data, 'base64');
+ }
+
+ const form = new FormData();
+ form.append('model', OPENAI_MODEL());
+ form.append('prompt', prompt);
+ form.append('size', size);
+ form.append('image', new Blob([pngBuffer], { type: 'image/png' }), 'card.png');
+ const res = await fetch('https://api.openai.com/v1/images/edits', {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
+ body: form,
+ });
+ const d = await res.json();
+ if (d.error) throw new Error(d.error.message?.slice(0, 200) || 'openai error');
+ if (!d.data?.[0]?.b64_json) throw new Error('response contained no image');
+ return Buffer.from(d.data[0].b64_json, 'base64');
+}
+
+// -------------------------------------------------------------- prompts ----
+
+function portraitPrompt(h) {
+ const motif = (h.domains || []).slice(0, 2).join(' and ') || 'computer security';
+ const era = h.era ? ` Period-accurate to ${h.era}.` : '';
+ return [
+ `Realistic semi-painterly digital portrait of ${h.name}, ${h.title}.`,
+ 'Head and shoulders, facing the camera, warm cinematic studio lighting with a cool rim light,',
+ 'detailed painted realism in the style of a premium collectible trading-card portrait.',
+ `Dark background with subtle faint motifs of ${motif}.${era}`,
+ 'Recognizable likeness, dignified and serious, treated as a documented historical figure.',
+ 'No text, no letters, no logos, no border, no card frame, no watermark.',
+ ].join(' ');
+}
+
+const ENHANCE_PROMPT =
+ 'Enhance this collectible trading card into a premium, professionally-printed card with real depth and polish: ' +
+ 'cinematic lighting and painterly refinement on the portrait, refined dark metal and matte material textures, ' +
+ 'a tasteful metallic foil accent border, embossed elements, soft inner shadows and subtle gloss — like a high-end ' +
+ 'collectible trading card. ABSOLUTELY CRITICAL: keep ALL text, numbers, names, aliases, titles, labels, stat values, ' +
+ 'bar lengths, the quote and the entire layout EXACTLY the same, fully legible and in identical positions — do not ' +
+ 'change, add, remove, reflow or misspell any text. Only improve the visual finish, depth, materials and lighting.';
+
+// -------------------------------------------------------------- browser ----
+
+function findChrome() {
+ if (process.env.CHROME_PATH) return process.env.CHROME_PATH;
+ const candidates = [
+ '/usr/bin/google-chrome',
+ '/usr/bin/chromium',
+ '/usr/bin/chromium-browser',
+ ...playwrightChromiums(),
+ ];
+ const found = candidates.find((p) => p && fssync.existsSync(p));
+ if (!found) {
+ throw new Error(
+ 'no Chrome/Chromium found. Set CHROME_PATH, or install one with `npx playwright install chromium`.'
+ );
+ }
+ return found;
+}
+
+/**
+ * A Playwright Chromium unpacked without root has no system GTK/ATK libraries.
+ * If a staged copy is present, put it on the loader path so the browser starts.
+ * Absent that directory this is a no-op and the system libraries are used.
+ */
+function browserEnv() {
+ const staged = path.join(
+ process.env.HOME || '',
+ '.local',
+ 'share',
+ 'chrome-deps',
+ 'usr',
+ 'lib',
+ 'x86_64-linux-gnu'
+ );
+ if (!fssync.existsSync(staged)) return undefined;
+ const existing = process.env.LD_LIBRARY_PATH;
+ return { ...process.env, LD_LIBRARY_PATH: existing ? `${staged}:${existing}` : staged };
+}
+
+function playwrightChromiums() {
+ const base = path.join(process.env.HOME || '', '.cache', 'ms-playwright');
+ if (!fssync.existsSync(base)) return [];
+ return fssync
+ .readdirSync(base)
+ .filter((d) => d.startsWith('chromium'))
+ .sort()
+ .reverse()
+ .map((d) => path.join(base, d, 'chrome-linux', 'chrome'));
+}
+
+// --------------------------------------------------------------- stages ----
+
+async function ensureDirs() {
+ for (const d of Object.values(DIRS)) await fs.mkdir(d, { recursive: true });
+}
+
+function approvedPortrait(h) {
+ const p = path.join(DIRS.approvedArt, `${cardId(h.number)}.png`);
+ return fssync.existsSync(p) ? p : null;
+}
+function workingPortrait(h) {
+ const p = path.join(DIRS.workingArt, `${cardId(h.number)}.png`);
+ return fssync.existsSync(p) ? p : null;
+}
+function portraitFor(h) {
+ return approvedPortrait(h) || workingPortrait(h);
+}
+
+/** Run `task` over `items` with a small worker pool, reporting as it goes. */
+async function pool(items, label, task) {
+ let i = 0;
+ let ok = 0;
+ let skipped = 0;
+ const failed = [];
+ async function worker() {
+ while (i < items.length) {
+ const item = items[i++];
+ const name = `${String(item.number).padStart(2, '0')} ${item.name}`;
+ try {
+ const r = await task(item);
+ if (r === 'skip') {
+ skipped++;
+ } else {
+ ok++;
+ process.stdout.write(`✓ ${name}\n`);
+ }
+ } catch (err) {
+ failed.push(item.number);
+ process.stdout.write(`✗ ${name}: ${err.message}\n`);
+ }
+ }
+ }
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, worker));
+ console.log(
+ `${label}: ${ok} done, ${skipped} already present, ${failed.length} failed` +
+ (failed.length ? ` (${failed.join(', ')})` : '')
+ );
+ return { ok, skipped, failed };
+}
+
+async function portraits(roster) {
+ await ensureDirs();
+ console.log(`portraits: ${provider()} / ${provider() === 'openai' ? OPENAI_MODEL() : GEMINI_MODEL()}`);
+ return pool(roster, 'portraits', async (h) => {
+ if (portraitFor(h)) return 'skip';
+ const buf = await withRetry(cardId(h.number), () => generateImage(portraitPrompt(h)));
+ await fs.writeFile(path.join(DIRS.workingArt, `${cardId(h.number)}.png`), buf);
+ });
+}
+
+async function render(roster) {
+ await ensureDirs();
+ const executablePath = findChrome();
+ console.log(`render: ${executablePath}`);
+ const browser = await chromium.launch({
+ executablePath,
+ args: ['--no-sandbox'],
+ env: browserEnv(),
+ });
+ const page = await browser.newPage({
+ viewport: { width: CARD_W, height: CARD_H },
+ deviceScaleFactor: 2,
+ });
+
+ const shoot = async (html, id, side) => {
+ // Keep the self-contained HTML next to the PNG so a card can be re-rendered
+ // or hand-corrected later without regenerating the portrait.
+ await fs.writeFile(path.join(DIRS.html, `${id}-${side}.html`), html);
+ await page.setContent(html, { waitUntil: 'networkidle' });
+ await page.evaluate(() => document.fonts.ready);
+ await page.locator('.card').screenshot({ path: path.join(DIRS.html, `${id}-${side}.png`) });
+ };
+
+ let ok = 0;
+ const failed = [];
+ for (const h of roster) {
+ const id = cardId(h.number);
+ try {
+ const art = portraitFor(h);
+ if (!art) throw new Error('no portrait art — run `portraits` first');
+ const uri = `data:image/png;base64,${(await fs.readFile(art)).toString('base64')}`;
+ await shoot(buildFront(h, uri), id, 'front');
+ await shoot(buildBack(h), id, 'back');
+ ok++;
+ console.log(`✓ ${String(h.number).padStart(2, '0')} ${h.name} (${h.rarity})`);
+ } catch (err) {
+ failed.push(h.number);
+ console.log(`✗ ${String(h.number).padStart(2, '0')} ${h.name}: ${err.message}`);
+ }
+ }
+ await browser.close();
+ console.log(`render: ${ok} done, ${failed.length} failed${failed.length ? ` (${failed.join(', ')})` : ''}`);
+ return { ok, failed };
+}
+
+async function enhance(roster) {
+ // See the header comment. This pass rewrote text on shipped Series Two faces.
+ if (!process.env.ENHANCE_I_WILL_CHECK_EVERY_FACE) {
+ console.error(
+ 'enhance: refusing to run.\n' +
+ ' On Series Two this pass fabricated quotes attributed to living people,\n' +
+ ' invented stat panels and misspelled a name, despite the prompt forbidding it.\n' +
+ ' There is no OCR check in this pipeline, so nothing catches it but your eyes.\n' +
+ ' Set ENHANCE_I_WILL_CHECK_EVERY_FACE=1 to run it anyway, then inspect every\n' +
+ ' face in dist/security/enhanced/ before publish.'
+ );
+ process.exit(1);
+ }
+ await ensureDirs();
+ console.log(`enhance: ${provider()} / ${provider() === 'openai' ? OPENAI_MODEL() : GEMINI_MODEL()}`);
+ console.log('enhance: WARNING — this pass rewrites text. Check every face before publish.');
+ const jobs = [];
+ for (const h of roster) {
+ for (const side of ['front', 'back']) {
+ const src = path.join(DIRS.html, `${cardId(h.number)}-${side}.png`);
+ if (fssync.existsSync(src)) jobs.push({ ...h, side, src, name: `${h.name} ${side}` });
+ }
+ }
+ if (!jobs.length) {
+ console.log('enhance: nothing rendered yet — run `render` first');
+ return { ok: 0, failed: [] };
+ }
+ return pool(jobs, 'enhance', async (job) => {
+ const out = path.join(DIRS.enhanced, `${cardId(job.number)}-${job.side}.png`);
+ if (fssync.existsSync(out)) return 'skip';
+ const buf = await withRetry(
+ `${cardId(job.number)}-${job.side}`,
+ async () => editImage(await fs.readFile(job.src), ENHANCE_PROMPT)
+ );
+ await fs.writeFile(out, buf);
+ });
+}
+
+async function publish(roster) {
+ await ensureDirs();
+ let ok = 0;
+ const missing = [];
+ const linked = [];
+ let fromHtml = 0;
+ for (const h of roster) {
+ let faces = 0;
+ for (const side of ['front', 'back']) {
+ const id = cardId(h.number);
+ const enhanced = path.join(DIRS.enhanced, `${id}-${side}.png`);
+ const plain = path.join(DIRS.html, `${id}-${side}.png`);
+ const src = fssync.existsSync(enhanced) ? enhanced : fssync.existsSync(plain) ? plain : null;
+ if (!src) {
+ missing.push(`${id}-${side}`);
+ continue;
+ }
+ if (src === plain) fromHtml++;
+ const dest = path.join(
+ DIRS.published,
+ `${String(h.number).padStart(3, '0')}-${h.slug}-${side}.png`
+ );
+ // The full-resolution face stays in dist/ as the print master; the copy the
+ // site loads is resized and recompressed, or a deck costs ~180MB to load.
+ await sharp(src)
+ .resize(WEB_W, WEB_H, { fit: 'fill' })
+ .png({ compressionLevel: 9, palette: true })
+ .toFile(dest);
+ ok++;
+ faces++;
+ }
+ // Only advertise a card on the site once both of its faces exist.
+ if (faces === 2) linked.push(h);
+ }
+ await writeArtPaths(linked);
+ console.log(`publish: ${ok} faces -> ${path.relative(ROOT, DIRS.published)}`);
+ if (fromHtml) console.log(`publish: ${fromHtml} used the text-accurate HTML render (the expected path for this set)`);
+ if (missing.length) console.log(`publish: ${missing.length} missing (${missing.join(', ')})`);
+ return { ok, missing };
+}
+
+/**
+ * Point the roster's `front`/`back` fields at the faces we just published, so the
+ * site and its "artwork rendered" counter pick them up. Rewrites only those two
+ * lines per card and leaves the hand-written copy untouched.
+ */
+async function writeArtPaths(published) {
+ if (!published.length) return;
+ const file = path.join(ROOT, 'src', 'data', 'security.ts');
+ const lines = (await fs.readFile(file, 'utf8')).split('\n');
+ const wanted = new Map(published.map((h) => [h.number, h]));
+ const out = [];
+ const inserted = new Set();
+ let current = null;
+ let changed = 0;
+
+ for (const line of lines) {
+ const num = line.match(/^ {4}number: (\d+),$/);
+ if (num) current = Number(num[1]);
+ const rewriting = current !== null && wanted.has(current);
+ // Drop any previous art lines for a card we are about to rewrite. `current`
+ // has to stay set past the insert below, or existing lines sitting *after*
+ // `status:` slip through and the card ends up with duplicate keys.
+ if (rewriting && /^ {4}(front|back): '.*',$/.test(line)) continue;
+ out.push(line);
+ if (rewriting && !inserted.has(current) && /^ {4}status: '.*',$/.test(line)) {
+ const h = wanted.get(current);
+ const stem = `/cards/security/${String(h.number).padStart(3, '0')}-${h.slug}`;
+ out.push(` front: '${stem}-front.png',`);
+ out.push(` back: '${stem}-back.png',`);
+ changed++;
+ inserted.add(current);
+ }
+ }
+
+ await fs.writeFile(file, out.join('\n'));
+ console.log(`publish: linked art paths for ${changed} card(s) in src/data/security.ts`);
+}
+
+async function validate(roster) {
+ const all = await loadRoster();
+ const errors = [];
+ const warnings = [];
+
+ const numbers = new Set();
+ const slugs = new Set();
+ for (const h of all) {
+ if (numbers.has(h.number)) errors.push(`duplicate card number ${h.number}`);
+ if (slugs.has(h.slug)) errors.push(`duplicate slug ${h.slug}`);
+ numbers.add(h.number);
+ slugs.add(h.slug);
+ }
+
+ for (const h of roster) {
+ const id = `${cardId(h.number)} ${h.name}`;
+ for (const field of ['slug', 'name', 'title', 'knownFor', 'rarity', 'nationality', 'era', 'scouting', 'note', 'status']) {
+ if (!h[field]) errors.push(`${id}: missing ${field}`);
+ }
+ if (!h.domains?.length) errors.push(`${id}: needs at least one domain`);
+ // The set claims to be documented history, so an unsourced card is a defect,
+ // not a warning. Series Two holds the same bar by hand; here it is enforced.
+ if (!h.sources?.length) errors.push(`${id}: needs at least one source`);
+ for (const s of h.sources || []) {
+ if (!s.label || !/^https?:\/\//.test(String(s.url))) {
+ errors.push(`${id}: source needs a label and an http(s) url, got ${JSON.stringify(s)}`);
+ }
+ }
+ if (h.domains?.length > 5) errors.push(`${id}: ${h.domains.length} domains, the back panel fits 5`);
+ for (const stat of ['impact', 'technical', 'defense', 'research', 'influence']) {
+ const v = h[stat];
+ if (typeof v !== 'number' || v < 0 || v > 100) errors.push(`${id}: ${stat} must be 0-100, got ${v}`);
+ }
+ const scoutWords = String(h.scouting).trim().split(/\s+/).length;
+ const noteWords = String(h.note).trim().split(/\s+/).length;
+ if (scoutWords > 70) warnings.push(`${id}: scouting report is ${scoutWords} words, panel is sized for ~65`);
+ if (noteWords > 35) warnings.push(`${id}: note is ${noteWords} words, sized for ~35`);
+ if (h.status !== 'locked') warnings.push(`${id}: status is "${h.status}", copy is not final`);
+ if (!portraitFor(h)) warnings.push(`${id}: no portrait art yet`);
+ }
+
+ for (const w of warnings) console.log(`warn ${w}`);
+ if (errors.length) {
+ console.error('\nValidation failed:');
+ for (const e of errors) console.error(` - ${e}`);
+ process.exit(1);
+ }
+ console.log(
+ `\nValidation passed: ${all.length} on the roster, ${roster.length} selected, ` +
+ `${warnings.length} warning${warnings.length === 1 ? '' : 's'}.`
+ );
+ return { warnings };
+}
+
+// ------------------------------------------------------------------ cli ----
+
+const HELP = `Security Professionals (Series Three) card pipeline
+
+ node scripts/security-pros.mjs ${esc(pro.scouting)} ${esc(pro.note)}
PROFESSIONALSSCOUTING REPORT
DOMAINS
${domains}
SKILL STACK
${bars(pro, accent)}
- This set is being built in public. Copy is drafted, stats are in flux, and{' '} - {illustratedCount === 0 - ? 'not a single portrait has been illustrated yet' - : `${illustratedCount} of ${totalPlanned} cards are illustrated so far`} - . What you see below is the working roster. + {illustratedCount >= totalPlanned + ? `All ${totalPlanned} cards are illustrated. The set was built in public and the + roster stays open to argument — every stat line and scouting report is a pull + request away from changing.` + : `This set is being built in public. Copy is drafted, stats are in flux, and ${ + illustratedCount === 0 + ? 'not a single portrait has been illustrated yet' + : `${illustratedCount} of ${totalPlanned} cards are illustrated so far` + }. What you see below is the working roster.`}
@@ -132,9 +146,9 @@ export default function SecurityProfessionalsPage() { The working roster- Open the scouting report for the sources behind each claim. Card pages go up - once the art does. Nothing here is final until the set is frozen — argue with - any of it on GitHub. + Open the scouting report for the sources behind each claim, or go to a + card's own page. Nothing here is final until the set is frozen — argue + with any of it on GitHub.
diff --git a/src/data/hacking.ts b/src/data/hacking.ts index 3a1d1e9..d4537e9 100644 --- a/src/data/hacking.ts +++ b/src/data/hacking.ts @@ -5,8 +5,8 @@ // keep scouting reports factual and neutral — this set documents history, it does not // endorse crimes. No invented quotes: the `note` field is editorial voice, not attribution. -import type { RosterEntry, Rarity, Status, Source } from './roster'; -import { rarityLabel, statusLabel } from './roster'; +import type { RosterEntry, Rarity, Status, Source } from './roster.ts'; +import { rarityLabel, statusLabel } from './roster.ts'; export type { Rarity, Status, Source }; export { rarityLabel, statusLabel }; diff --git a/src/data/security.ts b/src/data/security.ts index 333f2bd..0911938 100644 --- a/src/data/security.ts +++ b/src/data/security.ts @@ -10,8 +10,8 @@ // Every scouting report is factual and checkable. No invented quotes: `note` is editorial // voice, not attribution. -import type { RosterEntry, Rarity, Status, Source } from './roster'; -import { rarityLabel, statusLabel } from './roster'; +import type { RosterEntry, Rarity, Status, Source } from './roster.ts'; +import { rarityLabel, statusLabel } from './roster.ts'; export type { Rarity, Status, Source }; export { rarityLabel, statusLabel }; @@ -53,6 +53,8 @@ export const pros: Pro[] = [ 'With Martin Hellman, published “New Directions in Cryptography” in 1976 and showed that two parties who had never met could agree on a secret over a wire anyone could read. It broke a problem the field had considered permanent, and every TLS handshake since is a descendant. Shared the 2015 Turing Award for it, and spent decades afterwards arguing the civil-liberties side of the crypto policy fights.', note: 'The paper that made secure commerce on a public network conceivable at all.', status: 'locked', + front: '/cards/security/001-whitfield-diffie-front.png', + back: '/cards/security/001-whitfield-diffie-back.png', sources: [ { label: 'Whitfield Diffie', url: 'https://en.wikipedia.org/wiki/Whitfield_Diffie' }, { label: 'New Directions in Cryptography (1976)', url: 'https://ee.stanford.edu/~hellman/publications/24.pdf' }, @@ -78,6 +80,8 @@ export const pros: Pro[] = [ 'Co-author of the 1976 paper and Stanford professor who then refused to let the NSA classify academic cryptography out of existence, publishing and teaching through explicit pressure to stop. Also co-authored the early analysis of DES key length that argued, correctly and years early, that 56 bits was not enough. Shared the 2015 Turing Award with Diffie.', note: 'Won the maths, then won the argument about who is allowed to do the maths.', status: 'locked', + front: '/cards/security/002-martin-hellman-front.png', + back: '/cards/security/002-martin-hellman-back.png', sources: [ { label: 'Martin Hellman', url: 'https://en.wikipedia.org/wiki/Martin_Hellman' }, { label: 'ACM A.M. Turing Award 2015', url: 'https://amturing.acm.org/award_winners/hellman_4055781.cfm' }, @@ -102,6 +106,8 @@ export const pros: Pro[] = [ 'Co-invented the RSA cryptosystem at MIT in 1977 with Shamir and Adleman, turning public-key cryptography from a proposal into something you could ship. Went on to design MD2, MD4, MD5, RC4, RC5 and RC6, co-author the standard algorithms textbook, and spend his later career on verifiable election systems. Turing Award, 2002.', note: 'Half the acronyms in a 1990s protocol stack trace back to one office at MIT.', status: 'locked', + front: '/cards/security/003-ron-rivest-front.png', + back: '/cards/security/003-ron-rivest-back.png', sources: [ { label: 'Ron Rivest', url: 'https://en.wikipedia.org/wiki/Ron_Rivest' }, { label: 'ACM A.M. Turing Award 2002', url: 'https://amturing.acm.org/award_winners/rivest_1403005.cfm' }, @@ -126,6 +132,8 @@ export const pros: Pro[] = [ 'Co-invented RSA, then invented Shamir secret sharing, then co-developed differential cryptanalysis with Eli Biham — the technique that turned out to be the one the DES designers had quietly hardened against seventeen years earlier. Also a co-author of the cube attack and a long line of side-channel work. Turing Award, 2002.', note: 'Builds the lock and breaks the lock, usually in the same decade.', status: 'locked', + front: '/cards/security/004-adi-shamir-front.png', + back: '/cards/security/004-adi-shamir-back.png', sources: [ { label: 'Adi Shamir', url: 'https://en.wikipedia.org/wiki/Adi_Shamir' }, { label: 'Differential cryptanalysis', url: 'https://en.wikipedia.org/wiki/Differential_cryptanalysis' }, @@ -150,6 +158,8 @@ export const pros: Pro[] = [ 'The third name on the RSA paper, whose role was to keep breaking the schemes Rivest and Shamir proposed until one survived. Later coined the term “computer virus” for Fred Cohen’s 1983 experiments, and founded DNA computing with a molecular solution to the Hamiltonian path problem. Turing Award, 2002.', note: 'The designated attacker on the RSA team. The set needs more of those.', status: 'locked', + front: '/cards/security/005-leonard-adleman-front.png', + back: '/cards/security/005-leonard-adleman-back.png', sources: [ { label: 'Leonard Adleman', url: 'https://en.wikipedia.org/wiki/Leonard_Adleman' }, { label: 'ACM A.M. Turing Award 2002', url: 'https://amturing.acm.org/award_winners/adleman_7308544.cfm' }, @@ -174,6 +184,8 @@ export const pros: Pro[] = [ 'Released Pretty Good Privacy as free software in 1991, and it escaped onto the internet worldwide. The US government opened a three-year criminal investigation into him for munitions export, dropped it in 1996 without charges, and the crypto-export regime never recovered. He later co-founded Silent Circle and worked on ZRTP for encrypted voice.', note: 'The card for everyone who has ever typed a passphrase to read their own mail.', status: 'locked', + front: '/cards/security/006-phil-zimmermann-front.png', + back: '/cards/security/006-phil-zimmermann-back.png', sources: [ { label: 'Phil Zimmermann', url: 'https://en.wikipedia.org/wiki/Phil_Zimmermann' }, { label: 'Pretty Good Privacy', url: 'https://en.wikipedia.org/wiki/Pretty_Good_Privacy' }, @@ -198,6 +210,8 @@ export const pros: Pro[] = [ 'Wrote Applied Cryptography in 1994, the book that taught a generation of engineers how ciphers actually work, then designed Blowfish and co-designed Twofish. Spent the two decades after arguing the harder point: that security is a systems and incentives problem, not a maths problem. Coined “security theatre”, has written the Crypto-Gram newsletter since 1998, and helped report on the Snowden documents.', note: 'More people entered this field because of one book than any other single cause.', status: 'locked', + front: '/cards/security/007-bruce-schneier-front.png', + back: '/cards/security/007-bruce-schneier-back.png', sources: [ { label: 'Bruce Schneier', url: 'https://en.wikipedia.org/wiki/Bruce_Schneier' }, { label: 'Schneier on Security', url: 'https://www.schneier.com/' }, @@ -222,6 +236,8 @@ export const pros: Pro[] = [ 'Published the timing-attack paper in 1996 and differential power analysis in 1999, establishing that a correct implementation of a correct algorithm can still leak its key through how long it takes or how much current it draws. Co-authored the SSL 3.0 specification, founded Cryptography Research, and in 2018 was one of the co-discoverers of Spectre.', note: 'Twice made the whole industry re-audit hardware it had already shipped.', status: 'locked', + front: '/cards/security/008-paul-kocher-front.png', + back: '/cards/security/008-paul-kocher-back.png', sources: [ { label: 'Paul Kocher', url: 'https://en.wikipedia.org/wiki/Paul_Kocher' }, { label: 'Spectre (security vulnerability)', url: 'https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)' }, @@ -247,6 +263,8 @@ export const pros: Pro[] = [ 'Published “An Intrusion-Detection Model” in 1987, the paper that framed detection as statistical deviation from a profile of normal behaviour and gave the entire monitoring industry its foundation. Also wrote Cryptography and Data Security, worked on database security and lattice-based information flow, and was one of the few academics engaging seriously with both sides of the Clipper chip debate.', note: 'Every alert queue in every SOC is downstream of one 1987 paper.', status: 'locked', + front: '/cards/security/009-dorothy-denning-front.png', + back: '/cards/security/009-dorothy-denning-back.png', sources: [ { label: 'Dorothy E. Denning', url: 'https://en.wikipedia.org/wiki/Dorothy_E._Denning' }, { label: 'An Intrusion-Detection Model (1987)', url: 'https://www.cs.colostate.edu/~cs656/reading/ieee-se-13-2.pdf' }, @@ -272,6 +290,8 @@ export const pros: Pro[] = [ 'Produced one of the first detailed technical analyses of the 1988 Morris worm while the internet was still smoking, co-wrote Practical UNIX and Internet Security with Simson Garfinkel, co-created Tripwire, and founded Purdue’s COAST lab and then CERIAS — for years the largest academic security centre in the US. Has advised more federal panels than most people have attended.', note: 'Trained a large fraction of the people on the rest of these cards.', status: 'locked', + front: '/cards/security/010-gene-spafford-front.png', + back: '/cards/security/010-gene-spafford-back.png', sources: [ { label: 'Gene Spafford', url: 'https://en.wikipedia.org/wiki/Gene_Spafford' }, { label: 'CERIAS, Purdue University', url: 'https://www.cerias.purdue.edu/' }, @@ -296,6 +316,8 @@ export const pros: Pro[] = [ 'Cambridge professor who wrote Security Engineering, the closest thing the field has to a canonical text, and gave away earlier editions free online. Founded the security-economics research programme with the observation that systems fail because the people who could fix them are not the people who bear the loss. Spent years demonstrating that bank chip-and-PIN systems were weaker than the banks told courts they were. Died in 2024.', note: 'If you only read one book on this list, it is this one.', status: 'locked', + front: '/cards/security/011-ross-anderson-front.png', + back: '/cards/security/011-ross-anderson-back.png', sources: [ { label: 'Ross J. Anderson', url: 'https://en.wikipedia.org/wiki/Ross_J._Anderson' }, { label: 'Security Engineering, third edition', url: 'https://www.cl.cam.ac.uk/~rja14/book.html' }, @@ -320,6 +342,8 @@ export const pros: Pro[] = [ 'Found the protocol flaw in the NSA’s Clipper chip escrow scheme in 1994 and published it, which did more to end mandated key escrow than any amount of lobbying. Later led state-commissioned source reviews of US voting systems, published work on the vulnerabilities of master-keyed physical locks and wiretap systems, and became a law-school professor arguing the same points to a different audience.', note: 'Has broken the government’s preferred backdoor design once already.', status: 'locked', + front: '/cards/security/012-matt-blaze-front.png', + back: '/cards/security/012-matt-blaze-back.png', sources: [ { label: 'Matt Blaze', url: 'https://en.wikipedia.org/wiki/Matt_Blaze' }, { label: 'Protocol Failure in the Escrowed Encryption Standard (1994)', url: 'https://www.mattblaze.org/papers/eesproto.pdf' }, @@ -344,6 +368,8 @@ export const pros: Pro[] = [ 'Invented the spanning-tree protocol that made large bridged Ethernets possible, and did the early work on routing protocols designed to keep functioning when a participating node is actively hostile rather than merely broken. Co-wrote Network Security: Private Communication in a Public World, and designed the “ephemerizer” approach to making data reliably expire.', note: 'Designed for the assumption that some of the network is lying to you.', status: 'locked', + front: '/cards/security/013-radia-perlman-front.png', + back: '/cards/security/013-radia-perlman-back.png', sources: [ { label: 'Radia Perlman', url: 'https://en.wikipedia.org/wiki/Radia_Perlman' }, { label: 'Internet Hall of Fame profile', url: 'https://www.internethalloffame.org/inductee/radia-perlman/' }, @@ -368,6 +394,8 @@ export const pros: Pro[] = [ 'Wrote the 1989 paper cataloguing the security problems in TCP/IP — sequence-number prediction, source routing, DNS spoofing — years before anyone was exploiting them at scale. Co-authored Firewalls and Internet Security with Cheswick, co-invented encrypted key exchange, was a Usenet co-creator, and served as chief technologist at the FTC and on the Privacy and Civil Liberties Oversight Board.', note: 'Published the attack list first and spent thirty years watching it come true.', status: 'locked', + front: '/cards/security/014-steven-bellovin-front.png', + back: '/cards/security/014-steven-bellovin-back.png', sources: [ { label: 'Steven M. Bellovin', url: 'https://en.wikipedia.org/wiki/Steven_M._Bellovin' }, { label: 'Security Problems in the TCP/IP Protocol Suite (1989)', url: 'https://www.cs.columbia.edu/~smb/papers/ipext.pdf' }, @@ -393,6 +421,8 @@ export const pros: Pro[] = [ 'Co-wrote the 1994 book that taught the industry what a firewall was and how to reason about a perimeter. “An Evening with Berferd” documented him feeding a live intruder a fake environment for months and writing down everything, which is the honeypot genre’s founding text. Later ran the Internet Mapping Project at Bell Labs.', note: 'Invented the practice of watching the attacker instead of just shutting the door.', status: 'locked', + front: '/cards/security/015-william-cheswick-front.png', + back: '/cards/security/015-william-cheswick-back.png', sources: [ { label: 'William Cheswick', url: 'https://en.wikipedia.org/wiki/William_Cheswick' }, { label: 'An Evening with Berferd (1992)', url: 'https://www.cheswick.com/ches/papers/berferd.pdf' }, @@ -417,6 +447,8 @@ export const pros: Pro[] = [ 'Built DEC SEAL, generally credited as the first commercial firewall product, then the TIS Firewall Toolkit and Gauntlet — the code most early internet perimeters were actually made of. Ran whitehouse.gov’s first email server. Has spent the decades since publicly arguing that most of what the industry sells does not work, which has aged better than the products did.', note: 'The perimeter, as an idea people could buy, is largely his fault and his credit.', status: 'locked', + front: '/cards/security/016-marcus-ranum-front.png', + back: '/cards/security/016-marcus-ranum-back.png', sources: [ { label: 'Marcus J. Ranum', url: 'https://en.wikipedia.org/wiki/Marcus_J._Ranum' }, { label: 'Firewall (computing) — history', url: 'https://en.wikipedia.org/wiki/Firewall_(computing)' }, @@ -441,6 +473,8 @@ export const pros: Pro[] = [ 'Wrote TCP Wrapper, which for years was the access control on a very large share of Unix hosts on the internet, and Postfix, a mail server written from the start around privilege separation. Co-wrote SATAN with Dan Farmer and later the Coroner’s Toolkit and Forensic Discovery, giving incident responders their first real open tooling.', note: 'Shipped the defence, then shipped the tools to work out how it was beaten.', status: 'locked', + front: '/cards/security/017-wietse-venema-front.png', + back: '/cards/security/017-wietse-venema-back.png', sources: [ { label: 'Wietse Venema', url: 'https://en.wikipedia.org/wiki/Wietse_Venema' }, { label: 'Postfix', url: 'https://www.postfix.org/' }, @@ -465,6 +499,8 @@ export const pros: Pro[] = [ 'Wrote COPS as a student, then in 1995 released SATAN with Wietse Venema — a scanner that audited a network the way an attacker would, published openly so defenders could run it first. The press predicted the end of the internet; SGI fired him over it; the security scanner became a permanent product category. Later co-wrote Forensic Discovery.', note: 'The original argument that defenders should get the attacker’s tools too.', status: 'locked', + front: '/cards/security/018-dan-farmer-front.png', + back: '/cards/security/018-dan-farmer-back.png', sources: [ { label: 'Dan Farmer', url: 'https://en.wikipedia.org/wiki/Dan_Farmer' }, { label: 'SATAN', url: 'https://en.wikipedia.org/wiki/Security_Administrator_Tool_for_Analyzing_Networks' }, @@ -490,6 +526,8 @@ export const pros: Pro[] = [ 'Released Nmap in Phrack in 1997 and has maintained it ever since, adding OS fingerprinting, service detection and the NSE scripting engine. Runs seclists.org, hosting the Bugtraq and Full Disclosure archives that are the field’s institutional memory, and fought MPAA and studio takedown attempts over the tool. Nmap is the first command most defenders learn.', note: 'Twenty-eight years of one maintainer. The reliability is the achievement.', status: 'locked', + front: '/cards/security/019-gordon-lyon-front.png', + back: '/cards/security/019-gordon-lyon-back.png', sources: [ { label: 'Gordon Lyon', url: 'https://en.wikipedia.org/wiki/Gordon_Lyon' }, { label: 'Nmap', url: 'https://nmap.org/' }, @@ -514,6 +552,8 @@ export const pros: Pro[] = [ 'Wrote Snort in 1998 as a lightweight packet sniffer, and it became the intrusion detection system that put IDS within reach of organisations that could never have bought one. Founded Sourcefire around it, which Cisco acquired in 2013. The Snort rule syntax outlived the product and is still how a large part of the industry writes network detections.', note: 'A weekend tool that became the de facto standard for network detection.', status: 'locked', + front: '/cards/security/020-martin-roesch-front.png', + back: '/cards/security/020-martin-roesch-back.png', sources: [ { label: 'Martin Roesch', url: 'https://en.wikipedia.org/wiki/Martin_Roesch' }, { label: 'Snort', url: 'https://www.snort.org/' }, @@ -538,6 +578,8 @@ export const pros: Pro[] = [ 'Started Ethereal in 1998 because commercial protocol analysers cost more than his employer would spend, renamed it Wireshark in 2006 after a trademark problem, and has shepherded it ever since through thousands of contributed protocol dissectors. It is the default answer to “what is actually on the wire” for network engineers and incident responders alike.', note: 'The tool you reach for when nobody can agree on what the traffic is doing.', status: 'locked', + front: '/cards/security/021-gerald-combs-front.png', + back: '/cards/security/021-gerald-combs-back.png', sources: [ { label: 'Wireshark', url: 'https://en.wikipedia.org/wiki/Wireshark' }, { label: 'Wireshark project', url: 'https://www.wireshark.org/about.html' }, @@ -562,6 +604,8 @@ export const pros: Pro[] = [ 'Released Nessus in 1998 as a free scanner with its own plugin language, at a point when the alternative was an expensive appliance. Co-founded Tenable Network Security around it in 2002 and took the product closed-source in 2005, a decision the community argued about for years. Vulnerability management as a routine operational practice largely grew out of that codebase.', note: 'The scan report that lands in an admin’s inbox every Monday started here.', status: 'locked', + front: '/cards/security/022-renaud-deraison-front.png', + back: '/cards/security/022-renaud-deraison-back.png', sources: [ { label: 'Nessus (software)', url: 'https://en.wikipedia.org/wiki/Nessus_(software)' }, { label: 'Tenable leadership', url: 'https://www.tenable.com/about-tenable/leadership' }, @@ -586,6 +630,8 @@ export const pros: Pro[] = [ 'Maintained BIND for years and wrote or co-wrote a long run of DNS RFCs, then built the response-policy zone mechanism that lets defenders block malicious domains at the resolver. Founded the first anti-spam DNSBL, ran the Internet Software Consortium, operates F-root, and co-founded Farsight Security around passive DNS. Internet Hall of Fame, 2014.', note: 'Ran the plumbing, then built the filters when the plumbing got abused.', status: 'locked', + front: '/cards/security/023-paul-vixie-front.png', + back: '/cards/security/023-paul-vixie-back.png', sources: [ { label: 'Paul Vixie', url: 'https://en.wikipedia.org/wiki/Paul_Vixie' }, { label: 'Internet Hall of Fame profile', url: 'https://www.internethalloffame.org/inductee/paul-vixie/' }, @@ -610,6 +656,8 @@ export const pros: Pro[] = [ 'Wrote honeyd, co-designed the bcrypt password hash with David Mazières, and created systrace and privilege separation work in OpenSSH. At Google he led the Safe Browsing malware research that put interstitial warnings in front of drive-by download sites, and co-authored the papers that measured how large that problem actually was.', note: 'bcrypt alone has protected more passwords than most companies have users.', status: 'locked', + front: '/cards/security/024-niels-provos-front.png', + back: '/cards/security/024-niels-provos-back.png', sources: [ { label: 'Niels Provos', url: 'https://en.wikipedia.org/wiki/Niels_Provos' }, { label: 'A Future-Adaptable Password Scheme (bcrypt, 1999)', url: 'https://www.usenix.org/legacy/events/usenix99/provos/provos.pdf' }, @@ -632,8 +680,10 @@ export const pros: Pro[] = [ domains: ['Web application security', 'Cross-site scripting', 'Vulnerability data'], scouting: 'Was an information security officer at Yahoo before founding WhiteHat Security in 2001, where continuous scanning of thousands of production sites produced the first credible public statistics on how long real web vulnerabilities actually stay open. Co-founded the Web Application Security Consortium, and co-authored XSS Attacks: Cross Site Scripting Exploits and Defense in 2007 — the book that turned cross-site scripting from a curiosity into a class of bug developers were expected to know.', - note: 'Named on the request. Earns the slot anyway: the XSS book is the reference.', + note: 'The XSS book is still the reference. Web appsec became a discipline on his watch.', status: 'locked', + front: '/cards/security/025-jeremiah-grossman-front.png', + back: '/cards/security/025-jeremiah-grossman-back.png', sources: [ { label: 'XSS Attacks: Cross Site Scripting Exploits and Defense', url: 'https://books.google.com/books/about/XSS_Attacks.html?id=FKN5uL57tyAC' }, { label: 'Jeremiah Grossman’s blog', url: 'https://blog.jeremiahgrossman.com/' }, @@ -660,6 +710,8 @@ export const pros: Pro[] = [ 'Ran ha.ckers.org and published the XSS cheat sheet, which for years was the reference list of filter-evasion payloads that every web application firewall was tested against. Co-authored XSS Attacks with Grossman, released Slowloris to demonstrate that a single machine could hold a web server’s connections open indefinitely, and co-wrote Detecting Malice on behavioural detection.', note: 'The cheat sheet did more for input validation than any standards document.', status: 'locked', + front: '/cards/security/026-robert-hansen-front.png', + back: '/cards/security/026-robert-hansen-back.png', sources: [ { label: 'XSS Attacks: Cross Site Scripting Exploits and Defense', url: 'https://books.google.com/books/about/XSS_Attacks.html?id=FKN5uL57tyAC' }, { label: 'Slowloris (computer security)', url: 'https://en.wikipedia.org/wiki/Slowloris_(cyber_attack)' }, @@ -684,6 +736,8 @@ export const pros: Pro[] = [ 'Started the Open Web Application Security Project in September 2001 as a mailing list and a few documents, and it became the vendor-neutral body that produced the Top Ten, the ASVS, ZAP and the testing guides that appsec teams still work from. Later founded SourceClear on software composition analysis and has kept publicly pushing OWASP to stay relevant to modern supply-chain risk.', note: 'Built the commons the whole application security industry now cites.', status: 'locked', + front: '/cards/security/027-mark-curphey-front.png', + back: '/cards/security/027-mark-curphey-back.png', sources: [ { label: 'OWASP', url: 'https://en.wikipedia.org/wiki/OWASP' }, { label: 'Mark Curphey — OWASP Foundation', url: 'https://owasp.org/www-board-candidates/2022/mark_curphey_2022' }, @@ -708,6 +762,8 @@ export const pros: Pro[] = [ 'Wrote the first OWASP Top Ten and served as the volunteer chair of OWASP from 2003 to 2011, the years in which it went from a mailing list to the reference that PCI DSS and a long line of other standards cite by name. Co-founded Aspect Security and then Contrast Security, arguing for instrumentation inside the running application rather than scanning it from outside.', note: 'One list, and suddenly every auditor knew what SQL injection was.', status: 'locked', + front: '/cards/security/028-jeff-williams-front.png', + back: '/cards/security/028-jeff-williams-back.png', sources: [ { label: 'OWASP', url: 'https://en.wikipedia.org/wiki/OWASP' }, { label: 'OWASP Top Ten project', url: 'https://owasp.org/www-project-top-ten/' }, @@ -732,6 +788,8 @@ export const pros: Pro[] = [ 'Wrote Building Secure Software, Exploiting Software and Software Security, making the case that you cannot test defects out of a system you designed insecurely. Co-created BSIMM, which measures what security programmes actually do rather than what a standard says they should, and has run a long-standing interview series documenting the field’s own history.', note: 'Moved the argument from “scan it later” to “design it right”.', status: 'locked', + front: '/cards/security/029-gary-mcgraw-front.png', + back: '/cards/security/029-gary-mcgraw-back.png', sources: [ { label: 'Gary McGraw', url: 'https://en.wikipedia.org/wiki/Gary_McGraw' }, { label: 'BSIMM', url: 'https://www.bsimm.com/' }, @@ -756,6 +814,8 @@ export const pros: Pro[] = [ 'Co-wrote Writing Secure Code, which Bill Gates ordered Windows engineers to read during the 2002 Trustworthy Computing halt, and helped build the Security Development Lifecycle that came out of it — threat modelling, banned APIs, fuzzing and a final security review before ship. The SDL became the template most large software organisations copied.', note: 'Turned “be careful” into a checklist an entire company could be held to.', status: 'locked', + front: '/cards/security/030-michael-howard-front.png', + back: '/cards/security/030-michael-howard-back.png', sources: [ { label: 'Microsoft Security Development Lifecycle', url: 'https://en.wikipedia.org/wiki/Microsoft_Security_Development_Lifecycle' }, { label: 'Writing Secure Code, second edition', url: 'https://www.microsoftpressstore.com/store/writing-secure-code-9780735617223' }, @@ -780,6 +840,8 @@ export const pros: Pro[] = [ 'Drove threat modelling into shippable practice at Microsoft, including the Elevation of Privilege card game that got non-security engineers doing it voluntarily, then wrote Threat Modeling: Designing for Security and later Threats: What Every Engineer Should Learn. Served on the CVE editorial board and co-wrote The New School of Information Security on learning from breach data.', note: 'Four questions — what are we building, what can go wrong, what do we do, did we do a good job.', status: 'locked', + front: '/cards/security/031-adam-shostack-front.png', + back: '/cards/security/031-adam-shostack-back.png', sources: [ { label: 'Adam Shostack’s site', url: 'https://shostack.org/about/adam' }, { label: 'Elevation of Privilege threat modelling game', url: 'https://shostack.org/games/elevation-of-privilege' }, @@ -804,6 +866,8 @@ export const pros: Pro[] = [ 'Wrote ModSecurity, the open-source web application firewall that became the reference implementation and the base of the OWASP Core Rule Set. Then built SSL Labs, whose A-to-F grade for a server’s TLS configuration turned an obscure ops detail into something executives asked about, and wrote Bulletproof TLS and PKI as the manual for fixing the grade.', note: 'Made bad TLS visible and embarrassing, which fixed more of it than any RFC.', status: 'locked', + front: '/cards/security/032-ivan-ristic-front.png', + back: '/cards/security/032-ivan-ristic-back.png', sources: [ { label: 'Ivan Ristić', url: 'https://en.wikipedia.org/wiki/Ivan_Risti%C4%87' }, { label: 'Qualys SSL Labs', url: 'https://www.ssllabs.com/' }, @@ -828,6 +892,8 @@ export const pros: Pro[] = [ 'Co-wrote The Art of Software Security Assessment with John McDonald and Justin Schuh, still the most thorough published treatment of how to find memory-safety and logic flaws by reading code. Found deep bugs in Sendmail, OpenSSH and Flash while at ISS X-Force and IBM, and founded Azimuth Security. His work is why a generation of auditors know what to look for in a parser.', note: 'The book that turned code review from a chore into a craft.', status: 'locked', + front: '/cards/security/033-mark-dowd-front.png', + back: '/cards/security/033-mark-dowd-back.png', sources: [ { label: 'The Art of Software Security Assessment', url: 'https://www.informit.com/store/art-of-software-security-assessment-identifying-and-9780321444424' }, { label: 'Azimuth Security', url: 'https://www.azimuthsecurity.com/' }, @@ -853,6 +919,8 @@ export const pros: Pro[] = [ 'Built the graph-based binary diffing that made it routine to extract the vulnerability out of a vendor patch, founded zynamics around BinDiff and VxClass, and sold it to Google in 2011. Later worked in Project Zero and wrote the “weird machines” framing that treats exploitation as programming an unintended state machine inside the target.', note: 'Turned Patch Tuesday into a reliable source of exploit intelligence.', status: 'locked', + front: '/cards/security/034-halvar-flake-front.png', + back: '/cards/security/034-halvar-flake-back.png', sources: [ { label: 'BinDiff', url: 'https://www.zynamics.com/bindiff.html' }, { label: 'Thomas Dullien’s research blog', url: 'https://addxorrol.blogspot.com/' }, @@ -877,6 +945,8 @@ export const pros: Pro[] = [ 'Co-authored “Bypassing Browser Memory Protections”, the 2008 paper that showed how attackers were routing around DEP and ASLR, then at 25C3 the same year demonstrated a working rogue certificate authority built on MD5 collisions — which pushed the CA industry off MD5 for good. Co-founded Trail of Bits.', note: 'Demonstrated the theoretical break against a live CA, which is the only kind that moves anyone.', status: 'locked', + front: '/cards/security/035-alex-sotirov-front.png', + back: '/cards/security/035-alex-sotirov-back.png', sources: [ { label: 'MD5 considered harmful today (2008)', url: 'https://www.win.tue.nl/hashclash/rogue-ca/' }, { label: 'Bypassing Browser Memory Protections (2008)', url: 'https://www.blackhat.com/presentations/bh-usa-08/Sotirov_Dowd/bh08-sotirov-dowd.pdf' }, @@ -901,6 +971,8 @@ export const pros: Pro[] = [ 'Co-wrote the 1998 paper with Timothy Newsham showing that a network IDS which reassembles traffic differently from the host it protects can be walked straight past — the work that forced every vendor to rethink normalisation. Co-founded Matasano and later Latacora, and wrote the Cryptopals challenges that taught a generation how crypto actually breaks.', note: 'One paper made every IDS vendor rewrite their TCP stack.', status: 'locked', + front: '/cards/security/036-thomas-ptacek-front.png', + back: '/cards/security/036-thomas-ptacek-back.png', sources: [ { label: 'Insertion, Evasion, and Denial of Service (1998)', url: 'https://insecure.org/stf/secnet_ids/secnet_ids.html' }, { label: 'The Cryptopals crypto challenges', url: 'https://cryptopals.com/' }, @@ -925,6 +997,8 @@ export const pros: Pro[] = [ 'Johns Hopkins cryptographer who co-led the Open Crypto Audit Project’s review of TrueCrypt, contributed to the analysis of Dual_EC_DRBG and the RSA BSAFE backdoor, and worked on the attacks behind Logjam and DROWN. His blog is the standard place the rest of the industry goes to find out how badly a new crypto story is being reported.', note: 'Does the audit, then does the translation so the rest of us can follow it.', status: 'locked', + front: '/cards/security/037-matthew-green-front.png', + back: '/cards/security/037-matthew-green-back.png', sources: [ { label: 'Matthew D. Green', url: 'https://en.wikipedia.org/wiki/Matthew_D._Green' }, { label: 'A Few Thoughts on Cryptographic Engineering', url: 'https://blog.cryptographyengineering.com/' }, @@ -949,6 +1023,8 @@ export const pros: Pro[] = [ 'Co-authored the cold boot attack paper showing DRAM retains keys after power off, then spent a career demonstrating — in court and in front of legislatures — that deployed voting machines could be reprogrammed. Co-founded the Internet Security Research Group behind Let’s Encrypt, and co-authored the ZMap internet-wide scanner and the Logjam and FREAK results.', note: 'Let’s Encrypt alone changed the default state of the web from plaintext to TLS.', status: 'locked', + front: '/cards/security/038-j-alex-halderman-front.png', + back: '/cards/security/038-j-alex-halderman-back.png', sources: [ { label: 'J. Alex Halderman', url: 'https://en.wikipedia.org/wiki/J._Alex_Halderman' }, { label: 'Lest We Remember: Cold Boot Attacks on Encryption Keys', url: 'https://citp.princeton.edu/our-work/memory/' }, @@ -973,6 +1049,8 @@ export const pros: Pro[] = [ 'Co-authored “Mining Your Ps and Qs”, which scanned the whole IPv4 internet and found that a meaningful share of TLS and SSH keys shared factors because embedded devices generated them without entropy — and then factored them. Followed it with Logjam, DROWN and FREAK, a body of work whose method is: measure what is actually deployed, then break the weak part.', note: 'Proved the crypto was fine and the random number generators were not.', status: 'locked', + front: '/cards/security/039-nadia-heninger-front.png', + back: '/cards/security/039-nadia-heninger-back.png', sources: [ { label: 'Nadia Heninger', url: 'https://en.wikipedia.org/wiki/Nadia_Heninger' }, { label: 'Mining Your Ps and Qs (USENIX Security 2012)', url: 'https://factorable.net/paper.html' }, @@ -997,6 +1075,8 @@ export const pros: Pro[] = [ 'Built Have I Been Pwned in 2013 as a way for ordinary people to find out whether their account was in a dump, and it became infrastructure: browsers, password managers and the NIST-endorsed practice of checking passwords against known-breached lists all query it, using a k-anonymity scheme that never sends the password. Runs it as a public good rather than a product.', note: 'One person’s side project ended up inside the login flow of half the web.', status: 'locked', + front: '/cards/security/040-troy-hunt-front.png', + back: '/cards/security/040-troy-hunt-back.png', sources: [ { label: 'Troy Hunt', url: 'https://en.wikipedia.org/wiki/Troy_Hunt' }, { label: 'Have I Been Pwned', url: 'https://haveibeenpwned.com/' }, @@ -1021,6 +1101,8 @@ export const pros: Pro[] = [ 'Was security lead and signoff for Windows XP Service Pack 2 and Windows Server 2003, co-wrote the Threat Modeling book that came out of that era, then ran security at Mozilla for Firefox, at Apple on privacy and security strategy, at Fastly and Square, and became Intel’s first chief software security officer. Founded Thistle Technologies in 2020 to bring update infrastructure to embedded devices.', note: 'Has been the person accountable for a shipping product’s security more times than anyone.', status: 'locked', + front: '/cards/security/041-window-snyder-front.png', + back: '/cards/security/041-window-snyder-back.png', sources: [ { label: 'Window Snyder', url: 'https://en.wikipedia.org/wiki/Window_Snyder' }, { label: 'Thistle Technologies', url: 'https://thistle.tech/' }, @@ -1045,6 +1127,8 @@ export const pros: Pro[] = [ 'A founding member of Google’s security team who built and ran its incident response function, including the response to the 2009 Operation Aurora intrusion that Google chose to disclose publicly — a decision that reset industry norms on breach transparency. Co-authored Building Secure and Reliable Systems, and has served on US federal cybersecurity advisory bodies.', note: 'Ran the response that made public disclosure of a state intrusion normal.', status: 'locked', + front: '/cards/security/042-heather-adkins-front.png', + back: '/cards/security/042-heather-adkins-back.png', sources: [ { label: 'Building Secure and Reliable Systems', url: 'https://sre.google/books/building-secure-reliable-systems/' }, { label: 'Operation Aurora', url: 'https://en.wikipedia.org/wiki/Operation_Aurora' }, @@ -1069,6 +1153,8 @@ export const pros: Pro[] = [ 'Joined Google as a penetration tester, printed “Security Princess” on her business card, and rose to run Chrome. Led the multi-year project to mark plain HTTP as “Not secure” in the world’s most-used browser, which more than anything else moved the web to encryption by default, and pushed Project Zero’s 90-day disclosure deadline as an industry-wide forcing function.', note: 'Changed one string in a browser UI and moved the whole web to TLS.', status: 'locked', + front: '/cards/security/043-parisa-tabriz-front.png', + back: '/cards/security/043-parisa-tabriz-back.png', sources: [ { label: 'Parisa Tabriz', url: 'https://en.wikipedia.org/wiki/Parisa_Tabriz' }, { label: 'Chromium: marking HTTP as not secure', url: 'https://blog.chromium.org/2018/02/a-secure-web-is-here-to-stay.html' }, @@ -1093,6 +1179,8 @@ export const pros: Pro[] = [ 'Was CISO at Yahoo, where he objected to a government email-scanning programme, then CSO at Facebook through the investigation into foreign influence operations, leaving in 2018 over how much the company would disclose. Publicly challenged the NSA director on encryption backdoors, founded the Stanford Internet Observatory, and co-founded iSEC Partners and later Krebs Stamos Group.', note: 'The card about what the job costs when you do it honestly.', status: 'locked', + front: '/cards/security/044-alex-stamos-front.png', + back: '/cards/security/044-alex-stamos-back.png', sources: [ { label: 'Alex Stamos', url: 'https://en.wikipedia.org/wiki/Alex_Stamos' }, { label: 'Stanford Internet Observatory', url: 'https://cyber.fsi.stanford.edu/io' }, @@ -1117,6 +1205,8 @@ export const pros: Pro[] = [ 'Co-authored the 2003 CCIA report arguing that monoculture in operating systems was itself a national security risk, and was fired by @stake, then a Microsoft consultancy, the day it was published. Went on to serve as chief information security officer of In-Q-Tel and to give a run of Black Hat and USENIX keynotes that are still the most quoted long-form arguments about security policy the field has produced.', note: 'Paid for the paper with his job, and the paper was right.', status: 'locked', + front: '/cards/security/045-dan-geer-front.png', + back: '/cards/security/045-dan-geer-back.png', sources: [ { label: 'Dan Geer', url: 'https://en.wikipedia.org/wiki/Dan_Geer' }, { label: 'CyberInsecurity: The Cost of Monopoly (2003)', url: 'https://www.schneier.com/essays/archives/2003/09/cyberinsecurity_the.html' }, @@ -1141,6 +1231,8 @@ export const pros: Pro[] = [ 'Founded Mandiant in 2004 and built breach response into a business that governments and Fortune 100 boards call at 2am. In 2013 the company published APT1, a report attributing years of intrusions to a specific People’s Liberation Army unit with building photographs and operator handles — the moment public, named attribution became something private companies did.', note: 'Made attribution a commercial deliverable instead of a classified one.', status: 'locked', + front: '/cards/security/046-kevin-mandia-front.png', + back: '/cards/security/046-kevin-mandia-back.png', sources: [ { label: 'Mandiant', url: 'https://en.wikipedia.org/wiki/Mandiant' }, { label: 'APT1: Exposing One of China’s Cyber Espionage Units (2013)', url: 'https://www.mandiant.com/resources/reports/apt1-exposing-one-chinas-cyber-espionage-units' }, @@ -1165,6 +1257,8 @@ export const pros: Pro[] = [ 'Has been analysing malware at F-Secure since 1991, through the DOS virus era, Sasser and Blaster, Stuxnet and modern ransomware. Tracked down and interviewed the Pakistani brothers who wrote Brain, the first PC virus, twenty years after the fact. Formulated Hyppönen’s law — if it is smart, it is vulnerable — and is the researcher most often trusted to explain a live incident to the public.', note: 'The continuity card. One person watched the entire history of malware happen.', status: 'locked', + front: '/cards/security/047-mikko-hypponen-front.png', + back: '/cards/security/047-mikko-hypponen-back.png', sources: [ { label: 'Mikko Hyppönen', url: 'https://en.wikipedia.org/wiki/Mikko_Hypp%C3%B6nen' }, { label: 'Brain (computer virus)', url: 'https://en.wikipedia.org/wiki/Brain_(computer_virus)' }, @@ -1189,6 +1283,8 @@ export const pros: Pro[] = [ 'A former US Air Force cyber warfare officer who co-authored the analysis of the 2015 Ukrainian power grid attack and the later CRASHOVERRIDE and TRISIS malware, then founded Dragos to do industrial control system defence as a specialism rather than an IT afterthought. Co-wrote the SANS ICS courses that most OT defenders come through.', note: 'The set’s reminder that some networks fail into a physical consequence.', status: 'locked', + front: '/cards/security/048-robert-m-lee-front.png', + back: '/cards/security/048-robert-m-lee-back.png', sources: [ { label: 'Robert M. Lee — Dragos', url: 'https://www.dragos.com/leadership/robert-m-lee/' }, { label: 'Analysis of the Cyber Attack on the Ukrainian Power Grid (SANS/E-ISAC, 2016)', url: 'https://www.nerc.com/pa/CI/ESISAC/Documents/E-ISAC_SANS_Ukraine_DUC_18Mar2016.pdf' }, @@ -1214,6 +1310,8 @@ export const pros: Pro[] = [ 'Led incident response at Motorola Solutions and then at Dragos, running investigations inside live industrial networks where you cannot simply reimage the plant. Writes long-running public guidance on breaking into and surviving the profession, founded PancakesCon, and holds the SANS Lifetime Achievement Award for that work as much as the casework.', note: 'The mentorship is the contribution. Half a conference floor learned from those posts.', status: 'locked', + front: '/cards/security/049-lesley-carhart-front.png', + back: '/cards/security/049-lesley-carhart-back.png', sources: [ { label: 'Lesley Carhart', url: 'https://en.wikipedia.org/wiki/Lesley_Carhart' }, { label: 'Lesley Carhart’s blog', url: 'https://tisiphone.net/about/' }, @@ -1238,6 +1336,8 @@ export const pros: Pro[] = [ 'Was the threat intelligence lead for MITRE ATT&CK during the years it became the shared vocabulary defenders use to describe adversary behaviour, then went to Red Canary as director of intelligence. Teaches the SANS cyber threat intelligence course and spends most of her public work on the unglamorous question of how an intelligence report turns into a detection someone actually deploys.', note: 'ATT&CK gave the blue team a language. This is the card for the person who taught it.', status: 'locked', + front: '/cards/security/050-katie-nickels-front.png', + back: '/cards/security/050-katie-nickels-back.png', sources: [ { label: 'MITRE ATT&CK', url: 'https://attack.mitre.org/' }, { label: 'Katie Nickels — SANS Institute', url: 'https://www.sans.org/profiles/katie-nickels/' }, diff --git a/tsconfig.json b/tsconfig.json index 7d553a8..b8840c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,10 @@ "skipLibCheck": true, "strict": true, "noEmit": true, + // The data modules are imported by the card pipelines under `node --strip-types`, + // which cannot resolve an extensionless relative specifier. So they import + // "./roster.ts" explicitly, which needs this flag. Legal because noEmit is set. + "allowImportingTsExtensions": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler",