diff options
author | Emiliano Ciavatta | 2020-07-18 11:09:18 +0000 |
---|---|---|
committer | Emiliano Ciavatta | 2020-07-18 11:09:18 +0000 |
commit | 1317044508a9b7adf0d4d9faf4b9f75f5834a0c9 (patch) | |
tree | 40fbfa9b5e8f93e084f53a2a02498b72c414e955 | |
parent | db8ff43c5e1595c02e2ba67c3c78f239723f95bd (diff) |
Add service_port filter
-rw-r--r-- | application_router.go | 3 | ||||
-rw-r--r-- | frontend/src/components/Connection.js | 31 | ||||
-rw-r--r-- | frontend/src/components/Connection.scss | 4 | ||||
-rw-r--r-- | frontend/src/components/ConnectionContent.js | 31 | ||||
-rw-r--r-- | frontend/src/components/ServicePortFilter.js | 91 | ||||
-rw-r--r-- | frontend/src/components/ServicePortFilter.scss | 61 | ||||
-rw-r--r-- | frontend/src/views/App.js | 1 | ||||
-rw-r--r-- | frontend/src/views/Connections.js | 86 | ||||
-rw-r--r-- | frontend/src/views/Header.js | 15 | ||||
-rw-r--r-- | frontend/src/views/Header.scss | 10 |
10 files changed, 271 insertions, 62 deletions
diff --git a/application_router.go b/application_router.go index bd876da..9f497e8 100644 --- a/application_router.go +++ b/application_router.go @@ -19,6 +19,9 @@ func CreateApplicationRouter(applicationContext *ApplicationContext) *gin.Engine router.MaxMultipartMemory = 8 << 30 router.Use(static.Serve("/", static.LocalFile("./frontend/build", true))) + router.GET("/connections/:id", func(c *gin.Context) { + c.File("./frontend/build/index.html") + }) router.POST("/setup", func(c *gin.Context) { if applicationContext.IsConfigured { diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js index 8121d51..e41f542 100644 --- a/frontend/src/components/Connection.js +++ b/frontend/src/components/Connection.js @@ -67,6 +67,9 @@ class Connection extends Component { if (this.props.selected) { classes += " connection-selected"; } + if (this.props.containsFlag) { + classes += " contains-flag"; + } const popoverFor = function (name, content) { return <Popover id={`popover-${name}-${conn.id}`} className="connection-popover"> @@ -93,27 +96,27 @@ class Connection extends Component { <span className="connection-service"> <Button size="sm" style={{ "backgroundColor": serviceColor - }}>{serviceName}</Button> + }} onClick={() => this.props.addServicePortFilter(conn.port_dst)}>{serviceName}</Button> </span> </td> - <td className="clickable" onClick={() => this.props.onSelected()}>{conn.ip_src}</td> - <td className="clickable" onClick={() => this.props.onSelected()}>{conn.port_src}</td> - <td className="clickable" onClick={() => this.props.onSelected()}>{conn.ip_dst}</td> - <td className="clickable" onClick={() => this.props.onSelected()}>{conn.port_dst}</td> - <td className="clickable" onClick={() => this.props.onSelected()}> + <td className="clickable" onClick={this.props.onSelected}>{conn.ip_src}</td> + <td className="clickable" onClick={this.props.onSelected}>{conn.port_src}</td> + <td className="clickable" onClick={this.props.onSelected}>{conn.ip_dst}</td> + <td className="clickable" onClick={this.props.onSelected}>{conn.port_dst}</td> + <td className="clickable" onClick={this.props.onSelected}> <OverlayTrigger trigger={["focus", "hover"]} placement="right" overlay={popoverFor("duration", timeInfo)}> <span className="test-tooltip">{duration}s</span> </OverlayTrigger> </td> - <td className="clickable" onClick={() => this.props.onSelected()}>{conn.client_bytes}</td> - <td className="clickable" onClick={() => this.props.onSelected()}>{conn.server_bytes}</td> - <td> - <OverlayTrigger trigger={["focus", "hover"]} placement="right" - overlay={popoverFor("hide", <span>Hide this connection from the list</span>)}> - <span className={"connection-icon" + (conn.hidden ? " icon-enabled" : "")} - onClick={() => this.handleAction("hide")}>%</span> - </OverlayTrigger> + <td className="clickable" onClick={this.props.onSelected}>{conn.client_bytes}</td> + <td className="clickable" onClick={this.props.onSelected}>{conn.server_bytes}</td> + <td className="contains-flag"> + {/*<OverlayTrigger trigger={["focus", "hover"]} placement="right"*/} + {/* overlay={popoverFor("hide", <span>Hide this connection from the list</span>)}>*/} + {/* <span className={"connection-icon" + (conn.hidden ? " icon-enabled" : "")}*/} + {/* onClick={() => this.handleAction("hide")}>%</span>*/} + {/*</OverlayTrigger>*/} <OverlayTrigger trigger={["focus", "hover"]} placement="right" overlay={popoverFor("hide", <span>Mark this connection</span>)}> <span className={"connection-icon" + (conn.marked ? " icon-enabled" : "")} diff --git a/frontend/src/components/Connection.scss b/frontend/src/components/Connection.scss index 5ad195d..7c8e00c 100644 --- a/frontend/src/components/Connection.scss +++ b/frontend/src/components/Connection.scss @@ -39,6 +39,10 @@ background-color: $color-primary-2; } + &.contains-flag { + border-right: 3px solid $color-secondary-2; + } + } .connection-popover { diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js index 2028158..40fdcad 100644 --- a/frontend/src/components/ConnectionContent.js +++ b/frontend/src/components/ConnectionContent.js @@ -1,9 +1,7 @@ import React, {Component} from 'react'; import './ConnectionContent.scss'; -import {Col, Container, Dropdown, Row} from 'react-bootstrap'; +import {Dropdown} from 'react-bootstrap'; import axios from 'axios'; -import {withRouter} from "react-router-dom"; -import {Redirect} from "react-router"; class ConnectionContent extends Component { @@ -19,12 +17,6 @@ class ConnectionContent extends Component { this.setFormat = this.setFormat.bind(this); } - componentDidMount() { - if ('format' in this.props.match.params) { - this.setFormat(this.props.match.params.format); - } - } - componentDidUpdate(prevProps, prevState, snapshot) { if (this.props.connection !== null && ( this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) { @@ -51,11 +43,8 @@ class ConnectionContent extends Component { return <div>nope</div>; } - const format = this.state.format !== "default" ? `/${this.state.format}` : ""; - const redirect = <Redirect push to={`/connections/${this.props.connection.id}${format}`}/>; - let payload = content.map((c, i) => - <span key={`content-${i}`} className={c.from_client ? "from-client" : "from-server"} title="cccccc"> + <span key={`content-${i}`} className={c.from_client ? "from-client" : "from-server"}> {c.content} </span> ); @@ -63,11 +52,11 @@ class ConnectionContent extends Component { return ( <div className="connection-content"> <div className="connection-content-options"> - <Container> - <Row> - <Col md={2}>ciao</Col> - </Row> - </Container> + {/*<Container>*/} + {/* <Row>*/} + {/* <Col md={2}>ciao</Col>*/} + {/* </Row>*/} + {/*</Container>*/} <Dropdown onSelect={this.setFormat} > @@ -92,10 +81,6 @@ class ConnectionContent extends Component { </div> <pre>{payload}</pre> - - {/*{redirect}*/} - - </div> ); } @@ -103,4 +88,4 @@ class ConnectionContent extends Component { } -export default withRouter(ConnectionContent); +export default ConnectionContent; diff --git a/frontend/src/components/ServicePortFilter.js b/frontend/src/components/ServicePortFilter.js new file mode 100644 index 0000000..72f2643 --- /dev/null +++ b/frontend/src/components/ServicePortFilter.js @@ -0,0 +1,91 @@ +import React, {Component} from 'react'; +import './ServicePortFilter.scss'; +import {withRouter} from "react-router-dom"; +import {Redirect} from "react-router"; + +class ServicePortFilter extends Component { + + constructor(props) { + super(props); + this.state = { + servicePort: "", + servicePortUrl: null, + timeoutHandle: null + }; + + this.servicePortChanged = this.servicePortChanged.bind(this); + } + + componentDidMount() { + let params = new URLSearchParams(this.props.location.search); + let servicePort = params.get("service_port"); + if (servicePort !== null) { + this.setState({ + servicePort: servicePort, + servicePortUrl: servicePort + }); + } + } + + servicePortChanged(event) { + let value = event.target.value.replace(/[^\d]/gi, ''); + if (value.startsWith("0")) { + return; + } + if (value !== "") { + let port = parseInt(value); + if (port > 65565) { + return; + } + } + + if (this.state.timeoutHandle !== null) { + clearTimeout(this.state.timeoutHandle); + } + this.setState({ + servicePort: value, + timeoutHandle: setTimeout(() => + this.setState({servicePortUrl: value === "" ? null : value}), 300) + }); + } + + render() { + let redirect = null; + let urlParams = new URLSearchParams(this.props.location.search); + if (urlParams.get("service_port") !== this.state.servicePortUrl) { + if (this.state.servicePortUrl !== null) { + urlParams.set("service_port", this.state.servicePortUrl); + } else { + urlParams.delete("service_port"); + } + redirect = <Redirect push to={`${this.props.location.pathname}?${urlParams}`} />; + } + let active = this.state.servicePort !== ""; + + return ( + <div className={"filter d-inline-block" + (active ? " filter-active" : "")} + style={{"width": "200px"}}> + <div className="input-group"> + <div className="filter-name-wrapper"> + <span className="filter-name" id="filter-service_port">service_port:</span> + </div> + <input placeholder="all ports" aria-label="service_port" aria-describedby="filter-service_port" + className="form-control filter-value" onChange={this.servicePortChanged} value={this.state.servicePort} /></div> + + { active && + <div className="filter-delete"> + <span className="filter-delete-icon" onClick={() => this.setState({ + servicePort: "", + servicePortUrl: null + })}>del</span> + </div> + } + + {redirect} + </div> + ); + } + +} + +export default withRouter(ServicePortFilter); diff --git a/frontend/src/components/ServicePortFilter.scss b/frontend/src/components/ServicePortFilter.scss new file mode 100644 index 0000000..2b23444 --- /dev/null +++ b/frontend/src/components/ServicePortFilter.scss @@ -0,0 +1,61 @@ +@import '../colors.scss'; + +.filter { + margin: 0 10px; + position: relative; + + .filter-name-wrapper { + background-color: $color-primary-2; + padding: 3px 7px; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + } + + .filter-name { + font-size: 13px; + } + + .filter-value { + font-size: 13px; + padding-left: 0; + border-radius: 5px; + + &:focus { + background-color: $color-primary-2; + } + } + + &.filter-active { + .filter-name-wrapper { + background-color: $color-primary-4; + color: $color-primary-3; + } + + .filter-value { + font-size: 13px; + background-color: $color-primary-4; + color: $color-primary-3; + + &:focus { + background-color: $color-primary-4; + } + } + } + + .filter-delete { + position: absolute; + right: 10px; + top: 10px; + + z-index: 10; + font-size: 11px; + letter-spacing: -0.5px; + + color: $color-primary-2; + cursor: pointer; + + .filter-delete-icon { + font-weight: 800; + } + } +}
\ No newline at end of file diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js index 34e980e..e3119aa 100644 --- a/frontend/src/views/App.js +++ b/frontend/src/views/App.js @@ -26,7 +26,6 @@ class App extends Component { <Router> <Header onOpenServices={() => this.setState({servicesShow: true})}/> <Switch> - <Route path="/connections/:id/:format" children={<MainPane/>}/> <Route path="/connections/:id" children={<MainPane/>}/> <Route path="/" children={<MainPane/>}/> </Switch> diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js index e4a7b01..9b9fe35 100644 --- a/frontend/src/views/Connections.js +++ b/frontend/src/views/Connections.js @@ -5,7 +5,6 @@ import Connection from "../components/Connection"; import Table from 'react-bootstrap/Table'; import {Redirect} from 'react-router'; import {withRouter} from "react-router-dom"; -import {objectToQueryString} from "../utils"; class Connections extends Component { @@ -16,7 +15,11 @@ class Connections extends Component { connections: [], firstConnection: null, lastConnection: null, - showHidden: false + showHidden: false, + prevParams: null, + flagRule: null, + rules: null, + queryString: null }; this.scrollTopThreashold = 0.00001; @@ -26,10 +29,12 @@ class Connections extends Component { this.handleScroll = this.handleScroll.bind(this); this.connectionSelected = this.connectionSelected.bind(this); + this.addServicePortFilter = this.addServicePortFilter.bind(this); } componentDidMount() { - this.loadConnections({limit: this.queryLimit, hidden: this.state.showHidden}); + this.loadConnections({limit: this.queryLimit, hidden: this.state.showHidden}) + .then(() => this.setState({loaded: true})); } connectionSelected(c) { @@ -37,7 +42,13 @@ class Connections extends Component { this.props.onSelected(c); } - + componentDidUpdate(prevProps, prevState, snapshot) { + if (this.state.loaded && prevProps.location.search !== this.props.location.search) { + this.setState({queryString: this.props.location.search}); + this.loadConnections({limit: this.queryLimit, hidden: this.state.showHidden}) + .then(() => console.log("Connections reloaded after query string update")); + } + } handleScroll(e) { let relativeScroll = e.currentTarget.scrollTop / (e.currentTarget.scrollHeight - e.currentTarget.clientHeight); @@ -45,32 +56,38 @@ class Connections extends Component { this.loadConnections({ from: this.state.lastConnection.id, limit: this.queryLimit, hidden: this.state.showHidden - }); + }).then(() => console.log("Following connections loaded")); } if (!this.state.loading && relativeScroll < this.scrollTopThreashold) { this.loadConnections({ to: this.state.firstConnection.id, limit: this.queryLimit, hidden: this.state.showHidden - }); + }).then(() => console.log("Previous connections loaded")); } + } + addServicePortFilter(port) { + let urlParams = new URLSearchParams(this.props.location.search); + urlParams.set("service_port", port); + this.setState({queryString: "?" + urlParams}); } async loadConnections(params) { let url = "/api/connections"; - if (params !== undefined) { - const urlParams = new URLSearchParams(window.location.search); - let obj = Object.fromEntries(urlParams.entries()); - url += "?" + objectToQueryString({...params, ...obj}); // TODO: remove this shit + const urlParams = new URLSearchParams(this.props.location.search); + for (const [name, value] of Object.entries(params)) { + urlParams.set(name, value); } - this.setState({loading: true}); - let res = await axios.get(url); + + this.setState({loading: true, prevParams: params}); + let res = await axios.get(`${url}?${urlParams}`); let connections = this.state.connections; let firstConnection = this.state.firstConnection; let lastConnection = this.state.lastConnection; - if (res.data.length > 0) { - if (params !== undefined && params.from !== undefined) { + + if (params !== undefined && params.from !== undefined) { + if (res.data.length > 0) { connections = this.state.connections.concat(res.data); lastConnection = connections[connections.length - 1]; if (connections.length > this.maxConnections) { @@ -78,34 +95,55 @@ class Connections extends Component { connections.length - 1); firstConnection = connections[0]; } - } else if (params !== undefined && params.to !== undefined) { + } + } else if (params !== undefined && params.to !== undefined) { + if (res.data.length > 0) { connections = res.data.concat(this.state.connections); firstConnection = connections[0]; if (connections.length > this.maxConnections) { connections = connections.slice(0, this.maxConnections); lastConnection = connections[this.maxConnections - 1]; } - } else { + } + } else { + if (res.data.length > 0) { connections = res.data; firstConnection = connections[0]; lastConnection = connections[connections.length - 1]; + } else { + connections = []; + firstConnection = null; + lastConnection = null; } } + let flagRule = this.state.flagRule; + let rules = this.state.rules; + if (flagRule === null) { + let res = await axios.get("/api/rules"); + rules = res.data; + flagRule = rules.filter(rule => { + return rule.name === "flag"; + })[0]; + } + this.setState({ loading: false, connections: connections, + rules: res.data, + flagRule: flagRule, firstConnection: firstConnection, lastConnection: lastConnection }); } - render() { - let redirect = null; + let redirect; + let queryString = this.state.queryString !== null ? this.state.queryString : "" if (this.state.selected) { - const format = this.props.match.params.format; - redirect = <Redirect push to={`/connections/${this.state.selected}${format !== undefined ? ("/" + format) : ""}`} />; + let format = this.props.match.params.format; + format = format !== undefined ? "/" + format : ""; + redirect = <Redirect push to={`/connections/${this.state.selected}${format}${queryString}`} />; } let loading = null; @@ -123,8 +161,8 @@ class Connections extends Component { <tr> <th>service</th> <th>srcip</th> - <th>dstip</th> <th>srcport</th> + <th>dstip</th> <th>dstport</th> <th>duration</th> <th>up</th> @@ -137,14 +175,16 @@ class Connections extends Component { this.state.connections.map(c => <Connection key={c.id} data={c} onSelected={() => this.connectionSelected(c)} selected={this.state.selected === c.id} onMarked={marked => c.marked = marked} - onEnabled={enabled => c.hidden = !enabled}/> + onEnabled={enabled => c.hidden = !enabled} + containsFlag={c.matched_rules.includes(this.state.flagRule.id)} + addServicePortFilter={this.addServicePortFilter}/> ) } {loading} </tbody> </Table> - {/*{redirect}*/} + {redirect} </div> ); } diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js index f96b036..5118ec3 100644 --- a/frontend/src/views/Header.js +++ b/frontend/src/views/Header.js @@ -2,6 +2,7 @@ import React, {Component} from 'react'; import Typed from 'typed.js'; import './Header.scss'; import {Button} from "react-bootstrap"; +import ServicePortFilter from "../components/ServicePortFilter"; class Header extends Component { @@ -29,13 +30,25 @@ class Header extends Component { return ( <header className="header container-fluid"> <div className="row"> - <div className="col"> + <div className="col-auto"> <h1 className="header-title type-wrap"> <span style={{whiteSpace: 'pre'}} ref={(el) => { this.el = el; }}/> </h1> </div> + + <div className="col-auto"> + <div className="filters-bar-wrapper"> + <div className="filters-bar"> + <ServicePortFilter /> + {/*<ServicePortFilter name="started_before" default="infinity" />*/} + {/*<ServicePortFilter name="started_after" default="-infinity" />*/} + + </div> + </div> + </div> + <div className="col"> <div className="header-buttons"> <Button variant="yellow" size="sm">pcaps</Button> diff --git a/frontend/src/views/Header.scss b/frontend/src/views/Header.scss index e5040d8..806e398 100644 --- a/frontend/src/views/Header.scss +++ b/frontend/src/views/Header.scss @@ -2,6 +2,7 @@ .header { padding: 15px 30px; + height: 80px; > .row { background-color: $color-primary-0; @@ -9,10 +10,19 @@ .header-title { margin: 5px 0 5px -5px; + width: 200px; } .header-buttons { margin: 5px 0; text-align: right; } + + .filters-bar-wrapper { + height: 50px; + padding: 8px 0; + + .filters-bar { + } + } } |