diff options
24 files changed, 841 insertions, 248 deletions
diff --git a/frontend/src/backend.js b/frontend/src/backend.js index 5eb0e40..a02f7a8 100644 --- a/frontend/src/backend.js +++ b/frontend/src/backend.js @@ -1,5 +1,5 @@ -async function json(method, url, data, headers) { +async function json(method, url, data, headers, returnJson) { const options = { method: method, mode: "cors", @@ -15,7 +15,18 @@ async function json(method, url, data, headers) { options.body = JSON.stringify(data); } const result = await fetch(url, options); - return result.json(); + if (returnJson) { + if (result.status >= 200 && result.status < 300) { + return result.json(); + } else { + return Promise.reject({ + response: result, + json: await result.json() + }); + } + } else { + return result; + } } async function file(url, data, headers) { @@ -44,6 +55,18 @@ const backend = { delete: (url = "", data = null, headers = null) => { return json("DELETE", url, data, headers); }, + getJson: (url = "", headers = null) => { + return json("GET", url, null, headers, true); + }, + postJson: (url = "", data = null, headers = null) => { + return json("POST", url, data, headers, true); + }, + putJson: (url = "", data = null, headers = null) => { + return json("PUT", url, data, headers, true); + }, + deleteJson: (url = "", data = null, headers = null) => { + return json("DELETE", url, data, headers, true); + }, postFile: (url = "", data = null, headers = null) => { return file(url, data, headers); }, diff --git a/frontend/src/components/ConnectionContent.js b/frontend/src/components/ConnectionContent.js index 0c00e8e..0069424 100644 --- a/frontend/src/components/ConnectionContent.js +++ b/frontend/src/components/ConnectionContent.js @@ -22,20 +22,30 @@ class ConnectionContent extends Component { this.setFormat = this.setFormat.bind(this); } + componentDidMount() { + if (this.props.connection != null) { + this.loadStream(); + } + } + componentDidUpdate(prevProps, prevState, snapshot) { - if (this.props.connection !== null && ( + if (this.props.connection != null && ( this.props.connection !== prevProps.connection || this.state.format !== prevState.format)) { - this.setState({loading: true}); - // TODO: limit workaround. - backend.get(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => { - this.setState({ - connectionContent: res, - loading: false - }); - }); + this.loadStream(); } } + loadStream = () => { + this.setState({loading: true}); + // TODO: limit workaround. + backend.getJson(`/api/streams/${this.props.connection.id}?format=${this.state.format}&limit=999999`).then(res => { + this.setState({ + connectionContent: res, + loading: false + }); + }); + }; + setFormat(format) { if (this.validFormats.includes(format)) { this.setState({format: format}); diff --git a/frontend/src/components/MessageAction.js b/frontend/src/components/MessageAction.js index 2c85d84..2c6ebbc 100644 --- a/frontend/src/components/MessageAction.js +++ b/frontend/src/components/MessageAction.js @@ -1,6 +1,8 @@ import React, {Component} from 'react'; import './MessageAction.scss'; -import {Button, FormControl, InputGroup, Modal} from "react-bootstrap"; +import {Modal} from "react-bootstrap"; +import TextField from "./fields/TextField"; +import ButtonField from "./fields/ButtonField"; class MessageAction extends Component { @@ -35,14 +37,11 @@ class MessageAction extends Component { </Modal.Title> </Modal.Header> <Modal.Body> - <InputGroup> - <FormControl as="textarea" className="message-action-value" readOnly={true} - style={{"height": "300px"}} value={this.props.actionValue} ref={this.actionValue} /> - </InputGroup> + <TextField readonly value={this.props.actionValue} textRef={this.actionValue} rows={15} /> </Modal.Body> <Modal.Footer className="dialog-footer"> - <Button variant="green" onClick={this.copyActionValue}>{this.state.copyButtonText}</Button> - <Button variant="red" onClick={this.props.onHide}>close</Button> + <ButtonField variant="green" onClick={this.copyActionValue} name={this.state.copyButtonText} /> + <ButtonField variant="red" onClick={this.props.onHide} name="close" /> </Modal.Footer> </Modal> ); diff --git a/frontend/src/components/fields/ButtonField.js b/frontend/src/components/fields/ButtonField.js new file mode 100644 index 0000000..b32aee8 --- /dev/null +++ b/frontend/src/components/fields/ButtonField.js @@ -0,0 +1,44 @@ +import React, {Component} from 'react'; +import './ButtonField.scss'; +import './common.scss'; + +const classNames = require('classnames'); + +class ButtonField extends Component { + + constructor(props) { + super(props); + } + + render() { + const handler = () => { + if (typeof this.props.onClick === "function") { + this.props.onClick(); + } + }; + + let buttonClassnames = { + "button-bordered": this.props.bordered, + }; + if (this.props.variant) { + buttonClassnames[`button-variant-${this.props.variant}`] = true; + } + + let buttonStyle = {}; + if (this.props.color) { + buttonStyle["backgroundColor"] = this.props.color; + } + if (this.props.border) { + buttonStyle["borderColor"] = this.props.border; + } + + return ( + <div className={classNames( "field", "button-field", {"field-small": this.props.small})}> + <button type="button" className={classNames(classNames(buttonClassnames))} + onClick={handler} style={buttonStyle}>{this.props.name}</button> + </div> + ); + } +} + +export default ButtonField; diff --git a/frontend/src/components/fields/ButtonField.scss b/frontend/src/components/fields/ButtonField.scss new file mode 100644 index 0000000..933279e --- /dev/null +++ b/frontend/src/components/fields/ButtonField.scss @@ -0,0 +1,119 @@ +@import '../../colors.scss'; + +.button-field { + font-size: 0.9em; + + .button-bordered { + border-bottom: 5px solid $color-primary-1; + } + + &.field-small { + font-size: 0.8em; + } + + .button-variant-red { + color: $color-red-light; + background-color: $color-red; + + &.button-bordered { + border-bottom: 5px solid $color-red-dark; + } + + &:hover, + &:active { + color: $color-red-light; + background-color: $color-red-dark; + } + } + + .button-variant-pink { + color: $color-pink-light; + background-color: $color-pink; + + &.button-bordered { + border-bottom: 5px solid $color-pink-dark; + } + + &:hover, + &:active { + color: $color-pink-light; + background-color: $color-pink-dark; + } + } + + .button-variant-purple { + color: $color-purple-light; + background-color: $color-purple; + + &.button-bordered { + border-bottom: 5px solid $color-purple-dark; + } + + &:hover, + &:active { + color: $color-purple-light; + background-color: $color-purple-dark; + } + } + + .button-variant-deep-purple { + color: $color-deep-purple-light; + background-color: $color-deep-purple; + + &.button-bordered { + border-bottom: 5px solid $color-deep-purple-dark; + } + + &:hover, + &:active { + color: $color-deep-purple-light; + background-color: $color-deep-purple-dark; + } + } + + .button-variant-indigo { + color: $color-indigo-light; + background-color: $color-indigo; + + &.button-bordered { + border-bottom: 5px solid $color-indigo-dark; + } + + &:hover, + &:active { + color: $color-indigo-light; + background-color: $color-indigo-dark; + } + } + + .button-variant-blue { + color: $color-blue-light; + background-color: $color-blue; + + &.button-bordered { + border-bottom: 5px solid $color-blue-dark; + } + + &:hover, + &:active { + color: $color-blue-light; + background-color: $color-blue-dark; + } + } + + .button-variant-green { + color: $color-green-light; + background-color: $color-green; + + &.button-bordered { + border-bottom: 5px solid $color-green-dark; + } + + &:hover, + &:active { + color: $color-green-light; + background-color: $color-green-dark; + } + } + +} diff --git a/frontend/src/components/fields/CheckField.js b/frontend/src/components/fields/CheckField.js index 5cceac4..33f4f83 100644 --- a/frontend/src/components/fields/CheckField.js +++ b/frontend/src/components/fields/CheckField.js @@ -1,5 +1,6 @@ import React, {Component} from 'react'; import './CheckField.scss'; +import './common.scss'; import {randomClassName} from "../../utils"; const classNames = require('classnames'); @@ -23,10 +24,10 @@ class CheckField extends Component { }; return ( - <div className={classNames( "check-field", {"field-checked" : checked}, {"field-small": small})}> + <div className={classNames( "field", "check-field", {"field-checked" : checked}, {"field-small": small})}> <div className="field-input"> <input type="checkbox" id={this.id} checked={checked} onChange={handler} /> - <label htmlFor={this.id}>{(checked ? "✓ " : "✗ ") + name}</label> + <label htmlFor={this.id}>{(checked ? "✓ " : "✗ ") + (name != null ? name : "")}</label> </div> </div> ); diff --git a/frontend/src/components/fields/ChoiceField.js b/frontend/src/components/fields/ChoiceField.js new file mode 100644 index 0000000..d409b21 --- /dev/null +++ b/frontend/src/components/fields/ChoiceField.js @@ -0,0 +1,59 @@ +import React, {Component} from 'react'; +import './ChoiceField.scss'; +import './common.scss'; +import {randomClassName} from "../../utils"; + +const classNames = require('classnames'); + +class ChoiceField extends Component { + + constructor(props) { + super(props); + + this.state = { + expanded: false + }; + + this.id = `field-${this.props.name || "noname"}-${randomClassName()}`; + } + + render() { + const name = this.props.name || null; + const inline = this.props.inline; + + const collapse = () => this.setState({expanded: false}); + const expand = () => this.setState({expanded: true}); + + const handler = (key) => { + collapse(); + if (this.props.onChange) { + this.props.onChange(key); + } + }; + + const keys = this.props.keys || []; + const values = this.props.values || []; + + const options = keys.map((key, i) => + <span className="field-option" key={key} onClick={() => handler(key)}>{values[i]}</span> + ); + + return ( + <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})} + tabIndex={0} onBlur={collapse} onClick={() => this.state.expanded ? collapse() : expand()}> + <div className="field-value"> + {((inline && name) ? (name + ": ") : "") + (this.props.value || "select a value")} + </div> + <div className="field-options"> + {options} + </div> + </div> + </div> + ); + } +} + +export default ChoiceField; diff --git a/frontend/src/components/fields/ChoiceField.scss b/frontend/src/components/fields/ChoiceField.scss new file mode 100644 index 0000000..7f32b0e --- /dev/null +++ b/frontend/src/components/fields/ChoiceField.scss @@ -0,0 +1,69 @@ +@import '../../colors.scss'; + +.choice-field { + font-size: 0.9em; + + .field-name { + margin: 0; + } + + .field-select { + position: relative; + margin-top: 5px; + + .field-value { + background-color: $color-primary-2; + border: none; + color: $color-primary-4; + border-radius: 5px; + padding: 7px 10px; + cursor: pointer; + + &:after { + content: "⋎"; + position: absolute; + right: 10px; + } + } + + .field-options { + position: absolute; + top: 35px; + width: 100%; + z-index: 20; + border-top: 3px solid $color-primary-0; + border-radius: 5px; + background-color: $color-primary-2; + display: none; + + .field-option { + display: block; + padding: 5px 10px; + cursor: pointer; + border-radius: 5px; + } + + .field-option:hover { + background-color: $color-primary-1; + } + } + + &:focus { + outline: none; + } + } + + .field-select.select-expanded { + .field-options { + display: block; + } + + .field-value:after { + content: "⋏"; + } + } + + &.field-small { + font-size: 0.8em; + } +} diff --git a/frontend/src/components/fields/InputField.js b/frontend/src/components/fields/InputField.js index af3b3df..6cf967a 100644 --- a/frontend/src/components/fields/InputField.js +++ b/frontend/src/components/fields/InputField.js @@ -1,5 +1,6 @@ import React, {Component} from 'react'; import './InputField.scss'; +import './common.scss'; import {randomClassName} from "../../utils"; const classNames = require('classnames'); @@ -36,12 +37,12 @@ class InputField extends Component { }; let inputProps = {}; if (type !== "file") { - inputProps["value"] = value; + inputProps["value"] = value || this.props.initialValue; } return ( - <div className={classNames("input-field", {"field-active" : active}, {"field-invalid": invalid}, - {"field-small": small}, {"field-inline": inline})}> + <div className={classNames("field", "input-field", {"field-active" : active}, + {"field-invalid": invalid}, {"field-small": small}, {"field-inline": inline})}> <div className="field-wrapper"> { name && <div className="field-name"> diff --git a/frontend/src/components/fields/InputField.scss b/frontend/src/components/fields/InputField.scss index cdb8c9f..79e2b7e 100644 --- a/frontend/src/components/fields/InputField.scss +++ b/frontend/src/components/fields/InputField.scss @@ -14,42 +14,20 @@ position: relative; .field-value { - input, .file-label { + .file-label { background-color: $color-primary-2; + margin: 0; width: 100%; - border: none; color: $color-primary-4; border-radius: 5px; padding: 7px 10px; - - &:focus { - background-color: $color-primary-1; - color: $color-primary-4; - box-shadow: none; - outline: none; - } - - &[readonly] { - background-color: $color-primary-2; - border: none; - color: $color-primary-4; - } - - &[readonly]:focus { - background-color: $color-primary-1; - color: $color-primary-4; - box-shadow: none; - } + cursor: pointer; } input[type="file"] { display: none; } - .file-label { - margin: 0; - } - .file-label:after { content: "Browse"; position: absolute; diff --git a/frontend/src/components/fields/TextField.js b/frontend/src/components/fields/TextField.js index 86b98ed..de68c21 100644 --- a/frontend/src/components/fields/TextField.js +++ b/frontend/src/components/fields/TextField.js @@ -1,5 +1,6 @@ import React, {Component} from 'react'; import './TextField.scss'; +import './common.scss'; import {randomClassName} from "../../utils"; const classNames = require('classnames'); @@ -28,11 +29,11 @@ class TextField extends Component { }; return ( - <div className={classNames("text-field", {"field-active": this.props.active}, + <div className={classNames("field", "text-field", {"field-active": this.props.active}, {"field-invalid": this.props.invalid}, {"field-small": this.props.small})}> {name && <label htmlFor={this.id}>{name}:</label>} <textarea id={this.id} placeholder={this.props.defaultValue} onChange={handler} rows={rows} - readOnly={this.props.readonly} value={this.props.value} /> + readOnly={this.props.readonly} value={this.props.value} ref={this.props.textRef} /> {error && <div className="field-error">error: {error}</div>} </div> ); diff --git a/frontend/src/components/fields/TextField.scss b/frontend/src/components/fields/TextField.scss index 606f537..de831fb 100644 --- a/frontend/src/components/fields/TextField.scss +++ b/frontend/src/components/fields/TextField.scss @@ -10,32 +10,7 @@ } textarea { - background-color: $color-primary-2; - width: 100%; - border: none; - color: $color-primary-4; - border-radius: 5px; - padding: 7px 10px; resize: none; - - &:focus { - background-color: $color-primary-1; - color: $color-primary-4; - box-shadow: none; - outline: none; - } - - &[readonly] { - background-color: $color-primary-2; - border: none; - color: $color-primary-4; - } - - &[readonly]:focus { - background-color: $color-primary-1; - color: $color-primary-4; - box-shadow: none; - } } &.field-active { diff --git a/frontend/src/components/fields/common.scss b/frontend/src/components/fields/common.scss new file mode 100644 index 0000000..f83a988 --- /dev/null +++ b/frontend/src/components/fields/common.scss @@ -0,0 +1,54 @@ +@import '../../colors.scss'; + +.field { + + input, textarea { + background-color: $color-primary-2; + width: 100%; + border: none; + color: $color-primary-4; + border-radius: 5px; + padding: 7px 10px; + + &:focus { + background-color: $color-primary-1; + color: $color-primary-4; + box-shadow: none; + outline: none; + } + + &[readonly] { + background-color: $color-primary-2; + border: none; + color: $color-primary-4; + } + + &[readonly]:focus { + background-color: $color-primary-1; + color: $color-primary-4; + box-shadow: none; + } + } + + button { + border-radius: 0; + background-color: $color-primary-2; + border: none; + color: $color-primary-4; + outline: none; + padding: 5px 12px; + font-weight: 500; + + &:hover, + &:active { + background-color: $color-primary-1; + color: $color-primary-4; + } + + &:focus, + &:active { + outline: none !important; + box-shadow: none !important; + } + } +} diff --git a/frontend/src/components/fields/extensions/ColorField.js b/frontend/src/components/fields/extensions/ColorField.js new file mode 100644 index 0000000..edcb720 --- /dev/null +++ b/frontend/src/components/fields/extensions/ColorField.js @@ -0,0 +1,63 @@ +import React, {Component} from 'react'; +import {Button, ButtonGroup, Form, OverlayTrigger, Popover} from "react-bootstrap"; +import './ColorField.scss'; +import InputField from "../InputField"; + +class ColorField extends Component { + + constructor(props) { + super(props); + + this.state = { + invalid: false + }; + + this.colors = ["#E53935", "#D81B60", "#8E24AA", "#5E35B1", "#3949AB", "#1E88E5", "#039BE5", "#00ACC1", + "#00897B", "#43A047", "#7CB342", "#9E9D24", "#F9A825", "#FB8C00", "#F4511E", "#6D4C41"]; + } + + render() { + const colorButtons = this.colors.map((color) => + <span key={"button" + color} className="color-input" + style={{"backgroundColor": color, "borderColor": this.state.color === color ? "#fff" : color}} + onClick={() => { + this.setState({color: color}); + if (typeof this.props.onChange === "function") { + this.props.onChange(color); + } + document.body.click(); // magic to close popup + }} />); + + const popover = ( + <Popover id="popover-basic"> + <Popover.Title as="h3">choose a color</Popover.Title> + <Popover.Content> + <div className="colors-container"> + <div className="colors-row"> + {colorButtons.slice(0, 8)} + </div> + <div className="colors-row"> + {colorButtons.slice(8, 18)} + </div> + </div> + </Popover.Content> + </Popover> + ); + + return ( + <div className="color-field"> + <InputField {...this.props} name="color" /> + + <div className="color-picker"> + <OverlayTrigger trigger="click" placement="top" overlay={popover} rootClose> + <Button variant="picker" size="sm">pick</Button> + </OverlayTrigger> + </div> + + </div> + ); + } + +} + +export default ColorField; diff --git a/frontend/src/components/fields/extensions/ColorField.scss b/frontend/src/components/fields/extensions/ColorField.scss new file mode 100644 index 0000000..c8f617c --- /dev/null +++ b/frontend/src/components/fields/extensions/ColorField.scss @@ -0,0 +1,39 @@ +@import '../../../colors.scss'; + +.color-field { + display: flex; + align-items: flex-end; + + .input-field { + flex: 1; + + input { + border-bottom-right-radius: 0 !important; + border-top-right-radius: 0 !important; + } + } + + .color-picker { + margin-bottom: 5.5px; + + .btn-picker { + padding: 8.5px 15px; + border-bottom-right-radius: 5px; + border-top-right-radius: 5px; + background-color: $color-indigo; + } + } + + +} + +.colors-container { + width: 600px; + + .color-input { + display: inline-block; + width: 31px; + height: 31px; + cursor: pointer; + } +}
\ No newline at end of file diff --git a/frontend/src/components/fields/extensions/NumericField.js b/frontend/src/components/fields/extensions/NumericField.js new file mode 100644 index 0000000..8823c42 --- /dev/null +++ b/frontend/src/components/fields/extensions/NumericField.js @@ -0,0 +1,39 @@ +import React, {Component} from 'react'; +import InputField from "../InputField"; + +class NumericField extends Component { + + constructor(props) { + super(props); + + this.state = { + invalid: false + }; + } + + render() { + const handler = (value) => { + value = value.replace(/[^\d]/gi, ''); + let intValue = 0; + if (value !== "") { + intValue = parseInt(value); + } + const valid = + (!this.props.validate || (typeof this.props.validate === "function" && this.props.validate(intValue))) && + (!this.props.min || (typeof this.props.min === "number" && intValue >= this.props.min)) && + (!this.props.max || (typeof this.props.max === "number" && intValue <= this.props.max)); + this.setState({invalid: !valid}); + if (this.props.onChange) { + this.props.onChange(intValue); + } + }; + + return ( + <InputField {...this.props} onChange={handler} initialValue={this.props.initialValue || 0} + invalid={this.state.invalid} /> + ); + } + +} + +export default NumericField; diff --git a/frontend/src/components/filters/RulesConnectionsFilter.js b/frontend/src/components/filters/RulesConnectionsFilter.js index 621b6d6..0741bea 100644 --- a/frontend/src/components/filters/RulesConnectionsFilter.js +++ b/frontend/src/components/filters/RulesConnectionsFilter.js @@ -24,7 +24,7 @@ class RulesConnectionsFilter extends Component { let params = new URLSearchParams(this.props.location.search); let activeRules = params.getAll("matched_rules") || []; - backend.get("/api/rules").then(res => { + backend.getJson("/api/rules").then(res => { let rules = res.flatMap(rule => rule.enabled ? [{id: rule.id, name: rule.name}] : []); activeRules = rules.filter(rule => activeRules.some(id => rule.id === id)); this.setState({rules, activeRules, mounted: true}); diff --git a/frontend/src/components/panels/PcapPane.js b/frontend/src/components/panels/PcapPane.js index 701edf2..7e0fa6c 100644 --- a/frontend/src/components/panels/PcapPane.js +++ b/frontend/src/components/panels/PcapPane.js @@ -3,7 +3,7 @@ import './PcapPane.scss'; import Table from "react-bootstrap/Table"; import backend from "../../backend"; import {createCurlCommand, formatSize, timestampToTime2} from "../../utils"; -import {Button, Col, Container, Form, Row} from "react-bootstrap"; +import {Button, Col, Container, Row} from "react-bootstrap"; import InputField from "../fields/InputField"; import CheckField from "../fields/CheckField"; import TextField from "../fields/TextField"; @@ -15,45 +15,48 @@ class PcapPane extends Component { this.state = { sessions: [], - isFileValid: true, - isFileFocused: false, - selectedFile: null, + isUploadFileValid: true, + isUploadFileFocused: false, + uploadSelectedFile: null, uploadFlushAll: false, uploadStatusCode: null, - uploadOutput: null + uploadOutput: null, + isFileValid: true, + isFileFocused: false, + fileValue: "", + fileFlushAll: false, + fileStatusCode: null, + fileOutput: null, + deleteOriginalFile: false }; - - this.loadSessions = this.loadSessions.bind(this); - this.handleFileChange = this.handleFileChange.bind(this); - this.handleUploadPcap = this.handleUploadPcap.bind(this); } componentDidMount() { this.loadSessions(); } - loadSessions() { - backend.get("/api/pcap/sessions").then(res => this.setState({sessions: res})); - } + loadSessions = () => { + backend.getJson("/api/pcap/sessions").then(res => this.setState({sessions: res})); + }; - handleFileChange(file) { + handleUploadFileChange = (file) => { this.setState({ - isFileValid: file != null && file.type.endsWith("pcap"), - isFileFocused: false, - selectedFile: file + isUploadFileValid: file != null && file.type.endsWith("pcap"), + isUploadFileFocused: false, + uploadSelectedFile: file }); - } + }; - handleUploadPcap() { - if (this.state.selectedFile == null || !this.state.isFileValid) { - this.setState({isFileFocused: true}); + handleUploadPcap = () => { + if (this.state.uploadSelectedFile == null || !this.state.isUploadFileValid) { + this.setState({isUploadFileFocused: true}); return; } const formData = new FormData(); formData.append( "file", - this.state.selectedFile + this.state.uploadSelectedFile ); backend.postFile("/api/pcap/upload", formData).then(response => @@ -62,7 +65,33 @@ class PcapPane extends Component { uploadOutput: JSON.stringify(result) })) ); - } + }; + + handleFileChange = (file) => { + this.setState({ + isFileValid: file !== "" && file.endsWith("pcap"), + isFileFocused: false, + fileValue: file + }); + }; + + handleProcessPcap = () => { + if (this.state.fileValue === "" || !this.state.isFileValid) { + this.setState({isFileFocused: true}); + return; + } + + backend.post("/api/pcap/file", { + file: this.state.fileValue, + flush_all: this.state.fileFlushAll, + delete_original_file: this.state.deleteOriginalFile + }).then(response => + response.json().then(result => this.setState({ + fileStatusCode: response.status + " " + response.statusText, + fileOutput: JSON.stringify(result) + })) + ); + }; render() { let sessions = this.state.sessions.map(s => @@ -82,7 +111,7 @@ class PcapPane extends Component { const uploadOutput = this.state.uploadOutput != null ? this.state.uploadOutput : createCurlCommand("pcap/upload", "POST", null, { - file: "@" + ((this.state.selectedFile != null && this.state.isFileValid) ? this.state.selectedFile.name : + file: "@" + ((this.state.uploadSelectedFile != null && this.state.isUploadFileValid) ? this.state.uploadSelectedFile.name : "invalid.pcap"), flush_all: this.state.uploadFlushAll }) @@ -120,17 +149,17 @@ class PcapPane extends Component { <div className="pane-section"> <Container className="p-0"> <Row> - <Col> + <Col style={{"paddingRight": "0"}}> <div className="section-header"> <span className="api-request">POST /api/pcap/upload</span> <span className="api-response">{this.state.uploadStatusCode}</span> </div> <div className="section-content"> - <InputField type={"file"} name={"file"} invalid={!this.state.isFileValid} - active={this.state.isFileFocused} - onChange={this.handleFileChange} value={this.state.selectedFile} - defaultValue={"No .pcap[ng] selected"}/> + <InputField type={"file"} name={"file"} invalid={!this.state.isUploadFileValid} + active={this.state.isUploadFileFocused} + onChange={this.handleUploadFileChange} value={this.state.uploadSelectedFile} + defaultValue={"no .pcap[ng] selected"}/> <div className="upload-actions"> <div className="upload-options"> @@ -148,24 +177,31 @@ class PcapPane extends Component { <Col> <div className="section-header"> <span className="api-request">POST /api/pcap/file</span> - <span className="api-response"></span> + <span className="api-response">{this.state.fileStatusCode}</span> </div> <div className="section-content"> - <Form.Control type="text" id="pcap-upload" className="custom-file" - onChange={this.onLocalFileChange} placeholder="local .pcap/.pcapng" - custom - /> + <InputField name="file" active={this.state.isUploadFileFocused} + onChange={this.handleFileChange} value={this.state.uploadSelectedFile} + defaultValue={"local .pcap[ng] path"} inline/> + + <div className="upload-actions" style={{"marginTop": "11px"}}> + <div className="upload-options"> + <CheckField name="flush_all" checked={this.state.uploadFlushAll} + onChange={v => this.setState({uploadFlushAll: v})}/> + <CheckField name="delete_original_file" checked={this.state.uploadFlushAll} + onChange={v => this.setState({uploadFlushAll: v})}/> + </div> + <Button variant="blue" onClick={this.handleUploadPcap}>process</Button> + </div> + + <TextField value={uploadOutput} rows={4} readonly small={true}/> </div> </Col> </Row> </Container> - - </div> - </div> - ); } } diff --git a/frontend/src/components/panels/RulePane.js b/frontend/src/components/panels/RulePane.js new file mode 100644 index 0000000..fbc8785 --- /dev/null +++ b/frontend/src/components/panels/RulePane.js @@ -0,0 +1,166 @@ +import React, {Component} from 'react'; +import './RulePane.scss'; +import Table from "react-bootstrap/Table"; +import {Button, Col, Container, Row} from "react-bootstrap"; +import InputField from "../fields/InputField"; +import CheckField from "../fields/CheckField"; +import TextField from "../fields/TextField"; +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"; + +class RulePane extends Component { + + constructor(props) { + super(props); + + this.state = { + rules: [], + }; + } + + componentDidMount() { + this.loadRules(); + } + + loadRules = () => { + backend.getJson("/api/rules").then(res => this.setState({rules: res})); + }; + + + render() { + let rules = this.state.rules.map(r => + <tr className="table-row"> + <td>{r["id"].substring(0, 8)}</td> + <td>{r["name"]}</td> + <td>{r["notes"]}</td> + {/*<td>{((new Date(s["completed_at"]) - new Date(s["started_at"])) / 1000).toFixed(3)}s</td>*/} + {/*<td>{formatSize(s["size"])}</td>*/} + {/*<td>{s["processed_packets"]}</td>*/} + {/*<td>{s["invalid_packets"]}</td>*/} + {/*<td>undefined</td>*/} + {/*<td className="table-cell-action"><a target="_blank"*/} + {/* href={"/api/pcap/sessions/" + s["id"] + "/download"}>download</a>*/} + {/*</td>*/} + </tr> + ); + + return ( + <div className="pane-container rule-pane"> + <div className="pane-section"> + <div className="section-header"> + <span className="api-request">GET /api/rules</span> + <span className="api-response">200 OK</span> + </div> + + <div className="section-table"> + <Table borderless size="sm"> + <thead> + <tr> + <th>id</th> + <th>name</th> + <th>notes</th> + </tr> + </thead> + <tbody> + {rules} + </tbody> + </Table> + </div> + </div> + + <div className="pane-section"> + <div className="section-header"> + <span className="api-request">POST /api/rules</span> + <span className="api-response"></span> + </div> + + <div className="section-content"> + <Container className="p-0"> + <Row> + <Col> + <InputField name="name" inline /> + <ColorField value={this.state.test1} onChange={(e) => this.setState({test1: e})} inline /> + <TextField name="notes" rows={2} /> + </Col> + + <Col> + <div >filters:</div> + <NumericField name="service_port" inline value={this.state.test} onChange={(e) => this.setState({test: e})} validate={(e) => e%2 === 0} /> + + <NumericField name="client_port" inline /> + <InputField name="client_address" /> + </Col> + + <Col> + <NumericField name="min_duration" inline /> + <NumericField name="max_duration" inline /> + <NumericField name="min_bytes" inline /> + <NumericField name="max_bytes" inline /> + + </Col> + </Row> + </Container> + + <div className="post-rules-actions"> + <label>options:</label> + <div className="rules-options"> + <CheckField name={"enabled"} /> + </div> + + <ButtonField variant="blue" name="clear" bordered /> + <ButtonField variant="green" name="add_rule" bordered /> + </div> + + patterns: + <div className="section-table"> + <Table borderless size="sm"> + <thead> + <tr> + <th>regex</th> + <th>Aa</th> + <th>.*</th> + <th>\n+</th> + <th>UTF8</th> + <th>Uni_</th> + <th>min</th> + <th>max</th> + <th>direction</th> + <th>actions</th> + </tr> + </thead> + <tbody> + <tr> + <td style={{"width": "500px"}}><InputField small /></td> + <td><CheckField small /></td> + <td><CheckField small /></td> + <td><CheckField small /></td> + <td><CheckField small /></td> + <td><CheckField small /></td> + <td style={{"width": "70px"}}><NumericField small /></td> + <td style={{"width": "70px"}}><NumericField small /></td> + <td><ChoiceField small keys={[0, 1, 2]} values={["both", "c->s", "s->c"]} value="both" /></td> + <td><Button variant="green" size="sm">add</Button></td> + </tr> + </tbody> + </Table> + </div> + + <ButtonField name="add_rule" variant="green" bordered /> + <br /> + <ButtonField name="add_rule" small color="red"/> + <br /> + <ButtonField name="add_rule" bordered border={"green"} /> + </div> + </div> + + + </div> + ); + } + +} + +export default RulePane; diff --git a/frontend/src/components/panels/RulePane.scss b/frontend/src/components/panels/RulePane.scss new file mode 100644 index 0000000..b030c6a --- /dev/null +++ b/frontend/src/components/panels/RulePane.scss @@ -0,0 +1,16 @@ + +.rule-pane { + .post-rules-actions { + display: flex; + + .rules-options { + flex: 1; + } + + button { + margin-left: 10px; + } + } + + +}
\ No newline at end of file diff --git a/frontend/src/index.scss b/frontend/src/index.scss index 5d1bbfa..9dcc692 100644 --- a/frontend/src/index.scss +++ b/frontend/src/index.scss @@ -21,118 +21,6 @@ pre { font-size: 14px; } -.btn { - border-radius: 0; - background-color: $color-primary-2; - border: none; - border-bottom: 5px solid $color-primary-1; - color: $color-primary-4; - outline: none; - padding: 5px 12px; - font-weight: 500; - - &:hover, - &:active { - background-color: $color-primary-1; - color: $color-primary-4; - } - - &:focus, - &:active { - outline: none !important; - box-shadow: none !important; - } -} - -.btn-sm { - border: none; - font-size: 12px; -} - -.btn-red { - color: $color-red-light; - background-color: $color-red; - border-bottom: 5px solid $color-red-dark; - - &:hover, - &:active { - color: $color-red-light; - background-color: $color-red-dark; - } -} - -.btn-pink { - color: $color-pink-light; - background-color: $color-pink; - border-bottom: 5px solid $color-pink-dark; - - &:hover, - &:active { - color: $color-pink-light; - background-color: $color-pink-dark; - } -} - -.btn-purple { - color: $color-purple-light; - background-color: $color-purple; - border-bottom: 5px solid $color-purple-dark; - - &:hover, - &:active { - color: $color-purple-light; - background-color: $color-purple-dark; - } -} - -.btn-deep-purple { - color: $color-deep-purple-light; - background-color: $color-deep-purple; - border-bottom: 5px solid $color-deep-purple-dark; - - &:hover, - &:active { - color: $color-deep-purple-light; - background-color: $color-deep-purple-dark; - } -} - -.btn-indigo { - color: $color-indigo-light; - background-color: $color-indigo; - border-bottom: 5px solid $color-indigo-dark; - - &:hover, - &:active { - color: $color-indigo-light; - background-color: $color-indigo-dark; - } -} - -.btn-blue { - color: $color-blue-light; - background-color: $color-blue; - border-bottom: 5px solid $color-blue-dark; - - &:hover, - &:active { - color: $color-blue-light; - background-color: $color-blue-dark; - } -} - -.btn-green { - color: $color-green-light; - background-color: $color-green; - border-bottom: 5px solid $color-green-dark; - - &:hover, - &:active { - color: $color-green-light; - background-color: $color-green-dark; - } -} - a { color: $color-primary-4; @@ -190,3 +78,7 @@ textarea.form-control { .text-muted { color: $color-primary-4 !important; } + +.popover-header { + color: $color-primary-1; +}
\ No newline at end of file diff --git a/frontend/src/views/Connections.js b/frontend/src/views/Connections.js index da8958b..f3fec64 100644 --- a/frontend/src/views/Connections.js +++ b/frontend/src/views/Connections.js @@ -25,21 +25,21 @@ class Connections extends Component { this.scrollBottomThreashold = 0.99999; this.maxConnections = 500; this.queryLimit = 50; - - this.handleScroll = this.handleScroll.bind(this); - this.connectionSelected = this.connectionSelected.bind(this); - this.addServicePortFilter = this.addServicePortFilter.bind(this); } componentDidMount() { this.loadConnections({limit: this.queryLimit}) .then(() => this.setState({loaded: true})); + if (this.props.initialConnection != null) { + this.setState({selected: this.props.initialConnection.id}); + // TODO: scroll to initial connection + } } - connectionSelected(c) { + connectionSelected = (c) => { this.setState({selected: c.id}); this.props.onSelected(c); - } + }; componentDidUpdate(prevProps, prevState, snapshot) { if (this.state.loaded && prevProps.location.search !== this.props.location.search) { @@ -49,7 +49,7 @@ class Connections extends Component { } } - handleScroll(e) { + handleScroll = (e) => { 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,}) @@ -59,13 +59,13 @@ class Connections extends Component { this.loadConnections({to: this.state.firstConnection.id, limit: this.queryLimit,}) .then(() => console.log("Previous connections loaded")); } - } + }; - addServicePortFilter(port) { + 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"; @@ -75,7 +75,7 @@ class Connections extends Component { } this.setState({loading: true, prevParams: params}); - let res = await backend.get(`${url}?${urlParams}`); + let res = await backend.getJson(`${url}?${urlParams}`); let connections = this.state.connections; let firstConnection = this.state.firstConnection; @@ -115,7 +115,7 @@ class Connections extends Component { let flagRule = this.state.flagRule; let rules = this.state.rules; if (flagRule === null) { - rules = await backend.get("/api/rules"); + rules = await backend.getJson("/api/rules"); flagRule = rules.filter(rule => { return rule.name === "flag"; })[0]; diff --git a/frontend/src/views/Header.js b/frontend/src/views/Header.js index e2d0e6a..a022636 100644 --- a/frontend/src/views/Header.js +++ b/frontend/src/views/Header.js @@ -72,7 +72,9 @@ class Header extends Component { <Link to="/pcaps"> <Button variant="purple">pcaps</Button> </Link> - <Button variant="deep-purple" onClick={this.props.onOpenRules}>rules</Button> + <Link to="/rules"> + <Button variant="deep-purple">rules</Button> + </Link> <Button variant="indigo" onClick={this.props.onOpenServices}>services</Button> <Button variant="blue" onClick={this.props.onOpenConfig} disabled={false}>config</Button> diff --git a/frontend/src/views/MainPane.js b/frontend/src/views/MainPane.js index ce755d1..9d3f7b7 100644 --- a/frontend/src/views/MainPane.js +++ b/frontend/src/views/MainPane.js @@ -5,24 +5,25 @@ 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"; class MainPane extends Component { constructor(props) { super(props); this.state = { - selectedConnection: null + selectedConnection: null, + loading: false }; } componentDidMount() { - if ('id' in this.props.match.params) { - const id = this.props.match.params.id; - backend.get(`/api/connections/${id}`).then(res => { - if (res.status === 200) { - this.setState({selectedConnection: res}); - } - }); + const match = this.props.location.pathname.match(/^\/connections\/([a-f0-9]{24})$/); + if (match != null) { + this.setState({loading: true}); + backend.getJson(`/api/connections/${match[1]}`) + .then(connection => this.setState({selectedConnection: connection, loading: false})) + .catch(error => console.log(error)); } } @@ -32,12 +33,18 @@ class MainPane extends Component { <div className="container-fluid"> <div className="row"> <div className="col-md-6 pane"> - <Connections onSelected={(c) => this.setState({selectedConnection: c})} /> + { + !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 children={<ConnectionContent connection={this.state.selectedConnection} />} /> + <Route path="/rules" children={<RulePane />} /> + <Route exact path="/connections/:id" children={<ConnectionContent connection={this.state.selectedConnection} />} /> + <Route children={<ConnectionContent />} /> </Switch> </div> </div> |