Merge pull request #840 from VakarisZ/run_page_ui_improvements

Run page ui improvements
This commit is contained in:
VakarisZ 2020-09-28 12:36:24 +03:00 committed by GitHub
commit fad19258d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 801 additions and 551 deletions

View File

@ -89,7 +89,7 @@ script:
- cd monkey_island/cc/ui - cd monkey_island/cc/ui
- npm ci # See https://docs.npmjs.com/cli/ci.html - npm ci # See https://docs.npmjs.com/cli/ci.html
- eslint ./src --quiet # Test for errors - eslint ./src --quiet # Test for errors
- JS_WARNINGS_AMOUNT_UPPER_LIMIT=4 - JS_WARNINGS_AMOUNT_UPPER_LIMIT=7
- eslint ./src --max-warnings $JS_WARNINGS_AMOUNT_UPPER_LIMIT # Test for max warnings - eslint ./src --max-warnings $JS_WARNINGS_AMOUNT_UPPER_LIMIT # Test for max warnings
# Build documentation # Build documentation

View File

@ -57,6 +57,7 @@ A quick reference for usernames on different machines (if in doubt check officia
- Ubuntu: ubuntu - Ubuntu: ubuntu
- Oracle: clckwrk - Oracle: clckwrk
- CentOS: centos - CentOS: centos
- Debian: admin
- Everything else: ec2-user - Everything else: ec2-user
To manually verify the machine is compatible use commands to download and execute the monkey. To manually verify the machine is compatible use commands to download and execute the monkey.

View File

@ -51,7 +51,7 @@ def run_local_monkey():
logger.error('popen failed', exc_info=True) logger.error('popen failed', exc_info=True)
return False, "popen failed: %s" % exc return False, "popen failed: %s" % exc
return True, "pis: %s" % pid return True, ""
class LocalRun(flask_restful.Resource): class LocalRun(flask_restful.Resource):

View File

@ -4,7 +4,7 @@ import {Container} from 'react-bootstrap';
import RunServerPage from 'components/pages/RunServerPage'; import RunServerPage from 'components/pages/RunServerPage';
import ConfigurePage from 'components/pages/ConfigurePage'; import ConfigurePage from 'components/pages/ConfigurePage';
import RunMonkeyPage from 'components/pages/RunMonkeyPage'; import RunMonkeyPage from 'components/pages/RunMonkeyPage/RunMonkeyPage';
import MapPage from 'components/pages/MapPage'; import MapPage from 'components/pages/MapPage';
import TelemetryPage from 'components/pages/TelemetryPage'; import TelemetryPage from 'components/pages/TelemetryPage';
import StartOverPage from 'components/pages/StartOverPage'; import StartOverPage from 'components/pages/StartOverPage';
@ -30,7 +30,7 @@ const reportZeroTrustRoute = '/report/zeroTrust';
class AppComponent extends AuthComponent { class AppComponent extends AuthComponent {
updateStatus = () => { updateStatus = () => {
if (this.state.isLoggedIn === false){ if (this.state.isLoggedIn === false) {
return return
} }
this.auth.loggedIn() this.auth.loggedIn()

View File

@ -1,481 +0,0 @@
import React from 'react';
import {css} from '@emotion/core';
import {Button, Col, Card, Nav, Collapse, Row} from 'react-bootstrap';
import CopyToClipboard from 'react-copy-to-clipboard';
import GridLoader from 'react-spinners/GridLoader';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faClipboard} from '@fortawesome/free-solid-svg-icons/faClipboard';
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
import {faSync} from '@fortawesome/free-solid-svg-icons/faSync';
import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle';
import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle';
import {Link} from 'react-router-dom';
import AuthComponent from '../AuthComponent';
import AwsRunTable from '../run-monkey/AwsRunTable';
import MissingBinariesModal from '../ui-components/MissingBinariesModal';
const loading_css_override = css`
display: block;
margin-right: auto;
margin-left: auto;
`;
class RunMonkeyPageComponent extends AuthComponent {
constructor(props) {
super(props);
this.state = {
ips: [],
runningOnIslandState: 'not_running',
runningOnClientState: 'not_running',
awsClicked: false,
selectedIp: '0.0.0.0',
selectedOs: 'windows-32',
showManual: false,
showAws: false,
isOnAws: false,
awsUpdateClicked: false,
awsUpdateFailed: false,
awsMachines: [],
isLoadingAws: true,
isErrorWhileCollectingAwsMachines: false,
awsMachineCollectionErrorMsg: '',
showModal: false,
errorDetails: ''
};
this.closeModal = this.closeModal.bind(this);
}
componentDidMount() {
this.authFetch('/api')
.then(res => res.json())
.then(res => this.setState({
ips: res['ip_addresses'],
selectedIp: res['ip_addresses'][0]
}));
this.authFetch('/api/local-monkey')
.then(res => res.json())
.then(res => {
if (res['is_running']) {
this.setState({runningOnIslandState: 'running'});
} else {
this.setState({runningOnIslandState: 'not_running'});
}
});
this.fetchAwsInfo();
this.fetchConfig();
this.authFetch('/api/client-monkey')
.then(res => res.json())
.then(res => {
if (res['is_running']) {
this.setState({runningOnClientState: 'running'});
} else {
this.setState({runningOnClientState: 'not_running'});
}
});
this.props.onStatusChange();
}
fetchAwsInfo() {
return this.authFetch('/api/remote-monkey?action=list_aws')
.then(res => res.json())
.then(res => {
let is_aws = res['is_aws'];
if (is_aws) {
// On AWS!
// Checks if there was an error while collecting the aws machines.
let is_error_while_collecting_aws_machines = (res['error'] != null);
if (is_error_while_collecting_aws_machines) {
// There was an error. Finish loading, and display error message.
this.setState({
isOnAws: true,
isErrorWhileCollectingAwsMachines: true,
awsMachineCollectionErrorMsg: res['error'],
isLoadingAws: false
});
} else {
// No error! Finish loading and display machines for user
this.setState({isOnAws: true, awsMachines: res['instances'], isLoadingAws: false});
}
} else {
// Not on AWS. Finish loading and don't display the AWS div.
this.setState({isOnAws: false, isLoadingAws: false});
}
});
}
static generateLinuxCmd(ip, is32Bit) {
let bitText = is32Bit ? '32' : '64';
return `wget --no-check-certificate https://${ip}:5000/api/monkey/download/monkey-linux-${bitText}; chmod +x monkey-linux-${bitText}; ./monkey-linux-${bitText} m0nk3y -s ${ip}:5000`
}
static generateWindowsCmd(ip, is32Bit) {
let bitText = is32Bit ? '32' : '64';
return `powershell [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; (New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/monkey-windows-${bitText}.exe','.\\monkey.exe'); ;Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';`;
}
runLocalMonkey = () => {
this.authFetch('/api/local-monkey',
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'run'})
})
.then(res => res.json())
.then(res => {
if (res['is_running']) {
this.setState({
runningOnIslandState: 'installing'
});
} else {
/* If Monkey binaries are missing, change the state accordingly */
if (res['error_text'].startsWith('Copy file failed')) {
this.setState({
showModal: true,
errorDetails: res['error_text']
}
);
}
this.setState({
runningOnIslandState: 'not_running'
});
}
this.props.onStatusChange();
});
};
generateCmdDiv() {
let isLinux = (this.state.selectedOs.split('-')[0] === 'linux');
let is32Bit = (this.state.selectedOs.split('-')[1] === '32');
let cmdText = '';
if (isLinux) {
cmdText = RunMonkeyPageComponent.generateLinuxCmd(this.state.selectedIp, is32Bit);
} else {
cmdText = RunMonkeyPageComponent.generateWindowsCmd(this.state.selectedIp, is32Bit);
}
return (
<Card key={'cmdDiv' + this.state.selectedIp} style={{'margin': '0.5em'}}>
<div style={{'overflow': 'auto', 'padding': '0.5em'}}>
<CopyToClipboard text={cmdText} className="pull-right btn-sm">
<Button style={{margin: '-0.5em'}} title="Copy to Clipboard">
<FontAwesomeIcon icon={faClipboard}/>
</Button>
</CopyToClipboard>
<code>{cmdText}</code>
</div>
</Card>
)
}
setSelectedOs = (key) => {
this.setState({
selectedOs: key
});
};
setSelectedIp = (key) => {
this.setState({
selectedIp: key
});
};
static renderIconByState(state) {
if (state === 'running') {
return (<FontAwesomeIcon icon={faCheck} className="text-success" style={{'marginLeft': '5px'}}/>)
} else if (state === 'installing') {
return (<FontAwesomeIcon icon={faSync} className="text-success" style={{'marginLeft': '5px'}}/>)
} else {
return '';
}
}
toggleManual = () => {
this.setState({
showManual: !this.state.showManual
});
};
toggleAws = () => {
this.setState({
showAws: !this.state.showAws
});
};
runOnAws = () => {
this.setState({
awsClicked: true
});
let instances = this.awsTable.state.selection.map(x => this.instanceIdToInstance(x));
this.authFetch('/api/remote-monkey',
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({type: 'aws', instances: instances, island_ip: this.state.selectedIp})
}).then(res => res.json())
.then(res => {
let result = res['result'];
// update existing state, not run-over
let prevRes = this.awsTable.state.result;
for (let key in result) {
if (Object.prototype.hasOwnProperty.call(result, key)) {
prevRes[key] = result[key];
}
}
this.awsTable.setState({
result: prevRes,
selection: [],
selectAll: false
});
this.setState({
awsClicked: false
});
});
};
fetchConfig() {
return this.authFetch('/api/configuration/island')
.then(res => res.json())
.then(res => {
return res.configuration;
})
}
instanceIdToInstance = (instance_id) => {
let instance = this.state.awsMachines.find(
function (inst) {
return inst['instance_id'] === instance_id;
});
return {'instance_id': instance_id, 'os': instance['os']}
};
renderAwsMachinesDiv() {
return (
<div style={{'marginBottom': '2em'}}>
<div style={{'marginTop': '1em', 'marginBottom': '1em'}}>
<p className="alert alert-info">
<FontAwesomeIcon icon={faInfoCircle} style={{'marginRight': '5px'}}/>
Not sure what this is? Not seeing your AWS EC2 instances? <a
href="https://github.com/guardicore/monkey/wiki/Monkey-Island:-Running-the-monkey-on-AWS-EC2-instances"
rel="noopener noreferrer" target="_blank">Read the documentation</a>!
</p>
</div>
{
this.state.ips.length > 1 ?
<Nav variant="pills" activeKey={this.state.selectedIp} onSelect={this.setSelectedIp}
style={{'marginBottom': '2em'}}>
{this.state.ips.map(ip => <Nav.Item key={ip}><Nav.Link eventKey={ip}>{ip}</Nav.Link></Nav.Item>)}
</Nav>
: <div style={{'marginBottom': '2em'}}/>
}
<AwsRunTable
data={this.state.awsMachines}
ref={r => (this.awsTable = r)}
/>
<div style={{'marginTop': '1em'}}>
<Button
onClick={this.runOnAws}
className={'btn btn-default btn-md center-block'}
disabled={this.state.awsClicked}>
Run on selected machines
{this.state.awsClicked ?
<FontAwesomeIcon icon={faSync} className="text-success" style={{'marginLeft': '5px'}}/> : null}
</Button>
</div>
</div>
)
}
closeModal = () => {
this.setState({
showModal: false
})
};
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'}>
<h1 className="page-title">1. Run Monkey</h1>
<p style={{'marginBottom': '2em', 'fontSize': '1.2em'}}>
Go ahead and run the monkey!
<i> (Or <Link to="/configure">configure the monkey</Link> to fine tune its behavior)</i>
</p>
<p className={'text-center'}>
<Button onClick={this.runLocalMonkey}
variant={'outline-monkey'}
size='lg'
disabled={this.state.runningOnIslandState !== 'not_running'}
>
Run on Monkey Island Server
{RunMonkeyPageComponent.renderIconByState(this.state.runningOnIslandState)}
</Button>
<MissingBinariesModal
showModal={this.state.showModal}
onClose={this.closeModal}
errorDetails={this.state.errorDetails}/>
{
// TODO: implement button functionality
/*
<button
className="btn btn-default"
disabled={this.state.runningOnClientState !== 'not_running'}
style={{'marginLeft': '1em'}}>
Download and run locally
{ this.renderIconByState(this.state.runningOnClientState) }
</button>
*/
}
</p>
<p className="text-center">
OR
</p>
<p className={'text-center'}
style={this.state.showManual || !this.state.isOnAws ? {'marginBottom': '2em'} : {}}>
<Button onClick={this.toggleManual}
variant={'outline-monkey'}
size='lg'
className={(this.state.showManual ? 'active' : '')}>
Run on a machine of your choice
</Button>
</p>
<Collapse in={this.state.showManual}>
<div style={{'marginBottom': '2em'}}>
<p style={{'fontSize': '1.2em'}}>
Choose the operating system where you want to run the monkey:
</p>
<Row>
<Col>
<Nav variant='pills' fill id={'bootstrap-override'} className={'run-on-os-buttons'}
activeKey={this.state.selectedOs} onSelect={this.setSelectedOs}>
<Nav.Item>
<Nav.Link eventKey={'windows-32'}>
Windows (32 bit)
</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey='windows-64'>
Windows (64 bit)
</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey='linux-32'>
Linux (32 bit)
</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey='linux-64'>
Linux (64 bit)
</Nav.Link>
</Nav.Item>
</Nav>
</Col>
</Row>
{this.state.ips.length > 1 ?
<div>
<Row>
<Col>
<p style={{'fontSize': '1.2em'}}>
Choose the interface to communicate with:
</p>
</Col>
</Row>
<Row>
<Col>
<Nav variant="pills" fill activeKey={this.state.selectedIp} onSelect={this.setSelectedIp}
className={'run-on-os-buttons'}>
{this.state.ips.map(ip => <Nav.Item key={ip}>
<Nav.Link eventKey={ip}>{ip}</Nav.Link></Nav.Item>)}
</Nav>
</Col>
</Row>
</div>
: <div style={{'marginBottom': '2em'}}/>
}
<p style={{'fontSize': '1.2em'}}>
Copy the following command to your machine and run it with Administrator or root privileges.
</p>
{this.generateCmdDiv()}
</div>
</Collapse>
{
this.state.isLoadingAws ?
<div style={{'marginBottom': '2em', 'align': 'center'}}>
<div className='sweet-loading'>
<GridLoader
css={loading_css_override}
sizeUnit={'px'}
size={30}
color={'#ffcc00'}
loading={this.state.loading}
/>
</div>
</div>
: null
}
{
this.state.isOnAws ?
<p className="text-center">
OR
</p>
:
null
}
{
this.state.isOnAws ?
<p style={{'marginBottom': '2em'}} className={'text-center'}>
<Button onClick={this.toggleAws}
className={(this.state.showAws ? ' active' : '')}
size='lg'
variant={'outline-monkey'}>
Run on AWS machine of your choice
</Button>
</p>
:
null
}
<Collapse in={this.state.showAws}>
{
this.state.isErrorWhileCollectingAwsMachines ?
<div style={{'marginTop': '1em'}}>
<p className="alert alert-danger">
<FontAwesomeIcon icon={faExclamationTriangle} style={{'marginRight': '5px'}}/>
Error while collecting AWS machine data. Error
message: <code>{this.state.awsMachineCollectionErrorMsg}</code><br/>
Are you sure you've set the correct role on your Island AWS machine?<br/>
Not sure what this is? <a
href="https://github.com/guardicore/monkey/wiki/Monkey-Island:-Running-the-monkey-on-AWS-EC2-instances">Read
the documentation</a>!
</p>
</div>
:
this.renderAwsMachinesDiv()
}
</Collapse>
<p style={{'fontSize': '1.2em'}}>
Go ahead and monitor the ongoing infection in the <Link to="/infection/map">Infection Map</Link> view.
</p>
</Col>
);
}
}
export default RunMonkeyPageComponent;

