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
2 changes: 1 addition & 1 deletion openaev-front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,4 @@
"html-to-image": "patch:html-to-image@1.11.13#./patches/html-to-image.patch",
"react-apexcharts": "patch:react-apexcharts@1.7.0#./patches/react-apexcharts.patch"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { cleanup, render, screen } from '@testing-library/react';
import { type ReactNode } from 'react';
import { IntlProvider } from 'react-intl';
import { afterEach, describe, expect, it, vi } from 'vitest';

import XtmOneMcpAccess from '../../../../admin/components/profile/XtmOneMcpAccess';
import { type PlatformSettings, type User } from '../../../../utils/api-types';
import { UserContext, type UserContextType } from '../../../../utils/hooks/useAuth';

const theme = createTheme();

const TITLE = 'XTM One MCP server';
const MANAGE_LABEL = 'Manage in XTM One';

const DEFAULT_SETTINGS: Partial<PlatformSettings> = {
platform_xtm_one_configured: true,
platform_xtm_one_url: 'https://xtmone.example.com',
};

const renderCard = (settingsOverrides: Partial<PlatformSettings> = {}) => {
const userContext: UserContextType = {
me: { user_id: 'user-1' } as User,
settings: {
...DEFAULT_SETTINGS,
...settingsOverrides,
} as PlatformSettings,
isXTMHubAccessible: true,
userTenants: [],
currentUserTenant: null,
switchUserTenant: vi.fn(),
reloadUserTenants: vi.fn(),
};

const wrapper = ({ children }: { children: ReactNode }) => (
<ThemeProvider theme={theme}>
<IntlProvider locale="en" defaultLocale="en" onError={() => {}}>
<UserContext.Provider value={userContext}>{children}</UserContext.Provider>
</IntlProvider>
</ThemeProvider>
);

return render(<XtmOneMcpAccess />, { wrapper });
};

