diff --git a/opencti-documentation/docs/installation/auto.md b/opencti-documentation/docs/installation/auto.md index a3b364db9c70..26f4de97dc4d 100644 --- a/opencti-documentation/docs/installation/auto.md +++ b/opencti-documentation/docs/installation/auto.md @@ -4,6 +4,7 @@ title: Automatic installation sidebar_label: Automatic installation --- +> For production deployment, we advise you to deploy `Grakn` and `ElasticSearch` manually in a dedicated environment and then to start the other components using `Docker`. # Virtual machine template @@ -33,11 +34,9 @@ $ cd docker ### Configure the environment -Before running the docker-compose command, please change the admin token (this token must be a [valid UUID](https://www.uuidgenerator.net/)) and password of the application in the file `.env`. +Before running the `docker-compose` command, don't forget to change the admin token (this token must be a [valid UUID](https://www.uuidgenerator.net/)) and the password in the file `.env`. There is a file `.env.example` with a preset of variables for a demonstration purpose only. -They are a `.env.example` with indications of differents variables (if it's UUID or just text). - -If you cannot or don't want to use the `.env`, please, edit the file `docker-compose.yml`: +If you cannot or don't want to use the `.env`, please edit the file `docker-compose.yml` with: ```yaml - APP__ADMIN__PASSWORD=ChangeMe diff --git a/opencti-documentation/website/versioned_docs/version-3.0.3/installation/auto.md b/opencti-documentation/website/versioned_docs/version-3.0.3/installation/auto.md new file mode 100644 index 000000000000..adf342c1f98d --- /dev/null +++ b/opencti-documentation/website/versioned_docs/version-3.0.3/installation/auto.md @@ -0,0 +1,167 @@ +--- +id: version-3.0.3-auto +title: Automatic installation +sidebar_label: Automatic installation +original_id: auto +--- + +> For production deployment, we advise you to deploy `Grakn` and `ElasticSearch` manually in a dedicated environment and then to start the other components using `Docker`. + +# Virtual machine template + +OpenCTI could be deployed for **testing purposes** with a provided OVA file. + +## Download the OVA file + +The OVA file is available on the [Luatix Google Drive folder](https://drive.google.com/open?id=1bvB6RmdQNHMW_3h-88KbAit9GRZlL5Bj). It has been pre-configured with the minimal requirements of memory and CPU. + +## Launch the virtual machine + +Then open the OVA file with VirtualBox or convert the OVA to another type of virtual machine image and launch it. You can login within the VM or connect in SSH with the default login `opencti` and the default password `opencti`. + +> Once the virtual machine is launched, the **OpenCTI platform can take 3 to 5 minutes to start the first time**. Then you have access to the plaform using the URL **http://{IP_ADDRESS}:8080** and credentials `admin@opencti.io` / `admin`. + +# Using Docker + +OpenCTI could be deployed using the *docker-compose* command. + +## Clone the repository + +```bash +$ mkdir /path/to/your/app && cd /path/to/your/app +$ git clone https://github.com/OpenCTI-Platform/docker.git +$ cd docker +``` + +### Configure the environment + +Before running the `docker-compose` command, don't forget to change the admin token (this token must be a [valid UUID](https://www.uuidgenerator.net/)) and the password in the file `.env`. There is a file `.env.example` with a preset of variables for a demonstration purpose only. + +If you cannot or don't want to use the `.env`, please edit the file `docker-compose.yml` with: + +```yaml +- APP__ADMIN__PASSWORD=ChangeMe +- APP__ADMIN__TOKEN=ChangeMe +``` + +And change the variable `OPENCTI_TOKEN` (for the `worker` and all connectors) according to the value of `APP__ADMIN__TOKEN` + +```yaml +- OPENCTI_TOKEN=ChangeMe +``` + +As OpenCTI has a dependency to ElasticSearch and Grakn, you have to set the `vm.max_map_count` before running the containers, as mentioned in the [ElasticSearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-cli-run-prod-mode). + +```bash +$ sysctl -w vm.max_map_count=1048575 +``` + +To make this parameter persistent, please update your file `/etc/sysctl.conf` and add the line: +```bash +$ vm.max_map_count=1048575 +``` + +## Run + +In order to have the best experience with Docker, we recommend to use the Docker stack feature. In this mode we will have the capacity to easily scale your deployment. + +```bash +$ env $(cat .env | grep ^[A-Z] | xargs) docker stack deploy --compose-file docker-compose.yml opencti +``` + +> In some configuration, Grakn could fail to start with the following error: `Starting Storage.....FAILED!` +> You can restart it by using the command `$ docker service update --force opencti_grakn`. + +You can also deploy with the standard Docker command: + +```bash +$ docker-compose --compatibility up +``` + +You can now go to http://localhost:8080 and log in with the credentials configured in your environment variables. + +### Update the stack or delete the stack + +```bash +$ docker service update --force service_name +$ docker stack rm opencti +``` + +### Behind a reverse proxy + +If you want to use OpenCTI behind a reverse proxy with a context path, like `https://myproxy.com/opencti`, please change the base_path configuration. + +```yaml +- APP__BASE_PATH=/opencti +``` +By default OpenCTI use Websockets so dont forget to configure your proxy for this usage. + + +## Data persistence + +If you wish your OpenCTI data to be persistent in production, you should be aware of the `volumes` section for `Grakn`, `ElasticSearch` and `MinIO` services in the `docker-compose.yml`. + +Here is an example of volumes configuration: + +```yaml +volumes: + grakndata: + driver: local + driver_opts: + o: bind + type: none + esdata: + driver: local + driver_opts: + o: bind + type: none + s3data: + driver: local + driver_opts: + o: bind + type: none +``` + +## Memory configuration + +OpenCTI default `docker-compose.yml` file does not provide any specific memory configuration. But if you want to adapt some dependencies configuration, you can find some links below. + +### OpenCTI - Platform + +OpenCTI platform is based on a NodeJS runtime, with a memory limit of **512MB by default**. We do not provide any option to change this limit today. If you encounter any `OutOfMemory` exception, please open a [Github issue](https://github.com/OpenCTI-Platform/opencti/issues/new?assignees=&labels=&template=bug_report.md&title=). + +### OpenCTI - Workers and connectors + +OpenCTI workers and connectors are Python processes. If you want to limit the memory of the process we recommend to directly use Docker to do that. You can find more information in the [official Docker documentation](https://docs.docker.com/compose/compose-file/). + +> If you do not use Docker stack, think about `--compatibility` option. + +### Grakn + +Grakn is a JAVA process that rely on Cassandra (also a JAVA process). In order to setup the JAVA memory allocation, you can use the environment variable `SERVER_JAVAOPTS` and `STORAGE_JAVAOPTS`. + +> The current recommendation is `-Xms4G` for both options. + +You can find more information in the [official Grakn documentation](https://dev.grakn.ai/docs). + +### ElasticSearch + +ElasticSearch is also a JAVA process. In order to setup the JAVA memory allocation, you can use the environment variable `ES_JAVA_OPTS`. + +> The minimal recommended option today is `-Xms512M -Xmx512M`. + +You can find more information in the [official ElasticSearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html). + +### Redis + +Redis has a very small footprint and only provides an option to limit the maximum amount of memory that can be used by the process. You can use the option `--maxmemory` to limit the usage. + +You can find more information in the [Redis docker hub](https://hub.docker.com/r/bitnami/redis/). + +### MinIO + +MinIO is a small process and does not require a high amount of memory. More information are available for Linux here on the [Kernel tuning guide](https://github.com/minio/minio/tree/master/docs/deployment/kernel-tuning). + +### RabbitMQ + +The RabbitMQ memory configuration can be find in the [RabbitMQ official documentation](https://www.rabbitmq.com/memory.html). Basically RabbitMQ will consumed memory until a specific threshold. So it should be configure along with the Docker memory limitation. diff --git a/opencti-documentation/website/versions.json b/opencti-documentation/website/versions.json index 13c3ce631c3e..d221fdddab61 100644 --- a/opencti-documentation/website/versions.json +++ b/opencti-documentation/website/versions.json @@ -1,4 +1,5 @@ [ + "3.0.3", "3.0.2", "3.0.1", "3.0.0", diff --git a/opencti-platform/opencti-front/package.json b/opencti-platform/opencti-front/package.json index 2dcd8ead42b1..07d99573b070 100644 --- a/opencti-platform/opencti-front/package.json +++ b/opencti-platform/opencti-front/package.json @@ -1,6 +1,6 @@ { "name": "opencti-front", - "version": "3.0.2", + "version": "3.0.3", "author": "Luatix", "license": "Apache-2.0", "dependencies": { diff --git a/opencti-platform/opencti-front/src/components/list_lines/ListLines.js b/opencti-platform/opencti-front/src/components/list_lines/ListLines.js index 379fd7354a26..5dd7457a01d3 100644 --- a/opencti-platform/opencti-front/src/components/list_lines/ListLines.js +++ b/opencti-platform/opencti-front/src/components/list_lines/ListLines.js @@ -120,6 +120,8 @@ class ListLines extends Component { classes, handleSearch, handleChangeView, + disableCards, + enableDuplicates, handleRemoveFilter, handleToggleExports, openExports, @@ -184,7 +186,7 @@ class ListLines extends Component { ) : ( '' )} - {typeof handleChangeView === 'function' ? ( + {typeof handleChangeView === 'function' && !disableCards ? ( + + + + + ) : ( + '' + )} {typeof handleToggleExports === 'function' ? ( @@ -293,6 +307,8 @@ ListLines.propTypes = { handleSearch: PropTypes.func, handleSort: PropTypes.func.isRequired, handleChangeView: PropTypes.func, + disableCards: PropTypes.bool, + enableDuplicates: PropTypes.bool, handleRemoveFilter: PropTypes.func, handleToggleExports: PropTypes.func, openExports: PropTypes.bool, diff --git a/opencti-platform/opencti-front/src/components/list_lines/ListLinesContent.js b/opencti-platform/opencti-front/src/components/list_lines/ListLinesContent.js index 531d8cd26bed..bfbc10d2d86b 100644 --- a/opencti-platform/opencti-front/src/components/list_lines/ListLinesContent.js +++ b/opencti-platform/opencti-front/src/components/list_lines/ListLinesContent.js @@ -1,7 +1,7 @@ /* eslint-disable no-underscore-dangle */ import React, { Component } from 'react'; import * as PropTypes from 'prop-types'; -import { compose, differenceWith } from 'ramda'; +import { compose, differenceWith, propOr } from 'ramda'; import { withStyles } from '@material-ui/core/styles'; import { AutoSizer, @@ -37,7 +37,16 @@ class ListLinesContent extends Component { this.props.dataList, prevProps.dataList, ); - if (diff.length > 0) { + let selection = false; + if (this.props.selectedElements) { + if ( + Object.keys(this.props.selectedElements).length + !== Object.keys(propOr({}, 'selectedElements', prevProps)).length + ) { + selection = true; + } + } + if (diff.length > 0 || selection) { this.listRef.forceUpdateGrid(); } } @@ -85,6 +94,8 @@ class ListLinesContent extends Component { entityLink, me, onTagClick, + selectedElements, + onToggleEntity, } = this.props; const edge = dataList[index]; if (!edge) { @@ -106,6 +117,8 @@ class ListLinesContent extends Component { entityLink, me, onTagClick, + selectedElements, + onToggleEntity, })} ); @@ -183,9 +196,8 @@ ListLinesContent.propTypes = { paginationOptions: PropTypes.object, entityLink: PropTypes.string, onTagClick: PropTypes.func, + selectedElements: PropTypes.object, + onToggleEntity: PropTypes.func, }; -export default compose( - inject18n, - withStyles(styles), -)(ListLinesContent); +export default compose(inject18n, withStyles(styles))(ListLinesContent); diff --git a/opencti-platform/opencti-front/src/private/Root.js b/opencti-platform/opencti-front/src/private/Root.js index a1967cf5ec4b..941d92b1d3fd 100644 --- a/opencti-platform/opencti-front/src/private/Root.js +++ b/opencti-platform/opencti-front/src/private/Root.js @@ -20,7 +20,7 @@ import RootWorkspace from './components/workspaces/Root'; import Reports from './components/reports/Reports'; import RootReport from './components/reports/Root'; import ExternalReferences from './components/common/external_references/ExternalReferences'; -import Connectors from './components/connectors/Connectors'; +import RootData from './components/data/Root'; import Profile from './components/Profile'; import Message from '../components/Message'; import { NoMatch, BoundaryRoute } from './components/Error'; @@ -165,9 +165,8 @@ const Root = () => { )} /> } + path="/dashboard/data" + render={(routeProps) => } /> ({ maxHeight: 60, paddingRight: 0, }, - itemIcon: { - color: theme.palette.primary.main, - }, itemText: { whiteSpace: 'nowrap', overflow: 'hidden', @@ -458,10 +455,8 @@ class Dashboard extends Component { component={Link} to={`/dashboard/reports/all/${report.id}`} > - - + + - - + + ({ marginRight: 0, }, item: { - padding: '0 0 0 10px', - }, - itemField: { - padding: '0 15px 0 15px', + padding: '0 0 0 6px', }, toolbar: theme.mixins.toolbar, }); @@ -77,9 +74,23 @@ class StixDomainEntitiesRightBar extends Component { + + + + - + + + + + @@ -131,6 +161,7 @@ class StixDomainEntitiesRightBar extends Component { @@ -143,6 +174,7 @@ class StixDomainEntitiesRightBar extends Component { @@ -155,6 +187,7 @@ class StixDomainEntitiesRightBar extends Component { @@ -167,6 +200,7 @@ class StixDomainEntitiesRightBar extends Component { @@ -179,6 +213,7 @@ class StixDomainEntitiesRightBar extends Component { @@ -194,30 +229,39 @@ class StixDomainEntitiesRightBar extends Component { @@ -230,6 +274,7 @@ class StixDomainEntitiesRightBar extends Component { @@ -242,6 +287,7 @@ class StixDomainEntitiesRightBar extends Component { diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityKillChainLines.js b/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityKillChainLines.js index f449fecb0a58..ef0c3ef379a1 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityKillChainLines.js +++ b/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityKillChainLines.js @@ -18,6 +18,7 @@ import { values, sortWith, ascend, + descend, take, pathOr, } from 'ramda'; @@ -166,7 +167,7 @@ class StixDomainEntityKillChainLinesComponent extends Component { : { id: 'unknown', phase_name: t('Unknown'), phase_order: 99 }, n, )), - sortWith([ascend(prop('years'))]), + sortWith([descend(prop('years'))]), groupBy(path(['killChainPhase', 'id'])), mapObjIndexed((value, key) => assoc('attackPatterns', value, killChainPhases[key])), values, diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityVictimologyRegions.js b/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityVictimologyRegions.js index a5da2a069c5c..374d03917be6 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityVictimologyRegions.js +++ b/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityVictimologyRegions.js @@ -18,6 +18,9 @@ import { pathOr, pluck, concat, + sortWith, + ascend, + descend, } from 'ramda'; import graphql from 'babel-plugin-relay/macro'; import { withStyles } from '@material-ui/core/styles'; @@ -319,6 +322,11 @@ class StixDomainEntityVictimologyRegionsComponent extends Component { } } } + + const orderedFinalRegions = pipe( + values, + sortWith([ascend(prop('name'))]), + )(finalRegions); return (
@@ -337,319 +345,381 @@ class StixDomainEntityVictimologyRegionsComponent extends Component {
- {values(finalRegions).map((region) => ( -
- - - - - - - - {this.state.expandedLines[region.id] === true ? ( - - ) : ( - - )} - - - - - - {values(region.relations).map((stixRelation) => { - const link = `${entityLink}/relations/${stixRelation.id}`; - return ( - - - - - {t('Direct targeting of this region')} - ) : ( - stixRelation.to.name - ) - } - secondary={ - // eslint-disable-next-line no-nested-ternary - stixRelation.description - && stixRelation.description.length > 0 ? ( - - ) : stixRelation.inferred ? ( - {t('This relation is inferred')} - ) : ( - t('No description of this targeting') - ) - } - /> - {take( - 1, - pathOr( - [], - ['markingDefinitions', 'edges'], - stixRelation, - ), - ).map((markingDefinition) => ( - - ))} - - - - - - ); - })} - {values(region.countries).map((country) => { - return ( -
+ {orderedFinalRegions.map((region) => { + const orderedRelations = pipe( + values, + sortWith([descend(prop('years'))]), + )(region.relations); + const orderedCountries = pipe( + values, + sortWith([ascend(prop('name'))]), + )(region.countries); + return ( +
+ + + + + + + + {this.state.expandedLines[region.id] === true ? ( + + ) : ( + + )} + + + + + + {orderedRelations.map((stixRelation) => { + const link = `${entityLink}/relations/${stixRelation.id}`; + return ( - - + + - - - - {this.state.expandedLines[country.id] === true ? ( - + + {t('Direct targeting of this region')} + ) : ( - - )} - + stixRelation.to.name + ) + } + secondary={ + // eslint-disable-next-line no-nested-ternary + stixRelation.description + && stixRelation.description.length > 0 ? ( + + ) : stixRelation.inferred ? ( + {t('This relation is inferred')} + ) : ( + t('No description of this targeting') + ) + } + /> + {take( + 1, + pathOr( + [], + ['markingDefinitions', 'edges'], + stixRelation, + ), + ).map((markingDefinition) => ( + + ))} + + + - - - {values(country.relations).map((stixRelation) => { - const link = `${entityLink}/relations/${stixRelation.id}`; - return ( - - - - - - {t( - 'Direct targeting of this country', - )} - - ) : ( - stixRelation.to.name - ) - } - secondary={ - // eslint-disable-next-line no-nested-ternary - stixRelation.description - && stixRelation.description.length > 0 ? ( - - ) : stixRelation.inferred ? ( - {t('This relation is inferred')} - ) : ( - t('No description of this targeting') - ) - } - /> - {take( - 1, - pathOr( - [], - ['markingDefinitions', 'edges'], - stixRelation, - ), - ).map((markingDefinition) => ( - - ))} - - - - - - ); - })} - {values(country.cities).map((city) => ( -
- {values(city.relations).map((stixRelation) => { - const link = `${entityLink}/relations/${stixRelation.id}`; - return ( - { + const orderedSubRelations = pipe( + values, + sortWith([descend(prop('years'))]), + )(country.relations); + const orderedCities = pipe( + values, + sortWith([ascend(prop('name'))]), + )(country.cities); + return ( +
+ + + + + + + + {this.state.expandedLines[country.id] + === true ? ( + + ) : ( + + )} + + + + + + {orderedSubRelations.map((stixRelation) => { + const link = `${entityLink}/relations/${stixRelation.id}`; + return ( + + - - - - - {t( - 'Direct targeting of this country', - )} - - ) : ( - stixRelation.to.name - ) - } - secondary={ - // eslint-disable-next-line no-nested-ternary - stixRelation.description - && stixRelation.description.length + + + + {t( + 'Direct targeting of this country', + )} + + ) : ( + stixRelation.to.name + ) + } + secondary={ + // eslint-disable-next-line no-nested-ternary + stixRelation.description + && stixRelation.description.length > 0 ? ( - + ) : stixRelation.inferred ? ( - - {t('This relation is inferred')} - + + {t('This relation is inferred')} + ) : ( t( 'No description of this targeting', ) ) - } - /> - {take( - 1, - pathOr( - [], - ['markingDefinitions', 'edges'], - stixRelation, - ), - ).map((markingDefinition) => ( - - ))} - + {take( + 1, + pathOr( + [], + ['markingDefinitions', 'edges'], + stixRelation, + ), + ).map((markingDefinition) => ( + + ))} + + + - - - - - ); - })} -
- ))} - - -
- ) - })} -
-
-
- ))} + + + ); + })} + {orderedCities.map((city) => { + const orderedSubSubRelations = pipe( + values, + sortWith([descend(prop('years'))]), + )(city.relations); + return ( +
+ {orderedSubSubRelations.map( + (stixRelation) => { + const link = `${entityLink}/relations/${stixRelation.id}`; + return ( + + + + + + {t( + 'Direct targeting of this country', + )} + + ) : ( + stixRelation.to.name + ) + } + secondary={ + // eslint-disable-next-line no-nested-ternary + stixRelation.description + && stixRelation.description + .length > 0 ? ( + + ) : stixRelation.inferred ? ( + + {t( + 'This relation is inferred', + )} + + ) : ( + t( + 'No description of this targeting', + ) + ) + } + /> + {take( + 1, + pathOr( + [], + [ + 'markingDefinitions', + 'edges', + ], + stixRelation, + ), + ).map((markingDefinition) => ( + + ))} + + + + + + ); + }, + )} +
+ ); + })} + + +
+ ); + })} +
+
+
+ ); + })}
diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityVictimologySectors.js b/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityVictimologySectors.js index 775c29d30eb2..c1ec0e93efa9 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityVictimologySectors.js +++ b/opencti-platform/opencti-front/src/private/components/common/stix_domain_entities/StixDomainEntityVictimologySectors.js @@ -20,6 +20,9 @@ import { pluck, reduce, concat, + sortWith, + ascend, + descend, } from 'ramda'; import graphql from 'babel-plugin-relay/macro'; import { withStyles } from '@material-ui/core/styles'; @@ -280,7 +283,10 @@ class StixDomainEntityVictimologySectorsComponent extends Component { } } } - + const orderedFinalSectors = pipe( + values, + sortWith([ascend(prop('name'))]), + )(finalSectors); return (
@@ -299,226 +305,258 @@ class StixDomainEntityVictimologySectorsComponent extends Component {
- {values(finalSectors).map((sector) => ( -
- - - - - - - - {this.state.expandedLines[sector.id] === true ? ( - - ) : ( - - )} - - - - - - {values(sector.relations).map((stixRelation) => { - const link = `${entityLink}/relations/${stixRelation.id}`; - return ( - - - - - {t('Direct targeting of this sector')} - ) : ( - stixRelation.to.name - ) - } - secondary={ - // eslint-disable-next-line no-nested-ternary - stixRelation.description - && stixRelation.description.length > 0 ? ( - - ) : stixRelation.inferred ? ( - {t('This relation is inferred')} + {orderedFinalSectors.map((sector) => { + const orderedRelations = pipe( + values, + sortWith([descend(prop('years'))]), + )(sector.relations); + const orderedSubSectors = pipe( + values, + sortWith([ascend(prop('name'))]), + )(sector.subSectors); + return ( +
+ + + + + + + + {this.state.expandedLines[sector.id] === true ? ( + + ) : ( + + )} + + + + + + {orderedRelations.map((stixRelation) => { + const link = `${entityLink}/relations/${stixRelation.id}`; + return ( + + + + + + {t('Direct targeting of this sector')} + ) : ( - t('No description of this targeting') + stixRelation.to.name ) - } - /> - {take( - 1, - pathOr( - [], - ['markingDefinitions', 'edges'], - stixRelation, - ), - ).map((markingDefinition) => ( - 0 ? ( + + ) : stixRelation.inferred ? ( + {t('This relation is inferred')} + ) : ( + t('No description of this targeting') + ) + } /> - ))} - - - ( + + ))} + - - - ); - })} - {values(sector.subSectors).map((subsector) => ( -
- - - - - - - + + + + ); + })} + {orderedSubSectors.map((subsector) => { + const orderedSubRelations = pipe( + values, + sortWith([descend(prop('years'))]), + )(subsector.relations); + return ( +
+ - {this.state.expandedLines[subsector.id] - === true ? ( - - ) : ( - - )} - - - - - - {values(subsector.relations).map((stixRelation) => { - const link = `${entityLink}/relations/${stixRelation.id}`; - return ( - + + + + + - - - - - {t('Direct targeting of this sector')} - - ) : ( - stixRelation.to.name - ) - } - secondary={ - // eslint-disable-next-line no-nested-ternary - stixRelation.description - && stixRelation.description.length > 0 ? ( - + ) : ( + + )} + + + + + + {orderedSubRelations.map((stixRelation) => { + const link = `${entityLink}/relations/${stixRelation.id}`; + return ( + + + - ) : stixRelation.inferred ? ( - {t('This relation is inferred')} - ) : ( - t('No description of this targeting') - ) - } - /> - {take( - 1, - pathOr( - [], - ['markingDefinitions', 'edges'], - stixRelation, - ), - ).map((markingDefinition) => ( - - ))} - - - - - - ); - })} - - -
- ))} - - -
- ))} + + + {t( + 'Direct targeting of this sector', + )} + + ) : ( + stixRelation.to.name + ) + } + secondary={ + // eslint-disable-next-line no-nested-ternary + stixRelation.description + && stixRelation.description.length + > 0 ? ( + + ) : stixRelation.inferred ? ( + + {t('This relation is inferred')} + + ) : ( + t( + 'No description of this targeting', + ) + ) + } + /> + {take( + 1, + pathOr( + [], + ['markingDefinitions', 'edges'], + stixRelation, + ), + ).map((markingDefinition) => ( + + ))} + + + + + + ); + })} +
+
+
+ ); + })} +
+
+
+ ); + })}
diff --git a/opencti-platform/opencti-front/src/private/components/connectors/Connectors.js b/opencti-platform/opencti-front/src/private/components/data/Connectors.js similarity index 88% rename from opencti-platform/opencti-front/src/private/components/connectors/Connectors.js rename to opencti-platform/opencti-front/src/private/components/data/Connectors.js index f7710820e183..1e898c3a4186 100644 --- a/opencti-platform/opencti-front/src/private/components/connectors/Connectors.js +++ b/opencti-platform/opencti-front/src/private/components/data/Connectors.js @@ -4,8 +4,8 @@ import { compose } from 'ramda'; import { withStyles } from '@material-ui/core/styles/index'; import inject18n from '../../../components/i18n'; import { QueryRenderer } from '../../../relay/environment'; -import WorkersStatus, { workersStatusQuery } from './WorkersStatus'; -import ConnectorsStatus, { connectorsStatusQuery } from './ConnectorsStatus'; +import WorkersStatus, { workersStatusQuery } from './connectors/WorkersStatus'; +import ConnectorsStatus, { connectorsStatusQuery } from './connectors/ConnectorsStatus'; import Loader from '../../../components/Loader'; const styles = () => ({ diff --git a/opencti-platform/opencti-front/src/private/components/data/Curation.js b/opencti-platform/opencti-front/src/private/components/data/Curation.js new file mode 100644 index 000000000000..45526c3bb676 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/data/Curation.js @@ -0,0 +1,261 @@ +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; +import { + compose, + append, + filter, + propOr, + assoc, + dissoc, + pipe, + toPairs, + map, + last, + head, + omit, +} from 'ramda'; +import { withRouter } from 'react-router-dom'; +import { withStyles } from '@material-ui/core/styles'; +import { QueryRenderer } from '../../../relay/environment'; +import ListLines from '../../../components/list_lines/ListLines'; +import CurationToolBar from './curation/CurationToolBar'; +import CurationStixDomainEntitiesLines, { + curationStixDomainEntitiesLinesQuery, +} from './curation/CurationStixDomainEntitiesLines'; +import inject18n from '../../../components/i18n'; +import { + buildViewParamsFromUrlAndStorage, + saveViewParameters, +} from '../../../utils/ListParameters'; +import StixDomainEntitiesRightBar from '../common/stix_domain_entities/StixDomainEntitiesRightBar'; + +const styles = () => ({ + container: { + paddingRight: 250, + }, +}); + +class StixObservables extends Component { + constructor(props) { + super(props); + const params = buildViewParamsFromUrlAndStorage( + props.history, + props.location, + 'view-curation', + ); + this.state = { + sortBy: propOr('created_at', 'sortBy', params), + orderAsc: propOr(false, 'orderAsc', params), + searchTerm: propOr('', 'searchTerm', params), + view: propOr('lines', 'view', params), + filters: {}, + stixDomainEntitiesTypes: propOr([], 'stixDomainEntitiesTypes', params), + numberOfElements: { number: 0, symbol: '' }, + selectedElements: {}, + }; + } + + saveView() { + saveViewParameters( + this.props.history, + this.props.location, + 'view-curation', + dissoc('filters', this.state), + ); + } + + handleChangeView(mode) { + this.setState({ view: mode }, () => this.saveView()); + } + + handleSearch(value) { + this.setState({ searchTerm: value }, () => this.saveView()); + } + + handleSort(field, orderAsc) { + this.setState({ sortBy: field, orderAsc }, () => this.saveView()); + } + + handleResetSelectedElements() { + this.setState({ selectedElements: {} }); + } + + handleToggle(type) { + if (this.state.stixDomainEntitiesTypes.includes(type)) { + this.setState( + { + stixDomainEntitiesTypes: filter( + (t) => t !== type, + this.state.stixDomainEntitiesTypes, + ), + }, + () => this.saveView(), + ); + } else { + this.setState( + { + stixDomainEntitiesTypes: append( + type, + this.state.stixDomainEntitiesTypes, + ), + }, + () => this.saveView(), + ); + } + } + + handleAddFilter(key, id, value, event) { + event.stopPropagation(); + event.preventDefault(); + this.setState({ + filters: assoc(key, [{ id, value }], this.state.filters), + }); + } + + handleRemoveFilter(key) { + this.setState({ filters: dissoc(key, this.state.filters) }); + } + + setNumberOfElements(numberOfElements) { + this.setState({ numberOfElements }); + } + + handleToggleSelectEntity(entity) { + if (entity.id in this.state.selectedElements) { + this.setState({ + selectedElements: omit([entity.id], this.state.selectedElements), + }); + } else { + this.setState({ + selectedElements: assoc(entity.id, entity, this.state.selectedElements), + }); + } + } + + renderLines(paginationOptions) { + const { + sortBy, + orderAsc, + searchTerm, + filters, + numberOfElements, + selectedElements, + } = this.state; + const dataColumns = { + entity_type: { + label: 'Type', + width: '15%', + isSortable: true, + }, + name: { + label: 'Name', + width: '35%', + isSortable: true, + }, + tags: { + label: 'Tags', + width: '20%', + isSortable: false, + }, + created_at: { + label: 'Creation date', + width: '15%', + isSortable: true, + }, + markingDefinitions: { + label: 'Marking', + isSortable: false, + }, + }; + return ( +
+ + ( + + )} + /> + + +
+ ); + } + + render() { + const { classes } = this.props; + const { + view, + stixDomainEntitiesTypes, + sortBy, + orderAsc, + searchTerm, + filters, + } = this.state; + const finalFilters = pipe( + toPairs, + map((pair) => { + const values = last(pair); + const valIds = map((v) => v.id, values); + return { key: head(pair), values: valIds }; + }), + )(filters); + const paginationOptions = { + types: + stixDomainEntitiesTypes.length > 0 ? stixDomainEntitiesTypes : null, + search: searchTerm, + filters: finalFilters, + orderBy: sortBy, + orderMode: orderAsc ? 'asc' : 'desc', + }; + return ( +
+ {view === 'lines' ? this.renderLines(paginationOptions) : ''} + +
+ ); + } +} + +StixObservables.propTypes = { + classes: PropTypes.object, + t: PropTypes.func, + history: PropTypes.object, + location: PropTypes.object, +}; + +export default compose( + inject18n, + withRouter, + withStyles(styles), +)(StixObservables); diff --git a/opencti-platform/opencti-front/src/private/components/data/Root.js b/opencti-platform/opencti-front/src/private/components/data/Root.js new file mode 100644 index 000000000000..89b115dd9ce7 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/data/Root.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { Switch, Redirect } from 'react-router-dom'; +import Connectors from './Connectors'; +import Curation from './Curation'; +import { BoundaryRoute } from '../Error'; + +const Root = () => ( + + } + /> + + + +); + +export default Root; diff --git a/opencti-platform/opencti-front/src/private/components/connectors/ConnectorsStatus.js b/opencti-platform/opencti-front/src/private/components/data/connectors/ConnectorsStatus.js similarity index 84% rename from opencti-platform/opencti-front/src/private/components/connectors/ConnectorsStatus.js rename to opencti-platform/opencti-front/src/private/components/data/connectors/ConnectorsStatus.js index 6823751d5fbb..647a445538e5 100644 --- a/opencti-platform/opencti-front/src/private/components/connectors/ConnectorsStatus.js +++ b/opencti-platform/opencti-front/src/private/components/data/connectors/ConnectorsStatus.js @@ -18,12 +18,12 @@ import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; import List from '@material-ui/core/List'; import Chip from '@material-ui/core/Chip'; import Tooltip from '@material-ui/core/Tooltip'; -import { RotateLeft } from 'mdi-material-ui'; +import { RotateLeft, Delete } from 'mdi-material-ui'; import IconButton from '@material-ui/core/IconButton'; -import { FIVE_SECONDS } from '../../../utils/Time'; -import inject18n from '../../../components/i18n'; -import { commitMutation, MESSAGING$ } from '../../../relay/environment'; -import Security, { MODULES_MODMANAGE } from '../../../utils/Security'; +import { FIVE_SECONDS } from '../../../../utils/Time'; +import inject18n from '../../../../components/i18n'; +import { commitMutation, MESSAGING$ } from '../../../../relay/environment'; +import Security, { MODULES_MODMANAGE } from '../../../../utils/Security'; const interval$ = interval(FIVE_SECONDS); @@ -80,7 +80,7 @@ const inlineStylesHeaders = { }, name: { float: 'left', - width: '25%', + width: '20%', fontSize: 12, fontWeight: '700', }, @@ -106,7 +106,7 @@ const inlineStylesHeaders = { const inlineStyles = { name: { float: 'left', - width: '25%', + width: '20%', height: 20, whiteSpace: 'nowrap', overflow: 'hidden', @@ -145,6 +145,12 @@ const connectorsStatusResetStateMutation = graphql` } `; +const connectorsStatusDeletionMutation = graphql` + mutation ConnectorsStatusDeletionMutation($id: ID!) { + deleteConnector(id: $id) + } +`; + class ConnectorsStatusComponent extends Component { constructor(props) { super(props); @@ -173,6 +179,18 @@ class ConnectorsStatusComponent extends Component { }); } + handleDelete(connectorId) { + commitMutation({ + mutation: connectorsStatusDeletionMutation, + variables: { + id: connectorId, + }, + onCompleted: () => { + MESSAGING$.notifySuccess('The connector has been cleared'); + }, + }); + } + reverseBy(field) { this.setState({ sortBy: field, orderAsc: !this.state.orderAsc }); } @@ -254,11 +272,15 @@ class ConnectorsStatusComponent extends Component {   {sortedConnectors.map((connector) => ( - - + button={true} + > + - + color="primary" + > + + + + + diff --git a/opencti-platform/opencti-front/src/private/components/connectors/WorkersStatus.js b/opencti-platform/opencti-front/src/private/components/data/connectors/WorkersStatus.js similarity index 97% rename from opencti-platform/opencti-front/src/private/components/connectors/WorkersStatus.js rename to opencti-platform/opencti-front/src/private/components/data/connectors/WorkersStatus.js index 02639e4c057e..e06acd02757e 100644 --- a/opencti-platform/opencti-front/src/private/components/connectors/WorkersStatus.js +++ b/opencti-platform/opencti-front/src/private/components/data/connectors/WorkersStatus.js @@ -10,8 +10,8 @@ import CardContent from '@material-ui/core/CardContent'; import Grid from '@material-ui/core/Grid'; import { createRefetchContainer } from 'react-relay'; import { MultilineChart } from '@material-ui/icons'; -import inject18n from '../../../components/i18n'; -import { FIVE_SECONDS } from '../../../utils/Time'; +import inject18n from '../../../../components/i18n'; +import { FIVE_SECONDS } from '../../../../utils/Time'; const interval$ = interval(FIVE_SECONDS); diff --git a/opencti-platform/opencti-front/src/private/components/data/curation/CurationStixDomainEntitiesLines.js b/opencti-platform/opencti-front/src/private/components/data/curation/CurationStixDomainEntitiesLines.js new file mode 100644 index 000000000000..7c0c04cb1ea7 --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/data/curation/CurationStixDomainEntitiesLines.js @@ -0,0 +1,185 @@ +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; +import { createPaginationContainer } from 'react-relay'; +import graphql from 'babel-plugin-relay/macro'; +import { pathOr } from 'ramda'; +import ListLinesContent from '../../../../components/list_lines/ListLinesContent'; +import { + CurationStixDomainEntityLine, + CurationStixDomainEntityLineDummy, +} from './CurationStixDomainEntityLine'; +import { setNumberOfElements } from '../../../../utils/Number'; + +const nbOfRowsToLoad = 25; + +class CurationStixDomainEntitiesLines extends Component { + componentDidUpdate(prevProps) { + setNumberOfElements( + prevProps, + this.props, + 'stixDomainEntities', + this.props.setNumberOfElements.bind(this), + ); + } + + render() { + const { + initialLoading, + dataColumns, + relay, + onTagClick, + onToggleEntity, + selectedElements, + } = this.props; + return ( + } + DummyLineComponent={} + dataColumns={dataColumns} + nbOfRowsToLoad={nbOfRowsToLoad} + onTagClick={onTagClick.bind(this)} + selectedElements={selectedElements} + onToggleEntity={onToggleEntity.bind(this)} + /> + ); + } +} + +CurationStixDomainEntitiesLines.propTypes = { + classes: PropTypes.object, + paginationOptions: PropTypes.object, + dataColumns: PropTypes.object.isRequired, + data: PropTypes.object, + relay: PropTypes.object, + stixDomainEntities: PropTypes.object, + initialLoading: PropTypes.bool, + onTagClick: PropTypes.func, + setNumberOfElements: PropTypes.func, + onToggleEntity: PropTypes.func, + selectedElements: PropTypes.object, +}; + +export const curationStixDomainEntitiesLinesQuery = graphql` + query CurationStixDomainEntitiesLinesPaginationQuery( + $types: [String] + $search: String + $count: Int! + $cursor: ID + $orderBy: StixDomainEntitiesOrdering + $orderMode: OrderingMode + $filters: [StixDomainEntitiesFiltering] + ) { + ...CurationStixDomainEntitiesLines_data + @arguments( + types: $types + search: $search + count: $count + cursor: $cursor + orderBy: $orderBy + orderMode: $orderMode + filters: $filters + ) + } +`; + +export const curationStixDomainEntitiesLinesSearchQuery = graphql` + query CurationStixDomainEntitiesLinesSearchQuery($search: String) { + stixDomainEntities(search: $search) { + edges { + node { + id + entity_type + name + created_at + updated_at + } + } + } + } +`; + +export default createPaginationContainer( + CurationStixDomainEntitiesLines, + { + data: graphql` + fragment CurationStixDomainEntitiesLines_data on Query + @argumentDefinitions( + types: { type: "[String]" } + search: { type: "String" } + count: { type: "Int", defaultValue: 25 } + cursor: { type: "ID" } + orderBy: { type: "StixDomainEntitiesOrdering", defaultValue: "name" } + orderMode: { type: "OrderingMode", defaultValue: "asc" } + filters: { type: "[StixDomainEntitiesFiltering]" } + ) { + stixDomainEntities( + types: $types + search: $search + first: $count + after: $cursor + orderBy: $orderBy + orderMode: $orderMode + filters: $filters + ) @connection(key: "Pagination_stixDomainEntities") { + edges { + node { + id + entity_type + name + alias + created_at + markingDefinitions { + edges { + node { + id + definition + } + } + } + ...CurationStixDomainEntityLine_node + } + } + pageInfo { + endCursor + hasNextPage + globalCount + } + } + } + `, + }, + { + direction: 'forward', + getConnectionFromProps(props) { + return props.data && props.data.stixDomainEntities; + }, + getFragmentVariables(prevVars, totalCount) { + return { + ...prevVars, + count: totalCount, + }; + }, + getVariables(props, { count, cursor }, fragmentVariables) { + return { + types: fragmentVariables.types, + search: fragmentVariables.search, + count, + cursor, + orderBy: fragmentVariables.orderBy, + orderMode: fragmentVariables.orderMode, + filters: fragmentVariables.filters, + }; + }, + query: curationStixDomainEntitiesLinesQuery, + }, +); diff --git a/opencti-platform/opencti-front/src/private/components/data/curation/CurationStixDomainEntityLine.js b/opencti-platform/opencti-front/src/private/components/data/curation/CurationStixDomainEntityLine.js new file mode 100644 index 000000000000..0d96be90545a --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/data/curation/CurationStixDomainEntityLine.js @@ -0,0 +1,237 @@ +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; +import { createFragmentContainer } from 'react-relay'; +import graphql from 'babel-plugin-relay/macro'; +import { withStyles } from '@material-ui/core/styles'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import Checkbox from '@material-ui/core/Checkbox'; +import { compose, pathOr, take } from 'ramda'; +import inject18n from '../../../../components/i18n'; +import ItemMarking from '../../../../components/ItemMarking'; +import StixObjectTags from '../../common/stix_object/StixObjectTags'; + +const styles = (theme) => ({ + item: { + paddingLeft: 10, + height: 50, + }, + itemIcon: { + color: theme.palette.primary.main, + }, + bodyItem: { + height: 20, + fontSize: 13, + float: 'left', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + goIcon: { + position: 'absolute', + right: 10, + marginRight: 0, + }, + itemIconDisabled: { + color: theme.palette.grey[700], + }, + placeholder: { + display: 'inline-block', + height: '1em', + backgroundColor: theme.palette.grey[700], + }, +}); + +class CurationStixDomainEntityLineComponent extends Component { + render() { + const { + t, + fd, + classes, + dataColumns, + node, + onTagClick, + onToggleEntity, + selectedElements, + } = this.props; + return ( + + + + + +
+ {t(`entity_${node.entity_type}`)} +
+
+ {node.name} +
+
+ +
+
+ {fd(node.created_at)} +
+
+ {take(1, pathOr([], ['markingDefinitions', 'edges'], node)).map( + (markingDefinition) => ( + + ), + )} +
+
+ } + /> + + ); + } +} + +CurationStixDomainEntityLineComponent.propTypes = { + dataColumns: PropTypes.object, + node: PropTypes.object, + classes: PropTypes.object, + fd: PropTypes.func, + t: PropTypes.func, + onTagClick: PropTypes.func, + onToggleEntity: PropTypes.func, + selectedElements: PropTypes.object, +}; + +const CurationStixDomainEntityLineFragment = createFragmentContainer( + CurationStixDomainEntityLineComponent, + { + node: graphql` + fragment CurationStixDomainEntityLine_node on StixDomainEntity { + id + entity_type + name + description + alias + created_at + markingDefinitions { + edges { + node { + id + definition + } + } + } + tags { + edges { + node { + id + tag_type + value + color + } + relation { + id + } + } + } + } + `, + }, +); + +export const CurationStixDomainEntityLine = compose( + inject18n, + withStyles(styles), +)(CurationStixDomainEntityLineFragment); + +class CurationStixDomainEntityLineDummyComponent extends Component { + render() { + const { classes, dataColumns } = this.props; + return ( + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } + /> + + ); + } +} + +CurationStixDomainEntityLineDummyComponent.propTypes = { + dataColumns: PropTypes.object, + classes: PropTypes.object, +}; + +export const CurationStixDomainEntityLineDummy = compose( + inject18n, + withStyles(styles), +)(CurationStixDomainEntityLineDummyComponent); diff --git a/opencti-platform/opencti-front/src/private/components/data/curation/CurationToolBar.js b/opencti-platform/opencti-front/src/private/components/data/curation/CurationToolBar.js new file mode 100644 index 000000000000..5c40de32ea8e --- /dev/null +++ b/opencti-platform/opencti-front/src/private/components/data/curation/CurationToolBar.js @@ -0,0 +1,454 @@ +import React, { Component } from 'react'; +import * as PropTypes from 'prop-types'; +import graphql from 'babel-plugin-relay/macro'; +import { withStyles } from '@material-ui/core/styles'; +import { + compose, + values, + map, + uniq, + head, + propOr, + concat, + pluck, + flatten, + pathOr, + tail, + filter, +} from 'ramda'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import Tooltip from '@material-ui/core/Tooltip'; +import IconButton from '@material-ui/core/IconButton'; +import { Close, Delete } from '@material-ui/icons'; +import { Merge } from 'mdi-material-ui'; +import Drawer from '@material-ui/core/Drawer'; +import { ConnectionHandler } from 'relay-runtime'; +import Dialog from '@material-ui/core/Dialog'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogActions from '@material-ui/core/DialogActions'; +import Button from '@material-ui/core/Button'; +import Slide from '@material-ui/core/Slide'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; +import Radio from '@material-ui/core/Radio'; +import Alert from '@material-ui/lab/Alert/Alert'; +import Chip from '@material-ui/core/Chip'; +import { commitMutation } from '../../../../relay/environment'; +import inject18n from '../../../../components/i18n'; +import ItemIcon from '../../../../components/ItemIcon'; +import { truncate } from '../../../../utils/String'; +import ItemMarking from '../../../../components/ItemMarking'; + +const styles = (theme) => ({ + bottomNav: { + zIndex: 1000, + padding: '0 250px 0 74px', + backgroundColor: theme.palette.navBottom.background, + }, + title: { + flex: '1 1 100%', + }, + drawerPaper: { + minHeight: '100vh', + width: '50%', + position: 'fixed', + backgroundColor: theme.palette.navAlt.background, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + padding: 0, + }, + createButton: { + position: 'fixed', + bottom: 30, + right: 230, + }, + buttons: { + marginTop: 20, + textAlign: 'right', + }, + button: { + marginLeft: theme.spacing(2), + }, + header: { + backgroundColor: theme.palette.navAlt.backgroundHeader, + padding: '20px 20px 20px 60px', + }, + closeButton: { + position: 'absolute', + top: 12, + left: 5, + }, + container: { + padding: '10px 20px 20px 20px', + }, + alias: { + margin: '0 7px 7px 0', + }, +}); + +const Transition = React.forwardRef((props, ref) => ( + +)); +Transition.displayName = 'TransitionSlide'; + +const curationToolBarDeletionMutation = graphql` + mutation CurationToolBarDeletionMutation($id: [ID]!) { + stixDomainEntitiesDelete(id: $id) + } +`; + +const curationToolBarMergeMutation = graphql` + mutation CurationToolBarMergeMutation( + $id: ID! + $stixDomainEntitiesIds: [String]! + $alias: [String] + ) { + stixDomainEntityEdit(id: $id) { + mergeEntities( + stixDomainEntitiesIds: $stixDomainEntitiesIds + alias: $alias + ) { + id + } + } + } +`; + +class CurationToolBar extends Component { + constructor(props) { + super(props); + this.state = { + displayMerge: false, + displayDelete: false, + keptEntityId: null, + merging: false, + deleting: false, + }; + } + + handleOpenMerge() { + this.setState({ displayMerge: true }); + } + + handleCloseMerge() { + this.setState({ displayMerge: false }); + } + + handleOpenDelete() { + this.setState({ displayDelete: true }); + } + + handleCloseDelete() { + this.setState({ displayDelete: false }); + } + + submitDelete() { + this.setState({ deleting: true }); + const stixDomainEntitiesIds = Object.keys(this.props.selectedElements); + commitMutation({ + mutation: curationToolBarDeletionMutation, + variables: { + id: stixDomainEntitiesIds, + }, + updater: (store) => { + const container = store.getRoot(); + const userProxy = store.get(container.getDataID()); + const conn = ConnectionHandler.getConnection( + userProxy, + 'Pagination_stixDomainEntities', + this.props.paginationOptions, + ); + stixDomainEntitiesIds.map((id) => ConnectionHandler.deleteNode(conn, id)); + }, + onCompleted: () => { + this.setState({ deleting: false }); + this.props.handleResetSelectedElements(); + this.handleCloseDelete(); + }, + }); + } + + handleChangeKeptEntityId(entityId) { + this.setState({ keptEntityId: entityId }); + } + + submitMerge() { + this.setState({ merging: true }); + const { selectedElements } = this.props; + const { keptEntityId } = this.state; + const stixDomainEntitiesIds = Object.keys(selectedElements); + const selectedElementsList = values(selectedElements); + const keptElement = keptEntityId + ? head(filter((n) => n.id === keptEntityId, selectedElementsList)) + : head(selectedElementsList); + const names = pluck('name', selectedElementsList); + const aliases = flatten(pluck('alias', selectedElementsList)); + const newAliases = filter((n) => n.length > 0, uniq(concat(names, aliases))); + const filteredStixDomainEntitiesIds = keptEntityId + ? filter((n) => n !== keptEntityId, stixDomainEntitiesIds) + : tail(stixDomainEntitiesIds); + commitMutation({ + mutation: curationToolBarMergeMutation, + variables: { + id: keptElement.id, + stixDomainEntitiesIds: filteredStixDomainEntitiesIds, + alias: newAliases, + }, + updater: (store) => { + const container = store.getRoot(); + const userProxy = store.get(container.getDataID()); + const conn = ConnectionHandler.getConnection( + userProxy, + 'Pagination_stixDomainEntities', + this.props.paginationOptions, + ); + filteredStixDomainEntitiesIds.map((id) => ConnectionHandler.deleteNode(conn, id)); + }, + onCompleted: () => { + this.setState({ merging: false }); + this.props.handleResetSelectedElements(); + this.handleCloseMerge(); + }, + }); + } + + render() { + const { t, classes, selectedElements } = this.props; + const { keptEntityId } = this.state; + const numberOfSelectedElements = Object.keys(selectedElements).length; + const typesAreDifferent = uniq(map((n) => n.entity_type, values(selectedElements))).length > 1; + const selectedElementsList = values(selectedElements); + const keptElement = keptEntityId + ? head(filter((n) => n.id === keptEntityId, selectedElementsList)) + : head(selectedElementsList); + const names = pluck('name', selectedElementsList); + const aliases = flatten(pluck('alias', selectedElementsList)); + const newAliases = filter((n) => n.length > 0, uniq(concat(names, aliases))); + return ( + + + + {numberOfSelectedElements} {t('selected')} + + + + + + + + + + + + + + + + + +
+ + + + {t('Merge entities')} +
+
+ + {t('Selected entities')} + + + {selectedElementsList.map((element) => ( + + + + + +
+ {pathOr([], ['markingDefinitions', 'edges'], element) + .length > 0 ? ( + map( + (markingDefinition) => ( + + ), + element.markingDefinitions.edges, + ) + ) : ( + + )} +
+ + + +
+ ))} +
+ + {t('Merged entity')} + + + {t('Name')} + + {propOr(null, 'name', keptElement)} + + {t('Aliases')} + + {newAliases.map((label) => (label.length > 0 ? ( + + ) : ( + '' + )))} + + {t('Marking')} + + {pathOr([], ['markingDefinitions', 'edges'], keptElement).length + > 0 ? ( + map( + (markingDefinition) => ( + + ), + keptElement.markingDefinitions.edges, + ) + ) : ( + + )} + + {t( + 'The relations attached to selected entities will be copied to the merged entity.', + )} + +
+ +
+
+
+ + + + {t('Do you want to delete these entities?')} + + + + + + + +
+ ); + } +} + +CurationToolBar.propTypes = { + classes: PropTypes.object, + t: PropTypes.func, + paginationOptions: PropTypes.object, + selectedElements: PropTypes.object, + handleResetSelectedElements: PropTypes.func, +}; + +export default compose(inject18n, withStyles(styles))(CurationToolBar); diff --git a/opencti-platform/opencti-front/src/private/components/nav/LeftBar.js b/opencti-platform/opencti-front/src/private/components/nav/LeftBar.js index fd738d938737..602b3eb782c4 100644 --- a/opencti-platform/opencti-front/src/private/components/nav/LeftBar.js +++ b/opencti-platform/opencti-front/src/private/components/nav/LeftBar.js @@ -20,16 +20,18 @@ import { Layers, ListAlt, GroupWork, - Extension, } from '@material-ui/icons'; -import { Settings, Database, Binoculars } from 'mdi-material-ui'; +import { + Settings, Database, Binoculars, Flask, +} from 'mdi-material-ui'; import { compose } from 'ramda'; import logo from '../../../resources/images/logo_text.png'; import inject18n from '../../../components/i18n'; import Security, { KNOWLEDGE, EXPLORE, - SETTINGS, MODULES, + SETTINGS, + MODULES, } from '../../../utils/Security'; const styles = (theme) => ({ @@ -93,7 +95,7 @@ const LeftBar = ({ t, location, classes }) => { classes={{ root: classes.menuItem }} > - + { - + classes={{ root: classes.menuItem }} + > - + - + classes={{ root: classes.menuItem }} + > @@ -261,7 +267,7 @@ const LeftBar = ({ t, location, classes }) => { classes={{ root: classes.menuItem }} > - + @@ -369,16 +375,16 @@ const LeftBar = ({ t, location, classes }) => { - + - + + {location.pathname === '/dashboard/data' + || location.pathname.match('/dashboard/data/[a-z_]+$') ? ( + ) : ( '' )} diff --git a/opencti-platform/opencti-front/src/private/components/nav/TopMenuConnectors.js b/opencti-platform/opencti-front/src/private/components/nav/TopMenuData.js similarity index 61% rename from opencti-platform/opencti-front/src/private/components/nav/TopMenuConnectors.js rename to opencti-platform/opencti-front/src/private/components/nav/TopMenuData.js index 6549ad291908..f2fc0aa00b90 100644 --- a/opencti-platform/opencti-front/src/private/components/nav/TopMenuConnectors.js +++ b/opencti-platform/opencti-front/src/private/components/nav/TopMenuData.js @@ -19,22 +19,40 @@ const styles = theme => ({ }, }); -class TopMenuConnectors extends Component { +class TopMenuData extends Component { render() { const { t, location, classes } = this.props; return (
+