View File

@ -0,0 +1,63 @@
import {Button, Card, Nav} from 'react-bootstrap';
import CopyToClipboard from 'react-copy-to-clipboard';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faClipboard} from '@fortawesome/free-solid-svg-icons/faClipboard';
import React, {useEffect, useState} from 'react';
import PropTypes from 'prop-types';
export default function commandDisplay(props) {
const [selectedCommand, setSelectedCommand] = useState(props.commands[0]);
function setSelectedCommandByName(type){
setSelectedCommand(getCommandByName(props.commands, type));
}
function getCommandByName(commands, type){
return commands.find((command) => {return command.type === type});
}
useEffect(() => {
let sameTypeCommand = getCommandByName(props.commands, selectedCommand.type);
if( sameTypeCommand !== undefined){
setSelectedCommand(sameTypeCommand);
} else {
setSelectedCommand(props.commands[0]);
}
}, [props.commands]);
function renderNav() {
return (
<Nav variant='tabs' activeKey={selectedCommand.type} onSelect={setSelectedCommandByName}>
{props.commands.map(command => {
return (
<Nav.Item key={command.type}>
<Nav.Link eventKey={command.type}>{command.type}</Nav.Link>
</Nav.Item>);
})}
</Nav>);
}
return (
<div className={'command-display'}>
{renderNav()}
<Card>
<div style={{'overflow': 'auto', 'padding': '0.5em'}}>
<CopyToClipboard text={selectedCommand.command} className="pull-right btn-sm">
<Button style={{margin: '-0.5em'}} title="Copy to Clipboard">
<FontAwesomeIcon icon={faClipboard}/>
</Button>
</CopyToClipboard>
<code>{selectedCommand.command}</code>
</div>
</Card>
</div>
)
}
commandDisplay.propTypes = {
commands: PropTypes.arrayOf(PropTypes.exact({
type: PropTypes.string,
command: PropTypes.string
}))
}

