diff options
-rw-r--r-- | application_context.go | 12 | ||||
-rw-r--r-- | caronte.go | 2 | ||||
-rw-r--r-- | frontend/src/components/App.js | 6 | ||||
-rw-r--r-- | frontend/src/components/Notifications.js | 17 | ||||
-rw-r--r-- | frontend/src/components/Timeline.js | 88 | ||||
-rw-r--r-- | frontend/src/components/fields/ButtonField.js | 2 | ||||
-rw-r--r-- | frontend/src/components/fields/ChoiceField.js | 10 | ||||
-rw-r--r-- | frontend/src/components/objects/Connection.js | 36 | ||||
-rw-r--r-- | frontend/src/components/objects/ConnectionMatchedRules.js | 10 | ||||
-rw-r--r-- | frontend/src/components/objects/CopyLinkPopover.js | 54 | ||||
-rw-r--r-- | frontend/src/components/objects/MessageAction.js | 15 | ||||
-rw-r--r-- | frontend/src/components/panels/ConnectionsPane.js | 107 | ||||
-rw-r--r-- | frontend/src/components/panels/PcapsPane.js | 40 | ||||
-rw-r--r-- | frontend/src/components/panels/RulesPane.js | 51 | ||||
-rw-r--r-- | frontend/src/components/panels/SearchPane.js | 34 | ||||
-rw-r--r-- | frontend/src/components/panels/ServicesPane.js | 68 | ||||
-rw-r--r-- | frontend/src/serviceWorker.js | 208 | ||||
-rw-r--r-- | parsers/http_request_parser.go | 12 | ||||
-rw-r--r-- | pcap_importer.go | 39 |
19 files changed, 458 insertions, 353 deletions
diff --git a/application_context.go b/application_context.go index 8abb6f4..0410b88 100644 --- a/application_context.go +++ b/application_context.go @@ -39,6 +39,7 @@ type ApplicationContext struct { ConnectionStreamsController ConnectionStreamsController SearchController *SearchController StatisticsController StatisticsController + NotificationController *NotificationController IsConfigured bool Version string } @@ -70,13 +71,12 @@ func CreateApplicationContext(storage Storage, version string) (*ApplicationCont Version: version, } - applicationContext.configure() return applicationContext, nil } func (sm *ApplicationContext) SetConfig(config Config) { sm.Config = config - sm.configure() + sm.Configure() var upsertResults interface{} if _, err := sm.Storage.Update(Settings).Upsert(&upsertResults). Filter(OrderedDocument{{"_id", "config"}}).One(UnorderedDocument{"config": config}); err != nil { @@ -93,7 +93,11 @@ func (sm *ApplicationContext) SetAccounts(accounts gin.Accounts) { } } -func (sm *ApplicationContext) configure() { +func (sm *ApplicationContext) SetNotificationController(notificationController *NotificationController) { + sm.NotificationController = notificationController +} + +func (sm *ApplicationContext) Configure() { if sm.IsConfigured { return } @@ -110,7 +114,7 @@ func (sm *ApplicationContext) configure() { log.WithError(err).Panic("failed to create a RulesManager") } sm.RulesManager = rulesManager - sm.PcapImporter = NewPcapImporter(sm.Storage, *serverNet, sm.RulesManager) + sm.PcapImporter = NewPcapImporter(sm.Storage, *serverNet, sm.RulesManager, sm.NotificationController) sm.ServicesController = NewServicesController(sm.Storage) sm.SearchController = NewSearchController(sm.Storage) sm.ConnectionsController = NewConnectionsController(sm.Storage, sm.SearchController, sm.ServicesController) @@ -51,10 +51,12 @@ func main() { notificationController := NewNotificationController(applicationContext) go notificationController.Run() + applicationContext.SetNotificationController(notificationController) resourcesController := NewResourcesController(notificationController) go resourcesController.Run() + applicationContext.Configure() applicationRouter := CreateApplicationRouter(applicationContext, notificationController, resourcesController) if applicationRouter.Run(fmt.Sprintf("%s:%v", *bindAddress, *bindPort)) != nil { log.WithError(err).WithFields(logFields).Fatal("failed to create the server") diff --git a/frontend/src/components/App.js b/frontend/src/components/App.js index 0f700db..888ff86 100644 --- a/frontend/src/components/App.js +++ b/frontend/src/components/App.js @@ -15,10 +15,10 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import React, {Component} from 'react'; -import ConfigurationPage from "./pages/ConfigurationPage"; -import Notifications from "./Notifications"; +import React, {Component} from "react"; import dispatcher from "../dispatcher"; +import Notifications from "./Notifications"; +import ConfigurationPage from "./pages/ConfigurationPage"; import MainPage from "./pages/MainPage"; import ServiceUnavailablePage from "./pages/ServiceUnavailablePage"; diff --git a/frontend/src/components/Notifications.js b/frontend/src/components/Notifications.js index 56a4508..92731d9 100644 --- a/frontend/src/components/Notifications.js +++ b/frontend/src/components/Notifications.js @@ -17,6 +17,7 @@ import React, {Component} from "react"; import dispatcher from "../dispatcher"; +import {randomClassName} from "../utils"; import "./Notifications.scss"; const _ = require("lodash"); @@ -30,9 +31,15 @@ class Notifications extends Component { }; componentDidMount() { - dispatcher.register("notifications", n => this.notificationHandler(n)); + dispatcher.register("notifications", this.handleNotifications); } + componentWillUnmount() { + dispatcher.unregister(this.handleNotifications); + } + + handleNotifications = (n) => this.notificationHandler(n); + notificationHandler = (n) => { switch (n.event) { case "connected": @@ -54,6 +61,11 @@ class Notifications extends Component { n.description = `existing rule updated: ${n.message["name"]}`; n.variant = "blue"; return this.pushNotification(n); + case "pcap.completed": + n.title = "new pcap analyzed"; + n.description = `${n.message["processed_packets"]} packets processed`; + n.variant = "blue"; + return this.pushNotification(n); default: return; } @@ -61,6 +73,7 @@ class Notifications extends Component { pushNotification = (notification) => { const notifications = this.state.notifications; + notification.id = randomClassName(); notifications.push(notification); this.setState({notifications}); setTimeout(() => { @@ -103,7 +116,7 @@ class Notifications extends Component { if (n.variant) { notificationClassnames[`notification-${n.variant}`] = true; } - return <div className={classNames(notificationClassnames)} onClick={n.onClick}> + return <div key={n.id} className={classNames(notificationClassnames)} onClick={n.onClick}> <h3 className="notification-title">{n.title}</h3> <pre className="notification-description">{n.description}</pre> </div>; diff --git a/frontend/src/components/Timeline.js b/frontend/src/components/Timeline.js index 8d1fd40..94fa4d0 100644 --- a/frontend/src/components/Timeline.js +++ b/frontend/src/components/Timeline.js @@ -15,8 +15,9 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import React, {Component} from 'react'; -import './Timeline.scss'; +import {TimeRange, TimeSeries} from "pondjs"; +import React, {Component} from "react"; +import {withRouter} from "react-router-dom"; import { ChartContainer, ChartRow, @@ -27,15 +28,14 @@ import { styler, YAxis } from "react-timeseries-charts"; -import {TimeRange, TimeSeries} from "pondjs"; import backend from "../backend"; -import ChoiceField from "./fields/ChoiceField"; -import {withRouter} from "react-router-dom"; -import log from "../log"; import dispatcher from "../dispatcher"; +import log from "../log"; +import ChoiceField from "./fields/ChoiceField"; +import "./Timeline.scss"; const minutes = 60 * 1000; -const classNames = require('classnames'); +const classNames = require("classnames"); const leftSelectionPaddingMultiplier = 24; const rightSelectionPaddingMultiplier = 8; @@ -61,42 +61,17 @@ class Timeline extends Component { }); 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({ - selection: new TimeRange(payload.from, payload.to), - }); - this.adjustSelection(); - }); - - dispatcher.register("notifications", (payload) => { - if (payload.event === "services.edit" && this.state.metric !== "matched_rules") { - this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after services updates")); - } else if (payload.event.startsWith("rules") && this.state.metric === "matched_rules") { - this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after rules updates")); - } - }); - - dispatcher.register("pulse_timeline", (payload) => { - this.setState({pulseTimeline: true}); - setTimeout(() => this.setState({pulseTimeline: false}), payload.duration); - }); + dispatcher.register("connections_filters", this.handleConnectionsFiltersCallback); + dispatcher.register("connection_updates", this.handleConnectionUpdates); + dispatcher.register("notifications", this.handleNotifications); + dispatcher.register("pulse_timeline", this.handlePulseTimeline); } componentWillUnmount() { - dispatcher.unregister(this.connectionsFiltersCallback); + dispatcher.unregister(this.handleConnectionsFiltersCallback); + dispatcher.unregister(this.handleConnectionUpdates); + dispatcher.unregister(this.handleNotifications); + dispatcher.unregister(this.handlePulseTimeline); } loadStatistics = async (metric) => { @@ -217,6 +192,39 @@ class Timeline extends Component { }, 1000); }; + handleConnectionsFiltersCallback = (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")); + } + }; + + handleConnectionUpdates = (payload) => { + this.setState({ + selection: new TimeRange(payload.from, payload.to), + }); + this.adjustSelection(); + }; + + handleNotifications = (payload) => { + if (payload.event === "services.edit" && this.state.metric !== "matched_rules") { + this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after services updates")); + } else if (payload.event.startsWith("rules") && this.state.metric === "matched_rules") { + this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after rules updates")); + } else if (payload.event === "pcap.completed") { + this.loadStatistics(this.state.metric).then(() => log.debug("Statistics reloaded after pcap processed")); + } + }; + + handlePulseTimeline = (payload) => { + this.setState({pulseTimeline: true}); + setTimeout(() => this.setState({pulseTimeline: false}), payload.duration); + }; + adjustSelection = () => { const seriesRange = this.state.series.range(); const selection = this.state.selection; diff --git a/frontend/src/components/fields/ButtonField.js b/frontend/src/components/fields/ButtonField.js index de747a5..15ef179 100644 --- a/frontend/src/components/fields/ButtonField.js +++ b/frontend/src/components/fields/ButtonField.js @@ -58,7 +58,7 @@ class ButtonField extends Component { <div className={classNames("field", "button-field", {"field-small": this.props.small}, {"field-active": this.props.active})}> <button type="button" className={classNames(buttonClassnames)} - onClick={handler} style={buttonStyle}>{this.props.name}</button> + onClick={handler} style={buttonStyle} disabled={this.props.disabled}>{this.props.name}</button> </div> ); } diff --git a/frontend/src/components/fields/ChoiceField.js b/frontend/src/components/fields/ChoiceField.js index 14071c3..7e97d89 100644 --- a/frontend/src/components/fields/ChoiceField.js +++ b/frontend/src/components/fields/ChoiceField.js @@ -15,12 +15,12 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import React, {Component} from 'react'; -import './ChoiceField.scss'; -import './common.scss'; +import React, {Component} from "react"; import {randomClassName} from "../../utils"; +import "./ChoiceField.scss"; +import "./common.scss"; -const classNames = require('classnames'); +const classNames = require("classnames"); class ChoiceField extends Component { @@ -67,7 +67,7 @@ class ChoiceField extends Component { } return ( - <div className={classNames( "field", "choice-field", {"field-inline" : inline}, + <div className={classNames("field", "choice-field", {"field-inline": inline}, {"field-small": this.props.small})}> {!inline && name && <label className="field-name">{name}:</label>} <div className={classNames("field-select", {"select-expanded": this.state.expanded})} diff --git a/frontend/src/components/objects/Connection.js b/frontend/src/components/objects/Connection.js index f838606..e5896e9 100644 --- a/frontend/src/components/objects/Connection.js +++ b/frontend/src/components/objects/Connection.js @@ -21,26 +21,19 @@ import backend from "../../backend"; import dispatcher from "../../dispatcher"; import {dateTimeToTime, durationBetween, formatSize} from "../../utils"; import ButtonField from "../fields/ButtonField"; -import TextField from "../fields/TextField"; import "./Connection.scss"; +import CopyLinkPopover from "./CopyLinkPopover"; import LinkPopover from "./LinkPopover"; const classNames = require("classnames"); class Connection extends Component { - constructor(props) { - super(props); - this.state = { - update: false, - copiedMessage: false - }; + state = { + update: false + }; - this.copyTextarea = React.createRef(); - this.handleAction = this.handleAction.bind(this); - } - - handleAction(name) { + handleAction = (name) => { if (name === "hide") { const enabled = !this.props.data.hidden; backend.post(`/api/connections/${this.props.data.id}/${enabled ? "hide" : "show"}`) @@ -57,13 +50,7 @@ class Connection extends Component { this.setState({update: true}); }); } - if (name === "copy") { - this.copyTextarea.current.select(); - document.execCommand("copy"); - this.setState({copiedMessage: true}); - setTimeout(() => this.setState({copiedMessage: false}), 3000); - } - } + }; render() { let conn = this.props.data; @@ -88,12 +75,6 @@ class Connection extends Component { {conn.comment && <Form.Control as="textarea" readOnly={true} rows={2} defaultValue={conn.comment}/>} </div>; - const copyPopoverContent = <div> - {this.state.copiedMessage ? <span><strong>Copied!</strong></span> : - <span>Click to <strong>copy</strong> the connection id</span>} - <TextField readonly rows={1} value={conn.id} textRef={this.copyTextarea}/> - </div>; - return ( <tr className={classNames("connection", {"connection-selected": this.props.selected}, {"has-matched-rules": conn.matched_rules.length > 0})}> @@ -121,9 +102,8 @@ class Connection extends Component { <LinkPopover text={<span className={classNames("connection-icon", {"icon-enabled": conn.comment})} onClick={() => this.handleAction("comment")}>@</span>} content={commentPopoverContent} placement="right"/> - <LinkPopover text={<span className={classNames("connection-icon", {"icon-enabled": conn.hidden})} - onClick={() => this.handleAction("copy")}>#</span>} - content={copyPopoverContent} placement="right"/> + <CopyLinkPopover text="#" value={conn.id} + textClassName={classNames("connection-icon", {"icon-enabled": conn.hidden})}/> </td> </tr> ); diff --git a/frontend/src/components/objects/ConnectionMatchedRules.js b/frontend/src/components/objects/ConnectionMatchedRules.js index 92bde49..cfd1254 100644 --- a/frontend/src/components/objects/ConnectionMatchedRules.js +++ b/frontend/src/components/objects/ConnectionMatchedRules.js @@ -15,11 +15,11 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import React, {Component} from 'react'; -import './ConnectionMatchedRules.scss'; -import ButtonField from "../fields/ButtonField"; -import dispatcher from "../../dispatcher"; +import React, {Component} from "react"; import {withRouter} from "react-router-dom"; +import dispatcher from "../../dispatcher"; +import ButtonField from "../fields/ButtonField"; +import "./ConnectionMatchedRules.scss"; class ConnectionMatchedRules extends Component { @@ -28,7 +28,7 @@ class ConnectionMatchedRules extends Component { const rules = params.getAll("matched_rules"); if (!rules.includes(id)) { rules.push(id); - dispatcher.dispatch("connections_filters",{"matched_rules": rules}); + dispatcher.dispatch("connections_filters", {"matched_rules": rules}); } }; diff --git a/frontend/src/components/objects/CopyLinkPopover.js b/frontend/src/components/objects/CopyLinkPopover.js new file mode 100644 index 0000000..fa9266f --- /dev/null +++ b/frontend/src/components/objects/CopyLinkPopover.js @@ -0,0 +1,54 @@ +/* + * This file is part of caronte (https://github.com/eciavatta/caronte). + * Copyright (c) 2020 Emiliano Ciavatta. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import React, {Component} from "react"; +import TextField from "../fields/TextField"; +import LinkPopover from "./LinkPopover"; + +class CopyLinkPopover extends Component { + + state = {}; + + constructor(props) { + super(props); + + this.copyTextarea = React.createRef(); + } + + handleClick = () => { + this.copyTextarea.current.select(); + document.execCommand("copy"); + this.setState({copiedMessage: true}); + setTimeout(() => this.setState({copiedMessage: false}), 3000); + }; + + render() { + const copyPopoverContent = <div style={{"width": "400px"}}> + {this.state.copiedMessage ? <span><strong>Copied!</strong></span> : + <span>Click to <strong>copy</strong></span>} + <TextField readonly rows={1} value={this.props.value} textRef={this.copyTextarea}/> + </div>; + + return ( + <LinkPopover text={<span className={this.props.textClassName} + onClick={this.handleClick}>{this.props.text}</span>} + content={copyPopoverContent} placement="right"/> + ); + } +} + +export default CopyLinkPopover; diff --git a/frontend/src/components/objects/MessageAction.js b/frontend/src/components/objects/MessageAction.js index 2b46320..e0c96e8 100644 --- a/frontend/src/components/objects/MessageAction.js +++ b/frontend/src/components/objects/MessageAction.js @@ -15,11 +15,11 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import React, {Component} from 'react'; -import './MessageAction.scss'; +import React, {Component} from "react"; import {Modal} from "react-bootstrap"; -import TextField from "../fields/TextField"; import ButtonField from "../fields/ButtonField"; +import TextField from "../fields/TextField"; +import "./MessageAction.scss"; class MessageAction extends Component { @@ -34,7 +34,7 @@ class MessageAction extends Component { copyActionValue() { this.actionValue.current.select(); - document.execCommand('copy'); + document.execCommand("copy"); this.setState({copyButtonText: "copied!"}); setTimeout(() => this.setState({copyButtonText: "copy"}), 3000); } @@ -54,11 +54,12 @@ class MessageAction extends Component { </Modal.Title> </Modal.Header> <Modal.Body> - <TextField readonly value={this.props.actionValue} textRef={this.actionValue} rows={15} /> + <TextField readonly value={this.props.actionValue} textRef={this.actionValue} rows={15}/> </Modal.Body> <Modal.Footer className="dialog-footer"> - <ButtonField variant="green" bordered onClick={this.copyActionValue} name={this.state.copyButtonText} /> - <ButtonField variant="red" bordered onClick={this.props.onHide} name="close" /> + <ButtonField variant="green" bordered onClick={this.copyActionValue} + name={this.state.copyButtonText}/> + <ButtonField variant="red" bordered onClick={this.props.onHide} name="close"/> </Modal.Footer> </Modal> ); diff --git a/frontend/src/components/panels/ConnectionsPane.js b/frontend/src/components/panels/ConnectionsPane.js index ea47059..23c6114 100644 --- a/frontend/src/components/panels/ConnectionsPane.js +++ b/frontend/src/components/panels/ConnectionsPane.js @@ -15,20 +15,20 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import React, {Component} from 'react'; -import './ConnectionsPane.scss'; -import Connection from "../objects/Connection"; -import Table from 'react-bootstrap/Table'; +import React, {Component} from "react"; +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"; +import log from "../../log"; import {updateParams} from "../../utils"; +import ButtonField from "../fields/ButtonField"; +import Connection from "../objects/Connection"; +import ConnectionMatchedRules from "../objects/ConnectionMatchedRules"; +import "./ConnectionsPane.scss"; -const classNames = require('classnames'); +const classNames = require("classnames"); class ConnectionsPane extends Component { @@ -67,55 +67,56 @@ class ConnectionsPane extends Component { this.loadConnections(additionalParams, urlParams, true).then(() => log.debug("Connections loaded")); - this.connectionsFiltersCallback = payload => { - const newParams = updateParams(this.state.urlParams, payload); - if (this.state.urlParams.toString() === newParams.toString()) { - return; - } - - log.debug("Update following url params:", payload); - this.queryStringRedirect = true; - this.setState({urlParams: newParams}); - - this.loadConnections({limit: this.queryLimit}, newParams) - .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); - - this.notificationsCallback = payload => { - if (payload.event === "rules.new" || payload.event === "rules.edit") { - this.loadRules().then(() => log.debug("Loaded connection rules after notification update")); - } - if (payload.event === "services.edit") { - this.loadServices().then(() => log.debug("Services reloaded after notification update")); - } - }; - dispatcher.register("notifications", this.notificationsCallback); - - this.pulseConnectionsViewCallback = payload => { - this.setState({pulseConnectionsView: true}); - setTimeout(() => this.setState({pulseConnectionsView: false}), payload.duration); - }; - dispatcher.register("pulse_connections_view", this.pulseConnectionsViewCallback); + dispatcher.register("connections_filters", this.handleConnectionsFilters); + dispatcher.register("timeline_updates", this.handleTimelineUpdates); + dispatcher.register("notifications", this.handleNotifications); + dispatcher.register("pulse_connections_view", this.handlePulseConnectionsView); } componentWillUnmount() { - dispatcher.unregister(this.timelineUpdatesCallback); - dispatcher.unregister(this.notificationsCallback); - dispatcher.unregister(this.pulseConnectionsViewCallback); - dispatcher.unregister(this.connectionsFiltersCallback); + dispatcher.unregister(this.handleConnectionsFilters); + dispatcher.unregister(this.handleTimelineUpdates); + dispatcher.unregister(this.handleNotifications); + dispatcher.unregister(this.handlePulseConnectionsView); } + handleConnectionsFilters = (payload) => { + const newParams = updateParams(this.state.urlParams, payload); + if (this.state.urlParams.toString() === newParams.toString()) { + return; + } + + log.debug("Update following url params:", payload); + this.queryStringRedirect = true; + this.setState({urlParams: newParams}); + + this.loadConnections({limit: this.queryLimit}, newParams) + .then(() => log.info("ConnectionsPane reloaded after query string update")); + }; + + handleTimelineUpdates = (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}`)); + }; + + handleNotifications = (payload) => { + if (payload.event === "rules.new" || payload.event === "rules.edit") { + this.loadRules().then(() => log.debug("Loaded connection rules after notification update")); + } + if (payload.event === "services.edit") { + this.loadServices().then(() => log.debug("Services reloaded after notification update")); + } + }; + + handlePulseConnectionsView = (payload) => { + this.setState({pulseConnectionsView: true}); + setTimeout(() => this.setState({pulseConnectionsView: false}), payload.duration); + }; + connectionSelected = (c) => { this.connectionSelectedRedirect = true; this.setState({selected: c.id}); diff --git a/frontend/src/components/panels/PcapsPane.js b/frontend/src/components/panels/PcapsPane.js index fd3db75..64e7804 100644 --- a/frontend/src/components/panels/PcapsPane.js +++ b/frontend/src/components/panels/PcapsPane.js @@ -24,6 +24,7 @@ import ButtonField from "../fields/ButtonField"; import CheckField from "../fields/CheckField"; import InputField from "../fields/InputField"; import TextField from "../fields/TextField"; +import CopyLinkPopover from "../objects/CopyLinkPopover"; import LinkPopover from "../objects/LinkPopover"; import "./common.scss"; import "./PcapsPane.scss"; @@ -44,16 +45,20 @@ class PcapsPane extends Component { componentDidMount() { this.loadSessions(); - - dispatcher.register("notifications", (payload) => { - if (payload.event === "pcap.upload" || payload.event === "pcap.file") { - this.loadSessions(); - } - }); - + dispatcher.register("notifications", this.handleNotifications); document.title = "caronte:~/pcaps$"; } + componentWillUnmount() { + dispatcher.unregister(this.handleNotifications); + } + + handleNotifications = (payload) => { + if (payload.event.startsWith("pcap")) { + this.loadSessions(); + } + }; + loadSessions = () => { backend.get("/api/pcap/sessions") .then((res) => this.setState({sessions: res.json, sessionsStatusCode: res.status})) @@ -130,10 +135,19 @@ class PcapsPane extends Component { }; render() { - let sessions = this.state.sessions.map((s) => - <tr key={s.id} className="row-small row-clickable"> - <td>{s["id"].substring(0, 8)}</td> - <td>{dateTimeToTime(s["started_at"])}</td> + let sessions = this.state.sessions.map((s) => { + const startedAt = new Date(s["started_at"]); + const completedAt = new Date(s["completed_at"]); + let timeInfo = <div> + <span>Started at {startedAt.toLocaleDateString() + " " + startedAt.toLocaleTimeString()}</span><br/> + <span>Completed at {completedAt.toLocaleDateString() + " " + completedAt.toLocaleTimeString()}</span> + </div>; + + return <tr key={s.id} className="row-small row-clickable"> + <td><CopyLinkPopover text={s["id"].substring(0, 8)} value={s["id"]}/></td> + <td> + <LinkPopover text={dateTimeToTime(s["started_at"])} content={timeInfo} placement="right"/> + </td> <td>{durationBetween(s["started_at"], s["completed_at"])}</td> <td>{formatSize(s["size"])}</td> <td>{s["processed_packets"]}</td> @@ -143,8 +157,8 @@ class PcapsPane extends Component { placement="left"/></td> <td className="table-cell-action"><a href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a> </td> - </tr> - ); + </tr>; + }); const handleUploadFileChange = (file) => { this.setState({ diff --git a/frontend/src/components/panels/RulesPane.js b/frontend/src/components/panels/RulesPane.js index a66cde7..d872b47 100644 --- a/frontend/src/components/panels/RulesPane.js +++ b/frontend/src/components/panels/RulesPane.js @@ -15,26 +15,26 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import React, {Component} from 'react'; -import './common.scss'; -import './RulesPane.scss'; -import Table from "react-bootstrap/Table"; +import React, {Component} from "react"; import {Col, Container, Row} from "react-bootstrap"; -import InputField from "../fields/InputField"; -import CheckField from "../fields/CheckField"; -import TextField from "../fields/TextField"; +import Table from "react-bootstrap/Table"; import backend from "../../backend"; -import NumericField from "../fields/extensions/NumericField"; -import ColorField from "../fields/extensions/ColorField"; -import ChoiceField from "../fields/ChoiceField"; -import ButtonField from "../fields/ButtonField"; +import dispatcher from "../../dispatcher"; import validation from "../../validation"; +import ButtonField from "../fields/ButtonField"; +import CheckField from "../fields/CheckField"; +import ChoiceField from "../fields/ChoiceField"; +import ColorField from "../fields/extensions/ColorField"; +import NumericField from "../fields/extensions/NumericField"; +import InputField from "../fields/InputField"; +import TextField from "../fields/TextField"; +import CopyLinkPopover from "../objects/CopyLinkPopover"; import LinkPopover from "../objects/LinkPopover"; -import {randomClassName} from "../../utils"; -import dispatcher from "../../dispatcher"; +import "./common.scss"; +import "./RulesPane.scss"; -const classNames = require('classnames'); -const _ = require('lodash'); +const classNames = require("classnames"); +const _ = require("lodash"); class RulesPane extends Component { @@ -88,15 +88,20 @@ class RulesPane extends Component { this.reset(); this.loadRules(); - dispatcher.register("notifications", payload => { - if (payload.event === "rules.new" || payload.event === "rules.edit") { - this.loadRules(); - } - }); - + dispatcher.register("notifications", this.handleNotifications); document.title = "caronte:~/rules$"; } + componentWillUnmount() { + dispatcher.unregister(this.handleNotifications); + } + + handleNotifications = (payload) => { + if (payload.event === "rules.new" || payload.event === "rules.edit") { + this.loadRules(); + } + }; + loadRules = () => { backend.get("/api/rules").then(res => this.setState({rules: res.json, rulesStatusCode: res.status})) .catch(res => this.setState({rulesStatusCode: res.status, rulesResponse: JSON.stringify(res.json)})); @@ -249,7 +254,7 @@ class RulesPane extends Component { this.reset(); this.setState({selectedRule: _.cloneDeep(r)}); }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id})}> - <td>{r["id"].substring(0, 8)}</td> + <CopyLinkPopover text={r["id"].substring(0, 8)} value={r["id"]}/> <td>{r["name"]}</td> <td><ButtonField name={r["color"]} color={r["color"]} small/></td> <td>{r["notes"]}</td> @@ -260,7 +265,7 @@ class RulesPane extends Component { rule.patterns.concat(this.state.newPattern) : rule.patterns ).map(p => p === pattern ? - <tr key={randomClassName()}> + <tr key={"new_pattern"}> <td style={{"width": "500px"}}> <InputField small active={this.state.patternRegexFocused} value={pattern.regex} onChange={(v) => { diff --git a/frontend/src/components/panels/SearchPane.js b/frontend/src/components/panels/SearchPane.js index d3c0c8b..d36e85e 100644 --- a/frontend/src/components/panels/SearchPane.js +++ b/frontend/src/components/panels/SearchPane.js @@ -60,15 +60,14 @@ class SearchPane extends Component { this.reset(); this.loadSearches(); - dispatcher.register("notifications", payload => { - if (payload.event === "searches.new") { - this.loadSearches(); - } - }); - + dispatcher.register("notifications", this.handleNotification); document.title = "caronte:~/searches$"; } + componentWillUnmount() { + dispatcher.unregister(this.handleNotification); + } + loadSearches = () => { backend.get("/api/searches") .then(res => this.setState({searches: res.json, searchesStatusCode: res.status})) @@ -77,14 +76,18 @@ class SearchPane extends Component { performSearch = () => { const options = this.state.currentSearchOptions; + this.setState({loading: true}); if (this.validateSearch(options)) { backend.post("/api/searches/perform", options).then(res => { this.reset(); - this.setState({searchStatusCode: res.status}); + this.setState({searchStatusCode: res.status, loading: false}); this.loadSearches(); this.viewSearch(res.json.id); }).catch(res => { - this.setState({searchStatusCode: res.status, searchResponse: JSON.stringify(res.json)}); + this.setState({ + searchStatusCode: res.status, searchResponse: JSON.stringify(res.json), + loading: false + }); }); } }; @@ -156,6 +159,12 @@ class SearchPane extends Component { dispatcher.dispatch("connections_filters", {"performed_search": searchId}); }; + handleNotification = (payload) => { + if (payload.event === "searches.new") { + this.loadSearches(); + } + }; + render() { const options = this.state.currentSearchOptions; @@ -263,7 +272,8 @@ class SearchPane extends Component { onChange={v => this.updateParam(s => s["regex_search"]["not_pattern"] = v)}/> <div className="checkbox-line"> - <CheckField checked={options["regex_search"]["case_insensitive"]} name="case_insensitive" + <CheckField checked={options["regex_search"]["case_insensitive"]} + name="case_insensitive" readonly={textOptionsModified} small onChange={(v) => this.updateParam(s => s["regex_search"]["case_insensitive"] = v)}/> <CheckField checked={options["regex_search"]["multi_line"]} name="multi_line" @@ -284,8 +294,10 @@ class SearchPane extends Component { </div> <div className="section-footer"> - <ButtonField variant="red" name="cancel" bordered onClick={this.reset}/> - <ButtonField variant="green" name="perform_search" bordered onClick={this.performSearch}/> + <ButtonField variant="red" name="cancel" bordered disabled={this.state.loading} + onClick={this.reset}/> + <ButtonField variant="green" name="perform_search" bordered + disabled={this.state.loading} onClick={this.performSearch}/> </div> </div> </div> diff --git a/frontend/src/components/panels/ServicesPane.js b/frontend/src/components/panels/ServicesPane.js index bc82356..48d9e29 100644 --- a/frontend/src/components/panels/ServicesPane.js +++ b/frontend/src/components/panels/ServicesPane.js @@ -15,24 +15,24 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -import React, {Component} from 'react'; -import './common.scss'; -import './ServicesPane.scss'; -import Table from "react-bootstrap/Table"; +import React, {Component} from "react"; import {Col, Container, Row} from "react-bootstrap"; -import InputField from "../fields/InputField"; -import TextField from "../fields/TextField"; +import Table from "react-bootstrap/Table"; import backend from "../../backend"; -import NumericField from "../fields/extensions/NumericField"; -import ColorField from "../fields/extensions/ColorField"; -import ButtonField from "../fields/ButtonField"; +import dispatcher from "../../dispatcher"; +import {createCurlCommand} from "../../utils"; import validation from "../../validation"; +import ButtonField from "../fields/ButtonField"; +import ColorField from "../fields/extensions/ColorField"; +import NumericField from "../fields/extensions/NumericField"; +import InputField from "../fields/InputField"; +import TextField from "../fields/TextField"; import LinkPopover from "../objects/LinkPopover"; -import {createCurlCommand} from "../../utils"; -import dispatcher from "../../dispatcher"; +import "./common.scss"; +import "./ServicesPane.scss"; -const classNames = require('classnames'); -const _ = require('lodash'); +const classNames = require("classnames"); +const _ = require("lodash"); class ServicesPane extends Component { @@ -52,15 +52,20 @@ class ServicesPane extends Component { this.reset(); this.loadServices(); - dispatcher.register("notifications", payload => { - if (payload.event === "services.edit") { - this.loadServices(); - } - }); - + dispatcher.register("notifications", this.handleNotifications); document.title = "caronte:~/services$"; } + componentWillUnmount() { + dispatcher.unregister(this.handleNotifications); + } + + handleNotifications = (payload) => { + if (payload.event === "services.edit") { + this.loadServices(); + } + }; + loadServices = () => { backend.get("/api/services") .then(res => this.setState({services: Object.values(res.json), servicesStatusCode: res.status})) @@ -125,10 +130,10 @@ class ServicesPane extends Component { <tr key={s.port} onClick={() => { this.reset(); this.setState({isUpdate: true, currentService: _.cloneDeep(s)}); - }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port })}> + }} className={classNames("row-small", "row-clickable", {"row-selected": service.port === s.port})}> <td>{s["port"]}</td> <td>{s["name"]}</td> - <td><ButtonField name={s["color"]} color={s["color"]} small /></td> + <td><ButtonField name={s["color"]} color={s["color"]} small/></td> <td>{s["notes"]}</td> </tr> ); @@ -141,9 +146,9 @@ class ServicesPane extends Component { <div className="section-header"> <span className="api-request">GET /api/services</span> {this.state.servicesStatusCode && - <span className="api-response"><LinkPopover text={this.state.servicesStatusCode} - content={this.state.servicesResponse} - placement="left" /></span>} + <span className="api-response"><LinkPopover text={this.state.servicesStatusCode} + content={this.state.servicesResponse} + placement="left"/></span>} </div> <div className="section-content"> @@ -170,7 +175,7 @@ class ServicesPane extends Component { <span className="api-request">PUT /api/services</span> <span className="api-response"><LinkPopover text={this.state.serviceStatusCode} content={this.state.serviceResponse} - placement="left" /></span> + placement="left"/></span> </div> <div className="section-content"> @@ -179,17 +184,17 @@ class ServicesPane extends Component { <Col> <NumericField name="port" value={service.port} onChange={(v) => this.updateParam((s) => s.port = v)} - min={0} max={65565} error={this.state.servicePortError} /> + min={0} max={65565} error={this.state.servicePortError}/> <InputField name="name" value={service.name} onChange={(v) => this.updateParam((s) => s.name = v)} - error={this.state.serviceNameError} /> + error={this.state.serviceNameError}/> <ColorField value={service.color} error={this.state.serviceColorError} - onChange={(v) => this.updateParam((s) => s.color = v)} /> + onChange={(v) => this.updateParam((s) => s.color = v)}/> </Col> <Col> <TextField name="notes" rows={7} value={service.notes} - onChange={(v) => this.updateParam((s) => s.notes = v)} /> + onChange={(v) => this.updateParam((s) => s.notes = v)}/> </Col> </Row> </Container> @@ -199,8 +204,9 @@ class ServicesPane extends Component { <div className="section-footer"> {<ButtonField variant="red" name="cancel" bordered onClick={this.reset}/>} - <ButtonField variant={isUpdate ? "blue" : "green"} name={isUpdate ? "update_service" : "add_service"} - bordered onClick={this.updateService} /> + <ButtonField variant={isUpdate ? "blue" : "green"} + name={isUpdate ? "update_service" : "add_service"} + bordered onClick={this.updateService}/> </div> </div> </div> diff --git a/frontend/src/serviceWorker.js b/frontend/src/serviceWorker.js index c633a91..a1f0ba8 100644 --- a/frontend/src/serviceWorker.js +++ b/frontend/src/serviceWorker.js @@ -11,131 +11,131 @@ // opt-in, read https://bit.ly/CRA-PWA const isLocalhost = Boolean( - window.location.hostname === 'localhost' || + window.location.hostname === "localhost" || // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || + window.location.hostname === "[::1]" || // 127.0.0.0/8 are considered localhost for IPv4. window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ + /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ ) ); export function register(config) { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebook/create-react-app/issues/2374 - return; - } + if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + window.addEventListener("load", () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; - if (isLocalhost) { - // This is running on localhost. Let's check if a service worker still exists or not. - checkValidServiceWorker(swUrl, config); + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://bit.ly/CRA-PWA' - ); + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + "This web app is being served cache-first by a service " + + "worker. To learn more, visit https://bit.ly/CRA-PWA" + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } }); - } else { - // Is not localhost. Just register service worker - registerValidSW(swUrl, config); - } - }); - } + } } function registerValidSW(swUrl, config) { - navigator.serviceWorker - .register(swUrl) - .then((registration) => { - registration.onupdatefound = () => { - const installingWorker = registration.installing; - if (installingWorker == null) { - return; - } - installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - // At this point, the updated precached content has been fetched, - // but the previous service worker will still serve the older - // content until all client tabs are closed. - console.log( - 'New content is available and will be used when all ' + - 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' - ); + navigator.serviceWorker + .register(swUrl) + .then((registration) => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === "installed") { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + "New content is available and will be used when all " + + "tabs for this page are closed. See https://bit.ly/CRA-PWA." + ); - // Execute callback - if (config && config.onUpdate) { - config.onUpdate(registration); - } - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log('Content is cached for offline use.'); + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log("Content is cached for offline use."); - // Execute callback - if (config && config.onSuccess) { - config.onSuccess(registration); - } - } - } - }; - }; - }) - .catch((error) => { - console.error('Error during service worker registration:', error); - }); + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch((error) => { + console.error("Error during service worker registration:", error); + }); } function checkValidServiceWorker(swUrl, config) { - // Check if the service worker can be found. If it can't reload the page. - fetch(swUrl, { - headers: { 'Service-Worker': 'script' }, - }) - .then((response) => { - // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get('content-type'); - if ( - response.status === 404 || - (contentType != null && contentType.indexOf('javascript') === -1) - ) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then((registration) => { - registration.unregister().then(() => { - window.location.reload(); - }); - }); - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl, config); - } + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl, { + headers: {"Service-Worker": "script"}, }) - .catch(() => { - console.log( - 'No internet connection found. App is running in offline mode.' - ); - }); + .then((response) => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get("content-type"); + if ( + response.status === 404 || + (contentType != null && contentType.indexOf("javascript") === -1) + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then((registration) => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log( + "No internet connection found. App is running in offline mode." + ); + }); } export function unregister() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready - .then((registration) => { - registration.unregister(); - }) - .catch((error) => { - console.error(error.message); - }); - } + if ("serviceWorker" in navigator) { + navigator.serviceWorker.ready + .then((registration) => { + registration.unregister(); + }) + .catch((error) => { + console.error(error.message); + }); + } } diff --git a/parsers/http_request_parser.go b/parsers/http_request_parser.go index bc98f8f..56093c9 100644 --- a/parsers/http_request_parser.go +++ b/parsers/http_request_parser.go @@ -82,7 +82,7 @@ func (p HttpRequestParser) TryParse(content []byte) Metadata { Trailer: JoinArrayMap(request.Trailer), Reproducers: HttpRequestMetadataReproducers{ CurlCommand: curlCommand(content), - RequestsCode: requestsCode(request), + RequestsCode: requestsCode(request, body), FetchRequest: fetchRequest(request, body), }, } @@ -99,19 +99,15 @@ func curlCommand(content []byte) string { } } -func requestsCode(request *http.Request) string { +func requestsCode(request *http.Request, body string) string { var b strings.Builder - var params string - if request.Form != nil { - params = toJson(JoinArrayMap(request.PostForm)) - } headers := toJson(JoinArrayMap(request.Header)) cookies := toJson(CookiesMap(request.Cookies())) b.WriteString("import requests\n\nresponse = requests." + strings.ToLower(request.Method) + "(") b.WriteString("\"" + request.URL.String() + "\"") - if params != "" { - b.WriteString(", data = " + params) + if body != "" { + b.WriteString(", data = \"" + strings.Replace(body, "\"", "\\\"", -1) + "\"") } if headers != "" { b.WriteString(", headers = " + headers) diff --git a/pcap_importer.go b/pcap_importer.go index 41ed082..1c80b3f 100644 --- a/pcap_importer.go +++ b/pcap_importer.go @@ -29,6 +29,7 @@ import ( "os" "path" "path/filepath" + "sort" "sync" "time" ) @@ -39,13 +40,14 @@ const initialAssemblerPoolSize = 16 const importUpdateProgressInterval = 100 * time.Millisecond type PcapImporter struct { - storage Storage - streamPool *tcpassembly.StreamPool - assemblers []*tcpassembly.Assembler - sessions map[string]ImportingSession - mAssemblers sync.Mutex - mSessions sync.Mutex - serverNet net.IPNet + storage Storage + streamPool *tcpassembly.StreamPool + assemblers []*tcpassembly.Assembler + sessions map[string]ImportingSession + mAssemblers sync.Mutex + mSessions sync.Mutex + serverNet net.IPNet + notificationController *NotificationController } type ImportingSession struct { @@ -63,7 +65,8 @@ type ImportingSession struct { type flowCount [2]int -func NewPcapImporter(storage Storage, serverNet net.IPNet, rulesManager RulesManager) *PcapImporter { +func NewPcapImporter(storage Storage, serverNet net.IPNet, rulesManager RulesManager, + notificationController *NotificationController) *PcapImporter { streamPool := tcpassembly.NewStreamPool(NewBiDirectionalStreamFactory(storage, serverNet, rulesManager)) var result []ImportingSession @@ -76,13 +79,14 @@ func NewPcapImporter(storage Storage, serverNet net.IPNet, rulesManager RulesMan } return &PcapImporter{ - storage: storage, - streamPool: streamPool, - assemblers: make([]*tcpassembly.Assembler, 0, initialAssemblerPoolSize), - sessions: sessions, - mAssemblers: sync.Mutex{}, - mSessions: sync.Mutex{}, - serverNet: serverNet, + storage: storage, + streamPool: streamPool, + assemblers: make([]*tcpassembly.Assembler, 0, initialAssemblerPoolSize), + sessions: sessions, + mAssemblers: sync.Mutex{}, + mSessions: sync.Mutex{}, + serverNet: serverNet, + notificationController: notificationController, } } @@ -136,6 +140,9 @@ func (pi *PcapImporter) GetSessions() []ImportingSession { for _, session := range pi.sessions { sessions = append(sessions, session) } + sort.Slice(sessions, func(i, j int) bool { + return sessions[i].StartedAt.Before(sessions[j].StartedAt) + }) pi.mSessions.Unlock() return sessions } @@ -202,6 +209,8 @@ func (pi *PcapImporter) parsePcap(session ImportingSession, fileName string, flu handle.Close() pi.releaseAssembler(assembler) pi.progressUpdate(session, fileName, true, "") + pi.notificationController.Notify("pcap.completed", session) + return } |