Merge pull request #1909 from guardicore/957-island-reset-improvements

957 island reset improvements
This commit is contained in:
VakarisZ 2022-04-22 10:48:28 +03:00 committed by GitHub
commit b9efc2d552
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 218 additions and 222 deletions

View File

@ -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 - The ability to download the Monkey Island logs from the Infection Map page. #1640
### Changed ### 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 - "Communicate as Backdoor User" PBA's HTTP requests to request headers only and
include a timeout. #1577 include a timeout. #1577
- The setup procedure for custom server_config.json files to be simpler. #1576 - The setup procedure for custom server_config.json files to be simpler. #1576

View File

@ -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") ![Choose scenario](/images/usage/scenarios/choose-scenario.png "Choose a scenario")
To exit a scenario and select another one, click on "Start Over". To exit a scenario and select another one, click on "Reset".
![Reset](/images/usage/scenarios/reset.jpg "Reset")
![Start over](/images/usage/scenarios/start-over.png "Start over")
## Section contents ## Section contents

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

View File

@ -2,6 +2,9 @@ from mongoengine import BooleanField, EmbeddedDocument
class Config(EmbeddedDocument): class Config(EmbeddedDocument):
COLLECTION_NAME = "config"
""" """
No need to define this schema here. It will change often and is already is defined in No need to define this schema here. It will change often and is already is defined in
monkey_island.cc.services.config_schema. monkey_island.cc.services.config_schema.

View File

@ -2,4 +2,6 @@ from mongoengine import Document, StringField
class IslandMode(Document): class IslandMode(Document):
COLLECTION_NAME = "island_mode"
mode = StringField() mode = StringField()

View File

@ -19,6 +19,8 @@ class Root(flask_restful.Resource):
if not action: if not action:
return self.get_server_info() return self.get_server_info()
elif action == "delete-agent-data":
return jwt_required(Database.reset_db)(reset_config=False)
elif action == "reset": elif action == "reset":
return jwt_required(Database.reset_db)() return jwt_required(Database.reset_db)()
elif action == "is-up": elif action == "is-up":

View File