View File

@ -0,0 +1,16 @@
import React from 'react';
import InlineSelection from '../../ui-components/inline-selection/InlineSelection';
import LocalManualRunOptions from './LocalManualRunOptions';
function InterfaceSelection(props) {
return InlineSelection(getContents, props, LocalManualRunOptions)
}
const getContents = (props) => {
const ips = props.ips.map((ip) =>
<div key={ip}>{ip}</div>
);
return (<div>{ips}</div>);
}
export default InterfaceSelection;

View File

@ -0,0 +1,59 @@
import React, {useEffect, useState} from 'react';
import InlineSelection from '../../ui-components/inline-selection/InlineSelection';
import DropdownSelect from '../../ui-components/DropdownSelect';
import {OS_TYPES} from './OsTypes';
import GenerateLocalWindowsCmd from './commands/local_windows_cmd';
import GenerateLocalWindowsPowershell from './commands/local_windows_powershell';
import GenerateLocalLinuxWget from './commands/local_linux_wget';
import GenerateLocalLinuxCurl from './commands/local_linux_curl';
import CommandDisplay from './CommandDisplay';
const LocalManualRunOptions = (props) => {
return InlineSelection(getContents, {
...props,
onBackButtonClick: () => {props.setComponent()}
})
}
const getContents = (props) => {
const osTypes = {
[OS_TYPES.WINDOWS_64]: 'Windows 64bit',
[OS_TYPES.WINDOWS_32]: 'Windows 32bit',
[OS_TYPES.LINUX_64]: 'Linux 64bit',
[OS_TYPES.LINUX_32]: 'Linux 32bit'
}
const [osType, setOsType] = useState(OS_TYPES.WINDOWS_64);
const [selectedIp, setSelectedIp] = useState(props.ips[0]);
const [commands, setCommands] = useState(generateCommands());
useEffect(() => {
setCommands(generateCommands());
}, [osType, selectedIp])
function setIp(index) {
setSelectedIp(props.ips[index]);
}
function generateCommands() {
if (osType === OS_TYPES.WINDOWS_64 || osType === OS_TYPES.WINDOWS_32) {
return [{type: 'CMD', command: GenerateLocalWindowsCmd(selectedIp, osType)},
{type: 'Powershell', command: GenerateLocalWindowsPowershell(selectedIp, osType)}]
} else {
return [{type: 'CURL', command: GenerateLocalLinuxCurl(selectedIp, osType)},
{type: 'WGET', command: GenerateLocalLinuxWget(selectedIp, osType)}]
}
}
return (
<>
<DropdownSelect defaultKey={OS_TYPES.WINDOWS_64} options={osTypes} onClick={setOsType} variant={'outline-monkey'}/>
<DropdownSelect defaultKey={0} options={props.ips} onClick={setIp} variant={'outline-monkey'}/>
<CommandDisplay commands={commands}/>
</>
)
}
export default LocalManualRunOptions;

