aboutsummaryrefslogtreecommitdiff
path: root/frontend/src/components/panels/ConnectionsPane.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/panels/ConnectionsPane.jsx')
-rw-r--r--frontend/src/components/panels/ConnectionsPane.jsx310
1 files changed, 310 insertions, 0 deletions
diff --git a/frontend/src/components/panels/ConnectionsPane.jsx b/frontend/src/components/panels/ConnectionsPane.jsx
new file mode 100644
index 0000000..2c7fadd
--- /dev/null
+++ b/frontend/src/components/panels/ConnectionsPane.jsx
@@ -0,0 +1,310 @@
+/*
+ * 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 Table from "react-bootstrap/Table";
+import {Redirect} from "react-router";
+import {withRouter} from "react-router-dom";
+import backend from "../../backend";
+import dispatcher from "../../dispatcher";
+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";
+
+import classNames from 'classnames';
+
+class ConnectionsPane extends Component {
+
+ state = {
+ loading: false,
+ connections: [],
+ firstConnection: null,
+ lastConnection: null,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.scrollTopThreashold = 0.00001;
+ this.scrollBottomThreashold = 0.99999;
+ this.maxConnections = 200;
+ this.queryLimit = 50;
+ this.connectionsListRef = React.createRef();
+ this.lastScrollPosition = 0;
+ }
+
+ componentDidMount() {
+ 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];
+ additionalParams.from = id;
+ backend.get(`/api/connections/${id}`)
+ .then((res) => this.connectionSelected(res.json))
+ .catch((error) => log.error("Error loading initial connection", error));
+ }
+
+ this.loadConnections(additionalParams, urlParams, true).then(() => log.debug("Connections loaded"));
+
+ 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.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});
+ this.props.onSelected(c);
+ log.debug(`Connection ${c.id} selected`);
+ };
+
+ handleScroll = (e) => {
+ if (this.disableScrollHandler) {
+ this.lastScrollPosition = e.currentTarget.scrollTop;
+ return;
+ }
+
+ let relativeScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight);
+ if (!this.state.loading && relativeScroll > this.scrollBottomThreashold) {
+ this.loadConnections({from: this.state.lastConnection.id, limit: this.queryLimit,})
+ .then(() => log.info("Following connections loaded"));
+ }
+ if (!this.state.loading && relativeScroll < this.scrollTopThreashold) {
+ this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,})
+ .then(() => log.info("Previous connections loaded"));
+ if (this.state.showMoreRecentButton) {
+ this.setState({showMoreRecentButton: false});
+ }
+ } else {
+ if (this.lastScrollPosition > e.currentTarget.scrollTop) {
+ if (!this.state.showMoreRecentButton) {
+ this.setState({showMoreRecentButton: true});
+ }
+ } else {
+ if (this.state.showMoreRecentButton) {
+ this.setState({showMoreRecentButton: false});
+ }
+ }
+ }
+ this.lastScrollPosition = e.currentTarget.scrollTop;
+ };
+
+ async loadConnections(additionalParams, initialParams = null, isInitial = false) {
+ if (!initialParams) {
+ initialParams = this.state.urlParams;
+ }
+ const urlParams = new URLSearchParams(initialParams.toString());
+ for (const [name, value] of Object.entries(additionalParams)) {
+ urlParams.set(name, value);
+ }
+
+ this.setState({loading: true});
+ if (!this.state.rules) {
+ await this.loadRules();
+ }
+ if (!this.state.services) {
+ await this.loadServices();
+ }
+
+ let res = (await backend.get(`/api/connections?${urlParams}`)).json;
+
+ let connections = this.state.connections;
+ let firstConnection = this.state.firstConnection;
+ let lastConnection = this.state.lastConnection;
+
+ if (additionalParams && additionalParams.from && !additionalParams.to) {
+ if (res.length > 0) {
+ if (!isInitial) {
+ res = res.slice(1);
+ }
+ connections = this.state.connections.concat(res);
+ lastConnection = connections[connections.length - 1];
+ if (isInitial) {
+ firstConnection = connections[0];
+ }
+ if (connections.length > this.maxConnections) {
+ connections = connections.slice(connections.length - this.maxConnections,
+ connections.length - 1);
+ firstConnection = connections[0];
+ }
+ }
+ } else if (additionalParams && additionalParams.to && !additionalParams.from) {
+ if (res.length > 0) {
+ connections = res.slice(0, res.length - 1).concat(this.state.connections);
+ firstConnection = connections[0];
+ if (connections.length > this.maxConnections) {
+ connections = connections.slice(0, this.maxConnections);
+ lastConnection = connections[this.maxConnections - 1];
+ }
+ }
+ } else {
+ if (res.length > 0) {
+ connections = res;
+ firstConnection = connections[0];
+ lastConnection = connections[connections.length - 1];
+ } else {
+ connections = [];
+ firstConnection = null;
+ lastConnection = null;
+ }
+ }
+
+ this.setState({loading: false, connections, firstConnection, lastConnection});
+
+ if (firstConnection != null && lastConnection != null) {
+ dispatcher.dispatch("connection_updates", {
+ from: new Date(lastConnection["started_at"]),
+ to: new Date(firstConnection["started_at"])
+ });
+ }
+ }
+
+ loadRules = async () => {
+ return backend.get("/api/rules").then((res) => this.setState({rules: res.json}));
+ };
+
+ loadServices = async () => {
+ return backend.get("/api/services").then((res) => this.setState({services: res.json}));
+ };
+
+ render() {
+ let redirect;
+ if (this.connectionSelectedRedirect) {
+ redirect = <Redirect push to={`/connections/${this.state.selected}?${this.state.urlParams}`}/>;
+ this.connectionSelectedRedirect = false;
+ } else if (this.queryStringRedirect) {
+ redirect = <Redirect push to={`${this.props.location.pathname}?${this.state.urlParams}`}/>;
+ this.queryStringRedirect = false;
+ }
+
+ let loading = null;
+ if (this.state.loading) {
+ loading = <tr>
+ <td colSpan={10}>Loading...</td>
+ </tr>;
+ }
+
+ return (
+ <div className="connections-container">
+ {this.state.showMoreRecentButton && <div className="most-recent-button">
+ <ButtonField name="most_recent" variant="green" bordered onClick={() => {
+ this.disableScrollHandler = true;
+ this.connectionsListRef.current.scrollTop = 0;
+ this.loadConnections({limit: this.queryLimit})
+ .then(() => {
+ this.disableScrollHandler = false;
+ log.info("Most recent connections loaded");
+ });
+ }}/>
+ </div>}
+
+ <div className={classNames("connections", {"connections-pulse": this.state.pulseConnectionsView})}
+ onScroll={this.handleScroll} ref={this.connectionsListRef}>
+ <Table borderless size="sm">
+ <thead>
+ <tr>
+ <th>service</th>
+ <th>srcip</th>
+ <th>srcport</th>
+ <th>dstip</th>
+ <th>dstport</th>
+ <th>started_at</th>
+ <th>duration</th>
+ <th>up</th>
+ <th>down</th>
+ <th>actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ {
+ this.state.connections.flatMap((c) => {
+ return [<Connection key={c.id} data={c} onSelected={() => this.connectionSelected(c)}
+ selected={this.state.selected === c.id}
+ onMarked={(marked) => c.marked = marked}
+ onCommented={(comment) => c.comment = comment}
+ services={this.state.services}/>,
+ c.matched_rules.length > 0 &&
+ <ConnectionMatchedRules key={c.id + "_m"} matchedRules={c.matched_rules}
+ rules={this.state.rules}/>
+ ];
+ })
+ }
+ {loading}
+ </tbody>
+ </Table>
+
+ {redirect}
+ </div>
+ </div>
+ );
+ }
+
+}
+
+export default withRouter(ConnectionsPane);