Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
9823509
initial, minimal working version of WebSocketCopilotTarget
paulinek13 Dec 22, 2025
62fc335
add useful error message for "server rejected WebSocket connection: H…
paulinek13 Dec 23, 2025
107b715
improve error handling and logging
paulinek13 Dec 23, 2025
41013a2
enhance WebSocket URL validation
paulinek13 Dec 23, 2025
c8e7e83
small fix
paulinek13 Dec 23, 2025
af636b2
fix
paulinek13 Dec 23, 2025
c9ebcee
improve `_parse_message`
paulinek13 Dec 23, 2025
99040d0
useful links
paulinek13 Dec 23, 2025
1f21ebf
add timeouts for responses and connection
paulinek13 Dec 24, 2025
1806e79
start with tests
paulinek13 Dec 25, 2025
3962bbb
require `wss://` only
paulinek13 Dec 25, 2025
7588e8d
add configurable response timeout
paulinek13 Dec 25, 2025
b98f675
fix
paulinek13 Dec 25, 2025
0dab7bb
replace Enum with IntEnum and actually use it
paulinek13 Dec 25, 2025
c2df619
test_dict_to_websocket_static_method
paulinek13 Dec 25, 2025
18fd238
fix
paulinek13 Dec 25, 2025
73b07d0
Refactor WebSocket message parser to handle multiple frames per message
paulinek13 Dec 25, 2025
9a8a878
rename message types in the enum
paulinek13 Dec 25, 2025
4d3c15d
add raw WebSocket messages for testing
paulinek13 Dec 25, 2025
b095d74
remove emojis
paulinek13 Dec 25, 2025
38e6868
simpler way to get the final result
paulinek13 Dec 25, 2025
2430dbe
log full raw message when no parseable content found
paulinek13 Dec 25, 2025
5b2c54a
_value2member_map_
paulinek13 Dec 25, 2025
4a7a7b8
TestParseRawMessage
paulinek13 Dec 25, 2025
acb0a6d
test fix
paulinek13 Dec 25, 2025
276290f
TODO: use msal for auth
paulinek13 Dec 25, 2025
ded56c6
add device code flow authentication method
paulinek13 Dec 26, 2025
558f48f
Revert "TODO: use msal for auth" -- as we need browser automation any…
paulinek13 Dec 26, 2025
02e3a4e
Revert "add device code flow authentication method"
paulinek13 Dec 26, 2025
0a9ee34
add Playwright-based way of getting sydney access token
paulinek13 Dec 27, 2025
cbc06f0
use `msal-extensions` for encrypted token persistence
paulinek13 Dec 28, 2025
4299673
add CopilotAuthenticator to WebSocketCopilotTarget for automated auth…
paulinek13 Dec 28, 2025
c65205b
WORKING multi prompt example
paulinek13 Dec 29, 2025
fd745ba
unit tests update for `WebSocketCopilotTarget`
paulinek13 Dec 29, 2025
fd8bc17
fixes
paulinek13 Dec 29, 2025
7266d7e
AUTH FLOW improvements
paulinek13 Dec 29, 2025
5241160
`CopilotAuthenticator` tests
paulinek13 Dec 30, 2025
dfa0ace
various fixes
paulinek13 Dec 30, 2025
f865698
notebook
paulinek13 Jan 11, 2026
913f7b6
fixing precommit stuff
paulinek13 Jan 11, 2026
806f37f
refresh_token_async/get_token_async
paulinek13 Jan 11, 2026
53c7e10
ProactorEventLoop && notebook rerun
paulinek13 Jan 11, 2026
b11370a
Merge branch 'main' into feat/343/websocket_copilot_target
paulinek13 Jan 11, 2026
2612c74
Merge branch 'main' into feat/343/websocket_copilot_target
paulinek13 Jan 12, 2026
9941c07
Merge branch 'main' into feat/343/websocket_copilot_target
paulinek13 Jan 25, 2026
0f74780
remove unused memory disposal code
paulinek13 Jan 25, 2026
5254800
remove verbose param
paulinek13 Jan 25, 2026
5c692da
make base URL configurable
paulinek13 Jan 25, 2026
5e1cec1
fix tests after changes
paulinek13 Jan 25, 2026
73351d5
rename: `message` -> `raw_websocket_message`
paulinek13 Jan 25, 2026
ae9506a
tiny fix
paulinek13 Jan 25, 2026
ca55989
add `ManualCopilotAuthenticator`
paulinek13 Jan 26, 2026
8043991
add support for ManualCopilotAuthenticator
paulinek13 Jan 26, 2026
89f5ddb
Alternative Authentication with `ManualCopilotAuthenticator`
paulinek13 Jan 26, 2026
957b50c
add ManualCopilotAuthenticator to API reference
paulinek13 Jan 26, 2026
fa03a91
use `silent=True`
paulinek13 Jan 26, 2026
fc9c59d
fix mypy errors
paulinek13 Jan 26, 2026
e63e59a
notebook rerun
paulinek13 Jan 26, 2026
ea92957
minor reorg and test skipping
rlundeen2 Jan 27, 2026
6201f0c
test exclusion
rlundeen2 Jan 27, 2026
c144777
adding mock playwright for unit tests
rlundeen2 Jan 27, 2026
4bfde05
pre-commit
rlundeen2 Jan 27, 2026
cfcc8be
build
rlundeen2 Jan 27, 2026
ea88b70
trying this mypy
rlundeen2 Jan 27, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ cython_debug/