View File

@ -0,0 +1,6 @@
export const OS_TYPES = {
WINDOWS_32: 'win32',
WINDOWS_64: 'win64',
LINUX_32: 'linux32',
LINUX_64: 'linux64'
}

View File

@ -0,0 +1,26 @@
import React from 'react';
import {Col} from 'react-bootstrap';
import {Link} from 'react-router-dom';
import AuthComponent from '../../AuthComponent';
import RunOptions from './RunOptions';
class RunMonkeyPageComponent extends AuthComponent {
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'}>
<h1 className="page-title">1. Run Monkey</h1>
<p style={{'marginBottom': '2em', 'fontSize': '1.2em'}}>
Go ahead and run the monkey!
<i> (Or <Link to="/configure">configure the monkey</Link> to fine tune its behavior)</i>
</p>
<RunOptions />
</Col>
);
}
}
export default RunMonkeyPageComponent;

View File

@ -0,0 +1,123 @@
import React from 'react';
import {Button, Col, Row} from 'react-bootstrap';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
import {faSync} from '@fortawesome/free-solid-svg-icons/faSync';
import AuthComponent from '../../AuthComponent';
import IslandMonkeyRunErrorModal from '../../ui-components/IslandMonkeyRunErrorModal';
import '../../../styles/components/RunOnIslandButton.scss';
import {faTimes} from '@fortawesome/free-solid-svg-icons';
const MONKEY_STATES = {
RUNNING: 'running',
NOT_RUNNING: 'not_running',
STARTING: 'starting',
FAILED: 'failed'
}
class RunOnIslandButton extends AuthComponent {
constructor(props) {
super(props);
this.state = {
runningOnIslandState: MONKEY_STATES.NOT_RUNNING,
showModal: false,
errorDetails: ''
};
this.closeModal = this.closeModal.bind(this);
}
componentDidMount() {
this.authFetch('/api/local-monkey')
.then(res => res.json())
.then(res => {
if (res['is_running']) {
this.setState({runningOnIslandState: MONKEY_STATES.RUNNING});
} else {
this.setState({runningOnIslandState: MONKEY_STATES.NOT_RUNNING});
}
});
}
runIslandMonkey = () => {
this.setState({runningOnIslandState: MONKEY_STATES.STARTING}, this.sendRunMonkeyRequest)
};
sendRunMonkeyRequest() {
this.authFetch('/api/local-monkey',
{
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'run'})
})
.then(res => res.json())
.then(async res => {
if (res['is_running']) {
await new Promise(r => setTimeout(r, 1000));
this.setState({
runningOnIslandState: MONKEY_STATES.RUNNING
});
} else {
/* If Monkey binaries are missing, change the state accordingly */
if (res['error_text'] !== '') {
this.setState({
showModal: true,
errorDetails: res['error_text'],
runningOnIslandState: MONKEY_STATES.FAILED
}
);
}
}
});
}
closeModal = () => {
this.setState({
showModal: false
})
};
getMonkeyRunStateIcon = () => {
if (this.state.runningOnIslandState === MONKEY_STATES.RUNNING) {
return (<FontAwesomeIcon icon={faCheck}
className={`monkey-on-island-run-state-icon text-success`}/>)
} else if (this.state.runningOnIslandState === MONKEY_STATES.STARTING) {
return (<FontAwesomeIcon icon={faSync}
className={`monkey-on-island-run-state-icon text-success spinning-icon`}/>)
} else if (this.state.runningOnIslandState === MONKEY_STATES.FAILED) {
return (<FontAwesomeIcon icon={faTimes}
className={`monkey-on-island-run-state-icon text-danger`}/>)
} else {
return '';
}
}
render() {
let description = this.props.description !== undefined ? (<p>{this.props.description}</p>) : ''
let icon = this.props.icon !== undefined ? (<FontAwesomeIcon icon={this.props.icon}/>) : ''
return (
<Row>
<Col>
<IslandMonkeyRunErrorModal
showModal={this.state.showModal}
onClose={this.closeModal}
errorDetails={this.state.errorDetails}/>
<Button variant={'outline-monkey'} size='lg' className={'selection-button'}
onClick={this.runIslandMonkey}>
{icon}
<h1>{this.props.title}</h1>
{description}
{this.getMonkeyRunStateIcon()}
</Button>
</Col>
</Row>
);
}
}
export default RunOnIslandButton;

