diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e3ec488..fe50efd43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - The ability to download the Monkey Island logs from the Infection Map page. #1640 ### Changed +- Reset workflow. Now it's possible to delete data gathered by agents without + resetting the configuration and reset procedure requires fewer clicks. #957 - "Communicate as Backdoor User" PBA's HTTP requests to request headers only and include a timeout. #1577 - The setup procedure for custom server_config.json files to be simpler. #1576 diff --git a/docs/content/usage/scenarios/_index.md b/docs/content/usage/scenarios/_index.md index dedaf554c..36a15e44a 100644 --- a/docs/content/usage/scenarios/_index.md +++ b/docs/content/usage/scenarios/_index.md @@ -20,9 +20,8 @@ Choosing the "Custom" scenario will allow you to fine-tune your simulation and a ![Choose scenario](/images/usage/scenarios/choose-scenario.png "Choose a scenario") -To exit a scenario and select another one, click on "Start Over". - -![Start over](/images/usage/scenarios/start-over.png "Start over") +To exit a scenario and select another one, click on "Reset". +![Reset](/images/usage/scenarios/reset.jpg "Reset") ## Section contents diff --git a/docs/static/images/usage/scenarios/reset.jpg b/docs/static/images/usage/scenarios/reset.jpg new file mode 100644 index 000000000..092382524 Binary files /dev/null and b/docs/static/images/usage/scenarios/reset.jpg differ diff --git a/docs/static/images/usage/scenarios/start-over.png b/docs/static/images/usage/scenarios/start-over.png deleted file mode 100644 index 60deecfa1..000000000 Binary files a/docs/static/images/usage/scenarios/start-over.png and /dev/null differ diff --git a/monkey/monkey_island/cc/models/config.py b/monkey/monkey_island/cc/models/config.py index db5fd9e94..0e25b9ee3 100644 --- a/monkey/monkey_island/cc/models/config.py +++ b/monkey/monkey_island/cc/models/config.py @@ -2,6 +2,9 @@ from mongoengine import BooleanField, EmbeddedDocument class Config(EmbeddedDocument): + + COLLECTION_NAME = "config" + """ No need to define this schema here. It will change often and is already is defined in monkey_island.cc.services.config_schema. diff --git a/monkey/monkey_island/cc/models/island_mode_model.py b/monkey/monkey_island/cc/models/island_mode_model.py index dec93e501..8d6aab74a 100644 --- a/monkey/monkey_island/cc/models/island_mode_model.py +++ b/monkey/monkey_island/cc/models/island_mode_model.py @@ -2,4 +2,6 @@ from mongoengine import Document, StringField class IslandMode(Document): + COLLECTION_NAME = "island_mode" + mode = StringField() diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py index d3a36e6a2..aa2087913 100644 --- a/monkey/monkey_island/cc/resources/root.py +++ b/monkey/monkey_island/cc/resources/root.py @@ -19,6 +19,8 @@ class Root(flask_restful.Resource): if not action: return self.get_server_info() + elif action == "delete-agent-data": + return jwt_required(Database.reset_db)(reset_config=False) elif action == "reset": return jwt_required(Database.reset_db)() elif action == "is-up": diff --git a/monkey/monkey_island/cc/services/database.py b/monkey/monkey_island/cc/services/database.py index 7aeb1bfcf..46b5e0ffd 100644 --- a/monkey/monkey_island/cc/services/database.py +++ b/monkey/monkey_island/cc/services/database.py @@ -3,8 +3,10 @@ import logging from flask import jsonify from monkey_island.cc.database import mongo +from monkey_island.cc.models import Config from monkey_island.cc.models.agent_controls import AgentControls from monkey_island.cc.models.attack.attack_mitigations import AttackMitigations +from monkey_island.cc.models.island_mode_model import IslandMode from monkey_island.cc.services.config import ConfigService logger = logging.getLogger(__name__) @@ -15,19 +17,29 @@ class Database(object): pass @staticmethod - def reset_db(): + def reset_db(reset_config=True): logger.info("Resetting database") # We can't drop system collections. [ Database.drop_collection(x) for x in mongo.db.collection_names() - if not x.startswith("system.") and not x == AttackMitigations.COLLECTION_NAME + if Database._should_drop(x, reset_config) ] ConfigService.init_config() Database.init_agent_controls() logger.info("DB was reset") return jsonify(status="OK") + @staticmethod + def _should_drop(collection: str, drop_config: bool) -> bool: + if not drop_config: + if collection == IslandMode.COLLECTION_NAME or collection == Config.COLLECTION_NAME: + return False + return ( + not collection.startswith("system.") + and not collection == AttackMitigations.COLLECTION_NAME + ) + @staticmethod def drop_collection(collection_name: str): mongo.db[collection_name].drop() diff --git a/monkey/monkey_island/cc/ui/src/components/Main.tsx b/monkey/monkey_island/cc/ui/src/components/Main.tsx index d9dda8e4f..e28013df2 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.tsx +++ b/monkey/monkey_island/cc/ui/src/components/Main.tsx @@ -6,7 +6,6 @@ import ConfigurePage from './pages/ConfigurePage.js'; import RunMonkeyPage from './pages/RunMonkeyPage/RunMonkeyPage'; import MapPage from './pages/MapPage'; import TelemetryPage from './pages/TelemetryPage'; -import StartOverPage from './pages/StartOverPage'; import ReportPage from './pages/ReportPage'; import LicensePage from './pages/LicensePage'; import AuthComponent from './AuthComponent'; @@ -47,7 +46,6 @@ export const Routes = { RunMonkeyPage: '/run-monkey', MapPage: '/infection/map', TelemetryPage: '/infection/telemetry', - StartOverPage: '/start-over', LicensePage: '/license' } @@ -232,8 +230,6 @@ class AppComponent extends AuthComponent { )} {this.renderRoute(Routes.TelemetryPage, )} - {this.renderRoute(Routes.StartOverPage, - )} {this.redirectToReport()} {this.renderRoute(Routes.SecurityReport, void } -const SideNavComponent = ({disabled, - completedSteps, - defaultReport, - header=null}: Props) => { +const SideNavComponent = ({ + disabled, + completedSteps, + defaultReport, + header = null, + onStatusChange, + }: Props) => { + + const [showResetModal, setShowResetModal] = useState(false); return ( <> @@ -37,12 +44,12 @@ const SideNavComponent = ({disabled,
    {(header !== null) && - <> -
  • - {header} -
  • -
    - } + <> +
  • + {header} +
  • +
    + }
  • @@ -76,10 +83,17 @@ const SideNavComponent = ({disabled,
  • - + + { + setShowResetModal(false); + onStatusChange(); + }}/>
@@ -90,7 +104,7 @@ const SideNavComponent = ({disabled, Configuration
  • + className={getNavLinkClass()}> Telemetries
  • @@ -98,8 +112,12 @@ const SideNavComponent = ({disabled, ); + function areMonkeysDead() { + return (!completedSteps['runMonkey']) || (completedSteps['infectionDone']) + } + function getNavLinkClass() { - if(disabled){ + if (disabled) { return `nav-link disabled` } else { return '' diff --git a/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx b/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx index c8bac4396..55f22d8b6 100644 --- a/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx +++ b/monkey/monkey_island/cc/ui/src/components/layouts/SidebarLayoutComponent.tsx @@ -9,6 +9,7 @@ const SidebarLayoutComponent = ({component: Component, completedSteps = null, defaultReport = '', sideNavHeader = (<>), + onStatusChange = () => {}, ...other }) => ( { @@ -18,9 +19,10 @@ const SidebarLayoutComponent = ({component: Component, + header={sideNavHeader} + onStatusChange={onStatusChange}/> } - + ) }}/> ) diff --git a/monkey/monkey_island/cc/ui/src/components/pages/LandingPage.tsx b/monkey/monkey_island/cc/ui/src/components/pages/LandingPage.tsx index cc1741b20..d95a84dfe 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/LandingPage.tsx +++ b/monkey/monkey_island/cc/ui/src/components/pages/LandingPage.tsx @@ -12,8 +12,11 @@ import Logo from "../logo/LogoComponent"; const monkeyIcon = require('../../images/monkey-icon.svg') const infectionMonkey = require('../../images/infection-monkey.svg') -const LandingPageComponent = (props) => { +type Props = { + onStatusChange: () => void +} +const LandingPageComponent = (props: Props) => { return ( <> diff --git a/monkey/monkey_island/cc/ui/src/components/pages/StartOverPage.js b/monkey/monkey_island/cc/ui/src/components/pages/StartOverPage.js deleted file mode 100644 index 84b84d6f7..000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/StartOverPage.js +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import {Col, Button} from 'react-bootstrap'; -import {Link} from 'react-router-dom'; -import AuthComponent from '../AuthComponent'; -import StartOverModal from '../ui-components/StartOverModal'; -import '../../styles/pages/StartOverPage.scss'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle'; -import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; - -class StartOverPageComponent extends AuthComponent { - constructor(props) { - super(props); - - this.state = { - cleaned: false, - showCleanDialog: false, - allMonkeysAreDead: false - }; - - this.cleanup = this.cleanup.bind(this); - this.closeModal = this.closeModal.bind(this); - } - - updateMonkeysRunning = () => { - this.authFetch('/api') - .then(res => res.json()) - .then(res => { - // This check is used to prevent unnecessary re-rendering - this.setState({ - allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']) - }); - }); - }; - - render() { - return ( - - -

    Start Over

    -
    -

    - If you are finished and want to start over with a fresh configuration, erase the logs and clear the map - you can go ahead and -

    -

    - -

    -
    - - You don't have to reset the environment to keep running monkeys. - You can continue and Run More Monkeys as you wish, - and see the results on the Infection Map without deleting anything. -
    - {this.state.cleaned ? -
    - - Environment was reset successfully -
    - : ''} -
    - - ); - } - - cleanup = () => { - this.setState({ - cleaned: false - }); - return this.authFetch('/api?action=reset') - .then(res => res.json()) - .then(res => { - if (res['status'] === 'OK') { - this.setState({ - cleaned: true - }); - } - }).then(() => { - this.updateMonkeysRunning(); - this.props.onStatusChange(); - }); - }; - - closeModal = () => { - this.setState({ - showCleanDialog: false - }) - }; -} - -export default StartOverPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/IslandResetModal.tsx b/monkey/monkey_island/cc/ui/src/components/ui-components/IslandResetModal.tsx new file mode 100644 index 000000000..15281362f --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/IslandResetModal.tsx @@ -0,0 +1,130 @@ +import {Button, Col, Container, Modal, NavLink, Row} from 'react-bootstrap'; +import React, {useState} from 'react'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle'; + +import '../../styles/components/IslandResetModal.scss'; +import {Routes} from '../Main'; +import LoadingIcon from './LoadingIcon'; +import {faCheck} from '@fortawesome/free-solid-svg-icons'; +import AuthService from '../../services/AuthService'; + +type Props = { + show: boolean, + allMonkeysAreDead: boolean, + onClose: () => void +} + +// Button statuses +const Idle = 1; +const Loading = 2; +const Done = 3; + +const IslandResetModal = (props: Props) => { + + const [resetAllStatus, setResetAll] = useState(Idle); + const [deleteStatus, setDeleteStatus] = useState(Idle); + const auth = new AuthService(); + + return ( + { + setDeleteStatus(Idle); + props.onClose() + }} size={'lg'}> + + Reset the Island + + + { + !props.allMonkeysAreDead ? +
    + + Please stop all running agents before attempting to reset the Island. +
    + : + showModalButtons() + } +
    +
    + ) + + function displayDeleteData() { + if (deleteStatus === Idle) { + return ( + + ) + } else if (deleteStatus === Loading) { + return () + } else if (deleteStatus === Done) { + return () + } + } + + function displayResetAll() { + if (resetAllStatus === Idle) { + return ( + + ) + } else if (resetAllStatus === Loading) { + return () + } else if (resetAllStatus === Done) { + return () + } + } + + function resetIsland(url: string, callback: () => void) { + auth.authFetch(url) + .then(res => res.json()) + .then(res => { + if (res['status'] === 'OK') { + callback() + } + }) + } + + function showModalButtons() { + return ( + + +

    Delete data gathered by Monkey agents.

    +

    This will reset the Map and reports.

    + + + {displayDeleteData()} + +
    +
    + + +

    Reset everything.

    +

    You might want to before doing this.

    + + + {displayResetAll()} + +
    +
    ) + } +} + +export default IslandResetModal; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/StartOverModal.js b/monkey/monkey_island/cc/ui/src/components/ui-components/StartOverModal.js deleted file mode 100644 index 0718fdaa6..000000000 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/StartOverModal.js +++ /dev/null @@ -1,76 +0,0 @@ -import {Modal} from 'react-bootstrap'; -import React from 'react'; -import {GridLoader} from 'react-spinners'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle'; - - -class StartOverModal extends React.PureComponent { - - constructor(props) { - super(props); - - this.state = { - showCleanDialog: this.props.showCleanDialog, - allMonkeysAreDead: this.props.allMonkeysAreDead, - loading: false - }; - } - - componentDidUpdate(prevProps) { - if (this.props !== prevProps) { - this.setState({ showCleanDialog: this.props.showCleanDialog, - allMonkeysAreDead: this.props.allMonkeysAreDead}) - } - } - - render = () => { - return ( - this.props.onClose()}> - -

    -
    Reset environment
    -

    -

    - Are you sure you want to reset the environment? -

    - { - !this.state.allMonkeysAreDead ? -
    - - Some monkeys are still running. It's advised to kill all monkeys before resetting. -
    - : -
    - } - { - this.state.loading ?
    : this.showModalButtons() - } - - - ) - }; - - showModalButtons() { - return (
    - - -
    ) - } - - modalVerificationOnClick = async () => { - this.setState({loading: true}); - this.props.onVerify() - .then(() => {this.setState({loading: false}); - this.props.onClose();}) - - } -} - -export default StartOverModal; diff --git a/monkey/monkey_island/cc/ui/src/styles/components/IslandResetModal.scss b/monkey/monkey_island/cc/ui/src/styles/components/IslandResetModal.scss new file mode 100644 index 000000000..2c1c71110 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/IslandResetModal.scss @@ -0,0 +1,9 @@ +.island-reset-modal p { + font-size: 16.5px; + margin: 0; + padding-left: 5px; +} + +.island-reset-modal a { + padding: 0 0 4px 0; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss b/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss index 949191d13..078b7daa2 100644 --- a/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss +++ b/monkey/monkey_island/cc/ui/src/styles/components/SideNav.scss @@ -1,6 +1,13 @@ .sidebar .version-text { position: relative; } + .sidebar .license-link { position: relative; } + +.navigation .island-reset-button { + width: 100%; + text-align: left; + padding-left: 18px; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/pages/StartOverPage.scss b/monkey/monkey_island/cc/ui/src/styles/pages/StartOverPage.scss deleted file mode 100644 index ee4ab65ea..000000000 --- a/monkey/monkey_island/cc/ui/src/styles/pages/StartOverPage.scss +++ /dev/null @@ -1,9 +0,0 @@ -$yellow: #ffcc00; - -.modalLoader div{ - margin-left: auto; - margin-right: auto; -} -.modalLoader div>div{ - background-color: $yellow; -}