diff --git a/.travis.yml b/.travis.yml index 7715ff328..887b7cc67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -89,7 +89,7 @@ script: - cd monkey_island/cc/ui - npm ci # See https://docs.npmjs.com/cli/ci.html - 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 # Build documentation diff --git a/envs/os_compatibility/README.md b/envs/os_compatibility/README.md index 6b97b6612..e0ad30dec 100644 --- a/envs/os_compatibility/README.md +++ b/envs/os_compatibility/README.md @@ -57,6 +57,7 @@ A quick reference for usernames on different machines (if in doubt check officia - Ubuntu: ubuntu - Oracle: clckwrk - CentOS: centos +- Debian: admin - Everything else: ec2-user To manually verify the machine is compatible use commands to download and execute the monkey. diff --git a/monkey/monkey_island/cc/resources/local_run.py b/monkey/monkey_island/cc/resources/local_run.py index d743fc835..045729787 100644 --- a/monkey/monkey_island/cc/resources/local_run.py +++ b/monkey/monkey_island/cc/resources/local_run.py @@ -51,7 +51,7 @@ def run_local_monkey(): logger.error('popen failed', exc_info=True) return False, "popen failed: %s" % exc - return True, "pis: %s" % pid + return True, "" class LocalRun(flask_restful.Resource): diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index 46f5e20bc..32480db8e 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -4,7 +4,7 @@ import {Container} from 'react-bootstrap'; import RunServerPage from 'components/pages/RunServerPage'; 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 TelemetryPage from 'components/pages/TelemetryPage'; import StartOverPage from 'components/pages/StartOverPage'; @@ -30,7 +30,7 @@ const reportZeroTrustRoute = '/report/zeroTrust'; class AppComponent extends AuthComponent { updateStatus = () => { - if (this.state.isLoggedIn === false){ + if (this.state.isLoggedIn === false) { return } this.auth.loggedIn() diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js deleted file mode 100644 index 467812373..000000000 --- a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage.js +++ /dev/null @@ -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 ( - -
- - - - {cmdText} -
-
- ) - } - - setSelectedOs = (key) => { - this.setState({ - selectedOs: key - }); - }; - - setSelectedIp = (key) => { - this.setState({ - selectedIp: key - }); - }; - - static renderIconByState(state) { - if (state === 'running') { - return () - } else if (state === 'installing') { - return () - } 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 ( -
-
-

- - Not sure what this is? Not seeing your AWS EC2 instances? Read the documentation! -

-
- { - this.state.ips.length > 1 ? - - :
- } - - (this.awsTable = r)} - /> -
- -
-
- ) - } - - closeModal = () => { - this.setState({ - showModal: false - }) - }; - - render() { - return ( - -

1. Run Monkey

-

- Go ahead and run the monkey! - (Or configure the monkey to fine tune its behavior) -

-

- - - { - // TODO: implement button functionality - /* - - */ - } -

-

- OR -

-

- -

- -
-

- Choose the operating system where you want to run the monkey: -

- - - - - - - {this.state.ips.length > 1 ? -
- - -

- Choose the interface to communicate with: -

- -
- - - - - -
- :
- } -

- Copy the following command to your machine and run it with Administrator or root privileges. -

- {this.generateCmdDiv()} -
- - { - this.state.isLoadingAws ? -
-
- -
-
- : null - } - { - this.state.isOnAws ? -

- OR -

- : - null - } - { - this.state.isOnAws ? -

- -

- : - null - } - - { - this.state.isErrorWhileCollectingAwsMachines ? -
-

- - Error while collecting AWS machine data. Error - message: {this.state.awsMachineCollectionErrorMsg}
- Are you sure you've set the correct role on your Island AWS machine?
- Not sure what this is? Read - the documentation! -

-
- : - this.renderAwsMachinesDiv() - } - -
- -

- Go ahead and monitor the ongoing infection in the Infection Map view. -

- - ); - } -} - -export default RunMonkeyPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/CommandDisplay.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/CommandDisplay.js new file mode 100644 index 000000000..ff2f877dd --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/CommandDisplay.js @@ -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 ( + ); + } + + return ( +
+ {renderNav()} + +
+ + + + {selectedCommand.command} +
+
+
+ ) +} + +commandDisplay.propTypes = { + commands: PropTypes.arrayOf(PropTypes.exact({ + type: PropTypes.string, + command: PropTypes.string + })) +} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/InterfaceSelection.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/InterfaceSelection.js new file mode 100644 index 000000000..6e74fb4a0 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/InterfaceSelection.js @@ -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) => +
{ip}
+ ); + return (
{ips}
); +} + +export default InterfaceSelection; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/LocalManualRunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/LocalManualRunOptions.js new file mode 100644 index 000000000..b28285a18 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/LocalManualRunOptions.js @@ -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 ( + <> + + + + + ) +} + +export default LocalManualRunOptions; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/OsTypes.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/OsTypes.js new file mode 100644 index 000000000..b24c9b302 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/OsTypes.js @@ -0,0 +1,6 @@ +export const OS_TYPES = { + WINDOWS_32: 'win32', + WINDOWS_64: 'win64', + LINUX_32: 'linux32', + LINUX_64: 'linux64' +} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunMonkeyPage.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunMonkeyPage.js new file mode 100644 index 000000000..2a27c5be3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunMonkeyPage.js @@ -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 ( + +

1. Run Monkey

+

+ Go ahead and run the monkey! + (Or configure the monkey to fine tune its behavior) +

+ + + ); + } +} + +export default RunMonkeyPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnIslandButton.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnIslandButton.js new file mode 100644 index 000000000..b7bae48f1 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOnIslandButton.js @@ -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 () + } else if (this.state.runningOnIslandState === MONKEY_STATES.STARTING) { + return () + } else if (this.state.runningOnIslandState === MONKEY_STATES.FAILED) { + return () + } else { + return ''; + } + } + + render() { + let description = this.props.description !== undefined ? (

{this.props.description}

) : '' + let icon = this.props.icon !== undefined ? () : '' + return ( + + + + + + + ); + } +} + +export default RunOnIslandButton; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js new file mode 100644 index 000000000..a9fe1f8cf --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/RunOptions.js @@ -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 (
Loading
) + } + + function getDefaultContents() { + const newProps = cloneDeep({...props}); + return InlineSelection(defaultContents, newProps); + } + + function defaultContents() { + return ( + <> + + { + setComponent(LocalManualRunOptions, + {ips: ips, setComponent: setComponent}) + }}/> + + ); + } + + return currentContent; +} + +export default RunOptions; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js new file mode 100644 index 000000000..fb0171bfd --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_curl.js @@ -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\`;`; + } + + + diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js new file mode 100644 index 000000000..766822ee1 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_linux_wget.js @@ -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`; + } diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_cmd.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_cmd.js new file mode 100644 index 000000000..74afbe512 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_cmd.js @@ -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';`; +} diff --git a/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js new file mode 100644 index 000000000..1ebd1f4ac --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/pages/RunMonkeyPage/commands/local_windows_powershell.js @@ -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';`; +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/DropdownSelect.js b/monkey/monkey_island/cc/ui/src/components/ui-components/DropdownSelect.js new file mode 100644 index 000000000..8628c0b60 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/DropdownSelect.js @@ -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 ( + { setSelectedOption(key); + props.onClick(key)}} + active={(key === selectedOption)} + key={value}> + {value} + ); + } + + return ( + <> + + + {props.options[selectedOption]} + + + + {generateDropdownItems(props.options)} + + + + ) +} + +DropdownSelect.propTypes = { + options: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + defaultKey: PropTypes.oneOfType([PropTypes.string,PropTypes.number]), + onClick: PropTypes.func +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/Emoji.js b/monkey/monkey_island/cc/ui/src/components/ui-components/Emoji.js new file mode 100644 index 000000000..580103500 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/Emoji.js @@ -0,0 +1,12 @@ +import React from 'react'; +const Emoji = props => ( + + {props.symbol} + +); +export default Emoji; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/IslandMonkeyRunErrorModal.js b/monkey/monkey_island/cc/ui/src/components/ui-components/IslandMonkeyRunErrorModal.js new file mode 100644 index 000000000..67e7ee77d --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/IslandMonkeyRunErrorModal.js @@ -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 ( + + Some Monkey binaries are not found where they should be...
+ You can download the files from here, + at the bottommost section titled "Assets", and place them under the + directory monkey/monkey_island/cc/binaries. +
+ ) + } + + getMonkeyAlreadyRunningContent() { + return ( + + Most likely, monkey is already running on the Island. Wait until it finishes or kill the process to run again. + + ) + } + + getUndefinedErrorContent() { + return ( + + You encountered an undefined error. Please report it to support@infectionmonkey.com or our slack channel. + + ) + } + + 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 ( + this.props.onClose()}> + +

