diff --git a/monkey/common/utils/exceptions.py b/monkey/common/utils/exceptions.py index 8396b423b..0658b74f3 100644 --- a/monkey/common/utils/exceptions.py +++ b/monkey/common/utils/exceptions.py @@ -52,3 +52,15 @@ class FindingWithoutDetailsError(Exception): class DomainControllerNameFetchError(FailedExploitationError): """ Raise on failed attempt to extract domain controller's name """ + + +class InvalidCredentialsError(Exception): + """ Raise when credentials supplied are invalid""" + + +class NoCredentialsError(Exception): + """ Raise when no credentials have been supplied""" + + +class InvalidConfigurationError(Exception): + """ Raise when configuration is invalid """ diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 9636a62a0..172513d91 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -42,6 +42,7 @@ from monkey_island.cc.resources.security_report import SecurityReport from monkey_island.cc.resources.T1216_pba_file_download import T1216PBAFileDownload from monkey_island.cc.resources.telemetry import Telemetry from monkey_island.cc.resources.telemetry_feed import TelemetryFeed +from monkey_island.cc.resources.temp_configuration import TempConfiguration from monkey_island.cc.resources.version_update import VersionUpdate from monkey_island.cc.resources.zero_trust.finding_event import ZeroTrustFindingEvent from monkey_island.cc.resources.zero_trust.scoutsuite_auth.aws_keys import AWSKeys @@ -118,6 +119,9 @@ def init_app_url_rules(app): def init_api_resources(api): + # TODO hook up to a proper endpoint + api.add_resource(TempConfiguration, "/api/temp_configuration") + api.add_resource(Root, "/api") api.add_resource(Registration, "/api/registration") api.add_resource(Authenticate, "/api/auth") diff --git a/monkey/monkey_island/cc/resources/temp_configuration.py b/monkey/monkey_island/cc/resources/temp_configuration.py new file mode 100644 index 000000000..1c558e7f8 --- /dev/null +++ b/monkey/monkey_island/cc/resources/temp_configuration.py @@ -0,0 +1,57 @@ +from dataclasses import dataclass + +import flask_restful + +from common.utils.exceptions import ( + InvalidConfigurationError, + InvalidCredentialsError, + NoCredentialsError, +) +from monkey_island.cc.resources.auth.auth import jwt_required + + +@dataclass +class ResponseContents: + import_status: str = "imported" + message: str = "" + status_code: int = 200 + + def form_response(self): + return self.__dict__, self.status_code + + +# TODO remove once backend implementation is done +class TempConfiguration(flask_restful.Resource): + SUCCESS = False + + @jwt_required + def post(self): + # request_contents = json.loads(request.data) + try: + self.decrypt() + self.import_config() + return ResponseContents().form_response() + except InvalidCredentialsError: + return ResponseContents( + import_status="wrong_password", message="Wrong password supplied", status_code=403 + ).form_response() + except InvalidConfigurationError: + return ResponseContents( + import_status="invalid_configuration", + message="Invalid configuration supplied. " + "Maybe the format is outdated or the file is malformed", + status_code=400, + ).form_response() + except NoCredentialsError: + return ResponseContents( + import_status="password_required", + message="Configuration file is protected with a password. " + "Please enter the password", + status_code=403, + ).form_response() + + def decrypt(self): + return False + + def import_config(self): + return True diff --git a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx index 68f2bdea9..37e4bb178 100644 --- a/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx +++ b/monkey/monkey_island/cc/ui/src/components/configuration-components/ImportConfigModal.tsx @@ -1,8 +1,11 @@ import {Button, Modal, Form} from 'react-bootstrap'; -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; + import AuthComponent from '../AuthComponent'; -import '../../styles/components/configuration-components/ExportConfigModal.scss'; +import '../../styles/components/configuration-components/ImportConfigModal.scss'; +import {faCheck, faCross} from '@fortawesome/free-solid-svg-icons'; type Props = { @@ -10,29 +13,85 @@ type Props = { onClick: () => void } + +const UploadStatuses = { + clean: 'clean', + success: 'success', + error: 'error' +} + + +const UploadStatusIcon = (props: { status: string }) => { + switch (props.status) { + case UploadStatuses.success: + return (); + case UploadStatuses.error: + return (); + default: + return null; + } +} + +// TODO add types const ConfigImportModal = (props: Props) => { // TODO implement the back end - const configExportEndpoint = '/api/configuration/export'; + const configImportEndpoint = '/api/temp_configuration'; - const [pass, setPass] = useState(''); - const [radioValue, setRadioValue] = useState('password'); + const [uploadStatus, setUploadStatus] = useState(UploadStatuses.clean); + const [importDisabled, setImportDisabled] = useState(true); + const [configContents, setConfigContents] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); const authComponent = new AuthComponent({}); - function isExportBtnDisabled() { - return pass === '' && radioValue === 'password'; - } + useEffect(() => { + if (configContents !== '') { + sendConfigToServer(); + } + }, [configContents]) - function onSubmit() { - authComponent.authFetch(configExportEndpoint, + + function sendConfigToServer() { + authComponent.authFetch(configImportEndpoint, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ - should_encrypt: (radioValue === 'password'), - password: pass + config: configContents, + password: password }) } - ) + ).then(res => res.json()) + .then(res => { + if (res['import_status'] === 'password_required') { + setShowPassword(true); + } else if (res['import_status'] === 'wrong_password'){ + setErrorMessage(res['message']); + } + if (res['import_status'] === 'invalid_configuration'){ + setUploadStatus(UploadStatuses.error); + setErrorMessage(res['message']); + } else { + setUploadStatus(UploadStatuses.success); + } + if (res.status == 200) { + setImportDisabled(false); + } + }) + } + + + function uploadFile(event) { + let reader = new FileReader(); + reader.onload = (event) => { + setConfigContents(JSON.stringify(event.target.result)) + }; + reader.readAsText(event.target.files[0]); + event.target.value = null; + } + + function onImportClick() { } return ( @@ -47,16 +106,20 @@ const ConfigImportModal = (props: Props) => {
- + + + {showPassword && }
@@ -64,7 +127,7 @@ const ConfigImportModal = (props: Props) => { } const PasswordInput = (props: { - onChange: (passValue) => void + onChange: (passValue) => void, }) => { return (
@@ -76,29 +139,5 @@ const PasswordInput = (props: { ) } -const ExportPlaintextChoiceField = (props: { - radioValue: string, - onChange: (radioValue) => void -}) => { - return ( -
- { - props.onChange(evt.target.value); - }} - /> -

- Configuration might contain stolen credentials or sensitive data.
- It is advised to use password encryption option. -

-
- ) -} - export default ConfigImportModal; diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js index 55484b175..2d89a15d5 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -358,6 +358,7 @@ class ConfigurePageComponent extends AuthComponent { this.authFetch(apiEndpoint, request_options); } + // TODO remove after import implementation setConfigOnImport = (event) => { try { var newConfig = JSON.parse(event.target.result); @@ -377,6 +378,7 @@ class ConfigurePageComponent extends AuthComponent { ); } + // TODO remove after import implementation setConfigFromImportCandidate(){ this.setState({ configuration: this.state.importCandidateConfig, @@ -388,6 +390,7 @@ class ConfigurePageComponent extends AuthComponent { this.currentFormData = {}; } + // TODO remove after export implementation exportConfig = () => { this.updateConfigSection(); const configAsJson = JSON.stringify(this.state.configuration, null, 2); @@ -415,6 +418,7 @@ class ConfigurePageComponent extends AuthComponent { })); } + // TODO remove after import implementation importConfig = (event) => { let reader = new FileReader(); reader.onload = this.setConfigOnImport; diff --git a/monkey/monkey_island/cc/ui/src/styles/components/configuration-components/ImportConfigModal.scss b/monkey/monkey_island/cc/ui/src/styles/components/configuration-components/ImportConfigModal.scss new file mode 100644 index 000000000..78add3bb3 --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/components/configuration-components/ImportConfigModal.scss @@ -0,0 +1,30 @@ +.config-export-modal .config-export-password-input p { + display: inline-block; + width: auto; + margin-top: 0; + margin-bottom: 0; + margin-right: 10px; +} + +.config-export-modal .export-type-radio-buttons +.password-radio-button .config-export-password-input input { + display: inline-block; + width: auto; + top: 0; + transform: none; +} + +.config-export-modal .export-type-radio-buttons .password-radio-button input{ + margin-top: 0; + top: 50%; + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} + +.config-export-modal div.config-export-plaintext p.export-warning { + margin-left: 20px; +} + +.config-export-modal div.config-export-plaintext { + margin-top: 15px; +}