Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 2 additions & 12 deletions doc/gui/0_gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,25 +179,15 @@ Targets can also be auto-populated by adding the `target` initializer to your `~

### Configuration Editor

The **Configuration** page provides administrator-only editing for the files and scripts used to configure PyRIT. It has three tabs:
The **Configuration** page provides administrator-only editing for the files and scripts used to configure PyRIT. It has four tabs:

- **PyRIT Configuration** edits the active `.pyrit_conf` YAML file. The source may be a local file or an Azure Blob URI. Saving validates the configuration before replacing it.
- **Environment & Secrets** lists the configured local dotenv files and Azure Key Vault bootstrap secrets. Content is loaded only after selecting a source. Saves validate the dotenv document and reject the update if the source changed since it was loaded.
- **Initializers** shows the read-only startup sequence from the active `.pyrit_conf`, in run order, along with the catalog of registered initializers.
- **Custom Initializers** registers or removes Python initializer scripts. This tab requires `allow_custom_initializers: true`; scripts are stored in the configured local directory or Azure Blob container and must define a concrete `PyRITInitializer` subclass.

Use **Reload** to discard local edits and fetch the latest source content. Saved configuration and environment changes take effect after restarting PyRIT. Custom initializer scripts execute under the backend service identity, so only trusted administrators should manage them.

### Initializers

The **Initializers** page (in the left navigation) lets you review and extend how PyRIT sets itself up at startup — for example, the `target` initializer's `tags` and `auto_group` settings.

The page has two sections:

- **Baseline initializers** are read-only. They come from your active configuration file (`~/.pyrit/.pyrit_conf`) and run first, in order.
- **Additional initializers** are added in the GUI and saved to the memory database. They run after the baseline, in the order shown. You can add more than one initializer of the same type — each is its own invocation.

Use **Apply now** to re-run a single initializer immediately against the running backend — handy for picking up an environment or setting change without a restart. Saved additional initializers and `.pyrit_conf` edits otherwise take effect the next time the backend starts.

---

## Connection Health
Expand Down
23 changes: 17 additions & 6 deletions frontend/e2e/touch-targets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,20 @@ async function installTouchTargetMocks(page: Page): Promise<void> {
);
return;
}
if (apiPath === "/config" && method === "GET") {
await route.fulfill(
jsonResponse({
content: "initializers: []\n",
source: "C:/Users/test/.pyrit/.pyrit_conf",
version: "touch-target-config-v1",
})
);
return;
}
if (apiPath === "/initializers/settings" && method === "GET") {
await route.fulfill(
jsonResponse({
baseline: [],
additional: [],
configured: [],
})
);
return;
Expand Down Expand Up @@ -455,10 +464,11 @@ test.describe("Mobile touch targets", () => {
});

test("keeps the Initializer selector at least 44px", async ({ page }) => {
await page.goto("/initializers");
await page.goto("/config");
await page.getByRole("tab", { name: "Initializers", exact: true }).click();

await expectMinimumTouchTarget(
page.getByRole("combobox", { name: "Initializer to add" })
page.getByRole("button", { name: "Browse available initializers" })
);
await expectNoDocumentOverflow(page);
});
Expand Down Expand Up @@ -659,9 +669,10 @@ test("preserves compact desktop controls and existing sidebar dimensions", async
page.getByRole("button", { name: "Expand inner targets" })
);

await page.goto("/initializers");
await page.goto("/config");
await page.getByRole("tab", { name: "Initializers", exact: true }).click();
await expectCompactDesktopTarget(
page.getByRole("combobox", { name: "Initializer to add" })
page.getByRole("button", { name: "Browse available initializers" })
);

await startChatWithMessages(page);
Expand Down
3 changes: 0 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import ChatWindow from './components/Chat/ChatWindow'
import AttackNotFound from './components/Chat/AttackNotFound'
import Home from './components/Home/Home'
import TargetConfig from './components/Config/TargetConfig'
import Initializers from './components/Initializers/Initializers'
import Configuration from './components/Configuration/Configuration'
import AttackHistory from './components/History/AttackHistory'
import ScenarioCatalog from './components/Scenarios/ScenarioCatalog'
Expand Down Expand Up @@ -43,7 +42,6 @@ const VIEW_PATHS: Record<ViewName, string> = {
chat: '/chat',
history: '/history',
targets: '/targets',
initializers: '/initializers',
scenarios: '/scanner',
configuration: '/config',
}
Expand Down Expand Up @@ -491,7 +489,6 @@ function App() {
/>
}
/>
<Route path="/initializers" element={<Initializers />} />
<Route path="/scanner" element={<ScenarioCatalog />} />
<Route
path="/scanner/:scenarioName"
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/Configuration/Configuration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jest.mock('@/services/api', () => ({
updateEnvironmentFile: jest.fn(),
},
initializersApi: {
getSettings: jest.fn(),
listRegistered: jest.fn(),
listCustom: jest.fn(),
register: jest.fn(),
unregister: jest.fn(),
Expand Down Expand Up @@ -64,6 +66,19 @@ describe('Configuration', () => {
})
mockedInitializersApi.register.mockResolvedValue()
mockedInitializersApi.unregister.mockResolvedValue()
mockedInitializersApi.getSettings.mockResolvedValue({
configured: [{ initializer_name: 'target', parameters: { tags: ['default'] }, order_index: 0 }],
})
mockedInitializersApi.listRegistered.mockResolvedValue({
items: [{
initializer_name: 'target',
initializer_type: 'TargetInitializer',
description: 'Registers targets.',
required_env_vars: [],
supported_parameters: [],
}],
pagination: { limit: 200, has_more: false },
})
})

it('should load and display configuration content', async () => {
Expand Down Expand Up @@ -204,4 +219,14 @@ describe('Configuration', () => {
})
})

it('should show configured initializers without a runtime apply action', async () => {
const user = userEvent.setup()
renderPage()

await user.click(screen.getByRole('tab', { name: 'Initializers' }))

expect(await screen.findByTestId('configured-initializer-row-0')).toHaveTextContent('Registers targets.')
expect(screen.queryByRole('button', { name: 'Apply now' })).not.toBeInTheDocument()
})

})
7 changes: 6 additions & 1 deletion frontend/src/components/Configuration/Configuration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ArrowSyncRegular, SaveRegular } from '@fluentui/react-icons'
import { configurationApi } from '@/services/api'
import { toApiError } from '@/services/errors'
import EditorWorkspace from '@/components/EditorWorkspace'
import Initializers from '@/components/Initializers/Initializers'

import { useConfigurationStyles } from './Configuration.styles'
import CustomInitializerFiles from './CustomInitializerFiles'
Expand All @@ -27,7 +28,7 @@ interface StatusMessage {
text: string
}

type ConfigurationTab = 'configuration' | 'environment' | 'custom-initializers'
type ConfigurationTab = 'configuration' | 'environment' | 'initializers' | 'custom-initializers'

export default function Configuration() {
const styles = useConfigurationStyles()
Expand Down Expand Up @@ -102,6 +103,7 @@ export default function Configuration() {
if (
data.value === 'configuration'
|| data.value === 'environment'
|| data.value === 'initializers'
|| data.value === 'custom-initializers'
) {
setSelectedTab(data.value)
Expand All @@ -117,6 +119,7 @@ export default function Configuration() {
<TabList selectedValue={selectedTab} onTabSelect={handleTabSelect}>
<Tab value="configuration">PyRIT Configuration</Tab>
<Tab value="environment">Environment &amp; Secrets</Tab>
<Tab value="initializers">Initializers</Tab>
<Tab value="custom-initializers">Custom Initializers</Tab>
</TabList>

Expand All @@ -128,6 +131,8 @@ export default function Configuration() {

{selectedTab === 'custom-initializers' ? (
<CustomInitializerFiles />
) : selectedTab === 'initializers' ? (
<Initializers />
) : selectedTab === 'environment' ? (
<EnvironmentFiles />
) : loading ? (
Expand Down

This file was deleted.

Loading