Merge pull request #2 from JHUAPL/validation-improvements

Validation and schema management improvements
This commit is contained in:
Charles Frick
2025-10-14 09:53:25 -04:00
committed by GitHub
235 changed files with 11139 additions and 8397 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
# Node_Moludes
/stix-modeler-app/node_modules
# Distribution
/stix-modeler-app/dist/
# Schemas
/stix-modeler-app/schemas/*.json

View File

@@ -3,10 +3,7 @@
This is a fork of the [STIX Modeler](https://github.com/STIX-Modeler/UI/tree/develop), originally created by Jason Minnick
## Overview
A React-based user interface tool for visualizing, creating, and modifying STIX 2.1 bundles.
This material is based upon work supported by the U.S. Department of Homeland Security / Cybersecurity and Infrastructure Security Agency. Any views and conclusions contained on this page are those of the authors and should not be interpreted as necessarily representing the official policies, either
expressed or implied, of the U.S. Department of Homeland Security / Cybersecurity and Infrastructure Security Agency.
A React-based user interface tool for visualizing, creating, and modifying STIX 2.1 bundles
## New Features
- Define custom STIX Domain Objects (SDO) using schemas
@@ -19,7 +16,38 @@ expressed or implied, of the U.S. Department of Homeland Security / Cybersecurit
This modeler was developed in and optimized for use with node v20.11.1 and npm 10.2.4
Earlier versions of node may not be supported
Other versions of node may not be supported
All listed third-party dependencies grant use, modification, and distribution rights under the MIT License.
## Third-Party Dependencies
- "@vitejs/plugin-react": "5.0.4",
- "classnames": "2.5.1",
- "d3-hierarchy": "3.1.2",
- "deepmerge": "4.3.1",
- "lodash": "4.17.21",
- "mobx": "6.15.0",
- "mobx-react": "9.2.1",
- "moment": "2.30.1",
- "prop-types": "15.8.1",
- "rc-slider": "11.1.9",
- "react": "19.2.0",
- "react-datepicker": "8.7.0",
- "react-dom": "19.2.0",
- "react-tooltip": "5.29.1",
- "reactflow": "11.11.4",
- "sass": "1.93.2",
- "uuid": "13.0.0",
- "vite": "7.1.9"
## Third-Party Development Dependencies
- "@eslint/js": "9.37.0",
- "eslint": "9.37.0",
- "globals": "16.4.0",
- "jsdom": "27.0.0",
- "typescript-eslint": "8.46.0",
- "vitest": "3.2.4"
# Installation and Use
@@ -43,6 +71,13 @@ Earlier versions of node may not be supported
- Added functionality for creating new Group SDOs via clicking and selecting SDOs
- Updated dependencies and removed unused dependencies
- Upgraded handling of default field and relationship values
- Added bundle validation for required SDO properties
- Added bundle file export
- Added vitest testing infrastructure
- Added unknown object and property handling
- Added automatic schema loading
- Added UI configuration via config file
- Updated STIX schemas to latest versions
## Bug Fixes
@@ -51,6 +86,10 @@ Earlier versions of node may not be supported
- Fixed implied fields based on relationships between nodes (e.g. "created_by")
- Fixed import and modification of nodes with "hashes" fields
- Added ability to delete external_reference objects from external_references fields
- Fixed inclusion of invalid fields in relationship objects
- Fixed extension definition inconsistency
## Definitions
@@ -88,8 +127,6 @@ Specific vocab notes
- labels: there are placeholder values located in definition-adapters/Base.js. This can easily be updated to reflect your sharing group or company's standard list for each object or even hidden with the `control` property.
# Quality Assurance
## Style Guide
The source code follows a modification of the [Airbnb Javascript Style Guide](https://airbnb.io/javascript/react/)
## Automated Tools
The project uses eslint for quality assurance and styling.
- See current code quality issues: `npm run lint`

BIN
STIX-Modeler_User_Guide.pdf Normal file

Binary file not shown.

5515
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +0,0 @@
{
"name": "stix-modeler-app",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
"preview": "vite preview"
},
"dependencies": {
"classnames": "^2.5.1",
"deepmerge": "^4.3.1",
"lodash": "^4.17.21",
"mobx": "^6.12.0",
"mobx-react": "^9.1.0",
"moment": "^2.30.1",
"prop-types": "^15.8.1",
"rc-slider": "^10.5.0",
"react": "^18.2.0",
"react-datepicker": "^6.1.0",
"react-dom": "^18.2.0",
"react-tooltip": "^5.26.3",
"reactflow": "^11.10.4",
"sass": "^1.71.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"vite": "^5.2.6"
}
}

View File

@@ -1,7 +0,0 @@
@import '../../defaults';
.nodeLabel {
overflow: visible;
position: relative;
bottom: -45px;
}

View File

@@ -1,22 +0,0 @@
@import "../defaults";
.submission-error {
.header {
padding: 10px;
font-size: 18px;
img {
vertical-align: middle;
padding-right: 10px;
}
}
.row {
padding-left: 55px;
span {
color: $default-active-bg;
padding-right: 6px;
}
}
}

View File

@@ -1,133 +0,0 @@
@import '../../defaults';
.top-menu {
position: fixed;
top: 20px;
right: 20px;
.row {
display: flex;
flex-direction: row;
.menu-item-small {
width: 20px;
height: 20px;
}
.menu-item-medium {
width: 25px;
height: 20px;
}
.schema-paste-btn {
border-radius: 5px;
cursor: pointer;
padding: 5px 4px 5px 11px;
margin-left: 10px;
color: #fff;
font-weight: bold;
background-color: $default-active-bg;
}
.grouping-btn {
border-radius: 5px;
cursor: pointer;
padding: 5px 11px 5px 11px;
color: #fff;
font-weight: bold;
background-color: $default-active-bg;
width: 55px;
height: 80%;
}
.dropdown {
position: relative;
display: inline-block;
margin-left: 10px;
}
.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: $error-font;
}
.sdos-btn {
border-radius: 5px;
cursor: pointer;
padding: 5px 11px 5px 11px;
color: #fff;
font-weight: bold;
background-color: $default-active-bg;
margin-left: 10px;
.i {
width: 20px;
vertical-align: middle;
font-size: 16px;
}
}
.ctr-input {
padding-right: 10px;
width: 500px;
}
.json-btn {
border-radius: 5px;
cursor: pointer;
padding: 5px 4px 5px 8px;
margin-left: 10px;
color: #fff;
font-weight: bold;
background-color: $default-active-bg;
}
.json-paste-btn {
border-radius: 5px;
cursor: pointer;
padding: 5px 4px 5px 8px;
margin-left: 5px;
color: #fff;
font-weight: bold;
background-color: $default-active-bg;
}
.reset-btn {
border-radius: 5px;
cursor: pointer;
padding: 5px 11px 5px 11px;
color: #fff;
font-weight: bold;
background-color: $default-active-bg;
margin-left: 10px;
.i {
width: 20px;
vertical-align: middle;
font-size: 16px;
}
}
}
}

View File

@@ -1,139 +0,0 @@
@import '../../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: $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: $default-active-bg;
border-radius: 5px;
padding-top: 7px;
color: $light-font-0;
cursor: pointer;
font-size: 14px;
span {
padding-left: 1px;
}
.text {
padding-top: 3px;
}
}
.delete:hover {
background-color: $error-font;
}
}
.body {
overflow-y: scroll;
overflow-x: hidden;
flex: 1;
.preview {
padding-left: 20px;
padding-top: 10px;
cursor: pointer;
font-weight: bold;
font-family: $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: $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: $default-active-bg;
margin-left: 10px;
margin-top: 10px;
width: fit-content;
.i {
width: 20px;
vertical-align: middle;
font-size: 16px;
}
}
.item {
padding-left: 20px;
padding-top: 15px;
font-weight: normal;
.horizontal-slider {
width: 98%;
}
.item-header {
font-weight: bold;
color: $default-active-bg;
padding-bottom: 3px;
span {
padding-left: 3px;
vertical-align: middle;
font-size: 14px;
cursor: pointer;
}
}
}
.slider {
padding-bottom: 20px;
}
}
.footer {
height: 40px;
}
}

View File

@@ -1,90 +0,0 @@
import React from 'react';
import { observer } from 'mobx-react';
import Panel from '../ui/panel/Panel';
import Images from '../../imgs/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) => {
let src = relationship.source_ref.split('--')[0];
let target = relationship.target_ref.split('--')[0];
if (relationship.subTarget) {
target = relationship.subTarget;
}
if (relationship.x_reverse) {
const tmp = src;
src = target;
target = tmp;
}
const srcImg = Images.getImage(`${src}.png`);
const targetImg = Images.getImage(`${target}.png`);
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

@@ -1,12 +0,0 @@
@import "../../../defaults";
.growl {
position: fixed;
right: 10px;
top: 10px;
z-index: $growl-index;
background-color: $default-active-bg;
color: $light-font-0;
padding: 11px;
border-radius: 5px;
}

View File

@@ -1,288 +0,0 @@
import deepmerge from 'deepmerge';
import moment from 'moment';
const SPEC_VERSION = 2.1;
const COMMON_RELS = [
{
type: 'created-by', target: 'identity', x_exclusive: true, x_embed: 'created_by_ref',
},
{
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 = {};
common.required.map((item) => {
if (commonProps[item]) {
commonProps[item].required = true;
}
});
if (def.allOf) {
def.allOf.map((item) => {
if (item.hasOwnProperty('properties')) {
defProps = item.properties;
}
});
} else {
defProps = def.properties;
}
if (def.required) {
def.required.map((item) => {
if (defProps[item]) {
defProps[item].required = true;
}
});
}
for (const item in def) {
this[item] = def[item];
}
for (const rel of COMMON_RELS) {
def.relationships.push(rel);
}
const mergedProps = deepmerge(commonProps, defProps);
this.handleFields(mergedProps);
this.properties = mergedProps;
}
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 || '';
// set type for values with ref
if (ref.indexOf('timestamp.json') !== -1) {
mergedProps[prop].type = 'dts';
} else if (ref.indexOf('identifier.json') !== -1) {
mergedProps[prop].type = 'string';
} else if (ref.indexOf('dictionary.json') !== -1) {
mergedProps[prop].type = 'object';
}
// Set default blank values based on the prop
// type.
if (mergedProps[prop].type) {
mergedProps[prop].value = this.defaultValue(mergedProps[prop].type);
}
}
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',
}
],
}
];
}
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';
}
/**
* 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 = {};
}
if (mergedProps.created_by_ref) {
mergedProps.created_by_ref.type = 'literal';
}
if (mergedProps.lang) {
mergedProps.lang.value = 'en';
mergedProps.lang.control = 'hidden';
}
mergedProps.object_marking_refs.control = 'hidden';
mergedProps.granular_markings.control = 'hidden';
}
defaultValue(type) {
let def;
// ignores type path
if (type.includes('.json')) {
type = type.split('/').slice(-1)[0];
}
switch (type) {
case 'string':
def = '';
break;
case 'dts':
def = moment().utc(true).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
break;
case 'integer':
def = 0;
break;
case 'array':
def = [];
break;
case 'object':
def = {};
break;
case 'boolean':
def = false;
break;
}
return def;
}
flattenExtensionProperties(def) {
const properties = {};
let tmp = {};
if ('properties' in def) {
tmp = this.flattenExtensionProperties(def.properties);
for (const [key, value] of Object.entries(tmp)) {
properties[key] = value;
}
} else if ('extensions' in def) {
tmp = this.flattenExtensionProperties(def.extensions);
for (const [key, value] of Object.entries(tmp)) {
properties[key] = value;
}
} else {
for (const [key, value] of Object.entries(def)) {
if (key.includes('extension-definition')) {
tmp = this.flattenExtensionProperties(value);
for (const [key, value] of Object.entries(tmp)) {
properties[key] = value;
}
} else {
properties[key] = value;
}
}
}
return properties;
}
mergeExtension(def, extDef) {
const { properties, } = this;
let defProps;
if (def.allOf) {
def.allOf.map((item) => {
if ('properties' in item) {
defProps = this.flattenExtensionProperties(item.properties);
}
});
} else {
defProps = this.flattenExtensionProperties(def.properties);
}
if ('extension_type' in defProps) {
delete defProps.extension_type;
}
if (!('extensions' in properties)) {
this.properties.extensions = {};
this.properties.extensions.value = {};
this.properties.extensions.type = 'object';
this.properties.extensions.control = 'hidden';
}
const props = { extension_type: 'property-extension', };
for (const prop in defProps) {
if ((prop !== 'extension_type') && (defProps[prop].type)) {
const value = this.defaultValue(defProps[prop].type);
props[prop] = value;
defProps[prop].value = value;
}
}
const mergedProps = deepmerge(properties, defProps);
this.properties = mergedProps;
this.properties.extensions.value[extDef.id] = props;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

File diff suppressed because it is too large Load Diff

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

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

@@ -1,4 +1,4 @@
@import './defaults';
@use './defaults';
body, html, #app {
margin: 0px;
@@ -8,5 +8,5 @@ body, html, #app {
bottom: 0;
left: 0;
right: 0;
font-family: $default-font-family;
font-family: defaults.$default-font-family;
}

View File

@@ -3,7 +3,7 @@ import { inject, observer } from 'mobx-react';
import BottomMenu from './menus/BottomMenu';
import TopMenu from './menus/TopMenu';
import Details from './Details';
import SDOEditor from './schema/SDOEditor';
import ExtensionEditor from './schema/ExtensionEditor';
import FileImporter from './FileImporter';
import JsonViewer from './bundle/JsonViewer';
import JsonPaste from './bundle/JsonPaste';
@@ -11,7 +11,8 @@ import SchemaPaste from './schema/SchemaPaste';
import RelationshipPicker from './relationship/RelationshipPicker';
import RelationshipDetails from './relationship/RelationshipDetails';
import RelationshipEditor from './relationship/RelationshipEditor';
import SDOPicker from './schema/SDOPicker';
import ExtensionPicker from './schema/ExtensionPicker';
import LayoutPanel from './layout/LayoutPanel';
import Growl from './ui/growl/Growl';
import SubmissionError from './SubmissionError';
import Flow from './Flow/Flow';
@@ -20,8 +21,7 @@ import './canvas.scss';
class Canvas extends React.Component {
constructor(props) {
super(props);
super(props);
this.store = this.props.store.appStore;
this.generateNodeID = this.generateNodeID.bind(this);
@@ -48,10 +48,16 @@ class Canvas extends React.Component {
this.onClickShowRelDetailsHandler = this.onClickShowRelDetailsHandler.bind(
this
);
this.onClickShowSDOPickerHandler = this.onClickShowSDOPickerHandler.bind(
this.onClickShowExtensionPickerHandler = this.onClickShowExtensionPickerHandler.bind(
this
);
this.onClickHideSDOPickerHandler = this.onClickHideSDOPickerHandler.bind(
this.onClickHideExtensionPickerHandler = this.onClickHideExtensionPickerHandler.bind(
this
);
this.onClickShowLayoutPanelHandler = this.onClickShowLayoutPanelHandler.bind(
this
);
this.onClickHideLayoutPanelHandler = this.onClickHideLayoutPanelHandler.bind(
this
);
this.onClickShowImporterHandler = this.onClickShowImporterHandler.bind(
@@ -65,13 +71,13 @@ class Canvas extends React.Component {
this.onClickCreateRelHandler = this.onClickCreateRelHandler.bind(this);
this.onClickEditRelHandler = this.onClickEditRelHandler.bind(this);
this.onClickSelectRelHandler = this.onClickSelectRelHandler.bind(this);
this.onClickSelectSDOHandler = this.onClickSelectSDOHandler.bind(this);
this.onClickSelectExtHandler = this.onClickSelectExtHandler.bind(this);
this.onClickShowGrowlHandler = this.onClickShowGrowlHandler.bind(this);
this.onClickGroupNodeHandler = this.onClickGroupNodeHandler.bind(this);
this.onClickGroupModeHandler = this.onClickGroupModeHandler.bind(this);
this.onClickSubmitGroupingHandler = this.onClickSubmitGroupingHandler.bind(this);
this.onChangeNodeHandler = this.onChangeNodeHandler.bind(this);
this.onChangeSDOHandler = this.onChangeSDOHandler.bind(this);
this.onChangeExtHandler = this.onChangeExtHandler.bind(this);
this.onChangeSchemaHandler = this.onChangeSchemaHandler.bind(this);
this.onChangeBundleHandler = this.onChangeBundleHandler.bind(this);
this.onChangeDateHandler = this.onChangeDateHandler.bind(this);
@@ -129,24 +135,45 @@ class Canvas extends React.Component {
);
this.onClickSchemaPasteHandler = this.onClickSchemaPasteHandler.bind(this);
this.onClickDeleteHandler = this.onClickDeleteHandler.bind(this);
this.onClickDeleteSDOHandler = this.onClickDeleteSDOHandler.bind(this);
this.onClickDeleteExtHandler = this.onClickDeleteExtHandler.bind(this);
this.onClickDeleteRelHandler = this.onClickDeleteRelHandler.bind(this);
this.onClickSubmitHandler = this.onClickSubmitHandler.bind(this);
this.onClickExportHandler = this.onClickExportHandler.bind(this);
this.onClickShowSubmissionErrorHandler = this.onClickShowSubmissionErrorHandler.bind(this);
this.onClickHideSubmissionErrorHandler = this.onClickHideSubmissionErrorHandler.bind(
this
);
this.onClickErrorHandler = this.onClickErrorHandler.bind(this);
}
componentWillUnmount() {
document.removeEventListener('dragover', () => {}, false);
}
/**
* Select the node with specified id.
* @param {string} nodeId id of node
*/
onClickHandler(nodeId) {
const node = this.store.getNodeById(nodeId);
this.store.setShowDetails(true);
this.store.setSelected(node);
}
/**
* Select the node associated with the specified id,
* from the Submission Errors panel.
* @param {string} nodeId
*/
onClickErrorHandler(nodeId) {
this.store.setShowSubmissionError(false);
this.store.showSubmissionErrorBadge = false;
const node = this.store.getNodeById(nodeId);
this.store.setShowDetails(true);
this.store.setSelected(node);
}
/**
* Activate or deactivate grouping selection.
* @param {boolean} isGrouping whether currently selecting group
*/
onClickGroupModeHandler(isGrouping) {
this.store.setGroupMode(isGrouping);
if (!isGrouping) {
@@ -155,11 +182,20 @@ class Canvas extends React.Component {
}
}
/**
* Add or remove the node with specified node from
* grouping selection.
* @param {string} id node id
*/
onClickGroupNodeHandler(id) {
this.store.modifyGroup(id);
this.setUpdateFlow(true);
}
/**
* Create a new Grouping SDO, including all nodes
* from the grouping selection.
*/
onClickSubmitGroupingHandler() {
const id = this.generateNodeID('grouping--');
this.store.createGroup(id);
@@ -167,73 +203,140 @@ class Canvas extends React.Component {
this.setUpdateFlow(true);
}
/**
* Select the relationship with the specified ID
* to edit via the Relationship Editor panel.
* @param {*} relId relationship id
*/
onClickRelHandler(relId) {
const rel = this.store.getRelById(relId);
this.store.setShowRelEditor(true);
this.store.setSelectedRel(rel);
}
/**
* Hide the details panel.
*/
onClickHideDetailsHandler() {
this.store.setShowDetails(false);
}
/**
* Hide the Extension Editor panel.
*/
onClickHideEditorHandler() {
this.store.setShowEditor(false);
}
/**
* Hide the Json Paste panel.
*/
onClickHideJsonPasteHandler() {
this.store.setShowJSONPaste(false);
}
/**
* Show the Json Paste panel.
*/
onClickShowJsonPasteHandler() {
this.store.setShowJSONPaste(true);
}
/**
* Hide the Schema Paste panel.
*/
onClickHideSchemaPasteHandler() {
this.store.setShowSchemaPaste(false);
}
/**
* Show the Schema Paste panel.
*/
onClickShowSchemaPasteHandler() {
this.store.setShowSchemaPaste(true);
}
/**
* Display the growl message.
* @param {string} message growl message
*/
onClickShowGrowlHandler(message) {
this.store.setGrowlMessage(message);
this.store.setShowGrowl(true);
}
onClickHideSubmissionErrorHandler() {
this.store.resetSubmissionError();
/**
* Hide the Submission Error panel.
*/
onClickShowSubmissionErrorHandler() {
this.store.setShowSubmissionError(true);
this.store.validateSubmission();
}
/**
* Hide the Submission Error panel.
*/
onClickHideSubmissionErrorHandler() {
this.store.showSubmissionErrorBadge = false;
this.store.setShowSubmissionError(false);
}
/**
* Delete the selected node.
*/
onClickDeleteHandler() {
this.store.deleteSelectedNode();
this.setUpdateFlow(true);
}
onClickDeleteSDOHandler() {
this.store.deleteSelectedSDO();
/**
* Delete the selected extension.
*/
onClickDeleteExtHandler() {
this.store.deleteSelectedExt();
this.setUpdateFlow(true);
}
/**
* Delete the selected relationship.
*/
onClickDeleteRelHandler() {
this.store.deleteSelectedRelationship();
this.setUpdateFlow(true);
}
/**
* Update the specified property value for
* the selected node.
* @param {*} event
*/
onChangeNodeHandler(event) {
this.store.editNodeValues(event);
this.setUpdateFlow(true);
}
onChangeSDOHandler(event) {
this.store.editSDOValues(event);
this.forceUpdate();
/**
* Update the specified property value for
* the selected extension.
* @param {*} event
*/
onChangeExtHandler(event) {
this.store.editExtensionValues(event);
}
/**
* Import a schema from a file.
* @param {object} file schema json
*/
onChangeSchemaHandler(file) {
this.store.loadSchemaFromFile(file);
}
/**
* Import a bundle from a file.
* @param {*} file bundle json
*/
onChangeBundleHandler(file) {
this.store.loadBundleFromFile(file);
this.store.nodes.map((n) => {
@@ -242,58 +345,129 @@ class Canvas extends React.Component {
this.setUpdateFlow(true);
}
/**
* Update the Creator ID for SDO, SCO, and SROs created
* via the STIX UI.
* @param {string} id
*/
onChangeCreatorIDHandler(id) {
this.store.updateCreatorID(id);
}
/**
* Update the specified date property for the
* selected node.
* @param {string} property
* @param {*} datetime
*/
onChangeDateHandler(property, datetime) {
const value = this.store.generateTimestamp(datetime);
this.mutateOnEvent(property, value);
}
/**
* Update the specified array property for
* the selected node.
* @param {string} property
* @param {*} value
*/
onClickArrayHandler(property, value) {
this.mutateOnEvent(property, value);
}
/**
* Update the specified property for
* the selected node (for Slider inputs).
* @param {string} property
* @param {*} value
*/
onChangeSliderHandler(property, value) {
this.mutateOnEvent(property, value);
}
/**
* Update the specified boolean property
* for the selected node.
* @param {string} property
* @param {boolean} value
*/
onClickBooleanHandler(property, value) {
this.mutateOnEvent(property, value);
}
/**
* Update the specified kill chain property
* for the selected node.
* @param {string} property
* @param {*} value
*/
onChangePhaseHandler(property, value) {
this.mutateOnEvent(property, value);
}
/**
* Update the specified property for the
* selected node (for Confirm Text Area inputs).
* @param {string} property
* @param {string} value
*/
onClickAddTextHandler(property, value) {
this.mutateOnEvent(property, value);
}
/**
* Update the specified list property for the
* selected node.
* @param {string} property
* @param {*} value
*/
onChangeListHandler(property, value) {
this.mutateOnEvent(property, value);
}
/**
* Update the object property for the selected node.
* @param {string} property
* @param {*} event
*/
onChangeGenericObjectHandler(property, event) {
this.mutateOnEvent(property, event.currentTarget.value);
}
onClickRemovePhaseHander(property, value) {
this.store.removeKillChainPhase(value);
/**
* Remove the specified kill chain phase property
* for the selected node.
* @param {string} property
* @param {number} idx index of phase in kill chain
*/
onClickRemovePhaseHander(property, idx) {
this.store.deleteArrayObject(idx, property);
}
/**
* Update the specified array property for the selected value
* (for use with Comma Seperated Value inputs).
* @param {*} event
*/
onChangeCSVHandler(event) {
this.store.editCSVInput(event);
}
/**
* Create a new relationship between the specified
* source and target.
* @param {string} srcId id of source
* @param {string} targetId id of target
* @param {object} rel relationship object
*/
onClickCreateRelHandler(srcId, targetId, rel) {
const src = { id: srcId, };
const target = { id: targetId, };
const relationship = this.store.makeRelationship(src, target, rel);
if (relationship) {
this.onClickSelectRelHandler(relationship);
this.store.addCustomRelationship(rel, srcId, targetId);
// this.store.addCustomRelationship(rel, srcId, targetId);
this.store.addCustomRelationship(rel, srcId);
this.setUpdateFlow(true);
} else {
this.store.setGrowlMessage('Could not create relationship');
@@ -301,12 +475,20 @@ class Canvas extends React.Component {
}
}
/**
* Edit the specified relationship.
* @param {object} rel
*/
onClickEditRelHandler(rel) {
this.store.editRelationship(rel);
this.store.setShowRelEditor(false);
this.setUpdateFlow(true);
}
/**
* Select the specified relationship.
* @param {object} relationship
*/
onClickSelectRelHandler(relationship) {
this.store.setShowRelDetails(false);
this.store.manuallySelectRelationship(relationship);
@@ -314,107 +496,211 @@ class Canvas extends React.Component {
this.setUpdateFlow(true);
}
onClickSelectSDOHandler(sdo) {
this.store.setSelectedSDO(sdo);
/**
* Select the specified extension.
* @param {object} extension
*/
onClickSelectExtHandler(extension) {
this.store.setSelectedExt(extension)
this.store.setShowEditor(true);
}
/**
* Add an object to the specified object array
* property for the selected node.
* @param {string} field
* @param {list} requiredFields
*/
onClickAddObjectHandler(field, requiredFields) {
this.store.addDefaultObject(field, requiredFields);
}
/**
* Delete the specified property from an external reference
* for the selected node.
* @param {object} select property to delete
* @param {number} idx index of external reference in external references
*/
onClickDeletePropertyHandler(select, idx) {
this.store.deleteERObjectProperty(select, idx);
}
/**
* Delete the specified field for an object in the specified
* property array for the selected node.
* @param {string} select object property
* @param {number} idx index of object in node property
* @param {string} property node property
*/
onClickDeleteArrayObjectPropertyHandler(select, idx, property) {
this.store.deleteArrayObjectProperty(select, idx, property);
}
/**
* Delete the specified external reference from the
* external references property for the selected node.
* @param {number} idx external reference index
*/
onClickDeleteERHandler(idx) {
this.store.deleteERObject(idx);
}
/**
* Delete the specified object from the specified
* array property for the selected node.
* @param {number} idx index of object in node property
* @param {string} property node property
*/
onClickDeleteArrayObjectHandler(idx, property) {
this.store.deleteArrayObject(idx, property);
}
/**
* Update the specified field for an object in the specified
* property array for the selected node.
* @param {string} select object property
* @param {number} idx index of object in node property
* @param {string} property node property
*/
onChangeERHandler(input, select, idx) {
this.store.changeERValue(input, select, idx);
}
/**
* Update the specified object from the specified
* array property for the selected node.
* @param {number} idx index of object in node property
* @param {string} property node property
*/
onChangeArrayObjectHandler(input, field, idx, property) {
this.store.changeArrayObjectValue(input, field, idx, property);
}
/**
* Show the JSON Viewer panel.
*/
onClickShowJsonHandler() {
this.store.mutateBundle();
this.store.stringifyBundle();
this.store.setShowJSON(true);
}
/**
* Hide the JSON Viewer panel.
*/
onClickHideJsonHandler() {
this.store.setShowJSON(false);
}
/**
* Update the store pasteBundle value to
* the specified value.
* @param {*} event
*/
onChangeJSONPasteHandler(event) {
this.store.setPasteBundle(event.currentTarget.value);
}
/**
* Import a bundle from the Json Paste panel.
*/
onClickJSONPasteHandler() {
this.store.loadBundleFromPaste();
this.store.loadBundleFromPaste();
this.store.nodes.map((n) => {
this.transition(n.id, true);
});
this.setUpdateFlow(true);
}
/**
* Update the store pasteSchema value to
* the specified value.
* @param {*} event
*/
onChangeSchemaPasteHandler(event) {
this.store.setPasteSchema(event.currentTarget.value);
}
/**
* Import a schema from the Schema Paste panel.
*/
onClickSchemaPasteHandler() {
this.store.loadSchemaFromPaste();
}
/**
* Show the Relationship Details panel.
*/
onClickShowRelDetailsHandler() {
this.store.setShowRelDetails(true);
this.store.setShowRelPicker(false);
}
/**
* Hide the Relationship Details panel.
*/
onClickHideRelDetailsHandler() {
this.store.setShowRelDetails(false);
this.store.setShowRelPicker(true);
}
/**
* Hide the Relationship Editor panel.
*/
onClickHideRelEditorHandler() {
this.store.setShowRelEditor(false);
}
/**
* Hide the Relationship Picker panel.
*/
onClickHideRelPickerHandler() {
this.store.setShowRelPicker(false);
}
onClickShowSDOPickerHandler() {
this.store.setShowSDOPicker(true);
/**
* Show the Extension Picker panel.
*/
onClickShowExtensionPickerHandler() {
this.store.setShowExtensionPicker(true);
}
onClickHideSDOPickerHandler() {
this.store.setShowSDOPicker(false);
/**
* Hide the Extension Picker panel.
*/
onClickHideExtensionPickerHandler() {
this.store.setShowExtensionPicker(false);
}
onClickShowLayoutPanelHandler() {
this.store.setShowLayoutPanel(true);
}
onClickHideLayoutPanelHandler() {
this.store.setShowLayoutPanel(false);
}
/**
* Hide the File Importer panel.
*/
onClickHideImporterHandler() {
this.store.setShowImporter(false);
}
/**
* Show the File Importer panel.
*/
onClickShowImporterHandler() {
this.store.setShowImporter(true);
}
// Prevent event propagation.
onDragOverHandler(event) {
event.preventDefault();
}
/**
* Set the dragged source node to the specified node.
* @param {*} event
*/
onDragStartHandler(event) {
const node = JSON.parse(event.dataTransfer.getData('node'));
this.store.setDragging(node);
@@ -426,7 +712,11 @@ class Canvas extends React.Component {
}, 2500);
}
// Drop on canvas
/**
* Create a new node of the dropped icon type, either
* directly or as an observable for the drop target.
* @param {*} event
*/
onDropHandler(event) {
event.preventDefault();
const node = this.store.dragging;
@@ -471,7 +761,11 @@ class Canvas extends React.Component {
}
}
// Connect two nodes via a new relationship
/**
* Create a relationship between the source and target nodes.
* @param {string} sourceId source id
* @param {string} targetId target id
*/
onConnectNodeHandler(sourceId, targetId) {
const sourceNode = this.store.getNodeById(sourceId);
const targetNode = this.store.getNodeById(targetId);
@@ -487,44 +781,87 @@ class Canvas extends React.Component {
this.store.relationships.unshift(genericRel);
this.store.setShowRelPicker(true);
}
}
// Update store position from React Flow
onDragStopNodeHandler(node) {
const n = this.store.getNodeById(node.id);
if (n) {
n.position = node.position;
/**
* Update the store node position to its respective
* Flow node position.
* @param {object} flowNode React Flow node
*/
onDragStopNodeHandler(flowNode) {
const node = this.store.getNodeById(flowNode.id);
if (node) {
node.position = flowNode.position;
}
}
/**
* Add an object to the specified object property
* for the selected node.
* @param {string} field node property
* @param {object} o object to add
*/
onClickAddGenericObjectHandler(field, o) {
this.store.addGenericObject(field, o);
}
/**
* Delete an object from the specified object property
* for the selected node.
* @param {string} field node property
* @param {string} key key of object to delete
*/
onClickDeleteGenericObjectHandler(field, key) {
this.store.deleteGenericObject(field, key);
}
/**
* Reset the STIX UI.
*/
onClickResetHandler() {
this.store.reset();
}
onClickSubmitHandler() {
this.store.submit();
/**
* Export the STIX bundle.
*/
onClickExportHandler() {
this.store.stringifyBundle();
this.store.export();
}
/**
* Force React Flow to rerender.
* @param {boolean} update whether to rerender
*/
setUpdateFlow(update) {
this.store.setUpdateFlow(update);
}
/**
* Set the mouse position
* @param {number} x
* @param {number} y
*/
setMousePosition(x, y) {
this.store.setMousePosition(x, y);
}
/**
* Generate a new node id.
* @param {string} prefix prefix of id
* @returns new node id
*/
generateNodeID(prefix) {
return this.store.generateNodeID(prefix);
}
/**
* Convert a node property and value into an event object.
* @param {string} property node property
* @param {*} value node value
*/
mutateOnEvent(property, value) {
const event = {
currentTarget: {
@@ -536,10 +873,18 @@ class Canvas extends React.Component {
this.onChangeNodeHandler(event);
}
/**
* Set the position of the specified node.
* @param {string} id node id
* @param {boolean} random whether to set at random or mouse position
* @returns
*/
transition(id, random) {
const canvas = document.getElementById('canvas');
const node = this.store.getNodeById(id);
if (node.title == 'extension-definition') return;
const calculate = (min, max) => Math.random() * (max - 100 - min) + min;
const bounds = {
@@ -566,10 +911,11 @@ class Canvas extends React.Component {
}
}
render() {
const { nodes, } = this.store;
const { edges, } = this.store;
const sdos = this.store.getCustomSDOs();
const extensions = this.store.getExtensions();
return (
<div
@@ -598,14 +944,17 @@ class Canvas extends React.Component {
onClickShowSchemaPasteHandler={this.onClickShowSchemaPasteHandler}
onClickHideJsonHandler={this.onClickHideJsonHandler}
onClickResetHandler={this.onClickResetHandler}
onClickSubmitHandler={this.onClickSubmitHandler}
onClickShowSDOPickerHandler={this.onClickShowSDOPickerHandler}
onClickExportHandler={this.onClickExportHandler}
onClickShowExtensionPickerHandler={this.onClickShowExtensionPickerHandler}
onClickShowLayoutPanelHandler={this.onClickShowLayoutPanelHandler}
onClickShowImporterHandler={this.onClickShowImporterHandler}
onChangeCreatorIDHandler={this.onChangeCreatorIDHandler}
onClickGroupModeHandler={this.onClickGroupModeHandler}
onClickSubmitGroupingHandler={this.onClickSubmitGroupingHandler}
onClickShowErrorHandler={this.onClickShowSubmissionErrorHandler}
creatorID={this.store.creatorID}
groupMode={this.store.groupMode}
errors={this.store.showSubmissionErrorBadge}
/>
<BottomMenu
@@ -663,12 +1012,37 @@ class Canvas extends React.Component {
onClickDeleteRelHandler={this.onClickDeleteRelHandler}
/>
<SDOEditor
<ExtensionEditor
show={this.store.showEditor}
sdo={this.store.selectedSDO}
extension={this.store.selectedExt}
onClickHideHandler={this.onClickHideEditorHandler}
onChangeSDOHandler={this.onChangeSDOHandler}
onClickDeleteHandler={this.onClickDeleteSDOHandler}
onChangeExtHandler={this.onChangeExtHandler}
onChangeNodeHandler={this.onChangeNodeHandler}
onChangeDateHandler={this.onChangeDateHandler}
onClickArrayHandler={this.onClickArrayHandler}
onChangeListHandler={this.onChangeListHandler}
onChangeSliderHandler={this.onChangeSliderHandler}
onChangeCSVHandler={this.onChangeCSVHandler}
onClickBooleanHandler={this.onClickBooleanHandler}
onChangePhaseHandler={this.onChangePhaseHandler}
onClickRemovePhaseHander={this.onClickRemovePhaseHander}
onClickAddObjectHandler={this.onClickAddObjectHandler}
onClickDeleteERHandler={this.onClickDeleteERHandler}
onChangeERHandler={this.onChangeERHandler}
onClickDeletePropertyHandler={this.onClickDeletePropertyHandler}
onClickDeleteArrayObjectHandler={this.onClickDeleteArrayObjectHandler}
onChangeArrayObjectHandler={this.onChangeArrayObjectHandler}
onClickDeleteArrayObjectPropertyHandler={
this.onClickDeleteArrayObjectPropertyHandler
}
onChangeGenericObjectHandler={this.onChangeGenericObjectHandler}
onClickAddGenericObjectHandler={this.onClickAddGenericObjectHandler}
onClickDeleteGenericObjectHandler={
this.onClickDeleteGenericObjectHandler
}
onClickAddTextHandler={this.onClickAddTextHandler}
onClickDeleteHandler={this.onClickDeleteExtHandler}
/>
<FileImporter
@@ -680,7 +1054,7 @@ class Canvas extends React.Component {
<JsonViewer
show={this.store.showJSON}
json={this.store.mutatedBundle}
json={this.store.bundleJSON}
onClickHideHandler={this.onClickHideJsonHandler}
onClickShowGrowlHandler={this.onClickShowGrowlHandler}
/>
@@ -711,11 +1085,18 @@ class Canvas extends React.Component {
onClickShowRelDetailsHandler={this.onClickShowRelDetailsHandler}
/>
<SDOPicker
id="sdo-picker"
show={this.store.showSDOPicker}
sdos={sdos}
onClickHideHandler={this.onClickHideSDOPickerHandler}
<ExtensionPicker
id="extension-picker"
extensions={extensions}
show={this.store.showExtensionPicker}
onClickHideHandler={this.onClickHideExtensionPickerHandler}
onClickSelectExtHandler={this.onClickSelectExtHandler}
/>
<LayoutPanel
id="layout-panel"
show={this.store.showLayoutPanel}
onClickHideHandler={this.onClickHideLayoutPanelHandler}
onClickSelectSDOHandler={this.onClickSelectSDOHandler}
/>
@@ -729,6 +1110,7 @@ class Canvas extends React.Component {
error={this.store.failedCollection}
show={this.store.showSubmissionError}
onClickHideHandler={this.onClickHideSubmissionErrorHandler}
onClickNodeHandler={this.onClickErrorHandler}
/>
</div>
);

View File

@@ -17,7 +17,7 @@ import GenericObject from './ui/complex/GenericObject';
import ConfirmTextarea from './ui/complex/ConfirmTextarea';
import ObjectArray from './ui/complex/ObjectArray';
import Images from '../imgs/Images';
import Images from '../util/Images';
import './details.scss';
@@ -57,8 +57,9 @@ class Details extends React.Component {
}
for (const prop in props) {
const cls = 'item-header';
const header = (
<div className="item-header">
<div className={cls}>
{prop}
<span
data-tooltip-id={`${prop}-tooltip`}
@@ -90,13 +91,14 @@ class Details extends React.Component {
<Text
name={prop}
value={props[prop].value}
required={props[prop].required}
onChange={this.onChangeHandler}
/>
</div>
</div>
);
break;
case 'dts':
case 'timestamp':
control = (
<div className="item" key={prop}>
{header}
@@ -104,7 +106,9 @@ class Details extends React.Component {
<DateTime
name={prop}
selected={props[prop].value}
onChange={this.onChangeDateHandler}
required={props[prop].required}
onTextChange={this.onChangeHandler}
onDateChange={this.onChangeDateHandler}
/>
</div>
</div>
@@ -119,6 +123,7 @@ class Details extends React.Component {
field={prop}
value={props[prop].value}
description={props[prop].description}
required={props[prop].required}
onClickHandler={this.props.onClickArrayHandler}
/>
);
@@ -128,9 +133,9 @@ class Details extends React.Component {
if (Array.isArray(refField)) {
refField = refField[0];
}
const ref = refField.$ref ? refField.$ref : refField.type;
const ref = refField.$ref ?? refField.type;
if (ref === '../common/dictionary.json' || ref === 'object') {
if (ref.includes('dictionary.json') || ref === 'object') {
control = (
<ObjectArray
node={node}
@@ -138,6 +143,7 @@ class Details extends React.Component {
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}
@@ -159,13 +165,14 @@ class Details extends React.Component {
<Boolean
name={prop}
selected={props[prop].value}
required={props[prop].required}
onClick={this.props.onClickBooleanHandler}
/>
</div>
</div>
);
break;
case '../common/dictionary.json':
case 'dictionary':
case 'object':
control = (
<GenericObject
@@ -174,6 +181,7 @@ class Details extends React.Component {
description={props[prop].description}
key={uuid()}
field={prop}
required={props[prop].required}
onClickAddObjectHandler={
this.props.onClickAddGenericObjectHandler
}
@@ -189,7 +197,7 @@ class Details extends React.Component {
if (props[prop].$ref && !props[prop].control) {
switch (props[prop].$ref) {
case '../common/identifier.json':
case 'identifier':
control = (
<div className="item" key={prop}>
{header}
@@ -197,6 +205,7 @@ class Details extends React.Component {
<Text
name={prop}
value={props[prop].value}
required={props[prop].required}
onChange={this.onChangeHandler}
/>
</div>
@@ -218,6 +227,7 @@ class Details extends React.Component {
<Slider
value={props[prop].value}
field={prop}
required={props[prop].required}
onChangeHandler={this.props.onChangeSliderHandler}
/>
</div>
@@ -233,6 +243,7 @@ class Details extends React.Component {
key={prop}
name={prop}
value={props[prop].value}
required={props[prop].required}
onChangeHandler={this.props.onChangeCSVHandler}
/>
</div>
@@ -248,6 +259,7 @@ class Details extends React.Component {
field={prop}
value={props[prop].value}
description={props[prop].description}
required={props[prop].required}
onChangeHandler={this.props.onChangePhaseHandler}
onClickRemoveHandler={this.props.onClickRemovePhaseHander}
/>
@@ -260,7 +272,9 @@ class Details extends React.Component {
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}
@@ -278,6 +292,7 @@ class Details extends React.Component {
field={prop}
value={props[prop].value}
description={props[prop].description}
required={props[prop].required}
onClickHandler={this.props.onClickArrayHandler}
/>
);
@@ -290,6 +305,7 @@ class Details extends React.Component {
<TextArea
name={prop}
value={props[prop].value}
required={props[prop].required}
onChange={this.onChangeHandler}
/>
</div>
@@ -305,6 +321,7 @@ class Details extends React.Component {
key={prop}
name={prop}
value={props[prop].value}
required={props[prop].required}
onChangeHandler={this.props.onChangeCSVHandler}
/>
</div>
@@ -316,13 +333,15 @@ class Details extends React.Component {
<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
}
onClickDeleteArrayObjectHandler={
onClickDeleteObjectHandler={
this.props.onClickDeleteGenericObjectHandler
}
onChangeHandler={this.props.onChangeGenericObjectHandler}
@@ -337,6 +356,7 @@ class Details extends React.Component {
description={props[prop].description}
key={uuid()}
field={prop}
required={props[prop].required}
onClickAddTextHandler={this.props.onClickAddTextHandler}
/>
);
@@ -346,6 +366,49 @@ class Details extends React.Component {
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}

View File

@@ -1,35 +1,36 @@
import React from 'react';
import { Handle, Position } from 'reactflow';
import Images from '../../imgs/Images';
import Images from '../../util/Images';
import classNames from 'classnames';
import './FlowNode.scss';
export default class FlowNode extends React.Component {
constructor(props) {
super(props);
this.state = {
selected: false,
};
}
render() {
const { node, } = this.props.data;
let display = node.id.split('--')[0];
const border = node.selected ? 'solid 1px blue' : '';
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
<div className={cls}
style={{
height: '100%',
width: '100%',
position: 'absolute',
backgroundSize: 'contain',
backgroundImage: `url(${node.customImg ? node.customImg : Images.getImage(node.img)})`,
backgroundRepeat: 'no-repeat',
border: `${border}`,
}}
/>
<Handle
@@ -63,7 +64,7 @@ export default class FlowNode extends React.Component {
isConnectable={this.props.isConnectable}
/>
<div className="nodeLabel">
<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

@@ -1,23 +1,24 @@
/* eslint-disable react/prefer-stateless-function */
import React from 'react';
import { observer } from 'mobx-react';
import Panel from './ui/panel/Panel';
import Images from '../imgs/Images';
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 (!errorStructure.hasOwnProperty(item.node)) {
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({
@@ -34,7 +35,7 @@ class SubmissionError extends React.Component {
for (const item in errorStructure) {
const details = [];
const name = errorStructure[item].name;
if (errorStructure[item].details) {
errorStructure[item].details.map((detail) => {
details.push(
@@ -50,11 +51,11 @@ class SubmissionError extends React.Component {
});
msg.push(
<div key={item}>
<div className="header">
<div className='submission-item' key={item} onClick={() => this.props.onClickNodeHandler(item)}>
<div className="container-header">
<img src={Images.getImage(errorStructure[item].img)} width="30" />
{' '}
{item}
{name}
</div>
<div className="rows-container">
{details}
@@ -69,6 +70,9 @@ class SubmissionError extends React.Component {
show={this.props.show}
onClickHideHandler={this.props.onClickHideHandler}
>
<div className="header">
Errors
</div>
<div className="submission-error">
{msg}
</div>

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

@@ -1,4 +1,3 @@
/* eslint-disable react/prefer-stateless-function */
import React from 'react';
import { observer } from 'mobx-react';
import Panel from '../ui/panel/Panel';

View File

@@ -24,6 +24,11 @@
display: flex;
flex-direction: row;
justify-content: flex-end;
height: 30px;
padding: 2px;
}
.json-copy {
height: 35px;
font-size: 15px;
}
}

View File

@@ -24,6 +24,12 @@
display: flex;
flex-direction: row;
justify-content: flex-end;
height: 30px;
padding: 2px;
gap: 5px;
}
}
.json-copy {
height: 35px;
font-size: 15px;
}
}

View File

@@ -1,4 +1,4 @@
@import '../defaults';
@use '../defaults';
.details {
font-size: 18px;
@@ -14,7 +14,7 @@
display: flex;
flex-direction: row;
background-color: $lt-gray-bg;
background-color: defaults.$lt-gray-bg;
.title {
padding-top: 5px;
@@ -30,10 +30,10 @@
padding-left: 7px;
width: 80px;
display: flex;
background-color: $default-active-bg;
background-color: defaults.$default-active-bg;
border-radius: 5px;
padding-top: 7px;
color: $light-font-0;
color: defaults.$light-font-0;
cursor: pointer;
font-size: 14px;
@@ -48,10 +48,19 @@
}
.delete:hover {
background-color: $error-font;
background-color: defaults.$error-font;
}
}
.invalid {
border: red solid 1px;
}
.required-warning {
color: red;
font-size: 13px;
}
.body {
overflow-y: scroll;
overflow-x: hidden;
@@ -68,7 +77,7 @@
.item-header {
font-weight: bold;
color: $default-active-bg;
color: defaults.$default-active-bg;
padding-bottom: 3px;
span {
@@ -78,6 +87,24 @@
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 {

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

@@ -1,9 +1,8 @@
/* eslint-disable react/prefer-stateless-function */
import React from 'react';
import { observer } from 'mobx-react';
import MenuItem from './MenuItem';
import Images from '../../imgs/Images';
import Images from '../../util/Images';
import './BottomMenu.scss';

View File

@@ -1,4 +1,4 @@
@import '../../defaults';
@use '../../defaults';
.menu {
position: fixed;
@@ -29,12 +29,12 @@
width: 40px;
height: 40px;
border-radius: 5px;
background-color: $default-active-bg;
background-color: defaults.$default-active-bg;
div {
padding-top: 10px;
padding-left: 7px;
color: $light-font-0;
color: defaults.$light-font-0;
cursor: pointer;
}
}

View File

@@ -29,7 +29,7 @@ class TopMenu extends React.Component {
}
render() {
let groupLabel = 'Select';
let groupLabel = 'Group';
let groupClass = '';
let items;
@@ -38,7 +38,8 @@ class TopMenu extends React.Component {
groupClass = 'cancel-btn';
items = (
<div id="myDropdown" className="dropdown-content">
<a onClick={this.submitGroup}>Create Group</a>
<a onClick={this.submitGroup}>Create Group
</a>
</div>
);
}
@@ -48,7 +49,7 @@ class TopMenu extends React.Component {
<div
data-tooltip-id="select-tooltip"
data-tooltip-content="Select Nodes"
className={`grouping-btn menu-item ${groupClass}`}
className={`grouping-btn menu-btn menu-item ${groupClass}`}
onClick={this.flipGroupMode}
>
{groupLabel}
@@ -57,6 +58,8 @@ class TopMenu extends React.Component {
</div>
);
const badge = this.props.errors ? (<span className="badge"></span>) : undefined;
return (
<div className="top-menu">
<div className="row">
@@ -74,69 +77,85 @@ class TopMenu extends React.Component {
/>
</div>
<div
data-tooltip-id="paste-tooltip"
data-tooltip-content="Paste JSON"
className="json-paste-btn menu-item-medium"
onClick={this.props.onClickShowJsonPasteHandler}
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="view-tooltip"
data-tooltip-content="View JSON"
className="json-btn menu-item-small"
onClick={this.props.onClickShowJsonHandler}
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="schema-paste-btn menu-item-medium"
className="menu-btn menu-item"
onClick={this.props.onClickShowSchemaPasteHandler}
>
{'{ * }'}
<i className="material-icons">add_box</i>
</div>
<div
data-tooltip-id="sdo-tooltip"
data-tooltip-content="SDO Extensions"
data-tooltip-id="layout"
data-tooltip-content="Graph Layout and Filtering"
className="sdos-btn menu-item"
onClick={this.props.onClickShowSDOPickerHandler}
onClick={this.props.onClickShowLayoutPanelHandler}
>
Exts
Layout
</div>
<div
data-tooltip-id="import-tooltip"
data-tooltip-content="Import Data from File"
className="reset-btn menu-item"
className="menu-btn menu-item"
onClick={this.props.onClickShowImporterHandler}
>
Import
<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="Clear JSON"
className="reset-btn menu-item"
data-tooltip-content="Reset Bundle"
className="reset-btn menu-btn menu-item"
onClick={this.props.onClickResetHandler}
>
<span className="i material-icons">refresh</span>
{' '}
Reset
<span className="material-icons">refresh</span>
</div>
<div
data-tooltip-id="submit-tooltip"
data-tooltip-content="Submit JSON"
className="reset-btn menu-item"
onClick={this.props.onClickSubmitHandler}
data-tooltip-content="Export JSON"
className="menu-btn menu-item"
onClick={this.props.onClickExportHandler}
>
<span className="i material-icons">add</span>
<i className="material-icons">save</i>
{' '}
Submit
</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" />
@@ -147,6 +166,7 @@ class TopMenu extends React.Component {
<Tooltip id="import-tooltip" />
<Tooltip id="clear-tooltip" />
<Tooltip id="submit-tooltip" />
<Tooltip id="error-tooltip" />
</div>
</div>
);

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

@@ -4,15 +4,16 @@ 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 '../../imgs/Images';
import Images from '../../util/Images';
import '../details.scss';
import './RelationshipDetails.scss';
class RelationshipDetails extends React.Component {
constructor(props) {
super(props);
this.state = {
type: 'relates to',
type: 'related-to',
x_exclusive: false,
};
this.onSubmitHandler = this.onSubmitHandler.bind(this);
@@ -45,7 +46,7 @@ class RelationshipDetails extends React.Component {
reset() {
this.setState({
type: 'relates to',
type: 'related-to',
x_exclusive: false,
});
}

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

@@ -4,7 +4,7 @@ import { toJS } from 'mobx';
import { Tooltip } from 'react-tooltip';
import Panel from '../ui/panel/Panel';
import Text from '../ui/inputs/Text';
import Images from '../../imgs/Images';
import Images from '../../util/Images';
import './RelationshipDetails.scss';

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

@@ -1,4 +1,4 @@
@import "../../defaults";
@use "../../defaults";
.relationship-picker {
display: flex;
@@ -26,7 +26,7 @@
padding-top: 10px;
cursor: pointer;
font-weight: bold;
font-family: $default-font-family;
font-family: defaults.$default-font-family;
line-height: 30px;
img {
@@ -43,7 +43,7 @@
}
.rel-type {
color: $default-active-bg;
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

@@ -1,4 +1,3 @@
/* eslint-disable react/prefer-stateless-function */
import React from 'react';
import { observer } from 'mobx-react';
import Panel from '../ui/panel/Panel';

View File

@@ -1,4 +1,4 @@
@import '../../../defaults';
@use '../../../defaults';
button:focus {
outline: none;
@@ -8,8 +8,8 @@ button.def {
width: auto;
min-width: 130px;
height: 30px;
color: $light-font-0;
font-family: $default-font-family;
color: defaults.$light-font-0;
font-family: defaults.$default-font-family;
font-size: 14px;
border-color: transparent;
cursor: pointer;
@@ -21,7 +21,7 @@ button.def {
button.disabled {
background-color: rgba(128,128,128,.8) !important;
color: $gray-font-0 !important;
color: defaults.$gray-font-0 !important;
cursor: auto;
}

View File

@@ -2,6 +2,7 @@ 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';
@@ -18,10 +19,6 @@ class ConfirmTextarea extends React.Component {
};
}
componentDidMount() {
}
onChangeInputHandler(event) {
event.preventDefault();
@@ -42,6 +39,9 @@ class ConfirmTextarea extends React.Component {
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">
@@ -56,7 +56,10 @@ class ConfirmTextarea extends React.Component {
</span>
<Tooltip id={`${field}-tooltip`} />
</div>
<div className="ct-body">
<div className={classNames({
"ct-body": true,
"invalid": invalid
})}>
<div className="ct-block-input">
<div className="input">
<TextArea
@@ -74,6 +77,7 @@ class ConfirmTextarea extends React.Component {
{value}
</div>
</div>
{warning}
</div>
);
}

View File

@@ -2,6 +2,7 @@ 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';
@@ -17,8 +18,6 @@ class ExternalReferences extends React.Component {
this.onClickDeleteHandler = this.onClickDeleteHandler.bind(this);
}
componentDidMount() {}
onChangeERHandler(event, value) {
return undefined;
}
@@ -48,6 +47,10 @@ class ExternalReferences extends React.Component {
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">
@@ -71,12 +74,16 @@ class ExternalReferences extends React.Component {
<Tooltip id={`${field}-tooltip`} />
<Tooltip id={`${field}-control-tooltip`} />
</div>
<div className="er-body">
<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}
@@ -84,6 +91,7 @@ class ExternalReferences extends React.Component {
/>
))}
</div>
{warning}
</div>
);
}
@@ -92,8 +100,9 @@ class ExternalReferences extends React.Component {
function ReferenceBlock(props) {
const blocks = [];
const idx = props.i;
const selectID = `select-${props.i}`;
const inputID = `input-${props.i}`;
const prefix = props.prefix;
const selectID = `select-${prefix}-${props.i}`;
const inputID = `input-${prefix}-${props.i}`;
const propValues = [
'source_name',

View File

@@ -2,6 +2,7 @@ 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';
@@ -10,21 +11,18 @@ 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: '',
key: this.props.vocab? this.props.vocab[0] : '',
value: '',
};
}
componentDidMount() {
}
onChangeInputHandler(event) {
event.preventDefault();
@@ -33,8 +31,16 @@ class GenericObject extends React.Component {
});
}
onChangeSelectHandler(event) {
event.preventDefault();
const key = document.getElementById(`select-${this.props.field}`).value;
this.setState({
"key": key
});
}
onClickDeleteHandler(select, idx) {
this.props.onClickDeletePropertyHandler(select, idx);
this.props.onClickDeleteObjectHandler(select, idx);
}
onClickCreateBlankHandler() {
@@ -54,9 +60,13 @@ class GenericObject extends React.Component {
render() {
const { field, } = this.props;
const value = this.props.value ? this.props.value : [];
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(
@@ -65,11 +75,30 @@ class GenericObject extends React.Component {
v={value[key]}
k={key}
field={field}
onClickDeleteHandler={this.props.onClickDeleteObjectHandler}
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">
@@ -83,11 +112,14 @@ class GenericObject extends React.Component {
</span>
<Tooltip id={`${field}-tooltip`} />
</div>
<div className="go-body">
<div className={classNames({
"go-body": true,
"invalid": invalid
})}>
<div className="go-block-input">
<div className="input">
<Text name="key" value={this.state.key} onChange={this.onChangeInputHandler} />
{selector}
</div>
<div className="input">
<Text name="value" value={this.state.value} onChange={this.onChangeInputHandler} />
@@ -99,6 +131,7 @@ class GenericObject extends React.Component {
{rows}
</div>
{warning}
</div>
);
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { observer } from 'mobx-react';
import { Tooltip } from 'react-tooltip';
import classNames from 'classnames';
import './killchain.scss';
@@ -12,10 +13,6 @@ class KillChain extends React.Component {
this.populatePhase = this.populatePhase.bind(this);
}
componentDidMount() {
}
onChangePhaseHandler(event) {
const kcDomName = `kc-name-${this.props.node.id}`;
const phaseDomName = `phase-${this.props.node.id}`;
@@ -64,6 +61,9 @@ class KillChain extends React.Component {
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}`;
@@ -81,7 +81,10 @@ class KillChain extends React.Component {
</span>
<Tooltip id={`${field}-tooltip`} />
</div>
<div className="kill-chain-body">
<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>
@@ -112,12 +115,13 @@ class KillChain extends React.Component {
{' '}
{p.phase_name}
{' '}
<span onClick={() => this.props.onClickRemoveHandler(field, p)} className="material-icons">highlight_off</span>
<span onClick={() => this.props.onClickRemoveHandler(field, i)} className="material-icons">highlight_off</span>
</div>
</div>
))
}
</div>
{warning}
</div>
);
}

View File

@@ -2,6 +2,7 @@ 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';
@@ -16,8 +17,6 @@ class ObjectArray extends React.Component {
this.onClickDeletePropertyHandler = this.onClickDeletePropertyHandler.bind(this);
}
componentDidMount() {}
onChangeArrayObjectHandler(event, value) {
return undefined;
}
@@ -49,6 +48,9 @@ class ObjectArray extends React.Component {
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">
@@ -72,7 +74,10 @@ class ObjectArray extends React.Component {
<Tooltip id={`${field}-tooltip`} />
<Tooltip id={`add-${field}-tooltip`} />
</div>
<div className="er-body">
<div className={classNames({
"er-body": true,
"invalid": invalid,
})}>
{value.map((p, i) => (
<ObjectBlock
key={i}
@@ -86,6 +91,7 @@ class ObjectArray extends React.Component {
/>
))}
</div>
{warning}
</div>
);
}

View File

@@ -1,4 +1,4 @@
@import '../../../defaults';
@use '../../../defaults';
.ct-container {
padding-left: 20px;
@@ -7,7 +7,7 @@
.ct-header {
font-weight: bold;
color: $default-active-bg;
color: defaults.$default-active-bg;
padding-bottom: 3px;
span {
@@ -19,7 +19,7 @@
.ct-body {
overflow-x: hidden;
border: 1px solid $standard-border-color;
border: 1px solid defaults.$standard-border-color;
min-height: 50px;
width: 100%;
@@ -46,7 +46,7 @@
span {
cursor: pointer;
color: $default-active-bg;
color: defaults.$default-active-bg;
padding-left: 5px;
}
}
@@ -81,12 +81,12 @@
}
.remove {
color: $error-font;
color: defaults.$error-font;
}
.add {
padding-top: 15px;
color: $default-active-bg;
color: defaults.$default-active-bg;
}
}
}

View File

@@ -1,4 +1,4 @@
@import '../../../defaults';
@use '../../../defaults';
.er-container {
padding-left: 20px;
@@ -6,7 +6,7 @@
.er-header {
font-weight: bold;
color: $default-active-bg;
color: defaults.$default-active-bg;
padding-bottom: 3px;
span {
@@ -18,11 +18,11 @@
.er-body {
overflow: auto;
border: 1px solid $standard-border-color;
border: 1px solid defaults.$standard-border-color;
min-height: 50px;
.er-block {
border: 1px solid $standard-border-color;
border: 1px solid defaults.$standard-border-color;
margin: 10px;
.er-block-row {
@@ -43,7 +43,7 @@
}
.remove {
color: $error-font;
color: defaults.$error-font;
}
.remove-er {
@@ -53,7 +53,7 @@
.add {
padding-top: 15px;
color: $default-active-bg;
color: defaults.$default-active-bg;
}
}
}

View File

@@ -1,4 +1,4 @@
@import '../../../defaults';
@use '../../../defaults';
.go-container {
padding-left: 20px;
@@ -7,7 +7,7 @@
.go-header {
font-weight: bold;
color: $default-active-bg;
color: defaults.$default-active-bg;
padding-bottom: 3px;
span {
@@ -19,7 +19,7 @@
.go-body {
overflow-x: hidden;
border: 1px solid $standard-border-color;
border: 1px solid defaults.$standard-border-color;
min-height: 50px;
width: 100%;
@@ -41,7 +41,7 @@
span {
cursor: pointer;
color: $default-active-bg;
color: defaults.$default-active-bg;
padding-left: 5px;
}
}
@@ -71,12 +71,12 @@
}
.remove {
color: $error-font;
color: defaults.$error-font;
}
.add {
padding-top: 15px;
color: $default-active-bg;
color: defaults.$default-active-bg;
}
}
}

View File

@@ -1,4 +1,4 @@
@import '../../../defaults';
@use '../../../defaults';
.kill-chain-container {
padding-left: 20px;
@@ -6,7 +6,7 @@
.kill-chain-header {
font-weight: bold;
color: $default-active-bg;
color: defaults.$default-active-bg;
padding-bottom: 3px;
span {
@@ -19,7 +19,7 @@
.kill-chain-body {
overflow: auto;
border: 1px solid $standard-border-color;
border: 1px solid defaults.$standard-border-color;
.kill-chain-options {
display: flex;
@@ -35,7 +35,7 @@
padding: 5px 5px 5px 10px;
.material-icons {
vertical-align: middle;
color: $error-font;
color: defaults.$error-font;
cursor: pointer;
}
}

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

@@ -10,8 +10,6 @@ class ArraySelector extends React.Component {
super(props);
}
componentDidMount() {}
onClickHandler(field, value) {
this.props.onClickHandler(field, value);
}
@@ -19,8 +17,11 @@ class ArraySelector extends React.Component {
render() {
const items = this.props.vocab ? this.props.vocab : [];
const { field, } = this.props;
const { value, } = 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,
@@ -40,7 +41,10 @@ class ArraySelector extends React.Component {
</span>
<Tooltip id={`${field}-tooltip`} />
</div>
<div className="array-container-body">
<div className={classNames({
"array-container-body": true,
"invalid": invalid
})}>
{items.map((item, i) => {
if (value && value.indexOf(item) > -1) {
cls = classNames({
@@ -63,6 +67,7 @@ class ArraySelector extends React.Component {
);
})}
</div>
{warning}
</div>
);
}

View File

@@ -9,10 +9,6 @@ class Boolean extends React.Component {
super(props);
}
componentDidMount() {
}
onClickHandler(field, value) {
this.props.onClickHandler(field, value);
}

View File

@@ -9,21 +9,18 @@ class CSVInput extends React.Component {
super(props);
}
componentDidMount() {
}
onClickHandler(field, value) {
this.props.onClickHandler(field, value);
}
render() {
const value = this.props.value ? this.props.value.join() : '';
const value = this.props.value ?? "";
return (
<Text
name={this.props.name}
value={value}
required={this.props.required}
onChange={this.props.onChangeHandler}
/>
);

View File

@@ -1,6 +1,8 @@
import React from 'react';
import DatePicker from 'react-datepicker';
import Text from './Text.jsx';
import 'react-datepicker/dist/react-datepicker.css';
import './datetime.scss';
@@ -12,15 +14,24 @@ export default class DateTime extends React.Component {
}
onChange(datetime) {
this.props.onChange(this.props.name, 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);
dts = dateObj;
if (isNaN(dateObj.getTime())) {
return control;
} else {
dts = dateObj;
}
}
return (

View File

@@ -1,6 +1,8 @@
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';
@@ -36,6 +38,10 @@ class LabeledText extends React.Component {
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>
@@ -44,14 +50,18 @@ class LabeledText extends React.Component {
type={inputType}
ref={(c) => { this.input = c; }}
autoComplete={this.props.autocomplete || 'off'}
className="def"
className={classNames({
"def": true,
"invalid": invalid
})}
placeholder={this.props.placeholder}
onChange={this.onChangeHandler}
onKeyDown={(e) => this.onKeyDownHandler(e)}
value={this.props.value}
value={value}
disabled={this.props.disabled}
id={this.props.id}
/>
{warning}
</div>
);
}

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

@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { observer } from 'mobx-react';
import classNames from 'classnames';
import './text.scss';
@@ -35,8 +36,11 @@ class Text extends React.Component {
}
render() {
const inputType = this.props.type ? this.props.type : 'text';
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
@@ -44,14 +48,18 @@ class Text extends React.Component {
type={inputType}
ref={(c) => { this.input = c; }}
autoComplete={this.props.autocomplete || 'off'}
className="def"
className={classNames({
"def": true,
"invalid": invalid
})}
placeholder={this.props.placeholder}
onChange={this.onChangeHandler}
onKeyDown={(e) => this.onKeyDownHandler(e)}
value={this.props.value}
value={value}
disabled={this.props.disabled}
id={this.props.id}
/>
{warning}
</div>
);
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { observer } from 'mobx-react';
import classNames from 'classnames';
import './text.scss';
@@ -38,7 +39,10 @@ class TextArea extends React.Component {
}
render() {
const rows = this.props.rows ? this.props.rows : 1;
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>
@@ -48,14 +52,18 @@ class TextArea extends React.Component {
this.input = c;
}}
autoComplete={this.props.autocomplete || 'off'}
className="def"
className={classNames({
"def": true,
"invalid": invalid
})}
placeholder={this.props.placeholder}
onChange={this.onChangeHandler}
onKeyDown={(e) => this.onKeyDownHandler(e)}
value={this.props.value}
value={value}
disabled={this.props.disabled}
id={this.props.id}
/>
{warning}
</div>
);
}

View File

@@ -1,4 +1,4 @@
@import '../../../defaults';
@use '../../../defaults';
.array-container {
padding-left: 20px;
@@ -6,7 +6,7 @@
.array-container-header {
font-weight: bold;
color: $default-active-bg;
color: defaults.$default-active-bg;
padding-bottom: 3px;
span {
@@ -21,12 +21,12 @@
}
.remove {
color: $error-font;
color: defaults.$error-font;
}
.add {
padding-top: 15px;
color: $default-active-bg;
color: defaults.$default-active-bg;
}
.array-container-input {
@@ -51,7 +51,7 @@
.array-container-body {
height: 100px;
overflow: auto;
border: 1px solid $standard-border-color;
border: 1px solid defaults.$standard-border-color;
.array-container-item {
cursor: pointer;
@@ -60,11 +60,11 @@
}
.array-container-selected {
background-color: $default-active-bg;
background-color: defaults.$default-active-bg;
}
.array-container-item:hover {
background-color: $light-font-0;
background-color: defaults.$light-font-0;
}
.array-block-input {
@@ -83,7 +83,7 @@
span {
cursor: pointer;
color: $default-active-bg;
color: defaults.$default-active-bg;
padding-left: 5px;
padding-top: 40%;
}

View File

@@ -1,10 +1,10 @@
@import '../../../defaults';
@use '../../../defaults';
.boolean {
display: flex;
flex-direction: row;
line-height: 30px;
border: 1px solid $standard-border-color;
border: 1px solid defaults.$standard-border-color;
width: 99%;
div {
@@ -14,7 +14,7 @@
}
.selected {
background-color: $default-active-bg;
background-color: defaults.$default-active-bg;
}
}

View File

@@ -1,4 +1,4 @@
@import '../../../defaults';
@use '../../../defaults';
.react-datepicker-wrapper {
width: 100%;
@@ -6,11 +6,11 @@
.react-datepicker__input-container input {
height: 39px;
border: 1px solid $standard-border-color;
border: 1px solid defaults.$standard-border-color;
background-color: transparent;
width: 97%;
padding-left: 10px;
font-family: $default-font-family;
font-family: defaults.$default-font-family;
color: #000;
font-size: 16px;
font-weight: bold;

View File

@@ -1,14 +1,14 @@
@import '../../../defaults';
@use '../../../defaults';
input.def {
height: 39px;
border: 1px solid $standard-border-color;
border: 1px solid defaults.$standard-border-color;
background-color: transparent;
width: 97%;
padding-left: 10px;
color: $dark-font-0;
color: defaults.$dark-font-0;
font-size: 16px;
font-family: $default-font-family;
font-family: defaults.$default-font-family;
}
.custom-file-selector {
@@ -16,5 +16,5 @@ input.def {
padding: 5px 4px 5px 11px;
color: #fff;
font-weight: bold;
background-color: $default-active-bg;
background-color: defaults.$default-active-bg;
}

View File

@@ -1,4 +1,4 @@
@import '../../../defaults';
@use '../../../defaults';
.rc-slider-track {
background-color: transparet;

View File

@@ -1,4 +1,4 @@
@import '../../../defaults';
@use '../../../defaults';
input:focus,
textarea:focus,
@@ -10,13 +10,17 @@ input.def,
textarea,
select {
height: 39px;
border: 1px solid $standard-border-color;
border: 1px solid defaults.$standard-border-color;
background-color: transparent;
width: 97%;
padding-left: 10px;
color: $dark-font-0;
color: defaults.$dark-font-0;
font-size: 16px;
font-family: $default-font-family;
font-family: defaults.$default-font-family;
}
input.def {
background-color: white;
}
textarea {

View File

@@ -1,4 +1,4 @@
@import '../../../defaults';
@use '../../../defaults';
.mask {
position: fixed;
@@ -7,7 +7,7 @@
right: 0px;
bottom: 0px;
background-color: rgba(0, 0, 0,.2);
z-index: $panel-mask-index;
z-index: defaults.$panel-mask-index;
.panel {
position: absolute;
@@ -17,7 +17,7 @@
bottom: 0px;
background-color: #fff;
box-shadow: -20px 25px 50px 0px #000;
z-index: $panel-index;
z-index: defaults.$panel-index;
display: flex;
flex-direction: column;
}

View File

@@ -3,6 +3,7 @@ $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;

View File

@@ -20,10 +20,7 @@ class Artifact extends Base {
this.properties.payload_bin.type = 'string';
this.properties.url.type = 'string';
this.properties.encryption_algorithm.type = 'string';
this.properties.hashes.value = {};
this.properties.hashes.control = 'genericobject';
this.properties.encryption_algorithm.vocab = this.definitions["encryption-algorithm-enum"].enum;
}
}

View File

@@ -11,6 +11,7 @@ class AttackPattern extends Base {
prefix: 'attack-pattern--',
active: true,
relationships: [
{ type: 'delivers', target: 'malware', },
{ type: 'targets', target: 'identity', },
{ type: 'targets', target: 'location', },
{ type: 'targets', target: 'vulnerability', },

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

@@ -17,9 +17,6 @@ class Certificate extends Base {
super(common, def);
this.properties.hashes.value = {};
this.properties.hashes.control = 'genericobject';
this.properties.x509_v3_extensions.type = 'string';
}
}

View File

@@ -24,12 +24,7 @@ class Custom extends Base {
const def = deepmerge(definition_extension, rawDefinition);
super(common, def);
const extProps = { extension_type: extensionDefinition.extension_types[0], };
this.properties.extensions = {};
this.properties.extensions.type = 'object';
this.properties.extensions.value = {};
this.properties.extensions.value[extensionDefinition.id] = extProps;
this.properties.extensions.control = 'hidden';
this.extensions.push(extensionDefinition.uiid);
}
}

Some files were not shown because too many files have changed in this diff Show More