Skip to content

feat(trace): SequenceDetector primitive — session-window multi-step attack detection#522

Open
stealthwhizz wants to merge 7 commits into
GenAI-Security-Project:mainfrom
stealthwhizz:feature/trace-sequence-detector
Open

feat(trace): SequenceDetector primitive — session-window multi-step attack detection#522
stealthwhizz wants to merge 7 commits into
GenAI-Security-Project:mainfrom
stealthwhizz:feature/trace-sequence-detector

Conversation

@stealthwhizz
Copy link
Copy Markdown
Contributor

Summary

  • Adds SequenceDetector to finbot/ctf/detectors/primitives/ — the first detector in FinBot that can match attack patterns spanning multiple events across a session or workflow window
  • Adds StepSpec TypedDict matching the interface approved during community bonding
  • Adds composite DB index (session_id, timestamp, event_type) on ctf_events to keep session-window history queries fast
  • 17 unit tests + 1 p95 benchmark test (8.22ms p95 on 1,000 rows, limit 10ms)

This is the Week 1 deliverable for TRACE (GSoC 2026).

What it does

All 14 existing detectors fire on a single event. SequenceDetector queries CTFEvent history to match an ordered sequence of steps within a configurable window. Challenge authors configure it from YAML — no Python required:

detector_class: SequenceDetector
detector_config:
  steps:
    - event_type: "agent.*.tool_call_success"
      conditions: { tool_name: "approve_invoice" }
      label: "First micro-payment"
    - event_type: "agent.*.tool_call_success"
      conditions: { tool_name: "approve_invoice" }
      label: "Second micro-payment"
  within_n_events: 50
  within_seconds: 300
  order_matters: true
  window: "session"

Files changed

File Change
finbot/ctf/detectors/primitives/sequence_detector.py New primitive
finbot/ctf/detectors/primitives/__init__.py Export SequenceDetector, StepSpec
migrations/versions/2026_06_03_add_ctf_event_session_index.py Composite index
tests/unit/ctf/test_sequence_detector.py 17 unit tests
tests/unit/ctf/test_sequence_detector_benchmark.py p95 benchmark

Test plan

  • 17 unit tests pass: full sequence, partial, ordering enforcement, session/workflow windows, all condition operators, glob event_type matching
  • p95 benchmark: 8.22ms on 1,000-row session with composite index (limit: 10ms)
  • Regression: existing detector suite unaffected (no changes to base, registry, or existing detectors)

Notes for reviewers

  • The within_n_events window uses ORDER BY timestamp DESC LIMIT n then reverses — this keeps the query bounded while preserving chronological order for step matching
  • Conditions check details JSON first, then fall back to named CTFEvent columns (tool_name, agent_name, etc.) — this avoids ambiguity between payload fields and model attributes
  • The composite index covers the session-window query shape: WHERE session_id = ? ORDER BY timestamp ASC; event_type is included for potential index-only scans

Adds StepSpec dataclass and SequenceDetector base structure to
finbot/ctf/detectors/primitives/. Includes config validation,
get_relevant_event_types(), and stubbed private helpers for
history querying, step matching, and time-window checks.
check_event() and all helpers are NotImplementedError stubs
pending implementation.
…gration

- Add SequenceDetector to finbot/ctf/detectors/primitives/
  Detects multi-step attack patterns across a session or workflow window.
  Supports ordered step matching, glob event_type patterns, within_n_events
  and within_seconds windows, and all ToolCallDetector field operators.
  Challenge authors configure it from YAML with no Python required.

- Add composite index idx_ctf_event_session_ts_type on (session_id, timestamp,
  event_type) to keep session-window history queries below 10ms p95.

- Export SequenceDetector from finbot/ctf/detectors/primitives/__init__.py

- Add 17 unit tests covering full sequence detection, partial sequences,
  order enforcement, session/workflow windows, condition operators, and
  glob event_type matching.
- Add StepSpec TypedDict to sequence_detector.py matching the approved
  interface spec; export it from primitives __init__

- Add benchmark test: seeds 1,000 CTFEvent rows with composite index,
  runs check_event 100 times, asserts p95 < 10ms
  Current result: p50 ~7ms, p95 ~8ms on SQLite
Copilot AI review requested due to automatic review settings June 2, 2026 19:49
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a new SequenceDetector primitive for multi-step pattern detection, plus database/index support and accompanying tests/benchmarking to validate correctness and query performance.

Changes:

  • Introduce SequenceDetector with configurable step matching over session/workflow windows.
  • Add Alembic migration for a composite ctf_events index used by session-window queries.
  • Add unit tests and a SQLite benchmark to catch functional and query-latency regressions; refactor vendor DB session acquisition to use get_db().

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
finbot/ctf/detectors/primitives/sequence_detector.py New detector implementation: config validation, history query, step/condition matching.
finbot/ctf/detectors/primitives/__init__.py Exports SequenceDetector and StepSpec.
migrations/versions/2026_06_03_add_ctf_event_session_index.py Adds composite index intended to speed up session-window queries.
tests/unit/ctf/test_sequence_detector.py Unit coverage for config validation, ordering, windows, and some condition operators.
tests/unit/ctf/test_sequence_detector_benchmark.py Benchmark test for p95 latency of check_event query path.
finbot/tools/data/vendor.py Switches vendor tools from db_session() context manager to get_db() generator pattern.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +43 to +56
db = next(get_db())
vendor_repo = VendorRepository(db, session_context)
vendor = vendor_repo.get_vendor(vendor_id)
if not vendor:
raise ValueError("Vendor not found")

return {
"vendor_id": vendor.id,
"company_name": vendor.company_name,
"contact_name": vendor.contact_name,
"email": vendor.email,
"phone": vendor.phone,
"status": vendor.status,
}
Comment on lines +35 to +52
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
)
Base.metadata.create_all(bind=engine)

# Create the composite index from the migration
with engine.connect() as conn:
conn.execute(
text(
"CREATE INDEX IF NOT EXISTS idx_ctf_event_session_ts_type "
"ON ctf_events (session_id, timestamp, event_type)"
)
)
conn.commit()

Session = sessionmaker(bind=engine)
session = Session()
Comment on lines +125 to +133
import asyncio

latencies_ms: list[float] = []
for _ in range(BENCHMARK_RUNS):
t0 = time.perf_counter()
asyncio.get_event_loop().run_until_complete(
det.check_event(trigger_event, session)
)
latencies_ms.append((time.perf_counter() - t0) * 1000)
Comment on lines +20 to +29
def upgrade() -> None:
# Composite index for SequenceDetector session-window queries:
# WHERE namespace = ? AND session_id = ? ORDER BY timestamp ASC
# The event_type column is included to support index-only scans when
# filtering by step event_type after the session window is resolved.
op.create_index(
"idx_ctf_event_session_ts_type",
"ctf_events",
["session_id", "timestamp", "event_type"],
)
Comment on lines +104 to +111
if within_seconds is not None:
event_time = event.get("timestamp")
if isinstance(event_time, str):
event_time = datetime.fromisoformat(event_time.replace("Z", "+00:00"))
elif not isinstance(event_time, datetime):
event_time = datetime.now(UTC)
cutoff = event_time - timedelta(seconds=within_seconds)
query = query.filter(CTFEvent.timestamp >= cutoff)
Comment on lines +203 to +233
def _check_condition(self, actual: Any, condition: Any) -> bool:
"""Check if actual value satisfies condition (ToolCallDetector operators)."""
if not isinstance(condition, dict):
return actual == condition

for operator, expected in condition.items():
op = operator.lower()
if op == "exists":
return (actual is not None) == expected
if actual is None:
return False
if op in ("equals", "eq"):
return actual == expected
if op == "in":
return actual in expected
if op == "not_in":
return actual not in expected
if op == "contains":
return expected in str(actual).lower()
if op == "gt":
return float(actual) > float(expected)
if op == "gte":
return float(actual) >= float(expected)
if op == "lt":
return float(actual) < float(expected)
if op == "lte":
return float(actual) <= float(expected)
if op == "matches":
return bool(re.search(expected, str(actual), re.IGNORECASE))

return False
from unittest.mock import MagicMock

from finbot.ctf.detectors.primitives.sequence_detector import SequenceDetector
from finbot.ctf.detectors.result import DetectionResult
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants