diff --git a/opencti-platform/opencti-front/lang/front/de.json b/opencti-platform/opencti-front/lang/front/de.json index 2225d5f7087f..4ecd15f1a08d 100644 --- a/opencti-platform/opencti-front/lang/front/de.json +++ b/opencti-platform/opencti-front/lang/front/de.json @@ -541,6 +541,7 @@ "Create observables from this indicator": "Erstellen von Observablen aus diesem Indikator", "Create relations in bulk": "Beziehungen in Massen erstellen", "Create relations in bulk for": "Erstellen von Relationen im Bulk für", + "Create Relationship": "Beziehung erstellen", "created": "Erstellt", "Created entities": "Erstellte Entitäten", "Created the": "Erstellt die", @@ -1822,6 +1823,7 @@ "No restrictions": "Keine Einschränkungen", "No task": "Keine Aufgabe", "No tasks has been found.": "Es wurden keine Aufgaben gefunden.", + "No valid target entities": "Keine gültigen Zieleinheiten", "No template found for this name": "Keine Vorlage für diesen Namen gefunden", "No value": "Kein Wert", "No widget in this workspace": "Kein Widget in diesem Arbeitsbereich", diff --git a/opencti-platform/opencti-front/lang/front/en.json b/opencti-platform/opencti-front/lang/front/en.json index 786faf82ecaa..a15d11364900 100644 --- a/opencti-platform/opencti-front/lang/front/en.json +++ b/opencti-platform/opencti-front/lang/front/en.json @@ -541,6 +541,7 @@ "Create observables from this indicator": "Create observables from this indicator", "Create relations in bulk": "Create relations in bulk", "Create relations in bulk for": "Create relations in bulk for", + "Create Relationship": "Create Relationship", "created": "Created", "Created entities": "Created entities", "Created the": "Created the", @@ -1822,6 +1823,7 @@ "No restrictions": "No restrictions", "No task": "No task", "No tasks has been found.": "No tasks has been found.", + "No valid target entities": "No valid target entities", "No template found for this name": "No template found for this name", "No value": "No value", "No widget in this workspace": "No widget in this workspace", diff --git a/opencti-platform/opencti-front/lang/front/es.json b/opencti-platform/opencti-front/lang/front/es.json index 83a57fd02ec0..b5e980203afb 100644 --- a/opencti-platform/opencti-front/lang/front/es.json +++ b/opencti-platform/opencti-front/lang/front/es.json @@ -541,6 +541,7 @@ "Create observables from this indicator": "Crear observables a partir de este indicador", "Create relations in bulk": "Crear relaciones en bloque", "Create relations in bulk for": "Crear relaciones en bloque para", + "Create Relationship": "Nueva relación", "created": "Creado", "Created entities": "Entidades creadas", "Created the": "Creado el", @@ -1822,6 +1823,7 @@ "No restrictions": "Sin restricciones", "No task": "Ninguna tarea", "No tasks has been found.": "No se han encontrado tareas.", + "No valid target entities": "No hay entidades de destino válidas", "No template found for this name": "No se ha encontrado ninguna plantilla para este nombre", "No value": "Sin valor", "No widget in this workspace": "Sin elementos gráficos en este espacio de trabajo", diff --git a/opencti-platform/opencti-front/lang/front/fr.json b/opencti-platform/opencti-front/lang/front/fr.json index 1ec8f255eb61..94e62970667b 100644 --- a/opencti-platform/opencti-front/lang/front/fr.json +++ b/opencti-platform/opencti-front/lang/front/fr.json @@ -541,6 +541,7 @@ "Create observables from this indicator": "Créer des observables à partir de cet indicateur", "Create relations in bulk": "Créer des relations en masse", "Create relations in bulk for": "Créer des relations en masse pour", + "Create Relationship": "Créer une relation", "created": "Créé", "Created entities": "Entités créées", "Created the": "Créé le", @@ -1822,6 +1823,7 @@ "No restrictions": "Aucune restriction", "No task": "Aucune tâche", "No tasks has been found.": "Aucune tâche n'a été trouvée.", + "No valid target entities": "Aucune entité cible valide", "No template found for this name": "Aucun modèle n'a été trouvé pour ce nom", "No value": "Aucune valeur", "No widget in this workspace": "Aucun widget dans cet espace de travail", diff --git a/opencti-platform/opencti-front/lang/front/ja.json b/opencti-platform/opencti-front/lang/front/ja.json index a3573a5810bf..3e57e5adfee4 100644 --- a/opencti-platform/opencti-front/lang/front/ja.json +++ b/opencti-platform/opencti-front/lang/front/ja.json @@ -541,6 +541,7 @@ "Create observables from this indicator": "このインジケータから観測値を作成する", "Create relations in bulk": "リレーションの一括作成", "Create relations in bulk for": "リレーションシップの一括作成", + "Create Relationship": "リレーションシップを作成", "created": "作成日時", "Created entities": "作成されたエンティティ", "Created the": "作成日時", @@ -1822,6 +1823,7 @@ "No restrictions": "制限なし", "No task": "タスクなし", "No tasks has been found.": "タスクが見つかりませんでした。", + "No valid target entities": "有効なターゲットエンティティがありません", "No template found for this name": "この名前に対応するテンプレートは見つかりませんでした", "No value": "値なし", "No widget in this workspace": "このワークスペースにウィジェットはありません", diff --git a/opencti-platform/opencti-front/lang/front/ko.json b/opencti-platform/opencti-front/lang/front/ko.json index dad9ea307438..5bb3c82997ea 100644 --- a/opencti-platform/opencti-front/lang/front/ko.json +++ b/opencti-platform/opencti-front/lang/front/ko.json @@ -541,6 +541,7 @@ "Create observables from this indicator": "이 지표에서 관찰 가능 항목 생성", "Create relations in bulk": "대량으로 관계 만들기", "Create relations in bulk for": "다음에 대한 대량 관계 만들기", + "Create Relationship": "관계 만들기", "created": "생성됨", "Created entities": "생성된 엔터티", "Created the": "생성됨", @@ -1822,6 +1823,7 @@ "No restrictions": "제한 없음", "No task": "작업 없음", "No tasks has been found.": "작업이 발견되지 않았습니다.", + "No valid target entities": "유효한 대상 엔터티 없음", "No template found for this name": "이 이름에 대한 템플릿을 찾을 수 없습니다", "No value": "값 없음", "No widget in this workspace": "이 작업 공간에 위젯 없음", diff --git a/opencti-platform/opencti-front/lang/front/zh.json b/opencti-platform/opencti-front/lang/front/zh.json index 4c7acc683544..5784ff5bb303 100644 --- a/opencti-platform/opencti-front/lang/front/zh.json +++ b/opencti-platform/opencti-front/lang/front/zh.json @@ -541,6 +541,7 @@ "Create observables from this indicator": "从该指标创建观测值", "Create relations in bulk": "批量创建关系", "Create relations in bulk for": "批量创建关系", + "Create Relationship": "创建关系", "created": "创建", "Created entities": "创建的实体", "Created the": "已创建", @@ -1822,6 +1823,7 @@ "No restrictions": "无限制", "No task": "没有任务", "No tasks has been found.": "没有找到任务。", + "No valid target entities": "没有有效的目标实体", "No template found for this name": "此名称未找到模板", "No value": "没有价值", "No widget in this workspace": "此工作区没有控件", diff --git a/opencti-platform/opencti-front/src/private/components/analyses/malware_analyses/Root.tsx b/opencti-platform/opencti-front/src/private/components/analyses/malware_analyses/Root.tsx index 03028818acdb..a5cda6e0e5af 100644 --- a/opencti-platform/opencti-front/src/private/components/analyses/malware_analyses/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/analyses/malware_analyses/Root.tsx @@ -30,6 +30,8 @@ import { getMainRepresentative } from '../../../../utils/defaultRepresentatives' import { getCurrentTab, getPaddingRight } from '../../../../utils/utils'; import MalwareAnalysisEdition from './MalwareAnalysisEdition'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootMalwareAnalysisSubscription($id: ID!) { @@ -86,7 +88,7 @@ const RootMalwareAnalysis = () => { useSubscription(subConfig); const link = `/dashboard/analyses/malware_analyses/${malwareAnalysisId}/knowledge`; return ( - <> + { )} + RelateComponent={CreateRelationshipButtonComponent} noAliases={true} /> { return ; }} /> - + ); }; export default RootMalwareAnalysis; diff --git a/opencti-platform/opencti-front/src/private/components/arsenal/channels/Root.tsx b/opencti-platform/opencti-front/src/private/components/arsenal/channels/Root.tsx index 4d6f35f7c8b2..68828a9fd3d7 100644 --- a/opencti-platform/opencti-front/src/private/components/arsenal/channels/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/arsenal/channels/Root.tsx @@ -27,6 +27,8 @@ import useHelper from '../../../../utils/hooks/useHelper'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import ChannelEdition from './ChannelEdition'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootChannelSubscription($id: ID!) { @@ -101,7 +103,7 @@ const RootChannel = ({ queryRef, channelId }: RootChannelProps) => { const paddingRight = getPaddingRight(location.pathname, channelId, '/dashboard/arsenal/channels'); const link = `/dashboard/arsenal/channels/${channelId}/knowledge`; return ( - <> + {channel ? ( <> @@ -145,6 +147,7 @@ const RootChannel = ({ queryRef, channelId }: RootChannelProps) => { )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} /> { ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/arsenal/malwares/Root.tsx b/opencti-platform/opencti-front/src/private/components/arsenal/malwares/Root.tsx index 187e0b591540..2270c0a63259 100644 --- a/opencti-platform/opencti-front/src/private/components/arsenal/malwares/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/arsenal/malwares/Root.tsx @@ -29,6 +29,8 @@ import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import MalwareEdition from './MalwareEdition'; import EditEntityControlledDial from '../../../../components/EditEntityControlledDial'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootMalwareSubscription($id: ID!) { @@ -106,7 +108,7 @@ const RootMalware = ({ queryRef, malwareId }: RootMalwareProps) => { const paddingRight = getPaddingRight(location.pathname, malwareId, '/dashboard/arsenal/malwares'); const link = `/dashboard/arsenal/malwares/${malwareId}/knowledge`; return ( - <> + {malware ? ( <> @@ -155,6 +157,7 @@ const RootMalware = ({ queryRef, malwareId }: RootMalwareProps) => { /> )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} /> { ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/arsenal/tools/Root.tsx b/opencti-platform/opencti-front/src/private/components/arsenal/tools/Root.tsx index 8abf914e242f..62cfb2d42f56 100644 --- a/opencti-platform/opencti-front/src/private/components/arsenal/tools/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/arsenal/tools/Root.tsx @@ -27,6 +27,8 @@ import useHelper from '../../../../utils/hooks/useHelper'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import ToolEdition from './ToolEdition'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootToolSubscription($id: ID!) { @@ -103,7 +105,7 @@ const RootTool = ({ queryRef, toolId }: RootToolProps) => { const paddingRight = getPaddingRight(location.pathname, toolId, '/dashboard/arsenal/tools'); const link = `/dashboard/arsenal/tools/${toolId}/knowledge`; return ( - <> + {tool ? ( <> @@ -146,6 +148,7 @@ const RootTool = ({ queryRef, toolId }: RootToolProps) => { )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} /> { ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/arsenal/vulnerabilities/Root.tsx b/opencti-platform/opencti-front/src/private/components/arsenal/vulnerabilities/Root.tsx index a2bcb8a3b8de..cb3ad0a65a19 100644 --- a/opencti-platform/opencti-front/src/private/components/arsenal/vulnerabilities/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/arsenal/vulnerabilities/Root.tsx @@ -27,6 +27,8 @@ import useHelper from '../../../../utils/hooks/useHelper'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import VulnerabilityEdition from './VulnerabilityEdition'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootVulnerabilitySubscription($id: ID!) { @@ -101,7 +103,7 @@ const RootVulnerability = ({ queryRef, vulnerabilityId }: RootVulnerabilityProps const paddingRight = getPaddingRight(location.pathname, vulnerabilityId, '/dashboard/arsenal/vulnerabilities'); const link = `/dashboard/arsenal/vulnerabilities/${vulnerabilityId}/knowledge`; return ( - <> + {vulnerability ? ( <> @@ -145,6 +147,7 @@ const RootVulnerability = ({ queryRef, vulnerabilityId }: RootVulnerabilityProps )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} isOpenctiAlias={true} /> @@ -263,7 +266,7 @@ const RootVulnerability = ({ queryRef, vulnerabilityId }: RootVulnerabilityProps ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/common/menus/CreateRelationshipButtonComponent.tsx b/opencti-platform/opencti-front/src/private/components/common/menus/CreateRelationshipButtonComponent.tsx new file mode 100644 index 000000000000..3b53482dec43 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/common/menus/CreateRelationshipButtonComponent.tsx @@ -0,0 +1,30 @@ +import React, { FunctionComponent } from 'react'; +import Security from 'src/utils/Security'; +import { KNOWLEDGE_KNUPDATE } from 'src/utils/hooks/useGranted'; +import StixCoreRelationshipCreationFromControlledDial from '../stix_core_relationships/StixCoreRelationshipCreationFromControlledDial'; + +interface CreateRelationshipButtonComponentProps { + id: string, + defaultStartTime?: string | number | Date, + defaultStopTime?: string | number | Date, +} + +const CreateRelationshipButtonComponent: FunctionComponent = ({ + id, + defaultStartTime, + defaultStopTime, +}) => { + const startTime = new Date(defaultStartTime ?? new Date()).toISOString(); + const stopTime = new Date(defaultStopTime ?? new Date()).toISOString(); + return ( + + + + ); +}; + +export default CreateRelationshipButtonComponent; diff --git a/opencti-platform/opencti-front/src/private/components/common/menus/CreateRelationshipContextProvider.tsx b/opencti-platform/opencti-front/src/private/components/common/menus/CreateRelationshipContextProvider.tsx new file mode 100644 index 000000000000..d38e7c627185 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/common/menus/CreateRelationshipContextProvider.tsx @@ -0,0 +1,68 @@ +import React, { ReactNode, createContext, useMemo, useState } from 'react'; + +interface CreateRelationshipContextStateType { + relationshipTypes?: string[]; + stixCoreObjectTypes?: string[]; + connectionKey?: string; + reversed?: boolean; + paginationOptions?: unknown; + onCreate?: () => void; +} + +export interface CreateRelationshipContextType { + state: CreateRelationshipContextStateType; + setState: (state: CreateRelationshipContextStateType) => void; +} + +const defaultContext: CreateRelationshipContextType = { + state: { + relationshipTypes: [], + stixCoreObjectTypes: [], + connectionKey: 'Pagination_stixCoreObjects', + reversed: false, + }, + setState: () => {}, +}; + +export const CreateRelationshipContext = createContext(defaultContext); + +const CreateRelationshipContextProvider = ({ children }: { children: ReactNode }) => { + const [relationshipTypes, setRelationshipTypes] = useState([]); + const [stixCoreObjectTypes, setStixCoreObjectTypes] = useState([]); + const [connectionKey, setConnectionKey] = useState('Pagination_stixCoreObjects'); + const [reversed, setReversed] = useState(false); + const [paginationOptions, setPaginationOptions] = useState(); + const [onCreate, setOnCreate] = useState<() => void>(); + const state = { + relationshipTypes, + stixCoreObjectTypes, + connectionKey, + reversed, + paginationOptions, + onCreate, + }; + const setState = ({ + relationshipTypes: updatedRelationshipTypes, + stixCoreObjectTypes: updatedStixCoreObjectTypes, + connectionKey: updatedConnectionKey, + reversed: updatedReversed, + paginationOptions: updatedPaginationOptions, + onCreate: updatedOnCreate, + }: CreateRelationshipContextStateType) => { + if (updatedRelationshipTypes) setRelationshipTypes(updatedRelationshipTypes); + if (updatedStixCoreObjectTypes) setStixCoreObjectTypes(updatedStixCoreObjectTypes); + if (updatedConnectionKey) setConnectionKey(updatedConnectionKey); + if (updatedReversed) setReversed(updatedReversed); + if (updatedPaginationOptions) setPaginationOptions(updatedPaginationOptions); + if (updatedOnCreate) setOnCreate(() => updatedOnCreate); // Dispatching inner function to let context consumer call the onCreate function + }; + const values = useMemo(() => ({ + state, + setState, + }), [...Object.values(state)]); + return + {children} + ; +}; + +export default CreateRelationshipContextProvider; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/CreateRelationshipControlledDial.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/CreateRelationshipControlledDial.tsx new file mode 100644 index 000000000000..2ecdf5e9e4b9 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/CreateRelationshipControlledDial.tsx @@ -0,0 +1,25 @@ +import { Button } from '@mui/material'; +import React from 'react'; +import { useFormatter } from '../../../../components/i18n'; + +const CreateRelationshipControlledDial = ({ onOpen }: { + onOpen: () => void +}) => { + const { t_i18n } = useFormatter(); + return ( + + ); +}; + +export default CreateRelationshipControlledDial; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/CreateRelationshipHeader.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/CreateRelationshipHeader.tsx new file mode 100644 index 000000000000..568ee1952afd --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/CreateRelationshipHeader.tsx @@ -0,0 +1,120 @@ +import React, { FunctionComponent, useContext, useState } from 'react'; +import { CreateRelationshipContext } from '@components/common/menus/CreateRelationshipContextProvider'; +import { Button, Typography } from '@mui/material'; +import StixDomainObjectCreation from '@components/common/stix_domain_objects/StixDomainObjectCreation'; +import StixCyberObservableCreation from '@components/observations/stix_cyber_observables/StixCyberObservableCreation'; +import { computeTargetStixCyberObservableTypes, computeTargetStixDomainObjectTypes } from '../../../../utils/stixTypeUtils'; +import { useFormatter } from '../../../../components/i18n'; +import { PaginationOptions } from '../../../../components/list_lines'; + +interface CreateRelationshipHeaderProps { + showCreates: boolean, + searchPaginationOptions?: PaginationOptions, +} + +// Custom header prop for entity/observable creation buttons in initial step +const CreateRelationshipHeader: FunctionComponent = ({ + showCreates, + searchPaginationOptions, +}) => { + const { t_i18n } = useFormatter(); + + const [openCreateEntity, setOpenCreateEntity] = useState(false); + const [openCreateObservable, setOpenCreateObservable] = useState(false); + const { state: { stixCoreObjectTypes } } = useContext(CreateRelationshipContext); + const targetEntityTypes = (stixCoreObjectTypes ?? []).length > 0 ? stixCoreObjectTypes ?? ['Stix-Core-Object'] : ['Stix-Core-Object']; + const targetStixDomainObjectTypes = computeTargetStixDomainObjectTypes(targetEntityTypes); + const targetStixCyberObservableTypes = computeTargetStixCyberObservableTypes(targetEntityTypes); + const showSDOCreation = targetStixDomainObjectTypes.length > 0; + const showSCOCreation = targetStixCyberObservableTypes.length > 0; + + const handleOpenCreateEntity = () => setOpenCreateEntity(true); + const handleCloseCreateEntity = () => setOpenCreateEntity(false); + const handleOpenCreateObservable = () => setOpenCreateObservable(true); + const handleCloseCreateObservable = () => setOpenCreateObservable(false); + + const entityTypes = [ + ...targetStixDomainObjectTypes, + ...targetStixCyberObservableTypes, + ]; + + return ( +
+ {t_i18n('Create a relationship')} + {showCreates + &&
+ {showSDOCreation && ( + + )} + {showSCOCreation && ( + + )} + + +
+ } +
+ ); +}; + +export default CreateRelationshipHeader; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/EntityStixCoreRelationships.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/EntityStixCoreRelationships.tsx index 577eef6664e2..f0fa2ed11dc9 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/EntityStixCoreRelationships.tsx +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/EntityStixCoreRelationships.tsx @@ -21,11 +21,11 @@ interface EntityStixCoreRelationshipsProps { defaultStartTime: string; defaultStopTime: string; relationshipTypes: string[]; - stixCoreObjectTypes: string[]; + stixCoreObjectTypes?: string[]; currentView: string; enableNestedView?: boolean; enableContextualView: boolean; - isRelationReversed: boolean; + isRelationReversed?: boolean; allDirections?: boolean; role?: string; paddingRightButtonAdd?: number; @@ -40,18 +40,18 @@ EntityStixCoreRelationshipsProps defaultStartTime, defaultStopTime, relationshipTypes, - stixCoreObjectTypes, + stixCoreObjectTypes = [], currentView, enableNestedView, enableContextualView, - isRelationReversed, + isRelationReversed = false, allDirections, role, paddingRightButtonAdd, handleChangeView, }) => { const classes = useStyles(); - const LOCAL_STORAGE_KEY = `relationships-${entityId}-${stixCoreObjectTypes?.join( + const LOCAL_STORAGE_KEY = `relationships-${entityId}-${stixCoreObjectTypes.join( '-', )}-${relationshipTypes?.join('-')}`; const localStorage = usePaginationLocalStorage( diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/StixCoreRelationshipCreationFromControlledDial.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/StixCoreRelationshipCreationFromControlledDial.tsx new file mode 100644 index 000000000000..d01bd84ab4c9 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/StixCoreRelationshipCreationFromControlledDial.tsx @@ -0,0 +1,64 @@ +import React, { FunctionComponent } from 'react'; +import { CircularProgress } from '@mui/material'; +import StixCoreRelationshipCreationFromControlledDialContent from '@components/common/stix_core_relationships/StixCoreRelationshipCreationFromControlledDialContent'; +import { stixCoreRelationshipCreationFromEntityQuery } from './StixCoreRelationshipCreationFromEntity'; +import { StixCoreRelationshipCreationFromEntityQuery } from './__generated__/StixCoreRelationshipCreationFromEntityQuery.graphql'; +import useQueryLoading from '../../../../utils/hooks/useQueryLoading'; +import Loader, { LoaderVariant } from '../../../../components/Loader'; + +export const renderLoader = () => { + return ( +
+ + + +
+ ); +}; + +interface StixCoreRelationshipCreationFromControlledDialProps { + entityId: string, + isReversable?: boolean, + defaultStartTime?: string, + defaultStopTime?: string, + controlledDial?: ({ onOpen }: { onOpen: () => void }) => React.ReactElement, +} + +const StixCoreRelationshipCreationFromControlledDial: FunctionComponent = ({ + entityId, + isReversable = false, + defaultStartTime, + defaultStopTime, + controlledDial, +}) => { + const queryRef = useQueryLoading(stixCoreRelationshipCreationFromEntityQuery, { id: entityId }); + if (queryRef) { + return ( + }> + + + ); + } + return ( + + ); +}; + +export default StixCoreRelationshipCreationFromControlledDial; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/StixCoreRelationshipCreationFromControlledDialContent.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/StixCoreRelationshipCreationFromControlledDialContent.tsx new file mode 100644 index 000000000000..7284a0cedb58 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/StixCoreRelationshipCreationFromControlledDialContent.tsx @@ -0,0 +1,489 @@ +import { + StixCoreRelationshipCreationFromEntityForm, + stixCoreRelationshipCreationFromEntityFromMutation, + stixCoreRelationshipCreationFromEntityQuery, + stixCoreRelationshipCreationFromEntityStixCoreObjectsLineFragment, + stixCoreRelationshipCreationFromEntityStixCoreObjectsLinesFragment, + stixCoreRelationshipCreationFromEntityStixCoreObjectsLinesQuery, + stixCoreRelationshipCreationFromEntityToMutation, + TargetEntity, +} from '@components/common/stix_core_relationships/StixCoreRelationshipCreationFromEntity'; +import { StixCoreRelationshipCreationFromEntityQuery } from '@components/common/stix_core_relationships/__generated__/StixCoreRelationshipCreationFromEntityQuery.graphql'; +import React, { FunctionComponent, useContext, useEffect, useState } from 'react'; +import { PreloadedQuery, usePreloadedQuery } from 'react-relay'; +import { CreateRelationshipContext } from '@components/common/menus/CreateRelationshipContextProvider'; +import { v4 as uuid } from 'uuid'; +import { + StixCoreRelationshipCreationFromEntityStixCoreObjectsLinesQuery, + StixCoreRelationshipCreationFromEntityStixCoreObjectsLinesQuery$variables, +} from '@components/common/stix_core_relationships/__generated__/StixCoreRelationshipCreationFromEntityStixCoreObjectsLinesQuery.graphql'; +import CreateRelationshipControlledDial from '@components/common/stix_core_relationships/CreateRelationshipControlledDial'; +import CreateRelationshipHeader from '@components/common/stix_core_relationships/CreateRelationshipHeader'; +import Drawer from '@components/common/drawer/Drawer'; +import StixCoreRelationshipCreationForm from '@components/common/stix_core_relationships/StixCoreRelationshipCreationForm'; +import { FormikConfig } from 'formik/dist/types'; +import { ConnectionHandler, RecordSourceSelectorProxy } from 'relay-runtime'; +import { + StixCoreRelationshipCreationFromEntityStixCoreObjectsLines_data$data, +} from '@components/common/stix_core_relationships/__generated__/StixCoreRelationshipCreationFromEntityStixCoreObjectsLines_data.graphql'; +import { ChevronRightOutlined } from '@mui/icons-material'; +import Fab from '@mui/material/Fab'; +import BulkRelationDialogContainer from '@components/common/bulk/dialog/BulkRelationDialogContainer'; +import { PaginationOptions } from '../../../../components/list_lines'; +import { UseLocalStorageHelpers, usePaginationLocalStorage } from '../../../../utils/hooks/useLocalStorage'; +import { emptyFilterGroup, useBuildEntityTypeBasedFilterContext } from '../../../../utils/filters/filtersUtils'; +import { commitMutation, handleErrorInForm } from '../../../../relay/environment'; +import { FilterGroup } from '../../../../utils/filters/filtersHelpers-types'; +import { useFormatter } from '../../../../components/i18n'; +import { UserContext } from '../../../../utils/hooks/useAuth'; +import useEntityToggle from '../../../../utils/hooks/useEntityToggle'; +import useQueryLoading from '../../../../utils/hooks/useQueryLoading'; +import { UsePreloadedPaginationFragment } from '../../../../utils/hooks/usePreloadedPaginationFragment'; +import DataTable from '../../../../components/dataGrid/DataTable'; +import { DataTableVariant } from '../../../../components/dataGrid/dataTableTypes'; +import { resolveRelationsTypes } from '../../../../utils/Relation'; +import { isNodeInConnection } from '../../../../utils/store'; +import { formatDate } from '../../../../utils/Time'; + +/** + * The first page of the create relationship drawer: selecting the entity/entites + * @param props.name The source entity's name + * @param props.entity_id The source entity's id + * @param props.entity_type The source entity's type + * @param props.setTargetEntities Dispatch to set relationship target entities + * @param props.targetEntities + * @param props.handleNextStep Function to continue on to the next step + * @returns JSX.Element + */ +const SelectEntity = ({ + name = '', + entity_id, + entity_type, + setTargetEntities, + targetEntities, + handleNextStep, + searchPaginationOptions, + localStorageKey, + helpers, + contextFilters, + virtualEntityTypes, +}: { + name?: string, + entity_id: string, + entity_type: string, + setTargetEntities: React.Dispatch, + targetEntities: TargetEntity[], + handleNextStep: () => void, + searchPaginationOptions: PaginationOptions, + localStorageKey: string, + helpers: UseLocalStorageHelpers, + contextFilters: FilterGroup, + virtualEntityTypes: string[], +}) => { + const { t_i18n } = useFormatter(); + const { platformModuleHelpers } = useContext(UserContext); + + const { + selectedElements, + } = useEntityToggle(localStorageKey); + + useEffect(() => { + const newTargetEntities: TargetEntity[] = Object.values(selectedElements).map((item) => ({ + id: item.id, + entity_type: item.entity_type ?? '', + name: item.name ?? item.observable_value ?? '', + })); + setTargetEntities(newTargetEntities); + }, [selectedElements]); + + const isRuntimeSort = platformModuleHelpers?.isRuntimeFieldEnable(); + const buildColumns = { + entity_type: { + label: 'Type', + percentWidth: 15, + isSortable: true, + }, + value: { + label: 'Value', + percentWidth: 32, + isSortable: false, + }, + createdBy: { + label: 'Author', + percentWidth: 15, + isSortable: isRuntimeSort, + }, + objectLabel: { + label: 'Labels', + percentWidth: 22, + isSortable: false, + }, + objectMarking: { + label: 'Marking', + percentWidth: 15, + isSortable: isRuntimeSort, + }, + }; + const queryRef = useQueryLoading( + stixCoreRelationshipCreationFromEntityStixCoreObjectsLinesQuery, + { ...searchPaginationOptions, count: 100 } as StixCoreRelationshipCreationFromEntityStixCoreObjectsLinesQuery$variables, + ); + + const preloadedPaginationProps = { + linesQuery: stixCoreRelationshipCreationFromEntityStixCoreObjectsLinesQuery, + linesFragment: stixCoreRelationshipCreationFromEntityStixCoreObjectsLinesFragment, + queryRef, + nodePath: ['stixCoreObjects', 'pageInfo', 'globalCount'], + setNumberOfElements: helpers.handleSetNumberOfElements, + } as UsePreloadedPaginationFragment; + + const [tableRootRef, setTableRootRef] = useState(null); + + const initialValues = { + searchTerm: '', + sortBy: 'created', + orderAsc: false, + openExports: false, + filters: emptyFilterGroup, + }; + + return ( +
+ {queryRef && ( +
+ data.stixCoreObjects?.edges?.map((n) => n?.node)} + storageKey={localStorageKey} + lineFragment={stixCoreRelationshipCreationFromEntityStixCoreObjectsLineFragment} + initialValues={initialValues} + toolbarFilters={contextFilters} + preloadedPaginationProps={preloadedPaginationProps} + entityTypes={virtualEntityTypes} + additionalHeaderButtons={[( + + )]} + /> +
+ )} + handleNextStep()} + disabled={Object.values(targetEntities).length < 1} + style={{ + position: 'fixed', + bottom: 40, + right: 30, + zIndex: 1001, + }} + > + {t_i18n('Continue')} + +
+ ); +}; + +/** + * The second page of the create relationship drawer: filling out the relationship + * @param props.sourceEntity The source entity + * @param props.targetEntities The target entities + * @param props.handleClose Function called on close + * @param props.isReversable Whether this relationship can be reversed + * @param props.defaultStartTime The default start time + * @param props.defaultStopTime The default stop time + * @returns JSX.Element + */ +const RenderForm = ({ + sourceEntity, + targetEntities, + handleClose, + isReversable, + defaultStartTime, + defaultStopTime, +}: { + sourceEntity: TargetEntity, + targetEntities: TargetEntity[], + handleClose: () => void, + isReversable?: boolean + defaultStartTime?: string, + defaultStopTime?: string, +}) => { + const { state: { + relationshipTypes: initialRelationshipTypes, + reversed: initiallyReversed, + onCreate, + connectionKey, + paginationOptions, + } } = useContext(CreateRelationshipContext); + const { schema } = useContext(UserContext); + const [reversed, setReversed] = useState(initiallyReversed ?? false); + + const handleReverse = () => setReversed(!reversed); + + let fromEntities = [sourceEntity]; + let toEntities = targetEntities; + if (reversed) { + fromEntities = targetEntities; + toEntities = [sourceEntity]; + } + const resolvedRelationshipTypes = (initialRelationshipTypes ?? []).length > 0 + ? initialRelationshipTypes ?? [] + : resolveRelationsTypes( + fromEntities[0].entity_type, + toEntities[0].entity_type, + schema?.schemaRelationsTypesMapping ?? new Map(), + ); + + const relationshipTypes = resolvedRelationshipTypes; + const startTime = defaultStartTime ?? (new Date()).toISOString(); + const stopTime = defaultStopTime ?? (new Date()).toISOString(); + + const commit = (finalValues: object) => { + return new Promise((resolve, reject) => { + commitMutation({ + mutation: reversed + ? stixCoreRelationshipCreationFromEntityToMutation + : stixCoreRelationshipCreationFromEntityFromMutation, + variables: { input: finalValues }, + updater: (store: RecordSourceSelectorProxy) => { + if (typeof onCreate !== 'function') { + const userProxy = store.get(store.getRoot().getDataID()); + const payload = store.getRootField('stixCoreRelationshipAdd'); + + const fromOrTo = reversed ? 'from' : 'to'; + const createdNode = connectionKey && payload !== null + ? payload.getLinkedRecord(fromOrTo) + : payload; + const connKey = connectionKey ?? 'Pagination_stixCoreRelationships'; + // When using connectionKey we use less props of PaginationOptions, we need to filter them + let conn; + if (userProxy && paginationOptions) { + conn = ConnectionHandler.getConnection( + userProxy, + connKey, + paginationOptions, + ); + } + + if (conn && payload !== null + && !isNodeInConnection(payload, conn) + && !isNodeInConnection(payload.getLinkedRecord(fromOrTo), conn) + ) { + const newEdge = payload.setLinkedRecord(createdNode, 'node'); + ConnectionHandler.insertEdgeBefore(conn, newEdge); + } + } + }, + optimisticUpdater: undefined, + setSubmitting: undefined, + optimisticResponse: undefined, + onError: (error: Error) => { + reject(error); + }, + onCompleted: (response: Response) => { + resolve(response); + }, + }); + }); + }; + + const onSubmit: FormikConfig['onSubmit'] = async (values, { setSubmitting, setErrors, resetForm }) => { + setSubmitting(true); + for (const targetEntity of targetEntities) { + const fromEntityId = reversed ? targetEntity.id : sourceEntity.id; + const toEntityId = reversed ? sourceEntity.id : targetEntity.id; + const finalValues = { + ...values, + confidence: parseInt(values.confidence, 10), + fromId: fromEntityId, + toId: toEntityId, + start_time: formatDate(values.start_time), + stop_time: formatDate(values.stop_time), + killChainPhases: values.killChainPhases.map((kcp) => kcp.value), + createdBy: values.createdBy?.value, + objectMarking: values.objectMarking.map((marking) => marking.value), + externalReferences: values.externalReferences.map((ref) => ref.value), + }; + try { + // eslint-disable-next-line no-await-in-loop + await commit(finalValues); + } catch (error) { + setSubmitting(false); + return handleErrorInForm(error, setErrors); + } + } + setSubmitting(false); + resetForm(); + handleClose(); + if (typeof onCreate === 'function') { + onCreate(); + } + return true; + }; + + return ( + + ); +}; + +interface StixCoreRelationshipCreationFromControlledDialContentProps { + queryRef: PreloadedQuery, + entityId: string, + isReversable?: boolean, + defaultStartTime?: string, + defaultStopTime?: string, + controlledDial?: ({ onOpen }: { onOpen: () => void }) => React.ReactElement, +} + +const StixCoreRelationshipCreationFromControlledDialContent: FunctionComponent = ({ + queryRef, + entityId, + isReversable = false, + defaultStartTime, + defaultStopTime, + controlledDial, +}) => { + const data = usePreloadedQuery(stixCoreRelationshipCreationFromEntityQuery, queryRef); + const [step, setStep] = useState(0); + const [targetEntities, setTargetEntities] = useState([]); + + const reset = () => { + setStep(0); + setTargetEntities([]); + }; + + const { state: { stixCoreObjectTypes } } = useContext(CreateRelationshipContext); + + const typeFilters = (stixCoreObjectTypes ?? []).length > 0 + ? { + mode: 'and', + filterGroups: [], + filters: [{ + id: uuid(), + key: 'entity_type', + values: stixCoreObjectTypes ?? [], + operator: 'eq', + mode: 'or', + }], + } + : emptyFilterGroup; + let virtualEntityTypes = stixCoreObjectTypes; + if (virtualEntityTypes === undefined || virtualEntityTypes.length < 1) { + virtualEntityTypes = ['Stix-Domain-Object', 'Stix-Cyber-Observable']; + } + const localStorageKey = `${entityId}_stixCoreRelationshipCreationFromEntity`; + + const [sortBy, setSortBy] = useState('_score'); + const [orderAsc, setOrderAsc] = useState(false); + + const { viewStorage, helpers } = usePaginationLocalStorage( + localStorageKey, + { filters: typeFilters }, + ); + const { searchTerm = '', orderAsc: storageOrderAsc, sortBy: storageSortBy, filters } = viewStorage; + + useEffect(() => { + if (storageSortBy && (storageSortBy !== sortBy)) setSortBy(storageSortBy); + if (storageOrderAsc !== undefined && (storageOrderAsc !== orderAsc)) setOrderAsc(storageOrderAsc); + }, [storageOrderAsc, storageSortBy]); + + const contextFilters = useBuildEntityTypeBasedFilterContext(virtualEntityTypes, filters); + const searchPaginationOptions: PaginationOptions = { + search: searchTerm, + filters: contextFilters, + orderBy: sortBy, + orderMode: orderAsc ? 'asc' : 'desc', + } as PaginationOptions; + + if (!data.stixCoreObject) { + throw Error('Can\'t resolve this entity'); + } + const { name, entity_type, observable_value } = data.stixCoreObject; + return ( + } + containerStyle={{ + minHeight: '100vh', + }} + > + {({ onClose }) => ( +
+ {step === 0 && ( + setStep(1)} + searchPaginationOptions={searchPaginationOptions} + localStorageKey={localStorageKey} + helpers={helpers} + contextFilters={contextFilters} + virtualEntityTypes={virtualEntityTypes} + /> + )} + {step === 1 && ( + { + reset(); + onClose(); + }} + isReversable={isReversable} + defaultStartTime={defaultStartTime} + defaultStopTime={defaultStopTime} + /> + )} +
+ )} +
+ ); +}; + +export default StixCoreRelationshipCreationFromControlledDialContent; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/StixCoreRelationshipCreationFromEntity.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/StixCoreRelationshipCreationFromEntity.tsx index 360d5d39c74b..8e63223a38e5 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/StixCoreRelationshipCreationFromEntity.tsx +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/StixCoreRelationshipCreationFromEntity.tsx @@ -332,7 +332,7 @@ export const stixCoreRelationshipCreationFromEntityStixCoreObjectsLineFragment = `; -const stixCoreRelationshipCreationFromEntityQuery = graphql` +export const stixCoreRelationshipCreationFromEntityQuery = graphql` query StixCoreRelationshipCreationFromEntityQuery($id: String!) { stixCoreObject(id: $id) { id @@ -439,7 +439,7 @@ export const stixCoreRelationshipCreationFromEntityFromMutation = graphql` } `; -const stixCoreRelationshipCreationFromEntityToMutation = graphql` +export const stixCoreRelationshipCreationFromEntityToMutation = graphql` mutation StixCoreRelationshipCreationFromEntityToMutation( $input: StixCoreRelationshipAddInput! ) { @@ -465,9 +465,13 @@ interface StixCoreRelationshipCreationFromEntityProps { onCreate?: () => void; openExports?: boolean; handleReverseRelation?: () => void; + controlledDial?: (({ onOpen, onClose }: { + onOpen: () => void; + onClose: () => void; + }) => React.ReactElement>) } -interface StixCoreRelationshipCreationFromEntityForm { +export interface StixCoreRelationshipCreationFromEntityForm { confidence: string; start_time: string; stop_time: string; @@ -502,6 +506,7 @@ const StixCoreRelationshipCreationFromEntity: FunctionComponent setOpen(true), + onClose: handleClose, + }) + : ''; + if (variant === 'inLine') { + openElement = ( + setOpen(true)} + style={{ float: 'left', margin: '-15px 0 0 -2px' }} + size="large" + > + + + ); + } else if (controlledDial === undefined && !openExports) { + openElement = ( + setOpen(true)} + color="primary" + aria-label="Add" + className={classes.createButton} + style={{ right: paddingRight || 30 }} + > + + + ); + } + return ( <> - {/* eslint-disable-next-line no-nested-ternary */} - {variant === 'inLine' ? ( - setOpen(true)} - style={{ float: 'left', margin: '-15px 0 0 -2px' }} - size="large" - > - - - ) : !openExports ? ( - setOpen(true)} - color="primary" - aria-label="Add" - className={classes.createButton} - style={{ right: paddingRight || 30 }} - > - - - ) : ( - '' - )} + {openElement} ; relationshipTypes: string[]; - stixCoreObjectTypes?: string[]; + stixCoreObjectTypes: string[]; isRelationReversed: boolean; currentView: string; enableNestedView?: boolean; @@ -38,7 +40,7 @@ EntityStixCoreRelationshipsEntitiesViewProps defaultStopTime, localStorage, relationshipTypes, - stixCoreObjectTypes = ['Stix-Core-Object'], + stixCoreObjectTypes, isRelationReversed, currentView, enableNestedView, @@ -61,7 +63,9 @@ EntityStixCoreRelationshipsEntitiesViewProps numberOfElements, openExports, } = viewStorage; - + const { isFeatureEnable } = useHelper(); + const isFABReplaced = isFeatureEnable('FAB_REPLACEMENT'); + const { setState: setCreateRelationshipContext } = useContext(CreateRelationshipContext); const { platformModuleHelpers } = useAuth(); const isRuntimeSort = platformModuleHelpers.isRuntimeFieldEnable(); const isObservables = isStixCyberObservables(stixCoreObjectTypes); @@ -152,6 +156,19 @@ EntityStixCoreRelationshipsEntitiesViewProps }; const finalView = currentView || view; + + setCreateRelationshipContext({ + stixCoreObjectTypes, + relationshipTypes, + connectionKey: 'Pagination_stixCoreObjects', + reversed: isRelationReversed, + }); + useEffect(() => { + setCreateRelationshipContext({ + paginationOptions, + }); + }, []); + return ( <> - - - + {!isFABReplaced && ( + + + + )} ); }; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/views/EntityStixCoreRelationshipsRelationshipsView.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/views/EntityStixCoreRelationshipsRelationshipsView.tsx index dcd8a56e51ab..779c7fa7fea9 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/views/EntityStixCoreRelationshipsRelationshipsView.tsx +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/views/EntityStixCoreRelationshipsRelationshipsView.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent, useState } from 'react'; +import React, { FunctionComponent, useState, useContext, useEffect } from 'react'; import useAuth from '../../../../../utils/hooks/useAuth'; import ListLines from '../../../../../components/list_lines/ListLines'; import { QueryRenderer } from '../../../../../relay/environment'; @@ -15,6 +15,8 @@ import { PaginationLocalStorage } from '../../../../../utils/hooks/useLocalStora import { DataColumns, PaginationOptions } from '../../../../../components/list_lines'; import { isFilterGroupNotEmpty, useRemoveIdAndIncorrectKeysFromFilterGroupObject } from '../../../../../utils/filters/filtersUtils'; import { FilterGroup } from '../../../../../utils/filters/filtersHelpers-types'; +import useHelper from '../../../../../utils/hooks/useHelper'; +import { CreateRelationshipContext } from '../../menus/CreateRelationshipContextProvider'; interface EntityStixCoreRelationshipsRelationshipsViewProps { entityId: string @@ -41,7 +43,7 @@ const EntityStixCoreRelationshipsRelationshipsView: FunctionComponent { + setCreateRelationshipContext({ + paginationOptions, + }); + }, []); + return ( <> - - - - + {!isFABReplaced && ( + + + + )} ); }; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/views/indicators/EntityStixCoreRelationshipsIndicatorsEntitiesView.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/views/indicators/EntityStixCoreRelationshipsIndicatorsEntitiesView.tsx index e14565951ba0..c7d4c2990d71 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/views/indicators/EntityStixCoreRelationshipsIndicatorsEntitiesView.tsx +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_relationships/views/indicators/EntityStixCoreRelationshipsIndicatorsEntitiesView.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useContext, useEffect } from 'react'; import ListLines from '../../../../../../components/list_lines/ListLines'; import ToolBar from '../../../../data/ToolBar'; import useEntityToggle from '../../../../../../utils/hooks/useEntityToggle'; @@ -14,6 +14,8 @@ import useAuth from '../../../../../../utils/hooks/useAuth'; import { QueryRenderer } from '../../../../../../relay/environment'; import { isFilterGroupNotEmpty, useRemoveIdAndIncorrectKeysFromFilterGroupObject } from '../../../../../../utils/filters/filtersUtils'; import { FilterGroup } from '../../../../../../utils/filters/filtersHelpers-types'; +import useHelper from '../../../../../../utils/hooks/useHelper'; +import { CreateRelationshipContext } from '../../../menus/CreateRelationshipContextProvider'; interface EntityStixCoreRelationshipsIndicatorsEntitiesViewProps { entityId: string @@ -66,7 +68,10 @@ const EntityStixCoreRelationshipsIndicatorsEntitiesView: FunctionComponent { + setCreateRelationshipContext({ + relationshipTypes, + stixCoreObjectTypes: stixDomainObjectTypes, + connectionKey: 'Pagination_indicators', + reversed: isRelationReversed, + paginationOptions, + }); + }, []); + return ( <> - - - + {!isFABReplaced && ( + + + + )} ); }; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectAttackPatterns.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectAttackPatterns.tsx index 6cec2a82c012..46d7c0d51b6e 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectAttackPatterns.tsx +++ b/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectAttackPatterns.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useContext, useEffect } from 'react'; import StixDomainObjectAttackPatternsKillChainContainer from '@components/common/stix_domain_objects/StixDomainObjectAttackPatternsKillChainContainer'; import { StixDomainObjectAttackPatternsKillChainQuery, @@ -14,6 +14,7 @@ import { useRemoveIdAndIncorrectKeysFromFilterGroupObject, } from '../../../../utils/filters/filtersUtils'; import useQueryLoading from '../../../../utils/hooks/useQueryLoading'; +import { CreateRelationshipContext } from '../menus/CreateRelationshipContextProvider'; interface StixDomainObjectAttackPatternsProps { stixDomainObjectId: string, @@ -29,6 +30,7 @@ const StixDomainObjectAttackPatterns: FunctionComponent { const LOCAL_STORAGE_KEY = `attack-patterns-${stixDomainObjectId}`; + const { setState: setCreateRelationshipContext } = useContext(CreateRelationshipContext); const { viewStorage, helpers, @@ -67,6 +69,14 @@ const StixDomainObjectAttackPatterns: FunctionComponent { + setCreateRelationshipContext({ + stixCoreObjectTypes: ['Attack-Pattern'], + paginationOptions: queryPaginationOptions, + }); + }, []); + return (
{ const { t_i18n } = useFormatter(); + const { setState: setCreateRelationshipContext } = useContext(CreateRelationshipContext); + const { isFeatureEnable } = useHelper(); + const isFABReplaced = isFeatureEnable('FAB_REPLACEMENT'); const [currentColorsReversed, setCurrentColorsReversed] = useState(false); const [targetEntities, setTargetEntities] = useState([]); const [selectedKillChain, setSelectedKillChain] = useState('mitre-attack'); @@ -105,6 +110,11 @@ const StixDomainObjectAttackPatternsKillChain: FunctionComponent { loadQuery(paginationOptions, { fetchPolicy: 'store-and-network' }); }, [queryRef]); + useEffect(() => { + setCreateRelationshipContext({ + onCreate: refetch, + }); + }, []); const handleToggleColorsReversed = () => { setCurrentColorsReversed(!currentColorsReversed); @@ -416,19 +426,21 @@ const StixDomainObjectAttackPatternsKillChain: FunctionComponent )} - - - + {!isFABReplaced && ( + + + + )} {currentView !== 'matrix-in-line' && { const classes = useStyles(); const theme = useTheme(); const { t_i18n } = useFormatter(); + const { isFeatureEnable } = useHelper(); + const isFABReplaced = isFeatureEnable('FAB_REPLACEMENT'); const { stixDomainObject, isOpenctiAlias, PopoverComponent, EditComponent, + RelateComponent, viewAs, onViewAs, disablePopover, @@ -611,6 +615,9 @@ const StixDomainObjectHeader = (props) => {
)} {EditComponent} + {isFABReplaced && RelateComponent && ( + + )} diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectNestedEntities.jsx b/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectNestedEntities.jsx deleted file mode 100644 index b515419c52b8..000000000000 --- a/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectNestedEntities.jsx +++ /dev/null @@ -1,205 +0,0 @@ -import React, { Component } from 'react'; -import * as PropTypes from 'prop-types'; -import { compose } from 'ramda'; -import withStyles from '@mui/styles/withStyles'; -import Typography from '@mui/material/Typography'; -import { ArrowDropDown, ArrowDropUp } from '@mui/icons-material'; -import List from '@mui/material/List'; -import StixNestedRefRelationshipCreationFromEntity from '../stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntity'; -import inject18n from '../../../../components/i18n'; -import Security from '../../../../utils/Security'; -import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; -import { QueryRenderer } from '../../../../relay/environment'; -import StixDomainObjectNestedEntitiesLines, { stixDomainObjectNestedEntitiesLinesQuery } from './StixDomainObjectNestedEntitiesLines'; - -const styles = (theme) => ({ - paper: { - margin: 0, - padding: 15, - borderRadius: 4, - }, - item: { - paddingLeft: 10, - height: 50, - }, - itemIcon: { - color: theme.palette.primary.main, - }, - itemHead: { - paddingLeft: 10, - textTransform: 'uppercase', - }, - bodyItem: { - height: 20, - fontSize: 13, - float: 'left', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - goIcon: { - position: 'absolute', - right: -10, - }, - itemIconDisabled: { - color: theme.palette.grey[700], - }, - placeholder: { - display: 'inline-block', - height: '1em', - backgroundColor: theme.palette.grey[700], - }, -}); - -const inlineStylesHeaders = { - iconSort: { - position: 'absolute', - margin: '0 0 0 5px', - padding: 0, - top: '0px', - }, - relationship_type: { - float: 'left', - width: '20%', - fontSize: 12, - fontWeight: '700', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - cursor: 'pointer', - }, - entity_type: { - float: 'left', - width: '20%', - fontSize: 12, - fontWeight: '700', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - name: { - float: 'left', - width: '40%', - fontSize: 12, - fontWeight: '700', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - start_time: { - float: 'left', - fontSize: 12, - fontWeight: '700', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - cursor: 'pointer', - }, -}; - -class StixDomainObjectNestedEntities extends Component { - constructor(props) { - super(props); - this.state = { - sortBy: null, - orderAsc: false, - searchTerm: '', - view: 'lines', - relationReversed: false, - }; - } - - handleSort(field, orderAsc) { - this.setState({ sortBy: field, orderAsc }); - } - - SortHeader(field, label, isSortable) { - const { t } = this.props; - const sortComponent = this.state.orderAsc ? ( - - ) : ( - - ); - if (isSortable) { - return ( -
- {t(label)} - {this.state.sortBy === field ? sortComponent : ''} -
- ); - } - return ( -
- {t(label)} -
- ); - } - - render() { - const { searchTerm, sortBy, orderAsc } = this.state; - const { entityId, t, entityType, targetStixCoreObjectTypes } = this.props; - const paginationOptions = { - fromOrToId: entityId, - search: searchTerm, - orderBy: sortBy, - orderMode: orderAsc ? 'asc' : 'desc', - }; - return ( -
- - {t('Nested objects')} - - } - > - - -
- - ( - - )} - /> - -
- ); - } -} - -StixDomainObjectNestedEntities.propTypes = { - entityId: PropTypes.string, - entityType: PropTypes.string, - paginationOptions: PropTypes.object, - classes: PropTypes.object, - t: PropTypes.func, - navigate: PropTypes.object, - defaultStartTime: PropTypes.string, - defaultStopTime: PropTypes.string, -}; - -export default compose( - inject18n, - withStyles(styles), -)(StixDomainObjectNestedEntities); diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectNestedEntities.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectNestedEntities.tsx new file mode 100644 index 000000000000..d6b5004e233e --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/common/stix_domain_objects/StixDomainObjectNestedEntities.tsx @@ -0,0 +1,105 @@ +import { IconButton, List, styled, Typography } from '@mui/material'; +import React, { FunctionComponent, useContext, useEffect } from 'react'; +import { useFormatter } from 'src/components/i18n'; +import Security from 'src/utils/Security'; +import { KNOWLEDGE_KNUPDATE } from 'src/utils/hooks/useGranted'; +import useHelper from 'src/utils/hooks/useHelper'; +import { QueryRenderer } from 'src/relay/environment'; +import { Add } from '@mui/icons-material'; +import StixNestedRefRelationshipCreationFromEntityFabless from '../stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntityFabless'; +import { CreateRelationshipContext } from '../menus/CreateRelationshipContextProvider'; +import StixNestedRefRelationshipCreationFromEntity from '../stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntity'; +import StixDomainObjectNestedEntitiesLines, { stixDomainObjectNestedEntitiesLinesQuery } from './StixDomainObjectNestedEntitiesLines'; +import { StixDomainObjectNestedEntitiesLines_data$data } from './__generated__/StixDomainObjectNestedEntitiesLines_data.graphql'; + +interface StixDomainObjectNestedEntitiesProps { + entityId: string, + entityType: string, + targetStixCoreObjectTypes: string[], +} + +const StixDomainObjectNestedEntities: FunctionComponent< +StixDomainObjectNestedEntitiesProps +> = ({ + entityId, + entityType, + targetStixCoreObjectTypes, +}) => { + const { t_i18n } = useFormatter(); + const { setState } = useContext(CreateRelationshipContext); + const { isFeatureEnable } = useHelper(); + const isFABReplaced = isFeatureEnable('FAB_REPLACEMENT'); + + const StyledContainer = styled('div')({ marginTop: 20 }); + + const paginationOptions = { + fromOrToId: entityId, + search: '', + orderBy: null, + orderMode: 'desc', + }; + + useEffect(() => setState({ + paginationOptions, + }), []); + + return ( + + + {t_i18n('Nested objects')} + + } + > + {isFABReplaced + ? { + return ( + + + + ); + }} + /> + : + } + +
+ + ( + + )} + /> + + + ); +}; + +export default StixDomainObjectNestedEntities; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationForm.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationForm.tsx new file mode 100644 index 000000000000..f30cd204c963 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationForm.tsx @@ -0,0 +1,287 @@ +import { Button, MenuItem, useTheme } from '@mui/material'; +import { Field, Form, Formik, FormikHelpers } from 'formik'; +import React, { FunctionComponent } from 'react'; +import DateTimePickerField from 'src/components/DateTimePickerField'; +import SelectField from 'src/components/fields/SelectField'; +import { useFormatter } from 'src/components/i18n'; +import { itemColor } from 'src/utils/Colors'; +import { dayStartDate } from 'src/utils/Time'; +import { fieldSpacingContainerStyle } from 'src/utils/field'; +import * as Yup from 'yup'; +import ItemIcon from 'src/components/ItemIcon'; +import { truncate } from 'src/utils/String'; +import { getMainRepresentative } from 'src/utils/defaultRepresentatives'; +import { ArrowRightAlt } from '@mui/icons-material'; +import { TargetEntity } from '../stix_core_relationships/StixCoreRelationshipCreationFromEntity'; + +const RelationshipCard = ({ entity, multiple = false }: { + entity: TargetEntity, + multiple?: boolean, +}) => { + const { t_i18n } = useFormatter(); + const theme = useTheme(); + const color = itemColor(entity.entity_type); + + return ( +
+
+
+ +
+
+ {t_i18n(`entity_${entity.entity_type}`)} +
+
+
+ + {multiple + ? {t_i18n('Multiple entities selected')} + : truncate(getMainRepresentative(entity), 20) + } + +
+
+ ); +}; + +export interface StixNestedRefRelationshipCreationFormValues { + from_id: string, + to_ids: string[], + relationship_type: string, + start_time: string, + stop_time: string, +} + +interface StixNestedRefRelationshipCreationFormProps { + sourceEntity: TargetEntity, + targetEntities: TargetEntity[], + relationshipTypes: string[], + defaultStartTime?: string, + defaultStopTime?: string, + onSubmit: (values: StixNestedRefRelationshipCreationFormValues, helpers: FormikHelpers) => void, + handleClose: () => void, + handleBack: () => void, + handleReverse?: () => void, +} + +const StixNestedRefRelationshipCreationForm: FunctionComponent< +StixNestedRefRelationshipCreationFormProps +> = ({ + sourceEntity, + targetEntities, + relationshipTypes, + defaultStartTime, + defaultStopTime, + onSubmit, + handleClose, + handleBack, + handleReverse, +}) => { + if (targetEntities.length < 1) handleBack(); // Must have at least one target entity + + const { t_i18n } = useFormatter(); + const theme = useTheme(); + const stixNestedRefRelationshipValidation = () => Yup.object().shape({ + relationship_type: Yup.string().required(t_i18n('This field is required')), + start_time: Yup.date() + .typeError(t_i18n('The value must be a datetime (yyyy-MM-dd hh:mm (a|p)m)')) + .required(t_i18n('This field is required')), + stop_time: Yup.date() + .typeError(t_i18n('The value must be a datetime (yyyy-MM-dd hh:mm (a|p)m)')) + .required(t_i18n('This field is required')), + }); + + const initialValues: StixNestedRefRelationshipCreationFormValues = { + from_id: sourceEntity.id, + to_ids: targetEntities.map((target) => target.id), + relationship_type: relationshipTypes?.[0] ?? undefined, + start_time: defaultStartTime ?? dayStartDate().toISOString(), + stop_time: defaultStopTime ?? dayStartDate().toISOString(), + }; + + const targetEntity = targetEntities[0]; + + return ( + + {({ submitForm, isSubmitting }) => ( +
+
+
+ +
+ +
+ {typeof handleReverse === 'function' && ( + + )} +
+ 1} + /> +
+ + {relationshipTypes.map((type) => ( + + {t_i18n(`relationship_${type}`)} + + ))} + + + + +
+ + +
+
+
+ )} +
+ ); +}; + +export default StixNestedRefRelationshipCreationForm; diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntity.jsx b/opencti-platform/opencti-front/src/private/components/common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntity.jsx index dc7c1ef458e0..5ef8cb168c3f 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntity.jsx +++ b/opencti-platform/opencti-front/src/private/components/common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntity.jsx @@ -167,7 +167,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -const stixNestedRefRelationshipResolveTypes = graphql` +export const stixNestedRefRelationshipResolveTypes = graphql` query StixNestedRefRelationshipCreationFromEntityResolveQuery($id: String!, $toType: String!) { stixSchemaRefRelationships(id: $id, toType: $toType) { entity { @@ -278,7 +278,7 @@ const stixNestedRefRelationshipResolveTypes = graphql` } `; -const stixNestedRefRelationshipCreationFromEntityMutation = graphql` +export const stixNestedRefRelationshipCreationFromEntityMutation = graphql` mutation StixNestedRefRelationshipCreationFromEntityMutation( $input: StixRefRelationshipAddInput! ) { diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntityFabless.tsx b/opencti-platform/opencti-front/src/private/components/common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntityFabless.tsx new file mode 100644 index 000000000000..e86ad39e80c2 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntityFabless.tsx @@ -0,0 +1,427 @@ +import React, { FunctionComponent, useContext, useRef, useState } from 'react'; +import { useFormatter } from 'src/components/i18n'; +import { QueryRenderer, commitMutation, handleErrorInForm } from 'src/relay/environment'; +import ListLines from 'src/components/list_lines/ListLines'; +import { emptyFilterGroup, removeIdFromFilterGroupObject } from 'src/utils/filters/filtersUtils'; +import useFiltersState from 'src/utils/filters/useFiltersState'; +import useEntityToggle from 'src/utils/hooks/useEntityToggle'; +import { ChevronRightOutlined } from '@mui/icons-material'; +import { ConnectionHandler, RecordSourceSelectorProxy, graphql } from 'relay-runtime'; +import { FormikConfig } from 'formik'; +import { formatDate } from 'src/utils/Time'; +import { useLazyLoadQuery } from 'react-relay'; +import { Fab } from '@mui/material'; +import CreateRelationshipControlledDial from '@components/common/stix_core_relationships/CreateRelationshipControlledDial'; +import CreateRelationshipHeader from '@components/common/stix_core_relationships/CreateRelationshipHeader'; +import { TargetEntity } from '../stix_core_relationships/StixCoreRelationshipCreationFromEntity'; +import Drawer from '../drawer/Drawer'; +import { renderLoader } from '../stix_core_relationships/StixCoreRelationshipCreationFromControlledDial'; +import { stixNestedRefRelationshipCreationFromEntityMutation, stixNestedRefRelationshipResolveTypes } from './StixNestedRefRelationshipCreationFromEntity'; +import { StixNestedRefRelationshipCreationFromEntityResolveQuery$data } from './__generated__/StixNestedRefRelationshipCreationFromEntityResolveQuery.graphql'; +import { CreateRelationshipContext } from '../menus/CreateRelationshipContextProvider'; +import StixNestedRefRelationshipCreationFromEntityLines, { stixNestedRefRelationshipCreationFromEntityLinesQuery } from './StixNestedRefRelationshipCreationFromEntityLines'; +import { StixNestedRefRelationshipCreationFromEntityLinesQuery$data } from './__generated__/StixNestedRefRelationshipCreationFromEntityLinesQuery.graphql'; +import StixNestedRefRelationshipCreationForm, { StixNestedRefRelationshipCreationFormValues } from './StixNestedRefRelationshipCreationForm'; +import { StixNestedRefRelationshipCreationFromEntityFablessTargetTypesQuery } from './__generated__/StixNestedRefRelationshipCreationFromEntityFablessTargetTypesQuery.graphql'; + +const supportedTargetEntityTypes = graphql` + query StixNestedRefRelationshipCreationFromEntityFablessTargetTypesQuery( + $id: String! + ) { + stixNestedRefRelationshipFromEntityType(id: $id) + } +`; + +/** + * The first page of the create relationship drawer: selecting the entity/entites + * @param props.id The source entity's id + * @param props.entityType The source entity's type + * @param props.setTargetEntities Dispatch to set relationship target entities + * @param props.handleNextStep Function to continue on to the next step + * @param props.stixNestedRefTypes List of valid target entity types + * @returns JSX.Element + */ +const SelectEntity = ({ + id, + entityType, + setTargetEntities, + handleNextStep, + stixNestedRefTypes, +}: { + id: string, + entityType: string, + setTargetEntities: React.Dispatch, + handleNextStep: () => void, + stixNestedRefTypes: string[], +}) => { + const { t_i18n } = useFormatter(); + const [filters, helpers] = useFiltersState(emptyFilterGroup, emptyFilterGroup); + const [sortBy, setSortBy] = useState('_score'); + const [orderAsc, setOrderAsc] = useState(false); + const [numberOfElements, setNumberOfElements] = useState({ + number: 0, + symbol: '', + }); + const [searchTerm, setSearchTerm] = useState(''); + const containerRef = useRef(null); + const { + onToggleEntity, + selectedElements, + deSelectedElements, + } = useEntityToggle(`${id}_stixNestedRefRelationshipCreationFromEntity`); + const onInstanceToggleEntity = (entity: TargetEntity) => { + onToggleEntity(entity); + if (entity.id in (selectedElements || {})) { + const newSelectedElements = { ...selectedElements }; + delete newSelectedElements[entity.id]; + setTargetEntities(Object.values(newSelectedElements)); + } else { + setTargetEntities(Object.values({ + [entity.id]: entity, + ...(selectedElements ?? {}), + })); + } + }; + const searchPaginationOptions = { + search: searchTerm, + filters: removeIdFromFilterGroupObject(filters), + orderBy: sortBy, + orderMode: orderAsc ? 'asc' : 'desc', + types: stixNestedRefTypes, + }; + const handleSort = (field: string, sortOrderAsc: boolean) => { + setSortBy(field); + setOrderAsc(sortOrderAsc); + }; + const dataColumns = { + entity_type: { + label: 'Type', + width: '15%', + isSortable: true, + }, + value: { + label: 'Value', + width: '32%', + isSortable: false, + }, + createdBy: { + label: 'Author', + width: '15%', + isSortable: false, + }, + objectLabel: { + label: 'Labels', + width: '22%', + isSortable: false, + }, + objectMarking: { + label: 'Marking', + width: '15%', + isSortable: false, + }, + }; + return ( +
+ + {stixNestedRefTypes.length > 0 + ? ( + { + if (props) { + return ( + + ); + } return (<>); + }} + /> + ) : ( +
+ {t_i18n('No valid target entities')} +
+ ) + } +
+ handleNextStep()} + disabled={Object.values(selectedElements).length < 1} + style={{ + position: 'fixed', + bottom: 40, + right: 30, + zIndex: 1001, + }} + > + {t_i18n('Continue')} + +
+ ); +}; + +/** + * The second page of the create relationship drawer: filling out the relationship + * @param props.data The source entity + * @param props.targetEntities The target entities + * @param props.handleClose Function called on close + * @param props.isReversable Whether this relationship can be reversed + * @param props.defaultStartTime The default start time + * @param props.defaultStopTime The default stop time + * @returns JSX.Element + */ +const RenderForm = ({ + data, + targetEntities, + handleClose, + handleBack, + isReversable, + defaultStartTime, + defaultStopTime, +}: { + data: StixNestedRefRelationshipCreationFromEntityResolveQuery$data, + targetEntities: TargetEntity[], + handleClose: () => void, + handleBack: () => void, + isReversable?: boolean + defaultStartTime?: string, + defaultStopTime?: string, +}) => { + if (data?.stixSchemaRefRelationships === null || data?.stixSchemaRefRelationships === undefined) return <>; + const { state: { + reversed: initiallyReversed, + onCreate, + paginationOptions, + } } = useContext(CreateRelationshipContext); + const [reversed, setReversed] = useState(initiallyReversed ?? false); + + const handleReverse = () => setReversed(!reversed); + + const sourceEntity = data.stixSchemaRefRelationships.entity as TargetEntity; + let fromEntities = [sourceEntity]; + let toEntities = targetEntities; + if (reversed) { + fromEntities = targetEntities; + toEntities = [sourceEntity]; + } + let relationshipTypes: string[] = []; + if ((!data.stixSchemaRefRelationships.from + || data.stixSchemaRefRelationships.from.length === 0) + && (!data.stixSchemaRefRelationships.to + || data.stixSchemaRefRelationships.to.length !== 0)) { + if (reversed) { + relationshipTypes = data.stixSchemaRefRelationships.to as string[] ?? []; + } + } else { + relationshipTypes = data.stixSchemaRefRelationships.from as string[] ?? []; + } + const startTime = defaultStartTime ?? (new Date()).toISOString(); + const stopTime = defaultStopTime ?? (new Date()).toISOString(); + + const commit = (finalValues: object) => { + return new Promise((resolve, reject) => { + commitMutation({ + mutation: stixNestedRefRelationshipCreationFromEntityMutation, + variables: { input: finalValues }, + updater: (store: RecordSourceSelectorProxy) => { + if (typeof onCreate !== 'function') { + const payload = store.getRootField('stixRefRelationshipAdd'); + const container = store.getRoot(); + const userProxy = store.get(container.getDataID()); + if (userProxy != null && payload != null && paginationOptions != null) { + const newEdge = payload.setLinkedRecord(payload, 'node'); + const conn = ConnectionHandler.getConnection( + userProxy, + 'Pagination_stixNestedRefRelationships', + paginationOptions, + ); + if (conn != null) { + ConnectionHandler.insertEdgeBefore(conn, newEdge); + } + } + } + }, + optimisticUpdater: undefined, + setSubmitting: undefined, + optimisticResponse: undefined, + onError: (error: Error) => { + reject(error); + }, + onCompleted: (response: Response) => { + resolve(response); + }, + }); + }); + }; + + const onSubmit: FormikConfig['onSubmit'] = async (values, { setSubmitting, setErrors, resetForm }) => { + setSubmitting(true); + for (const targetEntity of targetEntities) { + const fromEntityId = reversed ? targetEntity.id : sourceEntity.id; + const toEntityId = reversed ? sourceEntity.id : targetEntity.id; + const finalValues = { + fromId: fromEntityId, + toId: toEntityId, + relationship_type: values.relationship_type, + start_time: formatDate(values.start_time), + stop_time: formatDate(values.stop_time), + }; + try { + // eslint-disable-next-line no-await-in-loop + await commit(finalValues); + } catch (error) { + setSubmitting(false); + return handleErrorInForm(error, setErrors); + } + } + setSubmitting(false); + resetForm(); + handleClose(); + if (typeof onCreate === 'function') { + onCreate(); + } + return true; + }; + + return ( + + ); +}; + +interface StixNestedRefRelationshipCreationFromEntityFablessProps { + id: string, + entityType: string, + isReversable?: boolean, + defaultStartTime?: string, + defaultStopTime?: string, + controlledDial?: ({ onOpen }: { onOpen: () => void }) => React.ReactElement, +} + +const StixNestedRefRelationshipCreationFromEntityFabless: FunctionComponent< +StixNestedRefRelationshipCreationFromEntityFablessProps +> = ({ + id, + entityType, + isReversable, + defaultStartTime, + defaultStopTime, + controlledDial, +}) => { + const [step, setStep] = useState(0); + const [targetEntities, setTargetEntities] = useState([]); + + const { stixNestedRefRelationshipFromEntityType } = useLazyLoadQuery(supportedTargetEntityTypes, { id }); + const stixNestedRefTypes: string[] = [...(stixNestedRefRelationshipFromEntityType ?? [])] as string[]; + + const reset = () => { + setStep(0); + setTargetEntities([]); + }; + + return ( + } + > +
+ {step === 0 && ( + setStep(1)} + stixNestedRefTypes={stixNestedRefTypes} + /> + )} + {step === 1 && ( + { + if (props?.stixSchemaRefRelationships) { + return ( + setStep(0)} + isReversable={isReversable} + defaultStartTime={defaultStartTime} + defaultStopTime={defaultStopTime} + /> + ); + } + return renderLoader(); + }} + /> + )} +
+
+ ); +}; + +export default StixNestedRefRelationshipCreationFromEntityFabless; diff --git a/opencti-platform/opencti-front/src/private/components/entities/events/Root.tsx b/opencti-platform/opencti-front/src/private/components/entities/events/Root.tsx index fb9c7285e473..4dc24a9efe2a 100644 --- a/opencti-platform/opencti-front/src/private/components/entities/events/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/entities/events/Root.tsx @@ -28,6 +28,8 @@ import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import EventEdition from './EventEdition'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootEventsSubscription($id: ID!) { @@ -100,7 +102,7 @@ const RootEvent = ({ eventId, queryRef }: RootEventProps) => { const link = `/dashboard/entities/events/${eventId}/knowledge`; const paddingRight = getPaddingRight(location.pathname, eventId, '/dashboard/entities/events'); return ( - <> + {event ? ( <> @@ -143,6 +145,7 @@ const RootEvent = ({ eventId, queryRef }: RootEventProps) => { )} + RelateComponent={CreateRelationshipButtonComponent} /> { ) : ( )} - + ); }; const Root = () => { diff --git a/opencti-platform/opencti-front/src/private/components/entities/individuals/Root.tsx b/opencti-platform/opencti-front/src/private/components/entities/individuals/Root.tsx index debe73c7df08..4aab87ae7db8 100644 --- a/opencti-platform/opencti-front/src/private/components/entities/individuals/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/entities/individuals/Root.tsx @@ -30,6 +30,8 @@ import IndividualEdition from './IndividualEdition'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootIndividualsSubscription($id: ID!) { @@ -133,7 +135,7 @@ const RootIndividual = ({ individualId, queryRef }: RootIndividualProps) => { } return ( - <> + {individual ? ( <> @@ -178,6 +180,7 @@ const RootIndividual = ({ individualId, queryRef }: RootIndividualProps) => { )} + RelateComponent={CreateRelationshipButtonComponent} onViewAs={handleChangeViewAs} viewAs={viewAs} /> @@ -328,7 +331,7 @@ const RootIndividual = ({ individualId, queryRef }: RootIndividualProps) => { ) : ( )} - + ); }; const Root = () => { diff --git a/opencti-platform/opencti-front/src/private/components/entities/organizations/Root.tsx b/opencti-platform/opencti-front/src/private/components/entities/organizations/Root.tsx index b2f3930f70df..9b6dd3d7cd5e 100644 --- a/opencti-platform/opencti-front/src/private/components/entities/organizations/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/entities/organizations/Root.tsx @@ -30,6 +30,8 @@ import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import OrganizationEdition from './OrganizationEdition'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootOrganizationSubscription($id: ID!) { @@ -127,7 +129,7 @@ const RootOrganization = ({ organizationId, queryRef }: RootOrganizationProps) = const link = `/dashboard/entities/organizations/${organizationId}/knowledge`; const paddingRight = getPaddingRight(location.pathname, organizationId, '/dashboard/entities/organizations', viewAs === 'knowledge'); return ( - <> + {organization ? ( <> @@ -177,6 +179,7 @@ const RootOrganization = ({ organizationId, queryRef }: RootOrganizationProps) = )} + RelateComponent={CreateRelationshipButtonComponent} onViewAs={handleChangeViewAs} viewAs={viewAs} /> @@ -328,7 +331,7 @@ const RootOrganization = ({ organizationId, queryRef }: RootOrganizationProps) = ) : ( )} - + ); }; const Root = () => { diff --git a/opencti-platform/opencti-front/src/private/components/entities/sectors/Root.tsx b/opencti-platform/opencti-front/src/private/components/entities/sectors/Root.tsx index c6b16fdb561f..683110213ed2 100644 --- a/opencti-platform/opencti-front/src/private/components/entities/sectors/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/entities/sectors/Root.tsx @@ -28,6 +28,8 @@ import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import SectorEdition from './SectorEdition'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootSectorSubscription($id: ID!) { @@ -102,7 +104,7 @@ const RootSector = ({ sectorId, queryRef }: RootSectorProps) => { const paddingRight = getPaddingRight(location.pathname, sectorId, '/dashboard/entities/sectors'); const link = `/dashboard/entities/sectors/${sectorId}/knowledge`; return ( - <> + {sector ? ( <> @@ -147,6 +149,7 @@ const RootSector = ({ sectorId, queryRef }: RootSectorProps) => { )} + RelateComponent={CreateRelationshipButtonComponent} /> { ) : ( )} - + ); }; const Root = () => { diff --git a/opencti-platform/opencti-front/src/private/components/entities/systems/Root.tsx b/opencti-platform/opencti-front/src/private/components/entities/systems/Root.tsx index c677422eccbd..69b80b99bcd0 100644 --- a/opencti-platform/opencti-front/src/private/components/entities/systems/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/entities/systems/Root.tsx @@ -30,6 +30,8 @@ import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import SystemEdition from './SystemEdition'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootSystemsSubscription($id: ID!) { @@ -125,7 +127,7 @@ const RootSystem = ({ systemId, queryRef }: RootSystemProps) => { const link = `/dashboard/entities/systems/${systemId}/knowledge`; const paddingRight = getPaddingRight(location.pathname, systemId, '/dashboard/entities/systems'); return ( - <> + {system ? ( <> @@ -171,6 +173,7 @@ const RootSystem = ({ systemId, queryRef }: RootSystemProps) => { )} + RelateComponent={CreateRelationshipButtonComponent} onViewAs={handleChangeViewAs} viewAs={viewAs} /> @@ -321,7 +324,7 @@ const RootSystem = ({ systemId, queryRef }: RootSystemProps) => { ) : ( )} - + ); }; const Root = () => { diff --git a/opencti-platform/opencti-front/src/private/components/events/incidents/Root.tsx b/opencti-platform/opencti-front/src/private/components/events/incidents/Root.tsx index 73be4d747626..6b3c44fc8863 100644 --- a/opencti-platform/opencti-front/src/private/components/events/incidents/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/events/incidents/Root.tsx @@ -31,6 +31,8 @@ import Breadcrumbs from '../../../../components/Breadcrumbs'; import { getCurrentTab } from '../../../../utils/utils'; import IncidentEdition from './IncidentEdition'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootIncidentSubscription($id: ID!) { @@ -103,7 +105,7 @@ const RootIncidentComponent = ({ queryRef }) => { return 0; }; return ( - <> + {incident ? ( <> @@ -146,6 +148,7 @@ const RootIncidentComponent = ({ queryRef }) => { )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} /> { ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/locations/administrative_areas/Root.tsx b/opencti-platform/opencti-front/src/private/components/locations/administrative_areas/Root.tsx index 51bf20ded588..f2fbecea1ad6 100644 --- a/opencti-platform/opencti-front/src/private/components/locations/administrative_areas/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/locations/administrative_areas/Root.tsx @@ -31,6 +31,8 @@ import AdministrativeAreaEdition from './AdministrativeAreaEdition'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootAdministrativeAreasSubscription($id: ID!) { @@ -96,7 +98,7 @@ const RootAdministrativeAreaComponent = ({ queryRef, administrativeAreaId }) => const link = `/dashboard/locations/administrative_areas/${administrativeAreaId}/knowledge`; const paddingRight = getPaddingRight(location.pathname, administrativeArea?.id, '/dashboard/locations/administrative_areas'); return ( - <> + {administrativeArea ? ( <> @@ -146,6 +148,7 @@ const RootAdministrativeAreaComponent = ({ queryRef, administrativeAreaId }) => /> )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} isOpenctiAlias={true} /> @@ -272,7 +275,7 @@ const RootAdministrativeAreaComponent = ({ queryRef, administrativeAreaId }) => ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/locations/cities/Root.tsx b/opencti-platform/opencti-front/src/private/components/locations/cities/Root.tsx index 5e42b25efffc..ad45ac44926c 100644 --- a/opencti-platform/opencti-front/src/private/components/locations/cities/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/locations/cities/Root.tsx @@ -31,6 +31,8 @@ import CityEdition from './CityEdition'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootCitiesSubscription($id: ID!) { @@ -94,7 +96,7 @@ const RootCityComponent = ({ queryRef, cityId }) => { const link = `/dashboard/locations/cities/${cityId}/knowledge`; const paddingRight = getPaddingRight(location.pathname, city?.id, '/dashboard/locations/cities'); return ( - <> + {city ? ( <> @@ -140,6 +142,7 @@ const RootCityComponent = ({ queryRef, cityId }) => { )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} isOpenctiAlias={true} /> @@ -264,7 +267,7 @@ const RootCityComponent = ({ queryRef, cityId }) => { ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/locations/countries/Root.tsx b/opencti-platform/opencti-front/src/private/components/locations/countries/Root.tsx index 320cb27979ac..3617b9300ba4 100644 --- a/opencti-platform/opencti-front/src/private/components/locations/countries/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/locations/countries/Root.tsx @@ -31,6 +31,8 @@ import CountryEdition from './CountryEdition'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootCountriesSubscription($id: ID!) { @@ -96,7 +98,7 @@ const RootCountryComponent = ({ queryRef, countryId }) => { const link = `/dashboard/locations/countries/${countryId}/knowledge`; const paddingRight = getPaddingRight(location.pathname, country?.id, '/dashboard/locations/countries'); return ( - <> + {country ? ( <> @@ -142,6 +144,7 @@ const RootCountryComponent = ({ queryRef, countryId }) => { )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} isOpenctiAlias={true} /> @@ -268,7 +271,7 @@ const RootCountryComponent = ({ queryRef, countryId }) => { ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/locations/positions/Root.tsx b/opencti-platform/opencti-front/src/private/components/locations/positions/Root.tsx index 1813fde0eb13..c4a2036218e2 100644 --- a/opencti-platform/opencti-front/src/private/components/locations/positions/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/locations/positions/Root.tsx @@ -28,6 +28,8 @@ import PositionEdition from './PositionEdition'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootPositionsSubscription($id: ID!) { @@ -101,7 +103,7 @@ const RootPosition = ({ positionId, queryRef }: RootPositionProps) => { const paddingRight = getPaddingRight(location.pathname, positionId, '/dashboard/locations/positions'); return ( - <> + {position ? ( <> @@ -147,6 +149,7 @@ const RootPosition = ({ positionId, queryRef }: RootPositionProps) => { )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} isOpenctiAlias={true} /> @@ -283,7 +286,7 @@ const RootPosition = ({ positionId, queryRef }: RootPositionProps) => { ) : ( )} - + ); }; const Root = () => { diff --git a/opencti-platform/opencti-front/src/private/components/locations/regions/Root.tsx b/opencti-platform/opencti-front/src/private/components/locations/regions/Root.tsx index 7fc4738d2d11..e03f53e5dd8c 100644 --- a/opencti-platform/opencti-front/src/private/components/locations/regions/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/locations/regions/Root.tsx @@ -32,6 +32,8 @@ import RegionEdition from './RegionEdition'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootRegionsSubscription($id: ID!) { @@ -97,7 +99,7 @@ const RootRegionComponent = ({ queryRef, regionId }) => { const link = `/dashboard/locations/regions/${regionId}/knowledge`; const paddingRight = getPaddingRight(location.pathname, region?.id, '/dashboard/locations/regions'); return ( - <> + {region ? ( <> @@ -144,6 +146,7 @@ const RootRegionComponent = ({ queryRef, regionId }) => { )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} isOpenctiAlias={true} /> @@ -268,7 +271,7 @@ const RootRegionComponent = ({ queryRef, regionId }) => { ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/observations/infrastructures/Root.tsx b/opencti-platform/opencti-front/src/private/components/observations/infrastructures/Root.tsx index 29a182acb00a..b6e310fdfb99 100644 --- a/opencti-platform/opencti-front/src/private/components/observations/infrastructures/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/observations/infrastructures/Root.tsx @@ -30,6 +30,8 @@ import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import InfrastructureEdition from './InfrastructureEdition'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootInfrastructureSubscription($id: ID!) { @@ -94,7 +96,7 @@ const RootInfrastructureComponent = ({ queryRef, infrastructureId }) => { return 0; }; return ( - <> + {infrastructure ? (
{ )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} /> { ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/Root.tsx b/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/Root.tsx index d385fbdb0152..3cb931bc19f7 100644 --- a/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/Root.tsx @@ -24,6 +24,7 @@ import FileManager from '../../common/files/FileManager'; import { useFormatter } from '../../../../components/i18n'; import Breadcrumbs from '../../../../components/Breadcrumbs'; import { getCurrentTab, getPaddingRight } from '../../../../utils/utils'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; const subscription = graphql` subscription RootStixCyberObservableSubscription($id: ID!) { @@ -93,7 +94,7 @@ const RootStixCyberObservable = ({ observableId, queryRef }: RootStixCyberObserv const link = `/dashboard/observations/observables/${observableId}/knowledge`; return ( - <> + {stixCyberObservable ? (
)} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableEntities.jsx b/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableEntities.jsx deleted file mode 100644 index ea953d5a5c5d..000000000000 --- a/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableEntities.jsx +++ /dev/null @@ -1,307 +0,0 @@ -import React, { Component } from 'react'; -import * as PropTypes from 'prop-types'; -import { compose } from 'ramda'; -import withStyles from '@mui/styles/withStyles'; -import Typography from '@mui/material/Typography'; -import Paper from '@mui/material/Paper'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; -import { ArrowDropDown, ArrowDropUp } from '@mui/icons-material'; -import { QueryRenderer } from '../../../../relay/environment'; -import inject18n from '../../../../components/i18n'; -import StixCyberObservableEntitiesLines, { stixCyberObservableEntitiesLinesQuery } from './StixCyberObservableEntitiesLines'; -import StixCoreRelationshipCreationFromEntity from '../../common/stix_core_relationships/StixCoreRelationshipCreationFromEntity'; -import Security from '../../../../utils/Security'; -import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; -import SearchInput from '../../../../components/SearchInput'; - -const styles = (theme) => ({ - paper: { - margin: 0, - padding: 15, - borderRadius: 4, - }, - item: { - paddingLeft: 10, - height: 50, - }, - itemIcon: { - color: theme.palette.primary.main, - }, - itemHead: { - paddingLeft: 10, - textTransform: 'uppercase', - }, - bodyItem: { - height: 20, - fontSize: 13, - float: 'left', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - goIcon: { - position: 'absolute', - right: -10, - }, - itemIconDisabled: { - color: theme.palette.grey[700], - }, - placeholder: { - display: 'inline-block', - height: '1em', - backgroundColor: theme.palette.grey[700], - }, -}); - -const inlineStylesHeaders = { - iconSort: { - position: 'absolute', - margin: '0 0 0 5px', - padding: 0, - top: '0px', - }, - relationship_type: { - float: 'left', - width: '10%', - fontSize: 12, - fontWeight: '700', - cursor: 'pointer', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - entity_tyoe: { - float: 'left', - width: '10%', - fontSize: 12, - fontWeight: '700', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - name: { - float: 'left', - width: '22%', - fontSize: 12, - fontWeight: '700', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - createdBy: { - float: 'left', - width: '12%', - fontSize: 12, - fontWeight: '700', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - creator: { - float: 'left', - width: '12%', - fontSize: 12, - fontWeight: '700', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - start_time: { - float: 'left', - width: '10%', - fontSize: 12, - fontWeight: '700', - cursor: 'pointer', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - stop_time: { - float: 'left', - width: '10%', - fontSize: 12, - fontWeight: '700', - cursor: 'pointer', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - confidence: { - float: 'left', - width: '12%', - fontSize: 12, - fontWeight: '700', - cursor: 'pointer', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, -}; - -class StixCyberObservableEntities extends Component { - constructor(props) { - super(props); - this.state = { - sortBy: null, - orderAsc: false, - searchTerm: '', - view: 'lines', - relationReversed: false, - }; - } - - handleReverseRelation() { - this.setState({ relationReversed: !this.state.relationReversed }); - } - - handleSort(field, orderAsc) { - this.setState({ sortBy: field, orderAsc }); - } - - handleSearch(value) { - this.setState({ searchTerm: value }); - } - - SortHeader(field, label, isSortable) { - const { t } = this.props; - const sortComponent = this.state.orderAsc ? ( - - ) : ( - - ); - if (isSortable) { - return ( -
- {t(label)} - {this.state.sortBy === field ? sortComponent : ''} -
- ); - } - return ( -
- {t(label)} -
- ); - } - - render() { - const { sortBy, orderAsc, searchTerm, relationReversed } = this.state; - const { classes, t, entityId, defaultStartTime, defaultStopTime } = this.props; - const paginationOptions = { - fromOrToId: entityId, - search: searchTerm, - orderBy: sortBy, - orderMode: orderAsc ? 'asc' : 'desc', - }; - return ( -
- - {t('Relations')} - - } - > - - -
- -
-
- - - - - -   - - - - {this.SortHeader('relationship_type', 'Relationship', true)} - {this.SortHeader('entity_tyoe', 'Entity type', false)} - {this.SortHeader('name', 'Name', false)} - {this.SortHeader('createdBy', 'Author', false)} - {this.SortHeader('creator', 'Creator', false)} - {this.SortHeader('start_time', 'First obs.', true)} - {this.SortHeader('stop_time', 'Last obs.', true)} - {this.SortHeader('confidence', 'Confidence level', true)} -
- } - /> -   - - ( - - )} - /> - - -
- ); - } -} - -StixCyberObservableEntities.propTypes = { - entityId: PropTypes.string, - relationship_type: PropTypes.string, - classes: PropTypes.object, - t: PropTypes.func, - navigate: PropTypes.func, - defaultStartTime: PropTypes.string, - defaultStopTime: PropTypes.string, -}; - -export default compose( - inject18n, - withStyles(styles), -)(StixCyberObservableEntities); diff --git a/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableEntities.tsx b/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableEntities.tsx new file mode 100644 index 000000000000..3df091683d54 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableEntities.tsx @@ -0,0 +1,212 @@ +import React, { FunctionComponent, useContext, useEffect, useState } from 'react'; +import { Add, ArrowDropDown, ArrowDropUp } from '@mui/icons-material'; +import { IconButton, List, ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText, Paper, Typography } from '@mui/material'; +import { useFormatter } from '../../../../components/i18n'; +import { CreateRelationshipContext } from '../../common/menus/CreateRelationshipContextProvider'; +import useHelper from '../../../../utils/hooks/useHelper'; +import Security from '../../../../utils/Security'; +import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; +import StixCoreRelationshipCreationFromControlledDial from '../../common/stix_core_relationships/StixCoreRelationshipCreationFromControlledDial'; +import StixCoreRelationshipCreationFromEntity from '../../common/stix_core_relationships/StixCoreRelationshipCreationFromEntity'; +import SearchInput from '../../../../components/SearchInput'; +import { QueryRenderer } from '../../../../relay/environment'; +import StixCyberObservableEntitiesLines, { stixCyberObservableEntitiesLinesQuery } from './StixCyberObservableEntitiesLines'; +import { StixCyberObservableEntitiesLinesPaginationQuery$data } from './__generated__/StixCyberObservableEntitiesLinesPaginationQuery.graphql'; + +interface StixCyberObservableEntitiesProps { + entityId: string; + defaultStartTime: string; + defaultStopTime: string; +} + +const StixCyberObservableEntities: FunctionComponent = ({ + entityId, + defaultStartTime, + defaultStopTime, +}) => { + const { t_i18n } = useFormatter(); + const { setState } = useContext(CreateRelationshipContext); + const { isFeatureEnable } = useHelper(); + const isFABReplaced = isFeatureEnable('FAB_REPLACEMENT'); + + const [sortBy, setSortBy] = useState(); + const [orderAsc, setOrderAsc] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [relationReversed, setRelationReversed] = useState(false); + + const handleSort = (field: string) => { + setSortBy(field); + setOrderAsc(!orderAsc); + }; + const handleSearch = (term: string) => setSearchTerm(term); + const handleReverseRelation = () => setRelationReversed(!relationReversed); + const sortHeader = (field: string, label: string, isSortable: boolean) => { + const fieldWidths: Record = { + relationship_type: '10%', + entity_type: '10%', + name: '22%', + createdBy: '12%', + creator: '12%', + start_time: '10%', + stop_time: '10%', + confidence: '12%', + }; + const SortComponentStyles: React.CSSProperties = { + position: 'absolute', + margin: '0 0 0 5px', + padding: 0, + top: '0px', + }; + const SortComponent = orderAsc + ? () + : (); + return ( +
handleSort(field) : undefined} + > + {t_i18n(label)} + {sortBy === field ? SortComponent : ''} +
+ ); + }; + + const paginationOptions = { + fromOrToId: entityId, + search: searchTerm, + orderBy: sortBy, + orderMode: orderAsc ? 'asc' : 'desc', + }; + + useEffect(() => setState({ + paginationOptions, + }), []); + + return ( +
+ + {t_i18n('Relations')} + + } + > + {isFABReplaced + ? ( + { + return ( + + + + ); + }} + /> + ) + : ( + + ) + } + +
+ +
+
+ + + + +   + + + {sortHeader('relationship_type', 'Relationship', true)} + {sortHeader('entity_type', 'Entity Type', false)} + {sortHeader('name', 'Name', false)} + {sortHeader('createdBy', 'Author', false)} + {sortHeader('creator', 'Creator', false)} + {sortHeader('start_time', 'First obs.', true)} + {sortHeader('stop_time', 'Last obs.', true)} + {sortHeader('confidence', 'Confidence level', true)} +
} + /> +   + + ( + + )} + /> + + +
+ ); +}; + +export default StixCyberObservableEntities; diff --git a/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableNestedEntities.jsx b/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableNestedEntities.jsx deleted file mode 100644 index d3c756a6c969..000000000000 --- a/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableNestedEntities.jsx +++ /dev/null @@ -1,273 +0,0 @@ -import React, { Component } from 'react'; -import * as PropTypes from 'prop-types'; -import { compose } from 'ramda'; -import withStyles from '@mui/styles/withStyles'; -import Typography from '@mui/material/Typography'; -import Paper from '@mui/material/Paper'; -import { ArrowDropDown, ArrowDropUp } from '@mui/icons-material'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; -import inject18n from '../../../../components/i18n'; -import StixNestedRefRelationCreationFromEntity from '../../common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntity'; -import Security from '../../../../utils/Security'; -import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; -import SearchInput from '../../../../components/SearchInput'; -import { QueryRenderer } from '../../../../relay/environment'; -import StixCyberObservableNestedEntitiesLines, { stixCyberObservableNestedEntitiesLinesQuery } from './StixCyberObservableNestedEntitiesLines'; - -const styles = (theme) => ({ - paper: { - margin: 0, - padding: 15, - borderRadius: 4, - }, - item: { - paddingLeft: 10, - height: 50, - }, - itemIcon: { - color: theme.palette.primary.main, - }, - itemHead: { - paddingLeft: 10, - textTransform: 'uppercase', - }, - bodyItem: { - height: 20, - fontSize: 13, - float: 'left', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - goIcon: { - position: 'absolute', - right: -10, - }, - itemIconDisabled: { - color: theme.palette.grey[700], - }, - placeholder: { - display: 'inline-block', - height: '1em', - backgroundColor: theme.palette.grey[700], - }, -}); - -const inlineStylesHeaders = { - iconSort: { - position: 'absolute', - margin: '0 0 0 5px', - padding: 0, - top: '0px', - }, - relationship_type: { - float: 'left', - width: '10%', - fontSize: 12, - fontWeight: '700', - cursor: 'pointer', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - entity_type: { - float: 'left', - width: '10%', - fontSize: 12, - fontWeight: '700', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - name: { - float: 'left', - width: '22%', - fontSize: 12, - fontWeight: '700', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - creator: { - float: 'left', - width: '12%', - fontSize: 12, - fontWeight: '700', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - start_time: { - float: 'left', - width: '15%', - fontSize: 12, - fontWeight: '700', - cursor: 'pointer', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - stop_time: { - float: 'left', - width: '15%', - fontSize: 12, - fontWeight: '700', - cursor: 'pointer', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, -}; - -class StixCyberObservableNestedEntities extends Component { - constructor(props) { - super(props); - this.state = { - sortBy: null, - orderAsc: false, - searchTerm: '', - view: 'lines', - relationReversed: false, - }; - } - - handleSort(field, orderAsc) { - this.setState({ sortBy: field, orderAsc }); - } - - handleSearch(value) { - this.setState({ searchTerm: value }); - } - - SortHeader(field, label, isSortable) { - const { t } = this.props; - const sortComponent = this.state.orderAsc ? ( - - ) : ( - - ); - if (isSortable) { - return ( -
- {t(label)} - {this.state.sortBy === field ? sortComponent : ''} -
- ); - } - return ( -
- {t(label)} -
- ); - } - - render() { - const { searchTerm, sortBy, orderAsc } = this.state; - const { entityId, t, entityType, classes } = this.props; - const paginationOptions = { - fromOrToId: entityId, - search: searchTerm, - orderBy: sortBy, - orderMode: orderAsc ? 'asc' : 'desc', - }; - return ( -
- - {t('Nested objects')} - - } - > - - -
- -
-
- - - - - -   - - - - {this.SortHeader('relationship_type', 'Attribute', true)} - {this.SortHeader('entity_type', 'Entity type', false)} - {this.SortHeader('name', 'Name', false)} - {this.SortHeader('creator', 'Creator', false)} - {this.SortHeader('start_time', 'First obs.', true)} - {this.SortHeader('stop_time', 'Last obs.', true)} -
- } - /> -   - - ( - - )} - /> - - -
- ); - } -} - -StixCyberObservableNestedEntities.propTypes = { - entityId: PropTypes.string, - entityType: PropTypes.string, - paginationOptions: PropTypes.object, - classes: PropTypes.object, - t: PropTypes.func, - navigate: PropTypes.func, -}; - -export default compose( - inject18n, - withStyles(styles), -)(StixCyberObservableNestedEntities); diff --git a/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableNestedEntities.tsx b/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableNestedEntities.tsx new file mode 100644 index 000000000000..0dcca1719319 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/observations/stix_cyber_observables/StixCyberObservableNestedEntities.tsx @@ -0,0 +1,206 @@ +import React, { FunctionComponent, useContext, useEffect, useState } from 'react'; +import { Add, ArrowDropDown, ArrowDropUp } from '@mui/icons-material'; +import { IconButton, List, ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText, Paper, Typography } from '@mui/material'; +import { useFormatter } from '../../../../components/i18n'; +import { CreateRelationshipContext } from '../../common/menus/CreateRelationshipContextProvider'; +import useHelper from '../../../../utils/hooks/useHelper'; +import Security from '../../../../utils/Security'; +import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; +import StixNestedRefRelationshipCreationFromEntity from '../../common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntity'; +import SearchInput from '../../../../components/SearchInput'; +import { QueryRenderer } from '../../../../relay/environment'; +import StixCyberObservableNestedEntitiesLines, { stixCyberObservableNestedEntitiesLinesQuery } from './StixCyberObservableNestedEntitiesLines'; +import { StixCyberObservableNestedEntitiesLines_data$data } from './__generated__/StixCyberObservableNestedEntitiesLines_data.graphql'; +import StixNestedRefRelationshipCreationFromEntityFabless from '../../common/stix_nested_ref_relationships/StixNestedRefRelationshipCreationFromEntityFabless'; + +interface StixCyberObservableNestedEntitiesProps { + entityId: string, + entityType: string, + targetStixCoreObjectTypes: string[], +} + +const StixCyberObservableNestedEntities: FunctionComponent< +StixCyberObservableNestedEntitiesProps +> = ({ + entityId, + entityType, + targetStixCoreObjectTypes, +}) => { + const { t_i18n } = useFormatter(); + const { setState } = useContext(CreateRelationshipContext); + const { isFeatureEnable } = useHelper(); + const isFABReplaced = isFeatureEnable('FAB_REPLACEMENT'); + const [sortBy, setSortBy] = useState(); + const [orderAsc, setOrderAsc] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + + const handleSort = (field: string) => { + setSortBy(field); + setOrderAsc(!orderAsc); + }; + const handleSearch = (term: string) => setSearchTerm(term); + + const sortHeader = (field: string, label: string, isSortable: boolean) => { + const fieldWidths: Record = { + relationship_type: '10%', + entity_type: '10%', + name: '22%', + creator: '12%', + start_time: '15%', + stop_time: '15%', + }; + const SortComponentStyles: React.CSSProperties = { + position: 'absolute', + margin: '0 0 0 5px', + padding: 0, + top: '0px', + }; + const SortComponent = orderAsc + ? () + : (); + return ( +
handleSort(field) : undefined} + > + {t_i18n(label)} + {sortBy === field ? SortComponent : ''} +
+ ); + }; + + const paginationOptions = { + fromOrToId: entityId, + search: searchTerm, + orderBy: sortBy, + orderMode: orderAsc ? 'asc' : 'desc', + }; + + useEffect(() => setState({ + paginationOptions, + }), []); + + return ( +
+ + {t_i18n('Nested objects')} + + } + > + {isFABReplaced + ? ( + { + return ( + + + + ); + }} + /> + ) + : ( + + ) + } + +
+ +
+
+ + + + + +   + + + + {sortHeader('relationship_type', 'Attribute', true)} + {sortHeader('entity_type', 'Entity type', false)} + {sortHeader('name', 'Name', false)} + {sortHeader('creator', 'Creator', false)} + {sortHeader('start_time', 'First obs.', true)} + {sortHeader('stop_time', 'Last obs.', true)} +
+ } + /> +   + + ( + + )} + /> + + +
+ ); +}; + +export default StixCyberObservableNestedEntities; diff --git a/opencti-platform/opencti-front/src/private/components/techniques/attack_patterns/Root.tsx b/opencti-platform/opencti-front/src/private/components/techniques/attack_patterns/Root.tsx index cdd2c8de50f9..001a4dea803a 100644 --- a/opencti-platform/opencti-front/src/private/components/techniques/attack_patterns/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/techniques/attack_patterns/Root.tsx @@ -27,6 +27,8 @@ import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import AttackPatternEdition from './AttackPatternEdition'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootAttackPatternSubscription($id: ID!) { @@ -101,7 +103,7 @@ const RootAttackPattern = ({ attackPatternId, queryRef }: RootAttackPatternProps const link = `/dashboard/techniques/attack_patterns/${attackPatternId}/knowledge`; return ( - <> + {attackPattern ? ( <> @@ -143,6 +145,7 @@ const RootAttackPattern = ({ attackPatternId, queryRef }: RootAttackPatternProps )} + RelateComponent={CreateRelationshipButtonComponent} /> )} - + ); }; const Root = () => { diff --git a/opencti-platform/opencti-front/src/private/components/techniques/courses_of_action/Root.jsx b/opencti-platform/opencti-front/src/private/components/techniques/courses_of_action/Root.jsx index b301fd9fc532..f2cb62647df9 100644 --- a/opencti-platform/opencti-front/src/private/components/techniques/courses_of_action/Root.jsx +++ b/opencti-platform/opencti-front/src/private/components/techniques/courses_of_action/Root.jsx @@ -23,6 +23,8 @@ import { getCurrentTab, getPaddingRight } from '../../../../utils/utils'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import CourseOfActionEdition from './CourseOfActionEdition'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootCoursesOfActionSubscription($id: ID!) { @@ -87,7 +89,7 @@ class RootCourseOfAction extends Component { } = this.props; return ( -
+ )} + RelateComponent={CreateRelationshipButtonComponent} isOpenctiAlias={true} /> ; }} /> -
+ ); } } diff --git a/opencti-platform/opencti-front/src/private/components/techniques/narratives/Root.tsx b/opencti-platform/opencti-front/src/private/components/techniques/narratives/Root.tsx index fea04a408c2b..5398a6080dec 100644 --- a/opencti-platform/opencti-front/src/private/components/techniques/narratives/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/techniques/narratives/Root.tsx @@ -27,6 +27,8 @@ import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import NarrativeEdition from './NarrativeEdition'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootNarrativeSubscription($id: ID!) { @@ -100,7 +102,7 @@ const RootNarrative = ({ narrativeId, queryRef }: RootNarrativeProps) => { const paddingRight = getPaddingRight(location.pathname, narrativeId, '/dashboard/techniques/narratives'); const link = `/dashboard/techniques/narratives/${narrativeId}/knowledge`; return ( - <> + {narrative ? ( <> @@ -139,6 +141,7 @@ const RootNarrative = ({ narrativeId, queryRef }: RootNarrativeProps) => { )} + RelateComponent={CreateRelationshipButtonComponent} /> { ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/threats/campaigns/Root.tsx b/opencti-platform/opencti-front/src/private/components/threats/campaigns/Root.tsx index de6997f02870..dda8e9fbfd89 100644 --- a/opencti-platform/opencti-front/src/private/components/threats/campaigns/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/threats/campaigns/Root.tsx @@ -28,6 +28,8 @@ import useHelper from '../../../../utils/hooks/useHelper'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import CampaignEdition from './CampaignEdition'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootCampaignSubscription($id: ID!) { @@ -103,7 +105,7 @@ const RootCampaign = ({ campaignId, queryRef }: RootCampaignProps) => { const isOverview = location.pathname === `/dashboard/threats/campaigns/${campaignId}`; const paddingRight = getPaddingRight(location.pathname, campaignId, '/dashboard/threats/campaigns'); return ( - <> + {campaign ? ( <> @@ -149,6 +151,7 @@ const RootCampaign = ({ campaignId, queryRef }: RootCampaignProps) => { )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} /> { ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/threats/intrusion_sets/Root.tsx b/opencti-platform/opencti-front/src/private/components/threats/intrusion_sets/Root.tsx index f149b9ee75ab..879eeeadd0e4 100644 --- a/opencti-platform/opencti-front/src/private/components/threats/intrusion_sets/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/threats/intrusion_sets/Root.tsx @@ -28,6 +28,8 @@ import useHelper from '../../../../utils/hooks/useHelper'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import IntrusionSetEdition from './IntrusionSetEdition'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootIntrusionSetSubscription($id: ID!) { @@ -108,7 +110,7 @@ const RootIntrusionSet = ({ intrusionSetId, queryRef }: RootIntrusionSetProps) = const paddingRight = getPaddingRight(location.pathname, intrusionSetId, '/dashboard/threats/intrusion_sets'); const link = `/dashboard/threats/intrusion_sets/${intrusionSetId}/knowledge`; return ( - <> + {intrusionSet ? ( <> @@ -155,6 +157,7 @@ const RootIntrusionSet = ({ intrusionSetId, queryRef }: RootIntrusionSetProps) = )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} enableAskAi={true} /> @@ -270,7 +273,7 @@ const RootIntrusionSet = ({ intrusionSetId, queryRef }: RootIntrusionSetProps) = ) : ( )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/threats/threat_actors_group/Root.tsx b/opencti-platform/opencti-front/src/private/components/threats/threat_actors_group/Root.tsx index d36ba7d8ff33..aad90d3bf00f 100644 --- a/opencti-platform/opencti-front/src/private/components/threats/threat_actors_group/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/threats/threat_actors_group/Root.tsx @@ -28,6 +28,8 @@ import useHelper from '../../../../utils/hooks/useHelper'; import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import ThreatActorGroupEdition from './ThreatActorGroupEdition'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootThreatActorsGroupSubscription($id: ID!) { @@ -105,7 +107,7 @@ const RootThreatActorGroup = ({ queryRef, threatActorGroupId }: RootThreatActorG const paddingRight = getPaddingRight(location.pathname, threatActorGroupId, '/dashboard/threats/threat_actors_group'); const link = `/dashboard/threats/threat_actors_group/${threatActorGroupId}/knowledge`; return ( - <> + {threatActorGroup ? ( <> @@ -153,6 +155,7 @@ const RootThreatActorGroup = ({ queryRef, threatActorGroupId }: RootThreatActorG )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} /> )} - + ); }; diff --git a/opencti-platform/opencti-front/src/private/components/threats/threat_actors_individual/Root.tsx b/opencti-platform/opencti-front/src/private/components/threats/threat_actors_individual/Root.tsx index 3be9eec96fc5..88798ce4b788 100644 --- a/opencti-platform/opencti-front/src/private/components/threats/threat_actors_individual/Root.tsx +++ b/opencti-platform/opencti-front/src/private/components/threats/threat_actors_individual/Root.tsx @@ -28,6 +28,8 @@ import Security from '../../../../utils/Security'; import { KNOWLEDGE_KNUPDATE } from '../../../../utils/hooks/useGranted'; import ThreatActorIndividualEdition from './ThreatActorIndividualEdition'; import useHelper from '../../../../utils/hooks/useHelper'; +import CreateRelationshipContextProvider from '../../common/menus/CreateRelationshipContextProvider'; +import CreateRelationshipButtonComponent from '../../common/menus/CreateRelationshipButtonComponent'; const subscription = graphql` subscription RootThreatActorIndividualSubscription($id: ID!) { @@ -116,7 +118,7 @@ const RootThreatActorIndividualComponent = ({ const paddingRight = getPaddingRight(location.pathname, threatActorIndividualId, '/dashboard/threats/threat_actors_individual'); const link = `/dashboard/threats/threat_actors_individual/${threatActorIndividualId}/knowledge`; return ( - <> + {threatActorIndividual ? ( <> @@ -167,6 +169,7 @@ const RootThreatActorIndividualComponent = ({ /> )} + RelateComponent={CreateRelationshipButtonComponent} enableQuickSubscription={true} /> )} - + ); }; diff --git a/opencti-platform/opencti-front/src/schema/relay.schema.graphql b/opencti-platform/opencti-front/src/schema/relay.schema.graphql index 9c52eaad24dc..2e3153c65ea3 100644 --- a/opencti-platform/opencti-front/src/schema/relay.schema.graphql +++ b/opencti-platform/opencti-front/src/schema/relay.schema.graphql @@ -7783,6 +7783,7 @@ type Query { stixRefRelationship(id: String): StixRefRelationship stixRefRelationships(first: Int, after: ID, orderBy: StixRefRelationshipsOrdering, orderMode: OrderingMode, fromOrToId: String, fromId: StixRef, toId: StixRef, fromTypes: [String], toTypes: [String], relationship_type: [String], startTimeStart: DateTime, startTimeStop: DateTime, stopTimeStart: DateTime, stopTimeStop: DateTime, search: String, filters: FilterGroup, toStix: Boolean): StixRefRelationshipConnection stixNestedRefRelationships(first: Int, after: ID, orderBy: StixRefRelationshipsOrdering, orderMode: OrderingMode, fromOrToId: String, fromId: StixRef, toId: StixRef, fromTypes: [String], toTypes: [String], relationship_type: [String], startTimeStart: DateTime, startTimeStop: DateTime, stopTimeStart: DateTime, stopTimeStop: DateTime, search: String, filters: FilterGroup, toStix: Boolean): StixRefRelationshipConnection + stixNestedRefRelationshipFromEntityType(id: String): [String] stixSchemaRefRelationships(id: String, toType: String): DefinitionRefRelationship stixRefRelationshipsDistribution(field: String!, operation: StatsOperation!, relationship_type: [String], isTo: Boolean, toRole: String, toTypes: [String], startDate: DateTime, endDate: DateTime, dateAttribute: String, limit: Int, order: String): [Distribution] stixRefRelationshipsNumber(types: [String!], fromId: StixRef, endDate: DateTime): Number diff --git a/opencti-platform/opencti-front/tests_e2e/model/intrusionSetDetails.pageModel.ts b/opencti-platform/opencti-front/tests_e2e/model/intrusionSetDetails.pageModel.ts index 73c2111a8369..6d4db35a2a41 100644 --- a/opencti-platform/opencti-front/tests_e2e/model/intrusionSetDetails.pageModel.ts +++ b/opencti-platform/opencti-front/tests_e2e/model/intrusionSetDetails.pageModel.ts @@ -20,6 +20,6 @@ export default class IntrusionSetDetailsPage { } getCreateRelationshipButton() { - return this.page.getByLabel('Add', { exact: true }); + return this.page.getByRole('button', { name: 'Create Relationship' }); } } diff --git a/opencti-platform/opencti-graphql/config/schema/opencti.graphql b/opencti-platform/opencti-graphql/config/schema/opencti.graphql index 106a7f6bb552..24e3a26b520d 100644 --- a/opencti-platform/opencti-graphql/config/schema/opencti.graphql +++ b/opencti-platform/opencti-graphql/config/schema/opencti.graphql @@ -13546,6 +13546,9 @@ type Query { filters: FilterGroup toStix: Boolean ): StixRefRelationshipConnection @auth(for: [KNOWLEDGE]) + stixNestedRefRelationshipFromEntityType( + id: String + ): [String] @auth(for: [KNOWLEDGE]) stixSchemaRefRelationships( id: String toType: String diff --git a/opencti-platform/opencti-graphql/src/domain/stixRefRelationship.js b/opencti-platform/opencti-graphql/src/domain/stixRefRelationship.js index 90bdda0e2186..7cca58d0a7da 100644 --- a/opencti-platform/opencti-graphql/src/domain/stixRefRelationship.js +++ b/opencti-platform/opencti-graphql/src/domain/stixRefRelationship.js @@ -43,6 +43,25 @@ export const schemaRefRelationships = async (context, user, id, toType) => { return { entity, from, to }; }); }; +/** + * Given an entity's id, finds other entity types that we can create a nested + * ref relationship with. + * @param {*} context + * @param {*} user + * @param {*} id + * @returns List of entity types + */ +export const entityTypesWithNestedRefRelationships = async (context, user, id) => { + return findStixObjectOrStixRelationshipById(context, user, id) + .then((entity) => { + const supportedTargetTypes = schemaRelationsRefDefinition.getRelationsRef(entity.entity_type) + .filter((ref) => !notNestedRefRelation.includes(ref.databaseName)) + .flatMap((ref) => ref.toTypes) + .sort() + .filter((value, index, arr) => arr.indexOf(value) === index); // Unique + return supportedTargetTypes; + }); +}; export const isDatable = (entityType, relationshipType) => { return schemaRelationsRefDefinition.isDatable(entityType, relationshipType); }; diff --git a/opencti-platform/opencti-graphql/src/generated/graphql.ts b/opencti-platform/opencti-graphql/src/generated/graphql.ts index 06c741da0138..e915e10a8869 100644 --- a/opencti-platform/opencti-graphql/src/generated/graphql.ts +++ b/opencti-platform/opencti-graphql/src/generated/graphql.ts @@ -19211,6 +19211,7 @@ export type Query = { stixDomainObjectsTimeSeries?: Maybe>>; stixMetaObject?: Maybe; stixMetaObjects?: Maybe; + stixNestedRefRelationshipFromEntityType?: Maybe>>; stixNestedRefRelationships?: Maybe; stixObjectOrStixRelationship?: Maybe; stixObjectOrStixRelationships?: Maybe; @@ -21190,6 +21191,11 @@ export type QueryStixMetaObjectsArgs = { }; +export type QueryStixNestedRefRelationshipFromEntityTypeArgs = { + id?: InputMaybe; +}; + + export type QueryStixNestedRefRelationshipsArgs = { after?: InputMaybe; filters?: InputMaybe; @@ -37872,6 +37878,7 @@ export type QueryResolvers>>, ParentType, ContextType, RequireFields>; stixMetaObject?: Resolver, ParentType, ContextType, RequireFields>; stixMetaObjects?: Resolver, ParentType, ContextType, Partial>; + stixNestedRefRelationshipFromEntityType?: Resolver>>, ParentType, ContextType, Partial>; stixNestedRefRelationships?: Resolver, ParentType, ContextType, Partial>; stixObjectOrStixRelationship?: Resolver, ParentType, ContextType, RequireFields>; stixObjectOrStixRelationships?: Resolver, ParentType, ContextType, Partial>; diff --git a/opencti-platform/opencti-graphql/src/resolvers/stixRefRelationship.js b/opencti-platform/opencti-graphql/src/resolvers/stixRefRelationship.js index 1847231891da..16576b16332c 100644 --- a/opencti-platform/opencti-graphql/src/resolvers/stixRefRelationship.js +++ b/opencti-platform/opencti-graphql/src/resolvers/stixRefRelationship.js @@ -1,5 +1,6 @@ import { addStixRefRelationship, + entityTypesWithNestedRefRelationships, findAll, findById, findNested, @@ -29,6 +30,7 @@ const stixRefRelationshipResolvers = { stixRefRelationship: (_, { id }, context) => findById(context, context.user, id), stixRefRelationships: (_, args, context) => findAll(context, context.user, args), stixNestedRefRelationships: (_, args, context) => findNested(context, context.user, args), + stixNestedRefRelationshipFromEntityType: (_, { id }, context) => entityTypesWithNestedRefRelationships(context, context.user, id), stixSchemaRefRelationships: (_, { id, toType }, context) => schemaRefRelationships(context, context.user, id, toType), stixRefRelationshipsDistribution: (_, args, context) => distributionRelations(context, context.user, args), stixRefRelationshipsNumber: (_, args, context) => stixRefRelationshipsNumber(context, context.user, args), diff --git a/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/malwareAnalysis-test.js b/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/malwareAnalysis-test.js index 5abb0566a37b..052e040f9144 100644 --- a/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/malwareAnalysis-test.js +++ b/opencti-platform/opencti-graphql/tests/02-integration/02-resolvers/malwareAnalysis-test.js @@ -85,6 +85,30 @@ describe('MalwareAnalysis resolver standard behavior', () => { expect(queryResult.data.malwareAnalysis).not.toBeNull(); expect(queryResult.data.malwareAnalysis.id).toEqual(malwareAnalysisInternalId); }); + it('should malwareAnalysis list valid nested ref relationship targets', async () => { + // Given a Malware Analysis, we should be able to query valid target entity + // types for nested ref relationship creation + const TYPE_QUERY = gql` + query MalwareAnalysisNestedTypesQuery($id: String!) { + stixNestedRefRelationshipFromEntityType(id: $id) + } + `; + const queryResult = await queryAsAdmin({ + query: TYPE_QUERY, + variables: { id: malwareAnalysisInternalId }, + }); + expect(queryResult?.data?.stixNestedRefRelationshipFromEntityType).not.toBeNull(); + expect(queryResult.data.stixNestedRefRelationshipFromEntityType).toEqual([ + "Artifact", + "Domain-Name", + "Hostname", + "Network-Traffic", + "Software", + "Stix-Cyber-Observable", + "StixFile", + "Url" + ]); + }); it('should list malwareAnalyses', async () => { const queryResult = await queryAsAdmin({ query: LIST_QUERY, variables: { first: 10 } }); expect(queryResult.data.malwareAnalyses.edges.length).toEqual(2);