forked from p34709852/monkey
Merge pull request #1909 from guardicore/957-island-reset-improvements
957 island reset improvements
This commit is contained in:
commit
b9efc2d552
|
@ -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
|
||||
|
|
|
@ -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 |
|
@ -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.
|
||||
|
|
|
@ -2,4 +2,6 @@ from mongoengine import Document, StringField
|
|||
|
||||
|
||||
class IslandMode(Document):
|
||||
COLLECTION_NAME = "island_mode"
|
||||
|
||||
mode = StringField()
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,6 +112,10 @@ const SideNavComponent = ({disabled,
|
|||
<Logo/>
|
||||
</>);
|
||||
|
||||
function areMonkeysDead() {
|
||||
return (!completedSteps['runMonkey']) || (completedSteps['infectionDone'])
|
||||
}
|
||||
|
||||
function getNavLinkClass() {
|
||||
if (disabled) {
|
||||
return `nav-link disabled`
|
||||
|
|
|
@ -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>)
|
||||
}}/>
|
||||
)
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
$yellow: #ffcc00;
|
||||
|
||||
.modalLoader div{
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.modalLoader div>div{
|
||||
background-color: $yellow;
|
||||
}
|
Loading…
Reference in New Issue