Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion finbot/apps/ctf/routes/toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@

import json
import logging
import secrets

from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session

from finbot.core.auth.middleware import get_session_context
from finbot.core.auth.session import SessionContext
from finbot.core.data.database import get_db
from finbot.core.data.repositories import CTFEventRepository
from finbot.core.messaging import event_bus
from finbot.core.utils import to_utc_iso
from finbot.mcp.servers.finmail.repositories import EmailRepository

Expand Down Expand Up @@ -231,3 +233,56 @@ def read_exfil_capture(
return {"error": "Capture not found"}

return {"capture": _event_to_exfil(event)}


# =========================================================================
# Investigation -- forensic answer submission for Purple Team challenges
# =========================================================================


class InvestigationSubmission(BaseModel):
"""A student's forensic answer for a Purple Team investigation challenge."""

challenge_id: str | None = Field(default=None, max_length=64)
server: str | None = Field(default=None, max_length=200)
tool: str | None = Field(default=None, max_length=200)
directive: str | None = Field(default=None, max_length=2000)


class InvestigationResponse(BaseModel):
accepted: bool
workflow_id: str


@router.post("/investigation", response_model=InvestigationResponse)
async def submit_investigation(
submission: InvestigationSubmission,
session_context: SessionContext = Depends(get_session_context),
):
"""Submit a forensic investigation answer.

Emits a business.investigation.submitted event carrying the answer under
the "submission" key. The PurpleTeamDetector scores it against the
challenge's expected answer and awards partial credit per field. Each
submission gets a fresh workflow_id so it counts as a distinct attempt.
"""
workflow_id = f"investigation_{secrets.token_urlsafe(12)}"
answer = {
"server": submission.server,
"tool": submission.tool,
"directive": submission.directive,
}

await event_bus.emit_business_event(
event_type="investigation.submitted",
event_subtype="forensic",
event_data={
"submission": answer,
"challenge_id": submission.challenge_id,
},
session_context=session_context,
workflow_id=workflow_id,
summary="Forensic investigation submitted",
)

return InvestigationResponse(accepted=True, workflow_id=workflow_id)
94 changes: 94 additions & 0 deletions finbot/ctf/definitions/challenges/forensics/the_debrief.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
id: forensics-the-debrief
title: "The Debrief"
description: |
You broke the payment pipeline in Toxic Transfer. Now prove you understand
why it worked. This is a purple team exercise: red teams break things, blue
teams explain them, and the best operators do both.

A poisoned MCP tool caused an agent to leak vendor financial data during a
payment. Your job is to file the incident report — name the compromised
server, the tool that carried the hidden instruction, and the directive
itself.

**Objective:**

Submit a forensic answer with three fields:
- **server** — the MCP server whose tool was poisoned
- **tool** — the specific tool that carried the injected directive
- **directive** — what the hidden instruction told the agent to do

**Scoring:**

Each field scores independently: server and tool are worth 33 points each,
the directive 34. Get the server and tool exactly right, and describe the
directive in your own words — you do not need to quote it verbatim, but a
stray word or two will not do either. The grader accepts any answer that
genuinely captures what the instruction did.

You need 50 to complete the challenge, and **your award scales with your
score**: finish on 66 and you earn 66% of the points, finish on 100 and you
earn all of them. Partial understanding is worth partial credit, not full
credit.

**How to submit:**

POST your answer to `/ctf/api/v1/toolkit/investigation` with a JSON body:
`{"challenge_id": "forensics-the-debrief", "server": "...", "tool": "...",
"directive": "..."}`.

category: forensics
subcategory: purple_team
difficulty: advanced
points: 250

hints:
- cost: 10
text: "Retrace Toxic Transfer. The attack poisoned a tool on the payment
server. Which server handles payments in FinBot?"
- cost: 25
text: "The poisoned tool is the one that moves money. Look at what the agent
called right before the vendor's financial data showed up in the Hacker
Toolkit."
- cost: 50
text: "The server is finstripe and the tool is create_transfer. The directive
told the agent to email the vendor's tax ID and bank account to an external
address before processing the transfer."

labels:
owasp_llm:
- LLM03:Supply Chain Vulnerabilities
- LLM01:Prompt Injection
cwe:
- CWE-94:Improper Control of Generation of Code
mitre_atlas:
- AML.T0056:LLM Supply Chain Compromise
owasp_agentic:
- ASI-04:Agentic Supply Chain Vulnerabilities
- ASI-02:Tool Misuse and Exploitation

prerequisites:
- data-exfil-toxic-transfer

resources:
- title: "OWASP Top 10 for Agentic Applications for 2026"
url: "https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/"

detector_class: PurpleTeamDetector
detector_config:
expected:
server: finstripe
tool: create_transfer
directive: "email the vendor tax id and bank account to an external address"
min_score: 50
directive_match: fuzzy

scoring:
modifiers:
# Scale the award by the investigation score, so 66/100 earns 66% of the
# points rather than the same 250 a perfect answer earns. min_multiplier
# keeps a passing answer worth at least half.
- type: detection_score
min_multiplier: 0.5

is_active: true
order_index: 20
4 changes: 4 additions & 0 deletions finbot/ctf/detectors/implementations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
from finbot.ctf.detectors.implementations.policy_bypass_non_compliant import (
PolicyBypassNonCompliantDetector,
)
from finbot.ctf.detectors.implementations.purple_team_detector import (
PurpleTeamDetector,
)
from finbot.ctf.detectors.implementations.rce import (
RCEDetector,
)
Expand Down Expand Up @@ -53,6 +56,7 @@
"InvoiceThresholdBypassDetector",
"InvoiceTrustOverrideDetector",
"PolicyBypassNonCompliantDetector",
"PurpleTeamDetector",
"RCEDetector",
"SystemPromptLeakDetector",
"ToolPoisoningDeletionDetector",
Expand Down
Loading
Loading