# PyRIT secrets file
.env
.pyrit_cache/

# Cache for generating docs
doc/generate_docs/cache/*
Expand Down
7 changes: 4 additions & 3 deletions doc/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,13 @@ chapters:
- file: code/targets/7_http_target
- file: code/targets/8_openai_responses_target
- file: code/targets/9_message_normalizer
- file: code/targets/10_1_playwright_target
- file: code/targets/10_2_playwright_target_copilot
- file: code/targets/10_3_websocket_copilot_target
- file: code/targets/open_ai_completions
- file: code/targets/playwright_target
- file: code/targets/playwright_target_copilot
- file: code/targets/prompt_shield_target
- file: code/targets/use_huggingface_chat_target
- file: code/targets/realtime_target
- file: code/targets/use_huggingface_chat_target
- file: code/converters/0_converters
sections:
- file: code/converters/1_text_to_text_converters
Expand Down
3 changes: 3 additions & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ API Reference
Authenticator
AzureAuth
AzureStorageAuth
CopilotAuthenticator
ManualCopilotAuthenticator

:py:mod:`pyrit.auxiliary_attacks`
=================================
Expand Down Expand Up @@ -519,6 +521,7 @@ API Reference
PromptTarget
RealtimeTarget
TextTarget
WebSocketCopilotTarget

:py:mod:`pyrit.score`
=====================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"id": "0",
"metadata": {},
"source": [
"# Playwright Target - optional\n",
"# 10.1 Generic Playwright Target\n",
"\n",
"This notebook demonstrates how to interact with the **Playwright Target** in PyRIT.\n",
"\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,11 @@
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.17.2
# kernelspec:
# display_name: pyrit-dev
# language: python
# name: python3
# jupytext_version: 1.18.1
# ---

# %% [markdown]
# # Playwright Target - optional
# # 10.1 Generic Playwright Target
#
# This notebook demonstrates how to interact with the **Playwright Target** in PyRIT.
#
Expand Down Expand Up @@ -158,7 +154,7 @@ async def main(page: Page) -> None:

async def run() -> None:
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=False)
browser = await playwright.chromium.launch(headless=True)
context = await browser.new_context()
page: Page = await context.new_page()
await page.goto("http://127.0.0.1:5000")
Expand All @@ -169,10 +165,10 @@ async def run() -> None:

# Note in Windows this doesn't run in jupyter notebooks due to playwright limitations
# https://github.com/microsoft/playwright-python/issues/480
# await run()
await run()

if __name__ == "__main__":
asyncio.run(run())
# if __name__ == "__main__":
# asyncio.run(run())


# %% [markdown]
Expand Down
288 changes: 288 additions & 0 deletions doc/code/targets/10_3_websocket_copilot_target.ipynb

Large diffs are not rendered by default.

118 changes: 118 additions & 0 deletions doc/code/targets/10_3_websocket_copilot_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# ---
# jupyter:
# jupytext:
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.18.1
# kernelspec:
# display_name: pyrit (3.13.5)
# language: python
# name: python3
# ---

# %% [markdown]
# # 10.3 WebSocket Copilot Target
#
# The `WebSocketCopilotTarget` is an alternative to the `PlaywrightCopilotTarget` that is designed to be more reliable by minimizing dependence on browser automation. Instead of driving the Copilot UI, it communicates directly with Copilot over a WebSocket connection.
#
# By default, this target uses automated authentication which requires:
# - `COPILOT_USERNAME` and `COPILOT_PASSWORD` environment variables
# - Playwright installed: `pip install playwright && playwright install chromium`
#
# Some environments are not suited for automated authentication (e.g. they have security policies with retrieving tokens or have MFA). See the [Alternative Authentication](#alternative-authentication-with-manualcopilotauthenticator) section below.

# %% [markdown]
# ## Basic Usage with `PromptSendingAttack`
#
# The simplest way to interact with the `WebSocketCopilotTarget` is through the `PromptSendingAttack` class.

# %%
# type: ignore
from pyrit.executor.attack import ConsoleAttackResultPrinter, PromptSendingAttack
from pyrit.prompt_target import WebSocketCopilotTarget
from pyrit.setup import IN_MEMORY, initialize_pyrit_async

await initialize_pyrit_async(memory_db_type=IN_MEMORY, silent=True)

target = WebSocketCopilotTarget()
attack = PromptSendingAttack(objective_target=target)

objective = "Tell me a joke about AI"

result = await attack.execute_async(objective=objective)
await ConsoleAttackResultPrinter().print_conversation_async(result=result)

# %% [markdown]
# ## Multi-Turn Conversations
#
# The `WebSocketCopilotTarget` supports multi-turn conversations by leveraging Copilot's server-side conversation management. It automatically generates consistent `session_id` and `conversation_id` values for each PyRIT conversation, enabling Copilot to maintain context across multiple turns.
#
# However, this target does not support setting a system prompt nor modifying conversation history. As a result, it cannot be used with attack strategies that require altering prior messages (such as PAIR, TAP, or flip attack) or in contexts where a `PromptChatTarget` is required.
#
# Here is a simple multi-turn conversation example:

# %%
from pyrit.executor.attack import ConsoleAttackResultPrinter, MultiPromptSendingAttack
from pyrit.models import Message
from pyrit.prompt_target import WebSocketCopilotTarget
from pyrit.setup import IN_MEMORY, initialize_pyrit_async

await initialize_pyrit_async(memory_db_type=IN_MEMORY, silent=True)

target = WebSocketCopilotTarget()

prompts = [
"I'm thinking of a number between 1 and 10.",
"It's greater than 5.",
"It's an even number.",
"What number am I thinking of?",
]

messages = [Message.from_prompt(prompt=p, role="user") for p in prompts]
multi_turn_attack = MultiPromptSendingAttack(objective_target=target)

result = await multi_turn_attack.execute_async(
objective="Engage in a multi-turn conversation about a number guessing game",
user_messages=messages,
)

await ConsoleAttackResultPrinter().print_conversation_async(result=result)

# %% [markdown]
# ## Alternative Authentication with `ManualCopilotAuthenticator`
#
# If browser automation is not suitable for your environment, you can use the `ManualCopilotAuthenticator` instead. This authenticator accepts a pre-obtained access token that you can extract from your browser's DevTools.
#
# How to obtain the access token:
#
# 1. Open the Copilot webapp (e.g., https://m365.cloud.microsoft/chat) in a browser.
# 2. Open DevTools (F12 or Ctrl+Shift+I).
# 3. Go to the Network tab.
# 4. Filter by "Socket" connections or search for "Chathub".
# 5. Start typing in the chat to initiate a WebSocket connection.
# 6. Look for the latest WebSocket connection to `substrate.office.com/m365Copilot/Chathub`.
# 7. You may find the `access_token` in the request URL or in the request payload.
#
# You can either pass the token directly or set the `COPILOT_ACCESS_TOKEN` environment variable.

# %%
from pyrit.auth import ManualCopilotAuthenticator
from pyrit.executor.attack import ConsoleAttackResultPrinter, PromptSendingAttack
from pyrit.prompt_target import WebSocketCopilotTarget
from pyrit.setup import IN_MEMORY, initialize_pyrit_async

await initialize_pyrit_async(memory_db_type=IN_MEMORY, silent=True)

# Option 1: Pass the token directly
# auth = ManualCopilotAuthenticator(access_token="eyJ0eXAi...")

# Option 2: Use COPILOT_ACCESS_TOKEN environment variable
auth = ManualCopilotAuthenticator()

target = WebSocketCopilotTarget(authenticator=auth)
attack_manual = PromptSendingAttack(objective_target=target)

result_manual = await attack_manual.execute_async(objective="Hello! Who are you?")
await ConsoleAttackResultPrinter().print_conversation_async(result=result_manual)
2 changes: 1 addition & 1 deletion doc/contributing/1c_install_conda.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ This is a guide for how to install PyRIT into a `conda` environment.
```bash
pip install -e '.[dev]'
```
If you plan to use the [Playwright integration](../code/targets/playwright_target.py), install with the playwright extra:
If you plan to use the [Playwright integration](../code/targets/10_1_playwright_target.py), install with the playwright extra:
```bash
pip install -e '.[dev,playwright]'
```
Expand Down
4 changes: 4 additions & 0 deletions pyrit/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@
get_default_azure_scope,
)
from pyrit.auth.azure_storage_auth import AzureStorageAuth
from pyrit.auth.copilot_authenticator import CopilotAuthenticator
from pyrit.auth.manual_copilot_authenticator import ManualCopilotAuthenticator

__all__ = [
"Authenticator",
"AzureAuth",
"AzureStorageAuth",
"CopilotAuthenticator",
"ManualCopilotAuthenticator",
"TokenProviderCredential",
"get_azure_token_provider",
"get_azure_async_token_provider",
Expand Down
29 changes: 22 additions & 7 deletions pyrit/auth/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,45 @@
from __future__ import annotations

import abc
from abc import abstractmethod


class Authenticator(abc.ABC):
"""Abstract base class for authenticators."""

token: str

@abstractmethod
def refresh_token(self) -> str:
"""
Refresh the authentication token.
Refresh the authentication token synchronously.

Returns:
str: The refreshed authentication token.
"""
raise NotImplementedError("refresh_token method not implemented")
raise NotImplementedError("Either refresh_token or refresh_token_async method must be implemented")

async def refresh_token_async(self) -> str:
"""
Refresh the authentication token asynchronously.

Returns:
str: The refreshed authentication token.
"""
raise NotImplementedError("Either refresh_token or refresh_token_async method must be implemented")

@abstractmethod
def get_token(self) -> str:
"""
Get the current authentication token.
Get the current authentication token synchronously.

Returns:
str: The current authentication token.
"""
raise NotImplementedError("Either get_token or get_token_async method must be implemented")

async def get_token_async(self) -> str:
"""
Get the current authentication token asynchronously.

Returns:
str: The current authentication token.
"""
raise NotImplementedError("get_token method not implemented")
raise NotImplementedError("Either get_token or get_token_async method must be implemented")
Loading