aboutsummaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/components/ServicePortFilter.js91
-rw-r--r--frontend/src/components/filters/BooleanConnectionsFilter.js72
-rw-r--r--frontend/src/components/filters/BooleanConnectionsFilter.scss26
-rw-r--r--frontend/src/components/filters/FiltersDefinitions.js69
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.js90
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.scss136
-rw-r--r--frontend/src/components/filters/StringConnectionsFilter.js146
-rw-r--r--frontend/src/components/filters/StringConnectionsFilter.scss (renamed from frontend/src/components/ServicePortFilter.scss)17
-rw-r--r--frontend/src/utils.js61
-rw-r--r--frontend/src/views/App.js16
-rw-r--r--frontend/src/views/Connections.js19
-rw-r--r--frontend/src/views/Filters.js98
-rw-r--r--frontend/src/views/Filters.scss0
-rw-r--r--frontend/src/views/Header.js37
14 files changed, 750 insertions, 128 deletions
diff --git a/frontend/src/components/ServicePortFilter.js b/frontend/src/components/ServicePortFilter.js
deleted file mode 100644
index 72f2643..0000000
--- a/frontend/src/components/ServicePortFilter.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import React, {Component} from 'react';
-import './ServicePortFilter.scss';
-import {withRouter} from "react-router-dom";
-import {Redirect} from "react-router";
-
-class ServicePortFilter extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- servicePort: "",
- servicePortUrl: null,
- timeoutHandle: null
- };
-
- this.servicePortChanged = this.servicePortChanged.bind(this);
- }
-
- componentDidMount() {
- let params = new URLSearchParams(this.props.location.search);
- let servicePort = params.get("service_port");
- if (servicePort !== null) {
- this.setState({
- servicePort: servicePort,
- servicePortUrl: servicePort
- });
- }
- }
-
- servicePortChanged(event) {
- let value = event.target.value.replace(/[^\d]/gi, '');
- if (value.startsWith("0")) {
- return;
- }
- if (value !== "") {
- let port = parseInt(value);
- if (port > 65565) {
- return;
- }
- }
-
- if (this.state.timeoutHandle !== null) {
- clearTimeout(this.state.timeoutHandle);
- }
- this.setState({
- servicePort: value,
- timeoutHandle: setTimeout(() =>
- this.setState({servicePortUrl: value === "" ? null : value}), 300)
- });
- }
-
- render() {
- let redirect = null;
- let urlParams = new URLSearchParams(this.props.location.search);
- if (urlParams.get("service_port") !== this.state.servicePortUrl) {
- if (this.state.servicePortUrl !== null) {
- urlParams.set("service_port", this.state.servicePortUrl);
- } else {
- urlParams.delete("service_port");
- }
- redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />;
- }
- let active = this.state.servicePort !== "";
-
- return (
- <div className={"filter d-inline-block" + (active ? " filter-active" : "")}
- style={{"width": "200px"}}>
- <div className="input-group">
- <div className="filter-name-wrapper">
- <span className="filter-name" id="filter-service_port">service_port:</span>
- </div>
- <input placeholder="all ports" aria-label="service_port" aria-describedby="filter-service_port"
- className="form-control filter-value" onChange={this.servicePortChanged} value={this.state.servicePort} /></div>
-
- { active &&
- <div className="filter-delete">
- <span className="filter-delete-icon" onClick={() => this.setState({
- servicePort: "",
- servicePortUrl: null
- })}>del</span>
- </div>
- }
-
- {redirect}
- </div>
- );
- }
-
-}
-
-export default withRouter(ServicePortFilter);
diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.js b/frontend/src/components/filters/BooleanConnectionsFilter.js
new file mode 100644
index 0000000..7dea7cf
--- /dev/null
+++ b/frontend/src/components/filters/BooleanConnectionsFilter.js
@@ -0,0 +1,72 @@
+import React, {Component} from 'react';
+import {withRouter} from "react-router-dom";
+import {Redirect} from "react-router";
+import './BooleanConnectionsFilter.scss';
+
+const classNames = require('classnames');
+
+class BooleanConnectionsFilter extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ filterActive: "false"
+ };
+
+ this.filterChanged = this.filterChanged.bind(this);
+ this.needRedirect = false;
+ }
+
+ componentDidMount() {
+ let params = new URLSearchParams(this.props.location.search);
+ this.setState({filterActive: this.toBoolean(params.get(this.props.filterName)).toString()});
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ let urlParams = new URLSearchParams(this.props.location.search);
+ let externalActive = this.toBoolean(urlParams.get(this.props.filterName));
+ let filterActive = this.toBoolean(this.state.filterActive);
+ // if the filterActive state is changed by another component (and not by filterChanged func) and
+ // the query string is not equals at the filterActive state, update the state of the component
+ if (this.toBoolean(prevState.filterActive) === filterActive && filterActive !== externalActive) {
+ this.setState({filterActive: externalActive.toString()});
+ }
+ }
+
+ toBoolean(value) {
+ return value !== null && value.toLowerCase() === "true";
+ }
+
+ filterChanged() {
+ this.needRedirect = true;
+ this.setState({filterActive: (!this.toBoolean(this.state.filterActive)).toString()});
+ }
+
+ render() {
+ let redirect = null;
+ if (this.needRedirect) {
+ let urlParams = new URLSearchParams(this.props.location.search);
+ if (this.toBoolean(this.state.filterActive)) {
+ urlParams.set(this.props.filterName, "true");
+ } else {
+ urlParams.delete(this.props.filterName);
+ }
+ redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />;
+
+ this.needRedirect = false;
+ }
+
+ 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>
+
+ {redirect}
+ </div>
+ );
+ }
+
+}
+
+export default withRouter(BooleanConnectionsFilter);
diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.scss b/frontend/src/components/filters/BooleanConnectionsFilter.scss
new file mode 100644
index 0000000..f3d8697
--- /dev/null
+++ b/frontend/src/components/filters/BooleanConnectionsFilter.scss
@@ -0,0 +1,26 @@
+@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
new file mode 100644
index 0000000..a582d02
--- /dev/null
+++ b/frontend/src/components/filters/FiltersDefinitions.js
@@ -0,0 +1,69 @@
+import {
+ cleanNumber,
+ timestampToTime,
+ timeToTimestamp,
+ validate24HourTime,
+ validateIpAddress,
+ validateMin,
+ validatePort
+} from "../../utils";
+import StringConnectionsFilter from "./StringConnectionsFilter";
+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"];
+
+export const filtersDefinitions = {
+ service_port: <StringConnectionsFilter filterName="service_port"
+ defaultFilterValue="all_ports"
+ replaceFunc={cleanNumber}
+ validateFunc={validatePort}/>,
+ matched_rules: <RulesConnectionsFilter />,
+ client_address: <StringConnectionsFilter filterName="client_address"
+ defaultFilterValue="all_addresses"
+ validateFunc={validateIpAddress} />,
+ client_port: <StringConnectionsFilter filterName="client_port"
+ defaultFilterValue="all_ports"
+ replaceFunc={cleanNumber}
+ validateFunc={validatePort}/>,
+ min_duration: <StringConnectionsFilter filterName="min_duration"
+ defaultFilterValue="0"
+ replaceFunc={cleanNumber}
+ validateFunc={validateMin(0)}/>,
+ max_duration: <StringConnectionsFilter filterName="max_duration"
+ defaultFilterValue="∞"
+ replaceFunc={cleanNumber} />,
+ min_bytes: <StringConnectionsFilter filterName="min_bytes"
+ defaultFilterValue="0"
+ replaceFunc={cleanNumber}
+ validateFunc={validateMin(0)} />,
+ max_bytes: <StringConnectionsFilter filterName="max_bytes"
+ defaultFilterValue="∞"
+ replaceFunc={cleanNumber} />,
+ started_after: <StringConnectionsFilter filterName="started_after"
+ defaultFilterValue="00:00:00"
+ validateFunc={validate24HourTime}
+ encodeFunc={timeToTimestamp}
+ decodeFunc={timestampToTime} />,
+ started_before: <StringConnectionsFilter filterName="started_before"
+ defaultFilterValue="00:00:00"
+ validateFunc={validate24HourTime}
+ encodeFunc={timeToTimestamp}
+ decodeFunc={timestampToTime} />,
+ closed_after: <StringConnectionsFilter filterName="closed_after"
+ defaultFilterValue="00:00:00"
+ validateFunc={validate24HourTime}
+ encodeFunc={timeToTimestamp}
+ decodeFunc={timestampToTime} />,
+ closed_before: <StringConnectionsFilter filterName="closed_before"
+ defaultFilterValue="00:00:00"
+ validateFunc={validate24HourTime}
+ encodeFunc={timeToTimestamp}
+ decodeFunc={timestampToTime} />,
+ marked: <BooleanConnectionsFilter filterName={"marked"} />,
+ hidden: <BooleanConnectionsFilter filterName={"hidden"} />
+};
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js
new file mode 100644
index 0000000..358085f
--- /dev/null
+++ b/frontend/src/components/filters/RulesConnectionsFilter.js
@@ -0,0 +1,90 @@
+import React, {Component} from 'react';
+import {withRouter} from "react-router-dom";
+import {Redirect} from "react-router";
+import './RulesConnectionsFilter.scss';
+import ReactTags from 'react-tag-autocomplete';
+import axios from 'axios';
+
+const classNames = require('classnames');
+
+class RulesConnectionsFilter extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ mounted: false,
+ rules: [],
+ activeRules: []
+ };
+
+ this.needRedirect = false;
+ }
+
+ componentDidMount() {
+ 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}] : []);
+ activeRules = rules.filter(rule => activeRules.some(id => rule.id === id));
+ this.setState({rules, activeRules, mounted: true});
+ });
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ let urlParams = new URLSearchParams(this.props.location.search);
+ let externalRules = urlParams.getAll("matched_rules") || [];
+ let activeRules = this.state.activeRules.map(r => r.id);
+ let compareRules = (first, second) => first.sort().join(",") === second.sort().join(",");
+ if (this.state.mounted &&
+ compareRules(prevState.activeRules.map(r => r.id), activeRules) &&
+ !compareRules(externalRules, activeRules)) {
+ this.setState({activeRules: externalRules.map(id => this.state.rules.find(r => r.id === id))});
+ }
+ }
+
+ onDelete(i) {
+ const activeRules = this.state.activeRules.slice(0);
+ activeRules.splice(i, 1);
+ this.needRedirect = true;
+ this.setState({ activeRules });
+ }
+
+ onAddition(rule) {
+ if (!this.state.activeRules.includes(rule)) {
+ const activeRules = [].concat(this.state.activeRules, rule);
+ this.needRedirect = true;
+ this.setState({activeRules});
+ }
+ }
+
+ render() {
+ let redirect = null;
+
+ if (this.needRedirect) {
+ let urlParams = new URLSearchParams(this.props.location.search);
+ urlParams.delete("matched_rules");
+ this.state.activeRules.forEach(rule => urlParams.append("matched_rules", rule.id));
+ redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />;
+
+ this.needRedirect = false;
+ }
+
+ return (
+ <div className={classNames("filter", "d-inline-block", {"filter-active" : this.state.filterActive === "true"})}>
+ <div className="filter-booleanq">
+ <ReactTags tags={this.state.activeRules} suggestions={this.state.rules}
+ onDelete={this.onDelete.bind(this)} onAddition={this.onAddition.bind(this)}
+ minQueryLength={0} placeholderText="rule_name"
+ suggestionsFilter={(suggestion, query) =>
+ suggestion.name.startsWith(query) && !this.state.activeRules.includes(suggestion)} />
+ </div>
+
+ {redirect}
+ </div>
+ );
+ }
+
+}
+
+export default withRouter(RulesConnectionsFilter);
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.scss b/frontend/src/components/filters/RulesConnectionsFilter.scss
new file mode 100644
index 0000000..d75066e
--- /dev/null
+++ b/frontend/src/components/filters/RulesConnectionsFilter.scss
@@ -0,0 +1,136 @@
+@import '../../colors';
+
+.react-tags {
+ position: relative;
+ padding: 0px 6px;
+ border-radius: 4px;
+
+ background-color: $color-primary-2;
+
+
+ /* shared font styles */
+ font-size: 12px;
+
+ /* clicking anywhere will focus the input */
+ cursor: text;
+
+ z-index: 10;
+}
+
+.react-tags.is-focused {
+ border-color: #B1B1B1;
+}
+
+.react-tags__selected {
+ display: inline;
+}
+
+.react-tags__selected-tag {
+ display: inline-block;
+ border: none;
+ margin: 0 6px 6px 0;
+ padding: 2px 4px;
+ border-radius: 2px;
+ background: $color-primary-4;
+ color: $color-primary-3;
+
+ font-size: 11px;
+}
+
+.react-tags__selected-tag:after {
+ content: '\2715';
+ color: $color-primary-3;
+ margin-left: 8px;
+}
+
+.react-tags__selected-tag:hover,
+.react-tags__selected-tag:focus {
+ border-color: #B1B1B1;
+}
+
+.react-tags__search {
+ display: inline-block;
+ padding: 7px 10px;
+
+ /* prevent autoresize overflowing the container */
+ max-width: 100%;
+}
+
+@media screen and (min-width: 30em) {
+
+ .react-tags__search {
+ /* this will become the offsetParent for suggestions */
+ position: relative;
+ }
+
+}
+
+.react-tags__search-input {
+ max-width: 100%;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: none;
+
+ background-color: $color-primary-2;
+ color: $color-primary-4;
+
+ /* match the font styles */
+ font-size: inherit;
+ line-height: inherit;
+}
+
+.react-tags__search-input::-ms-clear {
+ display: none;
+}
+
+.react-tags__suggestions {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ width: 100%;
+}
+
+@media screen and (min-width: 30em) {
+
+ .react-tags__suggestions {
+ width: 240px;
+ }
+
+}
+
+.react-tags__suggestions ul {
+ margin: 4px -1px;
+ padding: 0;
+ list-style: none;
+ background: $color-primary-4;
+ border-radius: 2px;
+ color: $color-primary-1;
+ font-size: 12px;
+}
+
+.react-tags__suggestions li {
+ border-bottom: 1px solid #ddd;
+ padding: 3px 5px;
+}
+
+.react-tags__suggestions li mark {
+ text-decoration: underline;
+ background: none;
+ font-weight: 600;
+}
+
+.react-tags__suggestions li:hover {
+ cursor: pointer;
+ background: $color-primary-0;
+ color: $color-primary-4;
+}
+
+.react-tags__suggestions li.is-active {
+ background: #b7cfe0;
+}
+
+.react-tags__suggestions li.is-disabled {
+ opacity: 0.5;
+ cursor: auto;
+} \ No newline at end of file
diff --git a/frontend/src/components/filters/StringConnectionsFilter.js b/frontend/src/components/filters/StringConnectionsFilter.js
new file mode 100644
index 0000000..490a569
--- /dev/null
+++ b/frontend/src/components/filters/StringConnectionsFilter.js
@@ -0,0 +1,146 @@
+import React, {Component} from 'react';
+import {withRouter} from "react-router-dom";
+import {Redirect} from "react-router";
+import './StringConnectionsFilter.scss';
+
+const classNames = require('classnames');
+
+class StringConnectionsFilter extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ fieldValue: "",
+ filterValue: null,
+ timeoutHandle: null,
+ invalidValue: false
+ };
+ this.needRedirect = false;
+ this.filterChanged = this.filterChanged.bind(this);
+ }
+
+ componentDidMount() {
+ let params = new URLSearchParams(this.props.location.search);
+ this.updateStateFromFilterValue(params.get(this.props.filterName));
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ let urlParams = new URLSearchParams(this.props.location.search);
+ let filterValue = urlParams.get(this.props.filterName);
+ if (prevState.filterValue === this.state.filterValue && this.state.filterValue !== filterValue) {
+ this.updateStateFromFilterValue(filterValue);
+ }
+ }
+
+ updateStateFromFilterValue(filterValue) {
+ if (filterValue !== null) {
+ let fieldValue = filterValue;
+ if (typeof this.props.decodeFunc === "function") {
+ fieldValue = this.props.decodeFunc(filterValue);
+ }
+ if (typeof this.props.replaceFunc === "function") {
+ fieldValue = this.props.replaceFunc(fieldValue);
+ }
+ if (this.isValueValid(fieldValue)) {
+ this.setState({
+ fieldValue: fieldValue,
+ filterValue: filterValue
+ });
+ } else {
+ this.setState({
+ fieldValue: fieldValue,
+ invalidValue: true
+ });
+ }
+ } else {
+ this.setState({fieldValue: "", filterValue: null});
+ }
+ }
+
+ isValueValid(value) {
+ return typeof this.props.validateFunc !== "function" ||
+ (typeof this.props.validateFunc === "function" && this.props.validateFunc(value));
+ }
+
+ filterChanged(event) {
+ let fieldValue = event.target.value;
+ if (this.state.timeoutHandle !== null) {
+ clearTimeout(this.state.timeoutHandle);
+ }
+
+ if (typeof this.props.replaceFunc === "function") {
+ fieldValue = this.props.replaceFunc(fieldValue);
+ }
+
+ if (fieldValue === "") {
+ this.needRedirect = true;
+ this.setState({fieldValue: "", filterValue: null, invalidValue: false});
+ return;
+ }
+
+ if (this.isValueValid(fieldValue)) {
+ let filterValue = fieldValue;
+ if (filterValue !== "" && typeof this.props.encodeFunc === "function") {
+ filterValue = this.props.encodeFunc(filterValue);
+ }
+
+ this.setState({
+ fieldValue: fieldValue,
+ timeoutHandle: setTimeout(() => {
+ this.needRedirect = true;
+ this.setState({filterValue: filterValue});
+ }, 500),
+ invalidValue: false
+ });
+ } else {
+ this.needRedirect = true;
+ this.setState({
+ fieldValue: fieldValue,
+ invalidValue: true
+ });
+ }
+ }
+
+ render() {
+ let redirect = null;
+ if (this.needRedirect) {
+ let urlParams = new URLSearchParams(this.props.location.search);
+ if (this.state.filterValue !== null) {
+ urlParams.set(this.props.filterName, this.state.filterValue);
+ } else {
+ urlParams.delete(this.props.filterName);
+ }
+ redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />;
+ this.needRedirect = false;
+ }
+ 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>
+ }
+
+ {redirect}
+ </div>
+ );
+ }
+
+}
+
+export default withRouter(StringConnectionsFilter);
diff --git a/frontend/src/components/ServicePortFilter.scss b/frontend/src/components/filters/StringConnectionsFilter.scss
index 2b23444..ecc8d0f 100644
--- a/frontend/src/components/ServicePortFilter.scss
+++ b/frontend/src/components/filters/StringConnectionsFilter.scss
@@ -1,4 +1,4 @@
-@import '../colors.scss';
+@import '../../colors';
.filter {
margin: 0 10px;
@@ -32,13 +32,20 @@
}
.filter-value {
- font-size: 13px;
background-color: $color-primary-4;
color: $color-primary-3;
+ }
+ }
+
+ &.filter-invalid {
+ .filter-name-wrapper {
+ background-color: $color-secondary-2;
+ color: $color-primary-4;
+ }
- &:focus {
- background-color: $color-primary-4;
- }
+ .filter-value {
+ background-color: $color-secondary-2;
+ color: $color-primary-4;
}
}
diff --git a/frontend/src/utils.js b/frontend/src/utils.js
index db9405c..26c10d3 100644
--- a/frontend/src/utils.js
+++ b/frontend/src/utils.js
@@ -1,14 +1,61 @@
+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 objectToQueryString(obj) {
- let str = [];
- for (let p in obj)
- if (obj.hasOwnProperty(p)) {
- str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
- }
- return str.join("&");
+export function validateIpAddress(ipAddress) {
+ let regex = /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/;
+ return regex.test(ipAddress);
+}
+
+export function validate24HourTime(time) {
+ return timeRegex.test(time);
+}
+
+export function cleanNumber(number) {
+ return number.replace(/[^\d]/gi, "").replace(/^0+/g, "");
+}
+
+export function validateMin(min) {
+ return function (value) {
+ return parseInt(value) > min;
+ };
+}
+
+export function validateMax(max) {
+ return function (value) {
+ return parseInt(value) < max;
+ };
+}
+
+export function validatePort(port) {
+ return validateMin(0)(port) && validateMax(65565)(port);
+}
+
+export function timeToTimestamp(time) {
+ let d = new Date();
+ let matches = time.match(timeRegex);
+
+ if (matches[1] !== undefined) {
+ d.setHours(matches[1]);
+ }
+ if (matches[2] !== undefined) {
+ d.setMinutes(matches[2]);
+ }
+ if (matches[3] !== undefined) {
+ d.setSeconds(matches[3]);
+ }
+
+ return Math.round(d.getTime() / 1000);
+}
+
+export function timestampToTime(timestamp) {
+ let d = new Date(timestamp * 1000);
+ let hours = d.getHours();
+ let minutes = "0" + d.getMinutes();
+ let seconds = "0" + d.getSeconds();
+ return hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);
}
diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js
index e3119aa..14ff7bf 100644
--- a/frontend/src/views/App.js
+++ b/frontend/src/views/App.js
@@ -5,26 +5,32 @@ import MainPane from "./MainPane";
import Footer from "./Footer";
import {BrowserRouter as Router, Route, Switch} from "react-router-dom";
import Services from "./Services";
+import Filters from "./Filters";
class App extends Component {
constructor(props) {
super(props);
this.state = {
- servicesShow: false
+ servicesWindowOpen: false,
+ filterWindowOpen: false
};
}
render() {
- let modal = "";
- if (this.state.servicesShow) {
- modal = <Services onHide={() => this.setState({servicesShow: false})}/>;
+ let modal;
+ if (this.state.servicesWindowOpen) {
+ modal = <Services onHide={() => this.setState({servicesWindowOpen: false})}/>;
+ }
+ if (this.state.filterWindowOpen) {
+ modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>;
}
return (
<div className="app">
<Router>
- <Header onOpenServices={() => this.setState({servicesShow: true})}/>
+ <Header onOpenServices={() => this.setState({servicesWindowOpen: true})}
+ onOpenFilters={() => this.setState({filterWindowOpen: true})}/>
<Switch>
<Route path="/connections/:id" children={<MainPane/>}/>
<Route path="/" children={<MainPane/>}/>
diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js
index 9b9fe35..62733d7 100644
--- a/frontend/src/views/Connections.js
+++ b/frontend/src/views/Connections.js
@@ -1,6 +1,6 @@
import React, {Component} from 'react';
import './Connections.scss';
-import axios from 'axios'
+import axios from 'axios';
import Connection from "../components/Connection";
import Table from 'react-bootstrap/Table';
import {Redirect} from 'react-router';
@@ -15,7 +15,6 @@ class Connections extends Component {
connections: [],
firstConnection: null,
lastConnection: null,
- showHidden: false,
prevParams: null,
flagRule: null,
rules: null,
@@ -33,7 +32,7 @@ class Connections extends Component {
}
componentDidMount() {
- this.loadConnections({limit: this.queryLimit, hidden: this.state.showHidden})
+ this.loadConnections({limit: this.queryLimit})
.then(() => this.setState({loaded: true}));
}
@@ -45,7 +44,7 @@ class Connections extends Component {
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.loaded && prevProps.location.search !== this.props.location.search) {
this.setState({queryString: this.props.location.search});
- this.loadConnections({limit: this.queryLimit, hidden: this.state.showHidden})
+ this.loadConnections({limit: this.queryLimit})
.then(() => console.log("Connections reloaded after query string update"));
}
}
@@ -53,16 +52,12 @@ class Connections extends Component {
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,
- hidden: this.state.showHidden
- }).then(() => console.log("Following connections loaded"));
+ this.loadConnections({from: this.state.lastConnection.id, limit: this.queryLimit,})
+ .then(() => console.log("Following connections loaded"));
}
if (!this.state.loading && relativeScroll < this.scrollTopThreashold) {
- this.loadConnections({
- to: this.state.firstConnection.id, limit: this.queryLimit,
- hidden: this.state.showHidden
- }).then(() => console.log("Previous connections loaded"));
+ this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,})
+ .then(() => console.log("Previous connections loaded"));
}
}
diff --git a/frontend/src/views/Filters.js b/frontend/src/views/Filters.js
new file mode 100644
index 0000000..23d3a00
--- /dev/null
+++ b/frontend/src/views/Filters.js
@@ -0,0 +1,98 @@
+import React, {Component} from 'react';
+import './Services.scss';
+import {Button, Col, Container, Modal, Row, Table} from "react-bootstrap";
+import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions";
+
+class Filters extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {};
+ filtersNames.forEach(elem => this.state[`${elem}_active`] = false);
+ }
+
+ componentDidMount() {
+ let newState = {};
+ filtersNames.forEach(elem => newState[`${elem}_active`] = localStorage.getItem(`filters.${elem}`) === "true");
+ this.setState(newState);
+ }
+
+ checkboxChangesHandler(filterName, event) {
+ this.setState({[`${filterName}_active`]: event.target.checked});
+ localStorage.setItem(`filters.${filterName}`, event.target.checked);
+ if (typeof window !== "undefined") {
+ window.dispatchEvent(new Event("quick-filters"));
+ }
+ }
+
+ generateRows(filtersNames) {
+ return filtersNames.map(name =>
+ <tr>
+ <td><input type="checkbox"
+ checked={this.state[`${name}_active`]}
+ onChange={event => this.checkboxChangesHandler(name, event)} /></td>
+ <td>{filtersDefinitions[name]}</td>
+ </tr>
+ );
+ }
+
+ render() {
+ return (
+ <Modal
+ {...this.props}
+ show="true"
+ size="lg"
+ aria-labelledby="filters-dialog"
+ centered
+ >
+ <Modal.Header>
+ <Modal.Title id="filters-dialog">
+ ~/filters
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <Container>
+ <Row>
+ <Col md={6}>
+ <Table borderless size="sm" className="filters-table">
+ <thead>
+ <tr>
+ <th>show</th>
+ <th>filter</th>
+ </tr>
+ </thead>
+ <tbody>
+ {this.generateRows(["service_port", "client_address", "min_duration",
+ "min_bytes", "started_after", "closed_after", "marked"])}
+ </tbody>
+ </Table>
+ </Col>
+ <Col md={6}>
+ <Table borderless size="sm" className="filters-table">
+ <thead>
+ <tr>
+ <th>show</th>
+ <th>filter</th>
+ </tr>
+ </thead>
+ <tbody>
+ {this.generateRows(["matched_rules", "client_port", "max_duration",
+ "max_bytes", "started_before", "closed_before", "hidden"])}
+ </tbody>
+ </Table>
+ </Col>
+
+ </Row>
+
+
+ </Container>
+ </Modal.Body>
+ <Modal.Footer className="dialog-footer">
+ <Button variant="red" onClick={this.props.onHide}>close</Button>
+ </Modal.Footer>
+ </Modal>
+ );
+ }
+}
+
+export default Filters;
diff --git a/frontend/src/views/Filters.scss b/frontend/src/views/Filters.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/frontend/src/views/Filters.scss
diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js
index 5118ec3..007be74 100644
--- a/frontend/src/views/Header.js
+++ b/frontend/src/views/Header.js
@@ -2,15 +2,18 @@ import React, {Component} from 'react';
import Typed from 'typed.js';
import './Header.scss';
import {Button} from "react-bootstrap";
-import ServicePortFilter from "../components/ServicePortFilter";
+import StringConnectionsFilter from "../components/filters/StringConnectionsFilter";
+import {cleanNumber, validateIpAddress, validateMin, validatePort} from "../utils";
+import RulesConnectionsFilter from "../components/filters/RulesConnectionsFilter";
+import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions";
class Header extends Component {
constructor(props) {
super(props);
- this.state = {
- servicesShow: false
- };
+ this.state = {};
+ filtersNames.forEach(elem => this.state[`${elem}_active`] = false);
+ this.fetchStateFromLocalStorage = this.fetchStateFromLocalStorage.bind(this);
}
componentDidMount() {
@@ -20,13 +23,33 @@ class Header extends Component {
cursorChar: "❚"
};
this.typed = new Typed(this.el, options);
+
+ this.fetchStateFromLocalStorage();
+
+ if (typeof window !== "undefined") {
+ window.addEventListener("quick-filters", this.fetchStateFromLocalStorage);
+ }
}
componentWillUnmount() {
this.typed.destroy();
+
+ if (typeof window !== "undefined") {
+ window.removeEventListener("quick-filters", this.fetchStateFromLocalStorage);
+ }
+ }
+
+ fetchStateFromLocalStorage() {
+ let newState = {};
+ filtersNames.forEach(elem => newState[`${elem}_active`] = localStorage.getItem(`filters.${elem}`) === "true");
+ this.setState(newState);
}
render() {
+ let quickFilters = filtersNames.filter(name => this.state[`${name}_active`])
+ .map(name => filtersDefinitions[name])
+ .slice(0, 5);
+
return (
<header className="header container-fluid">
<div className="row">
@@ -41,16 +64,14 @@ class Header extends Component {
<div className="col-auto">
<div className="filters-bar-wrapper">
<div className="filters-bar">
- <ServicePortFilter />
- {/*<ServicePortFilter name="started_before" default="infinity" />*/}
- {/*<ServicePortFilter name="started_after" default="-infinity" />*/}
-
+ {quickFilters}
</div>
</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">rules</Button>
<Button variant="red" onClick={this.props.onOpenServices}>