diff options
author | Emiliano Ciavatta | 2020-09-30 20:57:25 +0000 |
---|---|---|
committer | Emiliano Ciavatta | 2020-09-30 20:57:25 +0000 |
commit | d6e2aaad41f916c2080c59cf7b4e42bf87a1a03f (patch) | |
tree | 57545a722a62d2279bfcd2e36f1cbd1da5a5736a /frontend/src | |
parent | d5ed31be3b6c97f92be4e94b70d32d1b89932ae9 (diff) |
Complete setup page
Diffstat (limited to 'frontend/src')
24 files changed, 398 insertions, 688 deletions
diff --git a/frontend/src/components/Connection.js b/frontend/src/components/Connection.js index 95b27ff..44f9f18 100644 --- a/frontend/src/components/Connection.js +++ b/frontend/src/components/Connection.js @@ -2,9 +2,11 @@ import React, {Component} from 'react'; import './Connection.scss'; import {Form, OverlayTrigger, Popover} from "react-bootstrap"; import backend from "../backend"; -import {durationBetween, formatSize} from "../utils"; +import {dateTimeToTime, durationBetween, formatSize} from "../utils"; import ButtonField from "./fields/ButtonField"; +const classNames = require('classnames'); + class Connection extends Component { constructor(props) { @@ -60,14 +62,6 @@ class Connection extends Component { <span>Closed at {closedAt.toLocaleDateString() + " " + closedAt.toLocaleTimeString()}</span> </div>; - let classes = "connection"; - 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"> <Popover.Content> @@ -88,7 +82,8 @@ class Connection extends Component { </div>; return ( - <tr className={classes}> + <tr className={classNames("connection", {"connection-selected": this.props.selected}, + {"has-matched-rules": conn.matched_rules.length > 0})}> <td> <span className="connection-service"> <ButtonField small fullSpan color={serviceColor} name={serviceName} @@ -99,6 +94,7 @@ class Connection extends Component { <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}>{dateTimeToTime(conn.started_at)}</td> <td className="clickable" onClick={this.props.onSelected}> <OverlayTrigger trigger={["focus", "hover"]} placement="right" overlay={popoverFor("duration", timeInfo)}> @@ -107,7 +103,7 @@ class Connection extends Component { </td> <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn.client_bytes)}</td> <td className="clickable" onClick={this.props.onSelected}>{formatSize(conn.server_bytes)}</td> - <td className="contains-flag"> + <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" : "")}*/} diff --git a/frontend/src/components/Connection.scss b/frontend/src/components/Connection.scss index cb9eb5f..97ef0a4 100644 --- a/frontend/src/components/Connection.scss +++ b/frontend/src/components/Connection.scss @@ -39,8 +39,8 @@ background-color: $color-primary-2; } - &.contains-flag { - border-right: 3px solid $color-secondary-2; + &.has-matched-rules { + border-bottom: 0; } } diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js index 6bc0c96..ccaec0b 100644 --- a/frontend/src/components/ConnectionContent.js +++ b/frontend/src/components/ConnectionContent.js @@ -63,7 +63,7 @@ class ConnectionContent extends Component { } let unrollMap = (obj) => obj == null ? null : Object.entries(obj).map(([key, value]) => - <p><strong>{key}</strong>: {value}</p> + <p key={key}><strong>{key}</strong>: {value}</p> ); let m = connectionMessage.metadata; diff --git a/frontend/src/components/ConnectionMatchedRules.js b/frontend/src/components/ConnectionMatchedRules.js new file mode 100644 index 0000000..21f2a92 --- /dev/null +++ b/frontend/src/components/ConnectionMatchedRules.js @@ -0,0 +1,29 @@ +import React, {Component} from 'react'; +import './ConnectionMatchedRules.scss'; +import ButtonField from "./fields/ButtonField"; + +class ConnectionMatchedRules extends Component { + + constructor(props) { + super(props); + this.state = { + }; + } + + render() { + const matchedRules = this.props.matchedRules.map(mr => { + const rule = this.props.rules.find(r => r.id === mr); + return <ButtonField key={mr} onClick={() => this.props.addMatchedRulesFilter(rule.id)} name={rule.name} + color={rule.color} small />; + }); + + return ( + <tr className="connection-matches"> + <td className="row-label">matched_rules:</td> + <td className="rule-buttons" colSpan={9}>{matchedRules}</td> + </tr> + ); + } +} + +export default ConnectionMatchedRules; diff --git a/frontend/src/components/ConnectionMatchedRules.scss b/frontend/src/components/ConnectionMatchedRules.scss new file mode 100644 index 0000000..ed18f3c --- /dev/null +++ b/frontend/src/components/ConnectionMatchedRules.scss @@ -0,0 +1,23 @@ +@import '../colors.scss'; + +.connection-matches { + background-color: $color-primary-0; + + .rule-buttons { + padding: 0; + } + + .button-field { + display: inline-block; + margin-right: 5px; + + button { + font-size: 0.8em; + padding: 2px 10px; + } + } + + .row-label { + font-size: 0.8em; + } +} diff --git a/frontend/src/components/panels/ConfigurationPane.js b/frontend/src/components/panels/ConfigurationPane.js new file mode 100644 index 0000000..10309f6 --- /dev/null +++ b/frontend/src/components/panels/ConfigurationPane.js @@ -0,0 +1,162 @@ +import React, {Component} from 'react'; +import './common.scss'; +import './ConfigurationPane.scss'; +import LinkPopover from "../objects/LinkPopover"; +import {Col, Container, Row} from "react-bootstrap"; +import InputField from "../fields/InputField"; +import TextField from "../fields/TextField"; +import ButtonField from "../fields/ButtonField"; +import CheckField from "../fields/CheckField"; +import {createCurlCommand} from "../../utils"; +import Table from "react-bootstrap/Table"; +import validation from "../../validation"; +import backend from "../../backend"; + +class ConfigurationPane extends Component { + + constructor(props) { + super(props); + this.state = { + settings: { + "config": { + "server_address": "", + "flag_regex": "", + "auth_required": false + }, + "accounts": { + } + }, + newUsername: "", + newPassword: "" + }; + } + + saveSettings = () => { + if (this.validateSettings(this.state.settings)) { + backend.post("/setup", this.state.settings).then(_ => { + this.props.onConfigured(); + }).catch(res => { + this.setState({setupStatusCode: res.status, setupResponse: JSON.stringify(res.json)}); + }); + } + }; + + validateSettings = (settings) => { + let valid = true; + if (!validation.isValidAddress(settings.config.server_address, true)) { + this.setState({serverAddressError: "invalid ip_address"}); + valid = false; + } + if (settings.config.flag_regex.length < 8) { + this.setState({flagRegexError: "flag_regex.length < 8"}); + valid = false; + } + + return valid; + }; + + updateParam = (callback) => { + callback(this.state.settings); + this.setState({settings: this.state.settings}); + }; + + addAccount = () => { + if (this.state.newUsername.length !== 0 && this.state.newPassword.length !== 0) { + const settings = this.state.settings; + settings.accounts[this.state.newUsername] = this.state.newPassword; + + this.setState({ + newUsername: "", + newPassword: "", + settings: settings + }); + } else { + this.setState({ + newUsernameActive: this.state.newUsername.length === 0, + newPasswordActive: this.state.newPassword.length === 0 + }); + } + }; + + render() { + const settings = this.state.settings; + const curlCommand = createCurlCommand("/setup", "POST", settings); + + const accounts = Object.entries(settings.accounts).map(([username, password]) => + <tr key={username}> + <td>{username}</td> + <td><LinkPopover text="******" content={password} /></td> + <td><ButtonField variant="red" small rounded name="delete" + onClick={() => this.updateParam((s) => delete s.accounts[username]) }/></td> + </tr>).concat(<tr key={"new_account"}> + <td><InputField value={this.state.newUsername} small active={this.state.newUsernameActive} + onChange={(v) => this.setState({newUsername: v})} /></td> + <td><InputField value={this.state.newPassword} small active={this.state.newPasswordActive} + onChange={(v) => this.setState({newPassword: v})} /></td> + <td><ButtonField variant="green" small rounded name="add" onClick={this.addAccount}/></td> + </tr>); + + return ( + <div className="configuration-pane"> + <div className="pane"> + <div className="pane-container"> + <div className="pane-section"> + <div className="section-header"> + <span className="api-request">POST /setup</span> + <span className="api-response"><LinkPopover text={this.state.setupStatusCode} + content={this.state.setupResponse} + placement="left" /></span> + </div> + + <div className="section-content"> + <Container className="p-0"> + <Row> + <Col> + <InputField name="server_address" value={settings.config.server_address} + error={this.state.serverAddressError} + onChange={(v) => this.updateParam((s) => s.config.server_address = v)} /> + <InputField name="flag_regex" value={settings.config.flag_regex} + onChange={(v) => this.updateParam((s) => s.config.flag_regex = v)} + error={this.state.flagRegexError} /> + <div style={{"marginTop": "10px"}}> + <CheckField checked={settings.config.auth_required} name="auth_required" + onChange={(v) => this.updateParam((s) => s.config.auth_required = v)}/> + </div> + + </Col> + + <Col> + accounts: + <div className="section-table"> + <Table borderless size="sm"> + <thead> + <tr> + <th>username</th> + <th>password</th> + <th>actions</th> + </tr> + </thead> + <tbody> + {accounts} + </tbody> + </Table> + </div> + </Col> + </Row> + </Container> + + <TextField value={curlCommand} rows={4} readonly small={true}/> + </div> + + <div className="section-footer"> + <ButtonField variant="green" name="save" bordered onClick={this.saveSettings} /> + </div> + </div> + </div> + </div> + </div> + ); + } +} + +export default ConfigurationPane; diff --git a/frontend/src/components/panels/ConfigurationPane.scss b/frontend/src/components/panels/ConfigurationPane.scss new file mode 100644 index 0000000..955d2bc --- /dev/null +++ b/frontend/src/components/panels/ConfigurationPane.scss @@ -0,0 +1,19 @@ +@import '../../colors'; + +.configuration-pane { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: $color-primary-0; + + .pane { + flex-basis: 900px; + margin-bottom: 200px; + } + + .pane-container { + padding-bottom: 1px; + } + +} diff --git a/frontend/src/components/panels/MainPane.js b/frontend/src/components/panels/MainPane.js new file mode 100644 index 0000000..3202d6d --- /dev/null +++ b/frontend/src/components/panels/MainPane.js @@ -0,0 +1,56 @@ +import React, {Component} from 'react'; +import './common.scss'; +import './MainPane.scss'; +import Connections from "../../views/Connections"; +import ConnectionContent from "../ConnectionContent"; +import {Route, Switch, withRouter} from "react-router-dom"; +import PcapPane from "./PcapPane"; +import backend from "../../backend"; +import RulePane from "./RulePane"; +import ServicePane from "./ServicePane"; + +class MainPane extends Component { + + constructor(props) { + super(props); + this.state = { + selectedConnection: null, + loading: false + }; + } + + componentDidMount() { + const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/); + if (match != null) { + this.setState({loading: true}); + backend.get(`/api/connections/${match[1]}`) + .then(res => this.setState({selectedConnection: res.json, loading: false})) + .catch(error => console.log(error)); + } + } + + render() { + return ( + <div className="main-pane"> + <div className="pane connections-pane"> + { + !this.state.loading && + <Connections onSelected={(c) => this.setState({selectedConnection: c})} + initialConnection={this.state.selectedConnection} /> + } + </div> + <div className="pane details-pane"> + <Switch> + <Route path="/pcaps" children={<PcapPane />} /> + <Route path="/rules" children={<RulePane />} /> + <Route path="/services" children={<ServicePane />} /> + <Route exact path="/connections/:id" children={<ConnectionContent connection={this.state.selectedConnection} />} /> + <Route children={<ConnectionContent />} /> + </Switch> + </div> + </div> + ); + } +} + +export default withRouter(MainPane); diff --git a/frontend/src/components/panels/MainPane.scss b/frontend/src/components/panels/MainPane.scss new file mode 100644 index 0000000..04be347 --- /dev/null +++ b/frontend/src/components/panels/MainPane.scss @@ -0,0 +1,23 @@ +@import '../../colors'; + +.main-pane { + height: 100%; + display: flex; + padding: 0 15px; + background-color: $color-primary-2; + + .pane { + flex: 1; + } + + .connections-pane { + flex: 1 0; + margin-right: 7.5px; + } + + .details-pane { + flex: 1 1; + margin-left: 7.5px; + } + +} diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js index e83e3da..7b3fde6 100644 --- a/frontend/src/components/panels/PcapPane.js +++ b/frontend/src/components/panels/PcapPane.js @@ -109,7 +109,7 @@ class PcapPane extends Component { render() { let sessions = this.state.sessions.map(s => - <tr className="table-row"> + <tr key={s.id} className="table-row"> <td>{s["id"].substring(0, 8)}</td> <td>{dateTimeToTime(s["started_at"])}</td> <td>{durationBetween(s["started_at"], s["completed_at"])}</td> @@ -119,8 +119,7 @@ class PcapPane extends Component { <td><LinkPopover text={Object.keys(s["packets_per_service"]).length + " services"} content={JSON.stringify(s["packets_per_service"])} placement="left"/></td> - <td className="table-cell-action"><a target="_blank" rel="noopener noreferrer" - href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a> + <td className="table-cell-action"><a href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a> </td> </tr> ); diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js index 7cb849c..49364d2 100644 --- a/frontend/src/components/panels/RulePane.js +++ b/frontend/src/components/panels/RulePane.js @@ -13,6 +13,7 @@ import ChoiceField from "../fields/ChoiceField"; import ButtonField from "../fields/ButtonField"; import validation from "../../validation"; import LinkPopover from "../objects/LinkPopover"; +import {randomClassName} from "../../utils"; const classNames = require('classnames'); const _ = require('lodash'); @@ -220,7 +221,7 @@ class RulePane extends Component { const pattern = this.state.selectedPattern || this.state.newPattern; let rules = this.state.rules.map(r => - <tr onClick={() => { + <tr key={r.id} onClick={() => { this.reset(); this.setState({selectedRule: _.cloneDeep(r)}); }} className={classNames("row-small", "row-clickable", {"row-selected": rule.id === r.id })}> @@ -235,7 +236,7 @@ class RulePane extends Component { rule.patterns.concat(this.state.newPattern) : rule.patterns ).map(p => p === pattern ? - <tr> + <tr key={randomClassName()}> <td style={{"width": "500px"}}> <InputField small active={this.state.patternRegexFocused} value={pattern.regex} onChange={(v) => { @@ -272,7 +273,7 @@ class RulePane extends Component { </td> </tr> : - <tr className="row-small"> + <tr key={"new_pattern"} className="row-small"> <td>{p.regex}</td> <td>{p.flags.caseless ? "yes": "no"}</td> <td>{p.flags.dot_all ? "yes": "no"}</td> @@ -377,7 +378,7 @@ class RulePane extends Component { <thead> <tr> <th>regex</th> - <th>Aa</th> + <th>!Aa</th> <th>.*</th> <th>\n+</th> <th>UTF8</th> diff --git a/frontend/src/components/panels/ServicePane.js b/frontend/src/components/panels/ServicePane.js index b21ad6c..eaefa64 100644 --- a/frontend/src/components/panels/ServicePane.js +++ b/frontend/src/components/panels/ServicePane.js @@ -100,7 +100,7 @@ class ServicePane extends Component { const service = this.state.currentService; let services = this.state.services.map(s => - <tr onClick={() => { + <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 })}> diff --git a/frontend/src/index.scss b/frontend/src/index.scss index 9dcc692..9ba23de 100644 --- a/frontend/src/index.scss +++ b/frontend/src/index.scss @@ -11,8 +11,7 @@ body { -moz-osx-font-smoothing: grayscale; background-color: $color-primary-2; color: $color-primary-4; - height: 100%; - max-height: 100%; + height: 100vh; font-size: 100%; } @@ -67,10 +66,6 @@ a { } } -textarea.form-control { - resize: none; -} - .table { color: $color-primary-4; } diff --git a/frontend/src/views/App.js b/frontend/src/views/App.js index 8bd5e46..fb4454c 100644 --- a/frontend/src/views/App.js +++ b/frontend/src/views/App.js @@ -1,62 +1,46 @@ import React, {Component} from 'react'; +import './App.scss'; import Header from "./Header"; -import MainPane from "./MainPane"; +import MainPane from "../components/panels/MainPane"; import Footer from "./Footer"; import {BrowserRouter as Router} from "react-router-dom"; -import Services from "./Services"; import Filters from "./Filters"; -import Config from "./Config"; +import backend from "../backend"; +import ConfigurationPane from "../components/panels/ConfigurationPane"; class App extends Component { constructor(props) { super(props); - this.state = { - servicesWindowOpen: false, - filterWindowOpen: false, - configWindowOpen: false, - configDone: false - }; - - fetch('/api/services') - .then(response => { - if( response.status === 503){ - this.setState({configWindowOpen: true}); - } else if (response.status === 200){ - this.setState({configDone: true}); - } - }); + this.state = {}; + } + componentDidMount() { + backend.get("/api/services").then(_ => this.setState({configured: true})); } render() { let modal; - if (this.state.servicesWindowOpen) { - modal = <Services onHide={() => this.setState({servicesWindowOpen: false})}/>; - } - if (this.state.filterWindowOpen) { + if (this.state.filterWindowOpen && this.state.configured) { modal = <Filters onHide={() => this.setState({filterWindowOpen: false})}/>; } - if (this.state.configWindowOpen) { - modal = <Config onHide={() => this.setState({configWindowOpen: false})} - onDone={() => this.setState({configDone: true})}/>; - } return ( - <div className="app"> + <div className="main"> <Router> - <Header onOpenServices={() => this.setState({servicesWindowOpen: true})} - onOpenFilters={() => this.setState({filterWindowOpen: true})} - onOpenConfig={() => this.setState({configWindowOpen: true})} - onOpenUpload={() => this.setState({uploadWindowOpen: true})} - onConfigDone={this.state.configDone} - /> - <MainPane /> - {modal} - <Footer/> + <div className="main-header"> + <Header onOpenFilters={() => this.setState({filterWindowOpen: true})} /> + </div> + <div className="main-content"> + {this.state.configured ? <MainPane /> : + <ConfigurationPane onConfigured={() => this.setState({configured: true})} />} + {modal} + </div> + <div className="main-footer"> + {this.state.configured && <Footer/>} + </div> </Router> - </div> ); } diff --git a/frontend/src/views/App.scss b/frontend/src/views/App.scss new file mode 100644 index 0000000..b25d4c9 --- /dev/null +++ b/frontend/src/views/App.scss @@ -0,0 +1,15 @@ + +.main { + display: flex; + flex-direction: column; + height: 100vh; + + .main-content { + flex: 1 1; + overflow: hidden; + } + + .main-header, .main-footer { + flex: 0 0; + } +} diff --git a/frontend/src/views/Config.js b/frontend/src/views/Config.js deleted file mode 100644 index b11f827..0000000 --- a/frontend/src/views/Config.js +++ /dev/null @@ -1,242 +0,0 @@ -import React, {Component} from 'react'; -import './Config.scss'; -import {Button, ButtonGroup, Col, Container, Form, Modal, Row, Table, ToggleButton} from "react-bootstrap"; - -class Config extends Component { - - constructor(props) { - super(props); - - this.state = { - server_address: "", - flag_regex: "", - auth_required: false, - accounts: {}, - showSignup: false, - showConfig: true, - tmpUser:"", - tmpPass:"", - tmpConf:"", - errors:"" - }; - - this.serverIpChanged = this.serverIpChanged.bind(this); - this.flagRegexChanged = this.flagRegexChanged.bind(this); - this.authRequiredChanged = this.authRequiredChanged.bind(this); - this.userChanged = this.userChanged.bind(this); - this.passwdChanged = this.passwdChanged.bind(this); - this.confirmChanged = this.confirmChanged.bind(this); - } - - serverIpChanged(event) { - this.setState({server_address: event.target.value}); - } - - flagRegexChanged(event) { - this.setState({flag_regex: event.target.value}); - } - - authRequiredChanged() { - this.setState({auth_required: !this.value}); - this.checked = !this.checked; - this.value = !this.value; - } - - userChanged(event) { - this.setState({tmpUser: event.target.value}); - } - - passwdChanged(event) { - this.setState({tmpPass: event.target.value}); - } - - confirmChanged(event) { - this.setState({tmpConf: event.target.value}); - } - - setup() { - const requestOptions = { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - config: { - server_address: this.state.server_address, - flag_regex: this.state.flag_regex, - auth_required: this.state.auth_required, - }, - accounts: this.state.accounts - }) - }; - - fetch('/setup', requestOptions) - .then(response => { - if (response.status === 202 ){ - //this.setState({showConfig:false}); - this.props.onHide(); - this.props.onDone(); - } else { - response.json().then(data => { - this.setState( - {errors : data.error.toString()} - ); - }); - } - } - ); - - } - - signup(){ - if (this.state.tmpPass === this.state.tmpConf){ - const accounts = {...this.state.accounts}; - accounts[this.state.tmpUser] = this.state.tmpPass; - this.setState({accounts}); - console.log(this.state); - this.setState({showSignup:false,showConfig:true}) - } - this.setState({tmpUser : ""}); - this.setState({tmpPass : ""}); - this.setState({tmpConf : ""}); - } - - render() { - let rows = Object.keys(this.state.accounts).map(u => - <tr> - <td>{u}</td> - </tr> - ); - - - - return ( - <> - <Modal show={this.state.showSignup} size="lg" aria-labelledby="services-dialog" centered > - <Modal.Header> - <Modal.Title id="services-dialog"> - # passwd - </Modal.Title> - </Modal.Header> - <Modal.Body> - <Container> - <Row> - <Form id="passwd-form"> - <Form.Group controlId="username"> - <Form.Label>username:</Form.Label> - <Form.Control type="text" onChange={this.userChanged} value={this.state.tmpUser}/> - </Form.Group> - - <Form.Group controlId="password"> - <Form.Label>password:</Form.Label> - <Form.Control type="password" onChange={this.passwdChanged} value={this.state.tmpPass}/> - </Form.Group> - - <Form.Group controlId="confirmPassword"> - <Form.Label>confirm password:</Form.Label> - <Form.Control type="password" onChange={this.confirmChanged} value={this.state.tmpConf}/> - </Form.Group> - - - </Form> - - </Row> - - </Container> - </Modal.Body> - <Modal.Footer className="dialog-footer"> - <Button variant="green" onClick={() => this.signup()}>signup</Button> - <Button variant="red" onClick={() => this.setState({showSignup:false,showConfig:true})}>close</Button> - </Modal.Footer> - - </Modal> - <Modal - {...this.props} - show="true" - size="lg" - aria-labelledby="services-dialog" - centered - > - <Modal.Header> - <Modal.Title id="services-dialog"> - ~/.config - </Modal.Title> - </Modal.Header> - <Modal.Body> - <div class="blink"><span><b>Warning:</b></span> once the configuration is completed, it cannot be changed unless you reset caronte :(</div> - <hr/> - <Container> - <Row> - <Col md={5}> - - <ButtonGroup toggle className="mb-2"> - <ToggleButton - type="checkbox" - variant="secondary" - checked={this.state.auth_required} - value={this.state.auth_required} - onChange={() => this.authRequiredChanged()} - > - Authentication - </ToggleButton> - </ButtonGroup> - - <Table borderless size="sm" className="users-list"> - - <thead> - <tr> - <th>users</th> - </tr> - </thead> - <tbody> - {rows} - <tr> <td> - <Button size="sm" onClick={() => this.setState({showSignup:true,showConfig:false})}>new</Button> - </td> </tr> - </tbody> - </Table> - - - - </Col> - - <Col md={7}> - - <Form> - <Form.Group controlId="server_address"> - <Form.Label>server_address:</Form.Label> - <Form.Control type="text" onChange={this.serverIpChanged} value={this.state.server_address}/> - </Form.Group> - - <Form.Group controlId="flag_regex"> - <Form.Label>flag_regex:</Form.Label> - <Form.Control type="text" onChange={this.flagRegexChanged} value={this.state.flag_regex}/> - </Form.Group> - - </Form> - - </Col> - - </Row> - <Row> - <div class="error"> - <b> - {this.state.errors - .split('\n').map((item, key) => { - return <span key={key}>{item}<br/></span>}) - } - </b> - </div> - </Row> - - </Container> - </Modal.Body> - <Modal.Footer className="dialog-footer"> - <Button variant="green" onClick={() => this.setup()}>set</Button> - <Button variant="red" onClick={this.props.onHide}>close</Button> - </Modal.Footer> - </Modal> - </> - ); - } -} - -export default Config; diff --git a/frontend/src/views/Config.scss b/frontend/src/views/Config.scss deleted file mode 100644 index 331d7a7..0000000 --- a/frontend/src/views/Config.scss +++ /dev/null @@ -1,55 +0,0 @@ -@import '../colors.scss'; - -.curl-output { - width: 100%; - font-size: 13px; -} - -#passwd-form { - margin:auto; -} - -.users-list { - .btn { - width: 70px; - } - - td { - background-color: $color-primary-2; - border-top: 2px solid $color-primary-0; - vertical-align: middle; - text-align: center; - } - - th { - background-color: $color-primary-1; - text-align: center; - } -} - -.btn-color { - border: 3px solid #fff; -} - -.dialog-footer { - .btn { - width: 80px; - } -} - -.blink{ - - span{ - animation: blink 1s linear infinite; - } - @keyframes blink{ - 0%{opacity: 0;} - 50%{opacity: .5;} - 100%{opacity: 1;} - } - -} - -.error{ - color: red; -} diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js index 9dca7e9..73979c4 100644 --- a/frontend/src/views/Connections.js +++ b/frontend/src/views/Connections.js @@ -5,6 +5,7 @@ import Table from 'react-bootstrap/Table'; import {Redirect} from 'react-router'; import {withRouter} from "react-router-dom"; import backend from "../backend"; +import ConnectionMatchedRules from "../components/ConnectionMatchedRules"; class Connections extends Component { @@ -62,11 +63,21 @@ class Connections extends Component { }; addServicePortFilter = (port) => { - let urlParams = new URLSearchParams(this.props.location.search); + const urlParams = new URLSearchParams(this.props.location.search); urlParams.set("service_port", port); 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.setState({queryString: "?" + urlParams}); + } + }; + async loadConnections(params) { let url = "/api/connections"; const urlParams = new URLSearchParams(this.props.location.search); @@ -112,20 +123,15 @@ class Connections extends Component { } } - let flagRule = this.state.flagRule; let rules = this.state.rules; - if (flagRule === null) { + if (rules === null) { rules = (await backend.get("/api/rules")).json; - flagRule = rules.filter(rule => { - return rule.name === "flag"; - })[0]; } this.setState({ loading: false, connections: connections, - rules: res, - flagRule: flagRule, + rules: rules, firstConnection: firstConnection, lastConnection: lastConnection }); @@ -158,6 +164,7 @@ class Connections extends Component { <th>srcport</th> <th>dstip</th> <th>dstport</th> + <th>started_at</th> <th>duration</th> <th>up</th> <th>down</th> @@ -166,13 +173,18 @@ class Connections extends Component { </thead> <tbody> { - 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} - containsFlag={this.state.flagRule && c.matched_rules.includes(this.state.flagRule.id)} - addServicePortFilter={this.addServicePortFilter}/> - ) + 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} + onEnabled={enabled => c.hidden = !enabled} + addServicePortFilter={this.addServicePortFilter} />, + c.matched_rules.length > 0 && + <ConnectionMatchedRules key={c.id + "_m"} matchedRules={c.matched_rules} + rules={this.state.rules} + addMatchedRulesFilter={this.addMatchedRulesFilter} /> + ]; + }) } {loading} </tbody> diff --git a/frontend/src/views/Filters.js b/frontend/src/views/Filters.js index b62e7eb..ba7d467 100644 --- a/frontend/src/views/Filters.js +++ b/frontend/src/views/Filters.js @@ -1,5 +1,4 @@ import React, {Component} from 'react'; -import './Services.scss'; import {Col, Container, Modal, Row, Table} from "react-bootstrap"; import {filtersDefinitions, filtersNames} from "../components/filters/FiltersDefinitions"; import ButtonField from "../components/fields/ButtonField"; diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js index c38c22d..944f1d5 100644 --- a/frontend/src/views/Header.js +++ b/frontend/src/views/Header.js @@ -46,7 +46,7 @@ class Header extends Component { render() { let quickFilters = filtersNames.filter(name => this.state[`${name}_active`]) - .map(name => filtersDefinitions[name]) + .map(name => <React.Fragment key={name} >{filtersDefinitions[name]}</React.Fragment>) .slice(0, 5); return ( @@ -78,8 +78,9 @@ class Header extends Component { <Link to="/services"> <ButtonField variant="indigo" name="services" bordered /> </Link> - <ButtonField variant="blue" onClick={this.props.onOpenConfig} - disabled={false} name="config" bordered /> + <Link to="/config"> + <ButtonField variant="blue" name="config" bordered /> + </Link> </div> </div> </div> diff --git a/frontend/src/views/MainPane.js b/frontend/src/views/MainPane.js deleted file mode 100644 index d2950ab..0000000 --- a/frontend/src/views/MainPane.js +++ /dev/null @@ -1,59 +0,0 @@ -import React, {Component} from 'react'; -import './MainPane.scss'; -import Connections from "./Connections"; -import ConnectionContent from "../components/ConnectionContent"; -import {Route, Switch, withRouter} from "react-router-dom"; -import PcapPane from "../components/panels/PcapPane"; -import backend from "../backend"; -import RulePane from "../components/panels/RulePane"; -import ServicePane from "../components/panels/ServicePane"; - -class MainPane extends Component { - - constructor(props) { - super(props); - this.state = { - selectedConnection: null, - loading: false - }; - } - - componentDidMount() { - const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/); - if (match != null) { - this.setState({loading: true}); - backend.get(`/api/connections/${match[1]}`) - .then(res => this.setState({selectedConnection: res.json, loading: false})) - .catch(error => console.log(error)); - } - } - - render() { - return ( - <div className="main-pane"> - <div className="container-fluid"> - <div className="row"> - <div className="col-md-6 pane"> - { - !this.state.loading && - <Connections onSelected={(c) => this.setState({selectedConnection: c})} - initialConnection={this.state.selectedConnection} /> - } - </div> - <div className="col-md-6 pl-0 pane"> - <Switch> - <Route path="/pcaps" children={<PcapPane />} /> - <Route path="/rules" children={<RulePane />} /> - <Route path="/services" children={<ServicePane />} /> - <Route exact path="/connections/:id" children={<ConnectionContent connection={this.state.selectedConnection} />} /> - <Route children={<ConnectionContent />} /> - </Switch> - </div> - </div> - </div> - </div> - ); - } -} - -export default withRouter(MainPane); diff --git a/frontend/src/views/MainPane.scss b/frontend/src/views/MainPane.scss deleted file mode 100644 index 20720ba..0000000 --- a/frontend/src/views/MainPane.scss +++ /dev/null @@ -1,6 +0,0 @@ -.main-pane { - .pane { - height: calc(100vh - 210px); - position: relative; - } -} diff --git a/frontend/src/views/Services.js b/frontend/src/views/Services.js deleted file mode 100644 index 97368dc..0000000 --- a/frontend/src/views/Services.js +++ /dev/null @@ -1,210 +0,0 @@ -import React, {Component} from 'react'; -import './Services.scss'; -import {Button, ButtonGroup, Col, Container, Form, FormControl, InputGroup, Modal, Row, Table} from "react-bootstrap"; -import {createCurlCommand} from '../utils'; -import backend from "../backend"; - -class Services extends Component { - - constructor(props) { - super(props); - this.alphabet = 'abcdefghijklmnopqrstuvwxyz'; - this.colors = ["#E53935", "#D81B60", "#8E24AA", "#5E35B1", "#3949AB", "#1E88E5", "#039BE5", "#00ACC1", - "#00897B", "#43A047", "#7CB342", "#9E9D24", "#F9A825", "#FB8C00", "#F4511E", "#6D4C41"]; - - this.state = { - services: {}, - port: 0, - portValid: false, - name: "", - nameValid: false, - color: this.colors[0], - colorValid: false, - notes: "" - }; - - this.portChanged = this.portChanged.bind(this); - this.nameChanged = this.nameChanged.bind(this); - this.notesChanged = this.notesChanged.bind(this); - this.newService = this.newService.bind(this); - this.editService = this.editService.bind(this); - this.saveService = this.saveService.bind(this); - this.loadServices = this.loadServices.bind(this); - } - - componentDidMount() { - this.loadServices(); - } - - portChanged(event) { - let value = event.target.value.replace(/[^\d]/gi, ''); - let port = 0; - if (value !== "") { - port = parseInt(value); - } - this.setState({port: port}); - } - - nameChanged(event) { - let value = event.target.value.replace(/[\s]/gi, '_').replace(/[^\w]/gi, '').toLowerCase(); - this.setState({name: value}); - } - - notesChanged(event) { - this.setState({notes: event.target.value}); - } - - newService() { - this.setState({name: "", port: 0, notes: ""}); - } - - editService(service) { - this.setState({name: service.name, port: service.port, color: service.color, notes: service.notes}); - } - - saveService() { - if (this.state.portValid && this.state.nameValid) { - backend.put("/api/services", { - color: this.state.color, - name: this.state.name, - notes: this.state.notes, - port: this.state.port, - }).then(_ => { - this.newService(); - this.loadServices(); - }); - } - } - - loadServices() { - backend.get("/api/services").then(res => this.setState({services: res})); - } - - componentDidUpdate(prevProps, prevState, snapshot) { - if (this.state.name != null && prevState.name !== this.state.name) { - this.setState({ - nameValid: this.state.name.length >= 3 - }); - } - if (prevState.port !== this.state.port) { - this.setState({ - portValid: this.state.port > 0 && this.state.port <= 65565 - }); - } - } - - render() { - let output = ""; - if (!this.state.portValid) { - output += "assert(1 <= port <= 65565)\n"; - } - if (!this.state.nameValid) { - output += "assert(len(name) >= 3)\n"; - } - if (output === "") { - output = createCurlCommand("/services", "PUT", { - "port": this.state.port, - "name": this.state.name, - "color": this.state.color, - "notes": this.state.notes - }); - } - let rows = Object.values(this.state.services).map(s => - <tr> - <td><Button variant="btn-edit" size="sm" - onClick={() => this.editService(s)} style={{"backgroundColor": s.color}}>edit</Button></td> - <td>{s.port}</td> - <td>{s.name}</td> - </tr> - ); - - let colorButtons = this.colors.map((color, i) => - <Button size="sm" className="btn-color" key={"button" + this.alphabet[i]} - style={{"backgroundColor": color, "borderColor": this.state.color === color ? "#fff" : color}} - onClick={() => this.setState({color: color})}>{this.alphabet[i]}</Button>); - - return ( - <Modal - {...this.props} - show="true" - size="lg" - aria-labelledby="services-dialog" - centered - > - <Modal.Header> - <Modal.Title id="services-dialog"> - ~/services - </Modal.Title> - </Modal.Header> - <Modal.Body> - <Container> - <Row> - <Col md={7}> - <Table borderless size="sm" className="services-list"> - <thead> - <tr> - <th><Button size="sm" onClick={this.newService}>new</Button></th> - <th>port</th> - <th>name</th> - </tr> - </thead> - <tbody> - {rows} - </tbody> - </Table> - </Col> - <Col md={5}> - <Form> - <Form.Group controlId="servicePort"> - <Form.Label>port:</Form.Label> - <Form.Control type="text" onChange={this.portChanged} value={this.state.port}/> - </Form.Group> - - <Form.Group controlId="serviceName"> - <Form.Label>name:</Form.Label> - <Form.Control type="text" onChange={this.nameChanged} value={this.state.name}/> - </Form.Group> - - <Form.Group controlId="serviceColor"> - <Form.Label>color:</Form.Label> - <ButtonGroup aria-label="Basic example"> - {colorButtons.slice(0, 8)} - </ButtonGroup> - <ButtonGroup aria-label="Basic example"> - {colorButtons.slice(8, 18)} - </ButtonGroup> - </Form.Group> - - <Form.Group controlId="serviceNotes"> - <Form.Label>notes:</Form.Label> - <Form.Control as="textarea" rows={3} onChange={this.notesChanged} - value={this.state.notes}/> - </Form.Group> - </Form> - - - </Col> - - </Row> - - <Row> - <Col md={12}> - <InputGroup> - <FormControl as="textarea" rows={4} className="curl-output" readOnly={true} - value={output}/> - </InputGroup> - - </Col> - </Row> - </Container> - </Modal.Body> - <Modal.Footer className="dialog-footer"> - <Button variant="green" onClick={this.saveService}>save</Button> - <Button variant="red" onClick={this.props.onHide}>close</Button> - </Modal.Footer> - </Modal> - ); - } -} - -export default Services; diff --git a/frontend/src/views/Services.scss b/frontend/src/views/Services.scss deleted file mode 100644 index 2abb55e..0000000 --- a/frontend/src/views/Services.scss +++ /dev/null @@ -1,32 +0,0 @@ -@import '../colors.scss'; - -.curl-output { - width: 100%; - font-size: 13px; -} - -.services-list { - .btn { - width: 70px; - } - - td { - background-color: $color-primary-2; - border-top: 2px solid $color-primary-0; - vertical-align: middle; - } - - th { - background-color: $color-primary-1; - } -} - -.btn-color { - border: 3px solid #fff; -} - -.dialog-footer { - .btn { - width: 80px; - } -} |