Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
16 changes: 15 additions & 1 deletion doc/gui/0_gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,21 @@ The Configuration view manages the targets available for attacks.

#### Target Table

Lists all registered targets with their type, endpoint, and model name. Click "Set Active" to select a target for use in the Chat view. The active target is highlighted with an "Active" badge.
Lists all registered targets with their type, endpoint, and model name. Click "Set Active" to select a target for use in the Chat view. The active target is highlighted with an "Active" badge. The **Validate** column (with a beaker icon button) lets you probe a target's live capabilities and see a declared-vs-observed diff (see [Validating Targets](#validating-targets) below).

#### Validating Targets

The **Validate** column in the target table has a beaker icon button on every top-level row that runs PyRIT's `discover_target_capabilities_async` engine against the selected target and opens a modal showing declared-vs-observed capability flags and input modalities. The button is placed next to the capability columns (Inputs, Outputs, Multi-turn, …) so it sits with the data it inspects. Use this when you want to confirm that a target actually accepts the request shapes its class declares (for example, when an Azure OpenAI gateway strips a feature, or when a multimodal class is pointed at a text-only deployment) before launching a long attack run.

The dialog:

- Sends real requests to the target — this may incur cost and produce side effects (logs, billing, content-policy hits). Test prompts are written to memory tagged `capability_probe`.
- Caps per-probe timeout at 5 seconds for GUI responsiveness.
- Reports output modalities as declared (those are not actively probed) and renders an amber em-dash for them.
- Reports declared input-modality combinations the engine has no packaged test asset for (e.g., `function_call`, `tool_call`, `reasoning`, `url`) in a separate "Not probed (no asset)" row rather than as false red mismatches.
- Should NOT be run while an attack or scenario is actively using the same target — validation temporarily changes the target's runtime configuration during probing.

Only top-level registered targets have a Validate button; inner targets of composite wrappers (e.g., `RoundRobinTarget` children) are reachable only through the wrapper.

#### Creating Targets

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/Config/TargetTable.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export const useTargetTableStyles = makeStyles({
width: '160px',
textAlign: 'center',
},
validateCell: {
width: '90px',
textAlign: 'center',
},
modalityRow: {
display: 'inline-flex',
alignItems: 'center',
Expand Down
104 changes: 103 additions & 1 deletion frontend/src/components/Config/TargetTable.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { FluentProvider, webLightTheme } from '@fluentui/react-components'
import TargetTable from './TargetTable'
import type { TargetInstance } from '../../types'
import { targetsApi } from '@/services/api'

jest.mock('./TargetTable.styles', () => ({
useTargetTableStyles: () => new Proxy({}, { get: () => '' }),
}))

jest.mock('@/services/api', () => ({
targetsApi: {
validateCapabilities: jest.fn(),
},
}))

const mockedApi = targetsApi as jest.Mocked<typeof targetsApi>

const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<FluentProvider theme={webLightTheme}>{children}</FluentProvider>
)
Expand Down Expand Up @@ -397,4 +406,97 @@ describe('TargetTable', () => {

expect(screen.queryByLabelText('Expand inner targets')).not.toBeInTheDocument()
})

// --- F5: Validate button wiring ---

it('renders a Validate button on every top-level row', () => {
render(
<TestWrapper>
<TargetTable {...defaultProps} />
</TestWrapper>,
)
const validateButtons = screen.getAllByRole('button', { name: /^Validate capabilities for / })
// 3 sample targets, 1 button each (no active target → no extra active-row button)
expect(validateButtons).toHaveLength(3)
})

it('also renders a Validate button on the active-target summary row', () => {
render(
<TestWrapper>
<TargetTable {...defaultProps} activeTarget={sampleTargets[0]} />
</TestWrapper>,
)
const validateButtons = screen.getAllByRole('button', { name: /^Validate capabilities for / })
// 3 list rows + 1 active-row summary = 4
expect(validateButtons).toHaveLength(4)
})

it('does NOT render Validate buttons on inner-target rows (composite expansion)', () => {
const rrTarget: TargetInstance = {
target_registry_name: 'rr_gpt4o',
target_type: 'RoundRobinTarget',
model_name: 'gpt-4o',
target_specific_params: { weights: [1, 1] },
inner_targets: [
{
target_registry_name: 'inner_a',
target_type: 'OpenAIChatTarget',
endpoint: 'https://a.openai.azure.com',
model_name: 'gpt-4o',
},
{
target_registry_name: 'inner_b',
target_type: 'OpenAIChatTarget',
endpoint: 'https://b.openai.azure.com',
model_name: 'gpt-4o',
},
],
}
render(
<TestWrapper>
<TargetTable {...defaultProps} targets={[rrTarget]} />
</TestWrapper>,
)
// Before expanding: 1 top-level row → 1 Validate button
expect(screen.getAllByRole('button', { name: /^Validate capabilities for / })).toHaveLength(1)
// Expand
fireEvent.click(screen.getByLabelText('Expand inner targets'))
expect(screen.getByText('https://a.openai.azure.com')).toBeInTheDocument()
// After expanding: still only 1 Validate button (inner rows don't get one)
expect(screen.getAllByRole('button', { name: /^Validate capabilities for / })).toHaveLength(1)
})

it('opens the validation dialog when a Validate button is clicked', async () => {
// Pending promise so the dialog stays in the loading state we can detect.
mockedApi.validateCapabilities.mockReturnValue(new Promise(() => {}))
render(
<TestWrapper>
<TargetTable {...defaultProps} />
</TestWrapper>,
)
const validateButtons = screen.getAllByRole('button', { name: /^Validate capabilities for / })
fireEvent.click(validateButtons[0])
await waitFor(() => {
expect(mockedApi.validateCapabilities).toHaveBeenCalledWith('openai_chat_gpt4')
})
expect(screen.getByText(/Validate capabilities: openai_chat_gpt4/i)).toBeInTheDocument()
})

it('disables the Validate button for the row whose dialog is currently open', async () => {
mockedApi.validateCapabilities.mockReturnValue(new Promise(() => {}))
render(
<TestWrapper>
<TargetTable {...defaultProps} />
</TestWrapper>,
)
const validateButtons = screen.getAllByRole('button', { name: /^Validate capabilities for / })
fireEvent.click(validateButtons[0])
await waitFor(() => {
// The first row's Validate button is now disabled.
const stillButtons = screen.getAllByRole('button', { name: /^Validate capabilities for / })
expect(stillButtons[0]).toBeDisabled()
// The other rows' buttons remain enabled.
expect(stillButtons[1]).not.toBeDisabled()
})
})
})
42 changes: 42 additions & 0 deletions frontend/src/components/Config/TargetTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import {
ArrowHookUpLeftRegular,
ChevronRightRegular,
ChevronDownRegular,
BeakerRegular,
} from '@fluentui/react-icons'
import type { TargetInstance } from '../../types'
import { useTargetTableStyles } from './TargetTable.styles'
import ValidateCapabilitiesDialog from './ValidateCapabilitiesDialog'

