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
### 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

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")
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

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):
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.

View File

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

View File

@ -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":

View File

@ -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()

View File

@ -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 {
<SidebarLayoutComponent component={MapPage} {...defaultSideNavProps}/>)}
{this.renderRoute(Routes.TelemetryPage,
<SidebarLayoutComponent component={TelemetryPage} {...defaultSideNavProps}/>)}
{this.renderRoute(Routes.StartOverPage,
<SidebarLayoutComponent component={StartOverPage} {...defaultSideNavProps}/>)}
{this.redirectToReport()}
{this.renderRoute(Routes.SecurityReport,
<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 {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
import {faUndo} from '@fortawesome/free-solid-svg-icons/faUndo';
import '../styles/components/SideNav.scss';
import {CompletedSteps} from "./side-menu/CompletedSteps";
import {isReportRoute, Routes} from "./Main";
import {CompletedSteps} from './side-menu/CompletedSteps';
import {isReportRoute, Routes} from './Main';
import Logo from './logo/LogoComponent';
import IslandResetModal from './ui-components/IslandResetModal';
const logoImage = require('../images/monkey-icon.svg');
const infectionMonkeyImage = require('../images/infection-monkey.svg');
import Logo from "./logo/LogoComponent";
type Props = {
disabled?: boolean,
completedSteps: CompletedSteps,
defaultReport: string,
header?: ReactFragment
header?: ReactFragment,
onStatusChange: () => void
}
const SideNavComponent = ({disabled,
const SideNavComponent = ({
disabled,
completedSteps,
defaultReport,
header=null}: Props) => {
header = null,
onStatusChange,
}: Props) => {
const [showResetModal, setShowResetModal] = useState(false);
return (
<>
@ -76,10 +83,17 @@ const SideNavComponent = ({disabled,
</NavLink>
</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>
Start Over
</NavLink>
Reset
</Button>
<IslandResetModal show={showResetModal}
allMonkeysAreDead={areMonkeysDead()}
onClose={() => {
setShowResetModal(false);
onStatusChange();
}}/>
</li>
</ul>
@ -98,8 +112,12 @@ const SideNavComponent = ({disabled,
<Logo/>
</>);
function areMonkeysDead() {
return (!completedSteps['runMonkey']) || (completedSteps['infectionDone'])
}
function getNavLinkClass() {
if(disabled){
if (disabled) {
return `nav-link disabled`
} else {
return ''

View File

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

View File

@ -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 (
<>
<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 {
position: relative;
}
.sidebar .license-link {
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;
}