View File

@ -0,0 +1,71 @@
import React, {useEffect, useState} from 'react';
import NextSelectionButton from '../../ui-components/inline-selection/NextSelectionButton';
import LocalManualRunOptions from './LocalManualRunOptions';
import AuthComponent from '../../AuthComponent';
import {faLaptopCode} from '@fortawesome/free-solid-svg-icons/faLaptopCode';
import InlineSelection from '../../ui-components/inline-selection/InlineSelection';
import {cloneDeep} from 'lodash';
import {faExpandArrowsAlt} from '@fortawesome/free-solid-svg-icons';
import RunOnIslandButton from './RunOnIslandButton';
function RunOptions(props) {
const [currentContent, setCurrentContent] = useState(loadingContents());
const [ips, setIps] = useState([]);
const [initialized, setInitialized] = useState(false);
const authComponent = new AuthComponent({})
useEffect(() => {
if (initialized === false) {
authComponent.authFetch('/api')
.then(res => res.json())
.then(res => {
setIps([res['ip_addresses']][0]);
setInitialized(true);
});
}
})
useEffect(() => {
setCurrentContent(getDefaultContents());
}, [initialized])
function setComponent(component, props) {
if (component === undefined) {
setCurrentContent(getDefaultContents())
} else {
setCurrentContent(component({...props}))
}
}
function loadingContents() {
return (<div>Loading</div>)
}
function getDefaultContents() {
const newProps = cloneDeep({...props});
return InlineSelection(defaultContents, newProps);
}
function defaultContents() {
return (
<>
<RunOnIslandButton title={'From Island'}
description={'Start on Monkey Island server.'}
icon={faExpandArrowsAlt}/>
<NextSelectionButton title={'Manual'}
description={'Run on a machine via command.'}
icon={faLaptopCode}
onButtonClick={() => {
setComponent(LocalManualRunOptions,
{ips: ips, setComponent: setComponent})
}}/>
</>
);
}
return currentContent;
}
export default RunOptions;