describe('XtmOneMcpAccess', () => {
afterEach(() => {
cleanup();
vi.clearAllMocks();
});

describe('Visibility', () => {
it('renders the card when XTM One is configured', () => {
renderCard();
expect(screen.getByText(TITLE)).toBeDefined();
});

it('renders nothing when XTM One is not configured', () => {
const { container } = renderCard({ platform_xtm_one_configured: false });
expect(container.firstChild).toBeNull();
});

it('renders nothing when the XTM One URL is missing', () => {
const { container } = renderCard({ platform_xtm_one_url: undefined });
expect(container.firstChild).toBeNull();
});

it('renders nothing when the XTM One URL is not an http(s) URL', () => {
const { container } = renderCard({ platform_xtm_one_url: 'javascript:alert(1)' });
expect(container.firstChild).toBeNull();
});
});

describe('MCP endpoint', () => {
it('displays the MCP endpoint derived from the XTM One URL', () => {
renderCard();
expect(screen.getByText('https://xtmone.example.com/mcp/openaev')).toBeDefined();
});

it('normalizes trailing slashes in the configured URL', () => {
renderCard({ platform_xtm_one_url: 'https://xtmone.example.com///' });
expect(screen.getByText('https://xtmone.example.com/mcp/openaev')).toBeDefined();
});
});

describe('Manage in XTM One link', () => {
it('points to the XTM One profile MCP page, opening in a new tab safely', () => {
renderCard();
const link = screen.getByText(MANAGE_LABEL).closest('a');
expect(link).not.toBeNull();
expect(link?.getAttribute('href')).toBe('https://xtmone.example.com/profile/mcp');
expect(link?.getAttribute('target')).toBe('_blank');
expect(link?.getAttribute('rel')).toBe('noopener noreferrer');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,17 @@ import { useTheme } from '@mui/material/styles';

import { useFormatter } from '../../../components/i18n';
import useAuth from '../../../utils/hooks/useAuth';
import { toHttpUrl } from '../../../utils/url-helper';

/**
* Top-bar shortcut to the XTM One CTEM Command Center (the cross-product exposure
* posture dashboard / XTM One home). Opens the XTM One URL in a new tab.
*
* Shown only when XTM One is connected properly (`platform_xtm_one_configured`
* with `platform_xtm_one_url` set) and the agentic AI is not disabled. NOT
* Enterprise-gated: the CTEM Command Center is also available in full CE
* (metrics only).
* with `platform_xtm_one_url` set, guarded by the shared http(s)-only helper)
* and the agentic AI is not disabled. NOT Enterprise-gated: the CTEM Command
* Center is also available in full CE (metrics only).
*/
/**
* Returns the value only when it is a syntactically valid http(s) URL,
* otherwise undefined. Guards against a misconfigured (or otherwise
* unexpected) `platform_xtm_one_url` - e.g. a `javascript:` scheme - ever
* reaching the anchor href.
*/
const toHttpUrl = (value: string | undefined): string | undefined => {
if (!value) {
return undefined;
}
try {
const { protocol } = new URL(value);
return protocol === 'http:' || protocol === 'https:' ? value : undefined;
} catch {
return undefined;
}
};

const CtemCommandCenterButton = () => {
const theme = useTheme();
const { t } = useFormatter();
Expand Down
2 changes: 2 additions & 0 deletions openaev-front/src/admin/components/profile/Index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { countryOption } from '../../../utils/Option';
import PasswordForm from './PasswordForm';
import ProfileForm from './ProfileForm';
import UserForm from './UserForm';
import XtmOneMcpAccess from './XtmOneMcpAccess';

const Index = () => {
const { t } = useFormatter();
Expand Down Expand Up @@ -148,6 +149,7 @@ const Index = () => {
{t('API specifications')}
</Button>
</Paper>
<XtmOneMcpAccess />
</div>
);
};
Expand Down
87 changes: 87 additions & 0 deletions openaev-front/src/admin/components/profile/XtmOneMcpAccess.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ContentCopyOutlined, OpenInNewOutlined } from '@mui/icons-material';
import { Button, IconButton, Tooltip, Typography } from '@mui/material';

import Paper from '../../../components/common/Paper';
import { useFormatter } from '../../../components/i18n';
import useAuth from '../../../utils/hooks/useAuth';
import { toHttpUrl } from '../../../utils/url-helper';
import { copyToClipboard } from '../../../utils/utils';

/**
* "XTM One MCP server" profile card - shown only when the platform is
* connected to XTM One (`platform_xtm_one_configured` + `platform_xtm_one_url`,
* the same gate as the top-bar CTEM Command Center button).
*
* XTM One natively embeds an MCP (Model Context Protocol) server for every
* registered platform: AI clients (Cursor, Claude Desktop, custom agents)
* connect to `{platform_xtm_one_url}/mcp/openaev` with a personal XTM One
* API key and work with OpenAEV content under the caller's own identity.
* This card makes that endpoint discoverable from the user's profile, next
* to the classic API access card.
*/
const XtmOneMcpAccess = () => {
const { t } = useFormatter();
const { settings } = useAuth();

const xtmOneUrl = toHttpUrl(settings.platform_xtm_one_url)?.replace(/\/+$/, '');
if (settings.platform_xtm_one_configured !== true || !xtmOneUrl) {
return null;
}
Comment thread
SamuelHassine marked this conversation as resolved.

const mcpEndpointUrl = `${xtmOneUrl}/mcp/openaev`;
const xtmOneProfileUrl = `${xtmOneUrl}/profile/mcp`;

return (
<Paper>
<Typography variant="h1" style={{ marginBottom: 20 }}>
{t('XTM One MCP server')}
</Typography>
<Typography variant="body1">
{t('This platform is connected to XTM One, which natively exposes an MCP (Model Context Protocol) server for OpenAEV. AI clients such as Cursor or Claude Desktop can work with scenarios, simulations, payloads and findings with your own permissions.')}
</Typography>
<Typography variant="h4" gutterBottom style={{ marginTop: 20 }}>
{t('MCP endpoint URL')}
</Typography>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<pre style={{
flex: 1,
margin: 0,
}}
>
{mcpEndpointUrl}
</pre>
<Tooltip title={t('Copy MCP endpoint URL')}>
<IconButton
size="small"
aria-label={t('Copy MCP endpoint URL')}
onClick={() => copyToClipboard(t, mcpEndpointUrl)}
>
<ContentCopyOutlined fontSize="small" />
</IconButton>
</Tooltip>
</div>
<Typography variant="body2" style={{ marginTop: 20 }}>
{t('Authenticate with a personal XTM One API key passed as a bearer token. Your endpoint, connection status and ready-to-copy client configuration are available in your XTM One profile.')}
</Typography>
<Button
variant="contained"
color="primary"
component="a"
href={xtmOneProfileUrl}
target="_blank"
rel="noopener noreferrer"
endIcon={<OpenInNewOutlined />}
style={{ marginTop: 20 }}
>
{t('Manage in XTM One')}
</Button>
</Paper>
);
};

export default XtmOneMcpAccess;
8 changes: 7 additions & 1 deletion openaev-front/src/utils/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@
"attack-scenario": "Angriffsszenario",
"AttackPatterns": "AttackPatterns",
"Attribute mapping configuration": "Attribut-Zuordnungskonfiguration",
"Authenticate with a personal XTM One API key passed as a bearer token. Your endpoint, connection status and ready-to-copy client configuration are available in your XTM One profile.": "Authentifizieren Sie sich mit einem persönlichen XTM One API-Schlüssel als Bearer-Token. Ihr Endpunkt, der Verbindungsstatus und die kopierfertige Client-Konfiguration finden Sie in Ihrem XTM One Profil.",
"Author": "Autor",
"Authoritative": "Maßgeblich",
"Authorization": "Autorisierung",
Expand Down Expand Up @@ -449,6 +450,7 @@
"Controls how often an attack step is executed. Useful for simulating brute-force or slow, stealthy attacks.": "Steuert, wie oft ein Angriffsschritt ausgeführt wird. Nützlich für die Simulation von Brute-Force- oder langsamen, verdeckten Angriffen.",
"Copied to clipboard": "In die Zwischenablage kopiert",
"Copy": "Kopieren",
"Copy MCP endpoint URL": "MCP-Endpunkt-URL kopieren",
"Corporate login": "Firmenlogin",
"Country": "Land",
"Cover the following TTPs": "Decken Sie die folgenden TTPs ab",
Expand Down Expand Up @@ -1043,9 +1045,9 @@
"Global score": "Globale Bewertung",
"global-crisis": "Globale Krise",
"Granted": "Gewährt",
"Graphql_api": "GraphQL API",
"Greater than": "Größer als",
"Greater than or equals": "Größer als oder gleich",
"Graphql_api": "GraphQL API",
"Group": "Gruppe",
"Groups": "Gruppen",
"GT": "GT",
Expand Down Expand Up @@ -1398,6 +1400,7 @@
"manage custom variables": "benutzerdefinierte Variablen verwalten",
"Manage grants": "Verwalten von Zuschüssen",
"Manage grants for group: {groupName}": "Zuschüsse für die Gruppe verwalten: {groupName}",
"Manage in XTM One": "In XTM One verwalten",
"Manage players": "Spieler verwalten",
"Manage roles": "Verwalten von Rollen",
"Manage roles for group: {groupName}": "Rollen für Gruppe verwalten: {groupName}",
Expand Down Expand Up @@ -1448,6 +1451,7 @@
"MAYBE_PARTIAL_PREVENTED": "Vielleicht teilweise verhindert",
"Maybe_prevented": "Vielleicht verhindert",
"MAYBE_PREVENTED": "Vielleicht verhindert",
"MCP endpoint URL": "MCP-Endpunkt-URL",
"Mcp_server": "MCP Server",
"Media pressure": "Druck der Medien",
"Media pressure read": "Mediendruck gelesen",
Expand Down Expand Up @@ -2293,6 +2297,7 @@
"This is a communication check before the beginning of the simulation. Please click on the following link in order to confirm you successfully received this message <a href=${'comcheck.url}'>${comcheck.url}</a>.": "Dies ist eine Kommunikationsprüfung vor dem Beginn der Simulation. Bitte klicken Sie auf den folgenden Link, um zu bestätigen, dass Sie diese Nachricht erhalten haben: <a href='${comcheck.url}'>${comcheck.url}</a>.",
"This page is coming soon": "Diese Seite kommt bald",
"This page is not found on this OpenAEV application.": "Diese Seite wird in dieser OpenAEV-Anwendung nicht gefunden.",
"This platform is connected to XTM One, which natively exposes an MCP (Model Context Protocol) server for OpenAEV. AI clients such as Cursor or Claude Desktop can work with scenarios, simulations, payloads and findings with your own permissions.": "Diese Plattform ist mit XTM One verbunden, das nativ einen MCP-Server (Model Context Protocol) für OpenAEV bereitstellt. KI-Clients wie Cursor oder Claude Desktop können mit Szenarien, Simulationen, Payloads und Findings mit Ihren eigenen Berechtigungen arbeiten.",
"This report is not available": "Dieser Bericht ist nicht verfügbar",
"This scenario has never run, schedule or run it now!": "Dieses Szenario ist noch nie gelaufen, planen oder starten Sie es jetzt!",
"This scenario is scheduled to run, results will appear soon.": "Dieses Szenario ist für die Ausführung geplant, die Ergebnisse werden bald erscheinen.",
Expand Down Expand Up @@ -2564,6 +2569,7 @@
"XTM One - AI Assistant": "XTM One - KI-Assistent",
"XTM One (Agentic IA)": "XTM One (Agenten-KI)",
"XTM One AI": "XTM One KI",
"XTM One MCP server": "XTM One MCP-Server",
"Year": "Jahr",
"Yes": "Ja",
"Yes, close": "Ja, schließen",
Expand Down
8 changes: 7 additions & 1 deletion openaev-front/src/utils/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@
"attack-scenario": "Attack Scenario",
"AttackPatterns": "AttackPatterns",
"Attribute mapping configuration": "Attribute mapping configuration",
"Authenticate with a personal XTM One API key passed as a bearer token. Your endpoint, connection status and ready-to-copy client configuration are available in your XTM One profile.": "Authenticate with a personal XTM One API key passed as a bearer token. Your endpoint, connection status and ready-to-copy client configuration are available in your XTM One profile.",
"Author": "Author",
"Authoritative": "Authoritative",
"Authorization": "Authorization",
Expand Down Expand Up @@ -449,6 +450,7 @@
"Controls how often an attack step is executed. Useful for simulating brute-force or slow, stealthy attacks.": "Controls how often an attack step is executed. Useful for simulating brute-force or slow, stealthy attacks.",
"Copied to clipboard": "Copied to clipboard",
"Copy": "Copy",
"Copy MCP endpoint URL": "Copy MCP endpoint URL",
"Corporate login": "Corporate login",
"Country": "Country",
"Cover the following TTPs": "Cover the following TTPs",
Expand Down Expand Up @@ -1043,9 +1045,9 @@
"Global score": "Global score",
"global-crisis": "Global Crisis",
"Granted": "Granted",
"Graphql_api": "GraphQL API",
"Greater than": "Greater than",
"Greater than or equals": "Greater than or equals",
"Graphql_api": "GraphQL API",
"Group": "Group",
"Groups": "Groups",
"GT": "GT",
Expand Down Expand Up @@ -1398,6 +1400,7 @@
"manage custom variables": "manage custom variables",
"Manage grants": "Manage grants",
"Manage grants for group: {groupName}": "Manage grants for group: {groupName}",
"Manage in XTM One": "Manage in XTM One",
"Manage players": "Manage persons",
"Manage roles": "Manage roles",
"Manage roles for group: {groupName}": "Manage roles for group: {groupName}",
Expand Down Expand Up @@ -1448,6 +1451,7 @@
"MAYBE_PARTIAL_PREVENTED": "Maybe partial prevented",
"Maybe_prevented": "Maybe prevented",
"MAYBE_PREVENTED": "Maybe prevented",
"MCP endpoint URL": "MCP endpoint URL",
"Mcp_server": "MCP Server",
"Media pressure": "Media pressure",
"Media pressure read": "Media pressure read",
Expand Down Expand Up @@ -2293,6 +2297,7 @@
"This is a communication check before the beginning of the simulation. Please click on the following link in order to confirm you successfully received this message <a href=${'comcheck.url}'>${comcheck.url}</a>.": "This is a communication check before the beginning of the simulation. Please click on the following link in order to confirm you successfully received this message: <a href='${comcheck.url}'>${comcheck.url}</a>.",
"This page is coming soon": "This page is coming soon",
"This page is not found on this OpenAEV application.": "This page is not found on this OpenAEV application.",
"This platform is connected to XTM One, which natively exposes an MCP (Model Context Protocol) server for OpenAEV. AI clients such as Cursor or Claude Desktop can work with scenarios, simulations, payloads and findings with your own permissions.": "This platform is connected to XTM One, which natively exposes an MCP (Model Context Protocol) server for OpenAEV. AI clients such as Cursor or Claude Desktop can work with scenarios, simulations, payloads and findings with your own permissions.",
"This report is not available": "This report is not available",
"This scenario has never run, schedule or run it now!": "This scenario has never run, schedule or run it now!",
"This scenario is scheduled to run, results will appear soon.": "This scenario is scheduled to run, results will appear soon.",
Expand Down Expand Up @@ -2564,6 +2569,7 @@
"XTM One - AI Assistant": "XTM One - AI Assistant",
"XTM One (Agentic IA)": "XTM One (Agentic IA)",
"XTM One AI": "XTM One AI",
"XTM One MCP server": "XTM One MCP server",
"Year": "Year",
"Yes": "Yes",
"Yes, close": "Yes, close",
Expand Down
Loading
Loading