Skip to content

Add AI image generation to TabletopExercise (attack vector, evidence, atmosphere) #2

@klausagnoletti

Description

@klausagnoletti

Overview

TabletopExercise currently produces HTML/PDF/QMD exercise materials that are text-only — no images. Artifacts marked screenshot render as <pre><code> blocks even when a real image would be far more pedagogically effective. This issue tracks adding AI-generated images across three pedagogically distinct categories, inspired by tabletop RPG design principles.

Three image categories

1. Attack vector visuals (what the victim saw)

Make the deception mechanism visible, not narrated. Highest teaching value.

  • Phishing email as it appeared in the inbox
  • Fake login / credential-harvesting page
  • Ransomware note / splash screen
  • Fraudulent invoice or purchase order
  • Malicious USB drive label / autorun prompt

2. Evidence visuals (what investigators find)

Give players a shared spatial model to reason against.

  • Network diagram of compromised hosts
  • SIEM alert / log screenshot
  • Dark web listing of stolen data
  • Network packet capture (visual representation)
  • SCADA/ICS interface screen

3. Atmosphere visuals (mood / world-building)

Make the scenario feel inhabited before players explore it. Borrowed directly from TTRPG design.

  • NPC portraits
  • Location illustrations (hospital, factory floor, data center)
  • Cover art for the scenario card
  • Period photographs (historical scenarios only)

Provider flexibility (first-class requirement)

No single provider covers every use-case. All providers are selected via IMAGE_PROVIDER env var; API keys are read from env — never hardcoded. New providers can be added by implementing the ImageProvider interface and registering in the provider map — no changes to calling code needed.

Provider Env var Notes
openai (default) OPENAI_API_KEY DALL-E 3 — best prompt adherence
gemini GEMINI_API_KEY Google Imagen (imagen-3.0-generate-002)
stability STABILITY_API_KEY Stability AI REST API
replicate REPLICATE_API_KEY Runs any Replicate model (default: black-forest-labs/flux-schnell)
mistral MISTRAL_API_KEY Mistral image generation endpoint
anthropic ANTHROPIC_API_KEY Prompt-enhancement layer: Claude enriches/refines prompts before routing to a generation provider; no second key needed when Claude is already in the loop
ollama none Self-hosted at OLLAMA_BASE_URL (default: http://localhost:11434)

Image subtypes with difficulty guidance

Each subtype maps to a specialised prompt builder. When image_subtype is absent, it is inferred from artifact.type and scenario_type.

Subtype Category Appropriate difficulty
ransomware_note Attack vector Intermediate+
phishing_email Attack vector All
fraudulent_invoice Attack vector All
usb_device Attack vector All (strong social-eng teaching)
scada_interface Evidence Advanced / Historical
network_capture Evidence Advanced
dark_web_listing Evidence Intermediate+
network_diagram Evidence All
period_photograph Atmosphere Historical only
portrait Atmosphere All
location_illustration Atmosphere All
cover_art Atmosphere All

Style consistency via VisualStyle

A visual_style block in the exercise data ensures cohesive visuals within a scenario — and across all scenario cards in a malmon family (e.g., all LockBit cards share the same palette).

interface VisualStyle {
  art_style?: string;      // "photorealistic" | "noir graphic novel" | "hand-drawn ink wash"
  color_palette?: string;  // "muted sepia" | "high-contrast blue-grey"
  mood?: string;           // "tense, clinical" | "cold, corporate"
  seed?: number;           // passed to providers that support deterministic generation
}

The same visual_style object is appended to every prompt as a suffix. Reusing the same block across all scenario cards in a malmon family produces a unified visual identity across the handbook.


Implementation plan

New file: generate-images.ts

  • Provider registry pattern (see above)
  • Three exported generator functions (one per MCP tool):
    export async function generateAttackVectorImage(artifact, style, provider): Promise<string>
    export async function generateEvidenceImage(artifact, style, provider): Promise<string>
    export async function generateAtmosphereImage(context, style, provider): Promise<string>
  • No new npm dependencies — uses Bun's native fetch for all provider calls

schema.ts changes (backward-compatible)

export const VisualStyleSchema = z.object({
  art_style: z.string().optional(),
  color_palette: z.string().optional(),
  mood: z.string().optional(),
  seed: z.number().int().optional(),
});

// ArtifactSchema additions:
image_data: z.string().optional()      // base64 data URI
image_subtype: z.enum([...]).optional()

// TabletopExerciseDataSchema additions:
cover_image_data: z.string().optional()
visual_style: VisualStyleSchema.optional()

mcp-server.ts — Tools 8–10

All three tools share the same contract:

  1. Validate schema → call generator → return { updated_data, images_generated, provider_used } or { error }
  2. Never throw

Tool 8: generate_attack_vector_images — populates artifact.image_data for attack-vector-type artifacts
Tool 9: generate_evidence_images — populates artifact.image_data for evidence artifacts; optionally generates exercise_data.evidence_diagram_data
Tool 10: generate_atmosphere_images — generates cover_image_data + NPC portrait_data; accepts subtypes array to limit scope

generate-pdf.ts rendering

  • Cover page: if cover_image_data<img> in header
  • Artifact blocks: if artifact.image_data<img> instead of <pre><code>; facilitator mode shows text content as caption; participant mode shows image only

generate-qmd.ts rendering

  • If artifact.image_data, write [slug].png to outputDir and use ![title](./[slug].png) instead of the code block

SKILL.md additions

  • New Image Generation section with difficulty guidance per category
  • Recommended workflow: validate_exercise_data → image tools → generate_exercise / generate_exercise_qmd
  • How visual_style provides scenario-wide and cross-scenario consistency

test-mcp.ts — Tests 19–24

All tests avoid real API calls (test the no-key error path):

Test What it covers
19 generate_attack_vector_images returns structured error when no API key present
20 generate_evidence_images same
21 generate_atmosphere_images same
22 Invalid exercise_data → schema error returned before provider is checked
23 visual_style round-trips through schema validation
24 image_subtype accepted and returned in updated_data

Acceptance criteria

  • bun run test-mcp.ts → 75+ assertions, all pass (Tests 19–24 included)
  • With OPENAI_API_KEY set: generate_attack_vector_images on SSRF fixture → images_generated > 0, artifact has valid data URI in image_data
  • generate_exercise on updated data → facilitator.html contains <img tags
  • Swap IMAGE_PROVIDER=gemini GEMINI_API_KEY=... → same result via Imagen
  • No new npm dependencies
  • New provider can be added without modifying any calling code

Related

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions