aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components/panels
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-09-29 16:56:00 +0000
committerEmiliano Ciavatta2020-09-29 16:56:00 +0000
commitd994a21a0dfae9ee026e8aa3ccdee6c213c523aa (patch)
treea9d343300d3b9587bdaa664ef9f005ddc9529656 /frontend/src/components/panels
parent7f4cc5d3f3f92338a464853c182b9d6a3ea850eb (diff)
Complete rules page
Diffstat (limited to 'frontend/src/components/panels')
-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
5 files changed, 434 insertions, 123 deletions
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;
+ }
+ }
+}