Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
2d8473a
Adding initializer service
rlundeen2 May 13, 2026
40cfea2
pre-commit
rlundeen2 May 13, 2026
eebdb4e
Merge remote-tracking branch 'origin/main' into users/rlundeen/2026_0…
rlundeen2 May 13, 2026
31ecfc7
Merge branch 'main' into users/rlundeen/2026_05_11_scenario_gaps
rlundeen2 May 13, 2026
351ee5c
pr feedback
rlundeen2 May 13, 2026
c0d5a08
pr feedback
rlundeen2 May 13, 2026
bf1f00d
Merge branch 'users/rlundeen/2026_05_11_scenario_gaps' into users/rlu…
rlundeen2 May 13, 2026
328ce79
adding custom initializers to rest
rlundeen2 May 13, 2026
798c2e5
style: Optional -> | None, import inspect to top-level
rlundeen2 May 13, 2026
cbfc4df
Merge remote-tracking branch 'origin/main' into users/rlundeen/2026_0…
rlundeen2 May 13, 2026
abb3f62
adding content
rlundeen2 May 13, 2026
a75d767
self review
rlundeen2 May 13, 2026
69d9c96
self review
rlundeen2 May 13, 2026
92873dd
Merge branch 'main' into users/rlundeen/2026_05_13_add_initializer
rlundeen2 May 13, 2026
7926cc4
Refactor printers: extract formatting into lightweight base classes
rlundeen2 May 14, 2026
30f6151
Consolidate all printers into pyrit/printer/ module
rlundeen2 May 14, 2026
de61795
Add deprecation warnings for old printer import paths (removed in 0.1…
rlundeen2 May 14, 2026
837ed3f
Rename concrete printers to *MemoryPrinter, move pyrit internals out …
rlundeen2 May 14, 2026
788eceb
Refactor markdown printer, delete dead old ABC files
rlundeen2 May 14, 2026
f5045a0
refactoring frontend
rlundeen2 May 14, 2026
91a417a
Add missing __all__ to scenario printer deprecation shim
rlundeen2 May 14, 2026
f31d0d0
Fix type checker errors in from_dict methods and MemoryPrinter types
rlundeen2 May 14, 2026
86172c9
Fix ruff lint errors: return types, docstrings, noqa
rlundeen2 May 14, 2026
d117af2
Fix ty type check: make ScenarioResult identifier params optional
rlundeen2 May 14, 2026
65becc5
pr feedback
rlundeen2 May 14, 2026
1eeb7a3
pre-commit
rlundeen2 May 14, 2026
4f29026
fixing test
rlundeen2 May 14, 2026
70eea64
Merge remote-tracking branch 'origin/main' into users/rlundeen/2026_0…
rlundeen2 May 14, 2026
89e3202
Merge branch 'users/rlundeen/2026_05_13_printer_refactor' into users/…
rlundeen2 May 15, 2026
69ccff3
fixing test
rlundeen2 May 15, 2026
bf32513
self-review
rlundeen2 May 15, 2026
880feef
Merge branch 'users/rlundeen/2026_05_13_printer_refactor' into users/…
rlundeen2 May 15, 2026
b5ab93c
Use printer module for scenario results in CLI
rlundeen2 May 15, 2026
f97ad2f
Show strategy-level progress during scenario runs
rlundeen2 May 15, 2026
1169f74
Merge branch 'main' into users/rlundeen/2026_05_13_frontend_core_refa…
rlundeen2 May 18, 2026
e410c4b
pre-commit
rlundeen2 May 18, 2026
86bc912
Move setup_frontend into lifespan, share CLI helpers via pyrit.common
rlundeen2 May 19, 2026
c81b04e
main fix
rlundeen2 May 19, 2026
4b1e380
Merge remote-tracking branch 'origin/main' into users/rlundeen/2026_0…
rlundeen2 May 19, 2026
2bdaf6b
lint fixes
rlundeen2 May 19, 2026
8fa7299
MAINT: Fix GUI test failure and add unit test coverage for refactored…
rlundeen2 May 19, 2026
dfb10a7
MAINT: Restore scenario-declared CLI parameters (pyrit_scan + pyrit_s…
rlundeen2 May 19, 2026
6123bb0
Address PR review: typed scenario params, persistent shell loop, dock…
rlundeen2 May 19, 2026
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
6 changes: 4 additions & 2 deletions doc/code/scenarios/0_scenarios.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -390,9 +390,11 @@
}
],
"source": [
"from pyrit.cli.frontend_core import FrontendCore, print_scenarios_list_async\n",
"from pyrit.backend.services.scenario_service import get_scenario_service\n",
"from pyrit.cli._output import print_scenario_list\n",
"\n",
"await print_scenarios_list_async(context=FrontendCore()) # type: ignore"
"response = await get_scenario_service().list_scenarios_async(limit=200) # type: ignore\n",
"print_scenario_list(items=[s.model_dump() for s in response.items])"
]
},
{
Expand Down
6 changes: 4 additions & 2 deletions doc/code/scenarios/0_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,11 @@ def _build_display_group(self, *, technique_name: str, seed_group_name: str) ->
# ## Existing Scenarios

# %%
from pyrit.cli.frontend_core import FrontendCore, print_scenarios_list_async
from pyrit.backend.services.scenario_service import get_scenario_service
from pyrit.cli._output import print_scenario_list

await print_scenarios_list_async(context=FrontendCore()) # type: ignore
response = await get_scenario_service().list_scenarios_async(limit=200) # type: ignore
print_scenario_list(items=[s.model_dump() for s in response.items])

# %% [markdown]
#
Expand Down
9 changes: 4 additions & 5 deletions doc/code/scenarios/2_custom_scenario_parameters.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -290,15 +290,14 @@
}
],
"source": [
"from pyrit.cli.frontend_core import format_scenario_metadata\n",
"from pyrit.registry import ScenarioRegistry\n",
"from pyrit.backend.services.scenario_service import get_scenario_service\n",
"from pyrit.cli._output import print_scenario_list\n",
"\n",
"# Show scam (declares a parameter) and red_team_agent (none), so the\n",
"# Supported Parameters section is visible in one and absent in the other.\n",
"demo_names = {\"airt.scam\", \"foundry.red_team_agent\"}\n",
"for metadata in ScenarioRegistry.get_registry_singleton().list_metadata():\n",
" if metadata.registry_name in demo_names:\n",
" format_scenario_metadata(scenario_metadata=metadata)"
"response = await get_scenario_service().list_scenarios_async(limit=200) # type: ignore\n",
"print_scenario_list(items=[s.model_dump() for s in response.items if s.scenario_name in demo_names])"
]
},
{
Expand Down
9 changes: 4 additions & 5 deletions doc/code/scenarios/2_custom_scenario_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,14 @@
# CLI uses is callable programmatically:

# %%
from pyrit.cli.frontend_core import format_scenario_metadata
from pyrit.registry import ScenarioRegistry
from pyrit.backend.services.scenario_service import get_scenario_service
from pyrit.cli._output import print_scenario_list

# Show scam (declares a parameter) and red_team_agent (none), so the
# Supported Parameters section is visible in one and absent in the other.
demo_names = {"airt.scam", "foundry.red_team_agent"}
for metadata in ScenarioRegistry.get_registry_singleton().list_metadata():
if metadata.registry_name in demo_names:
format_scenario_metadata(scenario_metadata=metadata)
response = await get_scenario_service().list_scenarios_async(limit=200) # type: ignore
print_scenario_list(items=[s.model_dump() for s in response.items if s.scenario_name in demo_names])

# %% [markdown]
# Notice the `Supported Parameters:` section under `airt.scam`. It's absent
Expand Down
4 changes: 2 additions & 2 deletions doc/myst.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,11 @@ project:
children:
- file: api/pyrit_analytics.md
- file: api/pyrit_auth.md
- file: api/pyrit_cli_frontend_core.md
- file: api/pyrit_cli_pyrit_backend.md
- file: api/pyrit_cli_api_client.md
- file: api/pyrit_cli_pyrit_scan.md
- file: api/pyrit_cli_pyrit_shell.md
- file: api/pyrit_common.md
- file: api/pyrit_common_cli_helpers.md
- file: api/pyrit_datasets.md
- file: api/pyrit_embedding.md
- file: api/pyrit_exceptions.md
Expand Down
44 changes: 26 additions & 18 deletions docker/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,33 @@ if [ "$PYRIT_MODE" = "jupyter" ]; then
exec jupyter lab --ip=0.0.0.0 --port=8888 --no-browser --allow-root --notebook-dir=/app/notebooks
elif [ "$PYRIT_MODE" = "gui" ]; then
echo "Starting PyRIT GUI on port 8000..."
# Use Azure SQL if AZURE_SQL_SERVER is set (injected by Bicep), otherwise default to SQLite.
# Note: AZURE_SQL_DB_CONNECTION_STRING is in the .env file (loaded by Python dotenv),
# but we use AZURE_SQL_SERVER here because it's a direct env var from the Bicep template.
# Build CLI arguments
BACKEND_ARGS="--host 0.0.0.0 --port 8000"
# The thin backend only takes --host/--port/--config-file/--log-level.
# Translate AZURE_SQL_SERVER and PYRIT_INITIALIZER into a runtime config file
# so the FastAPI lifespan (ConfigurationLoader) picks them up on startup.
RUNTIME_CONFIG=/tmp/pyrit_runtime.yaml
{
if [ -n "$AZURE_SQL_SERVER" ]; then
echo "Using Azure SQL database (server: $AZURE_SQL_SERVER)" >&2
echo "memory_db_type: AzureSQL"
else
echo "Using SQLite database (AZURE_SQL_SERVER not set)" >&2
echo "memory_db_type: SQLite"
fi
if [ -n "$PYRIT_INITIALIZER" ]; then
echo "Using initializer: $PYRIT_INITIALIZER" >&2
echo "initializers:"
# Split comma-separated initializer names into a YAML list.
IFS=',' read -ra INIT_NAMES <<<"$PYRIT_INITIALIZER"
for name in "${INIT_NAMES[@]}"; do
echo " - $(echo "$name" | xargs)"
done
fi
} >"$RUNTIME_CONFIG"

if [ -n "$AZURE_SQL_SERVER" ]; then
echo "Using Azure SQL database (server: $AZURE_SQL_SERVER)"
BACKEND_ARGS="$BACKEND_ARGS --database AzureSQL"
else
echo "Using SQLite database (AZURE_SQL_SERVER not set)"
fi

if [ -n "$PYRIT_INITIALIZER" ]; then
echo "Using initializer: $PYRIT_INITIALIZER"
BACKEND_ARGS="$BACKEND_ARGS --initializers $PYRIT_INITIALIZER"
fi

exec python -m pyrit.cli.pyrit_backend $BACKEND_ARGS
exec python -m pyrit.backend.pyrit_backend \
--host 0.0.0.0 \
--port 8000 \
--config-file "$RUNTIME_CONFIG"
else
echo "ERROR: Invalid PYRIT_MODE '$PYRIT_MODE'. Must be 'jupyter' or 'gui'"
exit 1
Expand Down
32 changes: 24 additions & 8 deletions frontend/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def _find_pids_on_port(port):
def stop_servers():
"""Stop all running servers"""
print("🛑 Stopping servers...")
backend_pids = find_pids_by_pattern("pyrit.cli.pyrit_backend")
backend_pids = find_pids_by_pattern("pyrit.backend.pyrit_backend")
frontend_pids = find_pids_by_pattern("node.*vite")
# Also find any parent dev.py processes (detached wrappers)
wrapper_pids = find_pids_by_pattern("frontend/dev.py")
Expand All @@ -163,6 +163,10 @@ def start_backend(*, config_file: str | None = None, initializers: list[str] | N
Configuration (initializers, database, env files) is read automatically
from ~/.pyrit/.pyrit_conf by the pyrit_backend CLI via ConfigurationLoader,
unless overridden with *config_file*.

When *initializers* is supplied without a *config_file*, a tiny temporary
runtime config is written to forward those names — ``pyrit_backend`` only
accepts ``--config-file`` now (no ``--initializers`` flag).
"""
print("🚀 Starting backend on port 8000...")

Expand All @@ -178,20 +182,32 @@ def start_backend(*, config_file: str | None = None, initializers: list[str] | N
cmd = [
sys.executable,
"-m",
"pyrit.cli.pyrit_backend",
"pyrit.backend.pyrit_backend",
"--host",
"localhost",
"--port",
"8000",
"--log-level",
"info",
]
if config_file:
cmd.extend(["--config-file", config_file])

# Add initializers if specified
if initializers:
cmd.extend(["--initializers"] + initializers)
# Resolve config-file: explicit wins; otherwise synthesize one from initializers.
effective_config_file = config_file
if effective_config_file is None and initializers:
import tempfile

synthesized = tempfile.NamedTemporaryFile(
mode="w", suffix=".yaml", prefix="pyrit_dev_", delete=False
)
synthesized.write("initializers:\n")
for name in initializers:
synthesized.write(f" - {name}\n")
synthesized.close()
effective_config_file = synthesized.name
print(f" Wrote initializer overrides to {effective_config_file}")

if effective_config_file:
cmd.extend(["--config-file", effective_config_file])

# Pipe stdout/stderr so dev.py controls output ordering
return subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
Expand Down Expand Up @@ -456,7 +472,7 @@ def main():
elif command == "backend":
print("🚀 Starting backend only...")
# Kill stale backend processes
stale = find_pids_by_pattern("pyrit.cli.pyrit_backend")
stale = find_pids_by_pattern("pyrit.backend.pyrit_backend")
if stale:
print(f" Killing stale backend PIDs: {stale}")
kill_pids(stale)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ all = [
]

[project.scripts]
pyrit_backend = "pyrit.cli.pyrit_backend:main"
pyrit_backend = "pyrit.backend.pyrit_backend:main"
pyrit_scan = "pyrit.cli.pyrit_scan:main"
pyrit_shell = "pyrit.cli.pyrit_shell:main"

Expand Down
49 changes: 33 additions & 16 deletions pyrit/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
targets,
version,
)
from pyrit.memory import CentralMemory
from pyrit.setup.configuration_loader import ConfigurationLoader

# Check for development mode from environment variable
DEV_MODE = os.getenv("PYRIT_DEV_MODE", "false").lower() == "true"
Expand All @@ -40,17 +40,38 @@

@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""Manage application startup and shutdown lifecycle."""
# Initialization is handled by the pyrit_backend CLI before uvicorn starts.
# Running 'uvicorn pyrit.backend.main:app' directly is not supported;
# use 'pyrit_backend' instead.
try:
CentralMemory.get_memory_instance()
except ValueError:
logger.warning(
"CentralMemory is not initialized. "
"Start the server via 'pyrit_backend' CLI instead of running uvicorn directly."
)
"""
Initialize PyRIT on startup using the config file, then yield.

Config resolution order:
1. ``PYRIT_CONFIG_FILE`` env var (if set)
2. ``~/.pyrit/.pyrit_conf`` (if it exists)
3. Built-in defaults (SQLite, no initializers)
"""
config_file_env = os.getenv("PYRIT_CONFIG_FILE")
config_file = Path(config_file_env) if config_file_env else None

config = ConfigurationLoader.load_with_overrides(config_file=config_file)
await config.initialize_pyrit_async()

# Expose config values to route handlers via app.state
default_labels: dict[str, str] = {}
if config.operator:
default_labels["operator"] = config.operator
if config.operation:
default_labels["operation"] = config.operation
app.state.default_labels = default_labels
app.state.max_concurrent_scenario_runs = config.max_concurrent_scenario_runs
app.state.allow_custom_initializers = config.allow_custom_initializers

if config.allow_custom_initializers:
logger.warning("Custom initializer registration is ENABLED (allow_custom_initializers: true).")

# Mount the bundled frontend (or print a dev/missing-frontend notice).
# Done here rather than at module load so test imports of `pyrit.backend.main`
# don't emit noise and don't perform filesystem side effects.
setup_frontend()

yield


Expand Down Expand Up @@ -124,7 +145,3 @@ def setup_frontend() -> None:
print(" The frontend must be built and included in the package.")
print(" Run: python build_scripts/prepare_package.py")
print(" API endpoints will still work but the UI won't be available.")


# Set up frontend at module load time (needed when running via uvicorn)
setup_frontend()
29 changes: 2 additions & 27 deletions pyrit/backend/models/scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

from pydantic import BaseModel, Field

from pyrit.backend.models.attacks import AttackSummary
from pyrit.backend.models.common import PaginationInfo


Expand All @@ -25,7 +24,8 @@ class ScenarioParameterSummary(BaseModel):
description: str = Field(..., description="Human-readable description of the parameter")
default: str | None = Field(None, description="Default value as a display string, or None if required")
param_type: str = Field(..., description="Type of the parameter as a display string (e.g., 'int', 'str')")
choices: str | None = Field(None, description="Allowed values as a display string, or None if unconstrained")
choices: list[str] | None = Field(None, description="Allowed values as strings, or None if unconstrained")
is_list: bool = Field(False, description="True when the parameter accepts a list of values (e.g., list[str])")


class RegisteredScenario(BaseModel):
Expand Down Expand Up @@ -124,28 +124,3 @@ class ScenarioRunListResponse(BaseModel):
"""Response for listing scenario runs."""

items: list[ScenarioRunSummary] = Field(..., description="List of scenario runs")


# ============================================================================
# Scenario Results Detail Models
# ============================================================================


class AtomicAttackResults(BaseModel):
"""Results grouped by atomic attack name."""

atomic_attack_name: str = Field(..., description="Name of the atomic attack (strategy)")
display_group: str | None = Field(None, description="Display group label for UI grouping")
results: list[AttackSummary] = Field(..., description="Individual attack results")
success_count: int = Field(0, ge=0, description="Number of successful attacks")
failure_count: int = Field(0, ge=0, description="Number of failed attacks")
total_count: int = Field(0, ge=0, description="Total number of attack results")
total_retries: int = Field(0, ge=0, description="Sum of retries across all attacks in this group")
error_count: int = Field(0, ge=0, description="Number of attacks with errors")


class ScenarioRunDetail(BaseModel):
"""Full detailed results of a scenario run."""

run: ScenarioRunSummary = Field(..., description="The scenario run summary")
attacks: list[AtomicAttackResults] = Field(..., description="Results grouped by atomic attack")
Loading
Loading