aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components/panels/RulesPane.js
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-10-16 17:06:05 +0000
committerEmiliano Ciavatta2020-10-16 17:06:05 +0000
commit56f70a72196c777f248038bb2e2e4099e6e1367d (patch)
tree714ad5aed8698dfffbb472b3fa74909acb8cdead /frontend/src/components/panels/RulesPane.js
parent6204c99e69d1707a79c5e56685b47310106c60b0 (diff)
parent79b8b2fa3e8563c986da8baa3a761f2d4f0c6f47 (diff)
Merge branch 'develop'
Diffstat (limited to 'frontend/src/components/panels/RulesPane.js')
-rw-r--r--frontend/src/components/panels/RulesPane.js441
1 files changed, 441 insertions, 0 deletions
diff --git a/frontend/src/components/panels/RulesPane.js b/frontend/src/components/panels/RulesPane.js
new file mode 100644
index 0000000..cdfe185
--- /dev/null
+++ b/frontend/src/components/panels/RulesPane.js
@@ -0,0 +1,441 @@
+/*
+ * This file is part of caronte (https://github.com/eciavatta/caronte).
+ * Copyright (c) 2020 Emiliano Ciavatta.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, {Component} from "react";
+import {Col, Container, Row} from "react-bootstrap";
+import Table from "react-bootstrap/Table";
+import backend from "../../backend";
+import dispatcher from "../../dispatcher";
+import validation from "../../validation";
+import ButtonField from "../fields/ButtonField";
+import CheckField from "../fields/CheckField";
+import ChoiceField from "../fields/ChoiceField";
+import ColorField from "../fields/extensions/ColorField";
+import NumericField from "../fields/extensions/NumericField";
+import InputField from "../fields/InputField";
+import TextField from "../fields/TextField";
+import CopyLinkPopover from "../objects/CopyLinkPopover";
+import LinkPopover from "../objects/LinkPopover";
+import "./common.scss";
+import "./RulesPane.scss";
+
+const classNames = require("classnames");
+const _ = require("lodash");
+
+class RulesPane extends Component {
+
+ 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
+ };
+ state = {
+ rules: [],
+ newRule: this.emptyRule,
+ newPattern: this.emptyPattern
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.directions = {
+ 0: "both",
+ 1: "c->s",
+ 2: "s->c"
+ };
+ }
+
+ componentDidMount() {
+ this.reset();
+ this.loadRules();
+
+ dispatcher.register("notifications", this.handleNotifications);
+ document.title = "caronte:~/rules$";
+ }
+
+ componentWillUnmount() {
+ dispatcher.unregister(this.handleNotifications);
+ }
+
+ handleNotifications = (payload) => {
+ if (payload.event === "rules.new" || payload.event === "rules.edit") {
+ this.loadRules();
+ }
+ };
+
+ 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,
+ selectedPattern: null,
+ 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});
+ };
+
+ 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})}>
+ <CopyLinkPopover text={r["id"].substring(0, 8)} value={r["id"]}/>
+ <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={"new_pattern"}>
+ <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 RulesPane;