@ -3,8 +3,10 @@ import logging
from flask import jsonify from flask import jsonify
from monkey_island.cc.database import mongo 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.agent_controls import AgentControls
from monkey_island.cc.models.attack.attack_mitigations import AttackMitigations 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 from monkey_island.cc.services.config import ConfigService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,19 +17,29 @@ class Database(object):
pass pass
@staticmethod @staticmethod
def reset_db(): def reset_db(reset_config=True):
logger.info("Resetting database") logger.info("Resetting database")
# We can't drop system collections. # We can't drop system collections.
[ [
Database.drop_collection(x) Database.drop_collection(x)
for x in mongo.db.collection_names() 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() ConfigService.init_config()
Database.init_agent_controls() Database.init_agent_controls()
logger.info("DB was reset") logger.info("DB was reset")
return jsonify(status="OK") 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 @staticmethod
def drop_collection(collection_name: str): def drop_collection(collection_name: str):
mongo.db[collection_name].drop() mongo.db[collection_name].drop()

View File

@ -6,7 +6,6 @@ import ConfigurePage from './pages/ConfigurePage.js';
import RunMonkeyPage from './pages/RunMonkeyPage/RunMonkeyPage'; import RunMonkeyPage from './pages/RunMonkeyPage/RunMonkeyPage';
import MapPage from './pages/MapPage'; import MapPage from './pages/MapPage';
import TelemetryPage from './pages/TelemetryPage'; import TelemetryPage from './pages/TelemetryPage';
import StartOverPage from './pages/StartOverPage';
import ReportPage from './pages/ReportPage'; import ReportPage from './pages/ReportPage';
import LicensePage from './pages/LicensePage'; import LicensePage from './pages/LicensePage';
import AuthComponent from './AuthComponent'; import AuthComponent from './AuthComponent';
@ -47,7 +46,6 @@ export const Routes = {
RunMonkeyPage: '/run-monkey', RunMonkeyPage: '/run-monkey',
MapPage: '/infection/map', MapPage: '/infection/map',
TelemetryPage: '/infection/telemetry', TelemetryPage: '/infection/telemetry',
StartOverPage: '/start-over',
LicensePage: '/license' LicensePage: '/license'
} }
@ -232,8 +230,6 @@ class AppComponent extends AuthComponent {
<SidebarLayoutComponent component={MapPage} {...defaultSideNavProps}/>)} <SidebarLayoutComponent component={MapPage} {...defaultSideNavProps}/>)}
{this.renderRoute(Routes.TelemetryPage, {this.renderRoute(Routes.TelemetryPage,
<SidebarLayoutComponent component={TelemetryPage} {...defaultSideNavProps}/>)} <SidebarLayoutComponent component={TelemetryPage} {...defaultSideNavProps}/>)}
{this.renderRoute(Routes.StartOverPage,
<SidebarLayoutComponent component={StartOverPage} {...defaultSideNavProps}/>)}
{this.redirectToReport()} {this.redirectToReport()}
{this.renderRoute(Routes.SecurityReport, {this.renderRoute(Routes.SecurityReport,
<SidebarLayoutComponent component={ReportPage} <SidebarLayoutComponent component={ReportPage}

View File

@ -1,30 +1,37 @@
import React, {ReactFragment} from 'react'; import React, {ReactFragment, useState} from 'react';
import {Button} from 'react-bootstrap';
import {NavLink} from 'react-router-dom'; import {NavLink} from 'react-router-dom';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
import {faUndo} from '@fortawesome/free-solid-svg-icons/faUndo'; import {faUndo} from '@fortawesome/free-solid-svg-icons/faUndo';
import '../styles/components/SideNav.scss'; import '../styles/components/SideNav.scss';
import {CompletedSteps} from "./side-menu/CompletedSteps"; import {CompletedSteps} from './side-menu/CompletedSteps';
import {isReportRoute, Routes} from "./Main"; import {isReportRoute, Routes} from './Main';
import Logo from './logo/LogoComponent';
import IslandResetModal from './ui-components/IslandResetModal';
const logoImage = require('../images/monkey-icon.svg'); const logoImage = require('../images/monkey-icon.svg');
const infectionMonkeyImage = require('../images/infection-monkey.svg'); const infectionMonkeyImage = require('../images/infection-monkey.svg');
import Logo from "./logo/LogoComponent";
type Props = { type Props = {
disabled?: boolean, disabled?: boolean,
completedSteps: CompletedSteps, completedSteps: CompletedSteps,
defaultReport: string, defaultReport: string,
header?: ReactFragment header?: ReactFragment,
onStatusChange: () => void
} }
const SideNavComponent = ({disabled, const SideNavComponent = ({
disabled,
completedSteps, completedSteps,
defaultReport, defaultReport,
header=null}: Props) => { header = null,
onStatusChange,
}: Props) => {
const [showResetModal, setShowResetModal] = useState(false);
return ( return (
<> <>
@ -76,10 +83,17 @@ const SideNavComponent = ({disabled,
</NavLink> </NavLink>
</li> </li>
<li> <li>
<NavLink to={Routes.StartOverPage} className={getNavLinkClass()}> <Button variant={null} className={'island-reset-button'}
onClick={() => setShowResetModal(true)} href='#'>
<span className='number'><FontAwesomeIcon icon={faUndo} style={{'marginLeft': '-1px'}}/></span> <span className='number'><FontAwesomeIcon icon={faUndo} style={{'marginLeft': '-1px'}}/></span>
Start Over Reset
</NavLink> </Button>
<IslandResetModal show={showResetModal}
allMonkeysAreDead={areMonkeysDead()}
onClose={() => {
setShowResetModal(false);
onStatusChange();
}}/>
</li> </li>
</ul> </ul>
@ -98,6 +112,10 @@ const SideNavComponent = ({disabled,
<Logo/> <Logo/>
</>); </>);
function areMonkeysDead() {
return (!completedSteps['runMonkey']) || (completedSteps['infectionDone'])
}
function getNavLinkClass() { function getNavLinkClass() {
if (disabled) { if (disabled) {
return `nav-link disabled` return `nav-link disabled`

View File

@ -9,6 +9,7 @@ const SidebarLayoutComponent = ({component: Component,
completedSteps = null, completedSteps = null,
defaultReport = '', defaultReport = '',
sideNavHeader = (<></>), sideNavHeader = (<></>),
onStatusChange = () => {},
...other ...other
}) => ( }) => (
<Route {...other} render={() => { <Route {...other} render={() => {
@ -18,9 +19,10 @@ const SidebarLayoutComponent = ({component: Component,
<SideNavComponent disabled={sideNavDisabled} <SideNavComponent disabled={sideNavDisabled}
completedSteps={completedSteps} completedSteps={completedSteps}
defaultReport={defaultReport} defaultReport={defaultReport}
header={sideNavHeader}/> header={sideNavHeader}
onStatusChange={onStatusChange}/>
</Col>} </Col>}
<Component {...other} /> <Component onStatusChange={onStatusChange} {...other} />
</Row>) </Row>)
}}/> }}/>
) )

View File

@ -12,8 +12,11 @@ import Logo from "../logo/LogoComponent";
const monkeyIcon = require('../../images/monkey-icon.svg') const monkeyIcon = require('../../images/monkey-icon.svg')
const infectionMonkey = require('../../images/infection-monkey.svg') const infectionMonkey = require('../../images/infection-monkey.svg')
const LandingPageComponent = (props) => { type Props = {
onStatusChange: () => void
}
const LandingPageComponent = (props: Props) => {
return ( return (
<> <>
<ParticleBackground/> <ParticleBackground/>

View File

@ -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 (
<Col sm={{offset: 3, span: 9}} md={{offset: 3, span: 9}}
lg={{offset: 3, span: 9}} xl={{offset: 2, span: 7}}
className={'main'}>
<StartOverModal cleaned={this.state.cleaned}
showCleanDialog={this.state.showCleanDialog}
allMonkeysAreDead={this.state.allMonkeysAreDead}
onVerify={this.cleanup}
onClose={this.closeModal}/>
<h1 className="page-title">Start Over</h1>
<div style={{'fontSize': '1.2em'}}>
<p>
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
</p>
<p style={{margin: '20px'}} className={'text-center'}>
<Button className="btn btn-danger btn-lg center-block"
onClick={() => {
this.setState({showCleanDialog: true});
this.updateMonkeysRunning();
}
}>
Reset the Environment
</Button>
</p>
<div className="alert alert-info">
<FontAwesomeIcon icon={faInfoCircle} style={{'marginRight': '5px'}}/>
You don't have to reset the environment to keep running monkeys.
You can continue and <Link to="/run-monkey">Run More Monkeys</Link> as you wish,
and see the results on the <Link to="/infection/map">Infection Map</Link> without deleting anything.
</div>
{this.state.cleaned ?
<div className="alert alert-success">
<FontAwesomeIcon icon={faCheck} style={{'marginRight': '5px'}}/>
Environment was reset successfully
</div>
: ''}
</div>
</Col>
);
}
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;

View File

@ -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 (
<Modal show={props.show} onHide={() => {
setDeleteStatus(Idle);
props.onClose()
}} size={'lg'}>
<Modal.Header closeButton>
<Modal.Title>Reset the Island</Modal.Title>
</Modal.Header>
<Modal.Body>
{
!props.allMonkeysAreDead ?
<div className='alert alert-warning'>
<FontAwesomeIcon icon={faExclamationTriangle} style={{'marginRight': '5px'}}/>
Please stop all running agents before attempting to reset the Island.
</div>
:
showModalButtons()
}
</Modal.Body>
</Modal>
)
function displayDeleteData() {
if (deleteStatus === Idle) {
return (
<button type='button' className='btn btn-danger btn-lg' style={{margin: '5px'}}
onClick={() => {
setDeleteStatus(Loading);
resetIsland('/api?action=delete-agent-data',
() => {
setDeleteStatus(Done)
})
}}>
Delete data
</button>
)
} else if (deleteStatus === Loading) {
return (<LoadingIcon/>)
} else if (deleteStatus === Done) {
return (<FontAwesomeIcon icon={faCheck} className={'status-success'} size={'2x'}/>)
}
}
function displayResetAll() {
if (resetAllStatus === Idle) {
return (
<button type='button' className='btn btn-danger btn-lg' style={{margin: '5px'}}
onClick={() => {
setResetAll(Loading);
resetIsland('/api?action=reset',
() => {
setResetAll(Done);
props.onClose();
})
}}>
Reset the Island
</button>
)
} else if (resetAllStatus === Loading) {
return (<LoadingIcon/>)
} else if (resetAllStatus === Done) {
return (<FontAwesomeIcon icon={faCheck} className={'status-success'} size={'2x'}/>)
}
}
function resetIsland(url: string, callback: () => void) {
auth.authFetch(url)
.then(res => res.json())
.then(res => {
if (res['status'] === 'OK') {
callback()
}
})
}
function showModalButtons() {
return (<Container className={`text-left island-reset-modal`}>
<Row>
<Col>
<p>Delete data gathered by Monkey agents.</p>
<p>This will reset the Map and reports.</p>
</Col>
<Col sm={4} className={'text-center'}>
{displayDeleteData()}
</Col>
</Row>
<hr/>
<Row>
<Col>
<p>Reset everything.</p>
<p>You might want to <Button variant={'link'} href={Routes.ConfigurePage}>export
configuration</Button> before doing this.</p>
</Col>
<Col sm={4} className={'text-center'}>
{displayResetAll()}
</Col>
</Row>
</Container>)
}
}
export default IslandResetModal;

View File

@ -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 (
<Modal show={this.state.showCleanDialog} onHide={() => this.props.onClose()}>
<Modal.Body>
<h2>
<div className='text-center'>Reset environment</div>
</h2>
<p style={{'fontSize': '1.2em', 'marginBottom': '2em'}}>
Are you sure you want to reset the environment?
</p>
{
!this.state.allMonkeysAreDead ?
<div className='alert alert-warning'>
<FontAwesomeIcon icon={faExclamationTriangle} style={{'marginRight': '5px'}}/>
Some monkeys are still running. It's advised to kill all monkeys before resetting.
</div>
:
<div/>
}
{
this.state.loading ? <div className={'modalLoader'}><GridLoader/></div> : this.showModalButtons()
}
</Modal.Body>
</Modal>
)
};
showModalButtons() {
return (<div className='text-center'>
<button type='button' className='btn btn-danger btn-lg' style={{margin: '5px'}}
onClick={this.modalVerificationOnClick}>
Reset environment
</button>
<button type='button' className='btn btn-success btn-lg' style={{margin: '5px'}}
onClick={() => {this.props.onClose(); this.setState({showCleanDialog: false})}}>
Cancel
</button>
</div>)
}
modalVerificationOnClick = async () => {
this.setState({loading: true});
this.props.onVerify()
.then(() => {this.setState({loading: false});
this.props.onClose();})
}
}
export default StartOverModal;

View File

@ -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;
}

View File

@ -1,6 +1,13 @@
.sidebar .version-text { .sidebar .version-text {
position: relative; position: relative;
} }
.sidebar .license-link { .sidebar .license-link {
position: relative; position: relative;
} }
.navigation .island-reset-button {
width: 100%;
text-align: left;
padding-left: 18px;
}

View File

@ -1,9 +0,0 @@
$yellow: #ffcc00;
.modalLoader div{
margin-left: auto;
margin-right: auto;
}
.modalLoader div>div{
background-color: $yellow;
}