Skip to content

FEAT: Security & Azure deployment for CoPyRIT GUI#1554

Open
adrian-gavrila wants to merge 171 commits intomicrosoft:mainfrom
adrian-gavrila:adrian-gavrila/frontend-attack-view
Open

FEAT: Security & Azure deployment for CoPyRIT GUI#1554
adrian-gavrila wants to merge 171 commits intomicrosoft:mainfrom
adrian-gavrila:adrian-gavrila/frontend-attack-view

Conversation

@adrian-gavrila
Copy link
Copy Markdown
Contributor

@adrian-gavrila adrian-gavrila commented Mar 31, 2026

Changes

  • Authentication: Entra ID login with MSAL PKCE on the frontend, JWT validation and group-based authorization
    on the backend. Automatically disabled for local development.
  • Security headers: CSP, HSTS, X-Frame-Options, Cache-Control, and other standard browser security headers.
  • Infrastructure: Bicep template for Azure Container Apps — managed identity, Key Vault secrets, logging, and
    optional private networking.
  • Deployment pipeline: Azure DevOps pipeline that builds, pushes, and deploys to test with opt-in production
    promotion.
  • Docker: Updated container setup for Azure credential forwarding and config file mounting.

Tests & docs

  • Frontend auth tests (AuthProvider, msalConfig, API service).
  • Deployment guide (infra/README.md) and Docker quickstart (docker/QUICKSTART.md).

romanlutz and others added 30 commits February 28, 2026 14:49
- Add run_initializers_async to pyrit.setup for programmatic initialization
- Switch AIRTInitializer to Entra (Azure AD) auth, removing API key requirements
- Add --config-file flag to pyrit_backend CLI
- Use PyRIT configuration loader in FrontendCore and pyrit_backend
- Update AIRTTargetInitializer with new target types

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add conversation_stats model and attack_result extensions
- Add get_attack_results with filtering by harm categories, labels,
  attack type, and converter types to memory interface
- Implement SQLite-specific JSON filtering for attack results
- Add memory_models field for targeted_harm_categories
- Add prompt_metadata support to openai image/video/response targets
- Fix missing return statements in SQLite harm_category and label filters

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add attack CRUD routes with conversation management
- Add message sending with target dispatch and response handling
- Add attack mappers for domain-to-DTO conversion with signed blob URLs
- Add attack service with video remix support and piece persistence
- Expand target service and routes with registry-based target management
- Add version endpoint with database info

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add attack-centric chat UI with multi-conversation support
- Add conversation panel with branching and message actions
- Add attack history view with filtering
- Add labels bar for attack metadata
- Add target configuration with create dialog
- Add message mapper utilities for backend/frontend translation
- Add video playback support with signed blob URLs
- Add InputBox with attachment support and auto-expand
- Update dev.py with --detach, logs, and process management
- Add e2e tests for chat, config, and flow scenarios

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ssibility

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename supports_multiturn_chat to supports_multi_turn to align with TargetCapabilities field
- Use target_obj.capabilities.supports_multi_turn instead of isinstance check
- Update tests to set capabilities on mock targets

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…_async

Reverts the separate run_initializers_async function and restores the
original pattern where run_scenario_async calls initialize_pyrit_async
a second time with initializers. This avoids a larger refactor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Catch ValueError in get_conversation_messages route, return 400
- Fix target_registry_name field description
- Simplify redundant except (ValueError, Exception) to except Exception
- Fix docstring: converter_classes -> converter_types
- Fix test assertions: converter_types -> converter_classes (matches memory API)
- Remove dead tests for deleted helper methods
- Restore azure_openai_video target config to match main

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Move _inject_video_id_from_history and _strip_video_pieces methods from
  AttackService to OpenAIVideoTarget where they belong
- Update _validate_request to accept video_path pieces and check for
  video_path+image_path conflicts
