Validation and schema management improvements

This commit is contained in:
riplehk1
2025-10-14 09:45:57 -04:00
parent 489007068d
commit d114531327
235 changed files with 11139 additions and 8397 deletions

View 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
View 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?

View File

@@ -0,0 +1,7 @@
{
"creator_id": "identity--b085a68a-bf48-4316-9667-37af78cba894",
"schema_dir": "/schemas",
"schemas": [
]
}

View 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
},
},
}
);

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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

View 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```

View 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;

View 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;
}

File diff suppressed because it is too large Load Diff

View 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));

View 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));

View 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;

View 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%;
}

View 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}
</>
);
}

View 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>
</>
);
}
}

View 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;
}

View 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));

View 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;
}

View 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));

View 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;
}
}

View 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));

View 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;
}
}

View File

@@ -0,0 +1,7 @@
.canvas {
position: fixed;
top: 50px;
left: 50px;
bottom: 50px;
right: 50px;
}

View 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;
}
}

View 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));

View 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
}
}
}
}

View 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));

View 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;
}
}
}
}

View 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));

View 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));

View 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;
}
}

View File

@@ -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));

View File

@@ -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;
}
}

View File

@@ -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));

View File

@@ -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));

View File

@@ -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;
}
}
}
}

View 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));

View 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));

View 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));

View 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));

View 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));

View 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,
};

View 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;
}

View File

@@ -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);

View File

@@ -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>
);
}

View 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>
);
}

View 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);

View 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>
);
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -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;
}
}
}
}
}

View 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;
}
}
}
}

View 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));

View 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;
}

View 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);

View 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);

View 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);

View 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} />
);
}
}

View 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);

View 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,
};

View File

@@ -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;

View 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;

View 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);

View 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,
};

View 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);

View 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%;
}
}
}
}
}

View 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;
}
}

View 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;
}

View 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;
}

View File

@@ -0,0 +1,5 @@
@use '../../../defaults';
.rc-slider-track {
background-color: transparet;
}

View 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;
}

View 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));

View 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;
}

View 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;

View 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;

View 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;

View 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;

View 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);
}
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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