From bc989facca1f3381afed2f7c982da7784fad2327 Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Sun, 11 Oct 2020 21:49:40 +0200 Subject: [inconsistent] SearchOption validation checkpoint --- search_controller.go | 189 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 search_controller.go (limited to 'search_controller.go') diff --git a/search_controller.go b/search_controller.go new file mode 100644 index 0000000..ad47dbc --- /dev/null +++ b/search_controller.go @@ -0,0 +1,189 @@ +/* + * 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 . + */ + +package main + +import ( + "context" + log "github.com/sirupsen/logrus" + "strings" + "sync" + "time" +) + +const ( + secondsToNano = 1000 * 1000 * 1000 + maxSearchTimeout = 60 * secondsToNano +) + +type PerformedSearch struct { + ID RowID `bson:"_id" json:"id"` + SearchOptions SearchOptions `bson:"search_options" json:"search_options"` + AffectedConnections []RowID `bson:"affected_connections" json:"affected_connections,omitempty"` + AffectedConnectionsCount int `bson:"affected_connections_count" json:"affected_connections_count"` + StartedAt time.Time `bson:"started_at" json:"started_at"` + FinishedAt time.Time `bson:"finished_at" json:"finished_at"` + UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` + Timeout time.Duration `bson:"timeout" json:"timeout"` +} + +type SearchOptions struct { + TextSearch TextSearch `bson:"text_search" json:"text_search" validate:"either_with=RegexSearch"` + RegexSearch RegexSearch `bson:"regex_search" json:"regex_search" validate:"either_with=TextSearch"` + Timeout time.Duration `bson:"timeout" json:"timeout" binding:"max=60"` +} + +type TextSearch struct { + Terms []string `bson:"terms" json:"terms" binding:"parent_is_zero|either_with=ExactPhrase,isdefault|min=1,dive,min=3"` + ExcludedTerms []string `bson:"excluded_terms" json:"excluded_terms" binding:"required_with=Terms,dive,isdefault|min=1"` + ExactPhrase string `bson:"exact_phrase" json:"exact_phrase" binding:"isdefault|min=3,parent_is_zero|either_with=Terms"` + CaseSensitive bool `bson:"case_sensitive" json:"case_sensitive"` +} + +type RegexSearch struct { + Pattern string `bson:"pattern" json:"pattern" binding:"parent_is_zero|either_with=NotPattern,isdefault|min=3"` + NotPattern string `bson:"not_pattern" json:"not_pattern" binding:"parent_is_zero|either_with=Pattern,isdefault|min=3"` + CaseInsensitive bool `bson:"case_insensitive" json:"case_insensitive"` + MultiLine bool `bson:"multi_line" json:"multi_line"` + IgnoreWhitespaces bool `bson:"ignore_whitespaces" json:"ignore_whitespaces"` + DotCharacter bool `bson:"dot_character" json:"dot_character"` +} + +type SearchController struct { + storage Storage + performedSearches []PerformedSearch + mutex sync.Mutex +} + +func NewSearchController(storage Storage) *SearchController { + var searches []PerformedSearch + if err := storage.Find(Searches).All(&searches); err != nil { + // log.WithError(err).Panic("failed to retrieve performed searches") + } + + return &SearchController{ + storage: storage, + performedSearches: searches, + } +} + +func (sc *SearchController) PerformedSearches() []PerformedSearch { + sc.mutex.Lock() + defer sc.mutex.Unlock() + + return sc.performedSearches +} + +func (sc *SearchController) PerformSearch(c context.Context, options SearchOptions) PerformedSearch { + findQuery := sc.storage.Find(ConnectionStreams).Projection(OrderedDocument{{"connection_id", 1}}).Context(c) + timeout := options.Timeout * secondsToNano + if timeout <= 0 || timeout > maxSearchTimeout { + timeout = maxSearchTimeout + } + findQuery = findQuery.MaxTime(timeout) + + if !options.TextSearch.isZero() { + var text string + if options.TextSearch.ExactPhrase != "" { + text = "\"" + options.TextSearch.ExactPhrase + "\"" + } else { + text = strings.Join(options.TextSearch.Terms, " ") + if options.TextSearch.ExcludedTerms != nil { + text += " -" + strings.Join(options.TextSearch.ExcludedTerms, " -") + } + } + + findQuery = findQuery.Filter(OrderedDocument{{"$text", UnorderedDocument{ + "$search": text, + "$language": "none", + "$caseSensitive": options.TextSearch.CaseSensitive, + "$diacriticSensitive": false, + }}}) + } else { + var regexOptions string + if options.RegexSearch.CaseInsensitive { + regexOptions += "i" + } + if options.RegexSearch.MultiLine { + regexOptions += "m" + } + if options.RegexSearch.IgnoreWhitespaces { + regexOptions += "x" + } + if options.RegexSearch.DotCharacter { + regexOptions += "s" + } + + var regex UnorderedDocument + if options.RegexSearch.Pattern != "" { + regex = UnorderedDocument{"$regex": options.RegexSearch.Pattern, "$options": regexOptions} + } else { + regex = UnorderedDocument{"$not": + UnorderedDocument{"$regex": options.RegexSearch.NotPattern, "$options": regexOptions}} + } + + findQuery = findQuery.Filter(OrderedDocument{{"payload_string", regex}}) + } + + var connections []ConnectionStream + startedAt := time.Now() + if err := findQuery.All(&connections); err != nil { + log.WithError(err).Error("oh no") + } + affectedConnections := uniqueConnectionIds(connections) + + finishedAt := time.Now() + performedSearch := PerformedSearch{ + ID: NewRowID(), + SearchOptions: options, + AffectedConnections: affectedConnections, + AffectedConnectionsCount: len(affectedConnections), + StartedAt: startedAt, + FinishedAt: finishedAt, + UpdatedAt: finishedAt, + Timeout: options.Timeout, + } + if _, err := sc.storage.Insert(Searches).Context(c).One(performedSearch); err != nil { + log.WithError(err).Panic("failed to insert a new performed search") + } + + sc.mutex.Lock() + sc.performedSearches = append([]PerformedSearch{performedSearch}, sc.performedSearches...) + sc.mutex.Unlock() + + return performedSearch +} + +func (sc TextSearch) isZero() bool { + return sc.Terms == nil && sc.ExcludedTerms == nil && sc.ExactPhrase == "" +} + +func (sc RegexSearch) isZero() bool { + return RegexSearch{} == sc +} + +func uniqueConnectionIds(connections []ConnectionStream) []RowID { + keys := make(map[RowID]bool) + var out []RowID + for _, entry := range connections { + if _, value := keys[entry.ConnectionID]; !value { + keys[entry.ConnectionID] = true + out = append(out, entry.ConnectionID) + } + } + return out +} -- cgit v1.2.3-70-g09d2 From 828aeef9a7333aaabeaf9324a85aac56348b3805 Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Sun, 11 Oct 2020 22:50:20 +0200 Subject: Add SearchController --- application_context.go | 4 +-- application_router.go | 71 +++++++++++++---------------------------------- connections_controller.go | 47 +++++++++++++++++++------------ search_controller.go | 44 ++++++++++++++++++++--------- storage.go | 2 +- 5 files changed, 83 insertions(+), 85 deletions(-) (limited to 'search_controller.go') diff --git a/application_context.go b/application_context.go index fc76d00..8abb6f4 100644 --- a/application_context.go +++ b/application_context.go @@ -112,9 +112,9 @@ func (sm *ApplicationContext) configure() { sm.RulesManager = rulesManager sm.PcapImporter = NewPcapImporter(sm.Storage, *serverNet, sm.RulesManager) sm.ServicesController = NewServicesController(sm.Storage) - sm.ConnectionsController = NewConnectionsController(sm.Storage, sm.ServicesController) - sm.ConnectionStreamsController = NewConnectionStreamsController(sm.Storage) sm.SearchController = NewSearchController(sm.Storage) + sm.ConnectionsController = NewConnectionsController(sm.Storage, sm.SearchController, sm.ServicesController) + sm.ConnectionStreamsController = NewConnectionStreamsController(sm.Storage) sm.StatisticsController = NewStatisticsController(sm.Storage) sm.IsConfigured = true } diff --git a/application_router.go b/application_router.go index dc9f9d4..656b63e 100644 --- a/application_router.go +++ b/application_router.go @@ -22,8 +22,6 @@ import ( "fmt" "github.com/gin-gonic/contrib/static" "github.com/gin-gonic/gin" - "github.com/gin-gonic/gin/binding" - "github.com/go-playground/validator/v10" log "github.com/sirupsen/logrus" "net/http" "os" @@ -297,71 +295,42 @@ func CreateApplicationRouter(applicationContext *ApplicationContext, }) api.GET("/searches", func(c *gin.Context) { - success(c, applicationContext.SearchController.PerformedSearches()) + success(c, applicationContext.SearchController.GetPerformedSearches()) }) api.POST("/searches/perform", func(c *gin.Context) { var options SearchOptions - - parentIsZero := func (fl validator.FieldLevel) bool { - log.Println(fl.FieldName()) - log.Println("noooooo") - return fl.Parent().IsZero() - } - eitherWith := func (fl validator.FieldLevel) bool { - otherField := fl.Parent().FieldByName(fl.Param()) - log.Println(fl.Param()) - log.Println("bbbbbbbbbb") - return (fl.Field().IsZero() && !otherField.IsZero()) || (!fl.Field().IsZero() && otherField.IsZero()) - } - aaa := func (fl validator.FieldLevel) bool { - - log.Println("awww") - return fl.Field().IsZero() + if err := c.ShouldBindJSON(&options); err != nil { + badRequest(c, err) + return } - bbb := func (fl validator.FieldLevel) bool { - - log.Println("iiiii") - return true + // stupid checks because validator library is a shit + var badContentError error + if options.TextSearch.isZero() == options.RegexSearch.isZero() { + badContentError = errors.New("specify either 'text_search' or 'regex_search'") } - - if validate, ok := binding.Validator.Engine().(*validator.Validate); ok { - if err := validate.RegisterValidation("parent_is_zero", parentIsZero); err != nil { - log.WithError(err).Panic("cannot register 'topzero' validator") + if !options.TextSearch.isZero() { + if (options.TextSearch.Terms == nil) == (options.TextSearch.ExactPhrase == "") { + badContentError = errors.New("specify either 'terms' or 'exact_phrase'") } - if err := validate.RegisterValidation("either_with", eitherWith); err != nil { - log.WithError(err).Panic("cannot register 'either_with' validator") + if (options.TextSearch.Terms == nil) && (options.TextSearch.ExcludedTerms != nil) { + badContentError = errors.New("'excluded_terms' must be specified only with 'terms'") } - if err := validate.RegisterValidation("aaa", aaa); err != nil { - log.WithError(err).Panic("cannot register 'either_with' validator") - } - if err := validate.RegisterValidation("bbb", bbb); err != nil { - log.WithError(err).Panic("cannot register 'either_with' validator") + } + if !options.RegexSearch.isZero() { + if (options.RegexSearch.Pattern == "") == (options.RegexSearch.NotPattern == "") { + badContentError = errors.New("specify either 'pattern' or 'not_pattern'") } - } else { - log.Panic("cannot ") } - - if err := c.ShouldBindJSON(&options); err != nil { - badRequest(c, err) + if badContentError != nil { + badRequest(c, badContentError) return } - log.Println(options) - - - success(c, "ok") - - - - - - - - //success(c, applicationContext.SearchController.PerformSearch(c, options)) + success(c, applicationContext.SearchController.PerformSearch(c, options)) }) api.GET("/streams/:id", func(c *gin.Context) { diff --git a/connections_controller.go b/connections_controller.go index 30a5ee5..a293a80 100644 --- a/connections_controller.go +++ b/connections_controller.go @@ -48,33 +48,37 @@ type Connection struct { } type ConnectionsFilter struct { - From string `form:"from" binding:"omitempty,hexadecimal,len=24"` - To string `form:"to" binding:"omitempty,hexadecimal,len=24"` - ServicePort uint16 `form:"service_port"` - ClientAddress string `form:"client_address" binding:"omitempty,ip"` - ClientPort uint16 `form:"client_port"` - MinDuration uint `form:"min_duration"` - MaxDuration uint `form:"max_duration" binding:"omitempty,gtefield=MinDuration"` - MinBytes uint `form:"min_bytes"` - MaxBytes uint `form:"max_bytes" binding:"omitempty,gtefield=MinBytes"` - StartedAfter int64 `form:"started_after" ` - StartedBefore int64 `form:"started_before" binding:"omitempty,gtefield=StartedAfter"` - ClosedAfter int64 `form:"closed_after" ` - ClosedBefore int64 `form:"closed_before" binding:"omitempty,gtefield=ClosedAfter"` - Hidden bool `form:"hidden"` - Marked bool `form:"marked"` - MatchedRules []string `form:"matched_rules" binding:"dive,hexadecimal,len=24"` - Limit int64 `form:"limit"` + From string `form:"from" binding:"omitempty,hexadecimal,len=24"` + To string `form:"to" binding:"omitempty,hexadecimal,len=24"` + ServicePort uint16 `form:"service_port"` + ClientAddress string `form:"client_address" binding:"omitempty,ip"` + ClientPort uint16 `form:"client_port"` + MinDuration uint `form:"min_duration"` + MaxDuration uint `form:"max_duration" binding:"omitempty,gtefield=MinDuration"` + MinBytes uint `form:"min_bytes"` + MaxBytes uint `form:"max_bytes" binding:"omitempty,gtefield=MinBytes"` + StartedAfter int64 `form:"started_after" ` + StartedBefore int64 `form:"started_before" binding:"omitempty,gtefield=StartedAfter"` + ClosedAfter int64 `form:"closed_after" ` + ClosedBefore int64 `form:"closed_before" binding:"omitempty,gtefield=ClosedAfter"` + Hidden bool `form:"hidden"` + Marked bool `form:"marked"` + MatchedRules []string `form:"matched_rules" binding:"dive,hexadecimal,len=24"` + PerformedSearch string `form:"performed_search" binding:"omitempty,hexadecimal,len=24"` + Limit int64 `form:"limit"` } type ConnectionsController struct { storage Storage + searchController *SearchController servicesController *ServicesController } -func NewConnectionsController(storage Storage, servicesController *ServicesController) ConnectionsController { +func NewConnectionsController(storage Storage, searchesController *SearchController, + servicesController *ServicesController) ConnectionsController { return ConnectionsController{ storage: storage, + searchController: searchesController, servicesController: servicesController, } } @@ -144,6 +148,13 @@ func (cc ConnectionsController) GetConnections(c context.Context, filter Connect query = query.Filter(OrderedDocument{{"matched_rules", UnorderedDocument{"$all": matchedRules}}}) } + performedSearchID, _ := RowIDFromHex(filter.PerformedSearch) + if !performedSearchID.IsZero() { + performedSearch := cc.searchController.GetPerformedSearch(performedSearchID) + if !performedSearch.ID.IsZero() { + query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$in": performedSearch.AffectedConnections}}}) + } + } if filter.Limit > 0 && filter.Limit <= MaxQueryLimit { query = query.Limit(filter.Limit) } else { diff --git a/search_controller.go b/search_controller.go index ad47dbc..723cd93 100644 --- a/search_controller.go +++ b/search_controller.go @@ -26,14 +26,15 @@ import ( ) const ( - secondsToNano = 1000 * 1000 * 1000 - maxSearchTimeout = 60 * secondsToNano + secondsToNano = 1000 * 1000 * 1000 + maxSearchTimeout = 10 * secondsToNano + maxRecentSearches = 200 ) type PerformedSearch struct { ID RowID `bson:"_id" json:"id"` SearchOptions SearchOptions `bson:"search_options" json:"search_options"` - AffectedConnections []RowID `bson:"affected_connections" json:"affected_connections,omitempty"` + AffectedConnections []RowID `bson:"affected_connections" json:"-"` AffectedConnectionsCount int `bson:"affected_connections_count" json:"affected_connections_count"` StartedAt time.Time `bson:"started_at" json:"started_at"` FinishedAt time.Time `bson:"finished_at" json:"finished_at"` @@ -42,21 +43,21 @@ type PerformedSearch struct { } type SearchOptions struct { - TextSearch TextSearch `bson:"text_search" json:"text_search" validate:"either_with=RegexSearch"` - RegexSearch RegexSearch `bson:"regex_search" json:"regex_search" validate:"either_with=TextSearch"` + TextSearch TextSearch `bson:"text_search" json:"text_search"` + RegexSearch RegexSearch `bson:"regex_search" json:"regex_search"` Timeout time.Duration `bson:"timeout" json:"timeout" binding:"max=60"` } type TextSearch struct { - Terms []string `bson:"terms" json:"terms" binding:"parent_is_zero|either_with=ExactPhrase,isdefault|min=1,dive,min=3"` - ExcludedTerms []string `bson:"excluded_terms" json:"excluded_terms" binding:"required_with=Terms,dive,isdefault|min=1"` - ExactPhrase string `bson:"exact_phrase" json:"exact_phrase" binding:"isdefault|min=3,parent_is_zero|either_with=Terms"` + Terms []string `bson:"terms" json:"terms" binding:"isdefault|min=1,dive,min=3"` + ExcludedTerms []string `bson:"excluded_terms" json:"excluded_terms" binding:"isdefault|min=1,dive,min=3"` + ExactPhrase string `bson:"exact_phrase" json:"exact_phrase" binding:"isdefault|min=3"` CaseSensitive bool `bson:"case_sensitive" json:"case_sensitive"` } type RegexSearch struct { - Pattern string `bson:"pattern" json:"pattern" binding:"parent_is_zero|either_with=NotPattern,isdefault|min=3"` - NotPattern string `bson:"not_pattern" json:"not_pattern" binding:"parent_is_zero|either_with=Pattern,isdefault|min=3"` + Pattern string `bson:"pattern" json:"pattern" binding:"isdefault|min=3"` + NotPattern string `bson:"not_pattern" json:"not_pattern" binding:"isdefault|min=3"` CaseInsensitive bool `bson:"case_insensitive" json:"case_insensitive"` MultiLine bool `bson:"multi_line" json:"multi_line"` IgnoreWhitespaces bool `bson:"ignore_whitespaces" json:"ignore_whitespaces"` @@ -71,8 +72,8 @@ type SearchController struct { func NewSearchController(storage Storage) *SearchController { var searches []PerformedSearch - if err := storage.Find(Searches).All(&searches); err != nil { - // log.WithError(err).Panic("failed to retrieve performed searches") + if err := storage.Find(Searches).Limit(maxRecentSearches).All(&searches); err != nil { + log.WithError(err).Panic("failed to retrieve performed searches") } return &SearchController{ @@ -81,13 +82,27 @@ func NewSearchController(storage Storage) *SearchController { } } -func (sc *SearchController) PerformedSearches() []PerformedSearch { +func (sc *SearchController) GetPerformedSearches() []PerformedSearch { sc.mutex.Lock() defer sc.mutex.Unlock() return sc.performedSearches } +func (sc *SearchController) GetPerformedSearch(id RowID) PerformedSearch { + sc.mutex.Lock() + defer sc.mutex.Unlock() + + var performedSearch PerformedSearch + for _, search := range sc.performedSearches { + if search.ID == id { + performedSearch = search + } + } + + return performedSearch +} + func (sc *SearchController) PerformSearch(c context.Context, options SearchOptions) PerformedSearch { findQuery := sc.storage.Find(ConnectionStreams).Projection(OrderedDocument{{"connection_id", 1}}).Context(c) timeout := options.Timeout * secondsToNano @@ -163,6 +178,9 @@ func (sc *SearchController) PerformSearch(c context.Context, options SearchOptio sc.mutex.Lock() sc.performedSearches = append([]PerformedSearch{performedSearch}, sc.performedSearches...) + if len(sc.performedSearches) > maxRecentSearches { + sc.performedSearches = sc.performedSearches[:200] + } sc.mutex.Unlock() return performedSearch diff --git a/storage.go b/storage.go index 304e88c..8505bfe 100644 --- a/storage.go +++ b/storage.go @@ -77,7 +77,7 @@ func NewMongoStorage(uri string, port int, database string) (*MongoStorage, erro ConnectionStreams: db.Collection(ConnectionStreams), ImportingSessions: db.Collection(ImportingSessions), Rules: db.Collection(Rules), - Searches: db.Collection(Services), + Searches: db.Collection(Searches), Settings: db.Collection(Settings), Services: db.Collection(Services), Statistics: db.Collection(Statistics), -- cgit v1.2.3-70-g09d2 From 08456e7f2e1c1af6fc8fdbf580c0178a25b93f8b Mon Sep 17 00:00:00 2001 From: Emiliano Ciavatta Date: Thu, 15 Oct 2020 08:53:09 +0200 Subject: General improvements --- frontend/public/logo128.png | Bin 0 -> 6498 bytes frontend/public/logo192.png | Bin 6498 -> 0 bytes frontend/public/manifest.json | 8 +- frontend/src/components/Header.js | 2 +- frontend/src/components/Timeline.js | 122 +++++++++++++++------ frontend/src/components/Timeline.scss | 1 + .../components/filters/BooleanConnectionsFilter.js | 64 ++++------- .../src/components/filters/ExitSearchFilter.js | 9 +- .../src/components/filters/FiltersDispatcher.js | 57 ---------- .../components/filters/RulesConnectionsFilter.js | 81 ++++++-------- .../components/filters/StringConnectionsFilter.js | 77 ++++++------- frontend/src/components/objects/Connection.js | 4 +- .../components/objects/ConnectionMatchedRules.js | 21 ++-- frontend/src/components/pages/MainPage.js | 3 - frontend/src/components/panels/ConnectionsPane.js | 122 ++++++++++++--------- frontend/src/components/panels/SearchPane.scss | 1 + frontend/src/components/panels/StreamsPane.js | 2 +- frontend/src/dispatcher.js | 6 + search_controller.go | 4 + statistics_controller.go | 19 ++-- 20 files changed, 299 insertions(+), 304 deletions(-) create mode 100644 frontend/public/logo128.png delete mode 100644 frontend/public/logo192.png delete mode 100644 frontend/src/components/filters/FiltersDispatcher.js (limited to 'search_controller.go') diff --git a/frontend/public/logo128.png b/frontend/public/logo128.png new file mode 100644 index 0000000..1969e1d Binary files /dev/null and b/frontend/public/logo128.png differ diff --git a/frontend/public/logo192.png b/frontend/public/logo192.png deleted file mode 100644 index 1969e1d..0000000 Binary files a/frontend/public/logo192.png and /dev/null differ diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index 0409a59..32674ce 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "Caronte", - "name": "Caronte", + "short_name": "caronte", + "name": "caronte", "icons": [ { "src": "favicon.ico", @@ -8,9 +8,9 @@ "type": "image/x-icon" }, { - "src": "logo192.png", + "src": "logo128.png", "type": "image/png", - "sizes": "192x192" + "sizes": "128x128" }, { "src": "logo512.png", diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index b72b532..b4a2177 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -89,7 +89,7 @@ class Header extends Component {
- {/**/} + diff --git a/frontend/src/components/Timeline.js b/frontend/src/components/Timeline.js index 6b8806f..bc42a01 100644 --- a/frontend/src/components/Timeline.js +++ b/frontend/src/components/Timeline.js @@ -35,8 +35,12 @@ import log from "../log"; import dispatcher from "../dispatcher"; const minutes = 60 * 1000; +const _ = require('lodash'); const classNames = require('classnames'); +const leftSelectionPaddingMultiplier = 24; +const rightSelectionPaddingMultiplier = 8; + class Timeline extends Component { state = { @@ -50,25 +54,30 @@ class Timeline extends Component { this.selectionTimeout = null; } - filteredPort = () => { + additionalFilters = () => { const urlParams = new URLSearchParams(this.props.location.search); - return urlParams.get("service_port"); + if (this.state.metric === "matched_rules") { + return urlParams.getAll("matched_rules") || []; + } else { + return urlParams.get("service_port"); + } }; componentDidMount() { - const filteredPort = this.filteredPort(); - this.setState({filteredPort}); - this.loadStatistics(this.state.metric, filteredPort).then(() => log.debug("Statistics loaded after mount")); + const additionalFilters = this.additionalFilters(); + this.setState({filters: additionalFilters}); + this.loadStatistics(this.state.metric, additionalFilters).then(() => log.debug("Statistics loaded after mount")); dispatcher.register("connection_updates", payload => { this.setState({ selection: new TimeRange(payload.from, payload.to), }); + this.adjustSelection(); }); dispatcher.register("notifications", payload => { if (payload.event === "services.edit") { - this.loadServices().then(() => log.debug("Services reloaded after notification update")); + this.loadServices().then(() => this.adjustSelection()); } }); @@ -79,27 +88,48 @@ class Timeline extends Component { } componentDidUpdate(prevProps, prevState, snapshot) { - const filteredPort = this.filteredPort(); - if (this.state.filteredPort !== filteredPort) { - this.setState({filteredPort}); - this.loadStatistics(this.state.metric, filteredPort).then(() => - log.debug("Statistics reloaded after filtered port changes")); + 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(); + } } } - loadStatistics = async (metric, filteredPort) => { + loadStatistics = async (metric, filters) => { const urlParams = new URLSearchParams(); urlParams.set("metric", metric); - let services = await this.loadServices(); - if (filteredPort && services[filteredPort]) { - const service = services[filteredPort]; - services = {}; - services[filteredPort] = service; - } + 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); + } else { + let services = await this.loadServices(); + const filteredPort = filters; + if (filteredPort && services[filters]) { + const service = services[filteredPort]; + services = {}; + services[filteredPort] = service; + } - const ports = Object.keys(services); - ports.forEach(s => urlParams.append("ports", s)); + columns = Object.keys(services); + columns.forEach(port => urlParams.append("ports", port)); + } const metrics = (await backend.get("/api/statistics?" + urlParams)).json; if (metrics.length === 0) { @@ -109,8 +139,8 @@ class Timeline extends Component { const zeroFilledMetrics = []; const toTime = m => new Date(m["range_start"]).getTime(); let i = 0; - for (let interval = toTime(metrics[0]); interval <= toTime(metrics[metrics.length - 1]); interval += minutes) { - if (interval === toTime(metrics[i])) { + for (let interval = toTime(metrics[0]) - minutes; interval <= toTime(metrics[metrics.length - 1]) + minutes; interval += minutes) { + if (i < metrics.length && interval === toTime(metrics[i])) { const m = metrics[i++]; m["range_start"] = new Date(m["range_start"]); zeroFilledMetrics.push(m); @@ -118,30 +148,31 @@ class Timeline extends Component { const m = {}; m["range_start"] = new Date(interval); m[metric] = {}; - ports.forEach(p => m[metric][p] = 0); + columns.forEach(c => m[metric][c] = 0); zeroFilledMetrics.push(m); } } const series = new TimeSeries({ name: "statistics", - columns: ["time"].concat(ports), - points: zeroFilledMetrics.map(m => [m["range_start"]].concat(ports.map(p => m[metric][p] || 0))) + columns: ["time"].concat(columns), + points: zeroFilledMetrics.map(m => [m["range_start"]].concat(columns.map(c => + ((metric in m) && (m[metric] != null)) ? (m[metric][c] || 0) : 0 + ))) }); const start = series.range().begin(); const end = series.range().end(); - start.setTime(start.getTime() - minutes); - end.setTime(end.getTime() + minutes); this.setState({ metric, series, timeRange: new TimeRange(start, end), + columns, start, end }); - log.debug(`Loaded statistics for metric "${metric}" for services [${ports}]`); + log.debug(`Loaded statistics for metric "${metric}"`); }; loadServices = async () => { @@ -150,10 +181,22 @@ class Timeline extends Component { return services; }; + loadRules = async () => { + const rules = (await backend.get("/api/rules")).json; + this.setState({rules}); + return rules; + }; + createStyler = () => { - return styler(Object.keys(this.state.services).map(port => { - return {key: port, color: this.state.services[port].color, width: 2}; - })); + if (this.state.metric === "matched_rules") { + return styler(this.state.rules.map(rule => { + return {key: rule.id, color: rule.color, width: 2}; + })); + } else { + return styler(Object.keys(this.state.services).map(port => { + return {key: port, color: this.state.services[port].color, width: 2}; + })); + } }; handleTimeRangeChange = (timeRange) => { @@ -179,6 +222,15 @@ class Timeline extends Component { }, 1000); }; + adjustSelection = () => { + const seriesRange = this.state.series.range(); + const selection = this.state.selection; + const delta = selection.end() - selection.begin(); + const start = Math.max(selection.begin().getTime() - delta * leftSelectionPaddingMultiplier, seriesRange.begin().getTime()); + const end = Math.min(selection.end().getTime() + delta * rightSelectionPaddingMultiplier, seriesRange.end().getTime()); + this.setState({timeRange: new TimeRange(start, end)}); + }; + aggregateSeries = (func) => { const values = this.state.series.columns().map(c => this.state.series[func](c)); return Math[func](...values); @@ -207,7 +259,7 @@ class Timeline extends Component { max={this.aggregateSeries("max")} width="35" type="linear" transition={300}/> this.loadStatistics(metric, this.state.filteredPort) + "server_bytes_per_service", "duration_per_service", "matched_rules"]} + onChange={(metric) => this.loadStatistics(metric, this.state.filters) .then(() => log.debug("Statistics loaded after metric changes"))} value={this.state.metric}/>
diff --git a/frontend/src/components/Timeline.scss b/frontend/src/components/Timeline.scss index db8d9c8..262da1e 100644 --- a/frontend/src/components/Timeline.scss +++ b/frontend/src/components/Timeline.scss @@ -12,6 +12,7 @@ position: absolute; top: 5px; right: 10px; + width: 180px; } &.pulse-timeline { diff --git a/frontend/src/components/filters/BooleanConnectionsFilter.js b/frontend/src/components/filters/BooleanConnectionsFilter.js index a9a420e..c611a0d 100644 --- a/frontend/src/components/filters/BooleanConnectionsFilter.js +++ b/frontend/src/components/filters/BooleanConnectionsFilter.js @@ -17,65 +17,49 @@ import React, {Component} from 'react'; import {withRouter} from "react-router-dom"; -import {Redirect} from "react-router"; import CheckField from "../fields/CheckField"; +import dispatcher from "../../dispatcher"; class BooleanConnectionsFilter extends Component { - constructor(props) { - super(props); - this.state = { - filterActive: "false" - }; - - this.filterChanged = this.filterChanged.bind(this); - this.needRedirect = false; - } + state = { + filterActive: "false" + }; componentDidMount() { let params = new URLSearchParams(this.props.location.search); this.setState({filterActive: this.toBoolean(params.get(this.props.filterName)).toString()}); + + this.connectionsFiltersCallback = payload => { + const name = this.props.filterName; + if (name in payload && this.state.filterActive !== payload[name]) { + this.setState({filterActive: payload[name]}); + } + }; + dispatcher.register("connections_filters", this.connectionsFiltersCallback); } - 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()}); - } + componentWillUnmount() { + dispatcher.unregister(this.connectionsFiltersCallback); } - toBoolean(value) { + toBoolean = (value) => { return value !== null && value.toLowerCase() === "true"; - } + }; - filterChanged() { - this.needRedirect = true; - this.setState({filterActive: (!this.toBoolean(this.state.filterActive)).toString()}); - } + filterChanged = () => { + const newValue = (!this.toBoolean(this.state.filterActive)).toString(); + const urlParams = {}; + urlParams[this.props.filterName] = newValue === "true" ? "true" : null; + dispatcher.dispatch("connections_filters", urlParams); + this.setState({filterActive: newValue}); + }; 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 = ; - - this.needRedirect = false; - } - return (
- {redirect} + onChange={this.filterChanged}/>
); } diff --git a/frontend/src/components/filters/ExitSearchFilter.js b/frontend/src/components/filters/ExitSearchFilter.js index cfee298..68ca686 100644 --- a/frontend/src/components/filters/ExitSearchFilter.js +++ b/frontend/src/components/filters/ExitSearchFilter.js @@ -28,11 +28,16 @@ class ExitSearchFilter extends Component { let params = new URLSearchParams(this.props.location.search); this.setState({performedSearch: params.get("performed_search")}); - dispatcher.register("connections_filters", payload => { + this.connectionsFiltersCallback = payload => { if (this.state.performedSearch !== payload["performed_search"]) { this.setState({performedSearch: payload["performed_search"]}); } - }); + }; + dispatcher.register("connections_filters", this.connectionsFiltersCallback); + } + + componentWillUnmount() { + dispatcher.unregister(this.connectionsFiltersCallback); } render() { diff --git a/frontend/src/components/filters/FiltersDispatcher.js b/frontend/src/components/filters/FiltersDispatcher.js deleted file mode 100644 index 3769055..0000000 --- a/frontend/src/components/filters/FiltersDispatcher.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 . - */ - -import React, {Component} from 'react'; -import {withRouter} from "react-router-dom"; -import {Redirect} from "react-router"; -import dispatcher from "../../dispatcher"; - -class FiltersDispatcher extends Component { - - state = {}; - - componentDidMount() { - let params = new URLSearchParams(this.props.location.search); - this.setState({params}); - - dispatcher.register("connections_filters", payload => { - const params = this.state.params; - - Object.entries(payload).forEach(([key, value]) => { - if (value == null) { - params.delete(key); - } else { - params.set(key, value); - } - }); - - this.needRedirect = true; - this.setState({params}); - }); - } - - render() { - if (this.needRedirect) { - this.needRedirect = false; - return ; - } - - return null; - } -} - -export default withRouter(FiltersDispatcher); diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js index fc0ad4d..4c993dc 100644 --- a/frontend/src/components/filters/RulesConnectionsFilter.js +++ b/frontend/src/components/filters/RulesConnectionsFilter.js @@ -17,87 +17,74 @@ 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 backend from "../../backend"; +import dispatcher from "../../dispatcher"; const classNames = require('classnames'); +const _ = require('lodash'); class RulesConnectionsFilter extends Component { - constructor(props) { - super(props); - this.state = { - mounted: false, - rules: [], - activeRules: [] - }; - - this.needRedirect = false; - } + state = { + rules: [], + activeRules: [] + }; componentDidMount() { - let params = new URLSearchParams(this.props.location.search); + const params = new URLSearchParams(this.props.location.search); let activeRules = params.getAll("matched_rules") || []; backend.get("/api/rules").then(res => { let rules = res.json.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}); + this.setState({rules, activeRules}); }); + + this.connectionsFiltersCallback = payload => { + if ("matched_rules" in payload && !_.isEqual(payload["matched_rules"].sort(), this.state.activeRules.sort())) { + const newRules = this.state.rules.filter(r => payload["matched_rules"].includes(r.id)); + this.setState({ + activeRules: newRules.map(r => { + return {id: r.id, name: r.name}; + }) + }); + } + }; + dispatcher.register("connections_filters", this.connectionsFiltersCallback); } - 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))}); - } + componentWillUnmount() { + dispatcher.unregister(this.connectionsFiltersCallback); } - onDelete(i) { - const activeRules = this.state.activeRules.slice(0); + onDelete = (i) => { + const activeRules = _.clone(this.state.activeRules); activeRules.splice(i, 1); - this.needRedirect = true; - this.setState({ activeRules }); - } + this.setState({activeRules}); + dispatcher.dispatch("connections_filters", {"matched_rules": activeRules.map(r => r.id)}); + }; - onAddition(rule) { + onAddition = (rule) => { if (!this.state.activeRules.includes(rule)) { const activeRules = [].concat(this.state.activeRules, rule); - this.needRedirect = true; this.setState({activeRules}); + dispatcher.dispatch("connections_filters", {"matched_rules": activeRules.map(r => r.id)}); } - } + }; 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 = ; - - this.needRedirect = false; - } - return ( -
+
- suggestion.name.startsWith(query) && !this.state.activeRules.includes(suggestion)} /> + suggestion.name.startsWith(query) && !this.state.activeRules.includes(suggestion)}/>
- - {redirect}
); } diff --git a/frontend/src/components/filters/StringConnectionsFilter.js b/frontend/src/components/filters/StringConnectionsFilter.js index a3b45dc..c833220 100644 --- a/frontend/src/components/filters/StringConnectionsFilter.js +++ b/frontend/src/components/filters/StringConnectionsFilter.js @@ -17,37 +17,36 @@ import React, {Component} from 'react'; import {withRouter} from "react-router-dom"; -import {Redirect} from "react-router"; import InputField from "../fields/InputField"; +import dispatcher from "../../dispatcher"; 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); - } + state = { + fieldValue: "", + filterValue: null, + timeoutHandle: null, + invalidValue: false + }; componentDidMount() { let params = new URLSearchParams(this.props.location.search); this.updateStateFromFilterValue(params.get(this.props.filterName)); + + this.connectionsFiltersCallback = payload => { + const name = this.props.filterName; + if (name in payload && this.state.filterValue !== payload[name]) { + this.updateStateFromFilterValue(payload[name]); + } + }; + dispatcher.register("connections_filters", this.connectionsFiltersCallback); } - 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); - } + componentWillUnmount() { + dispatcher.unregister(this.connectionsFiltersCallback); } - updateStateFromFilterValue(filterValue) { + updateStateFromFilterValue = (filterValue) => { if (filterValue !== null) { let fieldValue = filterValue; if (typeof this.props.decodeFunc === "function") { @@ -70,15 +69,21 @@ class StringConnectionsFilter extends Component { } else { this.setState({fieldValue: "", filterValue: null}); } - } + }; - isValueValid(value) { + isValueValid = (value) => { return typeof this.props.validateFunc !== "function" || (typeof this.props.validateFunc === "function" && this.props.validateFunc(value)); - } + }; - filterChanged(fieldValue) { - if (this.state.timeoutHandle !== null) { + changeFilterValue = (value) => { + const urlParams = {}; + urlParams[this.props.filterName] = value; + dispatcher.dispatch("connections_filters", urlParams); + }; + + filterChanged = (fieldValue) => { + if (this.state.timeoutHandle) { clearTimeout(this.state.timeoutHandle); } @@ -87,11 +92,12 @@ class StringConnectionsFilter extends Component { } if (fieldValue === "") { - this.needRedirect = true; this.setState({fieldValue: "", filterValue: null, invalidValue: false}); - return; + return this.changeFilterValue(null); } + + if (this.isValueValid(fieldValue)) { let filterValue = fieldValue; if (filterValue !== "" && typeof this.props.encodeFunc === "function") { @@ -101,40 +107,27 @@ class StringConnectionsFilter extends Component { this.setState({ fieldValue: fieldValue, timeoutHandle: setTimeout(() => { - this.needRedirect = true; this.setState({filterValue: filterValue}); + this.changeFilterValue(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 = ; - this.needRedirect = false; - } let active = this.state.filterValue !== null; return (
- {redirect} + value={this.state.fieldValue} inline={true} small={true}/>
); } diff --git a/frontend/src/components/objects/Connection.js b/frontend/src/components/objects/Connection.js index e0e942a..96f2235 100644 --- a/frontend/src/components/objects/Connection.js +++ b/frontend/src/components/objects/Connection.js @@ -23,6 +23,7 @@ import {dateTimeToTime, durationBetween, formatSize} from "../../utils"; import ButtonField from "../fields/ButtonField"; import LinkPopover from "./LinkPopover"; import TextField from "../fields/TextField"; +import dispatcher from "../../dispatcher"; const classNames = require('classnames'); @@ -99,7 +100,8 @@ class Connection extends Component { this.props.addServicePortFilter(conn["port_dst"])}/> + onClick={() => dispatcher.dispatch("connections_filters", + {"service_port": conn["port_dst"].toString()})}/> {conn["ip_src"]} diff --git a/frontend/src/components/objects/ConnectionMatchedRules.js b/frontend/src/components/objects/ConnectionMatchedRules.js index 73d5c5d..92bde49 100644 --- a/frontend/src/components/objects/ConnectionMatchedRules.js +++ b/frontend/src/components/objects/ConnectionMatchedRules.js @@ -18,20 +18,25 @@ import React, {Component} from 'react'; import './ConnectionMatchedRules.scss'; import ButtonField from "../fields/ButtonField"; +import dispatcher from "../../dispatcher"; +import {withRouter} from "react-router-dom"; class ConnectionMatchedRules extends Component { - constructor(props) { - super(props); - this.state = { - }; - } + onMatchedRulesSelected = (id) => { + const params = new URLSearchParams(this.props.location.search); + const rules = params.getAll("matched_rules"); + if (!rules.includes(id)) { + rules.push(id); + dispatcher.dispatch("connections_filters",{"matched_rules": rules}); + } + }; render() { const matchedRules = this.props.matchedRules.map(mr => { const rule = this.props.rules.find(r => r.id === mr); - return this.props.addMatchedRulesFilter(rule.id)} name={rule.name} - color={rule.color} small />; + return this.onMatchedRulesSelected(rule.id)} name={rule.name} + color={rule.color} small/>; }); return ( @@ -43,4 +48,4 @@ class ConnectionMatchedRules extends Component { } } -export default ConnectionMatchedRules; +export default withRouter(ConnectionMatchedRules); diff --git a/frontend/src/components/pages/MainPage.js b/frontend/src/components/pages/MainPage.js index da57e1a..0b06f55 100644 --- a/frontend/src/components/pages/MainPage.js +++ b/frontend/src/components/pages/MainPage.js @@ -29,7 +29,6 @@ import Header from "../Header"; import Filters from "../dialogs/Filters"; import MainPane from "../panels/MainPane"; import SearchPane from "../panels/SearchPane"; -import FiltersDispatcher from "../filters/FiltersDispatcher"; class MainPage extends Component { @@ -70,8 +69,6 @@ class MainPage extends Component {
- -
); diff --git a/frontend/src/components/panels/ConnectionsPane.js b/frontend/src/components/panels/ConnectionsPane.js index 1f79ab8..33dd7c1 100644 --- a/frontend/src/components/panels/ConnectionsPane.js +++ b/frontend/src/components/panels/ConnectionsPane.js @@ -19,13 +19,13 @@ import React, {Component} from 'react'; import './ConnectionsPane.scss'; import Connection from "../objects/Connection"; import Table from 'react-bootstrap/Table'; -import {Redirect} from 'react-router'; import {withRouter} from "react-router-dom"; import backend from "../../backend"; import ConnectionMatchedRules from "../objects/ConnectionMatchedRules"; import log from "../../log"; import ButtonField from "../fields/ButtonField"; import dispatcher from "../../dispatcher"; +import {Redirect} from "react-router"; const classNames = require('classnames'); @@ -50,60 +50,91 @@ class ConnectionsPane extends Component { } componentDidMount() { - const initialParams = {limit: this.queryLimit}; + let urlParams = new URLSearchParams(this.props.location.search); + this.setState({urlParams}); + + const additionalParams = {limit: this.queryLimit}; const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/); if (match != null) { const id = match[1]; - initialParams.from = id; + additionalParams.from = id; backend.get(`/api/connections/${id}`) - .then(res => this.connectionSelected(res.json, false)) + .then(res => this.connectionSelected(res.json)) .catch(error => log.error("Error loading initial connection", error)); } - this.loadConnections(initialParams, true).then(() => log.debug("Connections loaded")); + this.loadConnections(additionalParams, urlParams, true).then(() => log.debug("Connections loaded")); + + this.connectionsFiltersCallback = payload => { + const params = this.state.urlParams; + const initialParams = params.toString(); + + Object.entries(payload).forEach(([key, value]) => { + if (value == null) { + params.delete(key); + } else if (Array.isArray(value)) { + params.delete(key); + value.forEach(v => params.append(key, v)); + } else { + params.set(key, value); + } + }); + + if (initialParams === params.toString()) { + return; + } - dispatcher.register("timeline_updates", payload => { + log.debug("Update following url params:", payload); + this.queryStringRedirect = true; + this.setState({urlParams}); + + this.loadConnections({limit: this.queryLimit}, urlParams) + .then(() => log.info("ConnectionsPane reloaded after query string update")); + }; + dispatcher.register("connections_filters", this.connectionsFiltersCallback); + + this.timelineUpdatesCallback = payload => { this.connectionsListRef.current.scrollTop = 0; this.loadConnections({ started_after: Math.round(payload.from.getTime() / 1000), started_before: Math.round(payload.to.getTime() / 1000), limit: this.maxConnections }).then(() => log.info(`Loading connections between ${payload.from} and ${payload.to}`)); - }); + }; + dispatcher.register("timeline_updates", this.timelineUpdatesCallback); - dispatcher.register("notifications", payload => { + this.notificationsCallback = payload => { if (payload.event === "rules.new" || payload.event === "rules.edit") { this.loadRules().then(() => log.debug("Loaded connection rules after notification update")); } - }); - - dispatcher.register("notifications", payload => { if (payload.event === "services.edit") { this.loadServices().then(() => log.debug("Services reloaded after notification update")); } - }); + }; + dispatcher.register("notifications", this.notificationsCallback); - dispatcher.register("pulse_connections_view", payload => { + this.pulseConnectionsViewCallback = payload => { this.setState({pulseConnectionsView: true}); setTimeout(() => this.setState({pulseConnectionsView: false}), payload.duration); - }); + }; + dispatcher.register("pulse_connections_view", this.pulseConnectionsViewCallback); + } + + componentWillUnmount() { + dispatcher.unregister(this.timelineUpdatesCallback); + dispatcher.unregister(this.notificationsCallback); + dispatcher.unregister(this.pulseConnectionsViewCallback); + dispatcher.unregister(this.connectionsFiltersCallback); } - connectionSelected = (c, doRedirect = true) => { - this.doSelectedConnectionRedirect = doRedirect; + connectionSelected = (c) => { + this.connectionSelectedRedirect = true; this.setState({selected: c.id}); this.props.onSelected(c); log.debug(`Connection ${c.id} selected`); }; - componentDidUpdate(prevProps, prevState, snapshot) { - if (prevProps.location.search !== this.props.location.search) { - this.loadConnections({limit: this.queryLimit}) - .then(() => log.info("ConnectionsPane reloaded after query string update")); - } - } - handleScroll = (e) => { if (this.disableScrollHandler) { this.lastScrollPosition = e.currentTarget.scrollTop; @@ -135,27 +166,12 @@ class ConnectionsPane extends Component { this.lastScrollPosition = e.currentTarget.scrollTop; }; - addServicePortFilter = (port) => { - const urlParams = new URLSearchParams(this.props.location.search); - urlParams.set("service_port", port); - this.doQueryStringRedirect = true; - this.setState({queryString: urlParams}); - }; - - addMatchedRulesFilter = (matchedRule) => { - const urlParams = new URLSearchParams(this.props.location.search); - const oldMatchedRules = urlParams.getAll("matched_rules") || []; - - if (!oldMatchedRules.includes(matchedRule)) { - urlParams.append("matched_rules", matchedRule); - this.doQueryStringRedirect = true; - this.setState({queryString: urlParams}); + async loadConnections(additionalParams, initialParams = null, isInitial = false) { + if (!initialParams) { + initialParams = this.state.urlParams; } - }; - - async loadConnections(params, isInitial = false) { - const urlParams = new URLSearchParams(this.props.location.search); - for (const [name, value] of Object.entries(params)) { + const urlParams = new URLSearchParams(initialParams.toString()); + for (const [name, value] of Object.entries(additionalParams)) { urlParams.set(name, value); } @@ -173,7 +189,7 @@ class ConnectionsPane extends Component { let firstConnection = this.state.firstConnection; let lastConnection = this.state.lastConnection; - if (params !== undefined && params.from !== undefined && params.to === undefined) { + if (additionalParams !== undefined && additionalParams.from !== undefined && additionalParams.to === undefined) { if (res.length > 0) { if (!isInitial) { res = res.slice(1); @@ -189,7 +205,7 @@ class ConnectionsPane extends Component { firstConnection = connections[0]; } } - } else if (params !== undefined && params.to !== undefined && params.from === undefined) { + } else if (additionalParams !== undefined && additionalParams.to !== undefined && additionalParams.from === undefined) { if (res.length > 0) { connections = res.slice(0, res.length - 1).concat(this.state.connections); firstConnection = connections[0]; @@ -235,12 +251,12 @@ class ConnectionsPane extends Component { render() { let redirect; - if (this.doSelectedConnectionRedirect) { - redirect = ; - this.doSelectedConnectionRedirect = false; - } else if (this.doQueryStringRedirect) { - redirect = ; - this.doQueryStringRedirect = false; + if (this.connectionSelectedRedirect) { + redirect = ; + this.connectionSelectedRedirect = false; + } else if (this.queryStringRedirect) { + redirect = ; + this.queryStringRedirect = false; } let loading = null; @@ -288,12 +304,10 @@ class ConnectionsPane extends Component { selected={this.state.selected === c.id} onMarked={marked => c.marked = marked} onEnabled={enabled => c.hidden = !enabled} - addServicePortFilter={this.addServicePortFilter} services={this.state.services}/>, c.matched_rules.length > 0 && + rules={this.state.rules}/> ]; }) } diff --git a/frontend/src/components/panels/SearchPane.scss b/frontend/src/components/panels/SearchPane.scss index 15fc7da..63e11fb 100644 --- a/frontend/src/components/panels/SearchPane.scss +++ b/frontend/src/components/panels/SearchPane.scss @@ -4,6 +4,7 @@ .searches-list { overflow: hidden; + flex: 2 1; .section-content { height: 100%; diff --git a/frontend/src/components/panels/StreamsPane.js b/frontend/src/components/panels/StreamsPane.js index bd1964e..1aa5c53 100644 --- a/frontend/src/components/panels/StreamsPane.js +++ b/frontend/src/components/panels/StreamsPane.js @@ -107,7 +107,7 @@ class StreamsPane extends Component { const json = JSON.parse(m.body); body = ; } catch (e) { - console.log(e); + log.error(e); } } diff --git a/frontend/src/dispatcher.js b/frontend/src/dispatcher.js index 943f7ec..fa08d48 100644 --- a/frontend/src/dispatcher.js +++ b/frontend/src/dispatcher.js @@ -15,6 +15,8 @@ * along with this program. If not, see . */ +const _ = require('lodash'); + class Dispatcher { constructor() { @@ -44,6 +46,10 @@ class Dispatcher { } }; + unregister = (callback) => { + this.listeners = _.without(callback); + }; + } const dispatcher = new Dispatcher(); diff --git a/search_controller.go b/search_controller.go index 723cd93..5ed762a 100644 --- a/search_controller.go +++ b/search_controller.go @@ -76,6 +76,10 @@ func NewSearchController(storage Storage) *SearchController { log.WithError(err).Panic("failed to retrieve performed searches") } + if searches == nil { + searches = []PerformedSearch{} + } + return &SearchController{ storage: storage, performedSearches: searches, diff --git a/statistics_controller.go b/statistics_controller.go index 57c7d95..fda7494 100644 --- a/statistics_controller.go +++ b/statistics_controller.go @@ -26,19 +26,19 @@ import ( type StatisticRecord struct { RangeStart time.Time `json:"range_start" bson:"_id"` - ConnectionsPerService map[uint16]int `json:"connections_per_service,omitempty" bson:"connections_per_service"` - ClientBytesPerService map[uint16]int `json:"client_bytes_per_service,omitempty" bson:"client_bytes_per_service"` - ServerBytesPerService map[uint16]int `json:"server_bytes_per_service,omitempty" bson:"server_bytes_per_service"` - TotalBytesPerService map[uint16]int `json:"total_bytes_per_service,omitempty" bson:"total_bytes_per_service"` - DurationPerService map[uint16]int64 `json:"duration_per_service,omitempty" bson:"duration_per_service"` - MatchedRules map[RowID]int64 `json:"matched_rules,omitempty" bson:"matched_rules"` + ConnectionsPerService map[uint16]int `json:"connections_per_service" bson:"connections_per_service"` + ClientBytesPerService map[uint16]int `json:"client_bytes_per_service" bson:"client_bytes_per_service"` + 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"` } type StatisticsFilter struct { RangeFrom time.Time `form:"range_from"` RangeTo time.Time `form:"range_to"` Ports []uint16 `form:"ports"` - RulesIDs []RowID `form:"rules_ids"` + RulesIDs []string `form:"rules_ids"` Metric string `form:"metric"` } @@ -57,7 +57,7 @@ func NewStatisticsController(storage Storage) StatisticsController { func (sc *StatisticsController) GetStatistics(context context.Context, filter StatisticsFilter) []StatisticRecord { var statisticRecords []StatisticRecord - query := sc.storage.Find(Statistics).Context(context) + query := sc.storage.Find(Statistics).Context(context).Sort("_id", true) if !filter.RangeFrom.IsZero() { query = query.Filter(OrderedDocument{{"_id", UnorderedDocument{"$lt": filter.RangeFrom}}}) } @@ -81,7 +81,7 @@ func (sc *StatisticsController) GetStatistics(context context.Context, filter St } for _, ruleID := range filter.RulesIDs { if filter.Metric == "" || filter.Metric == "matched_rules" { - query = query.Projection(OrderedDocument{{fmt.Sprintf("matched_rules.%s", ruleID.Hex()), 1}}) + query = query.Projection(OrderedDocument{{fmt.Sprintf("matched_rules.%s", ruleID), 1}}) } } @@ -91,6 +91,7 @@ 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{} -- cgit v1.2.3-70-g09d2