- Add ValueError when video_path is present but no video_id can be resolved
- Add 7 unit tests for the inject/strip logic
- Remove video-specific logic from attack_service._send_and_store_message

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adrian Gavrila and others added 4 commits April 1, 2026 12:46
- Change getApiScopes to request https://graph.microsoft.com/User.Read
- Reuse shared getApiScopes in api.ts instead of duplicate
- Update backend token validation audience to graph.microsoft.com
- Update test assertions for new scope
- Enables groups overage resolution via Graph API

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adrian Gavrila and others added 6 commits April 2, 2026 15:18
- Replace toBeInTheDocument with toBeVisible for user-facing assertions
- Add afterEach restoreAllMocks for full test isolation
- Refactor AuthConfig from global cache to React Context
- Rename useMsal instance to msalInstance for clarity
- Extract _authenticate_request_async from dispatch method
- Replace magic number with removeprefix for Bearer token parsing
- Rename overage methods/comments for clarity
- Add _client_id usage comment in auth middleware
- Clarify .azure directory mount in Docker run script
- Standardize Entra ID vs Azure terminology in docs
- Expand acronyms and add links in infra README
- Add what-if preview section to infra README

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract AuthConfigContext to separate file (react-refresh/only-export-components)
- Replace ghcr.io/astral-sh/uv container image with install script to comply
  with Microsoft container security policy (CSSC)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

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

This PR adds Entra ID (MSAL PKCE) authentication plus backend JWT validation and security headers for the CoPyRIT GUI, and introduces Azure Container Apps IaC + an Azure DevOps deployment pipeline to support hardened deployments.

Changes:

  • Add frontend MSAL PKCE auth flow and attach Bearer tokens to API requests; add backend JWT validation and security headers middleware.
  • Add Azure Container Apps Bicep templates + deployment documentation and example parameters.
  • Update Docker startup/config mounting and introduce an ADO pipeline for build/push/deploy.

Reviewed changes

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

Show a summary per file
File Description
uv.lock Adds PyJWT dependency to lockfile.
pyproject.toml Adds PyJWT dependency for backend JWT validation.
pyrit/backend/main.py Wires in security headers + Entra auth middleware; tightens CORS; disables docs in prod.
pyrit/backend/middleware/init.py Exposes SecurityHeadersMiddleware.
pyrit/backend/middleware/security_headers.py Adds CSP/HSTS/etc security header injection middleware.
pyrit/backend/middleware/auth.py Adds Entra JWT validation + group authorization middleware.
pyrit/backend/routes/auth.py Adds /api/auth/config endpoint to serve non-secret MSAL config.
infra/README.md Adds deployment guide for Azure Container Apps hardened setup.
infra/parameters.example.json Adds example deployment parameters.
infra/main.bicep Adds Bicep template for ACA, MI, KV secret refs, logging, optional private networking.
infra/main.json Generated ARM JSON for the Bicep template.
gui-deploy.yml Adds Azure DevOps pipeline to build/push image and deploy to test/prod.
frontend/src/main.tsx Wraps app in AuthProvider.
frontend/src/auth/msalConfig.ts Adds runtime MSAL configuration + API scope helpers.
frontend/src/auth/msalConfig.test.ts Adds unit tests for MSAL config helpers.
frontend/src/auth/AuthConfigContext.ts Adds context for auth config values.
frontend/src/auth/AuthProvider.tsx Adds MSAL initialization/login redirect wrapper for the app.
frontend/src/auth/AuthProvider.test.tsx Adds tests for auth init and auth-disabled behavior.
frontend/src/services/api.ts Adds token acquisition + Authorization header injection + 401 retry.
frontend/src/services/api.test.ts Updates tests for async request interceptor.
frontend/src/App.tsx Wires MSAL instance into API client from within the app.
frontend/src/App.test.tsx Mocks MSAL hooks and API client wiring in tests.
frontend/package.json Adds MSAL deps and related test tooling deps.
frontend/package-lock.json Updates lockfile for new dependencies.
frontend/.npmrc Pins npm registry and enables legacy peer deps.
build_scripts/prepare_package.py Changes npm install to use --legacy-peer-deps.
docker/start.sh Writes .env from PYRIT_ENV_CONTENTS; starts backend with args for GUI/AzureSQL/initializers.
docker/run_pyrit_docker.py Mounts .pyrit_conf and ~/.azure; enables interactive -it.
docker/QUICKSTART.md Updates Docker quickstart with new mounting + auth notes.
docker/Dockerfile Requires explicit BASE_IMAGE build arg (no default).
docker/docker-compose.yaml Mounts .pyrit_conf and ~/.azure into GUI container.
.pyrit_conf_example Updates example config (currently duplicates operator/operation).
.devcontainer/Dockerfile Changes uv installation method in devcontainer image.
Files not reviewed (1)
  • frontend/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)

