Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9947fd1
backend with APIs that will be needed in the short-term.
romanlutz Jan 25, 2026
560c7b9
add updated backend after review
romanlutz Jan 27, 2026
cef47db
more refinements to simplify and remove obsolete parts
romanlutz Jan 28, 2026
10bb4dd
Merge branch 'main' of https://github.com/Azure/PyRIT into romanlutz/…
romanlutz Jan 28, 2026
a06e9b8
explicitly remove playwright-report
romanlutz Jan 28, 2026
d5679b3
explicitly remove last-run.json
romanlutz Jan 28, 2026
7be9760
use normalizer properly, clean up, refactor
romanlutz Jan 28, 2026
27ca930
update with latest PR feedback, cleanup
romanlutz Feb 5, 2026
5fb3f2a
more simplifications
romanlutz Feb 5, 2026
e83d94e
Merge branch 'main' of https://github.com/Azure/PyRIT into romanlutz/…
romanlutz Feb 5, 2026
9b53976
update with target identifier
romanlutz Feb 5, 2026
a5f8882
Merge branch 'main' of https://github.com/Azure/PyRIT into romanlutz/…
romanlutz Feb 5, 2026
cf018b9
fix test, no default initializer
romanlutz Feb 5, 2026
1800a8f
Merge branch 'main' into romanlutz/backend_apis
romanlutz Feb 5, 2026
edef8b2
Adding attack identifier
rlundeen2 Feb 7, 2026
201b1d6
fixing all tests
rlundeen2 Feb 9, 2026
a785ee3
pre-commit
rlundeen2 Feb 9, 2026
d96097b
Merge branch 'main' of https://github.com/Azure/PyRIT into romanlutz/…
romanlutz Feb 10, 2026
d70eda8
fix: store labels as nested key in metadata instead of spreading
romanlutz Feb 10, 2026
877f034
fix missing labels in _build_summary
romanlutz Feb 10, 2026
97ba860
update to actually test labels, too
romanlutz Feb 10, 2026
bd543b3
remove duplicate exception handler
romanlutz Feb 10, 2026
86bd2ca
use lifespan instead of on_event
romanlutz Feb 10, 2026
a1dd6bc
thread-safe singletons (lru_cache)
romanlutz Feb 10, 2026
674eef5
CORS origins from env var
romanlutz Feb 10, 2026
973b77a
add error response for list targets
romanlutz Feb 10, 2026
2462125
pagination for targets
romanlutz Feb 10, 2026
5dc9bb4
Merge branch 'romanlutz/backend_apis' of https://github.com/romanlutz…
romanlutz Feb 10, 2026
aaf133d
mapping
romanlutz Feb 10, 2026
88b1a4f
handle original_value_data_type
romanlutz Feb 10, 2026
881a2cb
fixing warnings
rlundeen2 Feb 11, 2026
3d30e05
address copilot comments
romanlutz Feb 11, 2026
2a2d263
clean up imports
romanlutz Feb 11, 2026
928b62f
Merge branch 'main' of https://github.com/Azure/PyRIT into romanlutz/…
romanlutz Feb 11, 2026
fbd1648
Merge branch 'main' of https://github.com/Azure/PyRIT into romanlutz/…
romanlutz Feb 11, 2026
4262557
pr feedback
rlundeen2 Feb 12, 2026
e0f1c4e
feat: align labels with SDK path, optimize pagination, add lineage tr…
romanlutz Feb 12, 2026
4da2be4
Merge branch 'pr-1364' into romanlutz/backend_apis
romanlutz Feb 12, 2026
72a2eaf
refactor: decouple API DTOs from internal PyRIT identifier objects
romanlutz Feb 13, 2026
c336079
Merge branch 'main' of https://github.com/Azure/PyRIT into romanlutz/…
romanlutz Feb 13, 2026
0b8f148
Preserve message roles for frontend and add drift detection tests
romanlutz Feb 13, 2026
8ed0ce8
fix py310 test with mime type for wav
romanlutz Feb 13, 2026
d3418b4
fix ruff
romanlutz Feb 13, 2026
5e2a6ab
fix: cross-platform reliability for backend startup and e2e tests
romanlutz Feb 14, 2026
49822fc
refactor: improve type safety and fix mypy errors across mappers
romanlutz Feb 14, 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
30 changes: 30 additions & 0 deletions .github/instructions/style-guide.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,27 @@ def process(self, data: str) -> str:

## Documentation Standards

### Import Placement
- **MANDATORY**: All import statements MUST be at the top of the file
- Do NOT use inline/local imports inside functions or methods
- The only exception is breaking circular import dependencies, which should be rare and documented