interface TargetTableProps {
targets: TargetInstance[]
Expand Down Expand Up @@ -72,6 +74,7 @@ const COLUMN_TOOLTIPS = {
parameters: 'Target-specific configuration parameters (e.g., reasoning_effort, max_output_tokens)',
inputs: 'Modalities the target accepts as input',
outputs: 'Modalities the target can produce as output',
validate: 'Probe the target live and compare observed capabilities to the declared values shown in this row',
} as const

/** Composite icon: f(x) with a small return-arrow badge for function call outputs. */
Expand Down Expand Up @@ -244,6 +247,10 @@ export default function TargetTable({ targets, activeTarget, onSetActiveTarget }
// We use a Set of target_registry_name strings — when a name is in the set,
// that row's sub-rows are visible.
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())
// The target whose Validate dialog is currently open, or null.
// Inner-target rows (composite expansion) do NOT get a Validate button —
// they aren't registered by name in the backend TargetRegistry.
const [validateTarget, setValidateTarget] = useState<TargetInstance | null>(null)

const toggleExpanded = (registryName: string) => {
setExpandedRows((prev) => {
Expand Down Expand Up @@ -310,6 +317,18 @@ export default function TargetTable({ targets, activeTarget, onSetActiveTarget }
<TableCell className={styles.modalityCell}>
<ModalityCell modalities={activeTarget.capabilities?.supported_output_modalities} />
</TableCell>
<TableCell className={styles.validateCell}>
<Tooltip content={COLUMN_TOOLTIPS.validate} relationship="label">
<Button
appearance="secondary"
size="small"
icon={<BeakerRegular />}
aria-label={`Validate capabilities for ${activeTarget.target_registry_name}`}
disabled={validateTarget?.target_registry_name === activeTarget.target_registry_name}
onClick={() => setValidateTarget(activeTarget)}
/>
</Tooltip>
</TableCell>
<CapabilityCells target={activeTarget} />
<TableCell style={{ width: '160px' }}>
<Text size={200} className={styles.paramsCell}>
Expand Down Expand Up @@ -374,6 +393,11 @@ export default function TargetTable({ targets, activeTarget, onSetActiveTarget }
<span className={styles.helpHeader}>Outputs</span>
</Tooltip>
</TableHeaderCell>
<TableHeaderCell className={styles.validateCell}>
<Tooltip content={COLUMN_TOOLTIPS.validate} relationship="description">
<span className={styles.helpHeader}>Validate</span>
</Tooltip>
</TableHeaderCell>
{CAPABILITY_COLUMNS.map(({ key, label, tooltip }) => (
<TableHeaderCell key={key} className={styles.capabilityCell}>
<Tooltip content={tooltip} relationship="description">
Expand Down Expand Up @@ -444,6 +468,18 @@ export default function TargetTable({ targets, activeTarget, onSetActiveTarget }
<TableCell className={styles.modalityCell}>
<ModalityCell modalities={target.capabilities?.supported_output_modalities} />
</TableCell>
<TableCell className={styles.validateCell}>
<Tooltip content={COLUMN_TOOLTIPS.validate} relationship="label">
<Button
appearance="secondary"
size="small"
icon={<BeakerRegular />}
aria-label={`Validate capabilities for ${target.target_registry_name}`}
disabled={validateTarget?.target_registry_name === target.target_registry_name}
onClick={() => setValidateTarget(target)}
/>
</Tooltip>
</TableCell>
<CapabilityCells target={target} />
<TableCell>
<Text size={200} className={styles.paramsCell}>
Expand All @@ -465,6 +501,12 @@ export default function TargetTable({ targets, activeTarget, onSetActiveTarget }
})}
</TableBody>
</Table>

<ValidateCapabilitiesDialog
open={validateTarget != null}
target={validateTarget}
onClose={() => setValidateTarget(null)}
/>
</div>
)
}
Loading
Loading