1 | | - | import React, { Component } from 'react'; |
2 | | - | import * as PropTypes from 'prop-types'; |
3 | | - | import { createFragmentContainer, graphql } from 'react-relay'; |
4 | | - | import { Field, Form, Formik } from 'formik'; |
5 | | - | import withStyles from '@mui/styles/withStyles'; |
6 | | - | import { assoc, compose, difference, head, map, pathOr, pick, pipe, split, } from 'ramda'; |
7 | | - | import * as Yup from 'yup'; |
8 | | - | import inject18n from '../../../../components/i18n'; |
9 | | - | import TextField from '../../../../components/TextField'; |
10 | | - | import { SubscriptionFocus } from '../../../../components/Subscription'; |
11 | | - | import { commitMutation } from '../../../../relay/environment'; |
12 | | - | import CreatedByField from '../../common/form/CreatedByField'; |
13 | | - | import ObjectMarkingField from '../../common/form/ObjectMarkingField'; |
14 | | - | import MarkDownField from '../../../../components/MarkDownField'; |
15 | | - | import { convertCreatedBy, convertMarkings, convertStatus, } from '../../../../utils/Edition'; |
16 | | - | import StatusField from '../../common/form/StatusField'; |
17 | | - | |
18 | | - | const styles = (theme) => ({ |
19 | | - | drawerPaper: { |
20 | | - | minHeight: '100vh', |
21 | | - | width: '50%', |
22 | | - | position: 'fixed', |
23 | | - | overflow: 'hidden', |
24 | | - | |
25 | | - | transition: theme.transitions.create('width', { |
26 | | - | easing: theme.transitions.easing.sharp, |
27 | | - | duration: theme.transitions.duration.enteringScreen, |
28 | | - | }), |
29 | | - | padding: '30px 30px 30px 30px', |
30 | | - | }, |
31 | | - | createButton: { |
32 | | - | position: 'fixed', |
33 | | - | bottom: 30, |
34 | | - | right: 30, |
35 | | - | }, |
36 | | - | importButton: { |
37 | | - | position: 'absolute', |
38 | | - | top: 30, |
39 | | - | right: 30, |
40 | | - | }, |
41 | | - | }); |
42 | | - | |
43 | | - | const dataComponentMutationFieldPatch = graphql` |
44 | | - | mutation DataComponentEditionOverviewFieldPatchMutation( |
45 | | - | $id: ID! |
46 | | - | $input: [EditInput]! |
47 | | - | $commitMessage: String |
48 | | - | $references: [String] |
49 | | - | ) { |
50 | | - | dataComponentFieldPatch( |
51 | | - | id: $id |
52 | | - | input: $input |
53 | | - | commitMessage: $commitMessage |
54 | | - | references: $references |
55 | | - | ) { |
56 | | - | ...DataComponentEditionOverview_dataComponent |
57 | | - | ...DataComponent_dataComponent |
58 | | - | } |
59 | | - | } |
60 | | - | `; |
61 | | - | |
62 | | - | export const dataComponentEditionOverviewFocus = graphql` |
63 | | - | mutation DataComponentEditionOverviewFocusMutation($id: ID!, $input: EditContext!) { |
64 | | - | dataComponentContextPatch(id: $id, input: $input) { |
65 | | - | id |
66 | | - | } |
67 | | - | } |
68 | | - | `; |
69 | | - | |
70 | | - | const dataComponentMutationRelationAdd = graphql` |
71 | | - | mutation DataComponentEditionOverviewRelationAddMutation( |
72 | | - | $id: ID! |
73 | | - | $input: StixMetaRelationshipAddInput! |
74 | | - | ) { |
75 | | - | dataComponentRelationAdd(id: $id, input: $input) { |
76 | | - | from { |
77 | | - | ...DataComponentEditionOverview_dataComponent |
78 | | - | } |
79 | | - | } |
80 | | - | } |
81 | | - | `; |
82 | | - | |
83 | | - | const dataComponentMutationRelationDelete = graphql` |
84 | | - | mutation DataComponentEditionOverviewRelationDeleteMutation( |
85 | | - | $id: ID! |
86 | | - | $toId: StixRef! |
87 | | - | $relationship_type: String! |
88 | | - | ) { |
89 | | - | dataComponentRelationDelete( |
90 | | - | id: $id |
91 | | - | toId: $toId |
92 | | - | relationship_type: $relationship_type |
93 | | - | ) { |
94 | | - | ...DataComponentEditionOverview_dataComponent |
95 | | - | } |
96 | | - | } |
97 | | - | `; |
98 | | - | |
99 | | - | const dataComponentValidation = (t) => Yup.object() |
100 | | - | .shape({ |
101 | | - | name: Yup.string() |
102 | | - | .required(t('This field is required')), |
103 | | - | description: Yup.string() |
104 | | - | .min(3, t('The value is too short')) |
105 | | - | .max(5000, t('The value is too long')) |
106 | | - | .required(t('This field is required')), |
107 | | - | x_opencti_threat_hunting: Yup.string() |
108 | | - | .nullable(), |
109 | | - | x_opencti_log_sources: Yup.string() |
110 | | - | .nullable(), |
111 | | - | references: Yup.array() |
112 | | - | .required(t('This field is required')), |
113 | | - | x_opencti_workflow_id: Yup.object(), |
114 | | - | x_mitre_id: Yup.string() |
115 | | - | .nullable(), |
116 | | - | }); |
117 | | - | |
118 | | - | class DataComponentEditionOverviewComponent extends Component { |
119 | | - | handleChangeFocus(name) { |
120 | | - | commitMutation({ |
121 | | - | mutation: dataComponentEditionOverviewFocus, |
122 | | - | variables: { |
123 | | - | id: this.props.dataComponent.id, |
124 | | - | input: { |
125 | | - | focusOn: name, |
126 | | - | }, |
127 | | - | }, |
128 | | - | }); |
129 | | - | } |
130 | | - | |
131 | | - | handleSubmitField(name, value) { |
132 | | - | let finalValue = value; |
133 | | - | if (name === 'x_opencti_log_sources') { |
134 | | - | finalValue = split('\n', value); |
135 | | - | } |
136 | | - | if (name === 'x_opencti_workflow_id') { |
137 | | - | finalValue = value.value; |
138 | | - | } |
139 | | - | dataComponentValidation(this.props.t) |
140 | | - | .validateAt(name, { [name]: value }) |
141 | | - | .then(() => { |
142 | | - | commitMutation({ |
143 | | - | mutation: dataComponentMutationFieldPatch, |
144 | | - | variables: { |
145 | | - | id: this.props.dataComponent.id, |
146 | | - | input: { |
147 | | - | key: name, |
148 | | - | value: finalValue ?? '' |
149 | | - | }, |
150 | | - | }, |
151 | | - | }); |
152 | | - | }) |
153 | | - | .catch(() => false); |
154 | | - | } |
155 | | - | |
156 | | - | handleChangeCreatedBy(name, value) { |
157 | | - | if (!this.props.enableReferences) { |
158 | | - | commitMutation({ |
159 | | - | mutation: dataComponentMutationFieldPatch, |
160 | | - | variables: { |
161 | | - | id: this.props.dataComponent.id, |
162 | | - | input: { |
163 | | - | key: 'createdBy', |
164 | | - | value: value.value || '' |
165 | | - | }, |
166 | | - | }, |
167 | | - | }); |
168 | | - | } |
169 | | - | } |
170 | | - | |
171 | | - | handleChangeObjectMarking(name, values) { |
172 | | - | const { dataComponent } = this.props; |
173 | | - | const currentMarkingDefinitions = pipe( |
174 | | - | pathOr([], ['objectMarking', 'edges']), |
175 | | - | map((n) => ({ |
176 | | - | label: n.node.definition, |
177 | | - | value: n.node.id, |
178 | | - | })), |
179 | | - | )(dataComponent); |
180 | | - | |
181 | | - | const added = difference(values, currentMarkingDefinitions); |
182 | | - | const removed = difference(currentMarkingDefinitions, values); |
183 | | - | |
184 | | - | if (added.length > 0) { |
185 | | - | commitMutation({ |
186 | | - | mutation: dataComponentMutationRelationAdd, |
187 | | - | variables: { |
188 | | - | id: this.props.dataComponent.id, |
189 | | - | input: { |
190 | | - | toId: head(added).value, |
191 | | - | relationship_type: 'object-marking', |
192 | | - | }, |
193 | | - | }, |
194 | | - | }); |
195 | | - | } |
196 | | - | |
197 | | - | if (removed.length > 0) { |
198 | | - | commitMutation({ |
199 | | - | mutation: dataComponentMutationRelationDelete, |
200 | | - | variables: { |
201 | | - | id: this.props.dataComponent.id, |
202 | | - | toId: head(removed).value, |
203 | | - | relationship_type: 'object-marking', |
204 | | - | }, |
205 | | - | }); |
206 | | - | } |
207 | | - | } |
208 | | - | |
209 | | - | render() { |
210 | | - | const { |
211 | | - | t, |
212 | | - | dataComponent, |
213 | | - | context |
214 | | - | } = this.props; |
215 | | - | const createdBy = convertCreatedBy(dataComponent); |
216 | | - | const objectMarking = convertMarkings(dataComponent); |
217 | | - | const status = convertStatus(t, dataComponent); |
218 | | - | const initialValues = pipe( |
219 | | - | assoc('createdBy', createdBy), |
220 | | - | assoc('objectMarking', objectMarking), |
221 | | - | assoc('x_opencti_workflow_id', status), |
222 | | - | pick([ |
223 | | - | 'name', |
224 | | - | 'description', |
225 | | - | 'x_mitre_id', |
226 | | - | 'x_opencti_threat_hunting', |
227 | | - | 'x_opencti_log_sources', |
228 | | - | 'createdBy', |
229 | | - | 'killChainPhases', |
230 | | - | 'objectMarking', |
231 | | - | 'x_opencti_workflow_id', |
232 | | - | 'x_mitre_id', |
233 | | - | ]), |
234 | | - | )(dataComponent); |
235 | | - | return ( |
236 | | - | <Formik |
237 | | - | enableReinitialize={true} |
238 | | - | initialValues={initialValues} |
239 | | - | validationSchema={dataComponentValidation(t)} |
240 | | - | onSubmit={() => true} |
241 | | - | > |
242 | | - | {({ setFieldValue }) => ( |
243 | | - | <Form style={{ margin: '20px 0 20px 0' }}> |
244 | | - | <Field |
245 | | - | component={TextField} |
246 | | - | variant="standard" |
247 | | - | name="name" |
248 | | - | label={t('Name')} |
249 | | - | fullWidth={true} |
250 | | - | onFocus={this.handleChangeFocus.bind(this)} |
251 | | - | onSubmit={this.handleSubmitField.bind(this)} |
252 | | - | helperText={ |
253 | | - | <SubscriptionFocus context={context} fieldName="name" /> |
254 | | - | } |
255 | | - | /> |
256 | | - | <Field |
257 | | - | component={TextField} |
258 | | - | variant="standard" |
259 | | - | name="x_mitre_id" |
260 | | - | label={t('External ID')} |
261 | | - | fullWidth={true} |
262 | | - | style={{ marginTop: 20 }} |
263 | | - | onFocus={this.handleChangeFocus.bind(this)} |
264 | | - | onSubmit={this.handleSubmitField.bind(this)} |
265 | | - | helperText={ |
266 | | - | <SubscriptionFocus context={context} fieldName="x_mitre_id" /> |
267 | | - | } |
268 | | - | /> |
269 | | - | <Field |
270 | | - | component={MarkDownField} |
271 | | - | name="description" |
272 | | - | label={t('Description')} |
273 | | - | fullWidth={true} |
274 | | - | multiline={true} |
275 | | - | rows="4" |
276 | | - | style={{ marginTop: 20 }} |
277 | | - | onFocus={this.handleChangeFocus.bind(this)} |
278 | | - | onSubmit={this.handleSubmitField.bind(this)} |
279 | | - | helperText={ |
280 | | - | <SubscriptionFocus context={context} fieldName="description" /> |
281 | | - | } |
282 | | - | /> |
283 | | - | <Field |
284 | | - | component={MarkDownField} |
285 | | - | name="x_opencti_threat_hunting" |
286 | | - | label={t('Threat hunting techniques')} |
287 | | - | fullWidth={true} |
288 | | - | multiline={true} |
289 | | - | rows="4" |
290 | | - | style={{ marginTop: 20 }} |
291 | | - | onFocus={this.handleChangeFocus.bind(this)} |
292 | | - | onSubmit={this.handleSubmitField.bind(this)} |
293 | | - | helperText={ |
294 | | - | <SubscriptionFocus |
295 | | - | context={context} |
296 | | - | fieldName="x_opencti_threat_hunting" |
297 | | - | /> |
298 | | - | } |
299 | | - | /> |
300 | | - | <Field |
301 | | - | component={TextField} |
302 | | - | variant="standard" |
303 | | - | name="x_opencti_log_sources" |
304 | | - | label={t('Log sources (1 / line)')} |
305 | | - | fullWidth={true} |
306 | | - | multiline={true} |
307 | | - | rows="4" |
308 | | - | style={{ marginTop: 20 }} |
309 | | - | onFocus={this.handleChangeFocus.bind(this)} |
310 | | - | onSubmit={this.handleSubmitField.bind(this)} |
311 | | - | helperText={ |
312 | | - | <SubscriptionFocus |
313 | | - | context={context} |
314 | | - | fieldName="x_opencti_log_sources" |
315 | | - | /> |
316 | | - | } |
317 | | - | /> |
318 | | - | {dataComponent.workflowEnabled && ( |
319 | | - | <StatusField |
320 | | - | name="x_opencti_workflow_id" |
321 | | - | type="Course-Of-Action" |
322 | | - | onFocus={this.handleChangeFocus.bind(this)} |
323 | | - | onChange={this.handleSubmitField.bind(this)} |
324 | | - | setFieldValue={setFieldValue} |
325 | | - | style={{ marginTop: 20 }} |
326 | | - | helpertext={ |
327 | | - | <SubscriptionFocus |
328 | | - | context={context} |
329 | | - | fieldName="x_opencti_workflow_id" |
330 | | - | /> |
331 | | - | } |
332 | | - | /> |
333 | | - | )} |
334 | | - | <CreatedByField |
335 | | - | name="createdBy" |
336 | | - | style={{ |
337 | | - | marginTop: 20, |
338 | | - | width: '100%' |
339 | | - | }} |
340 | | - | setFieldValue={setFieldValue} |
341 | | - | helpertext={ |
342 | | - | <SubscriptionFocus context={context} fieldName="createdBy" /> |
343 | | - | } |
344 | | - | onChange={this.handleChangeCreatedBy.bind(this)} |
345 | | - | /> |
346 | | - | <ObjectMarkingField |
347 | | - | name="objectMarking" |
348 | | - | style={{ |
349 | | - | marginTop: 20, |
350 | | - | width: '100%' |
351 | | - | }} |
352 | | - | helpertext={ |
353 | | - | <SubscriptionFocus |
354 | | - | context={context} |
355 | | - | fieldname="objectMarking" |
356 | | - | /> |
357 | | - | } |
358 | | - | onChange={this.handleChangeObjectMarking.bind(this)} |
359 | | - | /> |
360 | | - | </Form> |
361 | | - | )} |
362 | | - | </Formik> |
363 | | - | ); |
364 | | - | } |
365 | | - | } |
366 | | - | |
367 | | - | DataComponentEditionOverviewComponent.propTypes = { |
368 | | - | classes: PropTypes.object, |
369 | | - | theme: PropTypes.object, |
370 | | - | t: PropTypes.func, |
371 | | - | dataComponent: PropTypes.object, |
372 | | - | context: PropTypes.array, |
373 | | - | }; |
374 | | - | |
375 | | - | const DataComponentEditionOverview = createFragmentContainer( |
376 | | - | DataComponentEditionOverviewComponent, |
377 | | - | { |
378 | | - | dataComponent: graphql` |
379 | | - | fragment DataComponentEditionOverview_dataComponent on DataComponent { |
380 | | - | id |
381 | | - | name |
382 | | - | description |
383 | | - | # x_opencti_threat_hunting |
384 | | - | # x_opencti_log_sources |
385 | | - | # x_mitre_id |
386 | | - | createdBy { |
387 | | - | ... on Identity { |
388 | | - | id |
389 | | - | name |
390 | | - | entity_type |
391 | | - | } |
392 | | - | } |
393 | | - | objectMarking { |
394 | | - | edges { |
395 | | - | node { |
396 | | - | id |
397 | | - | definition |
398 | | - | definition_type |
399 | | - | } |
400 | | - | } |
401 | | - | } |
402 | | - | status { |
403 | | - | id |
404 | | - | order |
405 | | - | template { |
406 | | - | name |
407 | | - | color |
408 | | - | } |
409 | | - | } |
410 | | - | workflowEnabled |
411 | | - | } |
412 | | - | `, |
413 | | - | }, |
414 | | - | ); |
415 | | - | |
416 | | - | export default compose( |
417 | | - | inject18n, |
418 | | - | withStyles(styles, { withTheme: true }), |
419 | | - | )(DataComponentEditionOverview); |
420 | | - | |