aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-09-29 16:56:00 +0000
committerEmiliano Ciavatta2020-09-29 16:56:00 +0000
commitd994a21a0dfae9ee026e8aa3ccdee6c213c523aa (patch)
treea9d343300d3b9587bdaa664ef9f005ddc9529656
parent7f4cc5d3f3f92338a464853c182b9d6a3ea850eb (diff)
Complete rules page
-rw-r--r--frontend/package.json1
-rw-r--r--frontend/src/backend.js31
-rw-r--r--frontend/src/components/ConnectionContent.js4
-rw-r--r--frontend/src/components/fields/ButtonField.js4
-rw-r--r--frontend/src/components/fields/ButtonField.scss2
-rw-r--r--frontend/src/components/fields/InputField.js13
-rw-r--r--frontend/src/components/fields/extensions/NumericField.js40
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.js2
-rw-r--r--frontend/src/components/filters/StringConnectionsFilter.js2
-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/PcapPane.js7
-rw-r--r--frontend/src/components/panels/PcapPane.scss47
-rw-r--r--frontend/src/components/panels/RulePane.js388
-rw-r--r--frontend/src/components/panels/RulePane.scss30
-rw-r--r--frontend/src/components/panels/common.scss85
-rw-r--r--frontend/src/validation.js8
-rw-r--r--frontend/src/views/App.js6
-rw-r--r--frontend/src/views/Config.js2
-rw-r--r--frontend/src/views/Connections.js4
-rw-r--r--frontend/src/views/Header.js1
-rw-r--r--frontend/src/views/MainPane.js2
-rw-r--r--frontend/src/views/Rules.js118
23 files changed, 536 insertions, 301 deletions
diff --git a/frontend/package.json b/frontend/package.json
index 3629e70..b13a7ea 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -13,6 +13,7 @@
"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/src/backend.js b/frontend/src/backend.js
index a02f7a8..2fbe920 100644
--- a/frontend/src/backend.js
+++ b/frontend/src/backend.js
@@ -1,5 +1,5 @@
-async function json(method, url, data, headers, returnJson) {
+async function json(method, url, data, headers) {
const options = {
method: method,
mode: "cors",
@@ -14,18 +14,17 @@ async function json(method, url, data, headers, returnJson) {
if (data != null) {
options.body = JSON.stringify(data);
}
- const result = await fetch(url, options);
- if (returnJson) {
- if (result.status >= 200 && result.status < 300) {
- return result.json();
- } else {
- return Promise.reject({
- response: result,
- json: await result.json()
- });
- }
- } else {
+ 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);
}
}
@@ -56,16 +55,16 @@ const backend = {
return json("DELETE", url, data, headers);
},
getJson: (url = "", headers = null) => {
- return json("GET", url, null, headers, true);
+ return json("GET", url, null, headers);
},
postJson: (url = "", data = null, headers = null) => {
- return json("POST", url, data, headers, true);
+ return json("POST", url, data, headers);
},
putJson: (url = "", data = null, headers = null) => {
- return json("PUT", url, data, headers, true);
+ return json("PUT", url, data, headers);
},
deleteJson: (url = "", data = null, headers = null) => {
- return json("DELETE", url, data, headers, true);
+ return json("DELETE", url, data, headers);
},
postFile: (url = "", data = null, headers = null) => {
return file(url, data, headers);
diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js
index 318965c..a9d34d3 100644
--- a/frontend/src/components/ConnectionContent.js
+++ b/frontend/src/components/ConnectionContent.js
@@ -1,6 +1,6 @@
import React, {Component} from 'react';
import './ConnectionContent.scss';
-import {Dropdown, Row} from 'react-bootstrap';
+import {Row} from 'react-bootstrap';
import MessageAction from "./MessageAction";
import backend from "../backend";
import ButtonField from "./fields/ButtonField";
@@ -42,7 +42,7 @@ class ConnectionContent extends Component {
// TODO: limit workaround.
backend.getJson(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => {
this.setState({
- connectionContent: res,
+ connectionContent: res.json,
loading: false
});
});
diff --git a/frontend/src/components/fields/ButtonField.js b/frontend/src/components/fields/ButtonField.js
index f2f02fd..cc32b0f 100644
--- a/frontend/src/components/fields/ButtonField.js
+++ b/frontend/src/components/fields/ButtonField.js
@@ -6,10 +6,6 @@ const classNames = require('classnames');
class ButtonField extends Component {
- constructor(props) {
- super(props);
- }
-
render() {
const handler = () => {
if (typeof this.props.onClick === "function") {
diff --git a/frontend/src/components/fields/ButtonField.scss b/frontend/src/components/fields/ButtonField.scss
index aabe80f..cfd20ff 100644
--- a/frontend/src/components/fields/ButtonField.scss
+++ b/frontend/src/components/fields/ButtonField.scss
@@ -11,7 +11,7 @@
font-size: 0.8em;
button {
- padding: 4px 12px;
+ padding: 3px 12px;
}
}
diff --git a/frontend/src/components/fields/InputField.js b/frontend/src/components/fields/InputField.js
index 6cf967a..b790891 100644
--- a/frontend/src/components/fields/InputField.js
+++ b/frontend/src/components/fields/InputField.js
@@ -20,16 +20,17 @@ class InputField extends Component {
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 defaultValue = this.props.defaultValue || null;
+
const handler = (e) => {
- if (this.props.onChange) {
+ 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("");
+ this.props.onChange(defaultValue);
} else {
this.props.onChange(e.target.value);
}
@@ -37,7 +38,7 @@ class InputField extends Component {
};
let inputProps = {};
if (type !== "file") {
- inputProps["value"] = value || this.props.initialValue;
+ inputProps["value"] = value || defaultValue;
}
return (
@@ -52,8 +53,8 @@ class InputField extends Component {
<div className="field-input">
<div className="field-value">
{ type === "file" && <label for={this.id} className={"file-label"}>
- {value.name || defaultValue}</label> }
- <input type={type} placeholder={defaultValue} id={this.id}
+ {value.name || this.props.placeholder}</label> }
+ <input type={type} placeholder={this.props.placeholder} id={this.id}
aria-describedby={this.id} onChange={handler} {...inputProps} />
</div>
{ type !== "file" && value !== "" &&
diff --git a/frontend/src/components/fields/extensions/NumericField.js b/frontend/src/components/fields/extensions/NumericField.js
index 8823c42..ed81ed7 100644
--- a/frontend/src/components/fields/extensions/NumericField.js
+++ b/frontend/src/components/fields/extensions/NumericField.js
@@ -11,25 +11,31 @@ class NumericField extends Component {
};
}
- render() {
- const handler = (value) => {
- value = value.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 (this.props.onChange) {
- this.props.onChange(intValue);
- }
- };
+ 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={handler} initialValue={this.props.initialValue || 0}
+ <InputField {...this.props} onChange={this.onChange} defaultValue={this.props.defaultValue || "0"}
invalid={this.state.invalid} />
);
}
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js
index 0741bea..f4d0b1c 100644
--- a/frontend/src/components/filters/RulesConnectionsFilter.js
+++ b/frontend/src/components/filters/RulesConnectionsFilter.js
@@ -25,7 +25,7 @@ class RulesConnectionsFilter extends Component {
let activeRules = params.getAll("matched_rules") || [];
backend.getJson("/api/rules").then(res => {
- let rules = res.flatMap(rule => rule.enabled ? [{id: rule.id, name: rule.name}] : []);
+ 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 a304198..f463593 100644
--- a/frontend/src/components/filters/StringConnectionsFilter.js
+++ b/frontend/src/components/filters/StringConnectionsFilter.js
@@ -115,7 +115,7 @@ class StringConnectionsFilter extends Component {
return (
<div className="filter" style={{"width": `${this.props.width}px`}}>
<InputField active={active} invalid={this.state.invalidValue} name={this.props.filterName}
- defaultValue={this.props.defaultFilterValue} onChange={this.filterChanged}
+ placeholder={this.props.defaultFilterValue} onChange={this.filterChanged}
value={this.state.fieldValue} inline={true} small={true} />
{redirect}
</div>
diff --git a/frontend/src/components/objects/LinkPopover.js b/frontend/src/components/objects/LinkPopover.js
new file mode 100644
index 0000000..58b2f6a
--- /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" 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/PcapPane.js b/frontend/src/components/panels/PcapPane.js
index a491dff..cfba037 100644
--- a/frontend/src/components/panels/PcapPane.js
+++ b/frontend/src/components/panels/PcapPane.js
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
-import './PcapPane.scss';
+// import './PcapPane.scss';
+import './common.scss';
import Table from "react-bootstrap/Table";
import backend from "../../backend";
import {createCurlCommand, formatSize, timestampToTime2} from "../../utils";
@@ -37,7 +38,7 @@ class PcapPane extends Component {
}
loadSessions = () => {
- backend.getJson("/api/pcap/sessions").then(res => this.setState({sessions: res}));
+ backend.getJson("/api/pcap/sessions").then(res => this.setState({sessions: res.json}));
};
handleUploadFileChange = (file) => {
@@ -104,7 +105,7 @@ class PcapPane extends Component {
<td>{s["processed_packets"]}</td>
<td>{s["invalid_packets"]}</td>
<td>undefined</td>
- <td className="table-cell-action"><a target="_blank"
+ <td className="table-cell-action"><a target="_blank" rel="noopener noreferrer"
href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a>
</td>
</tr>
diff --git a/frontend/src/components/panels/PcapPane.scss b/frontend/src/components/panels/PcapPane.scss
index ce28227..3c4d10b 100644
--- a/frontend/src/components/panels/PcapPane.scss
+++ b/frontend/src/components/panels/PcapPane.scss
@@ -1,51 +1,10 @@
@import '../../colors.scss';
.pane-container {
- background-color: $color-primary-3;
- padding: 10px 10px 0;
- height: 100%;
- .section-header {
- background-color: $color-primary-2;
- padding: 5px 10px;
- height: 31px;
-
- font-weight: 500;
- font-size: 14px;
-
- .api-response {
- float: right;
- }
- }
-
- .section-table {
- margin-top: 10px;
-
- .table-row {
- background-color: $color-primary-0;
- border-top: 3px solid $color-primary-3;
- border-bottom: 3px solid $color-primary-3;
- }
-
- .table-cell-action {
- font-size: 13px;
- font-weight: 600;
- }
- }
-
- .section-content {
- background-color: $color-primary-0;
- padding: 10px;
- }
-
- th {
- background-color: $color-primary-2;
- border-top: 3px solid $color-primary-3;
- border-bottom: 3px solid $color-primary-3;
- font-size: 13.5px;
- position: sticky;
- top: 10px;
- padding: 5px;
+ .table-cell-action {
+ font-size: 13px;
+ font-weight: 600;
}
.upload-actions {
diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js
index 2e91d91..01e37fa 100644
--- a/frontend/src/components/panels/RulePane.js
+++ b/frontend/src/components/panels/RulePane.js
@@ -1,4 +1,5 @@
import React, {Component} from 'react';
+import './common.scss';
import './RulePane.scss';
import Table from "react-bootstrap/Table";
import {Col, Container, Row} from "react-bootstrap";
@@ -10,6 +11,11 @@ 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";
+
+const classNames = require('classnames');
+const _ = require('lodash');
class RulePane extends Component {
@@ -18,103 +24,352 @@ class RulePane extends Component {
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.getJson("/api/rules").then(res => this.setState({rules: res}));
+ backend.getJson("/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.postJson("/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.putJson(`/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 className="table-row">
+ <tr 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>
- {/*<td>{((new Date(s["completed_at"]) - new Date(s["started_at"])) / 1000).toFixed(3)}s</td>*/}
- {/*<td>{formatSize(s["size"])}</td>*/}
- {/*<td>{s["processed_packets"]}</td>*/}
- {/*<td>{s["invalid_packets"]}</td>*/}
- {/*<td>undefined</td>*/}
- {/*<td className="table-cell-action"><a target="_blank"*/}
- {/* href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a>*/}
- {/*</td>*/}
+ </tr>
+ );
+
+ let patterns = (this.state.selectedPattern == null && !isUpdate ?
+ rule.patterns.concat(this.state.newPattern) :
+ rule.patterns
+ ).map(p => p === pattern ?
+ <tr>
+ <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 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">
+ <div className="pane-section rules-list">
<div className="section-header">
<span className="api-request">GET /api/rules</span>
- <span className="api-response">200 OK</span>
+ {this.state.rulesStatusCode &&
+ <span className="api-response"><LinkPopover text={this.state.rulesStatusCode}
+ content={this.state.rulesResponse}
+ placement="left" /></span>}
</div>
- <div className="section-table">
- <Table borderless size="sm">
- <thead>
- <tr>
- <th>id</th>
- <th>name</th>
- <th>notes</th>
- </tr>
- </thead>
- <tbody>
- {rules}
- </tbody>
- </Table>
+ <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">
+ <div className="pane-section rule-edit">
<div className="section-header">
- <span className="api-request">POST /api/rules</span>
- <span className="api-response"></span>
+ <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 />
- <ColorField value={this.state.test1} onChange={(e) => this.setState({test1: e})} inline />
- <TextField name="notes" rows={2} />
+ <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>
- <div >filters:</div>
- <NumericField name="service_port" inline value={this.state.test} onChange={(e) => this.setState({test: e})} validate={(e) => e%2 === 0} />
-
- <NumericField name="client_port" inline />
- <InputField name="client_address" />
+ <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} />
+ <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} />
+ <InputField name="client_address" value={rule.filter.client_address}
+ error={this.state.ruleClientAddressError}
+ onChange={(v) => this.updateParam((r) => r.filter.client_address = v)} />
</Col>
- <Col>
- <NumericField name="min_duration" inline />
- <NumericField name="max_duration" inline />
- <NumericField name="min_bytes" inline />
- <NumericField name="max_bytes" inline />
-
+ <Col style={{"paddingTop": "11px"}}>
+ <NumericField name="min_duration" inline value={rule.filter.min_duration}
+ error={this.state.ruleDurationError}
+ onChange={(v) => this.updateParam((r) => r.filter.min_duration = v)} />
+ <NumericField name="max_duration" inline value={rule.filter.max_duration}
+ error={this.state.ruleDurationError}
+ onChange={(v) => this.updateParam((r) => r.filter.max_duration = v)} />
+ <NumericField name="min_bytes" inline value={rule.filter.min_bytes}
+ error={this.state.ruleBytesError}
+ onChange={(v) => this.updateParam((r) => r.filter.min_bytes = v)} />
+ <NumericField name="max_bytes" inline value={rule.filter.max_bytes}
+ error={this.state.ruleBytesError}
+ onChange={(v) => this.updateParam((r) => r.filter.max_bytes = v)} />
</Col>
</Row>
</Container>
- <div className="post-rules-actions">
- <label>options:</label>
- <div className="rules-options">
- <CheckField name={"enabled"} />
- </div>
-
- <ButtonField variant="blue" name="clear" bordered />
- <ButtonField variant="green" name="add_rule" bordered />
- </div>
-
- patterns:
<div className="section-table">
<Table borderless size="sm">
<thead>
@@ -128,29 +383,24 @@ class RulePane extends Component {
<th>min</th>
<th>max</th>
<th>direction</th>
- <th>actions</th>
+ {!isUpdate && <th>actions</th> }
</tr>
</thead>
<tbody>
- <tr>
- <td style={{"width": "500px"}}><InputField small /></td>
- <td><CheckField small /></td>
- <td><CheckField small /></td>
- <td><CheckField small /></td>
- <td><CheckField small /></td>
- <td><CheckField small /></td>
- <td style={{"width": "70px"}}><NumericField small /></td>
- <td style={{"width": "70px"}}><NumericField small /></td>
- <td><ChoiceField inline small keys={[0, 1, 2]} values={["both", "c->s", "s->c"]} value="both" /></td>
- <td><ButtonField variant="green" small name="add" inline rounded /></td>
- </tr>
+ {patterns}
</tbody>
</Table>
+ {this.state.rulePatternsError != null &&
+ <span className="table-error">error: {this.state.rulePatternsError}</span>}
</div>
</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>
);
}
diff --git a/frontend/src/components/panels/RulePane.scss b/frontend/src/components/panels/RulePane.scss
index b030c6a..d45c366 100644
--- a/frontend/src/components/panels/RulePane.scss
+++ b/frontend/src/components/panels/RulePane.scss
@@ -1,16 +1,32 @@
.rule-pane {
- .post-rules-actions {
- display: flex;
+ display: flex;
+ flex-direction: column;
- .rules-options {
- flex: 1;
+ .rules-list {
+ flex: 2 1;
+ overflow: hidden;
+
+ .section-content {
+ height: 100%;
}
- button {
- margin-left: 10px;
+ .section-table {
+ height: calc(100% - 30px);
}
}
+ .rule-edit {
+ flex: 3 0;
+ display: flex;
+ flex-direction: column;
-} \ No newline at end of file
+ .section-content {
+ flex: 1;
+ }
+
+ .section-table {
+ max-height: 150px;
+ }
+ }
+}
diff --git a/frontend/src/components/panels/common.scss b/frontend/src/components/panels/common.scss
new file mode 100644
index 0000000..cab0de1
--- /dev/null
+++ b/frontend/src/components/panels/common.scss
@@ -0,0 +1,85 @@
+@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;
+ }
+ }
+}
diff --git a/frontend/src/validation.js b/frontend/src/validation.js
new file mode 100644
index 0000000..eac8774
--- /dev/null
+++ b/frontend/src/validation.js
@@ -0,0 +1,8 @@
+
+const validation = {
+ isValidColor: (color) => true, // TODO
+ 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 ccfdb3a..8bd5e46 100644
--- a/frontend/src/views/App.js
+++ b/frontend/src/views/App.js
@@ -5,7 +5,6 @@ import Footer from "./Footer";
import {BrowserRouter as Router} from "react-router-dom";
import Services from "./Services";
import Filters from "./Filters";
-import Rules from "./Rules";
import Config from "./Config";
class App extends Component {
@@ -15,7 +14,6 @@ class App extends Component {
this.state = {
servicesWindowOpen: false,
filterWindowOpen: false,
- rulesWindowOpen: false,
configWindowOpen: false,
configDone: false
};
@@ -40,9 +38,6 @@ class App extends Component {
if (this.state.filterWindowOpen) {
modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>;
}
- if (this.state.rulesWindowOpen) {
- modal = <Rules onHide={() => this.setState({rulesWindowOpen: false})}/>;
- }
if (this.state.configWindowOpen) {
modal = <Config onHide={() => this.setState({configWindowOpen: false})}
onDone={() => this.setState({configDone: true})}/>;
@@ -53,7 +48,6 @@ class App extends Component {
<Router>
<Header onOpenServices={() => this.setState({servicesWindowOpen: true})}
onOpenFilters={() => this.setState({filterWindowOpen: true})}
- onOpenRules={() => this.setState({rulesWindowOpen: true})}
onOpenConfig={() => this.setState({configWindowOpen: true})}
onOpenUpload={() => this.setState({uploadWindowOpen: true})}
onConfigDone={this.state.configDone}
diff --git a/frontend/src/views/Config.js b/frontend/src/views/Config.js
index a770378..b11f827 100644
--- a/frontend/src/views/Config.js
+++ b/frontend/src/views/Config.js
@@ -68,8 +68,6 @@ class Config extends Component {
})
};
- let msg = "";
-
fetch('/setup', requestOptions)
.then(response => {
if (response.status === 202 ){
diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js
index f3fec64..64068c5 100644
--- a/frontend/src/views/Connections.js
+++ b/frontend/src/views/Connections.js
@@ -75,7 +75,7 @@ class Connections extends Component {
}
this.setState({loading: true, prevParams: params});
- let res = await backend.getJson(`${url}?${urlParams}`);
+ let res = (await backend.getJson(`${url}?${urlParams}`)).json;
let connections = this.state.connections;
let firstConnection = this.state.firstConnection;
@@ -115,7 +115,7 @@ class Connections extends Component {
let flagRule = this.state.flagRule;
let rules = this.state.rules;
if (flagRule === null) {
- rules = await backend.getJson("/api/rules");
+ rules = (await backend.getJson("/api/rules")).json;
flagRule = rules.filter(rule => {
return rule.name === "flag";
})[0];
diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js
index 0b82011..06cb20e 100644
--- a/frontend/src/views/Header.js
+++ b/frontend/src/views/Header.js
@@ -1,7 +1,6 @@
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";
diff --git a/frontend/src/views/MainPane.js b/frontend/src/views/MainPane.js
index 9d3f7b7..6f4e3cd 100644
--- a/frontend/src/views/MainPane.js
+++ b/frontend/src/views/MainPane.js
@@ -22,7 +22,7 @@ class MainPane extends Component {
if (match != null) {
this.setState({loading: true});
backend.getJson(`/api/connections/${match[1]}`)
- .then(connection => this.setState({selectedConnection: connection, loading: false}))
+ .then(res => this.setState({selectedConnection: res.json, loading: false}))
.catch(error => console.log(error));
}
}
diff --git a/frontend/src/views/Rules.js b/frontend/src/views/Rules.js
deleted file mode 100644
index bbc3bb6..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 backend from "../backend";
-
-class Rules extends Component {
-
- constructor(props) {
- super(props);
-
- this.state = {
- rules: []
- };
- }
-
- componentDidMount() {
- this.loadRules();
- }
-
- loadRules() {
- backend.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;