View File

@ -0,0 +1,13 @@
import {OS_TYPES} from '../OsTypes';
export default function generateLocalLinuxCurl(ip, osType) {
let bitText = osType === OS_TYPES.LINUX_32 ? '32' : '64';
return `curl https://${ip}:5000/api/monkey/download/monkey-linux-${bitText} -k
-o monkey-linux-${bitText};
chmod +x monkey-linux-${bitText};
./monkey-linux-${bitText} m0nk3y -s ${ip}:5000\`;`;
}

View File

@ -0,0 +1,10 @@
import {OS_TYPES} from '../OsTypes';
export default function generateLocalLinuxWget(ip, osType) {
let bitText = osType === OS_TYPES.LINUX_32 ? '32' : '64';
return `wget --no-check-certificate https://${ip}:5000/api/monkey/download/
monkey-linux-${bitText};
chmod +x monkey-linux-${bitText};
./monkey-linux-${bitText} m0nk3y -s ${ip}:5000`;
}

View File

@ -0,0 +1,10 @@
import {OS_TYPES} from '../OsTypes';
export default function generateLocalWindowsCmd(ip, osType) {
let bitText = osType === OS_TYPES.WINDOWS_32 ? '32' : '64';
return `powershell [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true};
(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/
monkey-windows-${bitText}.exe','.\\monkey.exe');
;Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';`;
}

View File

@ -0,0 +1,10 @@
import {OS_TYPES} from '../OsTypes';
export default function generateLocalWindowsPowershell(ip, osType) {
let bitText = osType === OS_TYPES.WINDOWS_32 ? '32' : '64';
return `[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true};
(New-Object System.Net.WebClient).DownloadFile('https://${ip}:5000/api/monkey/download/
monkey-windows-${bitText}.exe','.\\monkey.exe');
;Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s ${ip}:5000';`;
}

View File

@ -0,0 +1,55 @@
import React, {useState} from 'react';
import {Dropdown} from 'react-bootstrap';
import PropTypes from 'prop-types';
export default function DropdownSelect(props) {
const [selectedOption, setSelectedOption] = useState(props.defaultKey);
function generateDropdownItems(data) {
if (Array.isArray(data)) {
return generateDropdownItemsFromArray(data);
} else if (typeof data === 'object') {
return generateDropdownItemsFromObject(data);
} else {
throw 'Component can only generate dropdown items from arrays and objects.'
}
}
function generateDropdownItemsFromArray(data) {
return data.map((x, i) => generateDropdownItem(i, x));
}
function generateDropdownItemsFromObject(data) {
return Object.entries(data).map(([key, value]) => generateDropdownItem(key, value));
}
function generateDropdownItem(key, value) {
return (
<Dropdown.Item onClick={() => { setSelectedOption(key);
props.onClick(key)}}
active={(key === selectedOption)}
key={value}>
{value}
</Dropdown.Item>);
}
return (
<>
<Dropdown>
<Dropdown.Toggle variant={props.variant !== undefined ? props.variant : 'success'} id='dropdown-basic'>
{props.options[selectedOption]}
</Dropdown.Toggle>
<Dropdown.Menu>
{generateDropdownItems(props.options)}
</Dropdown.Menu>
</Dropdown>
</>
)
}
DropdownSelect.propTypes = {
options: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
defaultKey: PropTypes.oneOfType([PropTypes.string,PropTypes.number]),
onClick: PropTypes.func
}

View File

@ -0,0 +1,12 @@
import React from 'react';
const Emoji = props => (
<span
className='emoji'
role='img'
aria-label={props.label ? props.label : ''}
aria-hidden={props.label ? 'false' : 'true'}
>
{props.symbol}
</span>
);
export default Emoji;

View File