frontend/package.json:33

  • @azure/msal-react@^5.0.7 declares a peer dependency on React ^19.2.1 (see package-lock), but this project depends on React ^18.3.1. The current workaround (legacy-peer-deps) bypasses npm's resolver but leaves an unsupported dependency combination that can break at runtime. Either pin @azure/msal-react to a version that supports React 18, or upgrade React (and related tooling) to the required major version.
  "dependencies": {
    "@azure/msal-browser": "^5.5.0",
    "@azure/msal-react": "^5.0.7",
    "@fluentui/react-components": "^9.54.0",
    "@fluentui/react-icons": "^2.0.258",
    "axios": "^1.13.5",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-error-boundary": "^6.1.1"
  },

Comment on lines 40 to +55
@@ -45,6 +48,11 @@ function App() {
/** Persisted filter state for the history view */
const [historyFilters, setHistoryFilters] = useState<HistoryFilters>({ ...DEFAULT_HISTORY_FILTERS })

// Wire MSAL instance into the API client for Bearer token injection
useEffect(() => {
setMsalInstance(msalInstance as PublicClientApplication)
}, [msalInstance])

Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

App calls useMsal() unconditionally. When auth is disabled, AuthProvider renders children without an MsalProvider, so this hook will throw at runtime and break local dev. Move MSAL wiring entirely into AuthProvider (it already calls setMsalInstance/setClientId) and remove useMsal usage from App, or ensure MsalProvider is always present (even in auth-disabled mode).

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +33
allowedGroupId: string
}

