Skip to content
Open
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
8 changes: 5 additions & 3 deletions .pyrit_conf_example
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,11 @@ allow_custom_initializers: false
# otherwise DefaultAzureCredential is used. Defaults to ~/.pyrit/scenario_presets.
# scenario_presets_source: https://account.blob.core.windows.net/container/scenario_presets

# Optional storage for ScenarioSuite definitions, which contain ordered references
# to scenario presets. This supports the same local directory and Azure Blob
# sources as scenario_presets_source. Defaults to ~/.pyrit/scenario_suites.
# Optional storage for operator-authored ScenarioSuite definitions, which contain
# ordered references to scenario presets. PyRIT merges these editable documents
# with read-only suites registered in code; a stored document takes precedence on
# a name collision. This supports the same local directory and Azure Blob sources
# as scenario_presets_source. Defaults to ~/.pyrit/scenario_suites.
# scenario_suites_source: https://account.blob.core.windows.net/container/scenario_suites

# Upload Folder for API-Created Targets
Expand Down
4 changes: 3 additions & 1 deletion doc/getting_started/pyrit_conf.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ Stores ScenarioSuite definitions in a local directory or Azure Blob container. A
scenario_suites_source: https://account.blob.core.windows.net/pyrit-storage/scenario-suites
```

Suites store ordered references to scenario presets and resolve each preset's current status when read. A SAS query string may be included; otherwise, PyRIT uses `DefaultAzureCredential`. The default is `~/.pyrit/scenario_suites`. Local writes use the same hidden lock-file and atomic replacement behavior as scenario presets.
Suites store ordered references to scenario presets and resolve each preset's current status when read. The `ScenarioSuiteRegistry` merges editable suites from this source with read-only suites registered by code. A stored document takes precedence when both layers use the same name, while creating or updating a document under a registered name is rejected so one name has one owner. Registered suites report no document version.

A SAS query string may be included; otherwise, PyRIT uses `DefaultAzureCredential`. The default is `~/.pyrit/scenario_suites`. Local writes use the same hidden lock-file and atomic replacement behavior as scenario presets. Startup scans the configured source so malformed suite documents are reported immediately without preventing the backend from starting.

### `initialization_scripts`

Expand Down
58 changes: 57 additions & 1 deletion frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -485,14 +485,39 @@ jest.mock("./components/ScenarioPresets/ScenarioPresetEditor", () => {
});

jest.mock("./components/ScenarioSuites/ScenarioSuiteLibrary", () => {
const MockScenarioSuiteLibrary = () => <div data-testid="scenario-suite-library" />;
const MockScenarioSuiteLibrary = ({
operationId,
defaultsReady,
}: {
operationId: string | null;
defaultsReady: boolean;
}) => (
<div
data-testid="scenario-suite-library"
data-operation-id={operationId ?? ""}
data-defaults-ready={String(defaultsReady)}
/>
);
MockScenarioSuiteLibrary.displayName = "MockScenarioSuiteLibrary";
return {
__esModule: true,
default: MockScenarioSuiteLibrary,
};
});

jest.mock("./components/ScenarioSuites/ScenarioSuiteExecutionPage", () => {
const { useParams } = jest.requireActual("react-router");
const MockScenarioSuiteExecutionPage = () => {
const { executionId } = useParams();
return <div data-testid="scenario-suite-execution-page">{executionId ?? ""}</div>;
};
MockScenarioSuiteExecutionPage.displayName = "MockScenarioSuiteExecutionPage";
return {
__esModule: true,
default: MockScenarioSuiteExecutionPage,
};
});

jest.mock("./components/ScenarioSuites/ScenarioSuiteEditor", () => {
const { useParams } = jest.requireActual("react-router");
const MockScenarioSuiteEditor = ({ mode }: { mode: string }) => {
Expand Down Expand Up @@ -767,6 +792,16 @@ describe("App", () => {
expect(editor).toHaveTextContent("nightly_suite");
});

it("renders transient suite execution progress as a nested registry route", () => {
renderApp("/registry/scenario-suites/executions/execution-1");

expect(screen.getByTestId("main-layout")).toHaveAttribute(
"data-current-view",
"registry"
);
expect(screen.getByTestId("scenario-suite-execution-page")).toHaveTextContent("execution-1");
});

it("passes the globally resolved Operation to the preset library", async () => {
const operationId = "11111111-1111-4111-8111-111111111111";
writeUserPreferences("local", {
Expand All @@ -788,6 +823,27 @@ describe("App", () => {
});
});

it("passes the globally resolved Operation to the suite library", async () => {
const operationId = "11111111-1111-4111-8111-111111111111";
writeUserPreferences("local", {
...DEFAULT_USER_PREFERENCES,
operation: { id: operationId },
});
mockGetOperation.mockResolvedValue({
id: operationId,
name: "Suite launch",
created_at: "2026-10-20T18:00:00Z",
});

renderApp("/registry/scenario-suites");

const library = screen.getByTestId("scenario-suite-library");
await waitFor(() => {
expect(library).toHaveAttribute("data-operation-id", operationId);
expect(library).toHaveAttribute("data-defaults-ready", "true");
});
});

it("redirects a bookmarked /scanner/presets to the registry section", async () => {
render(
<MemoryRouter initialEntries={["/scanner/presets"]}>
Expand Down
19 changes: 18 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import ScenarioPresetEditor from './components/ScenarioPresets/ScenarioPresetEdi
import ScenarioPresetLibrary from './components/ScenarioPresets/ScenarioPresetLibrary'
import { presetEditorRoutePath } from './components/ScenarioPresets/presetRoutes'
import ScenarioSuiteEditor from './components/ScenarioSuites/ScenarioSuiteEditor'
import ScenarioSuiteExecutionPage from './components/ScenarioSuites/ScenarioSuiteExecutionPage'
import ScenarioSuiteLibrary from './components/ScenarioSuites/ScenarioSuiteLibrary'
import DatasetCatalog from './components/Datasets/DatasetCatalog'
import DatasetSelection from './components/Datasets/DatasetSelection'
Expand Down Expand Up @@ -866,7 +867,23 @@ function AppContent({ operatorAlias }: { operatorAlias: string | null }) {
/>
<Route path="scenario-presets/new" element={<ScenarioPresetEditor mode="create" />} />
<Route path="scenario-presets/:presetName/edit" element={<ScenarioPresetEditor mode="edit" />} />
<Route path="scenario-suites" element={<ScenarioSuiteLibrary />} />
<Route
path="scenario-suites"
element={
<ScenarioSuiteLibrary
targets={registry.targets}
defaultObjectiveTarget={targetDefaults.objectiveTarget}
defaultAdversarialTarget={targetDefaults.adversarialTarget}
labels={globalLabels}
operationId={operationId}
defaultsReady={newActivityReady}
/>
}
/>
<Route
path="scenario-suites/executions/:executionId"
element={<ScenarioSuiteExecutionPage />}
/>
<Route path="scenario-suites/new" element={<ScenarioSuiteEditor mode="create" />} />
<Route path="scenario-suites/:suiteName/edit" element={<ScenarioSuiteEditor mode="edit" />} />
</Route>
Expand Down
33 changes: 33 additions & 0 deletions frontend/src/components/ScenarioSuites/LaunchSuiteDialog.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { makeStyles, tokens } from '@fluentui/react-components'

export const useLaunchSuiteDialogStyles = makeStyles({
body: {
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalM,
},
summary: {
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalXS,
},
summaryText: {
color: tokens.colorNeutralForeground3,
},
entryList: {
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalXXS,
margin: 0,
paddingLeft: tokens.spacingHorizontalXL,
},
entryIdentity: {
display: 'inline-flex',
alignItems: 'center',
flexWrap: 'wrap',
gap: tokens.spacingHorizontalXS,
},
numberInput: {
maxWidth: '10rem',
},
})
Loading
Loading