mirror of
https://github.com/JHUAPL/STIXMODELER_UI.git
synced 2026-01-08 22:07:56 -05:00
Validation and schema management improvements
This commit is contained in:
20
stix-modeler-app/.eslintrc.json
Normal file
20
stix-modeler-app/.eslintrc.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": "airbnb",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"comma-dangle": ["error", {
|
||||
"arrays": "never",
|
||||
"objects": "always",
|
||||
"imports": "never",
|
||||
"exports": "never",
|
||||
"functions": "never"
|
||||
}]
|
||||
}
|
||||
}
|
||||
24
stix-modeler-app/.gitignore
vendored
Normal file
24
stix-modeler-app/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
7
stix-modeler-app/config/config.json
Normal file
7
stix-modeler-app/config/config.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"creator_id": "identity--b085a68a-bf48-4316-9667-37af78cba894",
|
||||
"schema_dir": "/schemas",
|
||||
"schemas": [
|
||||
|
||||
]
|
||||
}
|
||||
22
stix-modeler-app/eslint.config.js
Normal file
22
stix-modeler-app/eslint.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import eslint from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default defineConfig(
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
tseslint.configs.stylistic,
|
||||
globalIgnores([
|
||||
"config/*",
|
||||
"dist/*",
|
||||
"node_modules/*",
|
||||
]),
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
20
stix-modeler-app/index.html
Normal file
20
stix-modeler-app/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Alegreya+Sans+SC:bold|Inconsolata|Anton|Permanent+Marker|Roboto:900|Righteous|Ropa+Sans|Titillium+Web:400,700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Two+Tone" rel="stylesheet">
|
||||
|
||||
<title>STIX 2.1 Modeler</title>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5466
stix-modeler-app/package-lock.json
generated
Normal file
5466
stix-modeler-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
stix-modeler-app/package.json
Normal file
42
stix-modeler-app/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "stix-modeler-app",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"test": "vitest",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"d3-hierarchy": "^3.1.2",
|
||||
"deepmerge": "^4.3.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mobx": "^6.0.0",
|
||||
"mobx-react": "^9.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"prop-types": "^15.8.0",
|
||||
"rc-slider": "^11.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-datepicker": "^8.7.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-tooltip": "^5.29.0",
|
||||
"reactflow": "^11.11.0",
|
||||
"sass": "^1.93.0",
|
||||
"uuid": "13.0.0",
|
||||
"vite": "^7.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"globals": "^16.4.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"typescript-eslint": "^8.46.0",
|
||||
"vitest": "^3.2.0"
|
||||
}
|
||||
}
|
||||
1
stix-modeler-app/public/vite.svg
Normal file
1
stix-modeler-app/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
5
stix-modeler-app/schemas/README.md
Normal file
5
stix-modeler-app/schemas/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Import Schemas #
|
||||
|
||||
Any schemas placed in this folder can be imported via ```config.json```.
|
||||
|
||||
**REMEMBER**: The browser can only import files within ```stix-modeler-app```
|
||||
15
stix-modeler-app/src/App.jsx
Normal file
15
stix-modeler-app/src/App.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Provider } from 'mobx-react';
|
||||
import Canvas from './components/Canvas';
|
||||
import { store } from './stores/Stores';
|
||||
|
||||
import './App.scss';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Canvas />
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
12
stix-modeler-app/src/App.scss
Normal file
12
stix-modeler-app/src/App.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@use './defaults';
|
||||
|
||||
body, html, #app {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
font-family: defaults.$default-font-family;
|
||||
}
|
||||
1118
stix-modeler-app/src/components/Canvas.jsx
Normal file
1118
stix-modeler-app/src/components/Canvas.jsx
Normal file
File diff suppressed because it is too large
Load Diff
437
stix-modeler-app/src/components/Details.jsx
Normal file
437
stix-modeler-app/src/components/Details.jsx
Normal file
@@ -0,0 +1,437 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { toJS } from 'mobx';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import Panel from './ui/panel/Panel';
|
||||
import Slider from './ui/inputs/Slider';
|
||||
import Text from './ui/inputs/Text';
|
||||
import TextArea from './ui/inputs/TextArea';
|
||||
import DateTime from './ui/inputs/DateTime';
|
||||
import ArraySelector from './ui/inputs/ArraySelector';
|
||||
import KillChain from './ui/complex/KillChain';
|
||||
import ExternalReferences from './ui/complex/ExternalReferences';
|
||||
import CSVInput from './ui/inputs/CSVInput';
|
||||
import Boolean from './ui/inputs/Boolean';
|
||||
import GenericObject from './ui/complex/GenericObject';
|
||||
import ConfirmTextarea from './ui/complex/ConfirmTextarea';
|
||||
import ObjectArray from './ui/complex/ObjectArray';
|
||||
|
||||
import Images from '../util/Images';
|
||||
|
||||
import './details.scss';
|
||||
|
||||
class Details extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChangeHandler = this.onChangeHandler.bind(this);
|
||||
this.onChangeDateHandler = this.onChangeDateHandler.bind(this);
|
||||
}
|
||||
|
||||
onChangeHandler(event) {
|
||||
this.props.onChangeNodeHandler(event);
|
||||
}
|
||||
|
||||
onChangeDateHandler(property, datetime) {
|
||||
this.props.onChangeDateHandler(property, datetime);
|
||||
}
|
||||
|
||||
render() {
|
||||
const node = toJS(this.props.node);
|
||||
let props = {};
|
||||
let img;
|
||||
const details = [];
|
||||
|
||||
const deleteIcon = <span className="material-icons">delete_forever</span>;
|
||||
|
||||
if (node.properties) {
|
||||
props = node.properties;
|
||||
img = (
|
||||
<img
|
||||
src={node.customImg ? node.customImg : Images.getImage(node.img)}
|
||||
alt={node.id}
|
||||
width="30"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
for (const prop in props) {
|
||||
const cls = 'item-header';
|
||||
const header = (
|
||||
<div className={cls}>
|
||||
{prop}
|
||||
<span
|
||||
data-tooltip-id={`${prop}-tooltip`}
|
||||
className="material-icons"
|
||||
data-tooltip-content={props[prop].description}
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id={`${prop}-tooltip`} />
|
||||
</div>
|
||||
);
|
||||
|
||||
let control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">{props[prop].value}</div>
|
||||
</div>
|
||||
);
|
||||
// If there is no type, we do not want to process. If a "control"
|
||||
// is defined, that indicates special handling of the value.
|
||||
if (props[prop].type && !props[prop].control) {
|
||||
switch (props[prop].type) {
|
||||
case 'number':
|
||||
case 'string':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<Text
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onChange={this.onChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'timestamp':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<DateTime
|
||||
name={prop}
|
||||
selected={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onTextChange={this.onChangeHandler}
|
||||
onDateChange={this.onChangeDateHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'array':
|
||||
if (props[prop].vocab) {
|
||||
control = (
|
||||
<ArraySelector
|
||||
vocab={props[prop].vocab}
|
||||
key={prop}
|
||||
field={prop}
|
||||
value={props[prop].value}
|
||||
description={props[prop].description}
|
||||
required={props[prop].required}
|
||||
onClickHandler={this.props.onClickArrayHandler}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Get array subtype, possibly nested
|
||||
let refField = props[prop].items;
|
||||
if (Array.isArray(refField)) {
|
||||
refField = refField[0];
|
||||
}
|
||||
const ref = refField.$ref ?? refField.type;
|
||||
|
||||
if (ref.includes('dictionary.json') || ref === 'object') {
|
||||
control = (
|
||||
<ObjectArray
|
||||
node={node}
|
||||
key={prop}
|
||||
field={prop}
|
||||
value={props[prop].value}
|
||||
description={props[prop].description}
|
||||
required={props[prop].required}
|
||||
onClickAddObjectHandler={this.props.onClickAddObjectHandler}
|
||||
onChangeObjectHandler={this.props.onChangeArrayObjectHandler}
|
||||
onClickDeleteArrayObjectHandler={this.props.onClickDeleteArrayObjectHandler}
|
||||
onClickDeleteArrayObjectPropertyHandler={
|
||||
this.props.onClickDeleteArrayObjectPropertyHandler
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
props[prop].control = 'listtextarea';
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<Boolean
|
||||
name={prop}
|
||||
selected={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onClick={this.props.onClickBooleanHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'dictionary':
|
||||
case 'object':
|
||||
control = (
|
||||
<GenericObject
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
description={props[prop].description}
|
||||
key={uuid()}
|
||||
field={prop}
|
||||
required={props[prop].required}
|
||||
onClickAddObjectHandler={
|
||||
this.props.onClickAddGenericObjectHandler
|
||||
}
|
||||
onClickDeleteObjectHandler={
|
||||
this.props.onClickDeleteGenericObjectHandler
|
||||
}
|
||||
onChangeHandler={this.props.onChangeGenericObjectHandler}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (props[prop].$ref && !props[prop].control) {
|
||||
switch (props[prop].$ref) {
|
||||
case 'identifier':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<Text
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onChange={this.onChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (props[prop].control) {
|
||||
case 'hidden':
|
||||
control = '';
|
||||
break;
|
||||
case 'slider':
|
||||
control = (
|
||||
<div className="item slider" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<Slider
|
||||
value={props[prop].value}
|
||||
field={prop}
|
||||
required={props[prop].required}
|
||||
onChangeHandler={this.props.onChangeSliderHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'csv':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<CSVInput
|
||||
key={prop}
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onChangeHandler={this.props.onChangeCSVHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'killchain':
|
||||
control = (
|
||||
<KillChain
|
||||
vocab={props[prop].vocab}
|
||||
node={node}
|
||||
key={prop}
|
||||
field={prop}
|
||||
value={props[prop].value}
|
||||
description={props[prop].description}
|
||||
required={props[prop].required}
|
||||
onChangeHandler={this.props.onChangePhaseHandler}
|
||||
onClickRemoveHandler={this.props.onClickRemovePhaseHander}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'externalrefs':
|
||||
control = (
|
||||
<ExternalReferences
|
||||
node={node}
|
||||
key={prop}
|
||||
field={prop}
|
||||
value={props[prop].value}
|
||||
prefix="node"
|
||||
description={props[prop].description}
|
||||
required={props[prop].required}
|
||||
onClickAddObjectHandler={this.props.onClickAddObjectHandler}
|
||||
onChangeERHandler={this.props.onChangeERHandler}
|
||||
onClickDeleteERHandler={this.props.onClickDeleteERHandler}
|
||||
onClickDeletePropertyHandler={
|
||||
this.props.onClickDeletePropertyHandler
|
||||
}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'stringselector':
|
||||
control = (
|
||||
<ArraySelector
|
||||
vocab={props[prop].vocab}
|
||||
key={prop}
|
||||
field={prop}
|
||||
value={props[prop].value}
|
||||
description={props[prop].description}
|
||||
required={props[prop].required}
|
||||
onClickHandler={this.props.onClickArrayHandler}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'textarea':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<TextArea
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onChange={this.onChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'listtextarea':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<CSVInput
|
||||
key={prop}
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onChangeHandler={this.props.onChangeCSVHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'genericobject':
|
||||
control = (
|
||||
<GenericObject
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
vocab={props[prop].vocab}
|
||||
description={props[prop].description}
|
||||
key={uuid()}
|
||||
field={prop}
|
||||
required={props[prop].required}
|
||||
onClickAddObjectHandler={
|
||||
this.props.onClickAddGenericObjectHandler
|
||||
}
|
||||
onClickDeleteObjectHandler={
|
||||
this.props.onClickDeleteGenericObjectHandler
|
||||
}
|
||||
onChangeHandler={this.props.onChangeGenericObjectHandler}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'confirmtextarea':
|
||||
control = (
|
||||
<ConfirmTextarea
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
description={props[prop].description}
|
||||
key={uuid()}
|
||||
field={prop}
|
||||
required={props[prop].required}
|
||||
onClickAddTextHandler={this.props.onClickAddTextHandler}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
details.push(control);
|
||||
}
|
||||
|
||||
const unknownProperties = Object.keys(props).filter(prop => props[prop].type === 'unknown');
|
||||
if (unknownProperties.length) {
|
||||
const msg = `Import extension schema(s) to enable modification`
|
||||
const header = (
|
||||
<div className="item-header">
|
||||
Unknown Properties
|
||||
<span
|
||||
data-tooltip-id="unknown-tooltip"
|
||||
className="material-icons"
|
||||
data-tooltip-content={msg}
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id="unknown-tooltip" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const propItems = [];
|
||||
const maxLength = 50;
|
||||
for (const prop of unknownProperties) {
|
||||
let value = props[prop].value;
|
||||
value = (typeof value == 'object')? JSON.stringify(value, null, 2) : String(value);
|
||||
value = (value.length > maxLength)? `${value.substring(0, maxLength)}...` : value;
|
||||
|
||||
propItems.push(
|
||||
<div className="item-value">
|
||||
<span className="unknown-header">{"\u2043"} {prop} </span>
|
||||
<span className='unknown-value'>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let control = (
|
||||
<div className="item" key="unknown">
|
||||
{header}
|
||||
<ul className="item-value" id='unknown-properties'>
|
||||
{propItems}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
details.push(control);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="details">
|
||||
<div className="header">
|
||||
<div className="title">
|
||||
{img}
|
||||
{' '}
|
||||
{node.id}
|
||||
</div>
|
||||
<div className="delete" onClick={this.props.onClickDeleteHandler}>
|
||||
{deleteIcon}
|
||||
{' '}
|
||||
<span className="text">Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="body">{details}</div>
|
||||
|
||||
<div className="footer" />
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(Details));
|
||||
115
stix-modeler-app/src/components/FileImporter.jsx
Normal file
115
stix-modeler-app/src/components/FileImporter.jsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import Panel from './ui/panel/Panel';
|
||||
import FileSelector from './ui/inputs/FileSelector';
|
||||
|
||||
import './details.scss';
|
||||
|
||||
class FileImporter extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChangeSchemaHandler = this.onChangeSchemaHandler.bind(this);
|
||||
this.onChangeBundleHandler = this.onChangeBundleHandler.bind(this);
|
||||
}
|
||||
|
||||
async onChangeSchemaHandler(event) {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
const files = Array.from(event.target.files);
|
||||
files.map((file) => {
|
||||
const fr = new FileReader();
|
||||
fr.readAsText(file, 'UTF-8');
|
||||
fr.onload = (e) => {
|
||||
const { result, } = e.target;
|
||||
this.props.onChangeSchemaHandler(result);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async onChangeBundleHandler(event) {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
const files = Array.from(event.target.files);
|
||||
files.map((file) => {
|
||||
const fr = new FileReader();
|
||||
fr.readAsText(file, 'UTF-8');
|
||||
fr.onload = (e) => {
|
||||
const { result, } = e.target;
|
||||
this.props.onChangeBundleHandler(result);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const details = [];
|
||||
|
||||
let header = (
|
||||
<div className="item-header">
|
||||
Import Schemas
|
||||
<span
|
||||
data-tooltip-id="schema-tip"
|
||||
data-tooltip-content="Import custom schema objects from file"
|
||||
className="material-icons"
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id="schema-tip" />
|
||||
</div>
|
||||
);
|
||||
|
||||
let control = (
|
||||
<div className="item" key="schemas">
|
||||
{header}
|
||||
<FileSelector
|
||||
name="schemas"
|
||||
type=".json"
|
||||
multiple
|
||||
onChange={this.onChangeSchemaHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
details.push(control);
|
||||
|
||||
header = (
|
||||
<div className="item-header">
|
||||
Import Bundle
|
||||
<span
|
||||
data-tooltip-id="file-tooltip"
|
||||
data-tooltip-content="Import STIX Bundle from file"
|
||||
className="material-icons"
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id="file-tooltip" />
|
||||
</div>
|
||||
);
|
||||
|
||||
control = (
|
||||
<div className="item" key="bundle">
|
||||
{header}
|
||||
<FileSelector
|
||||
name="bundle"
|
||||
type=".json"
|
||||
multiple={false}
|
||||
onChange={this.onChangeBundleHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
details.push(control);
|
||||
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="details">
|
||||
<div className="body">{details}</div>
|
||||
<div className="footer" />
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(FileImporter));
|
||||
155
stix-modeler-app/src/components/Flow/Flow.jsx
Normal file
155
stix-modeler-app/src/components/Flow/Flow.jsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
React, useState, useEffect, useCallback
|
||||
} from 'react';
|
||||
import ReactFlow, {
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
MarkerType,
|
||||
ConnectionMode,
|
||||
Background
|
||||
} from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import './Flow.scss';
|
||||
|
||||
import FlowNode from './FlowNode';
|
||||
import FlowEdge from './FlowEdge';
|
||||
|
||||
const nodeTypes = {
|
||||
default: FlowNode,
|
||||
};
|
||||
|
||||
const edgeTypes = {
|
||||
default: FlowEdge,
|
||||
};
|
||||
|
||||
const defaultViewport = { x: 0, y: 0, zoom: 1, };
|
||||
|
||||
function Flow(props) {
|
||||
const [reactFlowInstance, setReactFlowInstance] = useState(null);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
const createNode = (node) => {
|
||||
if (!node.position) return;
|
||||
const n = {
|
||||
id: node.id,
|
||||
data: {
|
||||
node
|
||||
},
|
||||
type: 'default',
|
||||
position: { x: node.position.x, y: node.position.y, },
|
||||
style: {
|
||||
width: 50, height: 50, padding: 0, borderColor: 'white',
|
||||
},
|
||||
};
|
||||
setNodes((nds) => nds.concat(n));
|
||||
};
|
||||
|
||||
const createEdge = (source, target, label, id) => {
|
||||
const edge = {
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
sourceHandle: 'center',
|
||||
targetHandle: 'center',
|
||||
label,
|
||||
labelShowBg: false,
|
||||
type: 'default',
|
||||
markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20, },
|
||||
};
|
||||
setEdges((eds) => eds.concat(edge));
|
||||
return edge;
|
||||
};
|
||||
|
||||
const renderNodes = () => {
|
||||
setNodes([]);
|
||||
props.nodes.forEach((node) => {
|
||||
createNode(node);
|
||||
});
|
||||
};
|
||||
|
||||
const renderEdges = () => {
|
||||
setEdges([]);
|
||||
props.edges.forEach((edge) => {
|
||||
createEdge(
|
||||
edge.source_ref,
|
||||
edge.target_ref,
|
||||
edge.relationship_type,
|
||||
edge.id
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const rerender = () => {
|
||||
renderNodes();
|
||||
renderEdges();
|
||||
props.setUpdateFlow(false);
|
||||
};
|
||||
|
||||
const onConnect = useCallback((params) => {
|
||||
props.onConnectNodeHandler(params.source, params.target);
|
||||
}, []);
|
||||
|
||||
const onEdgeClick = (event, edge) => {
|
||||
props.onClickRelHandler(edge.id);
|
||||
};
|
||||
|
||||
const onNodeClick = (event, node) => {
|
||||
if (props.groupMode) {
|
||||
props.onClickGroupNodeHandler(node.id);
|
||||
} else {
|
||||
props.onClickHandler(node.id);
|
||||
}
|
||||
};
|
||||
|
||||
const onNodeDragStop = useCallback((event, node) => {
|
||||
event.preventDefault();
|
||||
props.onDragStopNodeHandler(node);
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
const position = reactFlowInstance.screenToFlowPosition({
|
||||
x: event.clientX + 50,
|
||||
y: event.clientY + 50,
|
||||
});
|
||||
props.setMousePosition(position.x, position.y);
|
||||
},
|
||||
[reactFlowInstance]
|
||||
);
|
||||
|
||||
useEffect(rerender, [props.updateFlow]);
|
||||
useEffect(rerender, [props.updateFlow]);
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<div className="reactflow-wrapper">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onInit={setReactFlowInstance}
|
||||
onNodeClick={onNodeClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onDrop={onDrop}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
defaultViewport={defaultViewport}
|
||||
nodeOrigin={[0.5, 0.5]}
|
||||
attributionPosition="bottom-left"
|
||||
nodesDraggable
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
>
|
||||
<Background color="black" variant="dots" />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Flow;
|
||||
23
stix-modeler-app/src/components/Flow/Flow.scss
Normal file
23
stix-modeler-app/src/components/Flow/Flow.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
.centerHandle {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.react-flow__handle {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.react-flow__handle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.centerHandle:hover {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.reactflow-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
140
stix-modeler-app/src/components/Flow/FlowEdge.jsx
Normal file
140
stix-modeler-app/src/components/Flow/FlowEdge.jsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
BaseEdge, EdgeLabelRenderer, useStore, getBezierPath
|
||||
} from 'reactflow';
|
||||
|
||||
export default function FlowEdge(props) {
|
||||
const getPosition = useStore((store) => {
|
||||
const siblings = [];
|
||||
store.edges.forEach((e) => {
|
||||
if ((e.source === props.source || e.source === props.target)
|
||||
&& (e.target === props.source || e.target === props.target)) {
|
||||
siblings.push(e);
|
||||
}
|
||||
});
|
||||
if (siblings.length === 1) return [0, 1];
|
||||
|
||||
siblings.sort();
|
||||
const index = siblings.map((e) => e.id).indexOf(props.id);
|
||||
|
||||
return [index, siblings.length];
|
||||
});
|
||||
|
||||
const adjustedPosition = (props, position) => {
|
||||
const WIDTH = 50;
|
||||
let { sourceX, } = props;
|
||||
let { sourceY, } = props;
|
||||
let { targetX, } = props;
|
||||
let { targetY, } = props;
|
||||
let source = null;
|
||||
let target = null;
|
||||
|
||||
const isRight = (sourceX - targetX) > WIDTH;
|
||||
const isLeft = (targetX - sourceX) > WIDTH;
|
||||
const isBottom = (sourceY - targetY) > WIDTH;
|
||||
const isTop = (targetY - sourceY) > WIDTH;
|
||||
|
||||
// Calculate connection side base on position
|
||||
if (isBottom) {
|
||||
target = 'bottom';
|
||||
targetY = targetY + WIDTH / 2 + 4;
|
||||
} else if (isTop) {
|
||||
source = 'bottom';
|
||||
sourceY = sourceY + WIDTH / 2 + 4;
|
||||
}
|
||||
|
||||
if (isLeft) {
|
||||
if (source === null) {
|
||||
source = 'right';
|
||||
sourceX += WIDTH / 2;
|
||||
}
|
||||
if (target === null) {
|
||||
target = 'left';
|
||||
targetX -= WIDTH / 2;
|
||||
}
|
||||
} else if (isRight) {
|
||||
if (source === null) {
|
||||
source = 'left';
|
||||
sourceX -= WIDTH / 2;
|
||||
}
|
||||
if (target === null) {
|
||||
target = 'right';
|
||||
targetX += WIDTH / 2;
|
||||
}
|
||||
} else {
|
||||
if (source === null) {
|
||||
source = 'top';
|
||||
sourceY = sourceY - WIDTH / 2 + 4;
|
||||
}
|
||||
if (target === null) {
|
||||
target = 'top';
|
||||
targetY = targetY - WIDTH / 2 + 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust edge positioning to prevent overlap
|
||||
const index = position[0] + 1;
|
||||
const partitions = position[1];
|
||||
const sectionWidth = WIDTH / partitions;
|
||||
const offset = index * sectionWidth - (sectionWidth / 2);
|
||||
if (source === 'top' || source === 'bottom') {
|
||||
sourceX = (sourceX - WIDTH / 2) + offset;
|
||||
} else if (target === 'left' || target === 'right') {
|
||||
sourceY = (sourceY - WIDTH / 2) + offset;
|
||||
}
|
||||
|
||||
if (target === 'top' || target === 'bottom') {
|
||||
targetX = (targetX - WIDTH / 2) + offset;
|
||||
} else if (target === 'left' || target === 'right') {
|
||||
targetY = (targetY - WIDTH / 2) + offset;
|
||||
}
|
||||
|
||||
return {
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: source,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition: target,
|
||||
};
|
||||
};
|
||||
|
||||
const getLabel = (props, labelX, labelY) => {
|
||||
const yt = props.targetY;
|
||||
const ys = props.sourceY;
|
||||
const xt = props.targetX;
|
||||
const xs = props.sourceX;
|
||||
|
||||
let rotation;
|
||||
if (xt === xs) {
|
||||
rotation = (yt > ys) ? '90deg' : '-90deg';
|
||||
} else {
|
||||
rotation = `${Math.atan((yt - ys) / (xt - xs))}rad`;
|
||||
}
|
||||
|
||||
return (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px) rotate(${rotation}) `,
|
||||
}}
|
||||
>
|
||||
{props.label}
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
const adjusted = adjustedPosition(props, getPosition);
|
||||
const [path, labelX, labelY] = getBezierPath(adjusted);
|
||||
const label = getLabel(props, labelX, labelY);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge path={path} {...props} markerEnd={props.markerEnd} />
|
||||
;
|
||||
{label}
|
||||
</>
|
||||
);
|
||||
}
|
||||
73
stix-modeler-app/src/components/Flow/FlowNode.jsx
Normal file
73
stix-modeler-app/src/components/Flow/FlowNode.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import Images from '../../util/Images';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './FlowNode.scss';
|
||||
|
||||
export default class FlowNode extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { node, } = this.props.data;
|
||||
let display = node.id.split('--')[0];
|
||||
|
||||
let cls = classNames({
|
||||
'node-item': true,
|
||||
'selected': node.selected,
|
||||
});
|
||||
|
||||
let labelCls = classNames({
|
||||
"node-label": true,
|
||||
})
|
||||
|
||||
if (node.properties.name && node.properties.name.value) {
|
||||
display = node.properties.name.value;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className={cls}
|
||||
style={{
|
||||
backgroundImage: `url(${node.customImg ? node.customImg : Images.getImage(node.img)})`,
|
||||
}}
|
||||
/>
|
||||
<Handle
|
||||
className="centerHandle"
|
||||
id="center"
|
||||
type="source"
|
||||
isConnectable={this.props.isConnectable}
|
||||
/>
|
||||
<Handle
|
||||
position={Position.Left}
|
||||
id="left"
|
||||
type="source"
|
||||
isConnectable={this.props.isConnectable}
|
||||
/>
|
||||
<Handle
|
||||
position={Position.Right}
|
||||
id="right"
|
||||
type="source"
|
||||
isConnectable={this.props.isConnectable}
|
||||
/>
|
||||
<Handle
|
||||
position={Position.Top}
|
||||
id="top"
|
||||
type="source"
|
||||
isConnectable={this.props.isConnectable}
|
||||
/>
|
||||
<Handle
|
||||
position={Position.Bottom}
|
||||
id="bottom"
|
||||
type="source"
|
||||
isConnectable={this.props.isConnectable}
|
||||
/>
|
||||
|
||||
<div className={labelCls}>
|
||||
{display}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
25
stix-modeler-app/src/components/Flow/FlowNode.scss
Normal file
25
stix-modeler-app/src/components/Flow/FlowNode.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
@use '../../defaults';
|
||||
|
||||
.node-label {
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
bottom: -100%;
|
||||
line-height: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.node-item {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
background-size: contain;
|
||||
background-repeat: 'no-repeat';
|
||||
}
|
||||
|
||||
.selected {
|
||||
border: 1px solid blue;
|
||||
}
|
||||
|
||||
.hovered {
|
||||
border: 1px solid red;
|
||||
}
|
||||
82
stix-modeler-app/src/components/SubmissionError.jsx
Normal file
82
stix-modeler-app/src/components/SubmissionError.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Panel from './ui/panel/Panel';
|
||||
import Images from '../util/Images';
|
||||
|
||||
import './SubmissionError.scss';
|
||||
|
||||
class SubmissionError extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const errorStructure = {};
|
||||
const msg = [];
|
||||
|
||||
this.props.error.map((item, i) => {
|
||||
if (!(item.node in errorStructure)){
|
||||
errorStructure[item.node] = {};
|
||||
errorStructure[item.node].name = item.name;
|
||||
errorStructure[item.node].details = [];
|
||||
errorStructure[item.node].img = item.img;
|
||||
errorStructure[item.node].details.push({
|
||||
msg: item.msg,
|
||||
property: item.property,
|
||||
});
|
||||
} else {
|
||||
errorStructure[item.node].details.push({
|
||||
msg: item.msg,
|
||||
property: item.property,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
for (const item in errorStructure) {
|
||||
const details = [];
|
||||
const name = errorStructure[item].name;
|
||||
if (errorStructure[item].details) {
|
||||
errorStructure[item].details.map((detail) => {
|
||||
details.push(
|
||||
<div key={detail.property} className="row">
|
||||
<span>
|
||||
{detail.property}
|
||||
:
|
||||
</span>
|
||||
{' '}
|
||||
{detail.msg}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
msg.push(
|
||||
<div className='submission-item' key={item} onClick={() => this.props.onClickNodeHandler(item)}>
|
||||
<div className="container-header">
|
||||
<img src={Images.getImage(errorStructure[item].img)} width="30" />
|
||||
{' '}
|
||||
{name}
|
||||
</div>
|
||||
<div className="rows-container">
|
||||
{details}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="header">
|
||||
Errors
|
||||
</div>
|
||||
<div className="submission-error">
|
||||
{msg}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(SubmissionError));
|
||||
42
stix-modeler-app/src/components/SubmissionError.scss
Normal file
42
stix-modeler-app/src/components/SubmissionError.scss
Normal file
@@ -0,0 +1,42 @@
|
||||
@use "../defaults";
|
||||
|
||||
.header {
|
||||
padding: 20px;
|
||||
font-size: 30px;
|
||||
height: 40px;
|
||||
background-color: defaults.$lt-gray-bg;
|
||||
}
|
||||
|
||||
|
||||
.submission-error {
|
||||
|
||||
.container-header {
|
||||
padding: 10px;
|
||||
font-size: 18px;
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
padding-left: 55px;
|
||||
|
||||
span {
|
||||
color: defaults.$default-active-bg;
|
||||
padding-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.submission-item {
|
||||
border: 1px black solid;
|
||||
}
|
||||
|
||||
.submission-item:hover {
|
||||
background-color: beige;
|
||||
}
|
||||
|
||||
overflow-y: scroll;
|
||||
|
||||
}
|
||||
31
stix-modeler-app/src/components/bundle/JsonPaste.jsx
Normal file
31
stix-modeler-app/src/components/bundle/JsonPaste.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Panel from '../ui/panel/Panel';
|
||||
import Button from '../ui/button/Button';
|
||||
import TextArea from '../ui/inputs/TextArea';
|
||||
|
||||
import './JsonPaste.scss';
|
||||
|
||||
class JsonPaste extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="json-paste">
|
||||
|
||||
<div className="paste-area">
|
||||
<TextArea onChange={this.props.onChangeJSONPasteHandler} value={this.props.value} />
|
||||
</div>
|
||||
|
||||
<div className="json-controls">
|
||||
<Button cls="def standard json-copy" text="Load" onClick={this.props.onClickJSONPasteHandler}>
|
||||
<i className="material-icons">add</i>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(JsonPaste));
|
||||
34
stix-modeler-app/src/components/bundle/JsonPaste.scss
Normal file
34
stix-modeler-app/src/components/bundle/JsonPaste.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
.json-paste {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
.paste-area {
|
||||
flex: 1;
|
||||
padding: 5px 0px 0px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
div {
|
||||
flex: 1;
|
||||
textarea {
|
||||
height: 97%;
|
||||
font-family: courier;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.json-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.json-copy {
|
||||
height: 35px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
52
stix-modeler-app/src/components/bundle/JsonViewer.jsx
Normal file
52
stix-modeler-app/src/components/bundle/JsonViewer.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Panel from '../ui/panel/Panel';
|
||||
import Button from '../ui/button/Button';
|
||||
|
||||
import './JsonViewer.scss';
|
||||
|
||||
class JsonViewer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onClickCopyJSONHandler = this.onClickCopyJSONHandler.bind(this);
|
||||
}
|
||||
|
||||
onClickCopyJSONHandler() {
|
||||
const range = document.createRange();
|
||||
const message = 'JSON Copied to Clipboard';
|
||||
|
||||
range.selectNode(
|
||||
document.getElementById('json-content')
|
||||
);
|
||||
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(range);
|
||||
document.execCommand('copy');
|
||||
|
||||
window.getSelection().removeAllRanges();
|
||||
|
||||
this.props.onClickShowGrowlHandler(message);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="json-viewer">
|
||||
<div className="json-content">
|
||||
<pre id="json-content">{this.props.json}</pre>
|
||||
</div>
|
||||
|
||||
<div className="json-controls">
|
||||
<Button cls="def standard json-copy" text="Copy" onClick={this.onClickCopyJSONHandler}>
|
||||
<i className="material-icons">file_copy</i>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(JsonViewer));
|
||||
35
stix-modeler-app/src/components/bundle/JsonViewer.scss
Normal file
35
stix-modeler-app/src/components/bundle/JsonViewer.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
.json-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-height: 100px;
|
||||
|
||||
.json-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 5px 0px 0px 10px;
|
||||
font-family: courier;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
pre {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.json-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
padding: 2px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.json-copy {
|
||||
height: 35px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
7
stix-modeler-app/src/components/canvas.scss
Normal file
7
stix-modeler-app/src/components/canvas.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.canvas {
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 50px;
|
||||
bottom: 50px;
|
||||
right: 50px;
|
||||
}
|
||||
120
stix-modeler-app/src/components/details.scss
Normal file
120
stix-modeler-app/src/components/details.scss
Normal file
@@ -0,0 +1,120 @@
|
||||
@use '../defaults';
|
||||
|
||||
.details {
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.header {
|
||||
padding: 20px;
|
||||
font-size: 18px;
|
||||
height: 40px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
background-color: defaults.$lt-gray-bg;
|
||||
|
||||
.title {
|
||||
padding-top: 5px;
|
||||
img {
|
||||
vertical-align: middle;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.delete {
|
||||
padding-left: 7px;
|
||||
width: 80px;
|
||||
display: flex;
|
||||
background-color: defaults.$default-active-bg;
|
||||
border-radius: 5px;
|
||||
padding-top: 7px;
|
||||
color: defaults.$light-font-0;
|
||||
cursor: pointer;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
span {
|
||||
padding-left: 1px;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.delete:hover {
|
||||
background-color: defaults.$error-font;
|
||||
}
|
||||
}
|
||||
|
||||
.invalid {
|
||||
border: red solid 1px;
|
||||
}
|
||||
|
||||
.required-warning {
|
||||
color: red;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
flex: 1;
|
||||
|
||||
.item {
|
||||
padding-left: 20px;
|
||||
padding-top: 15px;
|
||||
font-weight: normal;
|
||||
|
||||
.horizontal-slider {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
font-weight: bold;
|
||||
color: defaults.$default-active-bg;
|
||||
padding-bottom: 3px;
|
||||
|
||||
span {
|
||||
padding-left: 3px;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.inferred-header {
|
||||
color: defaults.$warning-font;
|
||||
}
|
||||
|
||||
#unknown-properties {
|
||||
padding-left: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.unknown-header {
|
||||
color: defaults.$error-font;
|
||||
|
||||
}
|
||||
|
||||
.unknown-value {
|
||||
color: #849BB0;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
94
stix-modeler-app/src/components/layout/LayoutPanel.jsx
Normal file
94
stix-modeler-app/src/components/layout/LayoutPanel.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import Panel from '../ui/panel/Panel';
|
||||
import RadioGroup from '../ui/inputs/RadioGroup';
|
||||
import OrientationRadioGroup from '../ui/inputs/OrientationRadioGroup';
|
||||
|
||||
import '../layout/LayoutPanel.scss';
|
||||
|
||||
class LayoutPanel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.store = this.props.store.appStore;
|
||||
this.store.setNodeTypes(["campaign", "identity"]);
|
||||
this.layoutMethod = this.store.getLayoutMethod();
|
||||
this.state = {
|
||||
horizontalSpacing: '',
|
||||
verticalSpacing: '',
|
||||
};
|
||||
}
|
||||
|
||||
handleLayoutChange = (newLayoutMethod) => {
|
||||
this.store.setLayoutMethod(newLayoutMethod); // Update store
|
||||
this.store.setNodeLayout(); // Redraw graph
|
||||
};
|
||||
|
||||
handleOrientationChange = (newOrientation) => {
|
||||
this.store.setOrientation(newOrientation); // Update store
|
||||
this.store.setNodeLayout(); // Redraw graph
|
||||
}
|
||||
|
||||
handleHorizontalSpacingChange = (event) => {
|
||||
const newValue = event.target.value;
|
||||
this.store.setHorizontalSpacing(newValue);
|
||||
this.store.setNodeLayout(); // Redraw graph
|
||||
};
|
||||
|
||||
handleVerticalSpacingChange = (event) => {
|
||||
const newValue = event.target.value;
|
||||
this.store.setVerticalSpacing(newValue);
|
||||
this.store.setNodeLayout(); // Redraw graph
|
||||
};
|
||||
|
||||
handleAlignDistributeClick = (action) => {
|
||||
this.store.setAlignmentOrDistribution(action); // Store action type
|
||||
// this.store.updateNodeLayout(); // Apply layout changes
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="layout-panel">
|
||||
<div className="header">Layout Panel</div>
|
||||
<div className="content">
|
||||
<RadioGroup
|
||||
// defaultLayoutMethod={this.store.getLayoutMethod()}
|
||||
defaultLayoutMethod={""}
|
||||
onLayoutChange={this.handleLayoutChange}
|
||||
/>
|
||||
<OrientationRadioGroup
|
||||
defaultOrientation={this.store.getOrientation()}
|
||||
onOrientationChange={this.handleOrientationChange}
|
||||
/>
|
||||
<hr />
|
||||
<p>Node Default Spacing Options (pixels)</p>
|
||||
<div className="spacing-input">
|
||||
<label htmlFor="horizontalSpacing">Horizontal Spacing:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="horizontalSpacing"
|
||||
value={this.store.getHorizontalSpacing()}
|
||||
onChange={this.handleHorizontalSpacingChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="spacing-input">
|
||||
<label htmlFor="verticalSpacing">Vertical Spacing:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="verticalSpacing"
|
||||
value={this.store.getVerticalSpacing()}
|
||||
onChange={this.handleVerticalSpacingChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('store')(observer(LayoutPanel));
|
||||
48
stix-modeler-app/src/components/layout/LayoutPanel.scss
Normal file
48
stix-modeler-app/src/components/layout/LayoutPanel.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
.header {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
border: solid 1px #dddddd;
|
||||
}
|
||||
|
||||
.layout-panel {
|
||||
.spacing-input {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
label {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.icon-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 10px;
|
||||
|
||||
img {
|
||||
width: 40px; // adjust the size as needed
|
||||
height: 40px; // adjust the size as needed
|
||||
cursor: pointer;
|
||||
|
||||
transition: box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px 2px rgba(0, 123, 255, 0.75); // blue glow effect
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
stix-modeler-app/src/components/menus/BottomMenu.jsx
Normal file
37
stix-modeler-app/src/components/menus/BottomMenu.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import MenuItem from './MenuItem';
|
||||
import Images from '../../util/Images';
|
||||
|
||||
import './BottomMenu.scss';
|
||||
|
||||
class BottomMenu extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="menu">
|
||||
<div className="row">
|
||||
{
|
||||
this.props.objects.map((object, i) => {
|
||||
if (object.active) {
|
||||
return (
|
||||
<MenuItem
|
||||
key={i}
|
||||
object={object}
|
||||
image={object.customImg ? object.customImg : Images.getImage(object.img)}
|
||||
onDragStartHandler={this.props.onDragStartHandler}
|
||||
generateNodeID={this.props.generateNodeID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default (observer(BottomMenu));
|
||||
42
stix-modeler-app/src/components/menus/BottomMenu.scss
Normal file
42
stix-modeler-app/src/components/menus/BottomMenu.scss
Normal file
@@ -0,0 +1,42 @@
|
||||
@use '../../defaults';
|
||||
|
||||
.menu {
|
||||
position: fixed;
|
||||
width: 90vw;
|
||||
bottom: 20px;
|
||||
right: 25px;
|
||||
overflow-x: scroll;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
width: 40px;
|
||||
padding-right: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.obs {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 5px;
|
||||
background-color: defaults.$default-active-bg;
|
||||
|
||||
div {
|
||||
padding-top: 10px;
|
||||
padding-left: 7px;
|
||||
color: defaults.$light-font-0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
stix-modeler-app/src/components/menus/MenuItem.jsx
Normal file
41
stix-modeler-app/src/components/menus/MenuItem.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import './BottomMenu.scss';
|
||||
|
||||
class MenuItem extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onDragStartHandler = this.onDragStartHandler.bind(this);
|
||||
}
|
||||
|
||||
onDragStartHandler(event) {
|
||||
const id = this.props.generateNodeID(this.props.object.prefix);
|
||||
this.props.object.id = id;
|
||||
event.dataTransfer.setData('node', JSON.stringify(this.props.object));
|
||||
|
||||
this.props.onDragStartHandler(event);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { object, } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span data-tooltip-id={`${object.title}-tooltip`} data-tooltip-content={object.title}>
|
||||
<div
|
||||
className="menu-item"
|
||||
draggable="true"
|
||||
onDragStart={this.onDragStartHandler}
|
||||
>
|
||||
<img src={this.props.image} alt={object.title} draggable="false" />
|
||||
|
||||
</div>
|
||||
</span>
|
||||
<Tooltip id={`${object.title}-tooltip`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default (observer(MenuItem));
|
||||
174
stix-modeler-app/src/components/menus/TopMenu.jsx
Normal file
174
stix-modeler-app/src/components/menus/TopMenu.jsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
|
||||
import LabeledText from '../ui/inputs/LabeledText';
|
||||
|
||||
import './TopMenu.scss';
|
||||
|
||||
class TopMenu extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.updateCreatorID = this.updateCreatorID.bind(this);
|
||||
this.flipGroupMode = this.flipGroupMode.bind(this);
|
||||
this.submitGroup = this.submitGroup.bind(this);
|
||||
}
|
||||
|
||||
updateCreatorID(event) {
|
||||
const creatorID = event.currentTarget.value;
|
||||
this.props.onChangeCreatorIDHandler(creatorID);
|
||||
}
|
||||
|
||||
flipGroupMode() {
|
||||
const curr = this.props.groupMode;
|
||||
this.props.onClickGroupModeHandler(!curr);
|
||||
}
|
||||
|
||||
submitGroup() {
|
||||
this.props.onClickSubmitGroupingHandler();
|
||||
}
|
||||
|
||||
render() {
|
||||
let groupLabel = 'Group';
|
||||
let groupClass = '';
|
||||
let items;
|
||||
|
||||
if (this.props.groupMode) {
|
||||
groupLabel = 'Cancel';
|
||||
groupClass = 'cancel-btn';
|
||||
items = (
|
||||
<div id="myDropdown" className="dropdown-content">
|
||||
<a onClick={this.submitGroup}>Create Group
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const group = (
|
||||
<div className="dropdown">
|
||||
<div
|
||||
data-tooltip-id="select-tooltip"
|
||||
data-tooltip-content="Select Nodes"
|
||||
className={`grouping-btn menu-btn menu-item ${groupClass}`}
|
||||
onClick={this.flipGroupMode}
|
||||
>
|
||||
{groupLabel}
|
||||
{items}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const badge = this.props.errors ? (<span className="badge"></span>) : undefined;
|
||||
|
||||
return (
|
||||
<div className="top-menu">
|
||||
<div className="row">
|
||||
<div
|
||||
data-tooltip-id="creator-tooltip"
|
||||
data-tooltip-content="Creator ID"
|
||||
className="ctr-input"
|
||||
>
|
||||
<LabeledText
|
||||
name="creator-input"
|
||||
label="Creator ID"
|
||||
value={this.props.creatorID}
|
||||
placeholder="Creator ID"
|
||||
onChange={this.updateCreatorID}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
data-tooltip-id="view-tooltip"
|
||||
data-tooltip-content="View Bundle"
|
||||
className="menu-btn menu-item"
|
||||
onClick={this.props.onClickShowJsonHandler}
|
||||
>
|
||||
<i className="material-icons">description</i>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tooltip-id="paste-tooltip"
|
||||
data-tooltip-content="Paste Bundle"
|
||||
className="menu-btn menu-item"
|
||||
onClick={this.props.onClickShowJsonPasteHandler}
|
||||
>
|
||||
<i className="material-icons">note_add</i>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tooltip-id="schema-tooltip"
|
||||
data-tooltip-content="Paste Schema"
|
||||
className="menu-btn menu-item"
|
||||
onClick={this.props.onClickShowSchemaPasteHandler}
|
||||
>
|
||||
<i className="material-icons">add_box</i>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tooltip-id="layout"
|
||||
data-tooltip-content="Graph Layout and Filtering"
|
||||
className="sdos-btn menu-item"
|
||||
onClick={this.props.onClickShowLayoutPanelHandler}
|
||||
>
|
||||
Layout
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tooltip-id="import-tooltip"
|
||||
data-tooltip-content="Import Data from File"
|
||||
className="menu-btn menu-item"
|
||||
onClick={this.props.onClickShowImporterHandler}
|
||||
>
|
||||
<i className="material-icons">folder</i>
|
||||
</div>
|
||||
<div
|
||||
data-tooltip-id="sdo-tooltip"
|
||||
data-tooltip-content="SDO Extensions"
|
||||
className="menu-btn menu-item"
|
||||
onClick={this.props.onClickShowExtensionPickerHandler}
|
||||
>
|
||||
EXT
|
||||
</div>
|
||||
{group}
|
||||
<div
|
||||
data-tooltip-id="clear-tooltip"
|
||||
data-tooltip-content="Reset Bundle"
|
||||
className="reset-btn menu-btn menu-item"
|
||||
onClick={this.props.onClickResetHandler}
|
||||
>
|
||||
<span className="material-icons">refresh</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tooltip-id="submit-tooltip"
|
||||
data-tooltip-content="Export JSON"
|
||||
className="menu-btn menu-item"
|
||||
onClick={this.props.onClickExportHandler}
|
||||
>
|
||||
<i className="material-icons">save</i>
|
||||
{' '}
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-tooltip-id="error-tooltip"
|
||||
data-tooltip-content="Bundle Errors"
|
||||
className="menu-btn menu-item"
|
||||
onClick={this.props.onClickShowErrorHandler}
|
||||
>
|
||||
<span className="material-icons">error</span>
|
||||
{badge}
|
||||
</div>
|
||||
|
||||
<Tooltip id="creator-tooltip" />
|
||||
<Tooltip id="paste-tooltip" />
|
||||
<Tooltip id="view-tooltip" />
|
||||
<Tooltip id="schema-tooltip" />
|
||||
<Tooltip id="sdo-tooltip" />
|
||||
<Tooltip id="import-tooltip" />
|
||||
<Tooltip id="clear-tooltip" />
|
||||
<Tooltip id="submit-tooltip" />
|
||||
<Tooltip id="error-tooltip" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default (observer(TopMenu));
|
||||
98
stix-modeler-app/src/components/menus/TopMenu.scss
Normal file
98
stix-modeler-app/src/components/menus/TopMenu.scss
Normal file
@@ -0,0 +1,98 @@
|
||||
@use '../../defaults';
|
||||
|
||||
.top-menu {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.menu-item {
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
padding: 10px 8px 0px 8px;
|
||||
color: #fff;
|
||||
background-color: defaults.$default-active-bg;
|
||||
margin-left: 10px;
|
||||
text-align: center;
|
||||
|
||||
.i {
|
||||
width: 20px;
|
||||
vertical-align: middle;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: #2f689d;
|
||||
}
|
||||
|
||||
.grouping-btn {
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
background-color: defaults.$default-active-bg;
|
||||
width: 55px;
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: -25%;
|
||||
background-color: #f1f1f1;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown-content a {
|
||||
color: black;
|
||||
padding: 12px 16px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {background-color: #ddd;}
|
||||
|
||||
.dropdown:hover .dropdown-content {display: block;}
|
||||
|
||||
|
||||
.cancel-btn:hover {
|
||||
background-color: defaults.$error-font;
|
||||
}
|
||||
|
||||
.ctr-input {
|
||||
padding-right: 10px;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
|
||||
.reset-btn:hover {
|
||||
background-color: defaults.$error-font;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: red;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import Panel from '../ui/panel/Panel';
|
||||
import Text from '../ui/inputs/Text';
|
||||
import Boolean from '../ui/inputs/Boolean';
|
||||
import Images from '../../util/Images';
|
||||
|
||||
import '../details.scss';
|
||||
import './RelationshipDetails.scss';
|
||||
|
||||
class RelationshipDetails extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
type: 'related-to',
|
||||
x_exclusive: false,
|
||||
};
|
||||
this.onSubmitHandler = this.onSubmitHandler.bind(this);
|
||||
this.onChangeHandler = this.onChangeHandler.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
this.close = this.close.bind(this);
|
||||
}
|
||||
|
||||
onSubmitHandler() {
|
||||
const relationship = this.props.relationships[0];
|
||||
const rel = {
|
||||
type: this.state.type,
|
||||
targetObjectType: relationship.target,
|
||||
x_exclusive: this.state.x_exclusive,
|
||||
};
|
||||
const src = relationship.source_ref;
|
||||
const target = relationship.target_ref;
|
||||
this.props.onClickCreateRelHandler(src, target, rel);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
onChangeHandler(event) {
|
||||
if (event in this.state) {
|
||||
this.setState({ [event]: !this.state[event], });
|
||||
} else {
|
||||
const { value, } = event.target;
|
||||
this.setState({ type: value, });
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setState({
|
||||
type: 'related-to',
|
||||
x_exclusive: false,
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.reset();
|
||||
this.props.onClickHideHandler();
|
||||
}
|
||||
|
||||
render() {
|
||||
const deleteIcon = <span className="material-icons">delete_forever</span>;
|
||||
let preview;
|
||||
const details = [];
|
||||
if (this.props.relationships.length) {
|
||||
const relationship = this.props.relationships[0];
|
||||
const src = relationship.source_ref.split('--')[0];
|
||||
let target = relationship.target_ref.split('--')[0];
|
||||
|
||||
const srcImg = Images.getImage(`${src}.png`);
|
||||
const targetImg = Images.getImage(`${target}.png`);
|
||||
|
||||
if (relationship.subTarget) {
|
||||
target = relationship.subTarget;
|
||||
}
|
||||
|
||||
preview = (
|
||||
<div className="item" key="preview">
|
||||
<div className="item-header">
|
||||
Preview
|
||||
</div>
|
||||
<div className="preview item-value" key="preview-item">
|
||||
<img className="src-image" alt={src} src={srcImg} width="20" />
|
||||
{' '}
|
||||
{src}
|
||||
<span className="rel-type">
|
||||
{' '}
|
||||
{this.state.type}
|
||||
{' '}
|
||||
</span>
|
||||
{target}
|
||||
{' '}
|
||||
<img className="target-image" alt={target} src={targetImg} width="20" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const descriptions = {
|
||||
type: 'Name of relationship',
|
||||
x_exclusive: 'Relationship exclusive?',
|
||||
|
||||
};
|
||||
|
||||
for (const field in this.state) {
|
||||
const header = (
|
||||
<div className="item-header">
|
||||
{field}
|
||||
<span
|
||||
data-tooltip-id={`${field}-tooltip`}
|
||||
className="material-icons"
|
||||
data-tooltip-content={descriptions[field]}
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id={`${field}-tooltip`} />
|
||||
</div>
|
||||
);
|
||||
|
||||
let control;
|
||||
switch (field) {
|
||||
case 'type':
|
||||
control = (
|
||||
<div className="item" key={field}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<Text
|
||||
name={field}
|
||||
value={this.state.type}
|
||||
onChange={this.onChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
control = (
|
||||
<div className="item" key={field}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<Boolean
|
||||
name={field}
|
||||
selected={this.state[field]}
|
||||
onClick={this.onChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
details.push(control);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.close}
|
||||
>
|
||||
<div className="details">
|
||||
<div className="header">
|
||||
<div className="title">
|
||||
New Relationship
|
||||
</div>
|
||||
<div className="delete" onClick={this.close}>
|
||||
{deleteIcon}
|
||||
{' '}
|
||||
<span className="text">Discard</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="body">
|
||||
{preview}
|
||||
{details}
|
||||
<div
|
||||
className="submit-btn"
|
||||
onClick={this.onSubmitHandler}
|
||||
>
|
||||
<span className="i material-icons">add</span>
|
||||
{' '}
|
||||
Submit
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer" />
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(RelationshipDetails));
|
||||
@@ -0,0 +1,43 @@
|
||||
@use '../../defaults';
|
||||
|
||||
.preview {
|
||||
padding-left: 20px;
|
||||
padding-top: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-family: defaults.$default-font-family;
|
||||
line-height: 30px;
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
img.src-image {
|
||||
padding-right: 5px;
|
||||
}
|
||||
img.target-image {
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.rel-type {
|
||||
color: defaults.$default-active-bg;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
padding: 5px 11px 5px 11px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
background-color: defaults.$default-active-bg;
|
||||
margin-left: 10px;
|
||||
margin-top: 10px;
|
||||
width: fit-content;
|
||||
|
||||
.i {
|
||||
width: 20px;
|
||||
vertical-align: middle;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { toJS } from 'mobx';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import Panel from '../ui/panel/Panel';
|
||||
import Text from '../ui/inputs/Text';
|
||||
import Images from '../../util/Images';
|
||||
|
||||
import './RelationshipDetails.scss';
|
||||
|
||||
class RelationshipEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
type: this.props.relationship.relationship_type,
|
||||
};
|
||||
this.onSubmitHandler = this.onSubmitHandler.bind(this);
|
||||
this.onChangeHandler = this.onChangeHandler.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
this.close = this.close.bind(this);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setState({
|
||||
type: this.props.relationship.relationship_type,
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.reset();
|
||||
this.props.onClickHideHandler();
|
||||
}
|
||||
|
||||
onSubmitHandler() {
|
||||
const rel = {
|
||||
type: this.state.type,
|
||||
};
|
||||
|
||||
this.props.onClickEditRelHandler(rel);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
onChangeHandler(event) {
|
||||
if (event in this.state) {
|
||||
this.setState({ [event]: !this.state[event], });
|
||||
} else {
|
||||
const { value, } = event.target;
|
||||
this.setState({ type: value, });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const deleteIcon = <span className="material-icons">delete_forever</span>;
|
||||
let preview;
|
||||
let details;
|
||||
const relationship = toJS(this.props.relationship);
|
||||
let type;
|
||||
if (relationship.type) {
|
||||
let src = relationship.source_ref.split('--')[0];
|
||||
let target = relationship.target_ref.split('--')[0];
|
||||
type = this.state.type;
|
||||
|
||||
if (relationship.x_reverse) {
|
||||
const tmp = src;
|
||||
src = target;
|
||||
target = tmp;
|
||||
}
|
||||
|
||||
const srcImg = Images.getImage(`${src}.png`);
|
||||
const targetImg = Images.getImage(`${target}.png`);
|
||||
|
||||
if (relationship.subTarget) {
|
||||
target = relationship.subTarget;
|
||||
}
|
||||
preview = (
|
||||
<div className="item" key="preview">
|
||||
<div className="item-header">Preview</div>
|
||||
<div className="preview item-value" key="preview-item">
|
||||
<img
|
||||
className="src-image"
|
||||
src={srcImg}
|
||||
width="20"
|
||||
/>
|
||||
{' '}
|
||||
{src}
|
||||
<span className="rel-type">
|
||||
{' '}
|
||||
{type}
|
||||
{' '}
|
||||
</span>
|
||||
{target}
|
||||
{' '}
|
||||
<img
|
||||
className="target-image"
|
||||
src={targetImg}
|
||||
width="20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
details = (
|
||||
<div className="item" key="type">
|
||||
<div className="item-header">
|
||||
Type
|
||||
<span
|
||||
data-tooltip-id="name-tooltip"
|
||||
className="material-icons"
|
||||
data-tooltip-content="Name of relationship"
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id="name-tooltip" />
|
||||
</div>
|
||||
<div className="item-value">
|
||||
<Text name="type" value={type} onChange={this.onChangeHandler} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Panel show={this.props.show} onClickHideHandler={this.close}>
|
||||
<div className="details">
|
||||
<div className="header">
|
||||
<div className="title">Edit Relationship</div>
|
||||
<div
|
||||
className="delete"
|
||||
onClick={this.props.onClickDeleteRelHandler}
|
||||
>
|
||||
{deleteIcon}
|
||||
{' '}
|
||||
<span className="text">Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="body">
|
||||
{preview}
|
||||
{details}
|
||||
<div className="submit-btn" onClick={this.onSubmitHandler}>
|
||||
<span className="i material-icons">add</span>
|
||||
{' '}
|
||||
Update
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer" />
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(RelationshipEditor));
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Panel from '../ui/panel/Panel';
|
||||
import Images from '../../util/Images';
|
||||
|
||||
import './RelationshipPicker.scss';
|
||||
|
||||
class RelationshipPicker extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClickSelectRelHandler(relationship) {
|
||||
this.props.onClickSelectRelHandler(relationship);
|
||||
}
|
||||
|
||||
render() {
|
||||
// Do not allow relationship defininition for generic observables
|
||||
let create;
|
||||
if (this.props.relationships.length == 0 || this.props.relationships[0].target_ref) {
|
||||
create = (
|
||||
<div
|
||||
className="item"
|
||||
key="new-relationship"
|
||||
onClick={this.props.onClickShowRelDetailsHandler}
|
||||
>
|
||||
<img className="src-image" src={Images.getImage('add.png')} width="20" />
|
||||
<span className="rel-type"> Create New Relationship </span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="relationship-picker">
|
||||
<div className="header">
|
||||
<img src={Images.getImage('relationship.png')} width="20" />
|
||||
{' '}
|
||||
Possible Relationships
|
||||
</div>
|
||||
<div className="content">
|
||||
{create}
|
||||
{
|
||||
this.props.relationships.slice(1).map((relationship) => {
|
||||
const src = relationship.source_ref.split('--')[0];
|
||||
const target = relationship.subTarget ??
|
||||
relationship.target_ref.split('--')[0];
|
||||
const srcImg = Images.getImage(relationship.srcImg);
|
||||
const targetImg = Images.getImage(relationship.targetImg);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="item"
|
||||
key={relationship.id}
|
||||
onClick={() => this.onClickSelectRelHandler(relationship)}
|
||||
>
|
||||
<img className="src-image" src={srcImg} width="20" />
|
||||
{' '}
|
||||
{src}
|
||||
<span className="rel-type">
|
||||
{' '}
|
||||
{relationship.relationship_type}
|
||||
{' '}
|
||||
</span>
|
||||
{target}
|
||||
{' '}
|
||||
<img className="target-image" src={targetImg} width="20" />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(RelationshipPicker));
|
||||
@@ -0,0 +1,52 @@
|
||||
@use "../../defaults";
|
||||
|
||||
.relationship-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
.header {
|
||||
padding-top: 20px;
|
||||
padding-left: 20px;
|
||||
padding-bottom: 20px;
|
||||
height: 35px;
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
.item {
|
||||
padding-left: 20px;
|
||||
padding-top: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-family: defaults.$default-font-family;
|
||||
line-height: 30px;
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
img.src-image {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
img.target-image {
|
||||
padding-right: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.rel-type {
|
||||
color: defaults.$default-active-bg;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
353
stix-modeler-app/src/components/schema/ExtensionEditor.jsx
Normal file
353
stix-modeler-app/src/components/schema/ExtensionEditor.jsx
Normal file
@@ -0,0 +1,353 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { toJS } from 'mobx';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import Panel from '../ui/panel/Panel';
|
||||
import Images from '../../util/Images';
|
||||
|
||||
import ArraySelector from '../ui/inputs/ArraySelector';
|
||||
import Boolean from '../ui/inputs/Boolean';
|
||||
import CSVInput from '../ui/inputs/CSVInput';
|
||||
import DateTime from '../ui/inputs/DateTime';
|
||||
import ExternalReferences from '../ui/complex/ExternalReferences';
|
||||
import FileSelector from '../ui/inputs/FileSelector';
|
||||
import ObjectArray from '../ui/complex/ObjectArray';
|
||||
import Slider from '../ui/inputs/Slider';
|
||||
import Text from '../ui/inputs/Text';
|
||||
import TextArea from '../ui/inputs/TextArea';
|
||||
|
||||
|
||||
import '../details.scss';
|
||||
|
||||
class ExtensionEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChangeExtHandler = this.onChangeExtHandler.bind(this);
|
||||
this.onChangeHandler = this.onChangeHandler.bind(this)
|
||||
this.onChangeDateHandler = this.onChangeDateHandler.bind(this);
|
||||
}
|
||||
|
||||
onChangeExtHandler(event) {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
const value = URL.createObjectURL(event.target.files[0]);
|
||||
|
||||
const mutatedEvent = {
|
||||
currentTarget: {
|
||||
name: 'customImg',
|
||||
value: value
|
||||
},
|
||||
};
|
||||
this.props.onChangeExtHandler(mutatedEvent);
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
onChangeHandler(event) {
|
||||
this.props.onChangeNodeHandler(event);
|
||||
}
|
||||
|
||||
onChangeDateHandler(property, datetime) {
|
||||
this.props.onChangeDateHandler(property, datetime);
|
||||
}
|
||||
|
||||
render() {
|
||||
const extension = toJS(this.props.extension);
|
||||
let props = {};
|
||||
let img;
|
||||
const details = [];
|
||||
|
||||
const deleteIcon = <span className="material-icons">delete_forever</span>;
|
||||
|
||||
if (!extension) return;
|
||||
|
||||
props = extension.properties;
|
||||
if (extension.customImg !== undefined) {
|
||||
img = <img src={extension.customImg} alt="Custom" width="30" />;
|
||||
} else {
|
||||
img = <img src={Images.getImage(extension.img)} alt="Custom" width="30" />;
|
||||
}
|
||||
|
||||
let header = (
|
||||
<div className="item-header">
|
||||
Update Icon
|
||||
<span
|
||||
data-tooltip-id="icon-tooltip"
|
||||
className="material-icons"
|
||||
data-tooltip-content="Set SDO icon to a local image"
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id="icon-tooltip" />
|
||||
</div>
|
||||
);
|
||||
|
||||
let control = (
|
||||
<div className="item" key="icon">
|
||||
{header}
|
||||
<FileSelector
|
||||
name="image"
|
||||
type="image/*"
|
||||
key="icon"
|
||||
multiple={false}
|
||||
onChange={this.onChangeExtHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
details.push(control);
|
||||
|
||||
for (const prop in props) {
|
||||
const header = (
|
||||
<div className="item-header">
|
||||
{prop}
|
||||
<span
|
||||
data-tooltip-id={`${prop}-tooltip`}
|
||||
className="material-icons"
|
||||
data-tooltip-content={props[prop].description}
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id={`${prop}-tooltip`} />
|
||||
</div>
|
||||
);
|
||||
|
||||
let control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">{props[prop].value}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// If there is no type, we do not want to process. If a "control"
|
||||
// is defined, that indicates special handling of the value.
|
||||
if (props[prop].type && !props[prop].control) {
|
||||
switch (props[prop].type) {
|
||||
case 'number':
|
||||
case 'string':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<Text
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onChange={this.onChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'timestamp':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<DateTime
|
||||
name={prop}
|
||||
selected={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onTextChange={this.onChangeHandler}
|
||||
onDateChange={this.onChangeDateHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'array':
|
||||
if (props[prop].vocab) {
|
||||
control = (
|
||||
<ArraySelector
|
||||
vocab={props[prop].vocab}
|
||||
key={prop}
|
||||
field={prop}
|
||||
value={props[prop].value}
|
||||
description={props[prop].description}
|
||||
required={props[prop].required}
|
||||
onClickHandler={this.props.onClickArrayHandler}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Get array subtype, possibly nested
|
||||
let refField = props[prop].items;
|
||||
if (Array.isArray(refField)) {
|
||||
refField = refField[0];
|
||||
}
|
||||
const ref = refField.$ref ?? refField.type;
|
||||
|
||||
if (ref.includes('dictionary.json') || ref === 'object') {
|
||||
control = (
|
||||
<ObjectArray
|
||||
node={extension}
|
||||
key={prop}
|
||||
field={prop}
|
||||
value={props[prop].value}
|
||||
description={props[prop].description}
|
||||
required={props[prop].required}
|
||||
onClickAddObjectHandler={this.props.onClickAddObjectHandler}
|
||||
onChangeObjectHandler={this.props.onChangeArrayObjectHandler}
|
||||
onClickDeleteArrayObjectHandler={this.props.onClickDeleteArrayObjectHandler}
|
||||
onClickDeleteArrayObjectPropertyHandler={
|
||||
this.props.onClickDeleteArrayObjectPropertyHandler
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
props[prop].control = 'listtextarea';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<Boolean
|
||||
name={prop}
|
||||
selected={props[prop].value}
|
||||
onClick={this.props.onClickBooleanHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (props[prop].$ref && !props[prop].control) {
|
||||
switch (props[prop].$ref) {
|
||||
case '../common/identifier.json':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<Text
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onChange={this.onChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (props[prop].control) {
|
||||
case 'hidden':
|
||||
control = '';
|
||||
break;
|
||||
case 'slider':
|
||||
control = (
|
||||
<div className="item slider" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<Slider
|
||||
value={props[prop].value}
|
||||
field={prop}
|
||||
required={props[prop].required}
|
||||
onChangeHandler={this.props.onChangeSliderHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'externalrefs':
|
||||
control = (
|
||||
<ExternalReferences
|
||||
node={extension}
|
||||
key={prop}
|
||||
field={prop}
|
||||
value={props[prop].value}
|
||||
prefix="extension"
|
||||
description={props[prop].description}
|
||||
required={props[prop].required}
|
||||
onClickAddObjectHandler={this.props.onClickAddObjectHandler}
|
||||
onChangeERHandler={this.props.onChangeERHandler}
|
||||
onClickDeleteERHandler={this.props.onClickDeleteERHandler}
|
||||
onClickDeletePropertyHandler={
|
||||
this.props.onClickDeletePropertyHandler
|
||||
}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'stringselector':
|
||||
control = (
|
||||
<ArraySelector
|
||||
vocab={props[prop].vocab}
|
||||
key={prop}
|
||||
field={prop}
|
||||
value={props[prop].value}
|
||||
description={props[prop].description}
|
||||
required={props[prop].required}
|
||||
onClickHandler={this.props.onClickArrayHandler}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'textarea':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<TextArea
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onChange={this.onChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'listtextarea':
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">
|
||||
<CSVInput
|
||||
key={prop}
|
||||
name={prop}
|
||||
value={props[prop].value}
|
||||
required={props[prop].required}
|
||||
onChangeHandler={this.props.onChangeCSVHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
details.push(control);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="details">
|
||||
<div className="header">
|
||||
<div className="title">
|
||||
{img}
|
||||
{' '}
|
||||
{extension.id}
|
||||
</div>
|
||||
<div className="delete" onClick={this.props.onClickDeleteHandler}>
|
||||
{deleteIcon}
|
||||
{' '}
|
||||
<span className="text">Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="body">
|
||||
{details}
|
||||
</div>
|
||||
|
||||
<div className="footer" />
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(ExtensionEditor));
|
||||
52
stix-modeler-app/src/components/schema/ExtensionPicker.jsx
Normal file
52
stix-modeler-app/src/components/schema/ExtensionPicker.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Panel from '../ui/panel/Panel';
|
||||
import Images from '../../util/Images';
|
||||
|
||||
import '../relationship/RelationshipPicker.scss';
|
||||
|
||||
class ExtensionPicker extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClickSelectExtHandler(sdo) {
|
||||
this.props.onClickHideHandler();
|
||||
this.props.onClickSelectExtHandler(sdo);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="relationship-picker">
|
||||
<div className="header">STIX Domain Object (SDO) Extensions</div>
|
||||
<div className="content">
|
||||
{
|
||||
this.props.extensions.map((ext) => {
|
||||
let img = Images.getImage(ext.img);
|
||||
if (ext.customImg) {
|
||||
img = ext.customImg;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="item"
|
||||
key={ext.id}
|
||||
onClick={() => this.onClickSelectExtHandler(ext)}
|
||||
>
|
||||
<img className="src-image" src={img} width="20" />
|
||||
{' '}
|
||||
{ext.properties.name.value.length > 0 ? ext.properties.name.value : ext.id}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(ExtensionPicker));
|
||||
119
stix-modeler-app/src/components/schema/SDOEditor.jsx
Normal file
119
stix-modeler-app/src/components/schema/SDOEditor.jsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { toJS } from 'mobx';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import Panel from '../ui/panel/Panel';
|
||||
import FileSelector from '../ui/inputs/FileSelector';
|
||||
import Images from '../../imgs/Images';
|
||||
|
||||
import '../details.scss';
|
||||
|
||||
class SDOEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChangeHandler = this.onChangeHandler.bind(this);
|
||||
}
|
||||
|
||||
onChangeHandler(event) {
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
const value = URL.createObjectURL(event.target.files[0]);
|
||||
|
||||
const mutatedEvent = {
|
||||
currentTarget: {
|
||||
name: 'customImg',
|
||||
value,
|
||||
},
|
||||
};
|
||||
this.props.onChangeSDOHandler(mutatedEvent);
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const sdo = toJS(this.props.sdo);
|
||||
let props = {};
|
||||
let img;
|
||||
const details = [];
|
||||
|
||||
const deleteIcon = <span className="material-icons">delete_forever</span>;
|
||||
|
||||
if (sdo.properties) {
|
||||
props = sdo.properties;
|
||||
if (sdo.customImg !== undefined) {
|
||||
img = <img src={sdo.customImg} alt="Custom" width="30" />;
|
||||
} else {
|
||||
img = <img src={Images.getImage(sdo.img)} alt="Custom" width="30" />;
|
||||
}
|
||||
}
|
||||
|
||||
let header = (
|
||||
<div className="item-header">
|
||||
Update Icon
|
||||
<span
|
||||
data-tooltip-id="icon-tooltip"
|
||||
className="material-icons"
|
||||
data-tooltip-content="Set SDO icon to a local image"
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id="icon-tooltip" />
|
||||
</div>
|
||||
);
|
||||
|
||||
let control = (
|
||||
<div className="item" key="icon">
|
||||
{header}
|
||||
<FileSelector
|
||||
name="image"
|
||||
type="image/*"
|
||||
multiple={false}
|
||||
onChange={this.onChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
details.push(control);
|
||||
|
||||
for (const prop in props) {
|
||||
header = (
|
||||
<div className="item-header">
|
||||
{prop}
|
||||
</div>
|
||||
);
|
||||
|
||||
control = (
|
||||
<div className="item" key={prop}>
|
||||
{header}
|
||||
<div className="item-value">{props[prop].description}</div>
|
||||
</div>
|
||||
);
|
||||
details.push(control);
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="details">
|
||||
<div className="header">
|
||||
<div className="title">
|
||||
{img}
|
||||
{' '}
|
||||
{sdo.title}
|
||||
</div>
|
||||
<div className="delete" onClick={this.props.onClickDeleteHandler}>
|
||||
{deleteIcon}
|
||||
{' '}
|
||||
<span className="text">Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="body">{details}</div>
|
||||
|
||||
<div className="footer" />
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(SDOEditor));
|
||||
52
stix-modeler-app/src/components/schema/SDOPicker.jsx
Normal file
52
stix-modeler-app/src/components/schema/SDOPicker.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Panel from '../ui/panel/Panel';
|
||||
import Images from '../../imgs/Images';
|
||||
|
||||
import '../relationship/RelationshipPicker.scss';
|
||||
|
||||
class SDOPicker extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClickSelectSDOHandler(sdo) {
|
||||
this.props.onClickHideHandler();
|
||||
this.props.onClickSelectSDOHandler(sdo);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="relationship-picker">
|
||||
<div className="header">STIX Domain Object (SDO) Extensions</div>
|
||||
<div className="content">
|
||||
{
|
||||
this.props.sdos.map((sdo) => {
|
||||
let img = Images.getImage(sdo.img);
|
||||
if (sdo.customImg) {
|
||||
img = sdo.customImg;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="item"
|
||||
key={sdo.title}
|
||||
onClick={() => this.onClickSelectSDOHandler(sdo)}
|
||||
>
|
||||
<img className="src-image" src={img} width="20" />
|
||||
{' '}
|
||||
{sdo.title}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(SDOPicker));
|
||||
35
stix-modeler-app/src/components/schema/SchemaPaste.jsx
Normal file
35
stix-modeler-app/src/components/schema/SchemaPaste.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Panel from '../ui/panel/Panel';
|
||||
import Button from '../ui/button/Button';
|
||||
import TextArea from '../ui/inputs/TextArea';
|
||||
|
||||
import '../bundle/JsonPaste.scss';
|
||||
|
||||
class SchemaPaste extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Panel
|
||||
show={this.props.show}
|
||||
onClickHideHandler={this.props.onClickHideHandler}
|
||||
>
|
||||
<div className="json-paste">
|
||||
|
||||
<div className="paste-area">
|
||||
<TextArea onChange={this.props.onChangeSchemaPasteHandler} value={this.props.value} />
|
||||
</div>
|
||||
|
||||
<div className="json-controls">
|
||||
<Button cls="def standard json-copy" text="Load" onClick={this.props.onClickSchemaPasteHandler}>
|
||||
<i className="material-icons">add</i>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
} export default (observer(SchemaPaste));
|
||||
45
stix-modeler-app/src/components/ui/button/Button.jsx
Normal file
45
stix-modeler-app/src/components/ui/button/Button.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './button.scss';
|
||||
|
||||
export default class Button extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onClickHandler = this.onClickHandler.bind(this);
|
||||
}
|
||||
|
||||
onClickHandler() {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const clickHandler = this.props.disabled ? undefined : this.onClickHandler;
|
||||
const classMap = {
|
||||
def: true,
|
||||
disabled: this.props.disabled,
|
||||
};
|
||||
if (this.props.cls) {
|
||||
classMap[this.props.cls] = true;
|
||||
}
|
||||
const classes = classNames(classMap);
|
||||
return (
|
||||
<div>
|
||||
<button className={classes} onClick={clickHandler}>
|
||||
{this.props.children}
|
||||
{' '}
|
||||
{this.props.text}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
cls: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
48
stix-modeler-app/src/components/ui/button/button.scss
Normal file
48
stix-modeler-app/src/components/ui/button/button.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button.def {
|
||||
width: auto;
|
||||
min-width: 130px;
|
||||
height: 30px;
|
||||
color: defaults.$light-font-0;
|
||||
font-family: defaults.$default-font-family;
|
||||
font-size: 14px;
|
||||
border-color: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
button.disabled {
|
||||
background-color: rgba(128,128,128,.8) !important;
|
||||
color: defaults.$gray-font-0 !important;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.disco-relationship {
|
||||
background: transparent;
|
||||
border: 1px solid #243244 !important;
|
||||
|
||||
}
|
||||
|
||||
.standard {
|
||||
background-color: rgba(70,160,245,.8) !important;
|
||||
}
|
||||
|
||||
.confirm {
|
||||
background-color: rgba(83,129,60,.8) !important;
|
||||
}
|
||||
|
||||
.caution {
|
||||
background-color: rgba(202,202,57,.8) !important;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
background-color: rgba(143,44,44,.8) !important;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import TextArea from '../inputs/TextArea';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './confirmtextarea.scss';
|
||||
|
||||
class ConfirmTextarea extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChangeInputHandler = this.onChangeInputHandler.bind(this);
|
||||
this.onClickAddObjectHandler = this.onClickAddObjectHandler.bind(this);
|
||||
this.onClickDeleteHandler = this.onClickDeleteHandler.bind(this);
|
||||
|
||||
this.state = {
|
||||
value: '',
|
||||
};
|
||||
}
|
||||
|
||||
onChangeInputHandler(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
value: event.currentTarget.value,
|
||||
});
|
||||
}
|
||||
|
||||
onClickDeleteHandler(select, idx) {
|
||||
this.props.onClickDeletePropertyHandler(select, idx);
|
||||
}
|
||||
|
||||
onClickAddObjectHandler() {
|
||||
this.props.onClickAddTextHandler(this.props.field, this.state.value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { field, } = this.props;
|
||||
const value = this.props.value ? this.props.value : [];
|
||||
const { description, } = this.props;
|
||||
const {required, } = this.props;
|
||||
const invalid = required && !value.length;
|
||||
const warning = invalid? (<div className='required-warning'>This field is required</div>) : "";
|
||||
|
||||
return (
|
||||
<div className="ct-container">
|
||||
<div className="ct-header">
|
||||
{field}
|
||||
<span
|
||||
data-tooltip-id={`${field}-tooltip`}
|
||||
className="material-icons"
|
||||
data-tooltip-content={description}
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id={`${field}-tooltip`} />
|
||||
</div>
|
||||
<div className={classNames({
|
||||
"ct-body": true,
|
||||
"invalid": invalid
|
||||
})}>
|
||||
<div className="ct-block-input">
|
||||
<div className="input">
|
||||
<TextArea
|
||||
value={this.state.value}
|
||||
name={field}
|
||||
onChange={this.onChangeInputHandler}
|
||||
/>
|
||||
</div>
|
||||
<div className="add-container">
|
||||
<span onClick={this.onClickAddObjectHandler} className="add material-icons">control_point</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ct-output">
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default observer(ConfirmTextarea);
|
||||
@@ -0,0 +1,175 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import classNames from 'classnames';
|
||||
import Text from '../inputs/Text';
|
||||
|
||||
import './externalreferences.scss';
|
||||
|
||||
class ExternalReferences extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onClickHandler = this.onClickHandler.bind(this);
|
||||
this.onChangeERHandler = this.onChangeERHandler.bind(this);
|
||||
this.onClickDeleteERHandler = this.onClickDeleteERHandler.bind(this);
|
||||
this.onClickAddHandler = this.onClickAddHandler.bind(this);
|
||||
this.onClickDeleteHandler = this.onClickDeleteHandler.bind(this);
|
||||
}
|
||||
|
||||
onChangeERHandler(event, value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onClickAddHandler(input, select, idx) {
|
||||
this.props.onChangeERHandler(
|
||||
input.value,
|
||||
select.options[select.selectedIndex].value,
|
||||
idx
|
||||
);
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
onClickHandler() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onClickDeleteHandler(select, idx) {
|
||||
this.props.onClickDeletePropertyHandler(select, idx);
|
||||
}
|
||||
|
||||
onClickDeleteERHandler(idx) {
|
||||
this.props.onClickDeleteERHandler(idx);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { field, } = this.props;
|
||||
const value = this.props.value ? this.props.value : [];
|
||||
const { description, } = this.props;
|
||||
const { prefix, } = this.props;
|
||||
const { required, } = this.props;
|
||||
const invalid = required && !value.length;
|
||||
const warning = invalid? (<div className='required-warning'>This field is required</div>) : "";
|
||||
|
||||
return (
|
||||
<div className="er-container">
|
||||
<div className="er-header">
|
||||
{field}
|
||||
<span
|
||||
data-tooltip-id={`${field}-tooltip`}
|
||||
data-tooltip-content={description}
|
||||
className="material-icons"
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<span
|
||||
data-tooltip-id={`${field}-control-tooltip`}
|
||||
data-tooltip-content="Add an External Reference"
|
||||
onClick={() => this.props.onClickAddObjectHandler(field, ['source_name'])}
|
||||
className="add material-icons"
|
||||
>
|
||||
control_point
|
||||
</span>
|
||||
<Tooltip id={`${field}-tooltip`} />
|
||||
<Tooltip id={`${field}-control-tooltip`} />
|
||||
</div>
|
||||
<div className={classNames({
|
||||
"er-body": true,
|
||||
"invalid": invalid
|
||||
})}>
|
||||
{value.map((p, i) => (
|
||||
<ReferenceBlock
|
||||
key={i}
|
||||
i={i}
|
||||
kv={p}
|
||||
prefix={prefix}
|
||||
onChangeERHandler={this.onChangeERHandler}
|
||||
onClickDeleteERHandler={this.onClickDeleteERHandler}
|
||||
onClickAddHandler={this.onClickAddHandler}
|
||||
onClickDeleteHandler={this.onClickDeleteHandler}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default observer(ExternalReferences);
|
||||
|
||||
function ReferenceBlock(props) {
|
||||
const blocks = [];
|
||||
const idx = props.i;
|
||||
const prefix = props.prefix;
|
||||
const selectID = `select-${prefix}-${props.i}`;
|
||||
const inputID = `input-${prefix}-${props.i}`;
|
||||
|
||||
const propValues = [
|
||||
'source_name',
|
||||
'description',
|
||||
'url',
|
||||
'hashes',
|
||||
'external_id'
|
||||
];
|
||||
|
||||
for (const item in props.kv) {
|
||||
let remove = (
|
||||
<span
|
||||
onClick={() => props.onClickDeleteHandler(item, props.i)}
|
||||
className="remove material-icons"
|
||||
>
|
||||
highlight_off
|
||||
</span>
|
||||
);
|
||||
|
||||
if (item === 'source_name') {
|
||||
remove = undefined;
|
||||
}
|
||||
|
||||
blocks.push(
|
||||
<div key={uuid()} className="er-block-row">
|
||||
<div>
|
||||
{item}
|
||||
:
|
||||
{JSON.stringify(props.kv[item])}
|
||||
{' '}
|
||||
{remove}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="er-block">
|
||||
<div className="er-block-row">
|
||||
<select id={selectID}>
|
||||
{propValues.map((prop) => (
|
||||
<option key={uuid()} value={prop}>
|
||||
{prop}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Text id={inputID} onChange={props.onChangeERHandler} />
|
||||
<span
|
||||
className="add material-icons"
|
||||
onClick={() => props.onClickAddHandler(
|
||||
document.getElementById(inputID),
|
||||
document.getElementById(selectID),
|
||||
props.i
|
||||
)}
|
||||
>
|
||||
control_point
|
||||
</span>
|
||||
</div>
|
||||
{blocks}
|
||||
<div className="er-block-row">
|
||||
<span
|
||||
className="remove remove-er material-icons"
|
||||
onClick={() => props.onClickDeleteERHandler(idx)}
|
||||
>
|
||||
highlight_off
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
159
stix-modeler-app/src/components/ui/complex/GenericObject.jsx
Normal file
159
stix-modeler-app/src/components/ui/complex/GenericObject.jsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import classNames from 'classnames';
|
||||
import Text from '../inputs/Text';
|
||||
|
||||
import './genericobject.scss';
|
||||
|
||||
class GenericObject extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChangeSelectHandler = this.onChangeSelectHandler.bind(this);
|
||||
this.onChangeInputHandler = this.onChangeInputHandler.bind(this);
|
||||
this.onClickAddObjectHandler = this.onClickAddObjectHandler.bind(this);
|
||||
this.onClickDeleteHandler = this.onClickDeleteHandler.bind(this);
|
||||
this.onClickCreateBlankHandler = this.onClickCreateBlankHandler.bind(this);
|
||||
|
||||
this.state = {
|
||||
key: this.props.vocab? this.props.vocab[0] : '',
|
||||
value: '',
|
||||
};
|
||||
}
|
||||
|
||||
onChangeInputHandler(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
[event.currentTarget.name]: event.currentTarget.value,
|
||||
});
|
||||
}
|
||||
|
||||
onChangeSelectHandler(event) {
|
||||
event.preventDefault();
|
||||
const key = document.getElementById(`select-${this.props.field}`).value;
|
||||
this.setState({
|
||||
"key": key
|
||||
});
|
||||
}
|
||||
|
||||
onClickDeleteHandler(select, idx) {
|
||||
this.props.onClickDeleteObjectHandler(select, idx);
|
||||
}
|
||||
|
||||
onClickCreateBlankHandler() {
|
||||
this.setState({
|
||||
key: '',
|
||||
value: '',
|
||||
});
|
||||
}
|
||||
|
||||
onClickAddObjectHandler() {
|
||||
const o = {};
|
||||
|
||||
o[this.state.key] = this.state.value;
|
||||
|
||||
this.props.onClickAddObjectHandler(this.props.field, o);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { field, } = this.props;
|
||||
const { vocab, } = this.props;
|
||||
const value = this.props.value ?? {};
|
||||
const { description, } = this.props;
|
||||
const {required, } = this.props;
|
||||
const rows = [];
|
||||
const invalid = required && !Object.keys(value).length;
|
||||
const warning = invalid? (<div className='required-warning'>This field is required</div>) : "";
|
||||
|
||||
for (const key in value) {
|
||||
rows.push(
|
||||
<ExtBlocks
|
||||
key={uuid()}
|
||||
v={value[key]}
|
||||
k={key}
|
||||
field={field}
|
||||
onClickDeleteHandler={this.onClickDeleteHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let selector;
|
||||
if (vocab && vocab.length) {
|
||||
selector = (
|
||||
<select id={`select-${field}`} defaultValue={vocab[0]} onChange={this.onChangeSelectHandler}>
|
||||
{vocab.map((key) => (
|
||||
<option id={key} value={key}>
|
||||
{key}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
} else {
|
||||
selector = (
|
||||
<Text name="key" value={this.state.key} onChange={this.onChangeInputHandler} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="go-container">
|
||||
<div className="go-header">
|
||||
{field}
|
||||
<span
|
||||
data-tooltip-id={`${field}-tooltip`}
|
||||
className="material-icons"
|
||||
data-tooltip-content={description}
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id={`${field}-tooltip`} />
|
||||
</div>
|
||||
<div className={classNames({
|
||||
"go-body": true,
|
||||
"invalid": invalid
|
||||
})}>
|
||||
|
||||
<div className="go-block-input">
|
||||
<div className="input">
|
||||
{selector}
|
||||
</div>
|
||||
<div className="input">
|
||||
<Text name="value" value={this.state.value} onChange={this.onChangeInputHandler} />
|
||||
</div>
|
||||
<div className="add-container">
|
||||
<span onClick={this.onClickAddObjectHandler} className="add material-icons">control_point</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rows}
|
||||
</div>
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default observer(GenericObject);
|
||||
|
||||
function ExtBlocks(props) {
|
||||
let { v, } = props;
|
||||
|
||||
if (typeof props.v === 'object') {
|
||||
v = JSON.stringify(props.v);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="go-block">
|
||||
<div className="go-block-row">
|
||||
{props.k}
|
||||
:
|
||||
{' '}
|
||||
{v}
|
||||
{' '}
|
||||
<span onClick={() => props.onClickDeleteHandler(props.field, props.k)} className="remove material-icons">highlight_off</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
128
stix-modeler-app/src/components/ui/complex/KillChain.jsx
Normal file
128
stix-modeler-app/src/components/ui/complex/KillChain.jsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './killchain.scss';
|
||||
|
||||
class KillChain extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChangePhaseHandler = this.onChangePhaseHandler.bind(this);
|
||||
this.populatePhase = this.populatePhase.bind(this);
|
||||
}
|
||||
|
||||
onChangePhaseHandler(event) {
|
||||
const kcDomName = `kc-name-${this.props.node.id}`;
|
||||
const phaseDomName = `phase-${this.props.node.id}`;
|
||||
const kcIndex = document.getElementById(kcDomName).selectedIndex;
|
||||
const kcValue = document.getElementById(kcDomName)[kcIndex].value;
|
||||
const phaseValue = event.currentTarget.value;
|
||||
|
||||
const value = {
|
||||
kill_chain_name: kcValue,
|
||||
phase_name: phaseValue,
|
||||
};
|
||||
|
||||
this.props.onChangeHandler(this.props.field, value);
|
||||
|
||||
document.getElementById(kcDomName).selectedIndex = 0;
|
||||
document.getElementById(phaseDomName).selectedIndex = 0;
|
||||
|
||||
// Reset phase name so we don't keep adding the same values
|
||||
// multiple times.
|
||||
document.getElementById(phaseDomName).innerHTML = '';
|
||||
const option = document.createElement('option');
|
||||
option.value = 0;
|
||||
option.text = ' -- Select Phase -- ';
|
||||
document.getElementById(phaseDomName).add(option);
|
||||
}
|
||||
|
||||
populatePhase(event) {
|
||||
const phaseDomName = `phase-${this.props.node.id}`;
|
||||
const phaseDOM = document.getElementById(phaseDomName);
|
||||
const kc = event.currentTarget.value;
|
||||
|
||||
this.props.vocab.map((item) => {
|
||||
if (item.value === kc) {
|
||||
item.phases.map((phase) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = phase.phase_name;
|
||||
option.text = phase.label;
|
||||
phaseDOM.add(option);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const vocab = this.props.vocab ? this.props.vocab : [];
|
||||
const { field, } = this.props;
|
||||
const value = this.props.value ? this.props.value : [];
|
||||
const { description, } = this.props;
|
||||
const {required, } = this.props;
|
||||
const invalid = required && !value.length;
|
||||
const warning = invalid? (<div className='required-warning'>This field is required</div>) : "";
|
||||
|
||||
const kcName = `kc-name-${this.props.node.id}`;
|
||||
const phaseName = `phase-${this.props.node.id}`;
|
||||
|
||||
return (
|
||||
<div className="kill-chain-container">
|
||||
<div className="kill-chain-header">
|
||||
{field}
|
||||
<span
|
||||
data-tooltip-id={`${field}-tooltip`}
|
||||
className="material-icons"
|
||||
data-tooltip-content={description}
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id={`${field}-tooltip`} />
|
||||
</div>
|
||||
<div className={classNames({
|
||||
"kill-chain-body": true,
|
||||
"invalid": invalid
|
||||
})}>
|
||||
<div className="kill-chain-options">
|
||||
<select id={kcName} onChange={this.populatePhase}>
|
||||
<option value={0}> -- Select Kill Chain -- </option>
|
||||
{
|
||||
vocab.map((item) => (
|
||||
<option
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
>
|
||||
{item.label}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
|
||||
<select id={phaseName} onChange={this.onChangePhaseHandler}>
|
||||
<option value={0}> -- Select Phase -- </option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{
|
||||
value.map((p, i) => (
|
||||
<div key={i} className="kill-chain-row">
|
||||
<div>
|
||||
{p.kill_chain_name}
|
||||
{' '}
|
||||
-
|
||||
{' '}
|
||||
{p.phase_name}
|
||||
{' '}
|
||||
<span onClick={() => this.props.onClickRemoveHandler(field, i)} className="material-icons">highlight_off</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default observer(KillChain);
|
||||
162
stix-modeler-app/src/components/ui/complex/ObjectArray.jsx
Normal file
162
stix-modeler-app/src/components/ui/complex/ObjectArray.jsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import classNames from 'classnames';
|
||||
import Text from '../inputs/Text';
|
||||
import './externalreferences.scss';
|
||||
|
||||
class ObjectArray extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onClickHandler = this.onClickHandler.bind(this);
|
||||
this.onChangeArrayObjectHandler = this.onChangeArrayObjectHandler.bind(this);
|
||||
this.onClickDeleteObjectHandler = this.onClickDeleteObjectHandler.bind(this);
|
||||
this.onClickAddHandler = this.onClickAddHandler.bind(this);
|
||||
this.onClickDeletePropertyHandler = this.onClickDeletePropertyHandler.bind(this);
|
||||
}
|
||||
|
||||
onChangeArrayObjectHandler(event, value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onClickAddHandler(input, field, idx) {
|
||||
this.props.onChangeObjectHandler(
|
||||
input.value,
|
||||
field.value,
|
||||
idx,
|
||||
this.props.field
|
||||
);
|
||||
field.value = '';
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
onClickHandler() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onClickDeletePropertyHandler(select, idx) {
|
||||
this.props.onClickDeleteArrayObjectPropertyHandler(select, idx, this.props.field);
|
||||
}
|
||||
|
||||
onClickDeleteObjectHandler(idx, property) {
|
||||
this.props.onClickDeleteArrayObjectHandler(idx, property);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { field, } = this.props;
|
||||
const value = this.props.value ? this.props.value : [];
|
||||
const { description, } = this.props;
|
||||
const {required, } = this.props;
|
||||
const invalid = required && !value.length;
|
||||
const warning = invalid? (<div className='required-warning'>This field is required</div>) : "";
|
||||
|
||||
return (
|
||||
<div className="er-container">
|
||||
<div className="er-header">
|
||||
{field}
|
||||
<span
|
||||
data-tooltip-id={`${field}-tooltip`}
|
||||
className="material-icons"
|
||||
data-tooltip-content={description}
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<span
|
||||
data-tooltip-id={`add-${field}-tooltip`}
|
||||
data-tooltip-content={`Add to ${field}`}
|
||||
onClick={() => this.props.onClickAddObjectHandler(field, [])}
|
||||
className="add material-icons"
|
||||
>
|
||||
control_point
|
||||
</span>
|
||||
<Tooltip id={`${field}-tooltip`} />
|
||||
<Tooltip id={`add-${field}-tooltip`} />
|
||||
</div>
|
||||
<div className={classNames({
|
||||
"er-body": true,
|
||||
"invalid": invalid,
|
||||
})}>
|
||||
{value.map((p, i) => (
|
||||
<ObjectBlock
|
||||
key={i}
|
||||
i={i}
|
||||
kv={p}
|
||||
field={field}
|
||||
onChangeArrayObjectHandler={this.onChangeArrayObjectHandler}
|
||||
onClickDeleteObjectHandler={this.onClickDeleteObjectHandler}
|
||||
onClickAddHandler={this.onClickAddHandler}
|
||||
onClickDeletePropertyHandler={this.onClickDeletePropertyHandler}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default observer(ObjectArray);
|
||||
|
||||
function ObjectBlock(props) {
|
||||
const blocks = [];
|
||||
const idx = props.i;
|
||||
const fieldID = `field-${props.i}`;
|
||||
const inputID = `input-${props.i}`;
|
||||
const { field, } = props;
|
||||
|
||||
for (const item in props.kv) {
|
||||
let remove = (
|
||||
<span
|
||||
onClick={() => props.onClickDeletePropertyHandler(item, props.i)}
|
||||
className="remove material-icons"
|
||||
>
|
||||
highlight_off
|
||||
</span>
|
||||
);
|
||||
|
||||
if (item === 'source_name') {
|
||||
remove = undefined;
|
||||
}
|
||||
|
||||
blocks.push(
|
||||
<div key={uuid()} className="er-block-row">
|
||||
<div>
|
||||
{item}
|
||||
:
|
||||
{JSON.stringify(props.kv[item])}
|
||||
{' '}
|
||||
{remove}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="er-block">
|
||||
<div className="er-block-row">
|
||||
|
||||
<Text id={fieldID} onChange={props.onChangeArrayObjectHandler} />
|
||||
<Text id={inputID} onChange={props.onChangeArrayObjectHandler} />
|
||||
<span
|
||||
className="add material-icons"
|
||||
onClick={() => props.onClickAddHandler(
|
||||
document.getElementById(inputID),
|
||||
document.getElementById(fieldID),
|
||||
props.i
|
||||
)}
|
||||
>
|
||||
control_point
|
||||
</span>
|
||||
</div>
|
||||
{blocks}
|
||||
<div className="er-block-row">
|
||||
<span
|
||||
className="remove remove-er material-icons"
|
||||
onClick={() => props.onClickDeleteObjectHandler(idx, field)}
|
||||
>
|
||||
highlight_off
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
.ct-container {
|
||||
padding-left: 20px;
|
||||
padding-top: 20px;
|
||||
padding-right: 10px;
|
||||
|
||||
.ct-header {
|
||||
font-weight: bold;
|
||||
color: defaults.$default-active-bg;
|
||||
padding-bottom: 3px;
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ct-body {
|
||||
overflow-x: hidden;
|
||||
border: 1px solid defaults.$standard-border-color;
|
||||
min-height: 50px;
|
||||
width: 100%;
|
||||
|
||||
|
||||
.ct-block-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
|
||||
.input {
|
||||
margin: 10px 0px 10px 10px;
|
||||
flex: 1;
|
||||
|
||||
textarea {
|
||||
font-size: 14px;
|
||||
font-family: tahoma;
|
||||
}
|
||||
}
|
||||
|
||||
.add-container {
|
||||
width: 50px;
|
||||
padding: 35px 0px 0px 8px;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
color: defaults.$default-active-bg;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ct-output {
|
||||
font-family: tahoma;
|
||||
font-size: 14px;
|
||||
padding: 0px 15px 15px 15px;
|
||||
}
|
||||
|
||||
.go-block {
|
||||
margin: 10px;
|
||||
|
||||
.go-block-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 5px;
|
||||
input {
|
||||
margin: 10px 0px 10px 10px;
|
||||
flex: .5;
|
||||
}
|
||||
div {
|
||||
padding: 10px;
|
||||
flex: .5;
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: defaults.$error-font;
|
||||
}
|
||||
|
||||
.add {
|
||||
padding-top: 15px;
|
||||
color: defaults.$default-active-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
.er-container {
|
||||
padding-left: 20px;
|
||||
padding-top: 20px;
|
||||
|
||||
.er-header {
|
||||
font-weight: bold;
|
||||
color: defaults.$default-active-bg;
|
||||
padding-bottom: 3px;
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.er-body {
|
||||
overflow: auto;
|
||||
border: 1px solid defaults.$standard-border-color;
|
||||
min-height: 50px;
|
||||
|
||||
.er-block {
|
||||
border: 1px solid defaults.$standard-border-color;
|
||||
margin: 10px;
|
||||
|
||||
.er-block-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
select {
|
||||
margin: 10px 0px 10px 10px;
|
||||
flex: 0.5;
|
||||
}
|
||||
div {
|
||||
padding: 10px;
|
||||
flex: 0.5;
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: defaults.$error-font;
|
||||
}
|
||||
|
||||
.remove-er {
|
||||
margin-left: auto;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.add {
|
||||
padding-top: 15px;
|
||||
color: defaults.$default-active-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
.go-container {
|
||||
padding-left: 20px;
|
||||
padding-top: 20px;
|
||||
padding-right: 10px;
|
||||
|
||||
.go-header {
|
||||
font-weight: bold;
|
||||
color: defaults.$default-active-bg;
|
||||
padding-bottom: 3px;
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.go-body {
|
||||
overflow-x: hidden;
|
||||
border: 1px solid defaults.$standard-border-color;
|
||||
min-height: 50px;
|
||||
width: 100%;
|
||||
|
||||
|
||||
.go-block-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
|
||||
.input {
|
||||
margin: 10px 0px 10px 10px;
|
||||
flex: .5;
|
||||
}
|
||||
|
||||
.add-container {
|
||||
width: 50px;
|
||||
padding: 20px 0px 0px 15px;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
color: defaults.$default-active-bg;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.go-block {
|
||||
margin: 10px;
|
||||
|
||||
.go-block-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 5px;
|
||||
input {
|
||||
margin: 10px 0px 10px 10px;
|
||||
flex: .5;
|
||||
}
|
||||
div {
|
||||
padding: 10px;
|
||||
flex: .5;
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: defaults.$error-font;
|
||||
}
|
||||
|
||||
.add {
|
||||
padding-top: 15px;
|
||||
color: defaults.$default-active-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
stix-modeler-app/src/components/ui/complex/killchain.scss
Normal file
43
stix-modeler-app/src/components/ui/complex/killchain.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
.kill-chain-container {
|
||||
padding-left: 20px;
|
||||
padding-top: 20px;
|
||||
|
||||
.kill-chain-header {
|
||||
font-weight: bold;
|
||||
color: defaults.$default-active-bg;
|
||||
padding-bottom: 3px;
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.kill-chain-body {
|
||||
|
||||
overflow: auto;
|
||||
border: 1px solid defaults.$standard-border-color;
|
||||
|
||||
.kill-chain-options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
select {
|
||||
flex: .5;
|
||||
margin: 10px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.kill-chain-row {
|
||||
padding: 5px 5px 5px 10px;
|
||||
.material-icons {
|
||||
vertical-align: middle;
|
||||
color: defaults.$error-font;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
stix-modeler-app/src/components/ui/growl/Growl.jsx
Normal file
42
stix-modeler-app/src/components/ui/growl/Growl.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './growl.scss';
|
||||
|
||||
class Growl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClickHideHandler() {
|
||||
if (this.props.onClickHideHandler) {
|
||||
this.props.onClickHideHandler();
|
||||
} else {
|
||||
console.warn('No JSON Viewer close handler');
|
||||
}
|
||||
}
|
||||
|
||||
onClickPanelHandler(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
const cls = classNames({
|
||||
growl: true,
|
||||
'hide-mask': !this.props.show,
|
||||
});
|
||||
|
||||
if (this.props.timer) {
|
||||
this.props.timer();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cls}>
|
||||
<div className="panel">
|
||||
{this.props.message}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default (observer(Growl));
|
||||
12
stix-modeler-app/src/components/ui/growl/growl.scss
Normal file
12
stix-modeler-app/src/components/ui/growl/growl.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@use "../../../defaults";
|
||||
|
||||
.growl {
|
||||
position: fixed;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
z-index: defaults.$growl-index;
|
||||
background-color: defaults.$default-active-bg;
|
||||
color: defaults.$light-font-0;
|
||||
padding: 11px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
74
stix-modeler-app/src/components/ui/inputs/ArraySelector.jsx
Normal file
74
stix-modeler-app/src/components/ui/inputs/ArraySelector.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import classNames from 'classnames';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
|
||||
import './arrayselector.scss';
|
||||
|
||||
class ArraySelector extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClickHandler(field, value) {
|
||||
this.props.onClickHandler(field, value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const items = this.props.vocab ? this.props.vocab : [];
|
||||
const { field, } = this.props;
|
||||
const { value, } = this.props ?? [];
|
||||
const { description, } = this.props;
|
||||
const { required, } = this.props;
|
||||
const invalid = required && !value.length;
|
||||
const warning = invalid? (<div className='required-warning'>This field is required</div>) : "";
|
||||
|
||||
let cls = classNames({
|
||||
'array-container-item': true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="array-container">
|
||||
<div className="array-container-header">
|
||||
{field}
|
||||
{' '}
|
||||
<span
|
||||
data-tooltip-id={`${field}-tooltip`}
|
||||
className="material-icons"
|
||||
data-tooltip-content={description}
|
||||
>
|
||||
info
|
||||
</span>
|
||||
<Tooltip id={`${field}-tooltip`} />
|
||||
</div>
|
||||
<div className={classNames({
|
||||
"array-container-body": true,
|
||||
"invalid": invalid
|
||||
})}>
|
||||
{items.map((item, i) => {
|
||||
if (value && value.indexOf(item) > -1) {
|
||||
cls = classNames({
|
||||
'array-container-item': true,
|
||||
'array-container-selected': true,
|
||||
});
|
||||
} else {
|
||||
cls = classNames({
|
||||
'array-container-item': true,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cls}
|
||||
key={i}
|
||||
onClick={() => this.onClickHandler(field, item)}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default observer(ArraySelector);
|
||||
43
stix-modeler-app/src/components/ui/inputs/Boolean.jsx
Normal file
43
stix-modeler-app/src/components/ui/inputs/Boolean.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './boolean.scss';
|
||||
|
||||
class Boolean extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClickHandler(field, value) {
|
||||
this.props.onClickHandler(field, value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const value = this.props.selected;
|
||||
let trueCls = classNames({
|
||||
selected: false,
|
||||
});
|
||||
|
||||
let falseCls = classNames({
|
||||
selected: false,
|
||||
});
|
||||
|
||||
if (value) {
|
||||
trueCls = classNames({
|
||||
selected: true,
|
||||
});
|
||||
} else {
|
||||
falseCls = classNames({
|
||||
selected: true,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="boolean">
|
||||
<div className={trueCls} onClick={() => this.props.onClick(this.props.name, true)}>True</div>
|
||||
<div className={falseCls} onClick={() => this.props.onClick(this.props.name, false)}>False</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default observer(Boolean);
|
||||
28
stix-modeler-app/src/components/ui/inputs/CSVInput.jsx
Normal file
28
stix-modeler-app/src/components/ui/inputs/CSVInput.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import Text from './Text';
|
||||
|
||||
import './csvselector.scss';
|
||||
|
||||
class CSVInput extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClickHandler(field, value) {
|
||||
this.props.onClickHandler(field, value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const value = this.props.value ?? "";
|
||||
|
||||
return (
|
||||
<Text
|
||||
name={this.props.name}
|
||||
value={value}
|
||||
required={this.props.required}
|
||||
onChange={this.props.onChangeHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} export default observer(CSVInput);
|
||||
41
stix-modeler-app/src/components/ui/inputs/DateTime.jsx
Normal file
41
stix-modeler-app/src/components/ui/inputs/DateTime.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import DatePicker from 'react-datepicker';
|
||||
|
||||
import Text from './Text.jsx';
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import './datetime.scss';
|
||||
|
||||
export default class DateTime extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
onChange(datetime) {
|
||||
this.props.onDateChange(this.props.name, datetime);
|
||||
}
|
||||
|
||||
render() {
|
||||
let dts = this.props.selected;
|
||||
let control = (<Text
|
||||
name={this.props.name} value={dts} required={this.props.required}
|
||||
onChange={this.props.onTextChange}
|
||||
/>);
|
||||
|
||||
|
||||
if (typeof dts === 'string') {
|
||||
const dateObj = new Date(dts);
|
||||
if (isNaN(dateObj.getTime())) {
|
||||
return control;
|
||||
} else {
|
||||
dts = dateObj;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DatePicker selected={dts} onChange={this.onChange} name={this.props.name} />
|
||||
);
|
||||
}
|
||||
}
|
||||
31
stix-modeler-app/src/components/ui/inputs/FileSelector.jsx
Normal file
31
stix-modeler-app/src/components/ui/inputs/FileSelector.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import './fileselector.scss';
|
||||
|
||||
class FileSelector extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChangeHandler = this.onChangeHandler.bind(this);
|
||||
}
|
||||
|
||||
onChangeHandler(event) {
|
||||
this.props.onChange(event);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
className="def custom-file-selector"
|
||||
type="file"
|
||||
id="file-selector"
|
||||
name="file"
|
||||
multiple={this.props.multiple}
|
||||
accept={this.props.type}
|
||||
onChange={this.onChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default observer(FileSelector);
|
||||
75
stix-modeler-app/src/components/ui/inputs/LabeledText.jsx
Normal file
75
stix-modeler-app/src/components/ui/inputs/LabeledText.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Text from './Text';
|
||||
import { observer } from 'mobx-react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './text.scss';
|
||||
|
||||
class LabeledText extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChangeHandler = this.onChangeHandler.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.hasInitialFocus) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
if (this.input) {
|
||||
this.input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDownHandler(event) {
|
||||
if (event.keyCode === 13 && this.props.onReturn) {
|
||||
this.props.onReturn();
|
||||
} else if (event.keyCode === 27 && this.props.onEscape) {
|
||||
this.props.onEscape();
|
||||
}
|
||||
}
|
||||
|
||||
onChangeHandler(event) {
|
||||
this.props.onChange(event);
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputType = this.props.type ? this.props.type : 'text';
|
||||
const { required, } = this.props ?? "";
|
||||
const { value, } = this.props;
|
||||
const invalid = required && !value.length;
|
||||
const warning = invalid? (<div className='required-warning'>This field is required</div>) : "";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
name={this.props.name}
|
||||
type={inputType}
|
||||
ref={(c) => { this.input = c; }}
|
||||
autoComplete={this.props.autocomplete || 'off'}
|
||||
className={classNames({
|
||||
"def": true,
|
||||
"invalid": invalid
|
||||
})}
|
||||
placeholder={this.props.placeholder}
|
||||
onChange={this.onChangeHandler}
|
||||
onKeyDown={(e) => this.onKeyDownHandler(e)}
|
||||
value={value}
|
||||
disabled={this.props.disabled}
|
||||
id={this.props.id}
|
||||
/>
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default observer(LabeledText);
|
||||
|
||||
Text.propTypes = {
|
||||
hasInitialFocus: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onReturn: PropTypes.func,
|
||||
onEscape: PropTypes.func,
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const OrientationRadioGroup = ({ defaultOrientation, onOrientationChange }) => {
|
||||
const [selectedOption, setSelectedOption] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultOrientation) {
|
||||
setSelectedOption(defaultOrientation);
|
||||
}
|
||||
}, [defaultOrientation]);
|
||||
|
||||
const handleOptionChange = (event) => {
|
||||
const newOrientation = event.target.value;
|
||||
setSelectedOption(newOrientation);
|
||||
if (onOrientationChange) {
|
||||
onOrientationChange(newOrientation);
|
||||
} else {
|
||||
console.error("onOrientationChange function is missing!");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Choose Orientation</p>
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="radio"
|
||||
id="rowView"
|
||||
name="orientation"
|
||||
value="Row View"
|
||||
checked={selectedOption === 'Row View'}
|
||||
onChange={handleOptionChange}
|
||||
/>
|
||||
<label htmlFor="rowView">Row View</label>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
id="columnView"
|
||||
name="orientation"
|
||||
value="Column View"
|
||||
checked={selectedOption === 'Column View'}
|
||||
onChange={handleOptionChange}
|
||||
/>
|
||||
<label htmlFor="columnView">Column View</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrientationRadioGroup;
|
||||
61
stix-modeler-app/src/components/ui/inputs/RadioGroup.jsx
Normal file
61
stix-modeler-app/src/components/ui/inputs/RadioGroup.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const RadioGroup = ({ defaultLayoutMethod, onLayoutChange }) => {
|
||||
const [selectedOption, setSelectedOption] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultLayoutMethod) {
|
||||
setSelectedOption(defaultLayoutMethod);
|
||||
}
|
||||
}, [defaultLayoutMethod]);
|
||||
|
||||
const handleOptionChange = (event) => {
|
||||
const newLayout = event.target.value;
|
||||
setSelectedOption(newLayout);
|
||||
if (onLayoutChange) {
|
||||
onLayoutChange(newLayout);
|
||||
} else {
|
||||
console.error("onLayoutChange function is missing!");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Choose Layout Mode</p>
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
<input
|
||||
type="radio"
|
||||
id="hierarchy"
|
||||
name="layout"
|
||||
value="Hierarchy"
|
||||
checked={selectedOption === 'Hierarchy'}
|
||||
onChange={handleOptionChange}
|
||||
/>
|
||||
<label htmlFor="hierarchy">Hierarchy</label>
|
||||
|
||||
<input
|
||||
type="radio"
|
||||
id="grid"
|
||||
name="layout"
|
||||
value="Grid"
|
||||
checked={selectedOption === 'Grid'}
|
||||
onChange={handleOptionChange}
|
||||
/>
|
||||
<label htmlFor="grid">Grid</label>
|
||||
</div>
|
||||
{/* <div>
|
||||
<input
|
||||
type="radio"
|
||||
id="randomized"
|
||||
name="layout"
|
||||
value="Randomized"
|
||||
checked={selectedOption === 'Randomized'}
|
||||
onChange={handleOptionChange}
|
||||
/>
|
||||
<label htmlFor="randomized">Randomized</label>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadioGroup;
|
||||
32
stix-modeler-app/src/components/ui/inputs/Slider.jsx
Normal file
32
stix-modeler-app/src/components/ui/inputs/Slider.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import RCSlider from 'rc-slider/lib/Slider';
|
||||
import 'rc-slider/assets/index.css';
|
||||
|
||||
import './slider.scss';
|
||||
|
||||
class Slider extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChangeSliderHandler = this.onChangeSliderHandler.bind(this);
|
||||
}
|
||||
|
||||
onChangeSliderHandler(value) {
|
||||
this.props.onChangeHandler(this.props.field, value);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RCSlider
|
||||
className="horizontal-slider"
|
||||
value={this.props.value}
|
||||
marks={{
|
||||
10: 10, 20: 20, 30: 30, 40: 40, 50: 50, 60: 60, 70: 70, 80: 80, 90: 90, 100: 100,
|
||||
}}
|
||||
onChange={this.onChangeSliderHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} export default observer(Slider);
|
||||
73
stix-modeler-app/src/components/ui/inputs/Text.jsx
Normal file
73
stix-modeler-app/src/components/ui/inputs/Text.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { observer } from 'mobx-react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './text.scss';
|
||||
|
||||
class Text extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChangeHandler = this.onChangeHandler.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.hasInitialFocus) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
if (this.input) {
|
||||
this.input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDownHandler(event) {
|
||||
if (event.keyCode === 13 && this.props.onReturn) {
|
||||
this.props.onReturn();
|
||||
} else if (event.keyCode === 27 && this.props.onEscape) {
|
||||
this.props.onEscape();
|
||||
}
|
||||
}
|
||||
|
||||
onChangeHandler(event) {
|
||||
this.props.onChange(event);
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputType = this.props.type ?? 'text';
|
||||
const { required, } = this.props;
|
||||
const { value, } = this.props ?? "";
|
||||
const invalid = required && !`${value}`.length; // prevents 0 from being interpreted as missing field
|
||||
const warning = invalid? (<div className='required-warning'>This field is required</div>) : "";
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
name={this.props.name}
|
||||
type={inputType}
|
||||
ref={(c) => { this.input = c; }}
|
||||
autoComplete={this.props.autocomplete || 'off'}
|
||||
className={classNames({
|
||||
"def": true,
|
||||
"invalid": invalid
|
||||
})}
|
||||
placeholder={this.props.placeholder}
|
||||
onChange={this.onChangeHandler}
|
||||
onKeyDown={(e) => this.onKeyDownHandler(e)}
|
||||
value={value}
|
||||
disabled={this.props.disabled}
|
||||
id={this.props.id}
|
||||
/>
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default observer(Text);
|
||||
|
||||
Text.propTypes = {
|
||||
hasInitialFocus: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onReturn: PropTypes.func,
|
||||
onEscape: PropTypes.func,
|
||||
};
|
||||
70
stix-modeler-app/src/components/ui/inputs/TextArea.jsx
Normal file
70
stix-modeler-app/src/components/ui/inputs/TextArea.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './text.scss';
|
||||
|
||||
class TextArea extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChangeHandler = this.onChangeHandler.bind(this);
|
||||
|
||||
this.state = {
|
||||
value: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.hasInitialFocus) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
if (this.input) {
|
||||
this.input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDownHandler(event) {
|
||||
if (event.keyCode === 13 && this.props.onReturn) {
|
||||
this.props.onReturn();
|
||||
} else if (event.keyCode === 27 && this.props.onEscape) {
|
||||
this.props.onEscape();
|
||||
}
|
||||
}
|
||||
|
||||
onChangeHandler(event) {
|
||||
this.props.onChange(event);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {required, } = this.props;
|
||||
const { value, } = this.props;
|
||||
const invalid = required && !value.length;
|
||||
const warning = invalid? (<div className='required-warning'>This field is required</div>) : "";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<textarea
|
||||
name={this.props.name}
|
||||
ref={(c) => {
|
||||
this.input = c;
|
||||
}}
|
||||
autoComplete={this.props.autocomplete || 'off'}
|
||||
className={classNames({
|
||||
"def": true,
|
||||
"invalid": invalid
|
||||
})}
|
||||
placeholder={this.props.placeholder}
|
||||
onChange={this.onChangeHandler}
|
||||
onKeyDown={(e) => this.onKeyDownHandler(e)}
|
||||
value={value}
|
||||
disabled={this.props.disabled}
|
||||
id={this.props.id}
|
||||
/>
|
||||
{warning}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default observer(TextArea);
|
||||
93
stix-modeler-app/src/components/ui/inputs/arrayselector.scss
Normal file
93
stix-modeler-app/src/components/ui/inputs/arrayselector.scss
Normal file
@@ -0,0 +1,93 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
.array-container {
|
||||
padding-left: 20px;
|
||||
padding-top: 20px;
|
||||
|
||||
.array-container-header {
|
||||
font-weight: bold;
|
||||
color: defaults.$default-active-bg;
|
||||
padding-bottom: 3px;
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.array-container-input {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.remove {
|
||||
color: defaults.$error-font;
|
||||
}
|
||||
|
||||
.add {
|
||||
padding-top: 15px;
|
||||
color: defaults.$default-active-bg;
|
||||
}
|
||||
|
||||
.array-container-input {
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.array-container-input-item {
|
||||
overflow: hidden;
|
||||
|
||||
.array-container-item {
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
margin-top: 1px;
|
||||
border: 1px solid transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.array-container-body {
|
||||
height: 100px;
|
||||
overflow: auto;
|
||||
border: 1px solid defaults.$standard-border-color;
|
||||
|
||||
.array-container-item {
|
||||
cursor: pointer;
|
||||
line-height: 30px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.array-container-selected {
|
||||
background-color: defaults.$default-active-bg;
|
||||
}
|
||||
|
||||
.array-container-item:hover {
|
||||
background-color: defaults.$light-font-0;
|
||||
}
|
||||
|
||||
.array-block-input {
|
||||
display: flex;
|
||||
height: fit-content;
|
||||
flex-direction: col;
|
||||
width: 97%;
|
||||
|
||||
.input {
|
||||
margin: 10px 0px 10px 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.add-container {
|
||||
width: 50px;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
color: defaults.$default-active-bg;
|
||||
padding-left: 5px;
|
||||
padding-top: 40%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
stix-modeler-app/src/components/ui/inputs/boolean.scss
Normal file
20
stix-modeler-app/src/components/ui/inputs/boolean.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
.boolean {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
line-height: 30px;
|
||||
border: 1px solid defaults.$standard-border-color;
|
||||
width: 99%;
|
||||
|
||||
div {
|
||||
flex: .5;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: defaults.$default-active-bg;
|
||||
}
|
||||
|
||||
}
|
||||
17
stix-modeler-app/src/components/ui/inputs/datetime.scss
Normal file
17
stix-modeler-app/src/components/ui/inputs/datetime.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
.react-datepicker-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.react-datepicker__input-container input {
|
||||
height: 39px;
|
||||
border: 1px solid defaults.$standard-border-color;
|
||||
background-color: transparent;
|
||||
width: 97%;
|
||||
padding-left: 10px;
|
||||
font-family: defaults.$default-font-family;
|
||||
color: #000;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
20
stix-modeler-app/src/components/ui/inputs/fileselector.scss
Normal file
20
stix-modeler-app/src/components/ui/inputs/fileselector.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
input.def {
|
||||
height: 39px;
|
||||
border: 1px solid defaults.$standard-border-color;
|
||||
background-color: transparent;
|
||||
width: 97%;
|
||||
padding-left: 10px;
|
||||
color: defaults.$dark-font-0;
|
||||
font-size: 16px;
|
||||
font-family: defaults.$default-font-family;
|
||||
}
|
||||
|
||||
.custom-file-selector {
|
||||
border-radius: 5px;
|
||||
padding: 5px 4px 5px 11px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
background-color: defaults.$default-active-bg;
|
||||
}
|
||||
5
stix-modeler-app/src/components/ui/inputs/slider.scss
Normal file
5
stix-modeler-app/src/components/ui/inputs/slider.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
.rc-slider-track {
|
||||
background-color: transparet;
|
||||
}
|
||||
48
stix-modeler-app/src/components/ui/inputs/text.scss
Normal file
48
stix-modeler-app/src/components/ui/inputs/text.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input.def,
|
||||
textarea,
|
||||
select {
|
||||
height: 39px;
|
||||
border: 1px solid defaults.$standard-border-color;
|
||||
background-color: transparent;
|
||||
width: 97%;
|
||||
padding-left: 10px;
|
||||
color: defaults.$dark-font-0;
|
||||
font-size: 16px;
|
||||
font-family: defaults.$default-font-family;
|
||||
}
|
||||
|
||||
input.def {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
select {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder {
|
||||
color: #4f5257;
|
||||
}
|
||||
|
||||
::-ms-input-placeholder {
|
||||
color: red;
|
||||
}
|
||||
|
||||
::-moz-placeholder {
|
||||
color: red;
|
||||
}
|
||||
|
||||
::-moz-placeholder {
|
||||
color: red;
|
||||
}
|
||||
40
stix-modeler-app/src/components/ui/panel/Panel.jsx
Normal file
40
stix-modeler-app/src/components/ui/panel/Panel.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './panel.scss';
|
||||
|
||||
class Panel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onClickHideHandler = this.onClickHideHandler.bind(this);
|
||||
}
|
||||
|
||||
onClickHideHandler() {
|
||||
if (this.props.onClickHideHandler) {
|
||||
this.props.onClickHideHandler();
|
||||
} else {
|
||||
console.warn('No JSON Viewer close handler');
|
||||
}
|
||||
}
|
||||
|
||||
onClickPanelHandler(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
const cls = classNames({
|
||||
mask: true,
|
||||
'hide-mask': !this.props.show,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cls} onClick={this.onClickHideHandler}>
|
||||
<div className="panel" onClick={this.onClickPanelHandler}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} export default (observer(Panel));
|
||||
28
stix-modeler-app/src/components/ui/panel/panel.scss
Normal file
28
stix-modeler-app/src/components/ui/panel/panel.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
@use '../../../defaults';
|
||||
|
||||
.mask {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: rgba(0, 0, 0,.2);
|
||||
z-index: defaults.$panel-mask-index;
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: #fff;
|
||||
box-shadow: -20px 25px 50px 0px #000;
|
||||
z-index: defaults.$panel-index;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-mask {
|
||||
display: none;
|
||||
}
|
||||
16
stix-modeler-app/src/defaults.scss
Normal file
16
stix-modeler-app/src/defaults.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
$default-font-family: 'Alegreya Sans SC';
|
||||
|
||||
//colors
|
||||
$default-active-bg: #46a0f5;
|
||||
$error-font: #d31d18;
|
||||
$warning-font: #dda20f;
|
||||
$light-font-0: #e1e3e6;
|
||||
$gray-font-0: #c8c5c5;
|
||||
$dark-font-0: #000;
|
||||
$standard-border-color: #c5cbd2;
|
||||
$lt-gray-bg: #f1f3f5;
|
||||
|
||||
//define z-index for app
|
||||
$panel-mask-index: 5;
|
||||
$panel-index: 10;
|
||||
$growl-index: 15;
|
||||
29
stix-modeler-app/src/definition-adapters/Artifact.js
Normal file
29
stix-modeler-app/src/definition-adapters/Artifact.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/observable-common.json';
|
||||
import rawDefinition from '../definitions/artifact.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class Artifact extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'observable.png',
|
||||
prefix: 'artifact--',
|
||||
active: false,
|
||||
relationships: [],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.payload_bin.type = 'string';
|
||||
this.properties.url.type = 'string';
|
||||
this.properties.encryption_algorithm.type = 'string';
|
||||
this.properties.encryption_algorithm.vocab = this.definitions["encryption-algorithm-enum"].enum;
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new Artifact();
|
||||
|
||||
export default singleton;
|
||||
31
stix-modeler-app/src/definition-adapters/AttackPattern.js
Normal file
31
stix-modeler-app/src/definition-adapters/AttackPattern.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/common.json';
|
||||
import rawDefinition from '../definitions/attack-pattern.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class AttackPattern extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'attack-pattern.png',
|
||||
prefix: 'attack-pattern--',
|
||||
active: true,
|
||||
relationships: [
|
||||
{ type: 'delivers', target: 'malware', },
|
||||
{ type: 'targets', target: 'identity', },
|
||||
{ type: 'targets', target: 'location', },
|
||||
{ type: 'targets', target: 'vulnerability', },
|
||||
{ type: 'uses', target: 'malware', },
|
||||
{ type: 'uses', target: 'tool', }
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new AttackPattern();
|
||||
|
||||
export default singleton;
|
||||
26
stix-modeler-app/src/definition-adapters/AutonomousSystem.js
Normal file
26
stix-modeler-app/src/definition-adapters/AutonomousSystem.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/observable-common.json';
|
||||
import rawDefinition from '../definitions/autonomous-system.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class AutonomousSystem extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'observable.png',
|
||||
prefix: 'autonomous-system--',
|
||||
active: false,
|
||||
relationships: [],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.number.control = 'slider';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new AutonomousSystem();
|
||||
|
||||
export default singleton;
|
||||
402
stix-modeler-app/src/definition-adapters/Base.js
Normal file
402
stix-modeler-app/src/definition-adapters/Base.js
Normal file
@@ -0,0 +1,402 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import moment from 'moment';
|
||||
|
||||
const SPEC_VERSION = 2.1;
|
||||
|
||||
const COMMON_RELS = [
|
||||
{
|
||||
type: 'includes', target: 'grouping', x_embed: 'object_refs', x_reverse: true,
|
||||
},
|
||||
{
|
||||
type: 'applies-to', target: 'note', x_embed: 'object_refs', x_reverse: true,
|
||||
},
|
||||
{
|
||||
type: 'applies-to', target: 'opinion', x_embed: 'object_refs', x_reverse: true,
|
||||
},
|
||||
{
|
||||
type: 'references', target: 'report', x_embed: 'object_refs', x_reverse: true,
|
||||
},
|
||||
{
|
||||
type: 'applies-to', target: 'marking-definition', x_embed: 'object_marking_refs', x_reverse: true,
|
||||
}
|
||||
];
|
||||
|
||||
export class Base {
|
||||
constructor(common, def) {
|
||||
const commonProps = common.properties;
|
||||
let defProps = {};
|
||||
|
||||
// Set required common properties
|
||||
common.required.map((item) => {
|
||||
if (commonProps[item]) {
|
||||
commonProps[item].required = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Get type-specific properties
|
||||
if (def.allOf) {
|
||||
def.allOf.map((item) => {
|
||||
if ('properties' in item) {
|
||||
defProps = item.properties;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
defProps = def.properties;
|
||||
}
|
||||
|
||||
// Set required type-specific properties
|
||||
if (def.required) {
|
||||
def.required.map((item) => {
|
||||
if (defProps[item]) {
|
||||
defProps[item].required = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set properties for singleton from schema
|
||||
for (const item in def) {
|
||||
this[item] = def[item];
|
||||
}
|
||||
|
||||
// Add common relationships
|
||||
for (const rel of COMMON_RELS) {
|
||||
def.relationships.push(rel);
|
||||
}
|
||||
|
||||
// Only SROs and SCOs may have this property
|
||||
if ("created_by_ref" in commonProps) {
|
||||
def.relationships.push({
|
||||
type: 'created-by', target: 'identity', x_exclusive: true, x_embed: 'created_by_ref',
|
||||
}
|
||||
)}
|
||||
|
||||
const mergedProps = deepmerge(commonProps, defProps);
|
||||
|
||||
this.handleFields(mergedProps);
|
||||
|
||||
this.properties = mergedProps;
|
||||
this.extensions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default values and control types for the specified properties
|
||||
* @param {object} mergedProps properties (common and sdo-specific)
|
||||
*/
|
||||
handleFields(mergedProps) {
|
||||
// Start special handling of common object
|
||||
// properties.
|
||||
for (const prop in mergedProps) {
|
||||
// Get (possibly nested) ref
|
||||
let ref = mergedProps[prop].$ref;
|
||||
for (const a in mergedProps[prop].allOf) {
|
||||
if (mergedProps[prop].allOf[a].$ref) {
|
||||
ref = mergedProps[prop].allOf[a].$ref;
|
||||
}
|
||||
}
|
||||
ref = ref || '';
|
||||
|
||||
if (ref.length) {
|
||||
mergedProps[prop].type = ref;
|
||||
}
|
||||
|
||||
// Set default blank values based on the prop
|
||||
// type.
|
||||
if (mergedProps[prop].type) {
|
||||
mergedProps[prop].value = this.defaultValue(mergedProps[prop]);
|
||||
}
|
||||
}
|
||||
|
||||
if (mergedProps.type) {
|
||||
mergedProps.type.control = 'literal';
|
||||
if (mergedProps.type.enum) {
|
||||
mergedProps.type.value = mergedProps.type.enum[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (mergedProps.aliases) {
|
||||
mergedProps.aliases.control = 'csv';
|
||||
}
|
||||
|
||||
if (mergedProps.kill_chain_phases) {
|
||||
mergedProps.kill_chain_phases.control = 'killchain';
|
||||
mergedProps.kill_chain_phases.vocab = [
|
||||
{
|
||||
label: 'Lockheed Kill Chain',
|
||||
value: 'lockheed-martin-cyber-kill-chain',
|
||||
phases: [
|
||||
{
|
||||
label: 'Reconnaissance',
|
||||
phase_name: 'reconnaissance',
|
||||
},
|
||||
{
|
||||
label: 'Weaponize',
|
||||
phase_name: 'weaponization',
|
||||
},
|
||||
{
|
||||
label: 'Delivery',
|
||||
phase_name: 'delivery',
|
||||
},
|
||||
{
|
||||
label: 'Exploitation',
|
||||
phase_name: 'exploitation',
|
||||
},
|
||||
{
|
||||
label: 'Installation',
|
||||
phase_name: 'installation',
|
||||
},
|
||||
{
|
||||
label: 'Command & Control (C2)',
|
||||
phase_name: 'command-and-control',
|
||||
},
|
||||
{
|
||||
label: 'Actions On Objectives',
|
||||
phase_name: 'actions-on-objectives',
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'MITRE ATT&CK',
|
||||
value: 'mitre-attack',
|
||||
phases: [
|
||||
{
|
||||
label: 'Reconnaissance',
|
||||
phase_name: 'reconnaissance',
|
||||
},
|
||||
{
|
||||
label: 'Resource Development',
|
||||
phase_name: 'resource-development',
|
||||
},
|
||||
{
|
||||
label: 'Initial Access',
|
||||
phase_name: 'initial-access',
|
||||
},
|
||||
{
|
||||
label: 'Execution',
|
||||
phase_name: 'execution',
|
||||
},
|
||||
{
|
||||
label: 'Persistence',
|
||||
phase_name: 'persistence',
|
||||
},
|
||||
{
|
||||
label: 'Privilege Escalation',
|
||||
phase_name: 'privilege-escalation'
|
||||
},
|
||||
{
|
||||
label: 'Defense Evasion',
|
||||
phase_name: 'defense-evasion',
|
||||
},
|
||||
{
|
||||
label: 'Credential Access',
|
||||
phase_name: 'credential-access',
|
||||
},
|
||||
{
|
||||
label: 'Discovery',
|
||||
phase_name: 'discovery',
|
||||
},
|
||||
{
|
||||
label: 'Lateral Movement',
|
||||
phase_name: 'lateral-movement',
|
||||
},
|
||||
{
|
||||
label: 'Collection',
|
||||
phase_name: 'collection'
|
||||
},
|
||||
{
|
||||
label: 'Command & Control (C2)',
|
||||
phase_name: 'command-and-control',
|
||||
},
|
||||
{
|
||||
label: 'Exfiltration',
|
||||
phase_name: 'exfiltration',
|
||||
},
|
||||
{
|
||||
label: 'Impact',
|
||||
phase_name: 'impact'
|
||||
}
|
||||
],
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (mergedProps.external_references) {
|
||||
mergedProps.external_references.control = 'externalrefs';
|
||||
}
|
||||
|
||||
mergedProps.id.control = 'hidden';
|
||||
|
||||
if (mergedProps.confidence) {
|
||||
mergedProps.confidence.control = 'slider';
|
||||
}
|
||||
|
||||
if (mergedProps.description) {
|
||||
mergedProps.description.control = 'textarea';
|
||||
}
|
||||
|
||||
if (mergedProps.hashes) {
|
||||
mergedProps.hashes.control = 'killchain';
|
||||
mergedProps.hashes.vocab = [
|
||||
'MD5', 'SHA-1', 'SHA-256', 'SHA-512',
|
||||
'SHA3-256', 'SHA3-512', 'SSDEEP'
|
||||
]
|
||||
mergedProps.hashes.control = 'genericobject';
|
||||
mergedProps.hashes.type = 'object';
|
||||
mergedProps.hashes.value = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* These are defaults that are to be set by the TI orchestrator
|
||||
*/
|
||||
|
||||
mergedProps.spec_version.value = SPEC_VERSION;
|
||||
mergedProps.spec_version.control = 'literal';
|
||||
|
||||
if (mergedProps.extensions) {
|
||||
mergedProps.extensions.control = 'genericobject';
|
||||
mergedProps.extensions.type = 'object';
|
||||
mergedProps.extensions.value = {};
|
||||
mergedProps.extensions.control = 'hidden';
|
||||
}
|
||||
|
||||
|
||||
if (mergedProps.lang) {
|
||||
mergedProps.lang.value = 'en';
|
||||
mergedProps.lang.control = 'hidden';
|
||||
}
|
||||
|
||||
mergedProps.object_marking_refs.control = 'hidden';
|
||||
mergedProps.granular_markings.control = 'hidden';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default empty value for the specified
|
||||
* property's type
|
||||
* @param {object} def property
|
||||
* @returns default value for property
|
||||
*/
|
||||
defaultValue(def) {
|
||||
let type = def.type;
|
||||
let value;
|
||||
|
||||
type = type.split('/').slice(-1)[0];
|
||||
type = type.split('.json')[0];
|
||||
def.type = type;
|
||||
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
value = false;
|
||||
break;
|
||||
case 'dictionary':
|
||||
case 'external-reference':
|
||||
case 'hashes':
|
||||
case 'hashes-type':
|
||||
case 'object':
|
||||
case 'observable-container':
|
||||
value = {};
|
||||
def.type = 'object';
|
||||
break;
|
||||
case 'float':
|
||||
case 'integer':
|
||||
case 'number':
|
||||
value = 0;
|
||||
def.type = 'number';
|
||||
break;
|
||||
case 'binary':
|
||||
case 'hex':
|
||||
case 'identifier':
|
||||
case 'open-vocab':
|
||||
case 'string':
|
||||
case 'url-regex':
|
||||
value = "";
|
||||
def.type = 'string';
|
||||
break;
|
||||
case 'array':
|
||||
case 'list':
|
||||
def.type = 'array';
|
||||
case 'enum':
|
||||
case 'kill-chain-phase':
|
||||
value = [];
|
||||
break;
|
||||
case 'timestamp':
|
||||
value = moment().utc(true).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten nested extension properties
|
||||
* (Enables resiliency for non-standard schema formats)
|
||||
* @param {*} schema
|
||||
* @returns dictionary of properties
|
||||
*/
|
||||
flattenExtensionProperties(schema) {
|
||||
let extProps = {}
|
||||
let properties;
|
||||
if (schema.allOf) {
|
||||
schema.allOf.map((item) => {
|
||||
if ('properties' in item) {
|
||||
properties = item.properties
|
||||
}
|
||||
});
|
||||
} else {
|
||||
properties = schema.properties;
|
||||
}
|
||||
|
||||
// In case of nested property extensions
|
||||
let found = true;
|
||||
while (found) {
|
||||
found = false;
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
if (key == 'properties' || key == 'extensions' ||
|
||||
key.includes("extension-definition")) {
|
||||
properties = value;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [prop, def] of Object.entries(properties)) {
|
||||
if (prop != 'extension_type') {
|
||||
const value = this.defaultValue(def);
|
||||
extProps[prop] = def;
|
||||
def.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
return extProps;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Merge properties from a property-extension schema into the
|
||||
* SDO singleton
|
||||
* @param {*} def schema of extended properties
|
||||
* @param {*} extDef extension definition node
|
||||
*/
|
||||
mergeExtension(def, extDef) {
|
||||
const { properties, } = this;
|
||||
|
||||
if (!('extensions' in properties)) {
|
||||
this.properties.extensions = {};
|
||||
this.properties.extensions.value = {};
|
||||
}
|
||||
|
||||
// If this object uses extensions (such as network-traffic),
|
||||
// do not allow further editing via gui
|
||||
this.properties.extensions.type = 'object';
|
||||
this.properties.extensions.control = 'hidden';
|
||||
|
||||
const extProps = this.flattenExtensionProperties(def);
|
||||
// Only toplevel-property-extensions can have extension_properties field,
|
||||
// but this useful, so hold onto it
|
||||
extDef.extension_properties = Object.keys(extProps);
|
||||
const mergedProps = deepmerge(properties, extProps);
|
||||
this.properties = mergedProps;
|
||||
this.extensions.push(extDef.uiid);
|
||||
}
|
||||
}
|
||||
36
stix-modeler-app/src/definition-adapters/Campaign.js
Normal file
36
stix-modeler-app/src/definition-adapters/Campaign.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/common.json';
|
||||
import rawDefinition from '../definitions/campaign.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class Campaign extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'campaign.png',
|
||||
prefix: 'campaign--',
|
||||
active: true,
|
||||
relationships: [
|
||||
{ type: 'attributed-to', target: 'intrusion-set', },
|
||||
{ type: 'attributed-to', target: 'threat-actor', },
|
||||
{ type: 'targets', target: 'identity', },
|
||||
{ type: 'targets', target: 'vulnerability', },
|
||||
{ type: 'uses', target: 'attack-pattern', },
|
||||
{ type: 'uses', target: 'malware', },
|
||||
{ type: 'uses', target: 'tool', },
|
||||
{ type: 'compromises', target: 'infrastructure', },
|
||||
{ type: 'uses', target: 'infrastructure', },
|
||||
{ type: 'originates-from', target: 'location', x_exclusive: true, },
|
||||
{ type: 'targets', target: 'location', }
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new Campaign();
|
||||
|
||||
export default singleton;
|
||||
26
stix-modeler-app/src/definition-adapters/Certificate.js
Normal file
26
stix-modeler-app/src/definition-adapters/Certificate.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/observable-common.json';
|
||||
import rawDefinition from '../definitions/x509-certificate.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class Certificate extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'observable.png',
|
||||
prefix: 'x509-certificate--',
|
||||
active: false,
|
||||
relationships: [],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.x509_v3_extensions.type = 'string';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new Certificate();
|
||||
|
||||
export default singleton;
|
||||
31
stix-modeler-app/src/definition-adapters/CourseOfAction.js
Normal file
31
stix-modeler-app/src/definition-adapters/CourseOfAction.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/common.json';
|
||||
import rawDefinition from '../definitions/course-of-action.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class CourseOfAction extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'course-of-action.png',
|
||||
prefix: 'course-of-action--',
|
||||
active: true,
|
||||
relationships: [
|
||||
{ type: 'mitigates', target: 'attack-pattern', },
|
||||
{ type: 'mitigates', target: 'vulnerability', },
|
||||
{ type: 'mitigates', target: 'malware', },
|
||||
{ type: 'mitigates', target: 'tool', },
|
||||
{ type: 'investigates', target: 'indicator', },
|
||||
{ type: 'mitigates', target: 'indicator', }
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new CourseOfAction();
|
||||
|
||||
export default singleton;
|
||||
33
stix-modeler-app/src/definition-adapters/Custom.js
Normal file
33
stix-modeler-app/src/definition-adapters/Custom.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/common.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class Custom extends Base {
|
||||
constructor(rawDefinition, extensionDefinition) {
|
||||
let prefix = `${rawDefinition.title}--`;
|
||||
rawDefinition.allOf.map((item) => {
|
||||
if ('properties' in item) {
|
||||
if (item.properties.id.pattern) {
|
||||
prefix = `${rawDefinition.allOf[1].properties.id.pattern.substring(1)}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const definition_extension = {
|
||||
img: 'custom.png',
|
||||
prefix,
|
||||
active: true,
|
||||
relationships: [],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
super(common, def);
|
||||
|
||||
this.extensions.push(extensionDefinition.uiid);
|
||||
}
|
||||
}
|
||||
|
||||
const factory = (rawDefinition, extensionDefinition) => new Custom(rawDefinition, extensionDefinition);
|
||||
|
||||
export default factory;
|
||||
30
stix-modeler-app/src/definition-adapters/Directory.js
Normal file
30
stix-modeler-app/src/definition-adapters/Directory.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/observable-common.json';
|
||||
import rawDefinition from '../definitions/directory.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class Directory extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'observable.png',
|
||||
prefix: 'directory--',
|
||||
active: false,
|
||||
relationships: [
|
||||
{
|
||||
type: 'contains', target: 'observable', 'sub-target': 'directory', x_embed: 'contains_refs',
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.contains_refs.control = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new Directory();
|
||||
|
||||
export default singleton;
|
||||
26
stix-modeler-app/src/definition-adapters/DomainName.js
Normal file
26
stix-modeler-app/src/definition-adapters/DomainName.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/observable-common.json';
|
||||
import rawDefinition from '../definitions/domain-name.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class DomainName extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'observable.png',
|
||||
prefix: 'domain-name--',
|
||||
active: false,
|
||||
relationships: [],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.resolves_to_refs.control = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new DomainName();
|
||||
|
||||
export default singleton;
|
||||
31
stix-modeler-app/src/definition-adapters/EmailAddr.js
Normal file
31
stix-modeler-app/src/definition-adapters/EmailAddr.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/observable-common.json';
|
||||
import rawDefinition from '../definitions/email-addr.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class EmailAddr extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'observable.png',
|
||||
prefix: 'email-addr--',
|
||||
active: false,
|
||||
relationships: [
|
||||
{
|
||||
type: 'addr-belongs-to', target: 'observable', 'sub-target': 'user-account', x_embed: 'belongs_to_ref',
|
||||
},
|
||||
{ type: 'addr-belongs-to', target: 'user-account', x_embed: 'belongs_to_ref', }
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.belongs_to_ref.control = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new EmailAddr();
|
||||
|
||||
export default singleton;
|
||||
60
stix-modeler-app/src/definition-adapters/EmailMessage.js
Normal file
60
stix-modeler-app/src/definition-adapters/EmailMessage.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/observable-common.json';
|
||||
import rawDefinition from '../definitions/email-message.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class EmailMessage extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'observable.png',
|
||||
prefix: 'email-message--',
|
||||
active: false,
|
||||
relationships: [
|
||||
{
|
||||
type: 'from', target: 'observable', 'sub-target': 'email-addr', x_embed: 'from_ref',
|
||||
},
|
||||
{ type: 'from', target: 'email-addr', x_embed: 'from_ref', },
|
||||
{
|
||||
type: 'to', target: 'observable', 'sub-target': 'email-addr', x_embed: 'to_refs',
|
||||
},
|
||||
{ type: 'to', target: 'email-addr', x_embed: 'to_refs', },
|
||||
{
|
||||
type: 'cc', target: 'observable', 'sub-target': 'email-addr', x_embed: 'cc_refs',
|
||||
},
|
||||
{ type: 'cc', target: 'email-addr', x_embed: 'cc_refs', },
|
||||
{
|
||||
type: 'bcc', target: 'observable', 'sub-target': 'email-addr', x_embed: 'bcc_refs',
|
||||
},
|
||||
{ type: 'bcc', target: 'email-addr', x_embed: 'bcc_refs', },
|
||||
{
|
||||
type: 'sender', target: 'observable', 'sub-target': 'email-addr', x_embed: 'sender_ref',
|
||||
},
|
||||
{ type: 'sender', target: 'email-addr', x_embed: 'sender_ref', },
|
||||
{
|
||||
type: 'raw', target: 'observable', 'sub-target': 'artifact', x_embed: 'raw_email_ref',
|
||||
},
|
||||
{ type: 'raw', target: 'artifact', x_embed: 'raw_email_ref', }
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.date.type = 'timestamp';
|
||||
this.properties.additional_header_fields.value = {};
|
||||
|
||||
this.properties.from_ref.control = 'hidden';
|
||||
this.properties.sender_ref.control = 'hidden';
|
||||
this.properties.to_refs.control = 'hidden';
|
||||
this.properties.cc_refs.control = 'hidden';
|
||||
this.properties.bcc_refs.control = 'hidden';
|
||||
this.properties.raw_email_ref.control = 'hidden';
|
||||
this.properties.additional_header_fields.control = 'genericobject';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new EmailMessage();
|
||||
|
||||
export default singleton;
|
||||
@@ -0,0 +1,30 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/common.json';
|
||||
import rawDefinition from '../definitions/extension-definition.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class ExtensionDefinition extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: '',
|
||||
prefix: 'extension-definition--',
|
||||
active: false,
|
||||
relationships: [],
|
||||
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
// Hoist vocabs onto properties
|
||||
this.properties.extension_types.vocab = this.definitions['extension-type-enum'].enum;
|
||||
this.properties.extension_types.control = 'hidden';
|
||||
this.properties.extension_properties.control = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new ExtensionDefinition();
|
||||
|
||||
export default singleton;
|
||||
146
stix-modeler-app/src/definition-adapters/File.js
Normal file
146
stix-modeler-app/src/definition-adapters/File.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/observable-common.json';
|
||||
import rawDefinition from '../definitions/file.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class File extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'observable.png',
|
||||
prefix: 'file--',
|
||||
active: false,
|
||||
relationships: [
|
||||
{
|
||||
type: 'content',
|
||||
target: 'observable',
|
||||
'sub-target': 'artifact',
|
||||
x_embed: 'content_ref',
|
||||
},
|
||||
{ type: 'content', target: 'artifact', x_embed: 'content_ref', },
|
||||
{
|
||||
type: 'parent-directory',
|
||||
target: 'observable',
|
||||
'sub-target': 'directory',
|
||||
x_embed: 'parent_directory_ref',
|
||||
},
|
||||
{
|
||||
type: 'parent-directory',
|
||||
target: 'directory',
|
||||
x_embed: 'parent_directory_ref',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'artifact',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'directory',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'domain-name',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'ipv4-addr',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'ipv6-addr',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'email-message',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'email-addr',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'file',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'mac-addr',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'mutex',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'url',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'user-account',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'observable',
|
||||
'sub-target': 'windows-registry-key',
|
||||
x_embed: 'contains_refs',
|
||||
},
|
||||
{ type: 'contains', target: 'artifact', x_embed: 'contains_refs', },
|
||||
{ type: 'contains', target: 'directory', x_embed: 'contains_refs', },
|
||||
{ type: 'contains', target: 'domain-name', x_embed: 'contains_refs', },
|
||||
{ type: 'contains', target: 'ipv4-addr', x_embed: 'contains_refs', },
|
||||
{ type: 'contains', target: 'ipv6-addr', x_embed: 'contains_refs', },
|
||||
{ type: 'contains', target: 'email-message', x_embed: 'contains_refs', },
|
||||
{ type: 'contains', target: 'email-addr', x_embed: 'contains_refs', },
|
||||
{ type: 'contains', target: 'file', x_embed: 'contains_refs', },
|
||||
{ type: 'contains', target: 'mac-addr', x_embed: 'contains_refs', },
|
||||
{ type: 'contains', target: 'mutex', x_embed: 'contains_refs', },
|
||||
{ type: 'contains', target: 'url', x_embed: 'contains_refs', },
|
||||
{ type: 'contains', target: 'user-account', x_embed: 'contains_refs', },
|
||||
{
|
||||
type: 'contains',
|
||||
target: 'windows-registry-key',
|
||||
x_embed: 'contains_refs',
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
this.properties.hashes.value = {};
|
||||
|
||||
this.properties.content_ref.control = 'hidden';
|
||||
this.properties.parent_directory_ref.control = 'hidden';
|
||||
this.properties.contains_refs.control = 'hidden';
|
||||
|
||||
this.properties.magic_number_hex.type = 'string';
|
||||
this.properties.size.type = 'string';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new File();
|
||||
|
||||
export default singleton;
|
||||
27
stix-modeler-app/src/definition-adapters/Grouping.js
Normal file
27
stix-modeler-app/src/definition-adapters/Grouping.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/common.json';
|
||||
import rawDefinition from '../definitions/grouping.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class Grouping extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'grouping.png',
|
||||
prefix: 'grouping--',
|
||||
active: true,
|
||||
relationships: [],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.context.vocab = this.definitions['grouping-context-ov'].enum;
|
||||
this.properties.object_refs.control = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new Grouping();
|
||||
|
||||
export default singleton;
|
||||
40
stix-modeler-app/src/definition-adapters/IPv4Addr.js
Normal file
40
stix-modeler-app/src/definition-adapters/IPv4Addr.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/observable-common.json';
|
||||
import rawDefinition from '../definitions/ipv4-addr.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class IPv4Addr extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'observable.png',
|
||||
prefix: 'ipv4-addr--',
|
||||
active: false,
|
||||
relationships: [
|
||||
{ type: 'belongs-to', target: 'autonomous-system', x_embed: 'belongs_to_refs', },
|
||||
{
|
||||
type: 'belongs-to', target: 'observable', 'sub-target': 'autonomous-system', x_embed: 'belongs_to_refs',
|
||||
},
|
||||
{ type: 'resolves-to', target: 'mac-addr', x_embed: 'resolves_to_refs', },
|
||||
{
|
||||
type: 'resolves-to', target: 'observable', 'sub-target': 'mac-addr', x_embed: 'resolves_to_refs',
|
||||
},
|
||||
{ type: 'resolves-to', target: 'domain-name', x_embed: 'resolves_to_refs', },
|
||||
{
|
||||
type: 'resolves-to', target: 'observable', 'sub-target': 'domain-name', x_embed: 'resolves_to_refs',
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.resolves_to_refs.control = 'hidden';
|
||||
this.properties.belongs_to_refs.control = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new IPv4Addr();
|
||||
|
||||
export default singleton;
|
||||
36
stix-modeler-app/src/definition-adapters/IPv6Addr.js
Normal file
36
stix-modeler-app/src/definition-adapters/IPv6Addr.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/observable-common.json';
|
||||
import rawDefinition from '../definitions/ipv6-addr.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class IPv6Addr extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'observable.png',
|
||||
prefix: 'ipv6-addr--',
|
||||
active: false,
|
||||
relationships: [
|
||||
{ type: 'belongs-to', target: 'autonomous-system', x_embed: 'belongs_to_refs', },
|
||||
{
|
||||
type: 'belongs-to', target: 'observable', 'sub-target': 'autonomous-system', x_embed: 'belongs_to_refs',
|
||||
},
|
||||
{ type: 'resolves-to', target: 'mac-addr', x_embed: 'resolves_to_refs', },
|
||||
{
|
||||
type: 'resolves-to', target: 'observable', 'sub-target': 'mac-addr', x_embed: 'resolves_to_refs',
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.resolves_to_refs.control = 'hidden';
|
||||
this.properties.belongs_to_refs.control = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new IPv6Addr();
|
||||
|
||||
export default singleton;
|
||||
32
stix-modeler-app/src/definition-adapters/Identity.js
Normal file
32
stix-modeler-app/src/definition-adapters/Identity.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/common.json';
|
||||
import rawDefinition from '../definitions/identity.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class Identity extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'identity.png',
|
||||
prefix: 'identity--',
|
||||
active: true,
|
||||
relationships: [
|
||||
{ type: 'located-at', target: 'location', }
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.identity_class.vocab = this.definitions['identity-class-ov'].enum;
|
||||
this.properties.identity_class.control = 'stringselector';
|
||||
this.properties.sectors.vocab = this.definitions['industry-sector-ov'].enum;
|
||||
|
||||
this.properties.roles.control = 'csv';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new Identity();
|
||||
|
||||
export default singleton;
|
||||
40
stix-modeler-app/src/definition-adapters/Indicator.js
Normal file
40
stix-modeler-app/src/definition-adapters/Indicator.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/common.json';
|
||||
import rawDefinition from '../definitions/indicator.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class Indicator extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'indicator.png',
|
||||
prefix: 'indicator--',
|
||||
active: true,
|
||||
relationships: [
|
||||
{ type: 'indicates', target: 'attack-pattern', },
|
||||
{ type: 'indicates', target: 'campaign', },
|
||||
{ type: 'indicates', target: 'intrusion-set', },
|
||||
{ type: 'indicates', target: 'malware', },
|
||||
{ type: 'indicates', target: 'threat-actor', },
|
||||
{ type: 'indicates', target: 'tool', },
|
||||
{ type: 'indicates', target: 'infrastructure', },
|
||||
{ type: 'based-on', target: 'observed-data', }
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
// Hoist vocabs onto properties
|
||||
this.properties.indicator_types.vocab = this.definitions['indicator-type-ov'].enum;
|
||||
this.properties.pattern_type.vocab = this.definitions['pattern-type-ov'].enum;
|
||||
this.properties.pattern_type.control = 'stringselector';
|
||||
|
||||
this.properties.pattern.control = 'confirmtextarea';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new Indicator();
|
||||
|
||||
export default singleton;
|
||||
36
stix-modeler-app/src/definition-adapters/Infrastructure.js
Normal file
36
stix-modeler-app/src/definition-adapters/Infrastructure.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/common.json';
|
||||
import rawDefinition from '../definitions/infrastructure.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class Infrastructure extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'infrastructure.png',
|
||||
prefix: 'infrastructure--',
|
||||
active: true,
|
||||
relationships: [
|
||||
{ type: 'communicates-with', target: 'infrastructure', },
|
||||
{ type: 'consists-of', target: 'infrastructure', },
|
||||
{ type: 'controls', target: 'infrastructure', },
|
||||
{ type: 'uses', target: 'infrastructure', },
|
||||
{ type: 'delivers', target: 'malware', },
|
||||
{ type: 'has', target: 'vulnerability', },
|
||||
{ type: 'hosts', target: 'tool', },
|
||||
{ type: 'hosts', target: 'malware', },
|
||||
{ type: 'located-at', target: 'location', }
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.infrastructure_types.vocab = this.definitions['infrastructure-type-ov'].enum;
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new Infrastructure();
|
||||
|
||||
export default singleton;
|
||||
45
stix-modeler-app/src/definition-adapters/IntrusionSet.js
Normal file
45
stix-modeler-app/src/definition-adapters/IntrusionSet.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import _cloneDeep from 'lodash/cloneDeep';
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/common.json';
|
||||
import rawDefinition from '../definitions/intrusion-set.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class IntrusionSet extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'intrusion-set.png',
|
||||
prefix: 'intrusion-set--',
|
||||
active: true,
|
||||
relationships: [
|
||||
{ type: 'attributed-to', target: 'threat-actor', },
|
||||
{ type: 'targets', target: 'vulnerability', },
|
||||
{ type: 'targets', target: 'identity', },
|
||||
{ type: 'uses', target: 'tool', },
|
||||
{ type: 'uses', target: 'attack-pattern', },
|
||||
{ type: 'uses', target: 'malware', },
|
||||
{ type: 'compromises', target: 'infrastructure', },
|
||||
{ type: 'hosts', target: 'infrastructure', },
|
||||
{ type: 'owns', target: 'infrastructure', },
|
||||
{ type: 'uses', target: 'infrastructure', },
|
||||
{ type: 'originates-from', target: 'location', x_exclusive: true, },
|
||||
{ type: 'targets', target: 'location', }
|
||||
],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
|
||||
this.properties.goals.control = 'csv';
|
||||
this.properties.primary_motivation.vocab = _cloneDeep(this.definitions['attack-motivation-ov'].enum);
|
||||
this.properties.primary_motivation.control = 'stringselector';
|
||||
this.properties.secondary_motivations.vocab = this.definitions['attack-motivation-ov'].enum;
|
||||
this.properties.resource_level.vocab = _cloneDeep(this.definitions['attack-resource-level-ov'].enum);
|
||||
this.properties.resource_level.control = 'stringselector';
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new IntrusionSet();
|
||||
|
||||
export default singleton;
|
||||
24
stix-modeler-app/src/definition-adapters/Location.js
Normal file
24
stix-modeler-app/src/definition-adapters/Location.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import deepmerge from 'deepmerge';
|
||||
import common from '../definitions/common.json';
|
||||
import rawDefinition from '../definitions/location.json';
|
||||
|
||||
import { Base } from './Base';
|
||||
|
||||
class Location extends Base {
|
||||
constructor() {
|
||||
const definition_extension = {
|
||||
img: 'location.png',
|
||||
prefix: 'location--',
|
||||
active: true,
|
||||
relationships: [],
|
||||
};
|
||||
|
||||
const def = deepmerge(definition_extension, rawDefinition);
|
||||
|
||||
super(common, def);
|
||||
}
|
||||
}
|
||||
|
||||
const singleton = new Location();
|
||||
|
||||
export default singleton;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user