From c1d34d711a40e90d5ef0078cd21a4a46166c1437 Mon Sep 17 00:00:00 2001 From: Stephanya Casanova Date: Thu, 2 Jan 2025 08:59:51 +0100 Subject: [PATCH] [backend/frontend] refact simulation generation from with formik and rebase --- .../StixCoreObjectSimulationResult.jsx | 307 ++++++++++-------- .../src/modules/xtm/xtm-domain.js | 180 ++++++---- 2 files changed, 279 insertions(+), 208 deletions(-) diff --git a/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectSimulationResult.jsx b/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectSimulationResult.jsx index d1f4cfa35c7c..eb2fb2234913 100644 --- a/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectSimulationResult.jsx +++ b/opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectSimulationResult.jsx @@ -1,13 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { makeStyles, useTheme } from '@mui/styles'; import { CheckOutlined, OpenInNewOutlined, SensorOccupiedOutlined, ShieldOutlined, TrackChangesOutlined, ErrorOutlined, LaunchOutlined } from '@mui/icons-material'; import Tooltip from '@mui/material/Tooltip'; import Button from '@mui/material/Button'; -import FormControl from '@mui/material/FormControl'; -import InputLabel from '@mui/material/InputLabel'; -import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; -import TextField from '@mui/material/TextField'; import Box from '@mui/material/Box'; import { graphql, useLazyLoadQuery } from 'react-relay'; import DialogActions from '@mui/material/DialogActions'; @@ -18,7 +14,8 @@ import CircularProgress from '@mui/material/CircularProgress'; import Typography from '@mui/material/Typography'; import { Link } from 'react-router-dom'; import Alert from '@mui/material/Alert'; -import { Autocomplete } from '@mui/material'; +import { Field, Form, Formik } from 'formik'; +import * as Yup from 'yup'; import EEChip from '../entreprise_edition/EEChip'; import Drawer from '../drawer/Drawer'; import Chart from '../charts/Chart'; @@ -36,6 +33,10 @@ import { emptyFilterGroup } from '../../../../utils/filters/filtersUtils'; import useApiMutation from '../../../../utils/hooks/useApiMutation'; import Transition from '../../../../components/Transition'; import useXTM from '../../../../utils/hooks/useXTM'; +import SelectField from '../../../../components/fields/SelectField'; +import AutocompleteField from '../../../../components/AutocompleteField'; +import TextField from '../../../../components/TextField'; +import { useDynamicSchemaCreationValidation, yupShapeConditionalRequired } from '../../../../utils/hooks/useEntitySettings'; const useStyles = makeStyles((theme) => ({ simulationResults: { @@ -184,9 +185,9 @@ const StixCoreObjectSimulationResult = ({ id, type }) => { const { enabled, configured } = useAI(); const isSimulatedEmailsAvailable = enabled && configured && isEnterpriseEdition; const [simulationType, setSimulationType] = useState('technical'); - const [platforms, setPlatforms] = useState(['Windows']); - const [architecture, setArchitecture] = useState('x86_64'); - const [selection, setSelection] = useState('random'); + const [platforms, setPlatforms] = useState([{ label: 'Windows', value: 'Windows' }]); + const architecture = 'x86_64'; + const selection = 'random'; const [interval, setInterval] = useState(2); const [isSubmitting, setIsSubmitting] = useState(false); const [result, setResult] = useState(null); @@ -194,13 +195,6 @@ const StixCoreObjectSimulationResult = ({ id, type }) => { const [filters, helpers] = useFiltersState(emptyFilterGroup); const { t_i18n } = useFormatter(); const isGrantedToUpdate = useGranted([KNOWLEDGE_KNUPDATE]); - const [platformError, setPlatformError] = useState(''); - - const platformOptions = [ - { label: 'Windows', value: 'Windows' }, - { label: 'Linux', value: 'Linux' }, - { label: 'MacOS', value: 'MacOS' }, - ]; // Determine the query based on the type let attackPatternsQuery; @@ -216,12 +210,10 @@ const StixCoreObjectSimulationResult = ({ id, type }) => { const attackPatterns = useLazyLoadQuery(attackPatternsQuery, { id }); // Check if there are attack patterns in the entity - let hasAttackPatterns = false; - if (type === 'container' && attackPatterns?.stixCoreObject?.objects?.edges?.length > 0) { - hasAttackPatterns = true; - } else if (type === 'threat' && attackPatterns?.stixCoreRelationships?.edges?.length > 0) { - hasAttackPatterns = true; - } + const hasAttackPatterns = ( + (type === 'container' && attackPatterns?.stixCoreObject?.objects?.edges?.length > 0) + || (type === 'threat' && attackPatterns?.stixCoreRelationships?.edges?.length > 0) + ); const canGenerateScenario = () => { return ( @@ -331,6 +323,7 @@ const StixCoreObjectSimulationResult = ({ id, type }) => { // do nothing } }; + const renderCharts = () => { return ( { ); }; - // Validation for Platforms - useEffect(() => { - if (platforms.length === 0) { - setPlatformError(t_i18n('This field should not be empty')); - } else { - setPlatformError(''); - } - }, [platforms]); + const initialValues = { + simulationType, + platforms, + architecture, + interval, + selection, + }; + + const mandatoryAttributes = ['simulationtype', 'interval', 'selection']; + const basicShape = yupShapeConditionalRequired({ + simulationType: Yup.string().required(t_i18n('This field is required')), + platforms: Yup.array().min(1, t_i18n('Minimum one platform')).required(t_i18n('This field is required')), + architecture: Yup.string().required(t_i18n('This field is required')), + interval: Yup.number().required(t_i18n('This field is required')).positive(t_i18n('Interval must be a positive number')).integer(t_i18n('Interval must be an integer')), + selection: Yup.string().required(t_i18n('This field is required')), + }, mandatoryAttributes); + + const simulationGenerationValidator = useDynamicSchemaCreationValidation( + mandatoryAttributes, + basicShape, + ); + + const platformOptions = [ + { label: 'Windows', value: 'Windows' }, + { label: 'Linux', value: 'Linux' }, + { label: 'MacOS', value: 'MacOS' }, + ]; const renderForm = () => { return ( - <> - - {t_i18n('Simulation type')} - - - {(simulationType !== 'simulated') && ( - <> - {!hasAttackPatterns && ( - { + handleGenerate(values); + }} + > + {({ values }) => ( +
+
+ - {t_i18n('Technical (payloads) require attack patterns in this entity.')} - - )} - - platforms.includes(platform.value))} - onChange={(_event, newValue) => { - const newSelectedValues = newValue.map((platform) => platform.value); - setPlatforms(newSelectedValues); - }} - getOptionLabel={(option) => option.label} - renderInput={(params) => ( - - )} - renderOption={(props, option) => ( -
  • -
    {option.label ?? ''}
    -
  • + + {t_i18n('Technical (payloads)')} + + + {t_i18n('Simulated emails (generated by AI)')} + + + {t_i18n('Mixed (both)')} + +
    +
    + + {values.simulationType !== 'simulated' && ( + <> + {!hasAttackPatterns && ( + + {t_i18n('Technical (payloads) require attack patterns in this entity.')} + )} - disabled={!hasAttackPatterns} + +
    + setPlatforms(newValue)} + renderOption={(props, option) => ( +
  • +
    {option.label ?? ''}
    +
  • + )} + disabled={!hasAttackPatterns} + /> +
    + +
    + + x86_64 + arm64 + +
    + + )} + +
    + - - - {t_i18n('Targeted architecture')} - - - + {t_i18n('Cancel')} + + +
    +
    )} - setInterval(Number.isNaN(parseInt(event.target.value, 10)) ? 1 : parseInt(event.target.value, 10))} - style={fieldSpacingContainerStyle} - disabled={!canGenerateScenario()} - /> - - {t_i18n('Number of injects generated by attack pattern and platform')} - - -
    - - -
    - + ); }; + const renderCooking = () => { return (
    @@ -607,6 +631,7 @@ const StixCoreObjectSimulationResult = ({ id, type }) => {
    ); }; + const renderResult = () => { return ( <> @@ -637,15 +662,15 @@ const StixCoreObjectSimulationResult = ({ id, type }) => { ); }; + const renderResultError = () => { return ( - <> - } severity="error"> - {resultError} - - + } severity="error"> + {resultError} + ); }; + return ( <> {!oBasDisableDisplay && ( diff --git a/opencti-platform/opencti-graphql/src/modules/xtm/xtm-domain.js b/opencti-platform/opencti-graphql/src/modules/xtm/xtm-domain.js index 09eb7856d1c4..e2ce5ba41fd2 100644 --- a/opencti-platform/opencti-graphql/src/modules/xtm/xtm-domain.js +++ b/opencti-platform/opencti-graphql/src/modules/xtm/xtm-domain.js @@ -12,15 +12,15 @@ import { ENTITY_TYPE_INTRUSION_SET, ENTITY_TYPE_THREAT_ACTOR_GROUP } from '../../schema/stixDomainObject'; -import { UnsupportedError } from '../../config/errors'; import conf, { logApp } from '../../config/conf'; import { + createInjectInScenario, createInjectInScenario as obasCreateInjectInScenario, createScenario as obasCreateScenario, getAttackPatterns as obasGetAttackPatterns, - getInjectorContracts as obasGetInjectorContracts, - getKillChainPhases as obasGetKillChainPhases, + getKillChainPhases, getScenarioResult as obasGetScenarioResult, + searchInjectorContracts } from '../../database/xtm-obas'; import { isNotEmptyField } from '../../database/utils'; import { checkEnterpriseEdition } from '../../utils/ee'; @@ -98,33 +98,34 @@ export const resolveContent = async (context, user, stixCoreObject) => { const result = [...names, ...descriptions, ...files.map((n) => n.content)].join(' '); return result; }; -const generateTechnicalAttackPattern = async (obasAttackPattern, selection, simulationType, obasScenario, dependsOnDuration, interval) => { - let dependsOnDurationLocal = dependsOnDuration; - const obasInjectorContracts = await obasGetInjectorContracts(obasAttackPattern.attack_pattern_id); - let finalObasInjectorContracts = R.take(5, getShuffledArr(obasInjectorContracts)); - if (selection === 'random') { - finalObasInjectorContracts = R.take(1, finalObasInjectorContracts); - } - if (simulationType === 'technical') { - // eslint-disable-next-line no-restricted-syntax - for (const finalObasInjectorContract of finalObasInjectorContracts) { - const obasInjectorContractContent = JSON.parse(finalObasInjectorContract.injector_contract_content); - const title = `[${obasAttackPattern.attack_pattern_external_id}] ${obasAttackPattern.attack_pattern_name} - ${finalObasInjectorContract.injector_contract_labels.en}`; - await obasCreateInjectInScenario( - obasScenario.scenario_id, - obasInjectorContractContent.config.type, - finalObasInjectorContract.injector_contract_id, - title, - dependsOnDurationLocal, - null, - [{ value: 'opencti', color: '#001bda' }, { value: 'technical', color: '#b9461a' }] - ); - dependsOnDurationLocal += (interval * 60); - } - } else { - // TODO - logApp.info(`[OPENCTI-MODULE][XTM] simulationType ${simulationType} not implemented yet.`); - } + +const generateTechnicalAttackPattern = async (obasAttackPattern, finalObasInjectorContract, scenarioId, dependsOnDuration) => { + const obasInjectorContractContent = JSON.parse(finalObasInjectorContract.injector_contract_content); + const title = `[${obasAttackPattern.attack_pattern_external_id}] ${obasAttackPattern.attack_pattern_name} - ${finalObasInjectorContract.injector_contract_labels.en}`; + await obasCreateInjectInScenario( + scenarioId, + obasInjectorContractContent.config.type, + finalObasInjectorContract.injector_contract_id, + title, + dependsOnDuration, + null, + [{ value: 'opencti', color: '#001bda' }, { value: 'technical', color: '#b9461a' }] + ); +}; + +const generatePlaceholder = async (externalId, platforms, architecture, scenarioId, dependsOnDuration) => { + const title = `[${externalId}] Placeholder - ${platforms.join(',')} ${architecture}`; + await createInjectInScenario( + scenarioId, + 'openbas_manual', + 'd02e9132-b9d0-4daa-b3b1-4b9871f8472c', + title, + dependsOnDuration, + null, + [{ value: 'opencti', color: '#001bda' }, { value: 'technical', color: '#b9461a' }], + false, + `This placeholder is disabled because the TTP ${externalId} with platforms ${platforms.join(',')} and architecture ${architecture} is currently not covered. Please create the contracts for the missing TTPs`, + ); }; const generateAttackPatternEmail = async (obasAttackPattern, killChainPhaseName, killChainPhasesListOfNames, content, user, obasScenario, dependsOnDuration) => { @@ -311,9 +312,23 @@ const generateKillChainEmail = async (killChainPhaseName, killChainPhasesListOfN ); }; -export const generateOpenBasScenario = async (context, user, stixCoreObject, attackPatterns, labels, author, simulationType, interval, selection, useAI) => { +export const generateOpenBasScenario = async ( + context, + user, + stixCoreObject, + attackPatterns, + labels, + author, + simulationConfig +) => { + const { interval, selection, simulationType = 'technical', platforms = ['Windows'], architecture = 'x86_64' } = simulationConfig; + + if (simulationType !== 'technical') { + await checkEnterpriseEdition(context); + } + const startingTime = new Date().getTime(); - logApp.info('[OPENCTI-MODULE][XTM] Starting to generate OBAS scenario', { useAI, simulationType }); + logApp.info('[OPENCTI-MODULE][XTM] Starting to generate OBAS scenario', { simulationType }); const content = await resolveContent(context, user, stixCoreObject); const finalAttackPatterns = R.take(RESOLUTION_LIMIT, attackPatterns); @@ -322,7 +337,7 @@ export const generateOpenBasScenario = async (context, user, stixCoreObject, att const description = extractRepresentativeDescription(stixCoreObject); const subtitle = `Based on cyber threat knowledge authored by ${author.name}`; - // call to obas + // call to OpenBAS const obasScenario = await obasCreateScenario( name, subtitle, @@ -334,19 +349,18 @@ export const generateOpenBasScenario = async (context, user, stixCoreObject, att ); // Get kill chain phases - const sortByPhaseOrder = R.sortBy(R.prop('phase_order')); - const obasKillChainPhases = await obasGetKillChainPhases(); // Why it's not called only inside if (attackPatterns.length === 0) ?? - const sortedObasKillChainPhases = sortByPhaseOrder(obasKillChainPhases); + const obasKillChainPhases = await getKillChainPhases(); + const sortedObasKillChainPhases = obasKillChainPhases.sort((a, b) => a.phase_order - b.phase_order); const killChainPhasesListOfNames = sortedObasKillChainPhases.map((n) => n.phase_name).join(', '); - const indexedSortedObasKillChainPhase = R.indexBy(R.prop('phase_id'), sortedObasKillChainPhases); + const indexedSortedObasKillChainPhase = sortedObasKillChainPhases.reduce((acc, phase) => { + acc[phase.phase_id] = phase; + return acc; + }, {}); const createAndInjectScenarioPromises = []; let dependsOnDuration = 0; - if (attackPatterns.length === 0) { - if (!useAI) { - throw UnsupportedError('No attack pattern associated to this entity. Please use AI to generate the scenario. This feature will be enhanced in the future to cover more types of entities.'); - } + if (simulationType !== 'technical' && attackPatterns.length === 0) { // eslint-disable-next-line no-restricted-syntax for (const obasKillChainPhase of sortedObasKillChainPhases) { const killChainPhaseName = obasKillChainPhase.phase_name; @@ -358,6 +372,7 @@ export const generateOpenBasScenario = async (context, user, stixCoreObject, att } else { logApp.debug('[OPENCTI-MODULE][XTM] attack pattern found, no generation of kill chain phase email'); } + // Get contracts from OpenBAS related to found attack patterns // Get attack patterns @@ -370,13 +385,20 @@ export const generateOpenBasScenario = async (context, user, stixCoreObject, att const filteredObasAttackPatterns = obasAttackPatterns.filter((n) => attackPatternsMitreIds.includes(n.attack_pattern_external_id)); // Enrich with the earliest kill chain phase - const enrichedFilteredObasAttackPatterns = filteredObasAttackPatterns.map( - (n) => R.assoc('attack_pattern_kill_chain_phase', sortByPhaseOrder(n.attack_pattern_kill_chain_phases.map((o) => indexedSortedObasKillChainPhase[o])).at(0), n) - ); + const enrichedFilteredObasAttackPatterns = filteredObasAttackPatterns.map((n) => { + const earliestKillChainPhase = n.attack_pattern_kill_chain_phases + .map((phaseId) => indexedSortedObasKillChainPhase[phaseId]) + .sort((a, b) => a.phase_order - b.phase_order)[0]; + return { ...n, attack_pattern_kill_chain_phase: earliestKillChainPhase }; + }); // Sort attack pattern by kill chain phase - const sortByKillChainPhase = R.sortBy(R.path(['attack_pattern_kill_chain_phase', 'phase_order'])); - const sortedEnrichedFilteredObasAttackPatterns = sortByKillChainPhase(enrichedFilteredObasAttackPatterns); + const sortedEnrichedFilteredObasAttackPatterns = enrichedFilteredObasAttackPatterns.sort((a, b) => { + return a.attack_pattern_kill_chain_phase.phase_order - b.attack_pattern_kill_chain_phase.phase_order; + }); + + // Initialize an array to collect attack patterns without contracts + const attackPatternsWithoutInjectorContracts = []; // Get the injector contracts // eslint-disable-next-line no-restricted-syntax @@ -392,53 +414,77 @@ export const generateOpenBasScenario = async (context, user, stixCoreObject, att ); dependsOnDuration += (interval * 60); } else { - createAndInjectScenarioPromises.push(generateTechnicalAttackPattern(obasAttackPattern, selection, simulationType, obasScenario, dependsOnDuration, interval)); - dependsOnDuration += (interval * 60); + const obasInjectorContracts = await searchInjectorContracts(obasAttackPattern.attack_pattern_external_id, platforms, architecture); + + if (obasInjectorContracts.length === 0) { + attackPatternsWithoutInjectorContracts.push(obasAttackPattern.attack_pattern_external_id); + logApp.info(`[OPENCTI-MODULE][XTM] No injector contracts available for this attack pattern ${obasAttackPattern.attack_pattern_external_id}`); + createAndInjectScenarioPromises.push(generatePlaceholder(obasAttackPattern.externalId, platforms, architecture, obasScenario.scenario_id, dependsOnDuration)); + dependsOnDuration += (interval * 60); + } else { + let finalObasInjectorContracts = getShuffledArr(obasInjectorContracts).slice(0, 5); + if (selection === 'random') { + finalObasInjectorContracts = finalObasInjectorContracts.slice(0, 1); + } + if (simulationType === 'technical') { + // eslint-disable-next-line no-restricted-syntax + for (const finalObasInjectorContract of finalObasInjectorContracts) { + createAndInjectScenarioPromises.push(generateTechnicalAttackPattern(obasAttackPattern, finalObasInjectorContract, obasScenario.scenario_id, dependsOnDuration)); + dependsOnDuration += (interval * 60); + } + } else { + // TODO + logApp.info(`[OPENCTI-MODULE][XTM] simulationType ${simulationType} not implemented yet.`); + } + } } } // end loop for - await Promise.all(createAndInjectScenarioPromises); + + try { + await Promise.all(createAndInjectScenarioPromises); + } catch (error) { + logApp.error('[OPENCTI-MODULE][XTM] Error in scenario generation', { error }); + } const endingTime = new Date().getTime(); const totalTime = endingTime - startingTime; if (totalTime > 120000) { - logApp.warn(`[OPENCTI-MODULE][XTM] Long scenario generation time. Generating ${createAndInjectScenarioPromises.length} emails took ${totalTime} ms`, { useAI, simulationType }); + logApp.warn(`[OPENCTI-MODULE][XTM] Long scenario generation time. Generating ${createAndInjectScenarioPromises.length} emails took ${totalTime} ms`, { simulationType }); } - logApp.info(`[OPENCTI-MODULE][XTM] Generating ${createAndInjectScenarioPromises.length} emails took ${totalTime} ms`, { useAI, simulationType }); - return `${XTM_OPENBAS_URL}/admin/scenarios/${obasScenario.scenario_id}/injects`; + logApp.info(`[OPENCTI-MODULE][XTM] Generating ${createAndInjectScenarioPromises.length} emails took ${totalTime} ms`, { simulationType }); + + return { + urlResponse: `${XTM_OPENBAS_URL}/admin/scenarios/${obasScenario.scenario_id}/injects`, + attackPatternsWithoutInjectorContracts: attackPatternsWithoutInjectorContracts.join(',') + }; }; export const generateContainerScenario = async (context, user, args) => { if (getDraftContext(context, user)) throw new Error('Cannot generate scenario in draft'); - const { id, interval, selection, simulationType = 'technical', useAI = false } = args; - if (useAI || simulationType !== 'technical') { - await checkEnterpriseEdition(context); - } + const { id, simulationConfig } = args; + const container = await storeLoadById(context, user, id, ENTITY_TYPE_CONTAINER); const author = await listAllToEntitiesThroughRelations(context, user, id, RELATION_CREATED_BY, [ENTITY_TYPE_IDENTITY]); const labels = await listAllToEntitiesThroughRelations(context, user, id, RELATION_OBJECT_LABEL, [ENTITY_TYPE_LABEL]); const attackPatterns = await listAllToEntitiesThroughRelations(context, user, id, RELATION_OBJECT, [ENTITY_TYPE_ATTACK_PATTERN]); - return generateOpenBasScenario(context, user, container, attackPatterns, labels, (author && author.length > 0 ? author.at(0) : 'Unknown'), simulationType, interval, selection, useAI); + return generateOpenBasScenario(context, user, container, attackPatterns, labels, (author && author.length > 0 ? author.at(0) : 'Unknown'), simulationConfig); }; export const generateThreatScenario = async (context, user, args) => { if (getDraftContext(context, user)) throw new Error('Cannot generate scenario in draft'); - const { id, interval, selection, simulationType = 'technical', useAI = false } = args; - if (useAI || simulationType !== 'technical') { - await checkEnterpriseEdition(context); - } + const { id, simulationConfig } = args; + const stixCoreObject = await storeLoadById(context, user, id, ABSTRACT_STIX_DOMAIN_OBJECT); const labels = await listAllToEntitiesThroughRelations(context, user, id, RELATION_OBJECT_LABEL, [ENTITY_TYPE_LABEL]); const author = await listAllToEntitiesThroughRelations(context, user, id, RELATION_CREATED_BY, [ENTITY_TYPE_IDENTITY]); const attackPatterns = await listAllToEntitiesThroughRelations(context, user, id, RELATION_USES, [ENTITY_TYPE_ATTACK_PATTERN]); - return generateOpenBasScenario(context, user, stixCoreObject, attackPatterns, labels, (author && author.length > 0 ? author.at(0) : 'Unknown'), simulationType, interval, selection, useAI); + return generateOpenBasScenario(context, user, stixCoreObject, attackPatterns, labels, (author && author.length > 0 ? author.at(0) : 'Unknown'), simulationConfig); }; export const generateVictimScenario = async (context, user, args) => { if (getDraftContext(context, user)) throw new Error('Cannot generate scenario in draft'); - const { id, interval, selection, simulationType = 'technical', useAI = false } = args; - if (useAI || simulationType !== 'technical') { - await checkEnterpriseEdition(context); - } + const { id, simulationConfig } = args; + const stixCoreObject = await storeLoadById(context, user, id, ABSTRACT_STIX_DOMAIN_OBJECT); const labels = await listAllToEntitiesThroughRelations(context, user, id, RELATION_OBJECT_LABEL, [ENTITY_TYPE_LABEL]); const author = await listAllToEntitiesThroughRelations(context, user, id, RELATION_CREATED_BY, [ENTITY_TYPE_IDENTITY]); @@ -457,5 +503,5 @@ export const generateVictimScenario = async (context, user, args) => { ); const threatsIds = threats.map((n) => n.id); const attackPatterns = await listAllToEntitiesThroughRelations(context, user, threatsIds, RELATION_USES, [ENTITY_TYPE_ATTACK_PATTERN]); - return generateOpenBasScenario(context, user, stixCoreObject, attackPatterns, labels, (author && author.length > 0 ? author.at(0) : 'Unknown'), simulationType, interval, selection, useAI); + return generateOpenBasScenario(context, user, stixCoreObject, attackPatterns, labels, (author && author.length > 0 ? author.at(0) : 'Unknown'), simulationConfig); };