The Problem
AI security interviews don't look like typical software engineering interviews. There's rarely a LeetCode-style algorithm round. Instead, teams run a consistent set of formats that map directly to the daily job: system design for security architecture, live red teaming where you find vulnerabilities while narrating your thought process, security code review on real agent codebases, and threat modeling under a fixed time budget. Across dozens of real postings and take-home assignments, the same seven test types show up again and again — this course's build plan pulled them directly from job postings and take-homes companies actually give.
The failure mode candidates hit isn't lack of knowledge — most people who finish this course know the OWASP LLM Top 10 cold. It's that they've never rehearsed producing that knowledge live, under time pressure, while a stranger watches and occasionally interrupts with a follow-up. Threat modeling a toy chatbot alone at your desk with no time limit is a different skill from threat modeling the same system in 25 minutes while an interviewer asks "why did you skip that trust boundary?"
The interview is testing communication, not just correctness
A candidate who finds 3 real vulnerabilities but explains their reasoning clearly, states severity with a rubric, and proposes a specific fix will often outperform a candidate who finds 5 vulnerabilities but mumbles through them with no structure. Interviewers can't directly observe your security mindset — they infer it from how you narrate your process. Silence during a live red team exercise reads as "stuck," even if you're actually thinking productively. Say what you're trying next and why, even when you're not sure it'll work.
The four interview formats, and how to train for each
1. System design for security architecture — "Design a multi-layer guardrail system for this chatbot" or "design the security architecture for this new agent feature." Draw before you talk: layers (input, guardrails, model, output, audit), the trust boundary at each layer, and one specific tool per layer (NeMo Guardrails, Guardrails AI, a PII regex+NER pipeline). Interviewers are checking whether you default to defense-in-depth or a single point of failure.
2. Live red teaming (think-aloud attack discovery) — "Find the prompt injection vulnerabilities in this app," live, on a shared screen. State your attack taxonomy out loud before you start typing: direct injection, then indirect via any retrieved content, then encoding/roleplay jailbreaks. Try the boring attacks first — interviewers notice when a candidate jumps straight to an exotic technique and skips the basics.
3. Security code review on agent codebases — you're handed a repo (often intentionally vulnerable) and asked to find flaws around tool permissions, excessive agency, memory manipulation, and MCP security. Read the tool registry first, before you read any business logic — every tool the agent has access to is a line item in your review, not an afterthought.
4. Threat modeling under time pressure — 20-30 minutes to threat-model a described system (often multi-agent, with RAG and MCP tools). Draw the system as boxes and arrows before writing prose. Apply STRIDE, but translate it: "tampering" becomes prompt/context manipulation, "spoofing" becomes agent identity confusion in a multi-agent pipeline. Name at least one MITRE ATLAS tactic explicitly — it signals framework fluency, not just intuition.
During a live red teaming interview, you spend 90 seconds silently trying different jailbreak prompts against the target chatbot without saying anything. The interviewer looks concerned. What's the most likely reason, and what should you do differently?
Interviewers can't observe your reasoning directly — they infer your security mindset from what you say while you work. 90 seconds of silence looks identical whether you're making great progress or completely stuck; the interviewer has no way to tell the difference from the outside. The fix isn't to abandon the exercise or stop attempting real attacks — it's to say what you're trying and why: 'I'm starting with a roleplay jailbreak since this looks like a consumer-facing bot with light system prompt constraints' turns silence into visible, gradable reasoning, even on attempts that fail.
Drilling the format, not just the content
Studying the OWASP LLM Top 10 or MITRE ATLAS taxonomy is necessary but not sufficient — you also need repetition on producing structured answers under a clock. Spaced repetition (the same principle behind flashcard apps for language learning) works for this: drill a set of realistic interview prompts, self-grade against a checklist of what a strong answer hits, and let missed prompts come back sooner than ones you nailed.
A simple and effective scheduling method is the Leitner system: each question lives in a numbered box (1 through 5). Answer it well and it moves up a box, reviewed less frequently. Miss the key talking points and it drops back to box 1, reviewed again right away. This naturally concentrates your remaining prep time on your weakest formats instead of re-drilling what you've already mastered.
For each of the 7 real interview test formats, a strong answer has a checkable structure:
| Format | What a strong answer always includes |
|---|
| Prompt injection assessment | Severity-ranked findings, explicit rubric, reproduction steps, specific (not generic) remediation |
| Guardrail implementation | Layered architecture diagram, a named tool, logging of every blocked attempt |
| Red team engagement | Stated tool-selection logic, one full attack chain narrated end-to-end, prioritized remediation |
| Threat model | System drawn as boxes/arrows, STRIDE-for-AI applied per component, at least one named MITRE ATLAS tactic |
| Agent code review | Tool registry reviewed first, explicit check for approval gates on irreversible actions |
| Incident response | Clarifying questions asked before conclusions, root cause stated as a specific control gap, blameless post-mortem structure |
| Compliance mapping | Risk tier classified first, NIST AI RMF's 4 functions walked in order, roadmap prioritized by deadline and blast radius |
You're drilling interview questions with a Leitner-style flashcard system. You answer a threat-modeling card well and it moves from box 2 to box 3. The next day, you answer a live-red-teaming card poorly. What should happen to that card, and why does this scheduling approach make your remaining prep time more effective than reviewing all questions equally?
The entire point of box-based spaced repetition is to stop treating every question as equally worth your time. A card you've already demonstrated mastery on (moved to box 3+) doesn't need daily review — reviewing it anyway is wasted prep time. A card you just missed needs to come back immediately, before the gap in your answer becomes a gap in a real interview. Over several sessions this naturally reallocates your practice time toward whichever of the 7 interview formats you're actually weakest on, which a fixed round-robin schedule can't do.
Build
Build it
The interview_drill_runner.py script below implements a question bank covering all 7 real interview test formats (prompt injection assessment, guardrail implementation, red team engagement, threat modeling, agent code review, incident response, compliance mapping) plus a 5-box Leitner-scheduled flashcard runner. Each card includes the real prompt, what it's testing, and the specific talking points a strong answer hits. State persists across runs so review genuinely spaces out over time. Run it with --interactive to actually self-drill and grade your own spoken/written answers; without it, it runs a non-interactive demo pass so you can see the scheduling logic immediately.
"""
lesson_09_03_interview_drill_runner.py
A mock-interview question bank plus a CLI flashcard runner covering the 7
AI security interview test formats that companies actually give:
1. Prompt injection assessment
2. Guardrail implementation
3. Red team engagement (Garak + PyRIT)
4. Threat model an AI system
5. Agent security review (code review)
6. Incident response
7. Compliance mapping
Cards are scheduled with a simple 5-box Leitner system: answer correctly and
a card moves up a box (reviewed less often); answer incorrectly and it drops
back to box 1 (reviewed again immediately). State persists to a local JSON
file so repeated runs actually space out review.
Usage:
python lesson_09_03_interview_drill_runner.py # non-interactive demo run
python lesson_09_03_interview_drill_runner.py --interactive # real self-graded drill
python lesson_09_03_interview_drill_runner.py --category live_red_team --interactive
Requirements:
Standard library only. No API keys, no network calls, runs fast on CPU.
"""
from __future__ import annotations
import argparse
import json
import random
from dataclasses import dataclass, asdict
from pathlib import Path
STATE_FILE = Path("interview_drill_state.json")
BOX_INTERVALS_LABEL = {1: "review immediately", 2: "review soon", 3: "review in a few days",
4: "review in a week", 5: "mastered — spot check only"}
@dataclass
class Card:
id: str
category: str # system_design | live_red_team | code_review | threat_modeling | compliance
prompt: str
what_it_tests: str
talking_points: list[str]
time_budget_min: int
CARDS: list[Card] = [
Card(
id="pi-assessment-1",
category="live_red_team",
prompt=(
"Given this LLM customer support app, find and document all prompt injection "
"vulnerabilities. Provide a severity-ranked report with reproduction steps and "
"remediation guidance."
),
what_it_tests="OWASP LLM01 knowledge, attack creativity, report quality — this is the single most common AI security take-home.",
talking_points=[
"Narrate your process out loud: start with direct injection, then indirect via any retrieved/uploaded content, then encoding bypasses",
"Test the system prompt boundary explicitly — try to get it to reveal or override its instructions",
"For each finding, state severity using an explicit rubric (blast radius x likelihood), not just a gut feeling",
"End with a reproduction step a reader could run verbatim, and a specific remediation (structural separation, allowlist, human approval gate) — not 'add more guardrails'",
],
time_budget_min=30,
),
Card(
id="guardrail-impl-1",
category="system_design",
prompt=(
"Design and implement a multi-layer guardrail system for this customer support "
"chatbot. It must block prompt injection, detect PII, prevent unsafe content, and "
"log all blocked attempts."
),
what_it_tests="Familiarity with NeMo Guardrails / Guardrails AI, architecture design, and code quality.",
talking_points=[
"Draw the layers before writing code: input classification -> topic/PII filter -> model call -> output validation -> audit log",
"Justify why each layer is separate from model-level refusal (defense in depth, not a single point of failure)",
"Call out what gets logged and why (every blocked attempt, with the matched rule and confidence score)",
"Mention a specific tool (NeMo Guardrails' Colang rails, or Guardrails AI's Hub validators) rather than describing guardrails abstractly",
],
time_budget_min=35,
),
Card(
id="red-team-engagement-1",
category="live_red_team",
prompt=(
"Conduct a structured red team assessment against this RAG system. Use Garak and "
"PyRIT. Document findings, attack chains, and prioritized remediation."
),
what_it_tests="Tool proficiency (Garak probes, PyRIT orchestrators), methodology, and reporting discipline.",
talking_points=[
"State your tool selection logic out loud: Garak for broad automated probing first, PyRIT for targeted multi-turn/XPIA follow-up",
"Describe at least one specific Garak probe module and one PyRIT orchestrator (Crescendo or XPIAOrchestrator) by name",
"Walk through one full attack chain end-to-end, not just a list of isolated findings",
"Prioritize remediation by exploitability x business impact, and note what you'd re-test after fixes",
],
time_budget_min=40,
),
Card(
id="threat-model-1",
category="threat_modeling",
prompt=(
"Create a threat model for this multi-agent system with tool use, RAG, and MCP. "
"Identify trust boundaries, attack vectors, and blast radius for each."
),
what_it_tests="MITRE ATLAS familiarity, STRIDE for AI, and system design thinking under time pressure.",
talking_points=[
"Start by drawing the system, not writing prose — boxes for each agent, arrows for trust boundaries, tags for untrusted data sources",
"Apply STRIDE per component, but reframe for AI: 'tampering' becomes prompt/context manipulation, 'spoofing' becomes identity confusion between agents",
"For each capability (tool call), state the worst plausible misuse and its blast radius, not just the intended use",
"Map at least 2 attack vectors to specific MITRE ATLAS tactics by name to show framework fluency, not just intuition",
],
time_budget_min=25,
),
Card(
id="agent-code-review-1",
category="code_review",
prompt=(
"Review this agent codebase for security vulnerabilities. Focus on tool permissions, "
"excessive agency, memory manipulation, and MCP security."
),
what_it_tests="Code review skill, agent architecture understanding, and a security mindset applied to real code, not just concepts.",
talking_points=[
"Read the tool registry first — list every tool and ask 'does this agent's actual task need this capability?'",
"Check for a human approval gate on any irreversible action (send, delete, pay, write to external API)",
"Look at how memory/state persists across turns — can an earlier untrusted input influence a later, seemingly-unrelated decision?",
"For MCP specifically, check server-side input validation and whether tool outputs are treated as trusted or re-validated before use",
],
time_budget_min=30,
),
Card(
id="incident-response-1",
category="threat_modeling",
prompt=(
"A prompt injection attack bypassed our guardrails and the agent executed an "
"unauthorized tool call. Investigate, determine root cause, and write an incident "
"report with remediation."
),
what_it_tests="Forensic analysis, incident response process, and post-mortem writing under ambiguity.",
talking_points=[
"Ask clarifying questions first: what logs exist, what was the tool call, what data did it touch — don't guess before you have the trace",
"Reconstruct the attack chain from logs: entry point, how it bypassed the guardrail layer, what it triggered",
"State root cause as a specific control gap ('the send_email allowlist didn't cover this subdomain'), not 'the model made a mistake'",
"End with a blameless post-mortem: what changed, what monitoring now catches this, and the re-test that proves the fix works",
],
time_budget_min=25,
),
Card(
id="compliance-mapping-1",
category="compliance",
prompt=(
"Map this AI system's controls to NIST AI RMF and EU AI Act requirements. Identify "
"gaps and create a remediation roadmap."
),
what_it_tests="Framework knowledge (NIST AI RMF Govern/Map/Measure/Manage, EU AI Act risk tiers), gap analysis, and technical-to-policy translation.",
talking_points=[
"Classify the system's EU AI Act risk tier first (minimal/limited/high/unacceptable) — this determines which obligations even apply",
"Walk NIST AI RMF's 4 functions in order: Govern (policy exists?), Map (risks identified?), Measure (metrics tracked?), Manage (mitigations implemented?)",
"Be explicit that a technical control and a compliance requirement are not automatically the same thing — show the mapping, don't assert it",
"Prioritize the roadmap by regulatory deadline and blast radius, not by ease of implementation",
],
time_budget_min=25,
),
]
CATEGORIES = sorted({card.category for card in CARDS})
# ── Leitner scheduling ─────────────────────────────────────────────────────────
def load_state() -> dict[str, int]:
if STATE_FILE.exists():
return json.loads(STATE_FILE.read_text())
return {card.id: 1 for card in CARDS}
def save_state(state: dict[str, int]) -> None:
STATE_FILE.write_text(json.dumps(state, indent=2))
def due_cards(state: dict[str, int], category: str | None = None) -> list[Card]:
pool = [c for c in CARDS if category is None or c.category == category]
# Lower box = more overdue = higher priority; shuffle within a box for variety.
pool.sort(key=lambda c: state.get(c.id, 1))
random.shuffle(pool)
pool.sort(key=lambda c: state.get(c.id, 1))
return pool
def apply_result(state: dict[str, int], card: Card, correct: bool) -> None:
box = state.get(card.id, 1)
state[card.id] = min(box + 1, 5) if correct else 1
# ── Runner ─────────────────────────────────────────────────────────────────────
def print_card(card: Card, state: dict[str, int]) -> None:
box = state.get(card.id, 1)
print(f"\n[{card.category}] box {box} ({BOX_INTERVALS_LABEL[box]}) — time budget {card.time_budget_min} min")
print(f"PROMPT: {card.prompt}")
print(f"Tests: {card.what_it_tests}")
def print_talking_points(card: Card) -> None:
print("Talking points a strong answer hits:")
for point in card.talking_points:
print(f" - {point}")
def run_interactive(category: str | None) -> None:
state = load_state()
cards = due_cards(state, category)
print(f"Drilling {len(cards)} card(s)" + (f" in category '{category}'" if category else "") + ".\n")
for card in cards:
print_card(card, state)
input("\nThink out loud / write your answer, then press Enter to see the talking points...")
print_talking_points(card)
answer = input("\nDid you hit at least 3 of the talking points? (y/n): ").strip().lower()
apply_result(state, card, correct=answer.startswith("y"))
save_state(state)
print("\nSession complete. State saved to", STATE_FILE)
def run_demo(category: str | None) -> None:
"""Non-interactive walkthrough: shows the bank and simulates one scheduling pass."""
state = load_state()
cards = due_cards(state, category)
print(f"DEMO MODE — {len(cards)} card(s) due" + (f" in '{category}'" if category else "") + ".")
print("Run with --interactive to actually self-drill and grade your own answers.\n")
rng = random.Random(7)
for card in cards:
print_card(card, state)
print_talking_points(card)
simulated_correct = rng.random() > 0.35
apply_result(state, card, simulated_correct)
print(f"-> simulated grade: {'correct' if simulated_correct else 'missed'}; "
f"box now {state[card.id]} ({BOX_INTERVALS_LABEL[state[card.id]]})")
print("-" * 70)
save_state(state)
print(f"\nBox distribution after this session: " +
", ".join(f"box {b}: {sum(1 for v in state.values() if v == b)}" for b in range(1, 6)))
def main() -> None:
parser = argparse.ArgumentParser(description="Drill AI security interview questions with spaced repetition.")
parser.add_argument("--category", choices=CATEGORIES, default=None,
help=f"Restrict to one category: {', '.join(CATEGORIES)}")
parser.add_argument("--interactive", action="store_true", help="Run a real self-graded drill session.")
parser.add_argument("--list", action="store_true", help="List all cards and exit.")
args = parser.parse_args()
if args.list:
for card in CARDS:
print(f"{card.id:24s} [{card.category:16s}] {card.prompt[:80]}...")
return
if args.interactive:
run_interactive(args.category)
else:
run_demo(args.category)
if __name__ == "__main__":
main()