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
1 change: 1 addition & 0 deletions opencti-platform/opencti-front/lang/back/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1066,5 +1066,6 @@
"XTM Hub registration User name": "XTM-Hub-Registrierung Benutzername",
"XTM Hub should send connectivity email": "XTM Hub sollte Konnektivitäts-E-Mails senden",
"XTM Hub Token": "XTM-Hub-Token",
"XTM One registration manager": "XTM One Registrierungsmanager",
"XTM1 CGU acceptance status": "XTM1 CGU-Annahmestatus"
}
1 change: 1 addition & 0 deletions opencti-platform/opencti-front/lang/back/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1066,5 +1066,6 @@
"XTM Hub registration User name": "XTM Hub registration User name",
"XTM Hub should send connectivity email": "XTM Hub should send connectivity email",
"XTM Hub Token": "XTM Hub Token",
"XTM One registration manager": "XTM One registration manager",
"XTM1 CGU acceptance status": "XTM1 CGU acceptance status"
}
1 change: 1 addition & 0 deletions opencti-platform/opencti-front/lang/back/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1066,5 +1066,6 @@
"XTM Hub registration User name": "XTM Hub registro Nombre de usuario",
"XTM Hub should send connectivity email": "El eje de XTM debe enviar el email de la conectividad",
"XTM Hub Token": "Token del concentrador XTM",
"XTM One registration manager": "Gestor de inscripciones XTM One",
"XTM1 CGU acceptance status": "XTM1 Estado de aceptación de la UGC"
}
1 change: 1 addition & 0 deletions opencti-platform/opencti-front/lang/back/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1066,5 +1066,6 @@
"XTM Hub registration User name": "Nom de l'utilisateur pour l'enregistrement du XTM Hub",
"XTM Hub should send connectivity email": "XTM Hub doit envoyer un e-mail de connectivité",
"XTM Hub Token": "Token XTM Hub",
"XTM One registration manager": "Gestionnaire d'enregistrement XTM One",
"XTM1 CGU acceptance status": "XTM1 Statut d'acceptation des CGU"
}
1 change: 1 addition & 0 deletions opencti-platform/opencti-front/lang/back/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -1066,5 +1066,6 @@
"XTM Hub registration User name": "Registrazione XTM Hub Nome utente",
"XTM Hub should send connectivity email": "XTM Hub dovrebbe inviare un'e-mail di connettività",
"XTM Hub Token": "Token XTM Hub",
"XTM One registration manager": "Gestore delle registrazioni XTM One",
"XTM1 CGU acceptance status": "XTM1 Stato di accettazione della CGU"
}
1 change: 1 addition & 0 deletions opencti-platform/opencti-front/lang/back/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1066,5 +1066,6 @@
"XTM Hub registration User name": "XTMハブ登録ユーザー名",
"XTM Hub should send connectivity email": "XTM Hubは接続性の電子メールを送信する必要があります。",
"XTM Hub Token": "XTM ハブ・トークン",
"XTM One registration manager": "XTM One登録マネージャー",
"XTM1 CGU acceptance status": "XTM1 CGU受入状況"
}
1 change: 1 addition & 0 deletions opencti-platform/opencti-front/lang/back/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -1066,5 +1066,6 @@
"XTM Hub registration User name": "XTM Hub 등록 사용자 이름",
"XTM Hub should send connectivity email": "XTM 허브가 연결 이메일을 보내야 합니다",
"XTM Hub Token": "XTM 허브 토큰",
"XTM One registration manager": "XTM One 등록 관리자",
"XTM1 CGU acceptance status": "XTM1 CGU 승인 상태"
}
1 change: 1 addition & 0 deletions opencti-platform/opencti-front/lang/back/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1066,5 +1066,6 @@
"XTM Hub registration User name": "Регистрация концентратора XTM Имя пользователя",
"XTM Hub should send connectivity email": "Концентратор XTM должен отправить электронное сообщение о возможности подключения",
"XTM Hub Token": "Токен концентратора XTM",
"XTM One registration manager": "Менеджер регистрации XTM One",
"XTM1 CGU acceptance status": "Статус принятия XTM1 CGU"
}
1 change: 1 addition & 0 deletions opencti-platform/opencti-front/lang/back/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1066,5 +1066,6 @@
"XTM Hub registration User name": "XTM Hub 注册用户名",
"XTM Hub should send connectivity email": "XTM 中枢应发送连接电子邮件",
"XTM Hub Token": "XTM 中枢令牌",
"XTM One registration manager": "XTM One 注册管理器",
"XTM1 CGU acceptance status": "XTM1 CGU 验收状态"
}
3 changes: 2 additions & 1 deletion opencti-platform/opencti-graphql/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@
"timeout": 60000
},
"xtm": {
"xtm_one_url": "https://common.xtm1.filigran.io",
"xtm_one_url": "",
"xtm_one_token": "",
"openaev_url": "",
"openaev_token": "",
"openaev_reject_unauthorized": false,
Expand Down
10 changes: 9 additions & 1 deletion opencti-platform/opencti-graphql/src/domain/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -1930,7 +1930,15 @@ export const authenticateUserFromRequest = async (context, req) => {
try {
return await authenticateUserByToken(context, req, bearerToken);
} catch (err) {
logApp.warn('Error resolving user by token', { cause: err });
const tokenPrefix = bearerToken.substring(0, 20);
const userAgent = req.headers['user-agent'] || 'unknown';
const origin = req.headers.origin || req.headers.referer || 'unknown';
logApp.warn('Error resolving user by token', {
cause: err,
token_prefix: `${tokenPrefix}...`,
user_agent: userAgent,
origin,
});
return undefined;
}
}
Expand Down
1 change: 1 addition & 0 deletions opencti-platform/opencti-graphql/src/manager/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import './indicatorDecayManager';
import './garbageCollectionManager';
import './hubRegistrationManager';
import './xtmOneRegistrationManager';
import './telemetryManager';
import './retentionManager';
import './exclusionListCacheBuildManager';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import conf, { logApp } from '../config/conf';
import { executionContext, SYSTEM_USER } from '../utils/access';
import { registerWithXtmOne } from '../modules/xtm/one/xtm-one';
import type { ManagerDefinition } from './managerModule';
import { registerManager } from './managerModule';

const XTM_ONE_URL = conf.get('xtm:xtm_one_url');
const XTM_ONE_TOKEN = conf.get('xtm:xtm_one_token');
// Both URL *and* token are required. xtm_one_url alone is used by the
// chatbot proxy (httpChatbotProxy) and does NOT mean the registration
// manager should run — the token is the signal that XTM One registration
// is intentionally configured.
const XTM_ONE_ENABLED = !!(XTM_ONE_URL && XTM_ONE_TOKEN);
const SCHEDULE_TIME = 5 * 60 * 1000; // 5 minutes
const BOOT_DELAY = 30_000; // 30 seconds — let the platform finish init

export const xtmOneRegistrationManager = async () => {
const context = executionContext('xtm_one_registration_manager');
try {
await registerWithXtmOne(context, SYSTEM_USER);
} catch (error: any) {
logApp.error('[XTM One] Registration manager error', { error: error.message });
}
};

const XTM_ONE_REGISTRATION_MANAGER_DEFINITION: ManagerDefinition = {
id: 'XTM_ONE_REGISTRATION_MANAGER',
label: 'XTM One registration manager',
executionContext: 'xtm_one_registration_manager',
cronSchedulerHandler: {
handler: xtmOneRegistrationManager,
interval: SCHEDULE_TIME,
lockKey: 'xtm_one_registration_manager_lock',
},
enabledByConfig: XTM_ONE_ENABLED,
enabledToStart(): boolean {
return this.enabledByConfig;
},
enabled(): boolean {
return this.enabledByConfig;
},
warning(): boolean {
return false;
},
};

registerManager(XTM_ONE_REGISTRATION_MANAGER_DEFINITION);

// Fire once at boot so the platform appears in XTM One immediately
// instead of waiting for the first interval tick.
if (XTM_ONE_ENABLED) {
setTimeout(() => {
xtmOneRegistrationManager().catch((err: any) => {
logApp.warn('[XTM One] Boot registration failed, will retry on next tick', { error: err.message });
});
}, BOOT_DELAY);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import axios from 'axios';
import conf, { logApp } from '../../../config/conf';

const XTM_ONE_URL = conf.get('xtm:xtm_one_url');
const XTM_ONE_TOKEN = conf.get('xtm:xtm_one_token');

export interface XtmOneUserEntry {
email: string;
display_name: string;
api_key: string;
}

export interface XtmOneRegistrationInput {
platform_identifier: string;
platform_url: string;
platform_title: string;
platform_version: string;
platform_id: string;
enterprise_license_pem: string | undefined;
license_type: string | undefined;
admin_api_key: string;
users: XtmOneUserEntry[];
}

export interface XtmOneRegistrationResponse {
status: string;
platform_identifier: string;
ee_enabled: boolean;
user_integrations: number;
chat_web_token: string | null;
}

const xtmOneClient = {
isConfigured: (): boolean => {
return !!(XTM_ONE_URL && XTM_ONE_TOKEN);
},

register: async (input: XtmOneRegistrationInput): Promise<XtmOneRegistrationResponse | null> => {
if (!XTM_ONE_URL || !XTM_ONE_TOKEN) {
return null;
}
try {
const url = `${XTM_ONE_URL}/api/v1/platform/register`;
const response = await axios.post(url, input, {
headers: {
Authorization: `Bearer ${XTM_ONE_TOKEN}`,
'Content-Type': 'application/json',
},
timeout: 15000,
});
return response.data;
} catch (error: any) {
logApp.error('[XTM One] Registration failed', { error: error.message });
return null;
}
},
};

export default xtmOneClient;
113 changes: 113 additions & 0 deletions opencti-platform/opencti-graphql/src/modules/xtm/one/xtm-one.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import conf, { PLATFORM_VERSION, logApp } from '../../../config/conf';
import type { AuthContext, AuthUser } from '../../../types/user';
import type { BasicStoreSettings } from '../../../types/settings';
import { getEntityFromCache, getEntitiesListFromCache } from '../../../database/cache';
import { internalLoadById } from '../../../database/middleware-loader';
import { ENTITY_TYPE_SETTINGS, ENTITY_TYPE_USER } from '../../../schema/internalObject';
import { TokenDuration } from '../../../generated/graphql';
import { decodeLicensePem, getEnterpriseEditionActivePem } from '../../settings/licensing';
import { addUserTokenByAdmin } from '../../user/user-domain';
import xtmOneClient from './xtm-one-client';
import type { XtmOneUserEntry } from './xtm-one-client';

const XTM_ONE_TOKEN_NAME = 'XTM One';

let discoveredChatWebToken: string | null = null;

export const getDiscoveredChatWebToken = (): string | null => discoveredChatWebToken;

/**
* Register this OpenCTI instance with XTM One.
*
* Called on every tick by the XTM One registration manager. The /register
* endpoint is an upsert so repeated calls are safe and serve as both
* initial registration and periodic heartbeat.
*
* Every user with an email is sent on every tick so that XTM One can
* create/update them. An "XTM One" API token is provisioned once per
* user; the plaintext key is only available at creation time, so
* subsequent pings send the user with an empty api_key (XTM One keeps
* the previously stored credentials).
*/
export const registerWithXtmOne = async (context: AuthContext, user: AuthUser): Promise<void> => {
if (!xtmOneClient.isConfigured()) {
return;
}

const settings = await getEntityFromCache<BasicStoreSettings>(context, user, ENTITY_TYPE_SETTINGS);
if (!settings) {
logApp.warn('[XTM One] Cannot register: settings not available');
return;
}

const { pem } = getEnterpriseEditionActivePem(settings);

let licenseType: string | undefined;
try {
const licenseInfo = decodeLicensePem(settings);
if (licenseInfo.license_validated && licenseInfo.license_type) {
licenseType = licenseInfo.license_type;
}
} catch {
// license info not available — CE or invalid PEM
}

const users: XtmOneUserEntry[] = [];
try {
const allUsers = await getEntitiesListFromCache<AuthUser>(context, user, ENTITY_TYPE_USER);
for (const u of allUsers) {
if (!u.user_email) continue;
try {
const freshUser = await internalLoadById(context, user, u.id) as unknown as AuthUser;
if (!freshUser) continue;
const existingTokens = (freshUser as any).api_tokens ?? [];
const hasXtmOneToken = existingTokens.some((t: any) => t.name === XTM_ONE_TOKEN_NAME);
let apiKey = '';
if (!hasXtmOneToken) {
const newToken = await addUserTokenByAdmin(context, user, u.id, { name: XTM_ONE_TOKEN_NAME, duration: TokenDuration.Unlimited });
apiKey = newToken.plaintext_token;
}
// Always include the user so XTM One can create/update them on
// every ping. api_key is only available at token-creation time;
// XTM One treats an empty key as "keep existing credentials".
users.push({
email: u.user_email,
display_name: u.name || u.user_email,
api_key: apiKey,
});
} catch (tokenErr: any) {
logApp.warn('[XTM One] Failed to process token for user', { email: u.user_email, error: tokenErr.message });
}
}
} catch (err: any) {
logApp.warn('[XTM One] Failed to collect users', { error: err.message });
}

const adminToken = conf.get('app:admin:token') || '';

const result = await xtmOneClient.register({
platform_identifier: 'opencti',
platform_url: settings.platform_url || '',
platform_title: settings.platform_title || 'OpenCTI Platform',
platform_version: PLATFORM_VERSION,
platform_id: settings.internal_id || settings.id,
enterprise_license_pem: pem,
license_type: licenseType,
admin_api_key: adminToken,
users,
});

if (result) {
if (result.chat_web_token) {
discoveredChatWebToken = result.chat_web_token;
logApp.info('[XTM One] Chat web token discovered from registration');
}
logApp.info('[XTM One] Registration successful', {
status: result.status,
ee_enabled: result.ee_enabled,
user_integrations: result.user_integrations,
});
} else {
logApp.warn('[XTM One] Registration failed, will retry on next tick');
}
};
Loading