@ -0,0 +1,100 @@
import {Modal} from 'react-bootstrap';
import React from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons';
class IslandMonkeyRunErrorModal extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
showModal: this.props.showModal,
errorDetails: this.props.errorDetails
};
}
componentDidUpdate(prevProps) {
if (this.props !== prevProps) {
this.setState({
showModal: this.props.showModal,
errorDetails: this.props.errorDetails
})
}
}
getMissingBinariesContent() {
return (
<span>
Some Monkey binaries are not found where they should be...<br/>
You can download the files from <a href="https://github.com/guardicore/monkey/releases/latest"
target="blank">here</a>,
at the bottommost section titled "Assets", and place them under the
directory <code>monkey/monkey_island/cc/binaries</code>.
</span>
)
}
getMonkeyAlreadyRunningContent() {
return (
<span>
Most likely, monkey is already running on the Island. Wait until it finishes or kill the process to run again.
</span>
)
}
getUndefinedErrorContent() {
return (
<span>
You encountered an undefined error. Please report it to support@infectionmonkey.com or our slack channel.
</span>
)
}
getDisplayContentByError(errorMsg) {
if (errorMsg.includes('Permission denied:')) {
return this.getMonkeyAlreadyRunningContent()
} else if (errorMsg.startsWith('Copy file failed')) {
return this.getMissingBinariesContent()
} else {
return this.getUndefinedErrorContent()
}
}
render = () => {
return (
<Modal show={this.state.showModal} onHide={() => this.props.onClose()}>
<Modal.Body>
<h3>
<div className='text-center'>Uh oh...</div>
</h3>
<div style={{'marginTop': '1em', 'marginBottom': '1em'}}>
<p className="alert alert-warning">
<FontAwesomeIcon icon={faExclamationTriangle} style={{'marginRight': '5px'}}/>
{this.getDisplayContentByError(this.state.errorDetails)}
</p>
</div>
<hr/>
<h4>
Error Details
</h4>
<div style={{'marginTop': '1em', 'marginBottom': '1em'}}>
<pre>
{this.state.errorDetails}
</pre>
</div>
<div className='text-center'>
<button type='button' className='btn btn-success btn-lg' style={{margin: '5px'}}
onClick={() => this.props.onClose()}>
Dismiss
</button>
</div>
</Modal.Body>
</Modal>
)
};
}
export default IslandMonkeyRunErrorModal;

View File

@ -1,62 +0,0 @@
import {Modal} from 'react-bootstrap';
import React from 'react';
class MissingBinariesModal extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
showModal: this.props.showModal,
errorDetails: this.props.errorDetails
};
}
componentDidUpdate(prevProps) {
if (this.props !== prevProps) {
this.setState({
showModal: this.props.showModal,
errorDetails: this.props.errorDetails
})
}
}
render = () => {
return (
<Modal show={this.state.showModal} onHide={() => this.props.onClose()}>
<Modal.Body>
<h3>
<div className='text-center'>Uh oh...</div>
</h3>
<div style={{'marginTop': '1em', 'marginBottom': '1em'}}>
<p className="alert alert-warning">
<i className="glyphicon glyphicon-info-sign" style={{'marginRight': '5px'}}/>
Some Monkey binaries are not found where they should be...<br/>
You can download the files from <a href="https://github.com/guardicore/monkey/releases/latest" target="blank">here</a>,
at the bottommost section titled "Assets", and place them under the directory <code>monkey/monkey_island/cc/binaries</code>.
</p>
</div>
<hr/>
<h4>
Error Details
</h4>
<div style={{'marginTop': '1em', 'marginBottom': '1em'}}>
<pre>
{this.state.errorDetails}
</pre>
</div>
<div className='text-center'>
<button type='button' className='btn btn-success btn-lg' style={{margin: '5px'}}
onClick={() => this.props.onClose()}>
Dismiss
</button>
</div>
</Modal.Body>
</Modal>
)
};
}
export default MissingBinariesModal;

View File

@ -0,0 +1,22 @@
import {Button, Col, Row} from 'react-bootstrap';
import React from 'react';
import PropTypes from 'prop-types';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCaretLeft} from '@fortawesome/free-solid-svg-icons/faCaretLeft';
export default function backButton(props) {
return (
<Row>
<Col>
<Button variant={'outline-dark'} onClick={props.onClick} className={'back-button'}>
<FontAwesomeIcon icon={faCaretLeft} />
<h1>Back</h1>
</Button>
</Col>
</Row>
)
}
backButton.propTypes = {
onClick: PropTypes.func
}

View File

@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
export default function CommandSection(props){
return (
<div className={'command-section'}>
{props.commands[0].name}
{props.commands[0].command}
</div>
)
}
CommandSection.propTypes = {
commands: PropTypes.arrayOf(PropTypes.exact({
name: PropTypes.string,
command: PropTypes.string
}))
}

View File

@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import BackButton from './BackButton';
import {Col, Row, Container} from 'react-bootstrap';
export default function InlineSelection(WrappedComponent, props) {
return (
<Container className={'inline-selection-component'}>
<Row>
<Col lg={8} md={10} sm={12}>
<WrappedComponent {...props}/>
{renderBackButton(props)}
</Col>
</Row>
</Container>
)
}
function renderBackButton(props){
if(props.onBackButtonClick !== undefined){
return (<BackButton onClick={props.onBackButtonClick}/>);
}
}
InlineSelection.propTypes = {
setComponent: PropTypes.func,
ips: PropTypes.arrayOf(PropTypes.string),
onBackButtonClick: PropTypes.func
}

