diff options
author | Emiliano Ciavatta | 2020-10-15 11:50:37 +0000 |
---|---|---|
committer | Emiliano Ciavatta | 2020-10-15 11:50:37 +0000 |
commit | c745263e1b28e4cedffa88de764f11d6379d745d (patch) | |
tree | 1126959456f7a051118c518aec5d9afb96d0494b | |
parent | 97b0ee38fcf1e78e66dfe2a2816c95c3c3b10705 (diff) |
Update rules filters
-rw-r--r-- | frontend/src/components/Timeline.js | 77 | ||||
-rw-r--r-- | frontend/src/components/dialogs/Filters.js | 2 | ||||
-rw-r--r-- | frontend/src/components/fields/TagField.js | 29 | ||||
-rw-r--r-- | frontend/src/components/fields/TagField.scss | 99 | ||||
-rw-r--r-- | frontend/src/components/fields/common.scss | 1 | ||||
-rw-r--r-- | frontend/src/components/filters/RulesConnectionsFilter.js | 23 | ||||
-rw-r--r-- | frontend/src/components/filters/RulesConnectionsFilter.scss | 121 | ||||
-rw-r--r-- | frontend/src/components/objects/MessageAction.js | 2 | ||||
-rw-r--r-- | frontend/src/components/panels/PcapsPane.js | 6 | ||||
-rw-r--r-- | frontend/src/components/panels/SearchPane.js | 14 | ||||
-rw-r--r-- | frontend/src/components/panels/StreamsPane.js | 2 | ||||
-rw-r--r-- | statistics_controller.go | 5 |
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{} |