From d994a21a0dfae9ee026e8aa3ccdee6c213c523aa Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Tue, 29 Sep 2020 18:56:00 +0200 Subject: Complete rules page --- frontend/src/components/panels/PcapPane.js | 7 +- frontend/src/components/panels/PcapPane.scss | 47 +--- frontend/src/components/panels/RulePane.js | 388 ++++++++++++++++++++++----- frontend/src/components/panels/RulePane.scss | 30 ++- frontend/src/components/panels/common.scss | 85 ++++++ 5 files changed, 434 insertions(+), 123 deletions(-) create mode 100644 frontend/src/components/panels/common.scss (limited to 'frontend/src/components/panels') 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 { {s["processed_packets"]} {s["invalid_packets"]} undefined - download 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 => - + { + this.reset(); + this.setState({selectedRule: _.cloneDeep(r)}); + }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id })}> {r["id"].substring(0, 8)} {r["name"]} + {r["notes"]} - {/*{((new Date(s["completed_at"]) - new Date(s["started_at"])) / 1000).toFixed(3)}s*/} - {/*{formatSize(s["size"])}*/} - {/*{s["processed_packets"]}*/} - {/*{s["invalid_packets"]}*/} - {/*undefined*/} - {/*download*/} - {/**/} + + ); + + let patterns = (this.state.selectedPattern == null && !isUpdate ? + rule.patterns.concat(this.state.newPattern) : + rule.patterns + ).map(p => p === pattern ? + + + { + this.updateParam(() => pattern.regex = v); + this.setState({patternRegexFocused: pattern.regex === ""}); + }} /> + + this.updateParam(() => pattern.flags.caseless = v)}/> + this.updateParam(() => pattern.flags.dot_all = v)}/> + this.updateParam(() => pattern.flags.multi_line = v)}/> + this.updateParam(() => pattern.flags.utf_8_mode = v)}/> + this.updateParam(() => pattern.flags.unicode_property = v)}/> + + this.updateParam(() => pattern.min_occurrences = v)} /> + + + this.updateParam(() => pattern.max_occurrences = v)} /> + + s", "s->c"]} + value={this.directions[pattern.direction]} + onChange={(v) => this.updateParam(() => pattern.direction = v)} /> + {this.state.selectedPattern == null ? + this.addPattern(p)}/> : + this.updatePattern(p)}/>} + + + : + + {p.regex} + {p.flags.caseless ? "yes": "no"} + {p.flags.dot_all ? "yes": "no"} + {p.flags.multi_line ? "yes": "no"} + {p.flags.utf_8_mode ? "yes": "no"} + {p.flags.unicode_property ? "yes": "no"} + {p.min_occurrences} + {p.max_occurrences} + {this.directions[p.direction]} + {!isUpdate && this.editPattern(p) }/>} ); return (
-
+
GET /api/rules - 200 OK + {this.state.rulesStatusCode && + }
-
- - - - - - - - - - {rules} - -
idnamenotes
+
+
+ + + + + + + + + + + {rules} + +
idnamecolornotes
+
-
+
- POST /api/rules - + + {isUpdate ? `PUT /api/rules/${this.state.selectedRule.id}` : "POST /api/rules"} + +
- - this.setState({test1: e})} inline /> - + this.updateParam((r) => r.name = v)} + error={this.state.ruleNameError} /> + this.updateParam((r) => r.color = v)} /> + this.updateParam((r) => r.notes = v)} /> - -
filters:
- this.setState({test: e})} validate={(e) => e%2 === 0} /> - - - + + filters: + this.updateParam((r) => r.filter.service_port = v)} + min={0} max={65565} error={this.state.ruleServicePortError} /> + this.updateParam((r) => r.filter.client_port = v)} + min={0} max={65565} error={this.state.ruleClientPortError} /> + this.updateParam((r) => r.filter.client_address = v)} /> - - - - - - + + this.updateParam((r) => r.filter.min_duration = v)} /> + this.updateParam((r) => r.filter.max_duration = v)} /> + this.updateParam((r) => r.filter.min_bytes = v)} /> + this.updateParam((r) => r.filter.max_bytes = v)} />
-
- -
- -
- - - -
- - patterns:
@@ -128,29 +383,24 @@ class RulePane extends Component { - + {!isUpdate && } - - - - - - - - - - - - + {patterns}
min max directionactionsactions
s", "s->c"]} value="both" />
+ {this.state.rulePatternsError != null && + error: {this.state.rulePatternsError}}
-
- +
+ {} + +
+
); } 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; + } + } +} -- cgit v1.2.3-70-g09d2