aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml1
-rw-r--r--application_router.go9
-rw-r--r--frontend/package.json3
-rw-r--r--frontend/public/favicon.icobin3150 -> 34239 bytes
-rw-r--r--frontend/public/logo192.pngbin5347 -> 34239 bytes
-rw-r--r--frontend/public/logo512.pngbin9664 -> 34239 bytes
-rw-r--r--frontend/src/backend.js42
-rw-r--r--frontend/src/colors.scss32
-rw-r--r--frontend/src/components/Connection.js59
-rw-r--r--frontend/src/components/Connection.scss4
-rw-r--r--frontend/src/components/ConnectionContent.js100
-rw-r--r--frontend/src/components/ConnectionContent.scss12
-rw-r--r--frontend/src/components/ConnectionMatchedRules.js29
-rw-r--r--frontend/src/components/ConnectionMatchedRules.scss23
-rw-r--r--frontend/src/components/MessageAction.js13
-rw-r--r--frontend/src/components/fields/ButtonField.js49
-rw-r--r--frontend/src/components/fields/ButtonField.scss123
-rw-r--r--frontend/src/components/fields/CheckField.js37
-rw-r--r--frontend/src/components/fields/CheckField.scss39
-rw-r--r--frontend/src/components/fields/ChoiceField.js68
-rw-r--r--frontend/src/components/fields/ChoiceField.scss69
-rw-r--r--frontend/src/components/fields/InputField.js78
-rw-r--r--frontend/src/components/fields/InputField.scss125
-rw-r--r--frontend/src/components/fields/TextField.js43
-rw-r--r--frontend/src/components/fields/TextField.scss54
-rw-r--r--frontend/src/components/fields/common.scss54
-rw-r--r--frontend/src/components/fields/extensions/ColorField.js82
-rw-r--r--frontend/src/components/fields/extensions/ColorField.scss46
-rw-r--r--frontend/src/components/fields/extensions/NumericField.js45
-rw-r--r--frontend/src/components/filters/BooleanConnectionsFilter.js12
-rw-r--r--frontend/src/components/filters/BooleanConnectionsFilter.scss24
-rw-r--r--frontend/src/components/filters/FiltersDefinitions.js45
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.js6
-rw-r--r--frontend/src/components/filters/StringConnectionsFilter.js31
-rw-r--r--frontend/src/components/filters/StringConnectionsFilter.scss66
-rw-r--r--frontend/src/components/objects/LinkPopover.js33
-rw-r--r--frontend/src/components/objects/LinkPopover.scss7
-rw-r--r--frontend/src/components/panels/ConfigurationPane.js162
-rw-r--r--frontend/src/components/panels/ConfigurationPane.scss19
-rw-r--r--frontend/src/components/panels/MainPane.js56
-rw-r--r--frontend/src/components/panels/MainPane.scss23
-rw-r--r--frontend/src/components/panels/PcapPane.js251
-rw-r--r--frontend/src/components/panels/PcapPane.scss39
-rw-r--r--frontend/src/components/panels/RulePane.js413
-rw-r--r--frontend/src/components/panels/RulePane.scss32
-rw-r--r--frontend/src/components/panels/ServicePane.js190
-rw-r--r--frontend/src/components/panels/ServicePane.scss22
-rw-r--r--frontend/src/components/panels/common.scss101
-rw-r--r--frontend/src/index.scss102
-rw-r--r--frontend/src/utils.js55
-rw-r--r--frontend/src/validation.js8
-rw-r--r--frontend/src/views/App.js51
-rw-r--r--frontend/src/views/App.scss15
-rw-r--r--frontend/src/views/Connections.js83
-rw-r--r--frontend/src/views/Filters.js6
-rw-r--r--frontend/src/views/Footer.js4
-rw-r--r--frontend/src/views/Header.js30
-rw-r--r--frontend/src/views/Header.scss19
-rw-r--r--frontend/src/views/MainPane.js46
-rw-r--r--frontend/src/views/MainPane.scss6
-rw-r--r--frontend/src/views/Rules.js118
-rw-r--r--frontend/src/views/Services.js210
-rw-r--r--frontend/src/views/Services.scss32
-rw-r--r--frontend/yarn.lock218
-rw-r--r--rules_manager.go2
65 files changed, 2801 insertions, 975 deletions
diff --git a/.travis.yml b/.travis.yml
index 1d66f6e..b59b12f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,7 +5,6 @@ services:
- docker
before_script:
- - docker pull eciavatta/caronte-env
- docker pull mongo:4
- docker-compose -f docker-compose.testing.yml up -d --build
diff --git a/application_router.go b/application_router.go
index 852e68b..501956b 100644
--- a/application_router.go
+++ b/application_router.go
@@ -20,9 +20,12 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine
router.MaxMultipartMemory = 8 << 30
router.Use(static.Serve("/", static.LocalFile("./frontend/build", true)))
- router.GET("/connections/:id", func(c *gin.Context) {
- c.File("./frontend/build/index.html")
- })
+
+ for _, path := range []string{"/connections/:id", "/pcaps", "/rules", "/services", "/config"} {
+ router.GET(path, func(c *gin.Context) {
+ c.File("./frontend/build/index.html")
+ })
+ }
router.POST("/setup", func(c *gin.Context) {
if applicationContext.IsConfigured {
diff --git a/frontend/package.json b/frontend/package.json
index 661c2c4..b13a7ea 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,10 +9,11 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
- "axios": "^0.19.2",
"bootstrap": "^4.4.1",
+ "bs-custom-file-input": "^1.3.4",
"classnames": "^2.2.6",
"eslint-config-react-app": "^5.2.1",
+ "lodash": "^4.17.20",
"node-sass": "^4.14.0",
"react": "^16.13.1",
"react-bootstrap": "^1.0.1",
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
index bcd5dfd..1dc499d 100644
--- a/frontend/public/favicon.ico
+++ b/frontend/public/favicon.ico
Binary files differ
diff --git a/frontend/public/logo192.png b/frontend/public/logo192.png
index fc44b0a..1dc499d 100644
--- a/frontend/public/logo192.png
+++ b/frontend/public/logo192.png
Binary files differ
diff --git a/frontend/public/logo512.png b/frontend/public/logo512.png
index a4e47a6..1dc499d 100644
--- a/frontend/public/logo512.png
+++ b/frontend/public/logo512.png
Binary files differ
diff --git a/frontend/src/backend.js b/frontend/src/backend.js
new file mode 100644
index 0000000..72ee9dd
--- /dev/null
+++ b/frontend/src/backend.js
@@ -0,0 +1,42 @@
+
+async function json(method, url, data, json, headers) {
+ const options = {
+ method: method,
+ body: json != null ? JSON.stringify(json) : data,
+ mode: "cors",
+ cache: "no-cache",
+ credentials: "same-origin",
+ headers: headers || {
+ "Content-Type": "application/json"
+ },
+ redirect: "follow",
+ referrerPolicy: "no-referrer",
+ };
+ const response = await fetch(url, options);
+ const result = {
+ statusCode: response.status,
+ status: `${response.status} ${response.statusText}`,
+ json: await response.json()
+ };
+
+ if (response.status >= 200 && response.status < 300) {
+ return result;
+ } else {
+ return Promise.reject(result);
+ }
+}
+
+const backend = {
+ get: (url = "", headers = null) =>
+ json("GET", url, null,null, headers),
+ post: (url = "", data = null, headers = null) =>
+ json("POST", url, null, data, headers),
+ put: (url = "", data = null, headers = null) =>
+ json("PUT", url, null, data, headers),
+ delete: (url = "", data = null, headers = null) =>
+ json("DELETE", url, null, data, headers),
+ postFile: (url = "", data = null, headers = {}) =>
+ json("POST", url, data, null, headers)
+};
+
+export default backend;
diff --git a/frontend/src/colors.scss b/frontend/src/colors.scss
index 064f9f5..d982f03 100644
--- a/frontend/src/colors.scss
+++ b/frontend/src/colors.scss
@@ -10,10 +10,30 @@ $color-secondary-2: #df3030;
$color-secondary-3: #ff9d9d;
$color-secondary-4: #ffdfdf;
-$color-blue: #247085;
-$color-blue-light: #a5b8be;
-$color-blue-dark: #013b4c;
+$color-red: #E53935;
+$color-red-light: #FFEBEE;
+$color-red-dark: #B71C1C;
-$color-green: #25965d;
-$color-green-light: #cde4d8;
-$color-green-dark: #004321;
+$color-pink: #D81B60;
+$color-pink-light: #FCE4EC;
+$color-pink-dark: #880E4F;
+
+$color-purple: #8E24AA;
+$color-purple-light: #F3E5F5;
+$color-purple-dark: #4A148C;
+
+$color-deep-purple: #5E35B1;
+$color-deep-purple-light: #EDE7F6;
+$color-deep-purple-dark: #311B92;
+
+$color-indigo: #3949AB;
+$color-indigo-light: #E8EAF6;
+$color-indigo-dark: #1A237E;
+
+$color-green: #43A047;
+$color-green-light: #E8F5E9;
+$color-green-dark: #1B5E20;
+
+$color-blue: #1E88E5;
+$color-blue-light: #E3F2FD;
+$color-blue-dark: #0D47A1;
diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js
index 93c6438..44f9f18 100644
--- a/frontend/src/components/Connection.js
+++ b/frontend/src/components/Connection.js
@@ -1,7 +1,11 @@
import React, {Component} from 'react';
import './Connection.scss';
-import axios from 'axios'
-import {Button, Form, OverlayTrigger, Popover} from "react-bootstrap";
+import {Form, OverlayTrigger, Popover} from "react-bootstrap";
+import backend from "../backend";
+import {dateTimeToTime, durationBetween, formatSize} from "../utils";
+import ButtonField from "./fields/ButtonField";
+
+const classNames = require('classnames');
class Connection extends Component {
@@ -19,22 +23,18 @@ class Connection extends Component {
handleAction(name) {
if (name === "hide") {
const enabled = !this.props.data.hidden;
- axios.post(`/api/connections/${this.props.data.id}/${enabled ? "hide" : "show"}`)
- .then(res => {
- if (res.status === 202) {
- this.props.onEnabled(!enabled);
- this.setState({update: true});
- }
+ backend.post(`/api/connections/${this.props.data.id}/${enabled ? "hide" : "show"}`)
+ .then(_ => {
+ this.props.onEnabled(!enabled);
+ this.setState({update: true});
});
}
if (name === "mark") {
const marked = this.props.data.marked;
- axios.post(`/api/connections/${this.props.data.id}/${marked ? "unmark" : "mark"}`)
- .then(res => {
- if (res.status === 202) {
- this.props.onMarked(!marked);
- this.setState({update: true});
- }
+ backend.post(`/api/connections/${this.props.data.id}/${marked ? "unmark" : "mark"}`)
+ .then(_ => {
+ this.props.onMarked(!marked);
+ this.setState({update: true});
});
}
if (name === "copy") {
@@ -56,26 +56,12 @@ class Connection extends Component {
let startedAt = new Date(conn.started_at);
let closedAt = new Date(conn.closed_at);
let processedAt = new Date(conn.processed_at);
- let duration = ((closedAt - startedAt) / 1000).toFixed(3);
- if (duration > 1000 || duration < -1000) {
- duration = "∞";
- } else {
- duration += "s";
- }
let timeInfo = <div>
<span>Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}</span><br/>
<span>Processed at {processedAt.toLocaleDateString() + " " + processedAt.toLocaleTimeString()}</span><br/>
<span>Closed at {closedAt.toLocaleDateString() + " " + closedAt.toLocaleTimeString()}</span>
</div>;
- let classes = "connection";
- if (this.props.selected) {
- classes += " connection-selected";
- }
- if (this.props.containsFlag) {
- classes += " contains-flag";
- }
-
const popoverFor = function (name, content) {
return <Popover id={`popover-${name}-${conn.id}`} className="connection-popover">
<Popover.Content>
@@ -96,27 +82,28 @@ class Connection extends Component {
</div>;
return (
- <tr className={classes}>
+ <tr className={classNames("connection", {"connection-selected": this.props.selected},
+ {"has-matched-rules": conn.matched_rules.length > 0})}>
<td>
<span className="connection-service">
- <Button size="sm" style={{
- "backgroundColor": serviceColor
- }} onClick={() => this.props.addServicePortFilter(conn.port_dst)}>{serviceName}</Button>
+ <ButtonField small fullSpan color={serviceColor} name={serviceName}
+ onClick={() => this.props.addServicePortFilter(conn.port_dst)} />
</span>
</td>
<td className="clickable" onClick={this.props.onSelected}>{conn.ip_src}</td>
<td className="clickable" onClick={this.props.onSelected}>{conn.port_src}</td>
<td className="clickable" onClick={this.props.onSelected}>{conn.ip_dst}</td>
<td className="clickable" onClick={this.props.onSelected}>{conn.port_dst}</td>
+ <td className="clickable" onClick={this.props.onSelected}>{dateTimeToTime(conn.started_at)}</td>
<td className="clickable" onClick={this.props.onSelected}>
<OverlayTrigger trigger={["focus", "hover"]} placement="right"
overlay={popoverFor("duration", timeInfo)}>
- <span className="test-tooltip">{duration}</span>
+ <span className="test-tooltip">{durationBetween(startedAt, closedAt)}</span>
</OverlayTrigger>
</td>
- <td className="clickable" onClick={this.props.onSelected}>{conn.client_bytes}</td>
- <td className="clickable" onClick={this.props.onSelected}>{conn.server_bytes}</td>
- <td className="contains-flag">
+ <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn.client_bytes)}</td>
+ <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn.server_bytes)}</td>
+ <td>
{/*<OverlayTrigger trigger={["focus", "hover"]} placement="right"*/}
{/* overlay={popoverFor("hide", <span>Hide this connection from the list</span>)}>*/}
{/* <span className={"connection-icon" + (conn.hidden ? " icon-enabled" : "")}*/}
diff --git a/frontend/src/components/Connection.scss b/frontend/src/components/Connection.scss
index cb9eb5f..97ef0a4 100644
--- a/frontend/src/components/Connection.scss
+++ b/frontend/src/components/Connection.scss
@@ -39,8 +39,8 @@
background-color: $color-primary-2;
}
- &.contains-flag {
- border-right: 3px solid $color-secondary-2;
+ &.has-matched-rules {
+ border-bottom: 0;
}
}
diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js
index 20ec92b..ccaec0b 100644
--- a/frontend/src/components/ConnectionContent.js
+++ b/frontend/src/components/ConnectionContent.js
@@ -1,8 +1,10 @@
import React, {Component} from 'react';
import './ConnectionContent.scss';
-import {Button, Dropdown, Row} from 'react-bootstrap';
-import axios from 'axios';
+import {Row} from 'react-bootstrap';
import MessageAction from "./MessageAction";
+import backend from "../backend";
+import ButtonField from "./fields/ButtonField";
+import ChoiceField from "./fields/ChoiceField";
const classNames = require('classnames');
@@ -22,20 +24,30 @@ class ConnectionContent extends Component {
this.setFormat = this.setFormat.bind(this);
}
+ componentDidMount() {
+ if (this.props.connection != null) {
+ this.loadStream();
+ }
+ }
+
componentDidUpdate(prevProps, prevState, snapshot) {
- if (this.props.connection !== null && (
+ if (this.props.connection != null && (
this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) {
- this.setState({loading: true});
- // TODO: limit workaround.
- axios.get(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => {
- this.setState({
- connectionContent: res.data,
- loading: false
- });
- });
+ this.loadStream();
}
}
+ loadStream = () => {
+ this.setState({loading: true});
+ // TODO: limit workaround.
+ backend.get(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => {
+ this.setState({
+ connectionContent: res.json,
+ loading: false
+ });
+ });
+ };
+
setFormat(format) {
if (this.validFormats.includes(format)) {
this.setState({format: format});
@@ -51,7 +63,7 @@ class ConnectionContent extends Component {
}
let unrollMap = (obj) => obj == null ? null : Object.entries(obj).map(([key, value]) =>
- <p><strong>{key}</strong>: {value}</p>
+ <p key={key}><strong>{key}</strong>: {value}</p>
);
let m = connectionMessage.metadata;
@@ -83,12 +95,12 @@ class ConnectionContent extends Component {
}
return Object.entries(connectionMessage.metadata["reproducers"]).map(([actionName, actionValue]) =>
- <Button size="sm" key={actionName + "_button"} onClick={() => {
+ <ButtonField small key={actionName + "_button"} name={actionName} onClick={() => {
this.setState({
messageActionDialog: <MessageAction actionName={actionName} actionValue={actionValue}
onHide={() => this.setState({messageActionDialog: null})}/>
});
- }}>{actionName}</Button>
+ }} />
);
}
@@ -128,56 +140,16 @@ class ConnectionContent extends Component {
<span> | <strong>timestamp</strong>: {this.props.connection.started_at}</span>
</div>
<div className="header-actions col-auto">
- <Dropdown onSelect={this.setFormat}>
- <Dropdown.Toggle size="sm" id="connection-content-format">
- format
- </Dropdown.Toggle>
-
- <Dropdown.Menu>
- <Dropdown.Item eventKey="default"
- active={this.state.format === "default"}>plain</Dropdown.Item>
- <Dropdown.Item eventKey="hex"
- active={this.state.format === "hex"}>hex</Dropdown.Item>
- <Dropdown.Item eventKey="hexdump"
- active={this.state.format === "hexdump"}>hexdump</Dropdown.Item>
- <Dropdown.Item eventKey="base32"
- active={this.state.format === "base32"}>base32</Dropdown.Item>
- <Dropdown.Item eventKey="base64"
- active={this.state.format === "base64"}>base64</Dropdown.Item>
- <Dropdown.Item eventKey="ascii"
- active={this.state.format === "ascii"}>ascii</Dropdown.Item>
- <Dropdown.Item eventKey="binary"
- active={this.state.format === "binary"}>binary</Dropdown.Item>
- <Dropdown.Item eventKey="decimal"
- active={this.state.format === "decimal"}>decimal</Dropdown.Item>
- <Dropdown.Item eventKey="octal"
- active={this.state.format === "octal"}>octal</Dropdown.Item>
- </Dropdown.Menu>
- </Dropdown>
-
- <Dropdown>
- <Dropdown.Toggle size="sm" id="connection-content-view">
- view_as
- </Dropdown.Toggle>
-
- <Dropdown.Menu>
- <Dropdown.Item eventKey="default" active={true}>default</Dropdown.Item>
- </Dropdown.Menu>
-
- </Dropdown>
-
- <Dropdown>
- <Dropdown.Toggle size="sm" id="connection-content-download">
- download_as
- </Dropdown.Toggle>
-
- <Dropdown.Menu>
- <Dropdown.Item eventKey="nl_separated">nl_separated</Dropdown.Item>
- <Dropdown.Item eventKey="only_client">only_client</Dropdown.Item>
- <Dropdown.Item eventKey="only_server">only_server</Dropdown.Item>
- </Dropdown.Menu>
-
- </Dropdown>
+ <ChoiceField name="format" inline small onlyName
+ keys={["default", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"]}
+ values={["plain", "hex", "hexdump", "base32", "base64", "ascii", "binary", "decimal", "octal"]}
+ onChange={this.setFormat} value={this.state.value} />
+
+ <ChoiceField name="view_as" inline small onlyName keys={["default"]} values={["default"]} />
+
+ <ChoiceField name="download_as" inline small onlyName
+ keys={["nl_separated", "only_client", "only_server"]}
+ values={["nl_separated", "only_client", "only_server"]} />
</div>
</Row>
</div>
diff --git a/frontend/src/components/ConnectionContent.scss b/frontend/src/components/ConnectionContent.scss
index 8ee31ec..fde54c0 100644
--- a/frontend/src/components/ConnectionContent.scss
+++ b/frontend/src/components/ConnectionContent.scss
@@ -51,7 +51,7 @@
}
&:hover .connection-message-actions {
- display: block;
+ display: flex;
}
.connection-message-label {
@@ -89,17 +89,19 @@
.connection-content-header {
background-color: $color-primary-2;
padding: 0;
- height: 31px;
+ height: 33px;
.header-info {
- padding-top: 5px;
+ padding-top: 7px;
padding-left: 20px;
font-size: 12px;
}
.header-actions {
- .dropdown {
- display: inline-block;
+ display: flex;
+
+ .choice-field {
+ margin-top: -5px;
}
}
}
diff --git a/frontend/src/components/ConnectionMatchedRules.js b/frontend/src/components/ConnectionMatchedRules.js
new file mode 100644
index 0000000..21f2a92
--- /dev/null
+++ b/frontend/src/components/ConnectionMatchedRules.js
@@ -0,0 +1,29 @@
+import React, {Component} from 'react';
+import './ConnectionMatchedRules.scss';
+import ButtonField from "./fields/ButtonField";
+
+class ConnectionMatchedRules extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ };
+ }
+
+ render() {
+ const matchedRules = this.props.matchedRules.map(mr => {
+ const rule = this.props.rules.find(r => r.id === mr);
+ return <ButtonField key={mr} onClick={() => this.props.addMatchedRulesFilter(rule.id)} name={rule.name}
+ color={rule.color} small />;
+ });
+
+ return (
+ <tr className="connection-matches">
+ <td className="row-label">matched_rules:</td>
+ <td className="rule-buttons" colSpan={9}>{matchedRules}</td>
+ </tr>
+ );
+ }
+}
+
+export default ConnectionMatchedRules;
diff --git a/frontend/src/components/ConnectionMatchedRules.scss b/frontend/src/components/ConnectionMatchedRules.scss
new file mode 100644
index 0000000..ed18f3c
--- /dev/null
+++ b/frontend/src/components/ConnectionMatchedRules.scss
@@ -0,0 +1,23 @@
+@import '../colors.scss';
+
+.connection-matches {
+ background-color: $color-primary-0;
+
+ .rule-buttons {
+ padding: 0;
+ }
+
+ .button-field {
+ display: inline-block;
+ margin-right: 5px;
+
+ button {
+ font-size: 0.8em;
+ padding: 2px 10px;
+ }
+ }
+
+ .row-label {
+ font-size: 0.8em;
+ }
+}
diff --git a/frontend/src/components/MessageAction.js b/frontend/src/components/MessageAction.js
index 2c85d84..8f4b031 100644
--- a/frontend/src/components/MessageAction.js
+++ b/frontend/src/components/MessageAction.js
@@ -1,6 +1,8 @@
import React, {Component} from 'react';
import './MessageAction.scss';
-import {Button, FormControl, InputGroup, Modal} from "react-bootstrap";
+import {Modal} from "react-bootstrap";
+import TextField from "./fields/TextField";
+import ButtonField from "./fields/ButtonField";
class MessageAction extends Component {
@@ -35,14 +37,11 @@ class MessageAction extends Component {
</Modal.Title>
</Modal.Header>
<Modal.Body>
- <InputGroup>
- <FormControl as="textarea" className="message-action-value" readOnly={true}
- style={{"height": "300px"}} value={this.props.actionValue} ref={this.actionValue} />
- </InputGroup>
+ <TextField readonly value={this.props.actionValue} textRef={this.actionValue} rows={15} />
</Modal.Body>
<Modal.Footer className="dialog-footer">
- <Button variant="green" onClick={this.copyActionValue}>{this.state.copyButtonText}</Button>
- <Button variant="red" onClick={this.props.onHide}>close</Button>
+ <ButtonField variant="green" bordered onClick={this.copyActionValue} name={this.state.copyButtonText} />
+ <ButtonField variant="red" bordered onClick={this.props.onHide} name="close" />
</Modal.Footer>
</Modal>
);
diff --git a/frontend/src/components/fields/ButtonField.js b/frontend/src/components/fields/ButtonField.js
new file mode 100644
index 0000000..cc32b0f
--- /dev/null
+++ b/frontend/src/components/fields/ButtonField.js
@@ -0,0 +1,49 @@
+import React, {Component} from 'react';
+import './ButtonField.scss';
+import './common.scss';
+
+const classNames = require('classnames');
+
+class ButtonField extends Component {
+
+ render() {
+ const handler = () => {
+ if (typeof this.props.onClick === "function") {
+ this.props.onClick();
+ }
+ };
+
+ let buttonClassnames = {
+ "button-bordered": this.props.bordered,
+ };
+ if (this.props.variant) {
+ buttonClassnames[`button-variant-${this.props.variant}`] = true;
+ }
+
+ let buttonStyle = {};
+ if (this.props.color) {
+ buttonStyle["backgroundColor"] = this.props.color;
+ }
+ if (this.props.border) {
+ buttonStyle["borderColor"] = this.props.border;
+ }
+ if (this.props.fullSpan) {
+ buttonStyle["width"] = "100%";
+ }
+ if (this.props.rounded) {
+ buttonStyle["borderRadius"] = "3px";
+ }
+ if (this.props.inline) {
+ buttonStyle["marginTop"] = "8px";
+ }
+
+ return (
+ <div className={classNames( "field", "button-field", {"field-small": this.props.small})}>
+ <button type="button" className={classNames(classNames(buttonClassnames))}
+ onClick={handler} style={buttonStyle}>{this.props.name}</button>
+ </div>
+ );
+ }
+}
+
+export default ButtonField;
diff --git a/frontend/src/components/fields/ButtonField.scss b/frontend/src/components/fields/ButtonField.scss
new file mode 100644
index 0000000..cfd20ff
--- /dev/null
+++ b/frontend/src/components/fields/ButtonField.scss
@@ -0,0 +1,123 @@
+@import '../../colors.scss';
+
+.button-field {
+ font-size: 0.9em;
+
+ .button-bordered {
+ border-bottom: 5px solid $color-primary-1;
+ }
+
+ &.field-small {
+ font-size: 0.8em;
+
+ button {
+ padding: 3px 12px;
+ }
+ }
+
+ .button-variant-red {
+ color: $color-red-light;
+ background-color: $color-red;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-red-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-red-light;
+ background-color: $color-red-dark;
+ }
+ }
+
+ .button-variant-pink {
+ color: $color-pink-light;
+ background-color: $color-pink;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-pink-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-pink-light;
+ background-color: $color-pink-dark;
+ }
+ }
+
+ .button-variant-purple {
+ color: $color-purple-light;
+ background-color: $color-purple;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-purple-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-purple-light;
+ background-color: $color-purple-dark;
+ }
+ }
+
+ .button-variant-deep-purple {
+ color: $color-deep-purple-light;
+ background-color: $color-deep-purple;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-deep-purple-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-deep-purple-light;
+ background-color: $color-deep-purple-dark;
+ }
+ }
+
+ .button-variant-indigo {
+ color: $color-indigo-light;
+ background-color: $color-indigo;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-indigo-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-indigo-light;
+ background-color: $color-indigo-dark;
+ }
+ }
+
+ .button-variant-blue {
+ color: $color-blue-light;
+ background-color: $color-blue;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-blue-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-blue-light;
+ background-color: $color-blue-dark;
+ }
+ }
+
+ .button-variant-green {
+ color: $color-green-light;
+ background-color: $color-green;
+
+ &.button-bordered {
+ border-bottom: 5px solid $color-green-dark;
+ }
+
+ &:hover,
+ &:active {
+ color: $color-green-light;
+ background-color: $color-green-dark;
+ }
+ }
+
+}
diff --git a/frontend/src/components/fields/CheckField.js b/frontend/src/components/fields/CheckField.js
new file mode 100644
index 0000000..33f4f83
--- /dev/null
+++ b/frontend/src/components/fields/CheckField.js
@@ -0,0 +1,37 @@
+import React, {Component} from 'react';
+import './CheckField.scss';
+import './common.scss';
+import {randomClassName} from "../../utils";
+
+const classNames = require('classnames');
+
+class CheckField extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.id = `field-${this.props.name || "noname"}-${randomClassName()}`;
+ }
+
+ render() {
+ const checked = this.props.checked || false;
+ const small = this.props.small || false;
+ const name = this.props.name || null;
+ const handler = () => {
+ if (this.props.onChange) {
+ this.props.onChange(!checked);
+ }
+ };
+
+ return (
+ <div className={classNames( "field", "check-field", {"field-checked" : checked}, {"field-small": small})}>
+ <div className="field-input">
+ <input type="checkbox" id={this.id} checked={checked} onChange={handler} />
+ <label htmlFor={this.id}>{(checked ? "✓ " : "✗ ") + (name != null ? name : "")}</label>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default CheckField;
diff --git a/frontend/src/components/fields/CheckField.scss b/frontend/src/components/fields/CheckField.scss
new file mode 100644
index 0000000..ab932b4
--- /dev/null
+++ b/frontend/src/components/fields/CheckField.scss
@@ -0,0 +1,39 @@
+@import '../../colors.scss';
+
+.check-field {
+ font-size: 0.9em;
+ margin: 5px 0;
+
+ .field-input {
+ border-radius: 5px;
+ width: fit-content;
+ background-color: $color-primary-2;
+
+ input {
+ display: none;
+ }
+
+ label {
+ margin: 0;
+ padding: 6px 15px;
+ cursor: pointer;
+ }
+
+ &:hover {
+ background-color: $color-primary-1;
+ }
+ }
+
+ &.field-checked .field-input {
+ background-color: $color-primary-4 !important;
+ color: $color-primary-3;
+ }
+
+ &.field-small {
+ font-size: 0.8em;
+
+ .field-input label {
+ padding: 7px 15px;
+ }
+ }
+}
diff --git a/frontend/src/components/fields/ChoiceField.js b/frontend/src/components/fields/ChoiceField.js
new file mode 100644
index 0000000..73e950d
--- /dev/null
+++ b/frontend/src/components/fields/ChoiceField.js
@@ -0,0 +1,68 @@
+import React, {Component} from 'react';
+import './ChoiceField.scss';
+import './common.scss';
+import {randomClassName} from "../../utils";
+
+const classNames = require('classnames');
+
+class ChoiceField extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ expanded: false
+ };
+
+ this.id = `field-${this.props.name || "noname"}-${randomClassName()}`;
+ }
+
+ render() {
+ const name = this.props.name || null;
+ const inline = this.props.inline;
+
+ const collapse = () => this.setState({expanded: false});
+ const expand = () => this.setState({expanded: true});
+
+ const handler = (key) => {
+ collapse();
+ if (this.props.onChange) {
+ this.props.onChange(key);
+ }
+ };
+
+ const keys = this.props.keys || [];
+ const values = this.props.values || [];
+
+ const options = keys.map((key, i) =>
+ <span className="field-option" key={key} onClick={() => handler(key)}>{values[i]}</span>
+ );
+
+ let fieldValue = "";
+ if (inline && name) {
+ fieldValue = name;
+ }
+ if (!this.props.onlyName && inline && name) {
+ fieldValue += ": ";
+ }
+ if (!this.props.onlyName) {
+ fieldValue += this.props.value || "select a value";
+ }
+
+ return (
+ <div className={classNames( "field", "choice-field", {"field-inline" : inline},
+ {"field-small": this.props.small})}>
+ {!inline && name && <label className="field-name">{name}:</label>}
+ <div className={classNames("field-select", {"select-expanded": this.state.expanded})}
+ tabIndex={0} onBlur={collapse} onClick={() => this.state.expanded ? collapse() : expand()}>
+ <div className="field-value">{fieldValue}</div>
+ <div className="field-options">
+ {options}
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default ChoiceField;
diff --git a/frontend/src/components/fields/ChoiceField.scss b/frontend/src/components/fields/ChoiceField.scss
new file mode 100644
index 0000000..e7683b7
--- /dev/null
+++ b/frontend/src/components/fields/ChoiceField.scss
@@ -0,0 +1,69 @@
+@import '../../colors.scss';
+
+.choice-field {
+ font-size: 0.9em;
+
+ .field-name {
+ margin: 0;
+ }
+
+ .field-select {
+ position: relative;
+ margin-top: 5px;
+
+ .field-value {
+ background-color: $color-primary-2;
+ border: none;
+ color: $color-primary-4;
+ border-radius: 5px;
+ padding: 7px 25px 7px 10px;
+ cursor: pointer;
+
+ &:after {
+ content: "⋎";
+ position: absolute;
+ right: 10px;
+ }
+ }
+
+ .field-options {
+ position: absolute;
+ top: 35px;
+ width: 100%;
+ z-index: 20;
+ border-top: 3px solid $color-primary-0;
+ border-radius: 5px;
+ background-color: $color-primary-2;
+ display: none;
+
+ .field-option {
+ display: block;
+ padding: 5px 10px;
+ cursor: pointer;
+ border-radius: 5px;
+ }
+
+ .field-option:hover {
+ background-color: $color-primary-1;
+ }
+ }
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ .field-select.select-expanded {
+ .field-options {
+ display: block;
+ }
+
+ .field-value:after {
+ content: "⋏";
+ }
+ }
+
+ &.field-small {
+ font-size: 0.8em;
+ }
+}
diff --git a/frontend/src/components/fields/InputField.js b/frontend/src/components/fields/InputField.js
new file mode 100644
index 0000000..84c981b
--- /dev/null
+++ b/frontend/src/components/fields/InputField.js
@@ -0,0 +1,78 @@
+import React, {Component} from 'react';
+import './InputField.scss';
+import './common.scss';
+import {randomClassName} from "../../utils";
+
+const classNames = require('classnames');
+
+class InputField extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.id = `field-${this.props.name || "noname"}-${randomClassName()}`;
+ }
+
+ render() {
+ const active = this.props.active || false;
+ const invalid = this.props.invalid || false;
+ const small = this.props.small || false;
+ const inline = this.props.inline || false;
+ const name = this.props.name || null;
+ const value = this.props.value || "";
+ const defaultValue = this.props.defaultValue || "";
+ const type = this.props.type || "text";
+ const error = this.props.error || null;
+
+ const handler = (e) => {
+ if (typeof this.props.onChange === "function") {
+ if (type === "file") {
+ let file = e.target.files[0];
+ this.props.onChange(file);
+ } else if (e == null) {
+ this.props.onChange(defaultValue);
+ } else {
+ this.props.onChange(e.target.value);
+ }
+ }
+ };
+ let inputProps = {};
+ if (type !== "file") {
+ inputProps["value"] = value || defaultValue;
+ }
+
+ return (
+ <div className={classNames("field", "input-field", {"field-active" : active},
+ {"field-invalid": invalid}, {"field-small": small}, {"field-inline": inline})}>
+ <div className="field-wrapper">
+ { name &&
+ <div className="field-name">
+ <label>{name}:</label>
+ </div>
+ }
+ <div className="field-input">
+ <div className="field-value">
+ { type === "file" && <label for={this.id} className={"file-label"}>
+ {value.name || this.props.placeholder}</label> }
+ <input type={type} placeholder={this.props.placeholder} id={this.id}
+ aria-describedby={this.id} onChange={handler} {...inputProps}
+ readOnly={this.props.readonly} />
+ </div>
+ { type !== "file" && value !== "" &&
+ <div className="field-clear">
+ <span onClick={() => handler(null)}>del</span>
+ </div>
+ }
+ </div>
+ </div>
+ {error &&
+ <div className="field-error">
+ error: {error}
+ </div>
+ }
+ </div>
+ );
+ }
+}
+
+export default InputField;
diff --git a/frontend/src/components/fields/InputField.scss b/frontend/src/components/fields/InputField.scss
new file mode 100644
index 0000000..79e2b7e
--- /dev/null
+++ b/frontend/src/components/fields/InputField.scss
@@ -0,0 +1,125 @@
+@import '../../colors.scss';
+
+.input-field {
+ font-size: 0.9em;
+ margin: 5px 0;
+
+ .field-name {
+ label {
+ margin: 0;
+ }
+ }
+
+ .field-input {
+ position: relative;
+
+ .field-value {
+ .file-label {
+ background-color: $color-primary-2;
+ margin: 0;
+ width: 100%;
+ color: $color-primary-4;
+ border-radius: 5px;
+ padding: 7px 10px;
+ cursor: pointer;
+ }
+
+ input[type="file"] {
+ display: none;
+ }
+
+ .file-label:after {
+ content: "Browse";
+ position: absolute;
+ right: 0;
+ top: 0;
+ padding: 7px 10px 7px 12px;
+ background-color: $color-primary-1;
+ border-bottom-right-radius: 5px;
+ border-top-right-radius: 5px;
+ }
+ }
+ }
+
+ &.field-active {
+ &.field-inline .field-name {
+ background-color: $color-primary-4 !important;
+ color: $color-primary-3 !important;
+ }
+
+ .field-value input, .field-value .file-label {
+ background-color: $color-primary-4 !important;
+ color: $color-primary-3 !important;
+ }
+
+ .file-label:after {
+ background-color: $color-secondary-4 !important;
+ }
+ }
+
+ &.field-invalid {
+ &.field-inline .field-name {
+ background-color: $color-secondary-2 !important;
+ color: $color-primary-4 !important;
+ }
+
+ .field-value input, .field-value .file-label {
+ background-color: $color-secondary-2 !important;
+ color: $color-primary-4 !important;
+ }
+
+ .file-label:after {
+ background-color: $color-secondary-1 !important;
+ }
+ }
+
+ &.field-small {
+ font-size: 0.8em;
+ }
+
+ &.field-inline .field-wrapper {
+ display: flex;
+
+ .field-name {
+ background-color: $color-primary-2;
+ padding: 6px 7px;
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+ }
+
+ .field-input {
+ width: 100%;
+
+ input, .file-label {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0;
+ padding-left: 3px;
+ }
+ }
+
+ &:focus-within .field-name {
+ background-color: $color-primary-1;
+ }
+ }
+
+ .field-clear {
+ position: absolute;
+ right: 8px;
+ top: 8px;
+ z-index: 10;
+ font-size: 0.9em;
+ font-weight: 600;
+ letter-spacing: -0.5px;
+ cursor: pointer;
+ }
+
+ &.field-active .field-clear {
+ color: $color-primary-2;
+ }
+
+ .field-error {
+ padding: 5px 10px;
+ font-size: 0.9em;
+ color: $color-secondary-0;
+ }
+}
diff --git a/frontend/src/components/fields/TextField.js b/frontend/src/components/fields/TextField.js
new file mode 100644
index 0000000..de68c21
--- /dev/null
+++ b/frontend/src/components/fields/TextField.js
@@ -0,0 +1,43 @@
+import React, {Component} from 'react';
+import './TextField.scss';
+import './common.scss';
+import {randomClassName} from "../../utils";
+
+const classNames = require('classnames');
+
+class TextField extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.id = `field-${this.props.name || "noname"}-${randomClassName()}`;
+ }
+
+ render() {
+ const name = this.props.name || null;
+ const error = this.props.error || null;
+ const rows = this.props.rows || 3;
+
+ const handler = (e) => {
+ if (this.props.onChange) {
+ if (e == null) {
+ this.props.onChange("");
+ } else {
+ this.props.onChange(e.target.value);
+ }
+ }
+ };
+
+ return (
+ <div className={classNames("field", "text-field", {"field-active": this.props.active},
+ {"field-invalid": this.props.invalid}, {"field-small": this.props.small})}>
+ {name && <label htmlFor={this.id}>{name}:</label>}
+ <textarea id={this.id} placeholder={this.props.defaultValue} onChange={handler} rows={rows}
+ readOnly={this.props.readonly} value={this.props.value} ref={this.props.textRef} />
+ {error && <div className="field-error">error: {error}</div>}
+ </div>
+ );
+ }
+}
+
+export default TextField;
diff --git a/frontend/src/components/fields/TextField.scss b/frontend/src/components/fields/TextField.scss
new file mode 100644
index 0000000..de831fb
--- /dev/null
+++ b/frontend/src/components/fields/TextField.scss
@@ -0,0 +1,54 @@
+@import '../../colors.scss';
+
+.text-field {
+ font-size: 0.9em;
+ margin: 5px 0;
+
+ label {
+ display: block;
+ margin: 0;
+ }
+
+ textarea {
+ resize: none;
+ }
+
+ &.field-active {
+ textarea {
+ background-color: $color-primary-4 !important;
+ color: $color-primary-3 !important;
+ }
+ }
+
+ &.field-invalid {
+ textarea {
+ background-color: $color-secondary-2 !important;
+ color: $color-primary-4 !important;
+ }
+ }
+
+ &.field-small {
+ font-size: 0.8em;
+ }
+
+ .field-clear {
+ position: absolute;
+ right: 8px;
+ top: 8px;
+ z-index: 10;
+ font-size: 0.9em;
+ font-weight: 600;
+ letter-spacing: -0.5px;
+ cursor: pointer;
+ }
+
+ &.field-active .field-clear {
+ color: $color-primary-2;
+ }
+
+ .field-error {
+ padding: 5px 10px;
+ font-size: 0.9em;
+ color: $color-secondary-0;
+ }
+}
diff --git a/frontend/src/components/fields/common.scss b/frontend/src/components/fields/common.scss
new file mode 100644
index 0000000..f83a988
--- /dev/null
+++ b/frontend/src/components/fields/common.scss
@@ -0,0 +1,54 @@
+@import '../../colors.scss';
+
+.field {
+
+ input, textarea {
+ background-color: $color-primary-2;
+ width: 100%;
+ border: none;
+ color: $color-primary-4;
+ border-radius: 5px;
+ padding: 7px 10px;
+
+ &:focus {
+ background-color: $color-primary-1;
+ color: $color-primary-4;
+ box-shadow: none;
+ outline: none;
+ }
+
+ &[readonly] {
+ background-color: $color-primary-2;
+ border: none;
+ color: $color-primary-4;
+ }
+
+ &[readonly]:focus {
+ background-color: $color-primary-1;
+ color: $color-primary-4;
+ box-shadow: none;
+ }
+ }
+
+ button {
+ border-radius: 0;
+ background-color: $color-primary-2;
+ border: none;
+ color: $color-primary-4;
+ outline: none;
+ padding: 5px 12px;
+ font-weight: 500;
+
+ &:hover,
+ &:active {
+ background-color: $color-primary-1;
+ color: $color-primary-4;
+ }
+
+ &:focus,
+ &:active {
+ outline: none !important;
+ box-shadow: none !important;
+ }
+ }
+}
diff --git a/frontend/src/components/fields/extensions/ColorField.js b/frontend/src/components/fields/extensions/ColorField.js
new file mode 100644
index 0000000..96ebc49
--- /dev/null
+++ b/frontend/src/components/fields/extensions/ColorField.js
@@ -0,0 +1,82 @@
+import React, {Component} from 'react';
+import {OverlayTrigger, Popover} from "react-bootstrap";
+import './ColorField.scss';
+import InputField from "../InputField";
+import validation from "../../../validation";
+
+class ColorField extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ };
+
+ this.colors = ["#E53935", "#D81B60", "#8E24AA", "#5E35B1", "#3949AB", "#1E88E5", "#039BE5", "#00ACC1",
+ "#00897B", "#43A047", "#7CB342", "#9E9D24", "#F9A825", "#FB8C00", "#F4511E", "#6D4C41"];
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ if (prevProps.value !== this.props.value) {
+ this.onChange(this.props.value);
+ }
+ }
+
+ onChange = (value) => {
+ this.setState({invalid: value !== "" && !validation.isValidColor(value)});
+
+ if (typeof this.props.onChange === "function") {
+ this.props.onChange(value);
+ }
+ };
+
+ render() {
+ const colorButtons = this.colors.map((color) =>
+ <span key={"button" + color} className="color-input" style={{"backgroundColor": color}}
+ onClick={() => {
+ if (typeof this.props.onChange === "function") {
+ this.props.onChange(color);
+ }
+ document.body.click(); // magic to close popup
+ }} />);
+
+ const popover = (
+ <Popover id="popover-basic">
+ <Popover.Title as="h3">choose a color</Popover.Title>
+ <Popover.Content>
+ <div className="colors-container">
+ <div className="colors-row">
+ {colorButtons.slice(0, 8)}
+ </div>
+ <div className="colors-row">
+ {colorButtons.slice(8, 18)}
+ </div>
+ </div>
+ </Popover.Content>
+ </Popover>
+ );
+
+ let buttonStyles = {};
+ if (this.props.value) {
+ buttonStyles["backgroundColor"] = this.props.value;
+ }
+
+ return (
+ <div className="field color-field">
+ <div className="color-input">
+ <InputField {...this.props} onChange={this.onChange} invalid={this.state.invalid} name="color"
+ error={null} />
+ <div className="color-picker">
+ <OverlayTrigger trigger="click" placement="top" overlay={popover} rootClose>
+ <button type="button" className="picker-button" style={buttonStyles}>pick</button>
+ </OverlayTrigger>
+ </div>
+ </div>
+ {this.props.error && <div className="color-error">{this.props.error}</div>}
+ </div>
+ );
+ }
+
+}
+
+export default ColorField;
diff --git a/frontend/src/components/fields/extensions/ColorField.scss b/frontend/src/components/fields/extensions/ColorField.scss
new file mode 100644
index 0000000..6eabbda
--- /dev/null
+++ b/frontend/src/components/fields/extensions/ColorField.scss
@@ -0,0 +1,46 @@
+@import '../../../colors.scss';
+
+.color-field {
+ .color-input {
+ display: flex;
+ align-items: flex-end;
+
+ .input-field {
+ flex: 1;
+
+ input {
+ border-bottom-right-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+ }
+ }
+
+ .color-picker {
+ margin-bottom: 5px;
+
+ .picker-button {
+ font-size: 0.8em;
+ padding: 8px 15px;
+ border-bottom-right-radius: 5px;
+ border-top-right-radius: 5px;
+ background-color: $color-primary-1;
+ }
+ }
+ }
+
+ .color-error {
+ font-size: 0.8em;
+ color: $color-secondary-0;
+ margin-left: 10px;
+ }
+}
+
+.colors-container {
+ width: 600px;
+
+ .color-input {
+ display: inline-block;
+ width: 31px;
+ height: 31px;
+ cursor: pointer;
+ }
+}
diff --git a/frontend/src/components/fields/extensions/NumericField.js b/frontend/src/components/fields/extensions/NumericField.js
new file mode 100644
index 0000000..ed81ed7
--- /dev/null
+++ b/frontend/src/components/fields/extensions/NumericField.js
@@ -0,0 +1,45 @@
+import React, {Component} from 'react';
+import InputField from "../InputField";
+
+class NumericField extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ invalid: false
+ };
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ if (prevProps.value !== this.props.value) {
+ this.onChange(this.props.value);
+ }
+ }
+
+ onChange = (value) => {
+ value = value.toString().replace(/[^\d]/gi, '');
+ let intValue = 0;
+ if (value !== "") {
+ intValue = parseInt(value);
+ }
+ const valid =
+ (!this.props.validate || (typeof this.props.validate === "function" && this.props.validate(intValue))) &&
+ (!this.props.min || (typeof this.props.min === "number" && intValue >= this.props.min)) &&
+ (!this.props.max || (typeof this.props.max === "number" && intValue <= this.props.max));
+ this.setState({invalid: !valid});
+ if (typeof this.props.onChange === "function") {
+ this.props.onChange(intValue);
+ }
+ };
+
+ render() {
+ return (
+ <InputField {...this.props} onChange={this.onChange} defaultValue={this.props.defaultValue || "0"}
+ invalid={this.state.invalid} />
+ );
+ }
+
+}
+
+export default NumericField;
diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.js b/frontend/src/components/filters/BooleanConnectionsFilter.js
index 7dea7cf..4c5a78a 100644
--- a/frontend/src/components/filters/BooleanConnectionsFilter.js
+++ b/frontend/src/components/filters/BooleanConnectionsFilter.js
@@ -1,9 +1,7 @@
import React, {Component} from 'react';
import {withRouter} from "react-router-dom";
import {Redirect} from "react-router";
-import './BooleanConnectionsFilter.scss';
-
-const classNames = require('classnames');
+import CheckField from "../fields/CheckField";
class BooleanConnectionsFilter extends Component {
@@ -57,11 +55,9 @@ class BooleanConnectionsFilter extends Component {
}
return (
- <div className={classNames("filter", "d-inline-block", {"filter-active" : this.toBoolean(this.state.filterActive)})}>
- <div className="filter-boolean" onClick={this.filterChanged}>
- <span>{this.props.filterName}</span>
- </div>
-
+ <div className="filter" style={{"width": `${this.props.width}px`}}>
+ <CheckField checked={this.toBoolean(this.state.filterActive)} name={this.props.filterName}
+ onChange={this.filterChanged} />
{redirect}
</div>
);
diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.scss b/frontend/src/components/filters/BooleanConnectionsFilter.scss
deleted file mode 100644
index 941b967..0000000
--- a/frontend/src/components/filters/BooleanConnectionsFilter.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-@import '../../colors';
-
-.filter {
- .filter-boolean {
- padding: 0 10px;
- background-color: $color-primary-2;
- border-radius: 5px;
- cursor: pointer;
- height: 34px;
-
- span {
- display: block;
- font-size: 13px;
- padding: 6px 5px;
- }
- }
-
- &.filter-active {
- .filter-boolean {
- background-color: $color-primary-4;
- color: $color-primary-3;
- }
- }
-}
diff --git a/frontend/src/components/filters/FiltersDefinitions.js b/frontend/src/components/filters/FiltersDefinitions.js
index a582d02..02ccb42 100644
--- a/frontend/src/components/filters/FiltersDefinitions.js
+++ b/frontend/src/components/filters/FiltersDefinitions.js
@@ -12,7 +12,6 @@ import React from "react";
import RulesConnectionsFilter from "./RulesConnectionsFilter";
import BooleanConnectionsFilter from "./BooleanConnectionsFilter";
-
export const filtersNames = ["service_port", "matched_rules", "client_address", "client_port",
"min_duration", "max_duration", "min_bytes", "max_bytes", "started_after",
"started_before", "closed_after", "closed_before", "marked", "hidden"];
@@ -21,49 +20,71 @@ export const filtersDefinitions = {
service_port: <StringConnectionsFilter filterName="service_port"
defaultFilterValue="all_ports"
replaceFunc={cleanNumber}
- validateFunc={validatePort}/>,
+ validateFunc={validatePort}
+ key="service_port_filter"
+ width={200} />,
matched_rules: <RulesConnectionsFilter />,
client_address: <StringConnectionsFilter filterName="client_address"
defaultFilterValue="all_addresses"
- validateFunc={validateIpAddress} />,
+ validateFunc={validateIpAddress}
+ key="client_address_filter"
+ width={320} />,
client_port: <StringConnectionsFilter filterName="client_port"
defaultFilterValue="all_ports"
replaceFunc={cleanNumber}
- validateFunc={validatePort}/>,
+ validateFunc={validatePort}
+ key="client_port_filter"
+ width={200} />,
min_duration: <StringConnectionsFilter filterName="min_duration"
defaultFilterValue="0"
replaceFunc={cleanNumber}
- validateFunc={validateMin(0)}/>,
+ validateFunc={validateMin(0)}
+ key="min_duration_filter"
+ width={200} />,
max_duration: <StringConnectionsFilter filterName="max_duration"
defaultFilterValue="∞"
- replaceFunc={cleanNumber} />,
+ replaceFunc={cleanNumber}
+ key="max_duration_filter"
+ width={200} />,
min_bytes: <StringConnectionsFilter filterName="min_bytes"
defaultFilterValue="0"
replaceFunc={cleanNumber}
- validateFunc={validateMin(0)} />,
+ validateFunc={validateMin(0)}
+ key="min_bytes_filter"
+ width={200} />,
max_bytes: <StringConnectionsFilter filterName="max_bytes"
defaultFilterValue="∞"
- replaceFunc={cleanNumber} />,
+ replaceFunc={cleanNumber}
+ key="max_bytes_filter"
+ width={200} />,
started_after: <StringConnectionsFilter filterName="started_after"
defaultFilterValue="00:00:00"
validateFunc={validate24HourTime}
encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime} />,
+ decodeFunc={timestampToTime}
+ key="started_after_filter"
+ width={200} />,
started_before: <StringConnectionsFilter filterName="started_before"
defaultFilterValue="00:00:00"
validateFunc={validate24HourTime}
encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime} />,
+ decodeFunc={timestampToTime}
+ key="started_before_filter"
+ width={200} />,
closed_after: <StringConnectionsFilter filterName="closed_after"
defaultFilterValue="00:00:00"
validateFunc={validate24HourTime}
encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime} />,
+ decodeFunc={timestampToTime}
+ key="closed_after_filter"
+ width={200} />,
closed_before: <StringConnectionsFilter filterName="closed_before"
defaultFilterValue="00:00:00"
validateFunc={validate24HourTime}
encodeFunc={timeToTimestamp}
- decodeFunc={timestampToTime} />,
+ decodeFunc={timestampToTime}
+ key="closed_before_filter"
+ width={200} />,
marked: <BooleanConnectionsFilter filterName={"marked"} />,
hidden: <BooleanConnectionsFilter filterName={"hidden"} />
};
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js
index 358085f..8366189 100644
--- a/frontend/src/components/filters/RulesConnectionsFilter.js
+++ b/frontend/src/components/filters/RulesConnectionsFilter.js
@@ -3,7 +3,7 @@ import {withRouter} from "react-router-dom";
import {Redirect} from "react-router";
import './RulesConnectionsFilter.scss';
import ReactTags from 'react-tag-autocomplete';
-import axios from 'axios';
+import backend from "../../backend";
const classNames = require('classnames');
@@ -24,8 +24,8 @@ class RulesConnectionsFilter extends Component {
let params = new URLSearchParams(this.props.location.search);
let activeRules = params.getAll("matched_rules") || [];
- axios.get("/api/rules").then(res => {
- let rules = res.data.flatMap(rule => rule.enabled ? [{id: rule.id, name: rule.name}] : []);
+ backend.get("/api/rules").then(res => {
+ let rules = res.json.flatMap(rule => rule.enabled ? [{id: rule.id, name: rule.name}] : []);
activeRules = rules.filter(rule => activeRules.some(id => rule.id === id));
this.setState({rules, activeRules, mounted: true});
});
diff --git a/frontend/src/components/filters/StringConnectionsFilter.js b/frontend/src/components/filters/StringConnectionsFilter.js
index 490a569..f463593 100644
--- a/frontend/src/components/filters/StringConnectionsFilter.js
+++ b/frontend/src/components/filters/StringConnectionsFilter.js
@@ -1,9 +1,7 @@
import React, {Component} from 'react';
import {withRouter} from "react-router-dom";
import {Redirect} from "react-router";
-import './StringConnectionsFilter.scss';
-
-const classNames = require('classnames');
+import InputField from "../fields/InputField";
class StringConnectionsFilter extends Component {
@@ -62,8 +60,7 @@ class StringConnectionsFilter extends Component {
(typeof this.props.validateFunc === "function" && this.props.validateFunc(value));
}
- filterChanged(event) {
- let fieldValue = event.target.value;
+ filterChanged(fieldValue) {
if (this.state.timeoutHandle !== null) {
clearTimeout(this.state.timeoutHandle);
}
@@ -116,26 +113,10 @@ class StringConnectionsFilter extends Component {
let active = this.state.filterValue !== null;
return (
- <div className={classNames("filter", "d-inline-block", {"filter-active" : active},
- {"filter-invalid": this.state.invalidValue})} style={{"width": "200px"}}>
- <div className="input-group">
- <div className="filter-name-wrapper">
- <span className="filter-name" id={`filter-${this.props.filterName}`}>{this.props.filterName}:</span>
- </div>
- <input placeholder={this.props.defaultFilterValue} aria-label={this.props.filterName}
- aria-describedby={`filter-${this.props.filterName}`} className="form-control filter-value"
- onChange={this.filterChanged} value={this.state.fieldValue} />
- </div>
-
- { active &&
- <div className="filter-delete">
- <span className="filter-delete-icon" onClick={() => {
- this.needRedirect = true;
- this.setState({fieldValue: "", filterValue: null});
- }}>del</span>
- </div>
- }
-
+ <div className="filter" style={{"width": `${this.props.width}px`}}>
+ <InputField active={active} invalid={this.state.invalidValue} name={this.props.filterName}
+ placeholder={this.props.defaultFilterValue} onChange={this.filterChanged}
+ value={this.state.fieldValue} inline={true} small={true} />
{redirect}
</div>
);
diff --git a/frontend/src/components/filters/StringConnectionsFilter.scss b/frontend/src/components/filters/StringConnectionsFilter.scss
deleted file mode 100644
index 1476616..0000000
--- a/frontend/src/components/filters/StringConnectionsFilter.scss
+++ /dev/null
@@ -1,66 +0,0 @@
-@import '../../colors';
-
-.filter {
- margin: 0 10px;
- position: relative;
-
- .filter-name-wrapper {
- background-color: $color-primary-2;
- padding: 3px 7px;
- border-top-left-radius: 5px;
- border-bottom-left-radius: 5px;
- }
-
- .filter-name {
- font-size: 13px;
- }
-
- .filter-value {
- font-size: 13px;
- padding-left: 0;
- border-radius: 5px;
-
- &:focus {
- background-color: $color-primary-2;
- }
- }
-
- &.filter-active {
- .filter-name-wrapper {
- background-color: $color-primary-4;
- color: $color-primary-3;
- }
-
- .filter-value {
- background-color: $color-primary-4;
- color: $color-primary-3;
- }
- }
-
- &.filter-invalid {
- .filter-name-wrapper {
- background-color: $color-secondary-2;
- color: $color-primary-4;
- }
-
- .filter-value {
- background-color: $color-secondary-2;
- color: $color-primary-4;
- }
- }
-
- .filter-delete {
- position: absolute;
- right: 10px;
- top: 10px;
- z-index: 10;
- font-size: 11px;
- letter-spacing: -0.5px;
- color: $color-primary-2;
- cursor: pointer;
-
- .filter-delete-icon {
- font-weight: 800;
- }
- }
-}
diff --git a/frontend/src/components/objects/LinkPopover.js b/frontend/src/components/objects/LinkPopover.js
new file mode 100644
index 0000000..8768caa
--- /dev/null
+++ b/frontend/src/components/objects/LinkPopover.js
@@ -0,0 +1,33 @@
+import React, {Component} from 'react';
+import {randomClassName} from "../../utils";
+import {OverlayTrigger, Popover} from "react-bootstrap";
+import './LinkPopover.scss';
+
+class LinkPopover extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.id = `link-overlay-${randomClassName()}`;
+ }
+
+ render() {
+ const popover = (
+ <Popover id={this.id}>
+ {this.props.title && <Popover.Title as="h3">{this.props.title}</Popover.Title>}
+ <Popover.Content>
+ {this.props.content}
+ </Popover.Content>
+ </Popover>
+ );
+
+ return (this.props.content ?
+ <OverlayTrigger trigger={["hover", "focus"]} placement={this.props.placement || "top"} overlay={popover}>
+ <span className="link-popover">{this.props.text}</span>
+ </OverlayTrigger> :
+ <span className="link-popover-empty">{this.props.text}</span>
+ );
+ }
+}
+
+export default LinkPopover;
diff --git a/frontend/src/components/objects/LinkPopover.scss b/frontend/src/components/objects/LinkPopover.scss
new file mode 100644
index 0000000..d5f4879
--- /dev/null
+++ b/frontend/src/components/objects/LinkPopover.scss
@@ -0,0 +1,7 @@
+@import '../../colors.scss';
+
+.link-popover {
+ text-decoration: underline;
+ font-weight: 500;
+ cursor: pointer;
+} \ No newline at end of file
diff --git a/frontend/src/components/panels/ConfigurationPane.js b/frontend/src/components/panels/ConfigurationPane.js
new file mode 100644
index 0000000..10309f6
--- /dev/null
+++ b/frontend/src/components/panels/ConfigurationPane.js
@@ -0,0 +1,162 @@
+import React, {Component} from 'react';
+import './common.scss';
+import './ConfigurationPane.scss';
+import LinkPopover from "../objects/LinkPopover";
+import {Col, Container, Row} from "react-bootstrap";
+import InputField from "../fields/InputField";
+import TextField from "../fields/TextField";
+import ButtonField from "../fields/ButtonField";
+import CheckField from "../fields/CheckField";
+import {createCurlCommand} from "../../utils";
+import Table from "react-bootstrap/Table";
+import validation from "../../validation";
+import backend from "../../backend";
+
+class ConfigurationPane extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ settings: {
+ "config": {
+ "server_address": "",
+ "flag_regex": "",
+ "auth_required": false
+ },
+ "accounts": {
+ }
+ },
+ newUsername: "",
+ newPassword: ""
+ };
+ }
+
+ saveSettings = () => {
+ if (this.validateSettings(this.state.settings)) {
+ backend.post("/setup", this.state.settings).then(_ => {
+ this.props.onConfigured();
+ }).catch(res => {
+ this.setState({setupStatusCode: res.status, setupResponse: JSON.stringify(res.json)});
+ });
+ }
+ };
+
+ validateSettings = (settings) => {
+ let valid = true;
+ if (!validation.isValidAddress(settings.config.server_address, true)) {
+ this.setState({serverAddressError: "invalid ip_address"});
+ valid = false;
+ }
+ if (settings.config.flag_regex.length < 8) {
+ this.setState({flagRegexError: "flag_regex.length < 8"});
+ valid = false;
+ }
+
+ return valid;
+ };
+
+ updateParam = (callback) => {
+ callback(this.state.settings);
+ this.setState({settings: this.state.settings});
+ };
+
+ addAccount = () => {
+ if (this.state.newUsername.length !== 0 && this.state.newPassword.length !== 0) {
+ const settings = this.state.settings;
+ settings.accounts[this.state.newUsername] = this.state.newPassword;
+
+ this.setState({
+ newUsername: "",
+ newPassword: "",
+ settings: settings
+ });
+ } else {
+ this.setState({
+ newUsernameActive: this.state.newUsername.length === 0,
+ newPasswordActive: this.state.newPassword.length === 0
+ });
+ }
+ };
+
+ render() {
+ const settings = this.state.settings;
+ const curlCommand = createCurlCommand("/setup", "POST", settings);
+
+ const accounts = Object.entries(settings.accounts).map(([username, password]) =>
+ <tr key={username}>
+ <td>{username}</td>
+ <td><LinkPopover text="******" content={password} /></td>
+ <td><ButtonField variant="red" small rounded name="delete"
+ onClick={() => this.updateParam((s) => delete s.accounts[username]) }/></td>
+ </tr>).concat(<tr key={"new_account"}>
+ <td><InputField value={this.state.newUsername} small active={this.state.newUsernameActive}
+ onChange={(v) => this.setState({newUsername: v})} /></td>
+ <td><InputField value={this.state.newPassword} small active={this.state.newPasswordActive}
+ onChange={(v) => this.setState({newPassword: v})} /></td>
+ <td><ButtonField variant="green" small rounded name="add" onClick={this.addAccount}/></td>
+ </tr>);
+
+ return (
+ <div className="configuration-pane">
+ <div className="pane">
+ <div className="pane-container">
+ <div className="pane-section">
+ <div className="section-header">
+ <span className="api-request">POST /setup</span>
+ <span className="api-response"><LinkPopover text={this.state.setupStatusCode}
+ content={this.state.setupResponse}
+ placement="left" /></span>
+ </div>
+
+ <div className="section-content">
+ <Container className="p-0">
+ <Row>
+ <Col>
+ <InputField name="server_address" value={settings.config.server_address}
+ error={this.state.serverAddressError}
+ onChange={(v) => this.updateParam((s) => s.config.server_address = v)} />
+ <InputField name="flag_regex" value={settings.config.flag_regex}
+ onChange={(v) => this.updateParam((s) => s.config.flag_regex = v)}
+ error={this.state.flagRegexError} />
+ <div style={{"marginTop": "10px"}}>
+ <CheckField checked={settings.config.auth_required} name="auth_required"
+ onChange={(v) => this.updateParam((s) => s.config.auth_required = v)}/>
+ </div>
+
+ </Col>
+
+ <Col>
+ accounts:
+ <div className="section-table">
+ <Table borderless size="sm">
+ <thead>
+ <tr>
+ <th>username</th>
+ <th>password</th>
+ <th>actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ {accounts}
+ </tbody>
+ </Table>
+ </div>
+ </Col>
+ </Row>
+ </Container>
+
+ <TextField value={curlCommand} rows={4} readonly small={true}/>
+ </div>
+
+ <div className="section-footer">
+ <ButtonField variant="green" name="save" bordered onClick={this.saveSettings} />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default ConfigurationPane;
diff --git a/frontend/src/components/panels/ConfigurationPane.scss b/frontend/src/components/panels/ConfigurationPane.scss
new file mode 100644
index 0000000..955d2bc
--- /dev/null
+++ b/frontend/src/components/panels/ConfigurationPane.scss
@@ -0,0 +1,19 @@
+@import '../../colors';
+
+.configuration-pane {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: $color-primary-0;
+
+ .pane {
+ flex-basis: 900px;
+ margin-bottom: 200px;
+ }
+
+ .pane-container {
+ padding-bottom: 1px;
+ }
+
+}
diff --git a/frontend/src/components/panels/MainPane.js b/frontend/src/components/panels/MainPane.js
new file mode 100644
index 0000000..3202d6d
--- /dev/null
+++ b/frontend/src/components/panels/MainPane.js
@@ -0,0 +1,56 @@
+import React, {Component} from 'react';
+import './common.scss';
+import './MainPane.scss';
+import Connections from "../../views/Connections";
+import ConnectionContent from "../ConnectionContent";
+import {Route, Switch, withRouter} from "react-router-dom";
+import PcapPane from "./PcapPane";
+import backend from "../../backend";
+import RulePane from "./RulePane";
+import ServicePane from "./ServicePane";
+
+class MainPane extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ selectedConnection: null,
+ loading: false
+ };
+ }
+
+ componentDidMount() {
+ const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/);
+ if (match != null) {
+ this.setState({loading: true});
+ backend.get(`/api/connections/${match[1]}`)
+ .then(res => this.setState({selectedConnection: res.json, loading: false}))
+ .catch(error => console.log(error));
+ }
+ }
+
+ render() {
+ return (
+ <div className="main-pane">
+ <div className="pane connections-pane">
+ {
+ !this.state.loading &&
+ <Connections onSelected={(c) => this.setState({selectedConnection: c})}
+ initialConnection={this.state.selectedConnection} />
+ }
+ </div>
+ <div className="pane details-pane">
+ <Switch>
+ <Route path="/pcaps" children={<PcapPane />} />
+ <Route path="/rules" children={<RulePane />} />
+ <Route path="/services" children={<ServicePane />} />
+ <Route exact path="/connections/:id" children={<ConnectionContent connection={this.state.selectedConnection} />} />
+ <Route children={<ConnectionContent />} />
+ </Switch>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default withRouter(MainPane);
diff --git a/frontend/src/components/panels/MainPane.scss b/frontend/src/components/panels/MainPane.scss
new file mode 100644
index 0000000..04be347
--- /dev/null
+++ b/frontend/src/components/panels/MainPane.scss
@@ -0,0 +1,23 @@
+@import '../../colors';
+
+.main-pane {
+ height: 100%;
+ display: flex;
+ padding: 0 15px;
+ background-color: $color-primary-2;
+
+ .pane {
+ flex: 1;
+ }
+
+ .connections-pane {
+ flex: 1 0;
+ margin-right: 7.5px;
+ }
+
+ .details-pane {
+ flex: 1 1;
+ margin-left: 7.5px;
+ }
+
+}
diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js
new file mode 100644
index 0000000..7b3fde6
--- /dev/null
+++ b/frontend/src/components/panels/PcapPane.js
@@ -0,0 +1,251 @@
+import React, {Component} from 'react';
+import './PcapPane.scss';
+import './common.scss';
+import Table from "react-bootstrap/Table";
+import backend from "../../backend";
+import {createCurlCommand, dateTimeToTime, durationBetween, formatSize} from "../../utils";
+import InputField from "../fields/InputField";
+import CheckField from "../fields/CheckField";
+import TextField from "../fields/TextField";
+import ButtonField from "../fields/ButtonField";
+import LinkPopover from "../objects/LinkPopover";
+
+class PcapPane extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ sessions: [],
+ isUploadFileValid: true,
+ isUploadFileFocused: false,
+ uploadFlushAll: false,
+ isFileValid: true,
+ isFileFocused: false,
+ fileValue: "",
+ processFlushAll: false,
+ deleteOriginalFile: false
+ };
+ }
+
+ componentDidMount() {
+ this.loadSessions();
+ }
+
+ loadSessions = () => {
+ backend.get("/api/pcap/sessions")
+ .then(res => this.setState({sessions: res.json, sessionsStatusCode: res.status}))
+ .catch(res => this.setState({
+ sessions: res.json, sessionsStatusCode: res.status,
+ sessionsResponse: JSON.stringify(res.json)
+ }));
+ };
+
+ uploadPcap = () => {
+ if (this.state.uploadSelectedFile == null || !this.state.isUploadFileValid) {
+ this.setState({isUploadFileFocused: true});
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append("file", this.state.uploadSelectedFile);
+ formData.append("flush_all", this.state.uploadFlushAll);
+ backend.postFile("/api/pcap/upload", formData).then(res => {
+ this.setState({
+ uploadStatusCode: res.status,
+ uploadResponse: JSON.stringify(res.json)
+ });
+ this.resetUpload();
+ this.loadSessions();
+ }).catch(res => this.setState({
+ uploadStatusCode: res.status,
+ uploadResponse: JSON.stringify(res.json)
+ })
+ );
+ };
+
+ processPcap = () => {
+ if (this.state.fileValue === "" || !this.state.isFileValid) {
+ this.setState({isFileFocused: true});
+ return;
+ }
+
+ backend.post("/api/pcap/file", {
+ file: this.state.fileValue,
+ flush_all: this.state.processFlushAll,
+ delete_original_file: this.state.deleteOriginalFile
+ }).then(res => {
+ this.setState({
+ processStatusCode: res.status,
+ processResponse: JSON.stringify(res.json)
+ });
+ this.resetProcess();
+ this.loadSessions();
+ }).catch(res => this.setState({
+ processStatusCode: res.status,
+ processResponse: JSON.stringify(res.json)
+ })
+ );
+ };
+
+ resetUpload = () => {
+ this.setState({
+ isUploadFileValid: true,
+ isUploadFileFocused: false,
+ uploadFlushAll: false,
+ uploadSelectedFile: null
+ });
+ };
+
+ resetProcess = () => {
+ this.setState({
+ isFileValid: true,
+ isFileFocused: false,
+ fileValue: "",
+ processFlushAll: false,
+ deleteOriginalFile: false,
+ });
+ };
+
+ render() {
+ let sessions = this.state.sessions.map(s =>
+ <tr key={s.id} className="table-row">
+ <td>{s["id"].substring(0, 8)}</td>
+ <td>{dateTimeToTime(s["started_at"])}</td>
+ <td>{durationBetween(s["started_at"], s["completed_at"])}</td>
+ <td>{formatSize(s["size"])}</td>
+ <td>{s["processed_packets"]}</td>
+ <td>{s["invalid_packets"]}</td>
+ <td><LinkPopover text={Object.keys(s["packets_per_service"]).length + " services"}
+ content={JSON.stringify(s["packets_per_service"])}
+ placement="left"/></td>
+ <td className="table-cell-action"><a href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a>
+ </td>
+ </tr>
+ );
+
+ const handleUploadFileChange = (file) => {
+ this.setState({
+ isUploadFileValid: file == null || (file.type.endsWith("pcap") || file.type.endsWith("pcapng")),
+ isUploadFileFocused: false,
+ uploadSelectedFile: file,
+ uploadStatusCode: null,
+ uploadResponse: null
+ });
+ };
+
+ const handleFileChange = (file) => {
+ this.setState({
+ isFileValid: (file.endsWith("pcap") || file.endsWith("pcapng")),
+ isFileFocused: false,
+ fileValue: file,
+ processStatusCode: null,
+ processResponse: null
+ });
+ };
+
+ const uploadCurlCommand = createCurlCommand("pcap/upload", "POST", null, {
+ file: "@" + ((this.state.uploadSelectedFile != null && this.state.isUploadFileValid) ?
+ this.state.uploadSelectedFile.name : "invalid.pcap"),
+ flush_all: this.state.uploadFlushAll
+ });
+
+ const fileCurlCommand = createCurlCommand("pcap/file", "POST", {
+ file: this.state.fileValue,
+ flush_all: this.state.processFlushAll,
+ delete_original_file: this.state.deleteOriginalFile
+ });
+
+ return (
+ <div className="pane-container pcap-pane">
+ <div className="pane-section pcap-list">
+ <div className="section-header">
+ <span className="api-request">GET /api/pcap/sessions</span>
+ <span className="api-response"><LinkPopover text={this.state.sessionsStatusCode}
+ content={this.state.sessionsResponse}
+ placement="left"/></span>
+ </div>
+
+ <div className="section-content">
+ <div className="section-table">
+ <Table borderless size="sm">
+ <thead>
+ <tr>
+ <th>id</th>
+ <th>started_at</th>
+ <th>duration</th>
+ <th>size</th>
+ <th>processed_packets</th>
+ <th>invalid_packets</th>
+ <th>packets_per_service</th>
+ <th>actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ {sessions}
+ </tbody>
+ </Table>
+ </div>
+ </div>
+ </div>
+
+ <div className="double-pane-container">
+ <div className="pane-section">
+ <div className="section-header">
+ <span className="api-request">POST /api/pcap/upload</span>
+ <span className="api-response"><LinkPopover text={this.state.uploadStatusCode}
+ content={this.state.uploadResponse}
+ placement="left"/></span>
+ </div>
+
+ <div className="section-content">
+ <InputField type={"file"} name={"file"} invalid={!this.state.isUploadFileValid}
+ active={this.state.isUploadFileFocused}
+ onChange={handleUploadFileChange} value={this.state.uploadSelectedFile}
+ placeholder={"no .pcap[ng] selected"}/>
+ <div className="upload-actions">
+ <div className="upload-options">
+ <span>options:</span>
+ <CheckField name="flush_all" checked={this.state.uploadFlushAll}
+ onChange={v => this.setState({uploadFlushAll: v})}/>
+ </div>
+ <ButtonField variant="green" bordered onClick={this.uploadPcap} name="upload"/>
+ </div>
+
+ <TextField value={uploadCurlCommand} rows={4} readonly small={true}/>
+ </div>
+ </div>
+
+ <div className="pane-section">
+ <div className="section-header">
+ <span className="api-request">POST /api/pcap/file</span>
+ <span className="api-response"><LinkPopover text={this.state.processStatusCode}
+ content={this.state.processResponse}
+ placement="left"/></span>
+ </div>
+
+ <div className="section-content">
+ <InputField name="file" active={this.state.isFileFocused} invalid={!this.state.isFileValid}
+ onChange={handleFileChange} value={this.state.fileValue}
+ placeholder={"local .pcap[ng] path"} inline/>
+
+ <div className="upload-actions" style={{"marginTop": "11px"}}>
+ <div className="upload-options">
+ <CheckField name="flush_all" checked={this.state.processFlushAll}
+ onChange={v => this.setState({processFlushAll: v})}/>
+ <CheckField name="delete_original_file" checked={this.state.deleteOriginalFile}
+ onChange={v => this.setState({deleteOriginalFile: v})}/>
+ </div>
+ <ButtonField variant="blue" bordered onClick={this.processPcap} name="process"/>
+ </div>
+
+ <TextField value={fileCurlCommand} rows={4} readonly small={true}/>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+export default PcapPane;
diff --git a/frontend/src/components/panels/PcapPane.scss b/frontend/src/components/panels/PcapPane.scss
new file mode 100644
index 0000000..721560a
--- /dev/null
+++ b/frontend/src/components/panels/PcapPane.scss
@@ -0,0 +1,39 @@
+@import '../../colors.scss';
+
+.pcap-pane {
+ display: flex;
+ flex-direction: column;
+
+ .pcap-list {
+ flex: 1;
+ overflow: hidden;
+
+ .section-content {
+ height: 100%;
+ }
+
+ .section-table {
+ height: calc(100% - 30px);
+ }
+
+ .table-cell-action {
+ font-size: 13px;
+ font-weight: 600;
+ }
+ }
+
+ .upload-actions {
+ display: flex;
+ align-items: flex-end;
+ margin-bottom: 20px;
+ }
+
+ .upload-options {
+ flex: 1;
+
+ span {
+ font-size: 0.9em;
+ }
+ }
+
+}
diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js
new file mode 100644
index 0000000..49364d2
--- /dev/null
+++ b/frontend/src/components/panels/RulePane.js
@@ -0,0 +1,413 @@
+import React, {Component} from 'react';
+import './common.scss';
+import './RulePane.scss';
+import Table from "react-bootstrap/Table";
+import {Col, Container, Row} from "react-bootstrap";
+import InputField from "../fields/InputField";
+import CheckField from "../fields/CheckField";
+import TextField from "../fields/TextField";
+import backend from "../../backend";
+import NumericField from "../fields/extensions/NumericField";
+import ColorField from "../fields/extensions/ColorField";
+import ChoiceField from "../fields/ChoiceField";
+import ButtonField from "../fields/ButtonField";
+import validation from "../../validation";
+import LinkPopover from "../objects/LinkPopover";
+import {randomClassName} from "../../utils";
+
+const classNames = require('classnames');
+const _ = require('lodash');
+
+class RulePane extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ rules: [],
+ newRule: this.emptyRule,
+ newPattern: this.emptyPattern
+ };
+
+ this.directions = {
+ 0: "both",
+ 1: "c->s",
+ 2: "s->c"
+ };
+ }
+
+ componentDidMount() {
+ this.reset();
+ this.loadRules();
+ }
+
+ emptyRule = {
+ "name": "",
+ "color": "",
+ "notes": "",
+ "enabled": true,
+ "patterns": [],
+ "filter": {
+ "service_port": 0,
+ "client_address": "",
+ "client_port": 0,
+ "min_duration": 0,
+ "max_duration": 0,
+ "min_bytes": 0,
+ "max_bytes": 0
+ },
+ "version": 0
+ };
+
+ emptyPattern = {
+ "regex": "",
+ "flags": {
+ "caseless": false,
+ "dot_all": false,
+ "multi_line": false,
+ "utf_8_mode": false,
+ "unicode_property": false
+ },
+ "min_occurrences": 0,
+ "max_occurrences": 0,
+ "direction": 0
+ };
+
+ loadRules = () => {
+ backend.get("/api/rules").then(res => this.setState({rules: res.json, rulesStatusCode: res.status}))
+ .catch(res => this.setState({rulesStatusCode: res.status, rulesResponse: JSON.stringify(res.json)}));
+ };
+
+ addRule = () => {
+ if (this.validateRule(this.state.newRule)) {
+ backend.post("/api/rules", this.state.newRule).then(res => {
+ this.reset();
+ this.setState({ruleStatusCode: res.status});
+ this.loadRules();
+ }).catch(res => {
+ this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)});
+ });
+ }
+ };
+
+ updateRule = () => {
+ const rule = this.state.selectedRule;
+ if (this.validateRule(rule)) {
+ backend.put(`/api/rules/${rule.id}`, rule).then(res => {
+ this.reset();
+ this.setState({ruleStatusCode: res.status});
+ this.loadRules();
+ }).catch(res => {
+ this.setState({ruleStatusCode: res.status, ruleResponse: JSON.stringify(res.json)});
+ });
+ }
+ };
+
+ validateRule = (rule) => {
+ let valid = true;
+ if (rule.name.length < 3) {
+ this.setState({ruleNameError: "name.length < 3"});
+ valid = false;
+ }
+ if (!validation.isValidColor(rule.color)) {
+ this.setState({ruleColorError: "color is not hexcolor"});
+ valid = false;
+ }
+ if (!validation.isValidPort(rule.filter.service_port)) {
+ this.setState({ruleServicePortError: "service_port > 65565"});
+ valid = false;
+ }
+ if (!validation.isValidPort(rule.filter.client_port)) {
+ this.setState({ruleClientPortError: "client_port > 65565"});
+ valid = false;
+ }
+ if (!validation.isValidAddress(rule.filter.client_address)) {
+ this.setState({ruleClientAddressError: "client_address is not ip_address"});
+ valid = false;
+ }
+ if (rule.filter.min_duration > rule.filter.max_duration) {
+ this.setState({ruleDurationError: "min_duration > max_dur."});
+ valid = false;
+ }
+ if (rule.filter.min_bytes > rule.filter.max_bytes) {
+ this.setState({ruleBytesError: "min_bytes > max_bytes"});
+ valid = false;
+ }
+ if (rule.patterns.length < 1) {
+ this.setState({rulePatternsError: "patterns.length < 1"});
+ valid = false;
+ }
+
+ return valid;
+ };
+
+ reset = () => {
+ const newRule = _.cloneDeep(this.emptyRule);
+ const newPattern = _.cloneDeep(this.emptyPattern);
+ this.setState({
+ selectedRule: null,
+ newRule: newRule,
+ selectedPattern: null,
+ newPattern: newPattern,
+ patternRegexFocused: false,
+ patternOccurrencesFocused: false,
+ ruleNameError: null,
+ ruleColorError: null,
+ ruleServicePortError: null,
+ ruleClientPortError: null,
+ ruleClientAddressError: null,
+ ruleDurationError: null,
+ ruleBytesError: null,
+ rulePatternsError: null,
+ ruleStatusCode: null,
+ rulesStatusCode: null,
+ ruleResponse: null,
+ rulesResponse: null
+ });
+ };
+
+ updateParam = (callback) => {
+ const updatedRule = this.currentRule();
+ callback(updatedRule);
+ this.setState({newRule: updatedRule});
+ };
+
+ currentRule = () => this.state.selectedRule != null ? this.state.selectedRule : this.state.newRule;
+
+ addPattern = (pattern) => {
+ if (!this.validatePattern(pattern)) {
+ return;
+ }
+
+ const newPattern = _.cloneDeep(this.emptyPattern);
+ this.currentRule().patterns.push(pattern);
+ this.setState({
+ newPattern: newPattern
+ });
+ };
+
+ editPattern = (pattern) => {
+ this.setState({
+ selectedPattern: pattern
+ });
+ };
+
+ updatePattern = (pattern) => {
+ if (!this.validatePattern(pattern)) {
+ return;
+ }
+
+ this.setState({
+ selectedPattern: null
+ });
+ };
+
+ validatePattern = (pattern) => {
+ let valid = true;
+ if (pattern.regex === "") {
+ valid = false;
+ this.setState({patternRegexFocused: true});
+ }
+ if (pattern.min_occurrences > pattern.max_occurrences) {
+ valid = false;
+ this.setState({patternOccurrencesFocused: true});
+ }
+ return valid;
+ };
+
+ render() {
+ const isUpdate = this.state.selectedRule != null;
+ const rule = this.currentRule();
+ const pattern = this.state.selectedPattern || this.state.newPattern;
+
+ let rules = this.state.rules.map(r =>
+ <tr key={r.id} onClick={() => {
+ this.reset();
+ this.setState({selectedRule: _.cloneDeep(r)});
+ }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id })}>
+ <td>{r["id"].substring(0, 8)}</td>
+ <td>{r["name"]}</td>
+ <td><ButtonField name={r["color"]} color={r["color"]} small /></td>
+ <td>{r["notes"]}</td>
+ </tr>
+ );
+
+ let patterns = (this.state.selectedPattern == null && !isUpdate ?
+ rule.patterns.concat(this.state.newPattern) :
+ rule.patterns
+ ).map(p => p === pattern ?
+ <tr key={randomClassName()}>
+ <td style={{"width": "500px"}}>
+ <InputField small active={this.state.patternRegexFocused} value={pattern.regex}
+ onChange={(v) => {
+ this.updateParam(() => pattern.regex = v);
+ this.setState({patternRegexFocused: pattern.regex === ""});
+ }} />
+ </td>
+ <td><CheckField small checked={pattern.flags.caseless}
+ onChange={(v) => this.updateParam(() => pattern.flags.caseless = v)}/></td>
+ <td><CheckField small checked={pattern.flags.dot_all}
+ onChange={(v) => this.updateParam(() => pattern.flags.dot_all = v)}/></td>
+ <td><CheckField small checked={pattern.flags.multi_line}
+ onChange={(v) => this.updateParam(() => pattern.flags.multi_line = v)}/></td>
+ <td><CheckField small checked={pattern.flags.utf_8_mode}
+ onChange={(v) => this.updateParam(() => pattern.flags.utf_8_mode = v)}/></td>
+ <td><CheckField small checked={pattern.flags.unicode_property}
+ onChange={(v) => this.updateParam(() => pattern.flags.unicode_property = v)}/></td>
+ <td style={{"width": "70px"}}>
+ <NumericField small value={pattern.min_occurrences}
+ active={this.state.patternOccurrencesFocused}
+ onChange={(v) => this.updateParam(() => pattern.min_occurrences = v)} />
+ </td>
+ <td style={{"width": "70px"}}>
+ <NumericField small value={pattern.max_occurrences}
+ active={this.state.patternOccurrencesFocused}
+ onChange={(v) => this.updateParam(() => pattern.max_occurrences = v)} />
+ </td>
+ <td><ChoiceField inline small keys={[0, 1, 2]} values={["both", "c->s", "s->c"]}
+ value={this.directions[pattern.direction]}
+ onChange={(v) => this.updateParam(() => pattern.direction = v)} /></td>
+ <td>{this.state.selectedPattern == null ?
+ <ButtonField variant="green" small name="add" inline rounded onClick={() => this.addPattern(p)}/> :
+ <ButtonField variant="green" small name="save" inline rounded onClick={() => this.updatePattern(p)}/>}
+ </td>
+ </tr>
+ :
+ <tr key={"new_pattern"} className="row-small">
+ <td>{p.regex}</td>
+ <td>{p.flags.caseless ? "yes": "no"}</td>
+ <td>{p.flags.dot_all ? "yes": "no"}</td>
+ <td>{p.flags.multi_line ? "yes": "no"}</td>
+ <td>{p.flags.utf_8_mode ? "yes": "no"}</td>
+ <td>{p.flags.unicode_property ? "yes": "no"}</td>
+ <td>{p.min_occurrences}</td>
+ <td>{p.max_occurrences}</td>
+ <td>{this.directions[p.direction]}</td>
+ {!isUpdate && <td><ButtonField variant="blue" small rounded name="edit"
+ onClick={() => this.editPattern(p) }/></td>}
+ </tr>
+ );
+
+ return (
+ <div className="pane-container rule-pane">
+ <div className="pane-section rules-list">
+ <div className="section-header">
+ <span className="api-request">GET /api/rules</span>
+ {this.state.rulesStatusCode &&
+ <span className="api-response"><LinkPopover text={this.state.rulesStatusCode}
+ content={this.state.rulesResponse}
+ placement="left" /></span>}
+ </div>
+
+ <div className="section-content">
+ <div className="section-table">
+ <Table borderless size="sm">
+ <thead>
+ <tr>
+ <th>id</th>
+ <th>name</th>
+ <th>color</th>
+ <th>notes</th>
+ </tr>
+ </thead>
+ <tbody>
+ {rules}
+ </tbody>
+ </Table>
+ </div>
+ </div>
+ </div>
+
+ <div className="pane-section rule-edit">
+ <div className="section-header">
+ <span className="api-request">
+ {isUpdate ? `PUT /api/rules/${this.state.selectedRule.id}` : "POST /api/rules"}
+ </span>
+ <span className="api-response"><LinkPopover text={this.state.ruleStatusCode}
+ content={this.state.ruleResponse}
+ placement="left" /></span>
+ </div>
+
+ <div className="section-content">
+ <Container className="p-0">
+ <Row>
+ <Col>
+ <InputField name="name" inline value={rule.name}
+ onChange={(v) => this.updateParam((r) => r.name = v)}
+ error={this.state.ruleNameError} />
+ <ColorField inline value={rule.color} error={this.state.ruleColorError}
+ onChange={(v) => this.updateParam((r) => r.color = v)} />
+ <TextField name="notes" rows={2} value={rule.notes}
+ onChange={(v) => this.updateParam((r) => r.notes = v)} />
+ </Col>
+
+ <Col style={{"paddingTop": "6px"}}>
+ <span>filters:</span>
+ <NumericField name="service_port" inline value={rule.filter.service_port}
+ onChange={(v) => this.updateParam((r) => r.filter.service_port = v)}
+ min={0} max={65565} error={this.state.ruleServicePortError}
+ readonly={isUpdate} />
+ <NumericField name="client_port" inline value={rule.filter.client_port}
+ onChange={(v) => this.updateParam((r) => r.filter.client_port = v)}
+ min={0} max={65565} error={this.state.ruleClientPortError}
+ readonly={isUpdate} />
+ <InputField name="client_address" value={rule.filter.client_address}
+ error={this.state.ruleClientAddressError} readonly={isUpdate}
+ onChange={(v) => this.updateParam((r) => r.filter.client_address = v)} />
+ </Col>
+
+ <Col style={{"paddingTop": "11px"}}>
+ <NumericField name="min_duration" inline value={rule.filter.min_duration}
+ error={this.state.ruleDurationError} readonly={isUpdate}
+ onChange={(v) => this.updateParam((r) => r.filter.min_duration = v)} />
+ <NumericField name="max_duration" inline value={rule.filter.max_duration}
+ error={this.state.ruleDurationError} readonly={isUpdate}
+ onChange={(v) => this.updateParam((r) => r.filter.max_duration = v)} />
+ <NumericField name="min_bytes" inline value={rule.filter.min_bytes}
+ error={this.state.ruleBytesError} readonly={isUpdate}
+ onChange={(v) => this.updateParam((r) => r.filter.min_bytes = v)} />
+ <NumericField name="max_bytes" inline value={rule.filter.max_bytes}
+ error={this.state.ruleBytesError} readonly={isUpdate}
+ onChange={(v) => this.updateParam((r) => r.filter.max_bytes = v)} />
+ </Col>
+ </Row>
+ </Container>
+
+ <div className="section-table">
+ <Table borderless size="sm">
+ <thead>
+ <tr>
+ <th>regex</th>
+ <th>!Aa</th>
+ <th>.*</th>
+ <th>\n+</th>
+ <th>UTF8</th>
+ <th>Uni_</th>
+ <th>min</th>
+ <th>max</th>
+ <th>direction</th>
+ {!isUpdate && <th>actions</th> }
+ </tr>
+ </thead>
+ <tbody>
+ {patterns}
+ </tbody>
+ </Table>
+ {this.state.rulePatternsError != null &&
+ <span className="table-error">error: {this.state.rulePatternsError}</span>}
+ </div>
+ </div>
+
+ <div className="section-footer">
+ {<ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>}
+ <ButtonField variant={isUpdate ? "blue" : "green"} name={isUpdate ? "update_rule" : "add_rule"}
+ bordered onClick={isUpdate ? this.updateRule : this.addRule} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+}
+
+export default RulePane;
diff --git a/frontend/src/components/panels/RulePane.scss b/frontend/src/components/panels/RulePane.scss
new file mode 100644
index 0000000..d45c366
--- /dev/null
+++ b/frontend/src/components/panels/RulePane.scss
@@ -0,0 +1,32 @@
+
+.rule-pane {
+ display: flex;
+ flex-direction: column;
+
+ .rules-list {
+ flex: 2 1;
+ overflow: hidden;
+
+ .section-content {
+ height: 100%;
+ }
+
+ .section-table {
+ height: calc(100% - 30px);
+ }
+ }
+
+ .rule-edit {
+ flex: 3 0;
+ display: flex;
+ flex-direction: column;
+
+ .section-content {
+ flex: 1;
+ }
+
+ .section-table {
+ max-height: 150px;
+ }
+ }
+}
diff --git a/frontend/src/components/panels/ServicePane.js b/frontend/src/components/panels/ServicePane.js
new file mode 100644
index 0000000..eaefa64
--- /dev/null
+++ b/frontend/src/components/panels/ServicePane.js
@@ -0,0 +1,190 @@
+import React, {Component} from 'react';
+import './common.scss';
+import './ServicePane.scss';
+import Table from "react-bootstrap/Table";
+import {Col, Container, Row} from "react-bootstrap";
+import InputField from "../fields/InputField";
+import TextField from "../fields/TextField";
+import backend from "../../backend";
+import NumericField from "../fields/extensions/NumericField";
+import ColorField from "../fields/extensions/ColorField";
+import ButtonField from "../fields/ButtonField";
+import validation from "../../validation";
+import LinkPopover from "../objects/LinkPopover";
+import {createCurlCommand} from "../../utils";
+
+const classNames = require('classnames');
+const _ = require('lodash');
+
+class ServicePane extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ services: [],
+ currentService: this.emptyService,
+ };
+ }
+
+ componentDidMount() {
+ this.reset();
+ this.loadServices();
+ }
+
+ emptyService = {
+ "port": 0,
+ "name": "",
+ "color": "",
+ "notes": ""
+ };
+
+ loadServices = () => {
+ backend.get("/api/services")
+ .then(res => this.setState({services: Object.values(res.json), servicesStatusCode: res.status}))
+ .catch(res => this.setState({servicesStatusCode: res.status, servicesResponse: JSON.stringify(res.json)}));
+ };
+
+ updateService = () => {
+ const service = this.state.currentService;
+ if (this.validateService(service)) {
+ backend.put("/api/services", service).then(res => {
+ this.reset();
+ this.setState({serviceStatusCode: res.status});
+ this.loadServices();
+ }).catch(res => {
+ this.setState({serviceStatusCode: res.status, serviceResponse: JSON.stringify(res.json)});
+ });
+ }
+ };
+
+ validateService = (service) => {
+ let valid = true;
+ if (!validation.isValidPort(service.port, true)) {
+ this.setState({servicePortError: "port < 0 || port > 65565"});
+ valid = false;
+ }
+ if (service.name.length < 3) {
+ this.setState({serviceNameError: "name.length < 3"});
+ valid = false;
+ }
+ if (!validation.isValidColor(service.color)) {
+ this.setState({serviceColorError: "color is not hexcolor"});
+ valid = false;
+ }
+
+ return valid;
+ };
+
+ reset = () => {
+ this.setState({
+ isUpdate: false,
+ currentService: _.cloneDeep(this.emptyService),
+ servicePortError: null,
+ serviceNameError: null,
+ serviceColorError: null,
+ serviceStatusCode: null,
+ servicesStatusCode: null,
+ serviceResponse: null,
+ servicesResponse: null
+ });
+ };
+
+ updateParam = (callback) => {
+ callback(this.state.currentService);
+ this.setState({currentService: this.state.currentService});
+ };
+
+ render() {
+ const isUpdate = this.state.isUpdate;
+ const service = this.state.currentService;
+
+ let services = this.state.services.map(s =>
+ <tr key={s.port} onClick={() => {
+ this.reset();
+ this.setState({isUpdate: true, currentService: _.cloneDeep(s)});
+ }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}>
+ <td>{s["port"]}</td>
+ <td>{s["name"]}</td>
+ <td><ButtonField name={s["color"]} color={s["color"]} small /></td>
+ <td>{s["notes"]}</td>
+ </tr>
+ );
+
+ const curlCommand = createCurlCommand("/services", "PUT", service);
+
+ return (
+ <div className="pane-container service-pane">
+ <div className="pane-section services-list">
+ <div className="section-header">
+ <span className="api-request">GET /api/services</span>
+ {this.state.servicesStatusCode &&
+ <span className="api-response"><LinkPopover text={this.state.servicesStatusCode}
+ content={this.state.servicesResponse}
+ placement="left" /></span>}
+ </div>
+
+ <div className="section-content">
+ <div className="section-table">
+ <Table borderless size="sm">
+ <thead>
+ <tr>
+ <th>port</th>
+ <th>name</th>
+ <th>color</th>
+ <th>notes</th>
+ </tr>
+ </thead>
+ <tbody>
+ {services}
+ </tbody>
+ </Table>
+ </div>
+ </div>
+ </div>
+
+ <div className="pane-section service-edit">
+ <div className="section-header">
+ <span className="api-request">PUT /api/services</span>
+ <span className="api-response"><LinkPopover text={this.state.serviceStatusCode}
+ content={this.state.serviceResponse}
+ placement="left" /></span>
+ </div>
+
+ <div className="section-content">
+ <Container className="p-0">
+ <Row>
+ <Col>
+ <NumericField name="port" value={service.port}
+ onChange={(v) => this.updateParam((s) => s.port = v)}
+ min={0} max={65565} error={this.state.servicePortError} />
+ <InputField name="name" value={service.name}
+ onChange={(v) => this.updateParam((s) => s.name = v)}
+ error={this.state.serviceNameError} />
+ <ColorField value={service.color} error={this.state.serviceColorError}
+ onChange={(v) => this.updateParam((s) => s.color = v)} />
+ </Col>
+
+ <Col>
+ <TextField name="notes" rows={7} value={service.notes}
+ onChange={(v) => this.updateParam((s) => s.notes = v)} />
+ </Col>
+ </Row>
+ </Container>
+
+ <TextField value={curlCommand} rows={3} readonly small={true}/>
+ </div>
+
+ <div className="section-footer">
+ {<ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>}
+ <ButtonField variant={isUpdate ? "blue" : "green"} name={isUpdate ? "update_service" : "add_service"}
+ bordered onClick={this.updateService} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+}
+
+export default ServicePane;
diff --git a/frontend/src/components/panels/ServicePane.scss b/frontend/src/components/panels/ServicePane.scss
new file mode 100644
index 0000000..0b154e6
--- /dev/null
+++ b/frontend/src/components/panels/ServicePane.scss
@@ -0,0 +1,22 @@
+
+.service-pane {
+ display: flex;
+ flex-direction: column;
+
+ .services-list {
+ flex: 1;
+ overflow: hidden;
+
+ .section-content {
+ height: 100%;
+ }
+
+ .section-table {
+ height: calc(100% - 30px);
+ }
+ }
+
+ .service-edit {
+ flex: 0;
+ }
+}
diff --git a/frontend/src/components/panels/common.scss b/frontend/src/components/panels/common.scss
new file mode 100644
index 0000000..ea8da94
--- /dev/null
+++ b/frontend/src/components/panels/common.scss
@@ -0,0 +1,101 @@
+@import '../../colors.scss';
+
+.pane-container {
+ background-color: $color-primary-3;
+ padding: 10px 10px 0;
+ height: 100%;
+
+ .pane-section {
+ margin-bottom: 10px;
+ background-color: $color-primary-0;
+
+ .section-header {
+ background-color: $color-primary-2;
+ padding: 5px 10px;
+ font-weight: 500;
+ font-size: 0.9em;
+ display: flex;
+
+ .api-request {
+ flex: 1;
+ }
+ }
+
+ .section-content {
+ padding: 10px;
+ }
+
+ .section-table {
+ position: relative;
+ overflow-y: scroll;
+
+ .table-error {
+ font-size: 0.8em;
+ color: $color-secondary-0;
+ margin-left: 10px;
+ }
+ }
+
+ table {
+ margin-bottom: 0;
+
+ tbody tr {
+ background-color: $color-primary-3;
+ border-top: 3px solid $color-primary-0;
+ border-bottom: 3px solid $color-primary-0;
+ }
+
+ th {
+ background-color: $color-primary-2;
+ font-size: 0.8em;
+ position: sticky;
+ top: 0;
+ padding: 2px 5px;
+ border: none;
+ }
+
+ .row-small {
+ font-size: 0.9em;
+ }
+
+ .row-clickable {
+ cursor: pointer;
+
+ &:hover {
+ background-color: $color-primary-0;
+ }
+ }
+
+ .row-selected {
+ background-color: $color-primary-0;
+ }
+ }
+ }
+
+ .section-footer {
+ display: flex;
+ padding: 10px;
+ background-color: $color-primary-0;
+ justify-content: flex-end;
+
+ .button-field {
+ margin-left: 10px;
+ }
+ }
+
+ .double-pane-container {
+ display: flex;
+
+ .pane-section {
+ flex: 1;
+ }
+
+ .pane-section:nth-child(1) {
+ margin-right: 5px;
+ }
+
+ .pane-section:nth-child(2) {
+ margin-left: 5px;
+ }
+ }
+}
diff --git a/frontend/src/index.scss b/frontend/src/index.scss
index 53ce4dd..9ba23de 100644
--- a/frontend/src/index.scss
+++ b/frontend/src/index.scss
@@ -11,8 +11,8 @@ body {
-moz-osx-font-smoothing: grayscale;
background-color: $color-primary-2;
color: $color-primary-4;
- height: 100%;
- max-height: 100%;
+ height: 100vh;
+ font-size: 100%;
}
pre {
@@ -20,70 +20,6 @@ pre {
font-size: 14px;
}
-.btn {
- border-radius: 0;
- background-color: $color-primary-2;
- border: none;
- border-bottom: 5px solid $color-primary-1;
- color: $color-primary-4;
- outline: none;
- padding: 5px 12px;
- font-weight: 500;
-
- &:hover,
- &:active {
- background-color: $color-primary-1;
- color: $color-primary-4;
- }
-
- &:focus,
- &:active {
- outline: none !important;
- box-shadow: none !important;
- }
-}
-
-.btn-sm {
- border: none;
- font-size: 12px;
-}
-
-.btn-red {
- color: $color-secondary-4;
- background-color: $color-secondary-2;
- border-bottom: 5px solid $color-secondary-1;
-
- &:hover,
- &:active {
- color: $color-secondary-4;
- background-color: $color-secondary-1;
- }
-}
-
-.btn-blue {
- color: $color-blue-light;
- background-color: $color-blue;
- border-bottom: 5px solid $color-blue-dark;
-
- &:hover,
- &:active {
- color: $color-blue-light;
- background-color: $color-blue-dark;
- }
-}
-
-.btn-green {
- color: $color-green-light;
- background-color: $color-green;
- border-bottom: 5px solid $color-green-dark;
-
- &:hover,
- &:active {
- color: $color-green-light;
- background-color: $color-green-dark;
- }
-}
-
a {
color: $color-primary-4;
@@ -130,36 +66,6 @@ a {
}
}
-textarea.form-control {
- resize: none;
-}
-
-input.form-control,
-textarea.form-control {
- background-color: $color-primary-2;
- border: none;
- color: $color-primary-4;
- font-family: 'Fira Code', monospace;
-
- &:focus {
- background-color: $color-primary-1;
- color: $color-primary-4;
- box-shadow: none;
- }
-
- &[readonly] {
- background-color: $color-primary-2;
- border: none;
- color: $color-primary-4;
- }
-
- &[readonly]:focus {
- background-color: $color-primary-1;
- color: $color-primary-4;
- box-shadow: none;
- }
-}
-
.table {
color: $color-primary-4;
}
@@ -167,3 +73,7 @@ textarea.form-control {
.text-muted {
color: $color-primary-4 !important;
}
+
+.popover-header {
+ color: $color-primary-1;
+} \ No newline at end of file
diff --git a/frontend/src/utils.js b/frontend/src/utils.js
index 7381f69..e71067a 100644
--- a/frontend/src/utils.js
+++ b/frontend/src/utils.js
@@ -1,9 +1,19 @@
const timeRegex = /^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$/;
-export function createCurlCommand(subCommand, data) {
- let full = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
- return `curl --request PUT \\\n --url ${full}/api${subCommand} \\\n ` +
- `--header 'content-type: application/json' \\\n --data '${JSON.stringify(data)}'`;
+export function createCurlCommand(subCommand, method = null, json = null, data = null) {
+ const full = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
+
+ let contentType = null;
+ let content = null;
+ if (json != null) {
+ contentType = ' -H "Content-Type: application/json" \\\n';
+ content = ` -d '${JSON.stringify(json)}'`;
+ } else if (data != null) {
+ contentType = ' -H "Content-Type: multipart/form-data" \\\n';
+ content = " " + Object.entries(data).map(([key, value]) => `-F "${key}=${value}"`).join(" \\\n ");
+ }
+
+ return `curl${method != null ? " -X " + method : ""} "${full}/api${subCommand}" \\\n` + contentType + "" + content;
}
export function validateIpAddress(ipAddress) {
@@ -64,3 +74,40 @@ export function timestampToDateTime(timestamp) {
let d = new Date(timestamp);
return d.toLocaleDateString() + " " + d.toLocaleTimeString();
}
+
+export function dateTimeToTime(dateTime) {
+ if (typeof dateTime === "string") {
+ dateTime = new Date(dateTime);
+ }
+
+ let hours = dateTime.getHours();
+ let minutes = "0" + dateTime.getMinutes();
+ let seconds = "0" + dateTime.getSeconds();
+ return hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);
+}
+
+export function durationBetween(from, to) {
+ if (typeof from === "string") {
+ from = new Date(from);
+ }
+ if (typeof to === "string") {
+ to = new Date(to);
+ }
+ const duration = ((to - from) / 1000).toFixed(3);
+
+ return (duration > 1000 || duration < -1000) ? "∞" : duration + "s";
+}
+
+export function formatSize(size) {
+ if (size < 1000) {
+ return `${size}`;
+ } else if (size < 1000000) {
+ return `${(size / 1000).toFixed(1)}K`;
+ } else {
+ return `${(size / 1000000).toFixed(1)}M`;
+ }
+}
+
+export function randomClassName() {
+ return Math.random().toString(36).slice(2);
+}
diff --git a/frontend/src/validation.js b/frontend/src/validation.js
new file mode 100644
index 0000000..8f3409f
--- /dev/null
+++ b/frontend/src/validation.js
@@ -0,0 +1,8 @@
+
+const validation = {
+ isValidColor: (color) => /^#(?:[0-9a-fA-F]{3}){1,2}$/.test(color),
+ isValidPort: (port, required) => parseInt(port) > (required ? 0 : -1) && parseInt(port) <= 65565,
+ isValidAddress: (address) => true // TODO
+};
+
+export default validation;
diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js
index 6c101fa..fb4454c 100644
--- a/frontend/src/views/App.js
+++ b/frontend/src/views/App.js
@@ -1,49 +1,46 @@
import React, {Component} from 'react';
+import './App.scss';
import Header from "./Header";
-import MainPane from "./MainPane";
+import MainPane from "../components/panels/MainPane";
import Footer from "./Footer";
-import {BrowserRouter as Router, Route, Switch} from "react-router-dom";
-import Services from "./Services";
+import {BrowserRouter as Router} from "react-router-dom";
import Filters from "./Filters";
-import Rules from "./Rules";
+import backend from "../backend";
+import ConfigurationPane from "../components/panels/ConfigurationPane";
class App extends Component {
constructor(props) {
super(props);
- this.state = {
- servicesWindowOpen: false,
- filterWindowOpen: false,
- rulesWindowOpen: false
- };
+
+ this.state = {};
+ }
+
+ componentDidMount() {
+ backend.get("/api/services").then(_ => this.setState({configured: true}));
}
render() {
let modal;
- if (this.state.servicesWindowOpen) {
- modal = <Services onHide={() => this.setState({servicesWindowOpen: false})}/>;
- }
- if (this.state.filterWindowOpen) {
+ if (this.state.filterWindowOpen && this.state.configured) {
modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>;
}
- if (this.state.rulesWindowOpen) {
- modal = <Rules onHide={() => this.setState({rulesWindowOpen: false})}/>;
- }
return (
- <div className="app">
+ <div className="main">
<Router>
- <Header onOpenServices={() => this.setState({servicesWindowOpen: true})}
- onOpenFilters={() => this.setState({filterWindowOpen: true})}
- onOpenRules={() => this.setState({rulesWindowOpen: true})} />
- <Switch>
- <Route path="/connections/:id" children={<MainPane/>}/>
- <Route path="/" children={<MainPane/>}/>
- </Switch>
- {modal}
- <Footer/>
+ <div className="main-header">
+ <Header onOpenFilters={() => this.setState({filterWindowOpen: true})} />
+ </div>
+ <div className="main-content">
+ {this.state.configured ? <MainPane /> :
+ <ConfigurationPane onConfigured={() => this.setState({configured: true})} />}
+ {modal}
+ </div>
+ <div className="main-footer">
+ {this.state.configured && <Footer/>}
+ </div>
</Router>
-
</div>
);
}
diff --git a/frontend/src/views/App.scss b/frontend/src/views/App.scss
new file mode 100644
index 0000000..b25d4c9
--- /dev/null
+++ b/frontend/src/views/App.scss
@@ -0,0 +1,15 @@
+
+.main {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+
+ .main-content {
+ flex: 1 1;
+ overflow: hidden;
+ }
+
+ .main-header, .main-footer {
+ flex: 0 0;
+ }
+}
diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js
index 62733d7..73979c4 100644
--- a/frontend/src/views/Connections.js
+++ b/frontend/src/views/Connections.js
@@ -1,10 +1,11 @@
import React, {Component} from 'react';
import './Connections.scss';
-import axios from 'axios';
import Connection from "../components/Connection";
import Table from 'react-bootstrap/Table';
import {Redirect} from 'react-router';
import {withRouter} from "react-router-dom";
+import backend from "../backend";
+import ConnectionMatchedRules from "../components/ConnectionMatchedRules";
class Connections extends Component {
@@ -25,21 +26,21 @@ class Connections extends Component {
this.scrollBottomThreashold = 0.99999;
this.maxConnections = 500;
this.queryLimit = 50;
-
- this.handleScroll = this.handleScroll.bind(this);
- this.connectionSelected = this.connectionSelected.bind(this);
- this.addServicePortFilter = this.addServicePortFilter.bind(this);
}
componentDidMount() {
this.loadConnections({limit: this.queryLimit})
.then(() => this.setState({loaded: true}));
+ if (this.props.initialConnection != null) {
+ this.setState({selected: this.props.initialConnection.id});
+ // TODO: scroll to initial connection
+ }
}
- connectionSelected(c) {
+ connectionSelected = (c) => {
this.setState({selected: c.id});
this.props.onSelected(c);
- }
+ };
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.loaded && prevProps.location.search !== this.props.location.search) {
@@ -49,7 +50,7 @@ class Connections extends Component {
}
}
- handleScroll(e) {
+ handleScroll = (e) => {
let relativeScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight);
if (!this.state.loading && relativeScroll > this.scrollBottomThreashold) {
this.loadConnections({from: this.state.lastConnection.id, limit: this.queryLimit,})
@@ -59,13 +60,23 @@ class Connections extends Component {
this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,})
.then(() => console.log("Previous connections loaded"));
}
- }
+ };
- addServicePortFilter(port) {
- let urlParams = new URLSearchParams(this.props.location.search);
+ addServicePortFilter = (port) => {
+ const urlParams = new URLSearchParams(this.props.location.search);
urlParams.set("service_port", port);
this.setState({queryString: "?" + urlParams});
- }
+ };
+
+ addMatchedRulesFilter = (matchedRule) => {
+ const urlParams = new URLSearchParams(this.props.location.search);
+ const oldMatchedRules = urlParams.getAll("matched_rules") || [];
+
+ if (!oldMatchedRules.includes(matchedRule)) {
+ urlParams.append("matched_rules", matchedRule);
+ this.setState({queryString: "?" + urlParams});
+ }
+ };
async loadConnections(params) {
let url = "/api/connections";
@@ -75,15 +86,15 @@ class Connections extends Component {
}
this.setState({loading: true, prevParams: params});
- let res = await axios.get(`${url}?${urlParams}`);
+ let res = (await backend.get(`${url}?${urlParams}`)).json;
let connections = this.state.connections;
let firstConnection = this.state.firstConnection;
let lastConnection = this.state.lastConnection;
if (params !== undefined && params.from !== undefined) {
- if (res.data.length > 0) {
- connections = this.state.connections.concat(res.data);
+ if (res.length > 0) {
+ connections = this.state.connections.concat(res);
lastConnection = connections[connections.length - 1];
if (connections.length > this.maxConnections) {
connections = connections.slice(connections.length - this.maxConnections,
@@ -92,8 +103,8 @@ class Connections extends Component {
}
}
} else if (params !== undefined && params.to !== undefined) {
- if (res.data.length > 0) {
- connections = res.data.concat(this.state.connections);
+ if (res.length > 0) {
+ connections = res.concat(this.state.connections);
firstConnection = connections[0];
if (connections.length > this.maxConnections) {
connections = connections.slice(0, this.maxConnections);
@@ -101,8 +112,8 @@ class Connections extends Component {
}
}
} else {
- if (res.data.length > 0) {
- connections = res.data;
+ if (res.length > 0) {
+ connections = res;
firstConnection = connections[0];
lastConnection = connections[connections.length - 1];
} else {
@@ -112,21 +123,15 @@ class Connections extends Component {
}
}
- let flagRule = this.state.flagRule;
let rules = this.state.rules;
- if (flagRule === null) {
- let res = await axios.get("/api/rules");
- rules = res.data;
- flagRule = rules.filter(rule => {
- return rule.name === "flag";
- })[0];
+ if (rules === null) {
+ rules = (await backend.get("/api/rules")).json;
}
this.setState({
loading: false,
connections: connections,
- rules: res.data,
- flagRule: flagRule,
+ rules: rules,
firstConnection: firstConnection,
lastConnection: lastConnection
});
@@ -134,7 +139,7 @@ class Connections extends Component {
render() {
let redirect;
- let queryString = this.state.queryString !== null ? this.state.queryString : ""
+ let queryString = this.state.queryString !== null ? this.state.queryString : "";
if (this.state.selected) {
let format = this.props.match.params.format;
format = format !== undefined ? "/" + format : "";
@@ -159,6 +164,7 @@ class Connections extends Component {
<th>srcport</th>
<th>dstip</th>
<th>dstport</th>
+ <th>started_at</th>
<th>duration</th>
<th>up</th>
<th>down</th>
@@ -167,13 +173,18 @@ class Connections extends Component {
</thead>
<tbody>
{
- this.state.connections.map(c =>
- <Connection key={c.id} data={c} onSelected={() => this.connectionSelected(c)}
- selected={this.state.selected === c.id} onMarked={marked => c.marked = marked}
- onEnabled={enabled => c.hidden = !enabled}
- containsFlag={c.matched_rules.includes(this.state.flagRule.id)}
- addServicePortFilter={this.addServicePortFilter}/>
- )
+ this.state.connections.flatMap(c => {
+ return [<Connection key={c.id} data={c} onSelected={() => this.connectionSelected(c)}
+ selected={this.state.selected === c.id}
+ onMarked={marked => c.marked = marked}
+ onEnabled={enabled => c.hidden = !enabled}
+ addServicePortFilter={this.addServicePortFilter} />,
+ c.matched_rules.length > 0 &&
+ <ConnectionMatchedRules key={c.id + "_m"} matchedRules={c.matched_rules}
+ rules={this.state.rules}
+ addMatchedRulesFilter={this.addMatchedRulesFilter} />
+ ];
+ })
}
{loading}
</tbody>
diff --git a/frontend/src/views/Filters.js b/frontend/src/views/Filters.js
index 78f0342..ba7d467 100644
--- a/frontend/src/views/Filters.js
+++ b/frontend/src/views/Filters.js
@@ -1,7 +1,7 @@
import React, {Component} from 'react';
-import './Services.scss';
-import {Button, Col, Container, Modal, Row, Table} from "react-bootstrap";
+import {Col, Container, Modal, Row, Table} from "react-bootstrap";
import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions";
+import ButtonField from "../components/fields/ButtonField";
class Filters extends Component {
@@ -89,7 +89,7 @@ class Filters extends Component {
</Container>
</Modal.Body>
<Modal.Footer className="dialog-footer">
- <Button variant="red" onClick={this.props.onHide}>close</Button>
+ <ButtonField variant="red" bordered onClick={this.props.onHide} name="close" />
</Modal.Footer>
</Modal>
);
diff --git a/frontend/src/views/Footer.js b/frontend/src/views/Footer.js
index b6ffd9d..0a3c5a3 100644
--- a/frontend/src/views/Footer.js
+++ b/frontend/src/views/Footer.js
@@ -8,11 +8,11 @@ class Footer extends Component {
<footer className="footer container-fluid">
<div className="row">
<div className="col-12">
- <div className="footer-timeline">timeline</div>
+ <div className="footer-timeline">timeline - <a href="https://github.com/eciavatta/caronte/issues/12">to be implemented</a></div>
</div>
</div>
</footer>
- )
+ );
}
}
diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js
index 5d0f690..944f1d5 100644
--- a/frontend/src/views/Header.js
+++ b/frontend/src/views/Header.js
@@ -1,8 +1,9 @@
import React, {Component} from 'react';
import Typed from 'typed.js';
import './Header.scss';
-import {Button} from "react-bootstrap";
import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions";
+import {Link} from "react-router-dom";
+import ButtonField from "../components/fields/ButtonField";
class Header extends Component {
@@ -45,7 +46,7 @@ class Header extends Component {
render() {
let quickFilters = filtersNames.filter(name => this.state[`${name}_active`])
- .map(name => filtersDefinitions[name])
+ .map(name => <React.Fragment key={name} >{filtersDefinitions[name]}</React.Fragment>)
.slice(0, 5);
return (
@@ -60,21 +61,26 @@ class Header extends Component {
</div>
<div className="col-auto">
- <div className="filters-bar-wrapper">
- <div className="filters-bar">
- {quickFilters}
- </div>
+ <div className="filters-bar">
+ {quickFilters}
</div>
</div>
<div className="col">
<div className="header-buttons">
- <Button onClick={this.props.onOpenFilters}>filters</Button>
- <Button variant="yellow" size="sm">pcaps</Button>
- <Button variant="blue" onClick={this.props.onOpenRules}>rules</Button>
- <Button variant="red" onClick={this.props.onOpenServices}>
- services
- </Button>
+ <ButtonField variant="pink" onClick={this.props.onOpenFilters} name="filters" bordered />
+ <Link to="/pcaps">
+ <ButtonField variant="purple" name="pcaps" bordered />
+ </Link>
+ <Link to="/rules">
+ <ButtonField variant="deep-purple" name="rules" bordered />
+ </Link>
+ <Link to="/services">
+ <ButtonField variant="indigo" name="services" bordered />
+ </Link>
+ <Link to="/config">
+ <ButtonField variant="blue" name="config" bordered />
+ </Link>
</div>
</div>
</div>
diff --git a/frontend/src/views/Header.scss b/frontend/src/views/Header.scss
index e84e758..15d1375 100644
--- a/frontend/src/views/Header.scss
+++ b/frontend/src/views/Header.scss
@@ -14,12 +14,21 @@
}
.header-buttons {
- margin: 5px 0;
- text-align: right;
+ margin: 7px 0;
+ display: flex;
+ justify-content: flex-end;
+
+ .button-field {
+ margin-left: 7px;
+ }
}
- .filters-bar-wrapper {
- height: 50px;
- padding: 8px 0;
+ .filters-bar {
+ padding: 3px 0;
+
+ .filter {
+ display: inline-block;
+ margin-right: 10px;
+ }
}
}
diff --git a/frontend/src/views/MainPane.js b/frontend/src/views/MainPane.js
deleted file mode 100644
index 69de725..0000000
--- a/frontend/src/views/MainPane.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import React, {Component} from 'react';
-import './MainPane.scss';
-import Connections from "./Connections";
-import ConnectionContent from "../components/ConnectionContent";
-import {withRouter} from "react-router-dom";
-import axios from 'axios';
-
-class MainPane extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- selectedConnection: null
- };
- }
-
- componentDidMount() {
- if ('id' in this.props.match.params) {
- const id = this.props.match.params.id;
- axios.get(`/api/connections/${id}`).then(res => {
- if (res.status === 200) {
- this.setState({selectedConnection: res.data});
- }
- });
- }
- }
-
- render() {
- return (
- <div className="main-pane">
- <div className="container-fluid">
- <div className="row">
- <div className="col-md-6 pane">
- <Connections onSelected={(c) => this.setState({selectedConnection: c})} />
- </div>
- <div className="col-md-6 pl-0 pane">
- <ConnectionContent connection={this.state.selectedConnection}/>
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
-
-export default withRouter(MainPane);
diff --git a/frontend/src/views/MainPane.scss b/frontend/src/views/MainPane.scss
deleted file mode 100644
index 20720ba..0000000
--- a/frontend/src/views/MainPane.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.main-pane {
- .pane {
- height: calc(100vh - 210px);
- position: relative;
- }
-}
diff --git a/frontend/src/views/Rules.js b/frontend/src/views/Rules.js
deleted file mode 100644
index 3424410..0000000
--- a/frontend/src/views/Rules.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import React, {Component} from 'react';
-import './Services.scss';
-import {Button, ButtonGroup, Col, Container, Form, FormControl, InputGroup, Modal, Row, Table} from "react-bootstrap";
-import axios from "axios";
-
-class Rules extends Component {
-
- constructor(props) {
- super(props);
-
- this.state = {
- rules: []
- };
- }
-
- componentDidMount() {
- this.loadRules();
- }
-
- loadRules() {
- axios.get("/api/rules").then(res => this.setState({rules: res.data}));
- }
-
- render() {
- let rulesRows = this.state.rules.map(rule =>
- <tr key={rule.id}>
- <td><Button variant="btn-edit" size="sm"
- style={{"backgroundColor": rule.color}}>edit</Button></td>
- <td>{rule.name}</td>
- </tr>
- );
-
-
- return (
- <Modal
- {...this.props}
- show="true"
- size="lg"
- aria-labelledby="rules-dialog"
- centered
- >
- <Modal.Header>
- <Modal.Title id="rules-dialog">
- ~/rules
- </Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <Container>
- <Row>
- <Col md={7}>
- <Table borderless size="sm" className="rules-list">
- <thead>
- <tr>
- <th><Button size="sm" >new</Button></th>
- <th>name</th>
- </tr>
- </thead>
- <tbody>
- {rulesRows}
-
- </tbody>
- </Table>
- </Col>
- <Col md={5}>
- <Form>
- <Form.Group controlId="servicePort">
- <Form.Label>port:</Form.Label>
- <Form.Control type="text" />
- </Form.Group>
-
- <Form.Group controlId="serviceName">
- <Form.Label>name:</Form.Label>
- <Form.Control type="text" />
- </Form.Group>
-
- <Form.Group controlId="serviceColor">
- <Form.Label>color:</Form.Label>
- <ButtonGroup aria-label="Basic example">
-
- </ButtonGroup>
- <ButtonGroup aria-label="Basic example">
-
- </ButtonGroup>
- </Form.Group>
-
- <Form.Group controlId="serviceNotes">
- <Form.Label>notes:</Form.Label>
- <Form.Control as="textarea" rows={3} />
- </Form.Group>
- </Form>
-
-
- </Col>
-
- </Row>
-
- <Row>
- <Col md={12}>
- <InputGroup>
- <FormControl as="textarea" rows={4} className="curl-output" readOnly={true}
- />
- </InputGroup>
-
- </Col>
- </Row>
-
-
- </Container>
- </Modal.Body>
- <Modal.Footer className="dialog-footer">
- <Button variant="red" onClick={this.props.onHide}>close</Button>
- </Modal.Footer>
- </Modal>
- );
- }
-}
-
-export default Rules;
diff --git a/frontend/src/views/Services.js b/frontend/src/views/Services.js
deleted file mode 100644
index b95b01c..0000000
--- a/frontend/src/views/Services.js
+++ /dev/null
@@ -1,210 +0,0 @@
-import React, {Component} from 'react';
-import './Services.scss';
-import {Button, ButtonGroup, Col, Container, Form, FormControl, InputGroup, Modal, Row, Table} from "react-bootstrap";
-import axios from 'axios'
-import {createCurlCommand} from '../utils';
-
-class Services extends Component {
-
- constructor(props) {
- super(props);
- this.alphabet = 'abcdefghijklmnopqrstuvwxyz';
- this.colors = ["#E53935", "#D81B60", "#8E24AA", "#5E35B1", "#3949AB", "#1E88E5", "#039BE5", "#00ACC1",
- "#00897B", "#43A047", "#7CB342", "#9E9D24", "#F9A825", "#FB8C00", "#F4511E", "#6D4C41"];
-
- this.state = {
- services: {},
- port: 0,
- portValid: false,
- name: "",
- nameValid: false,
- color: this.colors[0],
- colorValid: false,
- notes: ""
- };
-
- this.portChanged = this.portChanged.bind(this);
- this.nameChanged = this.nameChanged.bind(this);
- this.notesChanged = this.notesChanged.bind(this);
- this.newService = this.newService.bind(this);
- this.editService = this.editService.bind(this);
- this.saveService = this.saveService.bind(this);
- this.loadServices = this.loadServices.bind(this);
- }
-
- componentDidMount() {
- this.loadServices();
- }
-
- portChanged(event) {
- let value = event.target.value.replace(/[^\d]/gi, '');
- let port = 0;
- if (value !== "") {
- port = parseInt(value);
- }
- this.setState({port: port});
- }
-
- nameChanged(event) {
- let value = event.target.value.replace(/[\s]/gi, '_').replace(/[^\w]/gi, '').toLowerCase();
- this.setState({name: value});
- }
-
- notesChanged(event) {
- this.setState({notes: event.target.value});
- }
-
- newService() {
- this.setState({name: "", port: 0, notes: ""});
- }
-
- editService(service) {
- this.setState({name: service.name, port: service.port, color: service.color, notes: service.notes});
- }
-
- saveService() {
- if (this.state.portValid && this.state.nameValid) {
- axios.put("/api/services", {
- name: this.state.name,
- port: this.state.port,
- color: this.state.color,
- notes: this.state.notes
- });
-
- this.newService();
- this.loadServices();
- }
- }
-
- loadServices() {
- axios.get("/api/services").then(res => this.setState({services: res.data}));
- }
-
- componentDidUpdate(prevProps, prevState, snapshot) {
- if (this.state.name != null && prevState.name !== this.state.name) {
- this.setState({
- nameValid: this.state.name.length >= 3
- });
- }
- if (prevState.port !== this.state.port) {
- this.setState({
- portValid: this.state.port > 0 && this.state.port <= 65565
- });
- }
- }
-
- render() {
- let output = "";
- if (!this.state.portValid) {
- output += "assert(1 <= port <= 65565)\n";
- }
- if (!this.state.nameValid) {
- output += "assert(len(name) >= 3)\n";
- }
- if (output === "") {
- output = createCurlCommand("/services", {
- "port": this.state.port,
- "name": this.state.name,
- "color": this.state.color,
- "notes": this.state.notes
- });
- }
- let rows = Object.values(this.state.services).map(s =>
- <tr>
- <td><Button variant="btn-edit" size="sm"
- onClick={() => this.editService(s)} style={{"backgroundColor": s.color}}>edit</Button></td>
- <td>{s.port}</td>
- <td>{s.name}</td>
- </tr>
- );
-
- let colorButtons = this.colors.map((color, i) =>
- <Button size="sm" className="btn-color" key={"button" + this.alphabet[i]}
- style={{"backgroundColor": color, "borderColor": this.state.color === color ? "#fff" : color}}
- onClick={() => this.setState({color: color})}>{this.alphabet[i]}</Button>);
-
- return (
- <Modal
- {...this.props}
- show="true"
- size="lg"
- aria-labelledby="services-dialog"
- centered
- >
- <Modal.Header>
- <Modal.Title id="services-dialog">
- ~/services
- </Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <Container>
- <Row>
- <Col md={7}>
- <Table borderless size="sm" className="services-list">
- <thead>
- <tr>
- <th><Button size="sm" onClick={this.newService}>new</Button></th>
- <th>port</th>
- <th>name</th>
- </tr>
- </thead>
- <tbody>
- {rows}
- </tbody>
- </Table>
- </Col>
- <Col md={5}>
- <Form>
- <Form.Group controlId="servicePort">
- <Form.Label>port:</Form.Label>
- <Form.Control type="text" onChange={this.portChanged} value={this.state.port}/>
- </Form.Group>
-
- <Form.Group controlId="serviceName">
- <Form.Label>name:</Form.Label>
- <Form.Control type="text" onChange={this.nameChanged} value={this.state.name}/>
- </Form.Group>
-
- <Form.Group controlId="serviceColor">
- <Form.Label>color:</Form.Label>
- <ButtonGroup aria-label="Basic example">
- {colorButtons.slice(0, 8)}
- </ButtonGroup>
- <ButtonGroup aria-label="Basic example">
- {colorButtons.slice(8, 18)}
- </ButtonGroup>
- </Form.Group>
-
- <Form.Group controlId="serviceNotes">
- <Form.Label>notes:</Form.Label>
- <Form.Control as="textarea" rows={3} onChange={this.notesChanged}
- value={this.state.notes}/>
- </Form.Group>
- </Form>
-
-
- </Col>
-
- </Row>
-
- <Row>
- <Col md={12}>
- <InputGroup>
- <FormControl as="textarea" rows={4} className="curl-output" readOnly={true}
- value={output}/>
- </InputGroup>
-
- </Col>
- </Row>
- </Container>
- </Modal.Body>
- <Modal.Footer className="dialog-footer">
- <Button variant="green" onClick={this.saveService}>save</Button>
- <Button variant="red" onClick={this.props.onHide}>close</Button>
- </Modal.Footer>
- </Modal>
- );
- }
-}
-
-export default Services;
diff --git a/frontend/src/views/Services.scss b/frontend/src/views/Services.scss
deleted file mode 100644
index 2abb55e..0000000
--- a/frontend/src/views/Services.scss
+++ /dev/null
@@ -1,32 +0,0 @@
-@import '../colors.scss';
-
-.curl-output {
- width: 100%;
- font-size: 13px;
-}
-
-.services-list {
- .btn {
- width: 70px;
- }
-
- td {
- background-color: $color-primary-2;
- border-top: 2px solid $color-primary-0;
- vertical-align: middle;
- }
-
- th {
- background-color: $color-primary-1;
- }
-}
-
-.btn-color {
- border: 3px solid #fff;
-}
-
-.dialog-footer {
- .btn {
- width: 80px;
- }
-}
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index b7fc583..6d111a3 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1166,24 +1166,24 @@
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
-"@fortawesome/fontawesome-common-types@^0.2.30":
- version "0.2.30"
- resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.30.tgz#2f1cc5b46bd76723be41d0013a8450c9ba92b777"
- integrity sha512-TsRwpTuKwFNiPhk1UfKgw7zNPeV5RhNp2Uw3pws+9gDAkPGKrtjR1y2lI3SYn7+YzyfuNknflpBA1LRKjt7hMg==
+"@fortawesome/fontawesome-common-types@^0.2.31":
+ version "0.2.31"
+ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.31.tgz#f15a39e5ab4e5dfda0717733bcacb9580e666ad9"
+ integrity sha512-xfnPyH6NN5r/h1/qDYoGB0BlHSID902UkQzxR8QsoKDh55KAPr8ruAoie12WQEEQT8lRE2wtV7LoUllJ1HqCag==
"@fortawesome/fontawesome-svg-core@^1.2.28":
- version "1.2.30"
- resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.30.tgz#f56dc6791861fe5d1af04fb8abddb94658c576db"
- integrity sha512-E3sAXATKCSVnT17HYmZjjbcmwihrNOCkoU7dVMlasrcwiJAHxSKeZ+4WN5O+ElgO/FaYgJmASl8p9N7/B/RttA==
+ version "1.2.31"
+ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.31.tgz#23fff9c521f1b57e79e4c2fd6cce8aa794e9d99e"
+ integrity sha512-lqUWRK+ylHQJG5Kiez4XrAZAfc7snxCc+X59quL3xPfMnxzfyf1lt+/hD7X1ZL4KlzAH2KFzMuEVrolo/rAkog==
dependencies:
- "@fortawesome/fontawesome-common-types" "^0.2.30"
+ "@fortawesome/fontawesome-common-types" "^0.2.31"
"@fortawesome/free-solid-svg-icons@^5.13.0":
- version "5.14.0"
- resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.14.0.tgz#970453f5e8c4915ad57856c3a0252ac63f6fec18"
- integrity sha512-M933RDM8cecaKMWDSk3FRYdnzWGW7kBBlGNGfvqLVwcwhUPNj9gcw+xZMrqBdRqxnSXdl3zWzTCNNGEtFUq67Q==
+ version "5.15.0"
+ resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.0.tgz#6f553f25cdebd7d5ae778598c2ddd3321dd63450"
+ integrity sha512-4dGRsOnGBPM7c0fd3LuiU6LgRSLn01rw1LJ370yC2iFMLUcLCLLynZhQbMhsiJmMwQM/YmPQblAdyHKVCgsIAA==
dependencies:
- "@fortawesome/fontawesome-common-types" "^0.2.30"
+ "@fortawesome/fontawesome-common-types" "^0.2.31"
"@fortawesome/react-fontawesome@^0.1.9":
version "0.1.11"
@@ -1407,9 +1407,9 @@
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@popperjs/core@^2.0.0":
- version "2.4.4"
- resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.4.tgz#11d5db19bd178936ec89cd84519c4de439574398"
- integrity sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg==
+ version "2.5.3"
+ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.5.3.tgz#4982b0b66b7a4cf949b86f5d25a8cf757d3cfd9d"
+ integrity sha512-RFwCobxsvZ6j7twS7dHIZQZituMIDJJNHS/qY6iuthVebxS3zhRY+jaC2roEKiAYaVuTcGmX6Luc6YBcf6zJVg==
"@restart/context@^2.1.4":
version "2.1.4"
@@ -1533,9 +1533,9 @@
loader-utils "^1.2.3"
"@testing-library/dom@*":
- version "7.24.2"
- resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.2.tgz#6d2b7dd21efbd5358b98c2777fc47c252f3ae55e"
- integrity sha512-ERxcZSoHx0EcN4HfshySEWmEf5Kkmgi+J7O79yCJ3xggzVlBJ2w/QjJUC+EBkJJ2OeSw48i3IoePN4w8JlVUIA==
+ version "7.24.3"
+ resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.3.tgz#dae3071463cf28dc7755b43d9cf2202e34cbb85d"
+ integrity sha512-6eW9fUhEbR423FZvoHRwbWm9RUUByLWGayYFNVvqTnQLYvsNpBS4uEuKH9aqr3trhxFwGVneJUonehL3B1sHJw==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/runtime" "^7.10.3"
@@ -1593,9 +1593,9 @@
integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A==
"@types/babel__core@^7.1.0":
- version "7.1.9"
- resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d"
- integrity sha512-sY2RsIJ5rpER1u3/aQ8OFSI7qGIy8o1NEEbgb2UaJcvOtXOMpd39ko723NBpjQFg9SIX7TXtjejZVGeIMLhoOw==
+ version "7.1.10"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.10.tgz#ca58fc195dd9734e77e57c6f2df565623636ab40"
+ integrity sha512-x8OM8XzITIMyiwl5Vmo2B1cR1S1Ipkyv4mdlbJjMa1lmuKvKY9FrBbEANIaMlnWn5Rf7uO+rC/VgYabNkE17Hw==
dependencies:
"@babel/parser" "^7.1.0"
"@babel/types" "^7.0.0"
@@ -1604,24 +1604,24 @@
"@types/babel__traverse" "*"
"@types/babel__generator@*":
- version "7.6.1"
- resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.1.tgz#4901767b397e8711aeb99df8d396d7ba7b7f0e04"
- integrity sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==
+ version "7.6.2"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.2.tgz#f3d71178e187858f7c45e30380f8f1b7415a12d8"
+ integrity sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==
dependencies:
"@babel/types" "^7.0.0"
"@types/babel__template@*":
- version "7.0.2"
- resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307"
- integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==
+ version "7.0.3"
+ resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.3.tgz#b8aaeba0a45caca7b56a5de9459872dde3727214"
+ integrity sha512-uCoznIPDmnickEi6D0v11SBpW0OuVqHJCa7syXqQHy5uktSCreIlt0iglsCnmvz8yCb38hGcWeseA8cWJSwv5Q==
dependencies:
"@babel/parser" "^7.1.0"
"@babel/types" "^7.0.0"
"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6":
- version "7.0.14"
- resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.14.tgz#e99da8c075d4fb098c774ba65dabf7dc9954bd13"
- integrity sha512-8w9szzKs14ZtBVuP6Wn7nMLRJ0D6dfB0VEBEyRgxrZ/Ln49aNMykrghM2FaNn4FJRzNppCSa0Rv9pBRM5Xc3wg==
+ version "7.0.15"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz#db9e4238931eb69ef8aab0ad6523d4d4caa39d03"
+ integrity sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A==
dependencies:
"@babel/types" "^7.3.0"
@@ -1691,9 +1691,9 @@
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/node@*":
- version "14.10.2"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-14.10.2.tgz#9b47a2c8e4dabd4db73b57e750b24af689600514"
- integrity sha512-IzMhbDYCpv26pC2wboJ4MMOa9GKtjplXfcAqrMeNJpUUwpM/2ATt2w1JPUXwS6spu856TvKZL2AOmeU2rAxskw==
+ version "14.11.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256"
+ integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA==
"@types/parse-json@^4.0.0":
version "4.0.0"
@@ -1771,16 +1771,16 @@
integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==
"@types/yargs@^13.0.0":
- version "13.0.10"
- resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.10.tgz#e77bf3fc73c781d48c2eb541f87c453e321e5f4b"
- integrity sha512-MU10TSgzNABgdzKvQVW1nuuT+sgBMWeXNc3XOs5YXV5SDAK+PPja2eUuBNB9iqElu03xyEDqlnGw0jgl4nbqGQ==
+ version "13.0.11"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.11.tgz#def2f0c93e4bdf2c61d7e34899b17e34be28d3b1"
+ integrity sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==
dependencies:
"@types/yargs-parser" "*"
"@types/yargs@^15.0.0":
- version "15.0.5"
- resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.5.tgz#947e9a6561483bdee9adffc983e91a6902af8b79"
- integrity sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==
+ version "15.0.7"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.7.tgz#dad50a7a234a35ef9460737a56024287a3de1d2b"
+ integrity sha512-Gf4u3EjaPNcC9cTu4/j2oN14nSVhr8PQ+BvBcBQHAhDZfl0bVIiLgvnRXv/dn58XhTm9UXvBpvJpDlwV65QxOA==
dependencies:
"@types/yargs-parser" "*"
@@ -2393,13 +2393,6 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428"
integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==
-axios@^0.19.2:
- version "0.19.2"
- resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
- integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
- dependencies:
- follow-redirects "1.5.10"
-
axobject-query@^2.0.2:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -2792,15 +2785,20 @@ browserslist@4.10.0:
pkg-up "^3.1.0"
browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.6.2, browserslist@^4.6.4, browserslist@^4.8.5, browserslist@^4.9.1:
- version "4.14.2"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.2.tgz#1b3cec458a1ba87588cc5e9be62f19b6d48813ce"
- integrity sha512-HI4lPveGKUR0x2StIz+2FXfDk9SfVMrxn6PLh1JeGUwcuoDkdKZebWiyLRJ68iIPDpMI4JLVDf7S7XzslgWOhw==
+ version "4.14.5"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015"
+ integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA==
dependencies:
- caniuse-lite "^1.0.30001125"
- electron-to-chromium "^1.3.564"
- escalade "^3.0.2"
+ caniuse-lite "^1.0.30001135"
+ electron-to-chromium "^1.3.571"
+ escalade "^3.1.0"
node-releases "^1.1.61"
+bs-custom-file-input@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/bs-custom-file-input/-/bs-custom-file-input-1.3.4.tgz#c275cb8d4f1c02ba026324292509fa9a747dbda8"
+ integrity sha512-NBsQzTnef3OW1MvdKBbMHAYHssCd613MSeJV7z2McXznWtVMnJCy7Ckyc+PwxV6Pk16cu6YBcYWh/ZE0XWNKCA==
+
bser@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05"
@@ -2977,10 +2975,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125:
- version "1.0.30001131"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001131.tgz#afad8a28fc2b7a0d3ae9407e71085a0ead905d54"
- integrity sha512-4QYi6Mal4MMfQMSqGIRPGbKIbZygeN83QsWq1ixpUwvtfgAZot5BrCKzGygvZaV+CnELdTwD0S4cqUNozq7/Cw==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135:
+ version "1.0.30001140"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001140.tgz#30dae27599f6ede2603a0962c82e468bca894232"
+ integrity sha512-xFtvBtfGrpjTOxTpjP5F2LmN04/ZGfYV8EQzUIC/RmKpdrmzJrjqlJ4ho7sGuAMPko2/Jl08h7x9uObCfBFaAA==
capture-exit@^2.0.0:
version "2.0.0"
@@ -3807,13 +3805,6 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
dependencies:
ms "2.0.0"
-debug@=3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
- integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
- dependencies:
- ms "2.0.0"
-
debug@^3.1.1, debug@^3.2.5:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@@ -3822,11 +3813,11 @@ debug@^3.1.1, debug@^3.2.5:
ms "^2.1.1"
debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
- integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1"
+ integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==
dependencies:
- ms "^2.1.1"
+ ms "2.1.2"
decamelize@^1.1.2, decamelize@^1.2.0:
version "1.2.0"
@@ -4021,9 +4012,9 @@ dom-accessibility-api@^0.3.0:
integrity sha512-PzwHEmsRP3IGY4gv/Ug+rMeaTIyTJvadCb+ujYXYeIylbHJezIyNToe8KfEgHTCEYyC+/bUghYOGg8yMGlZ6vA==
dom-accessibility-api@^0.5.1:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.2.tgz#ef3cdb5d3f0d599d8f9c8b18df2fb63c9793739d"
- integrity sha512-k7hRNKAiPJXD2aBqfahSo4/01cTsKWXf+LqJgglnkN2Nz8TsxXKQBXHhKe0Ye9fEfHEZY49uSA5Sr3AqP/sWKA==
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.3.tgz#0ea493c924d4070dfbf531c4aaca3d7a2c601aab"
+ integrity sha512-yfqzAi1GFxK6EoJIZKgxqJyK6j/OjEFEUi2qkNThD/kUhoCFSG1izq31B5xuxzbJBGw9/67uPtkPMYAzWL7L7Q==
dom-converter@^0.2:
version "0.2.0"
@@ -4146,10 +4137,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
-electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.564:
- version "1.3.570"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.570.tgz#3f5141cc39b4e3892a276b4889980dabf1d29c7f"
- integrity sha512-Y6OCoVQgFQBP5py6A/06+yWxUZHDlNr/gNDGatjH8AZqXl8X0tE4LfjLJsXGz/JmWJz8a6K7bR1k+QzZ+k//fg==
+electron-to-chromium@^1.3.378, electron-to-chromium@^1.3.571:
+ version "1.3.576"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.576.tgz#2e70234484e03d7c7e90310d7d79fd3775379c34"
+ integrity sha512-uSEI0XZ//5ic+0NdOqlxp0liCD44ck20OAGyLMSymIWTEAtHKVJi6JM18acOnRgUgX7Q65QqnI+sNncNvIy8ew==
elliptic@^6.5.3:
version "6.5.3"
@@ -4247,20 +4238,20 @@ es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstrac
string.prototype.trimstart "^1.0.1"
es-abstract@^1.18.0-next.0:
- version "1.18.0-next.0"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.0.tgz#b302834927e624d8e5837ed48224291f2c66e6fc"
- integrity sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==
+ version "1.18.0-next.1"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
+ integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
dependencies:
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
- is-callable "^1.2.0"
+ is-callable "^1.2.2"
is-negative-zero "^2.0.0"
is-regex "^1.1.1"
object-inspect "^1.8.0"
object-keys "^1.1.1"
- object.assign "^4.1.0"
+ object.assign "^4.1.1"
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
@@ -4299,7 +4290,7 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3:
d "^1.0.1"
ext "^1.1.2"
-escalade@^3.0.2:
+escalade@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e"
integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==
@@ -4922,13 +4913,6 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
-follow-redirects@1.5.10:
- version "1.5.10"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
- integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
- dependencies:
- debug "=3.1.0"
-
follow-redirects@^1.0.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
@@ -5853,10 +5837,10 @@ is-buffer@^1.0.2, is-buffer@^1.1.5:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
-is-callable@^1.1.4, is-callable@^1.2.0:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.1.tgz#4d1e21a4f437509d25ce55f8184350771421c96d"
- integrity sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==
+is-callable@^1.1.4, is-callable@^1.2.0, is-callable@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9"
+ integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==
is-ci@^2.0.0:
version "2.0.0"
@@ -6986,7 +6970,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.5, lodash@~4.17.10:
+"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5, lodash@~4.17.10:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@@ -7210,11 +7194,16 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-mime-db@1.44.0, "mime-db@>= 1.43.0 < 2":
+mime-db@1.44.0:
version "1.44.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
+"mime-db@>= 1.43.0 < 2":
+ version "1.45.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
+ integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
+
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24:
version "2.1.27"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
@@ -7371,7 +7360,7 @@ ms@2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
-ms@^2.1.1:
+ms@2.1.2, ms@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
@@ -7692,7 +7681,7 @@ object-visit@^1.0.0:
dependencies:
isobject "^3.0.0"
-object.assign@^4.1.0:
+object.assign@^4.1.0, object.assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd"
integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==
@@ -7778,9 +7767,9 @@ onetime@^5.1.0:
mimic-fn "^2.1.0"
open@^7.0.2:
- version "7.2.1"
- resolved "https://registry.yarnpkg.com/open/-/open-7.2.1.tgz#07b0ade11a43f2a8ce718480bdf3d7563a095195"
- integrity sha512-xbYCJib4spUdmcs0g/2mK1nKo/jO2T7INClWd/beL7PFkXRWgr8B23ssDHX/USPn2M2IjDR5UdpYs6I67SnTSA==
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/open/-/open-7.3.0.tgz#45461fdee46444f3645b6e14eb3ca94b82e1be69"
+ integrity sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw==
dependencies:
is-docker "^2.0.0"
is-wsl "^2.1.1"
@@ -8464,9 +8453,9 @@ postcss-lab-function@^2.0.1:
postcss-values-parser "^2.0.0"
postcss-load-config@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.0.tgz#c84d692b7bb7b41ddced94ee62e8ab31b417b003"
- integrity sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a"
+ integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==
dependencies:
cosmiconfig "^5.0.0"
import-cwd "^2.0.0"
@@ -8840,13 +8829,14 @@ postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4:
uniq "^1.0.1"
postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
- version "6.0.2"
- resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
- integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3"
+ integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==
dependencies:
cssesc "^3.0.0"
indexes-of "^1.0.1"
uniq "^1.0.1"
+ util-deprecate "^1.0.2"
postcss-svgo@^4.0.2:
version "4.0.2"
@@ -8896,9 +8886,9 @@ postcss@7.0.21:
supports-color "^6.1.0"
postcss@^7, postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.23, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6:
- version "7.0.32"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d"
- integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==
+ version "7.0.35"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24"
+ integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
@@ -9566,9 +9556,9 @@ regexpp@^3.0.0:
integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==
regexpu-core@^4.7.0:
- version "4.7.0"
- resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
- integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==
+ version "4.7.1"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
+ integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==
dependencies:
regenerate "^1.4.0"
regenerate-unicode-properties "^8.2.0"
@@ -10301,9 +10291,9 @@ spdx-expression-parse@^3.0.0:
spdx-license-ids "^3.0.0"
spdx-license-ids@^3.0.0:
- version "3.0.5"
- resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
- integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce"
+ integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==
spdy-transport@^3.0.0:
version "3.0.0"
@@ -11118,7 +11108,7 @@ use@^3.1.0:
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
diff --git a/rules_manager.go b/rules_manager.go
index 36379e6..a5dc7ce 100644
--- a/rules_manager.go
+++ b/rules_manager.go
@@ -106,7 +106,7 @@ func LoadRulesManager(storage Storage, flagRegex string) (RulesManager, error) {
if len(rulesManager.rules) == 0 {
if _, err := rulesManager.AddRule(context.Background(), Rule{
Name: "flag",
- Color: "#ff0000",
+ Color: "#E53935",
Notes: "Mark connections where the flag is stolen",
Patterns: []Pattern{
{Regex: flagRegex, Direction: DirectionToClient},