```python
# CORRECT — imports at the top of the file
from contextlib import closing
from sqlalchemy.exc import SQLAlchemyError

def update_entry(self, entry: Base) -> None:
with closing(self.get_session()) as session:
...

# INCORRECT — inline import inside a function
def update_entry(self, entry: Base) -> None:
from contextlib import closing # ← WRONG, must be at top of file
with closing(self.get_session()) as session:
...
```

### Docstring Format
- Use Google-style docstrings
- Include type information in parameter descriptions
Expand Down Expand Up @@ -457,4 +478,13 @@ Before committing code, ensure:

---

## File Editing Rules

### Never Use `sed` for File Edits
- **MANDATORY**: Never use `sed` (or similar stream-editing CLI tools) to modify source files
- `sed` frequently corrupts files, applies partial edits, or silently fails
- Always use the editor's built-in replace/edit tools (e.g., `replace_string_in_file`, `multi_replace_string_in_file`) to make targeted, verifiable changes

---

**Remember**: Clean code is written for humans to read. Make your intent clear and your code self-documenting.
32 changes: 29 additions & 3 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ python dev.py start
# OR use npm script
npm run start

# Start backend only (with airt initializer by default)
python dev.py backend

# Start frontend only (backend must be started separately)
python dev.py frontend
# OR
npm run dev

# Restart both servers
python dev.py restart
# OR
Expand All @@ -23,16 +31,34 @@ python dev.py stop
# OR
npm run stop

# Run Vite dev server only (backend must be started separately)
npm run dev

# Build for production
npm run build

# Preview production build
npm run preview
```

### Backend CLI

The backend uses `pyrit_backend` CLI which supports initializers:

```bash
# Start with default airt initializer (loads targets from env vars)
pyrit_backend --initializers airt

# Start without initializers
pyrit_backend

# Start with custom initialization script
pyrit_backend --initialization-scripts ./my_targets.py

# List available initializers
pyrit_backend --list-initializers

