aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEmiliano Ciavatta2020-10-15 11:50:37 +0000
committerEmiliano Ciavatta2020-10-15 11:50:37 +0000
commitc745263e1b28e4cedffa88de764f11d6379d745d (patch)
tree1126959456f7a051118c518aec5d9afb96d0494b
parent97b0ee38fcf1e78e66dfe2a2816c95c3c3b10705 (diff)
Update rules filters
-rw-r--r--frontend/src/components/Timeline.js77
-rw-r--r--frontend/src/components/dialogs/Filters.js2
-rw-r--r--frontend/src/components/fields/TagField.js29
-rw-r--r--frontend/src/components/fields/TagField.scss99
-rw-r--r--frontend/src/components/fields/common.scss1
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.js23
-rw-r--r--frontend/src/components/filters/RulesConnectionsFilter.scss121
-rw-r--r--frontend/src/components/objects/MessageAction.js2
-rw-r--r--frontend/src/components/panels/PcapsPane.js6
-rw-r--r--frontend/src/components/panels/SearchPane.js14
-rw-r--r--frontend/src/components/panels/StreamsPane.js2
-rw-r--r--statistics_controller.go5
12 files changed, 139 insertions, 242 deletions
diff --git a/frontend/src/components/Timeline.js b/frontend/src/components/Timeline.js
index bc42a01..1d88bcb 100644
--- a/frontend/src/components/Timeline.js
+++ b/frontend/src/components/Timeline.js
@@ -35,7 +35,6 @@ import log from "../log";
import dispatcher from "../dispatcher";
const minutes = 60 * 1000;
-const _ = require('lodash');
const classNames = require('classnames');
const leftSelectionPaddingMultiplier = 24;
@@ -54,19 +53,26 @@ class Timeline extends Component {
this.selectionTimeout = null;
}
- additionalFilters = () => {
+ componentDidMount() {
const urlParams = new URLSearchParams(this.props.location.search);
- if (this.state.metric === "matched_rules") {
- return urlParams.getAll("matched_rules") || [];
- } else {
- return urlParams.get("service_port");
- }
- };
+ this.setState({
+ servicePortFilter: urlParams.get("service_port") || null,
+ matchedRulesFilter: urlParams.getAll("matched_rules") || null
+ });
- componentDidMount() {
- const additionalFilters = this.additionalFilters();
- this.setState({filters: additionalFilters});
- this.loadStatistics(this.state.metric, additionalFilters).then(() => log.debug("Statistics loaded after mount"));
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics loaded after mount"));
+
+ this.connectionsFiltersCallback = payload => {
+ if ("service_port" in payload && this.state.servicePortFilter !== payload["service_port"]) {
+ this.setState({servicePortFilter: payload["service_port"]});
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after service port changed"));
+ }
+ if ("matched_rules" in payload && this.state.matchedRulesFilter !== payload["matched_rules"]) {
+ this.setState({matchedRulesFilter: payload["matched_rules"]});
+ this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after matched rules changed"));
+ }
+ };
+ dispatcher.register("connections_filters", this.connectionsFiltersCallback);
dispatcher.register("connection_updates", payload => {
this.setState({
@@ -76,8 +82,10 @@ class Timeline extends Component {
});
dispatcher.register("notifications", payload => {
- if (payload.event === "services.edit") {
- this.loadServices().then(() => this.adjustSelection());
+ if (payload.event === "services.edit" && this.state.metric !== "matched_rules") {
+ this.loadServices().then(() => log.debug("Statistics reloaded after services updates"));
+ } else if (payload.event.startsWith("rules") && this.state.metric === "matched_rules") {
+ this.loadServices().then(() => log.debug("Statistics reloaded after rules updates"));
}
});
@@ -87,41 +95,29 @@ class Timeline extends Component {
});
}
- componentDidUpdate(prevProps, prevState, snapshot) {
- const additionalFilters = this.additionalFilters();
- const updateStatistics = () => {
- this.setState({filters: additionalFilters});
- this.loadStatistics(this.state.metric, additionalFilters).then(() =>
- log.debug("Statistics reloaded after filters changes"));
- };
-
- if (this.state.metric === "matched_rules") {
- if (!Array.isArray(this.state.filters) ||
- !_.isEqual(_.sortBy(additionalFilters), _.sortBy(this.state.filters))) {
- updateStatistics();
- }
- } else {
- if (this.state.filters !== additionalFilters) {
- updateStatistics();
- }
- }
+ componentWillUnmount() {
+ dispatcher.unregister(this.connectionsFiltersCallback);
}
- loadStatistics = async (metric, filters) => {
+ loadStatistics = async (metric) => {
const urlParams = new URLSearchParams();
urlParams.set("metric", metric);
let columns = [];
if (metric === "matched_rules") {
let rules = await this.loadRules();
- filters.forEach(id => {
- urlParams.append("matched_rules", id);
- });
- columns = rules.map(r => r.id);
+ if (this.state.matchedRulesFilter.length > 0) {
+ this.state.matchedRulesFilter.forEach(id => {
+ urlParams.append("rules_ids", id);
+ });
+ columns = this.state.matchedRulesFilter;
+ } else {
+ columns = rules.map(r => r.id);
+ }
} else {
let services = await this.loadServices();
- const filteredPort = filters;
- if (filteredPort && services[filters]) {
+ const filteredPort = this.state.servicePortFilter;
+ if (filteredPort && services[filteredPort]) {
const service = services[filteredPort];
services = {};
services[filteredPort] = service;
@@ -172,7 +168,6 @@ class Timeline extends Component {
start,
end
});
- log.debug(`Loaded statistics for metric "${metric}"`);
};
loadServices = async () => {
@@ -279,7 +274,7 @@ class Timeline extends Component {
"server_bytes_per_service", "duration_per_service", "matched_rules"]}
values={["connections_per_service", "client_bytes_per_service",
"server_bytes_per_service", "duration_per_service", "matched_rules"]}
- onChange={(metric) => this.loadStatistics(metric, this.state.filters)
+ onChange={(metric) => this.loadStatistics(metric)
.then(() => log.debug("Statistics loaded after metric changes"))}
value={this.state.metric}/>
</div>
diff --git a/frontend/src/components/dialogs/Filters.js b/frontend/src/components/dialogs/Filters.js
index d2cce4f..a35ece2 100644
--- a/frontend/src/components/dialogs/Filters.js
+++ b/frontend/src/components/dialogs/Filters.js
@@ -16,7 +16,7 @@
*/
import React, {Component} from 'react';
-import {Col, Container, Modal, Row} from "react-bootstrap";
+import {Modal} from "react-bootstrap";
import ButtonField from "../fields/ButtonField";
import './Filters.scss';
import {cleanNumber, validateIpAddress, validateMin, validatePort} from "../../utils";
diff --git a/frontend/src/components/fields/TagField.js b/frontend/src/components/fields/TagField.js
index f1a48bd..89445b6 100644
--- a/frontend/src/components/fields/TagField.js
+++ b/frontend/src/components/fields/TagField.js
@@ -26,50 +26,47 @@ const _ = require('lodash');
class TagField extends Component {
+ state = {};
+
constructor(props) {
super(props);
this.id = `field-${this.props.name || "noname"}-${randomClassName()}`;
}
- state = {
-
- };
-
onAddition = (tag) => {
if (typeof this.props.onChange === "function") {
- const tags = [].concat(this.wrappedTags(), tag);
- this.props.onChange(tags.map(t => t.name), true, tag); // true == addition
+ this.props.onChange([].concat(this.props.tags, tag), true, tag); // true == addition
}
};
onDelete = (i) => {
if (typeof this.props.onChange === "function") {
- const tags = this.wrappedTags();
+ const tags = _.clone(this.props.tags);
const tag = tags[i];
tags.splice(i, 1);
- this.props.onChange(tags.map(t => t.name), true, tag); // false == delete
+ this.props.onChange(tags, true, tag); // false == delete
}
};
- wrappedTags = () => this.props.tags.map(t => new Object({"name": t}));
render() {
const small = this.props.small || false;
const name = this.props.name || null;
return (
- <div className={classNames( "field", "tag-field", {"field-small": small})}>
- { name &&
+ <div className={classNames("field", "tag-field", {"field-small": small},
+ {"field-inline": this.props.inline})}>
+ {name &&
<div className="field-name">
<label>{name}:</label>
</div>
}
- <ReactTags tags={this.wrappedTags() || []}
- autoresize={false}
- allowNew={this.props.allowNew || true}
- onDelete={this.onDelete} onAddition={this.onAddition}
- minQueryLength={this.props.min} placeholderText={this.props.placeholder || ""} />
+ <div className="field-input">
+ <ReactTags {...this.props} tags={this.props.tags || []} autoresize={false}
+ onDelete={this.onDelete} onAddition={this.onAddition}
+ placeholderText={this.props.placeholder || ""}/>
+ </div>
</div>
);
}
diff --git a/frontend/src/components/fields/TagField.scss b/frontend/src/components/fields/TagField.scss
index e77db97..737f11f 100644
--- a/frontend/src/components/fields/TagField.scss
+++ b/frontend/src/components/fields/TagField.scss
@@ -1,31 +1,68 @@
@import "../../colors.scss";
.tag-field {
+ font-size: 0.9em;
+ margin: 5px 0;
+
+ .field-name {
+ label {
+ margin: 0;
+ }
+ }
+
+ &.field-small {
+ font-size: 0.8em;
+ }
+
+ &.field-inline {
+ display: flex;
+
+ .field-name {
+ padding: 6px 0 6px 7px;
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+ background-color: $color-primary-2;
+ }
+
+ .field-input {
+ flex: 1;
+
+ .react-tags {
+ padding-left: 3px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ }
+
+ &:focus-within .field-name {
+ background-color: $color-primary-1;
+ }
+ }
+
.react-tags {
- font-size: 12px;
position: relative;
- z-index: 10;
- padding: 0 6px;
- cursor: text;
+ display: flex;
border-radius: 4px;
background-color: $color-primary-2;
- }
- .react-tags.is-focused {
- border-color: #b1b1b1;
+ &:focus-within,
+ &:focus-within .react-tags__search-input {
+ background-color: $color-primary-1;
+ }
}
.react-tags__selected {
- display: inline;
+ display: inline-block;
+ flex: 0 1;
+ margin: 6px 0;
+ white-space: nowrap;
}
.react-tags__selected-tag {
- font-size: 11px;
- display: inline-block;
- margin: 0 6px 6px 0;
+ font-size: 0.75em;
+ margin: 0 3px;
padding: 2px 4px;
color: $color-primary-3;
- border: none;
border-radius: 2px;
background: $color-primary-4;
}
@@ -39,12 +76,15 @@
.react-tags__selected-tag:hover,
.react-tags__selected-tag:focus {
border-color: #b1b1b1;
+ background-color: $color-primary-0;
+
+ &::after {
+ color: $color-primary-4;
+ }
}
.react-tags__search {
- display: inline-block;
- max-width: 100%;
- padding: 7px 10px;
+ flex: 1 0;
}
@media screen and (min-width: 30em) {
@@ -54,14 +94,7 @@
}
.react-tags__search-input {
- font-size: inherit;
- line-height: inherit;
- max-width: 100%;
- margin: 0;
- padding: 0;
color: $color-primary-4;
- border: 0;
- outline: none;
background-color: $color-primary-2;
}
@@ -71,6 +104,7 @@
.react-tags__suggestions {
position: absolute;
+ z-index: 50;
top: 100%;
left: 0;
width: 100%;
@@ -87,30 +121,33 @@
margin: 4px -1px;
padding: 0;
list-style: none;
- color: $color-primary-1;
- border-radius: 2px;
- background: $color-primary-4;
+ border-radius: 3px;
+ background: $color-primary-2;
}
.react-tags__suggestions li {
- padding: 3px 5px;
- border-bottom: 1px solid #ddd;
+ padding: 5px 10px;
}
.react-tags__suggestions li mark {
font-weight: 600;
- text-decoration: underline;
+ padding: 0;
+ color: $color-primary-4;
background: none;
}
.react-tags__suggestions li:hover {
cursor: pointer;
- color: $color-primary-4;
- background: $color-primary-0;
+ border-radius: 3px;
+ background: $color-primary-1;
+
+ mark {
+ color: $color-primary-4;
+ }
}
.react-tags__suggestions li.is-active {
- background: #b7cfe0;
+ background: $color-primary-3;
}
.react-tags__suggestions li.is-disabled {
diff --git a/frontend/src/components/fields/common.scss b/frontend/src/components/fields/common.scss
index 8fbef0d..e5dc65c 100644
--- a/frontend/src/components/fields/common.scss
+++ b/frontend/src/components/fields/common.scss
@@ -3,6 +3,7 @@
.field {
input,
textarea {
+ font-family: "Fira Code", monospace;
width: 100%;
padding: 7px 10px;
color: $color-primary-4;
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js
index 4c993dc..8e40d30 100644
--- a/frontend/src/components/filters/RulesConnectionsFilter.js
+++ b/frontend/src/components/filters/RulesConnectionsFilter.js
@@ -17,10 +17,9 @@
import React, {Component} from 'react';
import {withRouter} from "react-router-dom";
-import './RulesConnectionsFilter.scss';
-import ReactTags from 'react-tag-autocomplete';
import backend from "../../backend";
import dispatcher from "../../dispatcher";
+import TagField from "../fields/TagField";
const classNames = require('classnames');
const _ = require('lodash');
@@ -59,16 +58,8 @@ class RulesConnectionsFilter extends Component {
dispatcher.unregister(this.connectionsFiltersCallback);
}
- onDelete = (i) => {
- const activeRules = _.clone(this.state.activeRules);
- activeRules.splice(i, 1);
- this.setState({activeRules});
- dispatcher.dispatch("connections_filters", {"matched_rules": activeRules.map(r => r.id)});
- };
-
- onAddition = (rule) => {
- if (!this.state.activeRules.includes(rule)) {
- const activeRules = [].concat(this.state.activeRules, rule);
+ onChange = (activeRules) => {
+ if (!_.isEqual(activeRules.sort(), this.state.activeRules.sort())) {
this.setState({activeRules});
dispatcher.dispatch("connections_filters", {"matched_rules": activeRules.map(r => r.id)});
}
@@ -79,11 +70,9 @@ class RulesConnectionsFilter extends Component {
<div
className={classNames("filter", "d-inline-block", {"filter-active": this.state.filterActive === "true"})}>
<div className="filter-rules">
- <ReactTags tags={this.state.activeRules} suggestions={this.state.rules}
- onDelete={this.onDelete} onAddition={this.onAddition}
- minQueryLength={0} placeholderText="rule_name"
- suggestionsFilter={(suggestion, query) =>
- suggestion.name.startsWith(query) && !this.state.activeRules.includes(suggestion)}/>
+ <TagField tags={this.state.activeRules} onChange={this.onChange}
+ suggestions={_.differenceWith(this.state.rules, this.state.activeRules, _.isEqual)}
+ minQueryLength={0} name="matched_rules" inline small placeholder="rule_name"/>
</div>
</div>
);
diff --git a/frontend/src/components/filters/RulesConnectionsFilter.scss b/frontend/src/components/filters/RulesConnectionsFilter.scss
deleted file mode 100644
index 0bb4952..0000000
--- a/frontend/src/components/filters/RulesConnectionsFilter.scss
+++ /dev/null
@@ -1,121 +0,0 @@
-@import "../../colors";
-
-.filter-rules {
- .react-tags {
- font-size: 12px;
- position: relative;
- z-index: 10;
- padding: 0 6px;
- cursor: text;
- border-radius: 4px;
- background-color: $color-primary-2;
- }
-
- .react-tags.is-focused {
- border-color: #b1b1b1;
- }
-
- .react-tags__selected {
- display: inline;
- }
-
- .react-tags__selected-tag {
- font-size: 11px;
- display: inline-block;
- margin: 0 6px 6px 0;
- padding: 2px 4px;
- color: $color-primary-3;
- border: none;
- border-radius: 2px;
- background: $color-primary-4;
- }
-
- .react-tags__selected-tag::after {
- margin-left: 8px;
- content: "\2715";
- color: $color-primary-3;
- }
-
- .react-tags__selected-tag:hover,
- .react-tags__selected-tag:focus {
- border-color: #b1b1b1;
- }
-
- .react-tags__search {
- display: inline-block;
- max-width: 100%;
- padding: 7px 10px;
- }
-
- @media screen and (min-width: 30em) {
- .react-tags__search {
- position: relative;
- }
- }
-
- .react-tags__search-input {
- font-size: inherit;
- line-height: inherit;
- max-width: 100%;
- margin: 0;
- padding: 0;
- color: $color-primary-4;
- border: 0;
- outline: none;
- background-color: $color-primary-2;
- }
-
- .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 {
- font-size: 12px;
- margin: 4px -1px;
- padding: 0;
- list-style: none;
- color: $color-primary-1;
- border-radius: 2px;
- background: $color-primary-4;
- }
-
- .react-tags__suggestions li {
- padding: 3px 5px;
- border-bottom: 1px solid #ddd;
- }
-
- .react-tags__suggestions li mark {
- font-weight: 600;
- text-decoration: underline;
- background: none;
- }
-
- .react-tags__suggestions li:hover {
- cursor: pointer;
- color: $color-primary-4;
- background: $color-primary-0;
- }
-
- .react-tags__suggestions li.is-active {
- background: #b7cfe0;
- }
-
- .react-tags__suggestions li.is-disabled {
- cursor: auto;
- opacity: 0.5;
- }
-
-}
diff --git a/frontend/src/components/objects/MessageAction.js b/frontend/src/components/objects/MessageAction.js
index 9f199b7..2b46320 100644
--- a/frontend/src/components/objects/MessageAction.js
+++ b/frontend/src/components/objects/MessageAction.js
@@ -43,7 +43,7 @@ class MessageAction extends Component {
return (
<Modal
{...this.props}
- show="true"
+ show={true}
size="lg"
aria-labelledby="message-action-dialog"
centered
diff --git a/frontend/src/components/panels/PcapsPane.js b/frontend/src/components/panels/PcapsPane.js
index 8722230..900aacc 100644
--- a/frontend/src/components/panels/PcapsPane.js
+++ b/frontend/src/components/panels/PcapsPane.js
@@ -131,7 +131,7 @@ class PcapsPane extends Component {
render() {
let sessions = this.state.sessions.map(s =>
- <tr key={s.id} className="table-row">
+ <tr key={s.id} className="row-small row-clickable">
<td>{s["id"].substring(0, 8)}</td>
<td>{dateTimeToTime(s["started_at"])}</td>
<td>{durationBetween(s["started_at"], s["completed_at"])}</td>
@@ -166,13 +166,13 @@ class PcapsPane extends Component {
});
};
- const uploadCurlCommand = createCurlCommand("pcap/upload", "POST", null, {
+ const uploadCurlCommand = createCurlCommand("/pcap/upload", "POST", null, {
file: "@" + ((this.state.uploadSelectedFile != null && this.state.isUploadFileValid) ?
this.state.uploadSelectedFile.name : "invalid.pcap"),
flush_all: this.state.uploadFlushAll
});
- const fileCurlCommand = createCurlCommand("pcap/file", "POST", {
+ const fileCurlCommand = createCurlCommand("/pcap/file", "POST", {
file: this.state.fileValue,
flush_all: this.state.processFlushAll,
delete_original_file: this.state.deleteOriginalFile
diff --git a/frontend/src/components/panels/SearchPane.js b/frontend/src/components/panels/SearchPane.js
index 21ba139..1fb48ef 100644
--- a/frontend/src/components/panels/SearchPane.js
+++ b/frontend/src/components/panels/SearchPane.js
@@ -29,7 +29,6 @@ import dispatcher from "../../dispatcher";
import TagField from "../fields/TagField";
import CheckField from "../fields/CheckField";
-const classNames = require('classnames');
const _ = require('lodash');
class SearchPane extends Component {
@@ -161,7 +160,7 @@ class SearchPane extends Component {
const options = this.state.currentSearchOptions;
let searches = this.state.searches.map(s =>
- <tr key={s.id} className={classNames("row-small", "row-clickable", {"row-selected": false})}>
+ <tr key={s.id} className="row-small row-clickable">
<td>{s.id.substring(0, 8)}</td>
<td>{this.extractPattern(s["search_options"])}</td>
<td>{s["affected_connections_count"]}</td>
@@ -223,13 +222,14 @@ class SearchPane extends Component {
<div className="content-row">
<div className="text-search">
- <TagField tags={options.text_search.terms || []} name="terms" min={3} inline
+ <TagField tags={(options.text_search.terms || []).map(t => {return {name: t};})}
+ name="terms" min={3} inline allowNew={true}
readonly={regexOptionsModified || options.text_search.exact_phrase}
- onChange={(tags) => this.updateParam(s => s.text_search.terms = tags)}/>
- <TagField tags={options.text_search.excluded_terms || []} inline
- name="excluded_terms" min={3}
+ onChange={(tags) => this.updateParam(s => s.text_search.terms = tags.map(t => t.name))}/>
+ <TagField tags={(options.text_search.excluded_terms || []).map(t => {return {name: t};})}
+ name="excluded_terms" min={3} inline allowNew={true}
readonly={regexOptionsModified || options.text_search.exact_phrase}
- onChange={(tags) => this.updateParam(s => s.text_search.excluded_terms = tags)}/>
+ onChange={(tags) => this.updateParam(s => s.text_search.excluded_terms = tags.map(t => t.name))}/>
<span className="exclusive-separator">or</span>
diff --git a/frontend/src/components/panels/StreamsPane.js b/frontend/src/components/panels/StreamsPane.js
index 1aa5c53..be39777 100644
--- a/frontend/src/components/panels/StreamsPane.js
+++ b/frontend/src/components/panels/StreamsPane.js
@@ -65,7 +65,7 @@ class StreamsPane extends Component {
}
loadStream = (connectionId) => {
- this.setState({messages: []});
+ this.setState({messages: [], currentId: connectionId});
backend.get(`/api/streams/${connectionId}?format=${this.state.format}`)
.then(res => this.setState({messages: res.json}));
};
diff --git a/statistics_controller.go b/statistics_controller.go
index fda7494..29f3fec 100644
--- a/statistics_controller.go
+++ b/statistics_controller.go
@@ -31,14 +31,14 @@ type StatisticRecord struct {
ServerBytesPerService map[uint16]int `json:"server_bytes_per_service" bson:"server_bytes_per_service"`
TotalBytesPerService map[uint16]int `json:"total_bytes_per_service" bson:"total_bytes_per_service"`
DurationPerService map[uint16]int64 `json:"duration_per_service" bson:"duration_per_service"`
- MatchedRules map[string]int64 `json:"matched_rules" bson:"matched_rules"`
+ MatchedRules map[string]int64 `json:"matched_rules" bson:"matched_rules"`
}
type StatisticsFilter struct {
RangeFrom time.Time `form:"range_from"`
RangeTo time.Time `form:"range_to"`
Ports []uint16 `form:"ports"`
- RulesIDs []string `form:"rules_ids"`
+ RulesIDs []string `form:"rules_ids"`
Metric string `form:"metric"`
}
@@ -91,7 +91,6 @@ func (sc *StatisticsController) GetStatistics(context context.Context, filter St
}
}
- log.Println(query)
if err := query.All(&statisticRecords); err != nil {
log.WithError(err).WithField("filter", filter).Error("failed to retrieve statistics")
return []StatisticRecord{}