View File

@ -0,0 +1,30 @@
import {Button, Row, Col} from 'react-bootstrap';
import React from 'react';
import PropTypes from 'prop-types';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faAngleRight} from '@fortawesome/free-solid-svg-icons';
export default function nextSelectionButton(props) {
let description = props.description !== undefined ? (<p>{props.description}</p>) : ''
let icon = props.icon !== undefined ? (<FontAwesomeIcon icon={props.icon}/>) : ''
return (
<Row>
<Col>
<Button variant={'outline-monkey'} size='lg' className={'selection-button'}
onClick={props.onButtonClick}>
{icon}
<h1>{props.title}</h1>
{description}
<FontAwesomeIcon icon={faAngleRight} className={'angle-right'}/>
</Button>
</Col>
</Row>
)
}
nextSelectionButton.propTypes = {
title: PropTypes.string,
icon: FontAwesomeIcon,
description: PropTypes.string,
onButtonClick: PropTypes.func
}

View File

@ -12,6 +12,11 @@
@import 'components/PreviewPane'; @import 'components/PreviewPane';
@import 'components/AdvancedMultiSelect'; @import 'components/AdvancedMultiSelect';
@import 'components/particle-component/ParticleBackground'; @import 'components/particle-component/ParticleBackground';
@import 'components/inline-selection/InlineSelection';
@import 'components/inline-selection/NextSelectionButton';
@import 'components/inline-selection/BackButton';
@import 'components/inline-selection/CommandDisplay';
@import 'components/Icons';
// Define custom elements after bootstrap import // Define custom elements after bootstrap import

View File

@ -0,0 +1,13 @@
.spinning-icon {
animation: spin-animation 0.5s infinite;
display: inline-block;
}
@keyframes spin-animation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}

View File

@ -0,0 +1,7 @@
.monkey-on-island-run-state-icon {
display: inline-block;
position: absolute;
right: 23px;
top: 28%;
font-size: 1.1em;
}

View File

@ -0,0 +1,20 @@
.inline-selection-component .back-button {
width: 100%;
text-align: left;
}
.inline-selection-component .back-button h1{
font-size: 1.3em;
margin-top: 5px;
margin-bottom: 5px;
text-align: left;
display: inline-block;
}
.inline-selection-component .back-button svg{
font-size: 1.5em;
display: inline-block;
margin-right: 10px;
position: relative;
top: 1px;
}

View File

@ -0,0 +1,21 @@
.command-display {
margin-top: 30px;
}
.command-display .nav-tabs .nav-item a{
font-size: 0.8em;
color: $monkey-black;
}
.command-display .nav-tabs .nav-item a.active{
color: $monkey-alt;
}
.command-display .nav-tabs{
border-bottom: none;
}
.command-display div.card{
margin: 0;
border-top-left-radius: 0;
}

View File

@ -0,0 +1,17 @@
.inline-selection-component.container{
padding: 0;
margin-left: 15px;
}
.inline-selection-component .selection-button {
width: 100%;
}
.inline-selection-component .dropdown {
display: inline-block;
margin-right: 10px;
}
.inline-selection-component .command-display {
margin-bottom: 20px;
}

View File

@ -0,0 +1,34 @@
.inline-selection-component .selection-button {
width: 100%;
text-align: left;
margin-bottom: 20px;
padding-right: 40px;
}
.inline-selection-component .selection-button svg,
.inline-selection-component .selection-button h1 {
display: inline-block;
}
.inline-selection-component .selection-button h1 {
margin: 0;
font-size: 1.3em;
}
.inline-selection-component .selection-button p {
margin: 0;
font-size: 0.8em;
}
.inline-selection-component .selection-button svg {
margin-bottom: 1px;
margin-right: 7px;
}
.inline-selection-component .selection-button .angle-right {
display: inline-block;
position: absolute;
right: 23px;
top: 22%;
font-size: 1.7em;
}

View File

@ -2,9 +2,9 @@ Flask-JWT-Extended==3.24.1
Flask-Pymongo>=2.3.0 Flask-Pymongo>=2.3.0
Flask-Restful>=0.3.8 Flask-Restful>=0.3.8
PyInstaller==3.6 PyInstaller==3.6
awscli>=1.18.131 awscli==1.18.131
boto3>=1.14.54 boto3==1.14.54
botocore>=1.17.54,<1.18.0 botocore==1.17.54
cffi>=1.8,!=1.11.3 cffi>=1.8,!=1.11.3
dpath>=2.0 dpath>=2.0
flask>=1.1 flask>=1.1
@ -26,4 +26,4 @@ virtualenv>=20.0.26
werkzeug>=1.0.1 werkzeug>=1.0.1
wheel>=0.34.2 wheel>=0.34.2
pyjwt>=1.5.1 # not directly required, pinned by Snyk to avoid a vulnerability pyjwt>=1.5.1 # not directly required, pinned by Snyk to avoid a vulnerability