# Custom host/port
pyrit_backend --host 127.0.0.1 --port 8080
```

**Development Mode**: The `dev.py` script sets `PYRIT_DEV_MODE=true` so the backend expects the frontend to run separately on port 3000.

**Production Mode**: When installed from PyPI, the backend serves the bundled frontend and will exit if frontend files are missing.
Expand Down
90 changes: 39 additions & 51 deletions frontend/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@
import json
import os
import platform
import signal
import subprocess
import sys
import time
from pathlib import Path

# Ensure emoji and other Unicode characters don't crash on Windows consoles
# that use legacy encodings like cp1252. Characters that can't be encoded
# are replaced with '?' instead of raising UnicodeEncodeError.
sys.stdout.reconfigure(errors="replace") # type: ignore[attr-defined]
sys.stderr.reconfigure(errors="replace") # type: ignore[attr-defined]

# Determine workspace root (parent of frontend directory)
FRONTEND_DIR = Path(__file__).parent.absolute()
WORKSPACE_ROOT = FRONTEND_DIR.parent
Expand Down Expand Up @@ -77,8 +82,13 @@ def stop_servers():
print("✅ Servers stopped")


def start_backend():
"""Start the FastAPI backend"""
def start_backend(initializers: list[str] | None = None):
"""Start the FastAPI backend using pyrit_backend CLI.

Args:
initializers: Optional list of initializer names to run at startup.
If not specified, no initializers are run.
"""
print("🚀 Starting backend on port 8000...")

# Change to workspace root
Expand All @@ -88,40 +98,29 @@ def start_backend():
env = os.environ.copy()
env["PYRIT_DEV_MODE"] = "true"

# Start backend with uvicorn
if is_windows():
backend = subprocess.Popen(
[
sys.executable,
"-m",
"uvicorn",
"pyrit.backend.main:app",
"--host",
"0.0.0.0",
"--port",
"8000",
"--log-level",
"info",
],
env=env,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if is_windows() else 0,
)
else:
backend = subprocess.Popen(
[
sys.executable,
"-m",
"uvicorn",
"pyrit.backend.main:app",
"--host",
"0.0.0.0",
"--port",
"8000",
"--log-level",
"info",
],
env=env,
)
# Default to no initializers
if initializers is None:
initializers = []

# Build command using pyrit_backend CLI
cmd = [
sys.executable,
"-m",
"pyrit.cli.pyrit_backend",
"--host",
"0.0.0.0",
"--port",
"8000",
"--log-level",
"info",
]

# Add initializers if specified
if initializers:
cmd.extend(["--initializers"] + initializers)

# Start backend
backend = subprocess.Popen(cmd, env=env)

return backend

Expand All @@ -135,14 +134,7 @@ def start_frontend():

# Start frontend process
npm_cmd = "npm.cmd" if is_windows() else "npm"

if is_windows():
frontend = subprocess.Popen(
[npm_cmd, "run", "dev"],
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if is_windows() else 0,
)
else:
frontend = subprocess.Popen([npm_cmd, "run", "dev"])
frontend = subprocess.Popen([npm_cmd, "run", "dev"])

return frontend

Expand Down Expand Up @@ -182,12 +174,8 @@ def wait_for_interrupt(backend, frontend):

# Terminate processes
try:
if is_windows():
backend.send_signal(signal.CTRL_BREAK_EVENT)
frontend.send_signal(signal.CTRL_BREAK_EVENT)
else:
backend.terminate()
frontend.terminate()
backend.terminate()
frontend.terminate()

# Wait for clean shutdown
backend.wait(timeout=5)
Expand Down
26 changes: 24 additions & 2 deletions frontend/e2e/api.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
import { test, expect } from "@playwright/test";

// API tests go through the Vite dev server proxy (/api -> backend:8000)
// rather than hitting the backend directly, so they work as soon as
// Playwright's webServer (port 3000) is ready.

test.describe("API Health Check", () => {
// The backend may still be starting when Vite (port 3000) is already up.
// Poll the health endpoint through the proxy until the backend is ready.
test.beforeAll(async ({ request }) => {
const maxWait = 30_000;
const interval = 1_000;
const start = Date.now();
while (Date.now() - start < maxWait) {
try {
const resp = await request.get("/api/health");
if (resp.ok()) return;
} catch {
// Backend not ready yet
}
await new Promise((r) => setTimeout(r, interval));
}
throw new Error("Backend did not become healthy within 30 seconds");
});

test("should have healthy backend API", async ({ request }) => {
const response = await request.get("http://localhost:8000/api/health");
const response = await request.get("/api/health");

expect(response.ok()).toBe(true);
const data = await response.json();
expect(data).toBeDefined();
});

test("should get version from API", async ({ request }) => {
const response = await request.get("http://localhost:8000/api/version");
const response = await request.get("/api/version");

expect(response.ok()).toBe(true);
const data = await response.json();
Expand Down
6 changes: 4 additions & 2 deletions frontend/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ export default defineConfig({
/* Automatically start servers before running tests */
webServer: {
command: process.env.CI ? "cd .. && uv run python frontend/dev.py" : "python dev.py",
url: "http://localhost:3000",
// Use 127.0.0.1 to avoid Node.js 17+ resolving localhost to IPv6 ::1
url: "http://127.0.0.1:3000",
reuseExistingServer: !process.env.CI,
timeout: 30000,
// CI needs extra time for uv sync + backend startup
timeout: 120_000,
},
});
32 changes: 30 additions & 2 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import { defineConfig } from 'vite'
import { createLogger, defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

// Suppress noisy ECONNREFUSED proxy errors while the backend is starting.
// Without this, Vite logs dozens of "http proxy error" stack traces.
const logger = createLogger()
const originalError = logger.error
let proxyWarned = false
logger.error = (msg, options) => {
if (typeof msg === 'string' && msg.includes('http proxy error')) {
if (!proxyWarned) {
console.log('[vite] Waiting for backend on port 8000...')
proxyWarned = true
}
return
}
originalError(msg, options)
}

// https://vitejs.dev/config/
export default defineConfig({
customLogger: logger,
plugins: [react()],
resolve: {
alias: {
Expand All @@ -22,8 +39,19 @@ export default defineConfig({
cors: true,
proxy: {
'/api': {
target: 'http://localhost:8000',
// Use 127.0.0.1 to avoid Node.js 17+ resolving localhost to IPv6 ::1
target: 'http://127.0.0.1:8000',
changeOrigin: true,
// Return 502 on proxy errors so in-flight requests fail fast
// instead of hanging until the backend comes up.
configure: (proxy) => {
proxy.on('error', (_err, _req, res) => {
if (res && 'writeHead' in res && !res.headersSent) {
(res as import('http').ServerResponse).writeHead(502);
(res as import('http').ServerResponse).end();
}
});
},
},
},
watch: {
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ all = [
]

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

Expand Down Expand Up @@ -281,6 +282,8 @@ notice-rgx = "Copyright \\(c\\) Microsoft Corporation\\.\\s*\\n.*Licensed under
# https://github.com/Azure/PyRIT/issues/1176 is fully resolved
# TODO: Remove these ignores once the issues are fixed
"pyrit/{auxiliary_attacks,exceptions,models,ui}/**/*.py" = ["D101", "D102", "D103", "D104", "D105", "D106", "D107", "D401", "D404", "D417", "D418", "DOC102", "DOC201", "DOC202", "DOC402", "DOC501"]
# Backend API routes raise HTTPException handled by FastAPI, not true exceptions
"pyrit/backend/**/*.py" = ["DOC501"]
"pyrit/__init__.py" = ["D104"]

[tool.ruff.lint.pydocstyle]
Expand Down
Loading