export async function fetchAuthConfig(): Promise<AuthConfig> {
try {
const response = await fetch('/api/auth/config')
if (!response.ok) {
// Auth endpoint not available — treat as auth disabled
return { clientId: '', tenantId: '', allowedGroupId: '' }
}
return (await response.json()) as AuthConfig
} catch {
// Network error (e.g., backend not running yet) — treat as auth disabled
return { clientId: '', tenantId: '', allowedGroupId: '' }
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

AuthConfig expects allowedGroupId, but the backend /api/auth/config response returns allowedGroupIds (plural). As-is, fetchAuthConfig() will return an object missing allowedGroupId at runtime. Align the field name and type on both sides (e.g., allowedGroupIds: string or allowedGroupIds: string[]).

Suggested change
allowedGroupId: string
}
export async function fetchAuthConfig(): Promise<AuthConfig> {
try {
const response = await fetch('/api/auth/config')
if (!response.ok) {
// Auth endpoint not available — treat as auth disabled
return { clientId: '', tenantId: '', allowedGroupId: '' }
}
return (await response.json()) as AuthConfig
} catch {
// Network error (e.g., backend not running yet) — treat as auth disabled
return { clientId: '', tenantId: '', allowedGroupId: '' }
allowedGroupIds: string[]
}
function parseAuthConfig(
config: unknown,
): AuthConfig {
if (!config || typeof config !== 'object') {
return { clientId: '', tenantId: '', allowedGroupIds: [] }
}
const authConfig = config as {
clientId?: unknown
tenantId?: unknown
allowedGroupIds?: unknown
allowedGroupId?: unknown
}
const allowedGroupIds = Array.isArray(authConfig.allowedGroupIds)
? authConfig.allowedGroupIds.filter(
(groupId): groupId is string => typeof groupId === 'string',
)
: typeof authConfig.allowedGroupIds === 'string'
? [authConfig.allowedGroupIds]
: typeof authConfig.allowedGroupId === 'string'
? [authConfig.allowedGroupId]
: []
return {
clientId: typeof authConfig.clientId === 'string' ? authConfig.clientId : '',
tenantId: typeof authConfig.tenantId === 'string' ? authConfig.tenantId : '',
allowedGroupIds,
}
}
export async function fetchAuthConfig(): Promise<AuthConfig> {
try {
const response = await fetch('/api/auth/config')
if (!response.ok) {
// Auth endpoint not available — treat as auth disabled
return { clientId: '', tenantId: '', allowedGroupIds: [] }
}
return parseAuthConfig(await response.json())
} catch {
// Network error (e.g., backend not running yet) — treat as auth disabled
return { clientId: '', tenantId: '', allowedGroupIds: [] }

Copilot uses AI. Check for mistakes.
*/
export function getApiScopes(clientId: string): string[] {
if (!clientId) return ['openid', 'profile', 'email']
return [`${clientId}/access`]
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

The API scope string is likely incorrect. With the default Application ID URI (api://<client-id>), the scope should be api://<client-id>/access, not <client-id>/access. Using the current value will typically cause MSAL token acquisition to fail with an invalid scope / audience mismatch.

Suggested change
return [`${clientId}/access`]
return [`api://${clientId}/access`]

Copilot uses AI. Check for mistakes.
_PUBLIC_PATHS = {
"/api/health",
"/api/auth/config",
"/api/media",
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

/api/media is exempted from authentication. This endpoint serves local result artifacts and is reachable without a Bearer token, which can leak generated media to anyone who can guess/obtain a URL. If this needs to work with <img src=...>, consider a token-protected fetch+blob URL approach on the frontend, or make the backend issue short-lived signed URLs instead of making the route public.

Suggested change
"/api/media",

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +205
async def _resolve_excess_groups_async(self, claims: dict[str, Any], token: str) -> list[str]:
"""
Resolve group membership via Microsoft Graph when user is in >200 groups.

When a user is in >200 groups, Entra ID replaces the `groups` claim with
`_claim_sources` containing a Graph API endpoint. This method calls the
Microsoft Graph `getMemberObjects` endpoint to retrieve transitive group
memberships, using the user's access token.

Args:
claims: The decoded JWT claims containing _claim_sources.
token: The raw Bearer token to forward to Graph API.

Returns:
List of group IDs the user belongs to, or empty list on failure.
"""
try:
claim_sources = claims.get("_claim_sources", {})
src = claim_sources.get("src1", {})
endpoint = src.get("endpoint", "")

if not endpoint:
logger.debug("No group resolution endpoint found in _claim_sources")
return []

# The _claim_sources endpoint may be a legacy graph.windows.net URL.
# Rewrite to Microsoft Graph (graph.microsoft.com) which is the
# supported API. The legacy Azure AD Graph was retired in 2023.
if "graph.windows.net" in endpoint:
# Legacy format: https://graph.windows.net/{tenant}/users/{oid}/getMemberObjects
# Graph format: https://graph.microsoft.com/v1.0/me/getMemberObjects
endpoint = "https://graph.microsoft.com/v1.0/me/getMemberObjects"

all_group_ids: list[str] = []
async with httpx.AsyncClient() as client:
response = await client.post(
endpoint,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json={"securityEnabledOnly": True},
timeout=10.0,
)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

The “group overage” fallback calls Microsoft Graph using the same access token that was issued for this API (aud = app client ID). Graph will reject tokens not issued for the Graph audience, so this path will consistently fail (returning []) and can incorrectly deny users who hit group overage. Either remove this fallback and rely on ApplicationGroup to avoid overage, or implement a proper on-behalf-of exchange to obtain a Graph token before calling Graph.

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +78
# Operator and Operation Labels
# ------------------------------
# Default labels applied to all attacks created with PyRIT.
#
# - operator: Identifies who is running the attack (e.g., your team name or alias).
# - operation: Groups related attacks under a campaign or engagement name.
#
# Both are optional.
operator: roakey
operation: op_trash_panda

Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

This example config duplicates the operator/operation keys (they appear twice). In YAML, the later keys win, which is confusing for users copying the example. Remove the duplicate block so each key is defined once.

Suggested change
# Operator and Operation Labels
# ------------------------------
# Default labels applied to all attacks created with PyRIT.
#
# - operator: Identifies who is running the attack (e.g., your team name or alias).
# - operation: Groups related attacks under a campaign or engagement name.
#
# Both are optional.
operator: roakey
operation: op_trash_panda

Copilot uses AI. Check for mistakes.
Comment on lines 56 to 60
# Install uv system-wide and create pyrit-dev venv
COPY --from=ghcr.io/astral-sh/uv:0.10.8 /uv /uvx /bin/
RUN curl -LsSf https://astral.sh/uv/0.10.8/install.sh | sh \
&& mv /root/.local/bin/uv /bin/uv \
&& mv /root/.local/bin/uvx /bin/uvx
RUN uv venv /opt/venv --python 3.11 --prompt pyrit-dev \
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

Installing uv via curl ... | sh reduces supply-chain integrity compared to the previous approach of copying a pinned binary from ghcr.io/astral-sh/uv:0.10.8. Consider reverting to the pinned image copy, or at least verify the downloaded artifact (checksum/signature) to make devcontainer builds reproducible and auditable.

Copilot uses AI. Check for mistakes.
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.

6 participants