+
Uh oh...
+

+
+

+ + {this.getDisplayContentByError(this.state.errorDetails)} +

+
+
+

+ Error Details +

+
+
+              {this.state.errorDetails}
+            
+
+
+ +
+
+
+ ) + }; + +} + +export default IslandMonkeyRunErrorModal; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/MissingBinariesModal.js b/monkey/monkey_island/cc/ui/src/components/ui-components/MissingBinariesModal.js deleted file mode 100644 index ae7f6ac4e..000000000 --- a/monkey/monkey_island/cc/ui/src/components/ui-components/MissingBinariesModal.js +++ /dev/null @@ -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 ( - this.props.onClose()}> - -

-
Uh oh...
-

-
-

- - Some Monkey binaries are not found where they should be...
- You can download the files from here, - at the bottommost section titled "Assets", and place them under the directory monkey/monkey_island/cc/binaries. -

-
-
-

- Error Details -

-
-
-              {this.state.errorDetails}
-            
-
-
- -
-
-
- ) - }; - -} - -export default MissingBinariesModal; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/BackButton.js b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/BackButton.js new file mode 100644 index 000000000..5799a39ea --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/BackButton.js @@ -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 ( + + + + + + ) +} + +backButton.propTypes = { + onClick: PropTypes.func +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/CommandSection.js b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/CommandSection.js new file mode 100644 index 000000000..ec4a53f0e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/CommandSection.js @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + + +export default function CommandSection(props){ + return ( +
+ {props.commands[0].name} + {props.commands[0].command} +
+ ) +} + +CommandSection.propTypes = { + commands: PropTypes.arrayOf(PropTypes.exact({ + name: PropTypes.string, + command: PropTypes.string + })) +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js new file mode 100644 index 000000000..a699b7d7f --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/InlineSelection.js @@ -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 ( + + + + + {renderBackButton(props)} + + + + ) +} + +function renderBackButton(props){ + if(props.onBackButtonClick !== undefined){ + return (); + } +} + +InlineSelection.propTypes = { + setComponent: PropTypes.func, + ips: PropTypes.arrayOf(PropTypes.string), + onBackButtonClick: PropTypes.func +} diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js new file mode 100644 index 000000000..7a7c47087 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/inline-selection/NextSelectionButton.js @@ -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 ? (

{props.description}

) : '' + let icon = props.icon !== undefined ? () : '' + return ( + + + + + + ) +} + +nextSelectionButton.propTypes = { + title: PropTypes.string, + icon: FontAwesomeIcon, + description: PropTypes.string, + onButtonClick: PropTypes.func +} diff --git a/monkey/monkey_island/cc/ui/src/styles/Main.scss b/monkey/monkey_island/cc/ui/src/styles/Main.scss index e26220d0d..616217e52 100644 --- a/monkey/monkey_island/cc/ui/src/styles/Main.scss +++ b/monkey/monkey_island/cc/ui/src/styles/Main.scss @@ -12,6 +12,11 @@ @import 'components/PreviewPane'; @import 'components/AdvancedMultiSelect'; @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 diff --git a/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss b/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss new file mode 100644 index 000000000..ed75d8a9d --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/Icons.scss @@ -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); + } +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/RunOnIslandButton.scss b/monkey/monkey_island/cc/ui/src/styles/components/RunOnIslandButton.scss new file mode 100644 index 000000000..e13c0e8a4 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/RunOnIslandButton.scss @@ -0,0 +1,7 @@ +.monkey-on-island-run-state-icon { + display: inline-block; + position: absolute; + right: 23px; + top: 28%; + font-size: 1.1em; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/BackButton.scss b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/BackButton.scss new file mode 100644 index 000000000..02c5c409e --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/BackButton.scss @@ -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; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/CommandDisplay.scss b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/CommandDisplay.scss new file mode 100644 index 000000000..ebec04e20 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/CommandDisplay.scss @@ -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; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/InlineSelection.scss b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/InlineSelection.scss new file mode 100644 index 000000000..c66ccbd6d --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/InlineSelection.scss @@ -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; +} diff --git a/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/NextSelectionButton.scss b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/NextSelectionButton.scss new file mode 100644 index 000000000..71bac3053 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/inline-selection/NextSelectionButton.scss @@ -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; +} diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index 057ea6de2..7e7272a87 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -2,9 +2,9 @@ Flask-JWT-Extended==3.24.1 Flask-Pymongo>=2.3.0 Flask-Restful>=0.3.8 PyInstaller==3.6 -awscli>=1.18.131 -boto3>=1.14.54 -botocore>=1.17.54,<1.18.0 +awscli==1.18.131 +boto3==1.14.54 +botocore==1.17.54 cffi>=1.8,!=1.11.3 dpath>=2.0 flask>=1.1 @@ -26,4 +26,4 @@ virtualenv>=20.0.26 werkzeug>=1.0.1 wheel>=0.34.2 -pyjwt>=1.5.1 # not directly required, pinned by Snyk to avoid a vulnerability \ No newline at end of file +pyjwt>=1.5.1 # not directly required, pinned by Snyk to avoid a vulnerability