diff --git a/.travis.yml b/.travis.yml index 6abeb59b1..4400f7e9e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ install: # Python - pip install -r monkey/monkey_island/requirements.txt # for unit tests - pip install flake8 pytest dlint # for next stages +- pip install coverage # for code coverage - pip install -r monkey/infection_monkey/requirements.txt # for unit tests before_script: @@ -23,24 +24,28 @@ before_script: script: # Check Python code -# Check syntax errors and fail the build if any are found. +## Check syntax errors and fail the build if any are found. - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics -# Warn about linter issues. -# --exit-zero forces Flake8 to use the exit status code 0 even if there are errors, which means this will NOT fail the build. -# --count will print the total number of errors. -# --statistics Count the number of occurrences of each error/warning code and print a report. -# The output is redirected to a file. +## Warn about linter issues. +### --exit-zero forces Flake8 to use the exit status code 0 even if there are errors, which means this will NOT fail the build. +### --count will print the total number of errors. +### --statistics Count the number of occurrences of each error/warning code and print a report. +### The output is redirected to a file. - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics > flake8_warnings.txt -# Display the linter issues +## Display the linter issues - cat flake8_warnings.txt -# Make sure that we haven't increased the amount of warnings. +## Make sure that we haven't increased the amount of warnings. - PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT=190 -- if [ $(tail -n 1 flake8_warnings.txt) -gt $PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT ]; then echo "Too many warnings! Failing this build. Lower the amount of linter errors in this and try again. " && exit 1; fi +- if [ $(tail -n 1 flake8_warnings.txt) -gt $PYTHON_WARNINGS_AMOUNT_UPPER_LIMIT ]; then echo "Too many python linter warnings! Failing this build. Lower the amount of linter errors in this and try again. " && exit 1; fi +## Run unit tests - cd monkey # This is our source dir - python -m pytest # Have to use `python -m pytest` instead of `pytest` to add "{$builddir}/monkey/monkey" to sys.path. +## Calculate Code Coverage +- coverage run -m pytest + # Check JS code. The npm install must happen AFTER the flake8 because the node_modules folder will cause a lot of errors. - cd monkey_island/cc/ui - npm i @@ -51,6 +56,10 @@ script: - JS_WARNINGS_AMOUNT_UPPER_LIMIT=37 - eslint ./src --max-warnings $JS_WARNINGS_AMOUNT_UPPER_LIMIT +after_success: + # Upload code coverage results to codecov.io, see https://github.com/codecov/codecov-bash for more information + - bash <(curl -s https://codecov.io/bash) + notifications: slack: # Notify to slack rooms: diff --git a/README.md b/README.md index dd1d7982b..83fd878ad 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Infection Monkey -[![Build Status](https://travis-ci.com/guardicore/monkey.svg?branch=develop)](https://travis-ci.com/guardicore/monkey) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/guardicore/monkey)](https://github.com/guardicore/monkey/releases) + +[![Build Status](https://travis-ci.com/guardicore/monkey.svg?branch=develop)](https://travis-ci.com/guardicore/monkey) +[![codecov](https://codecov.io/gh/guardicore/monkey/branch/develop/graph/badge.svg)](https://codecov.io/gh/guardicore/monkey) + ![GitHub stars](https://img.shields.io/github/stars/guardicore/monkey) ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/guardicore/monkey) @@ -14,14 +17,15 @@ The Infection Monkey is an open source security tool for testing a data center's - The Infection Monkey is comprised of two parts: -* Monkey - A tool which infects other machines and propagates to them -* Monkey Island - A dedicated server to control and visualize the Infection Monkey's progress inside the data center -To read more about the Monkey, visit http://infectionmonkey.com +* **Monkey** - A tool which infects other machines and propagates to them. +* **Monkey Island** - A dedicated server to control and visualize the Infection Monkey's progress inside the data center. + +To read more about the Monkey, visit [infectionmonkey.com](https://infectionmonkey.com). ## Main Features + The Infection Monkey uses the following techniques and exploits to propagate to other machines. * Multiple propagation techniques: @@ -42,12 +46,10 @@ Check out the [Setup](https://github.com/guardicore/monkey/wiki/setup) page in t The Infection Monkey supports a variety of platforms, documented [in the wiki](https://github.com/guardicore/monkey/wiki/OS-compatibility). - ## Building the Monkey from source To deploy development version of monkey you should refer to readme in the [deployment scripts](deployment_scripts) folder. If you only want to build the monkey from source, see [Setup](https://github.com/guardicore/monkey/wiki/Setup#compile-it-yourself) -and follow the instructions at the readme files under [infection_monkey](infection_monkey) and [monkey_island](monkey_island). - +and follow the instructions at the readme files under [infection_monkey](monkey/infection_monkey) and [monkey_island](monkey/monkey_island). ### Build status | Branch | Status | @@ -56,13 +58,21 @@ and follow the instructions at the readme files under [infection_monkey](infecti | Master | [![Build Status](https://travis-ci.com/guardicore/monkey.svg?branch=master)](https://travis-ci.com/guardicore/monkey) | ## Tests + ### Unit Tests + In order to run all of the Unit Tests, run the command `python -m pytest` in the `monkey` directory. +To get a coverage report, first make sure the `coverage` package is installed using `pip install coverage`. Run the command +`coverage run -m unittest` in the `monkey` directory and then `coverage html`. The coverage report can be found in +`htmlcov.index`. + ### Blackbox tests + In order to run the Blackbox tests, refer to `envs/monkey_zoo/blackbox/README.md`. # License + Copyright (c) Guardicore Ltd See the [LICENSE](LICENSE) file for license rights and limitations (GPLv3). diff --git a/deployment_scripts/README.md b/deployment_scripts/README.md index f69a48b77..16b150852 100644 --- a/deployment_scripts/README.md +++ b/deployment_scripts/README.md @@ -1,25 +1,55 @@ -# Files used to deploy development version of infection monkey -## Windows +# Deployment guide for a development environemnt -Before running the script you must have git installed.
-Cd to scripts directory and use the scripts.
-First argument is an empty directory (script can create one) and second is branch you want to clone. -Example usages:
-./run_script.bat (Sets up monkey in current directory under .\infection_monkey)
-./run_script.bat "C:\test" (Sets up monkey in C:\test)
-powershell -ExecutionPolicy ByPass -Command ". .\deploy_windows.ps1; Deploy-Windows -monkey_home C:\test" (Same as above)
-./run_script.bat "" "master"(Sets up master branch instead of develop in current dir) -Don't forget to add python to PATH or do so while installing it via this script.
+This guide is for you if you wish to develop for Infection Monkey. If you only want to use it, please download the relevant version from [our website](https://infectionmonkey.com). -## Linux +## Prerequisites -Linux deployment script is meant for Ubuntu 16.x machines. -You must have root permissions, but don't run the script as root.
-Launch deploy_linux.sh from scripts directory.
-First argument should be an absolute path of an empty directory (script will create one if doesn't exist, default is ./infection_monkey). -Second parameter is the branch you want to clone (develop by default). -Example usages:
-./deploy_linux.sh (deploys under ./infection_monkey)
-./deploy_linux.sh "/home/test/monkey" (deploys under /home/test/monkey)
-./deploy_linux.sh "" "master" (deploys master branch in script directory)
-./deploy_linux.sh "/home/user/new" "master" (if directory "new" is not found creates it and clones master branch into it)
+Before running the script you must have `git` installed. If you don't have `git` installed, please follow [this guide](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). + +## Deploy on Windows + +Run the following command in powershell: + +```powershell +Invoke-WebRequest https://raw.githubusercontent.com/guardicore/monkey/develop/deployment_scripts/deploy_windows.ps1 -OutFile deploy_windows.ps1 +``` + +This will download our deploy script. It's a good idea to read it quickly before executing it! + +After downloading that script, execute it in `powershell`. + +The first argument is an empty directory (script can create one). The second argument is which branch you want to clone - by default, the script will check out the `develop` branch. Some example usages: + +- `.\deploy_windows.ps1` (Sets up monkey in current directory under .\infection_monkey) +- `.\deploy_windows.ps1 -monkey_home "C:\test"` (Sets up monkey in C:\test) +- `.\deploy_windows.ps1 -branch "master"` (Sets up master branch instead of develop in current dir) + +You may also pass in an optional `agents=$false` parameter to disable downloading the latest agent binaries. + +### Troubleshooting + +- If you run into Execution Policy warnings, you can disable them by prefixing the following snippet: `powershell -ExecutionPolicy ByPass -Command "[original command here]"` +- Don't forget to add python to PATH or do so while installing it via this script. + +## Deploy on Linux + +Linux deployment script is meant for Ubuntu 16 and Ubuntu 18 machines. + +Your user must have root permissions; however, don't run the script as root! + +```sh +wget https://raw.githubusercontent.com/guardicore/monkey/develop/deployment_scripts/deploy_linux.sh +``` + +This will download our deploy script. It's a good idea to read it quickly before executing it! + +Then execute the resulting script with your shell. + +After downloading that script, execute it in a shell. The first argument should be an absolute path of an empty directory (the script will create one if doesn't exist, default is ./infection_monkey). The second parameter is the branch you want to clone (develop by default). Some example usages: + +- `./deploy_linux.sh` (deploys under ./infection_monkey) +- `./deploy_linux.sh "/home/test/monkey"` (deploys under /home/test/monkey) +- `./deploy_linux.sh "" "master"` (deploys master branch in script directory) +- `./deploy_linux.sh "/home/user/new" "master"` (if directory "new" is not found creates it and clones master branch into it) + +You may also pass in an optional third `false` parameter to disable downloading the latest agent binaries. \ No newline at end of file diff --git a/deployment_scripts/config b/deployment_scripts/config index fb7a3d5b6..5607d37fd 100644 --- a/deployment_scripts/config +++ b/deployment_scripts/config @@ -5,21 +5,17 @@ MONKEY_FOLDER_NAME="infection_monkey" MONKEY_GIT_URL="https://github.com/guardicore/monkey" # Monkey binaries -LINUX_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/monkey-linux-32" +LINUX_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/v1.7.0/monkey-linux-32" LINUX_32_BINARY_NAME="monkey-linux-32" -LINUX_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/monkey-linux-64" +LINUX_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/v1.7.0/monkey-linux-64" LINUX_64_BINARY_NAME="monkey-linux-64" -WINDOWS_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/monkey-windows-32.exe" +WINDOWS_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/v1.7.0/monkey-windows-32.exe" WINDOWS_32_BINARY_NAME="monkey-windows-32.exe" -WINDOWS_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/monkey-windows-64.exe" +WINDOWS_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/v1.7.0/monkey-windows-64.exe" WINDOWS_64_BINARY_NAME="monkey-windows-64.exe" # Other binaries for monkey -TRACEROUTE_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/traceroute64" -TRACEROUTE_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/traceroute32" -SAMBACRY_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/sc_monkey_runner64.so" -SAMBACRY_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/1.6/sc_monkey_runner32.so" - -# Mongo url's -MONGO_DEBIAN_URL="https://downloads.mongodb.org/linux/mongodb-linux-x86_64-debian81-latest.tgz" -MONGO_UBUNTU_URL="https://downloads.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-latest.tgz" +TRACEROUTE_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/v1.7.0/traceroute64" +TRACEROUTE_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/v1.7.0/traceroute32" +SAMBACRY_64_BINARY_URL="https://github.com/guardicore/monkey/releases/download/v1.7.0/sc_monkey_runner64.so" +SAMBACRY_32_BINARY_URL="https://github.com/guardicore/monkey/releases/download/v1.7.0/sc_monkey_runner32.so" \ No newline at end of file diff --git a/deployment_scripts/config.ps1 b/deployment_scripts/config.ps1 index 095f7b899..b18b7c63c 100644 --- a/deployment_scripts/config.ps1 +++ b/deployment_scripts/config.ps1 @@ -2,28 +2,33 @@ $MONKEY_FOLDER_NAME = "infection_monkey" # Url of public git repository that contains monkey's source code $MONKEY_GIT_URL = "https://github.com/guardicore/monkey" +$MONKEY_RELEASES_URL = $MONKEY_GIT_URL + "/releases" +$MONKEY_LATEST_VERSION = "v1.7.0" +$MONKEY_DOWNLOAD_URL = $MONKEY_RELEASES_URL + "/download/" + $MONKEY_LATEST_VERSION + "/" # Link to the latest python download or install it manually -$PYTHON_URL = "https://www.python.org/ftp/python/3.7.4/python-3.7.4-amd64.exe" +$PYTHON_URL = "https://www.python.org/ftp/python/3.7.6/python-3.7.6-amd64.exe" + # Monkey binaries -$LINUX_32_BINARY_URL = "https://github.com/guardicore/monkey/releases/download/1.6/monkey-linux-32" +$LINUX_32_BINARY_URL = $MONKEY_DOWNLOAD_URL + "monkey-linux-32" $LINUX_32_BINARY_PATH = "monkey-linux-32" -$LINUX_64_BINARY_URL = "https://github.com/guardicore/monkey/releases/download/1.6/monkey-linux-64" +$LINUX_64_BINARY_URL = $MONKEY_DOWNLOAD_URL + "monkey-linux-64" $LINUX_64_BINARY_PATH = "monkey-linux-64" -$WINDOWS_32_BINARY_URL = "https://github.com/guardicore/monkey/releases/download/1.6/monkey-windows-32.exe" +$WINDOWS_32_BINARY_URL = $MONKEY_DOWNLOAD_URL + "monkey-windows-32.exe" $WINDOWS_32_BINARY_PATH = "monkey-windows-32.exe" -$WINDOWS_64_BINARY_URL = "https://github.com/guardicore/monkey/releases/download/1.6/monkey-windows-64.exe" +$WINDOWS_64_BINARY_URL = $MONKEY_DOWNLOAD_URL + "monkey-windows-64.exe" $WINDOWS_64_BINARY_PATH = "monkey-windows-64.exe" -$SAMBA_32_BINARY_URL = "https://github.com/VakarisZ/tempBinaries/raw/master/sc_monkey_runner32.so" -$SAMBA_32_BINARY_NAME= "sc_monkey_runner32.so" -$SAMBA_64_BINARY_URL = "https://github.com/VakarisZ/tempBinaries/raw/master/sc_monkey_runner64.so" +$SAMBA_32_BINARY_URL = $MONKEY_DOWNLOAD_URL + "sc_monkey_runner32.so" +$SAMBA_32_BINARY_NAME = "sc_monkey_runner32.so" +$SAMBA_64_BINARY_URL = $MONKEY_DOWNLOAD_URL + "sc_monkey_runner64.so" $SAMBA_64_BINARY_NAME = "sc_monkey_runner64.so" +$TRACEROUTE_64_BINARY_URL = $MONKEY_DOWNLOAD_URL + "traceroute64" +$TRACEROUTE_32_BINARY_URL = $MONKEY_DOWNLOAD_URL + "traceroute32" # Other directories and paths ( most likely you dont need to configure) -$MONKEY_ISLAND_DIR = "\monkey\monkey_island" -$MONKEY_DIR = "\monkey\infection_monkey" +$MONKEY_ISLAND_DIR = Join-Path "\monkey" -ChildPath "monkey_island" +$MONKEY_DIR = Join-Path "\monkey" -ChildPath "infection_monkey" $SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\bin" -$PYTHON_DLL = "C:\Windows\System32\python27.dll" $MK32_DLL = "mk32.zip" $MK64_DLL = "mk64.zip" $TEMP_PYTHON_INSTALLER = ".\python.exe" @@ -31,16 +36,14 @@ $TEMP_MONGODB_ZIP = ".\mongodb.zip" $TEMP_OPEN_SSL_ZIP = ".\openssl.zip" $TEMP_CPP_INSTALLER = "cpp.exe" $TEMP_NPM_INSTALLER = "node.msi" -$TEMP_PYWIN32_INSTALLER = "pywin32.exe" $TEMP_UPX_ZIP = "upx.zip" -$UPX_FOLDER = "upx394w" +$UPX_FOLDER = "upx-3.96-win64" # Other url's -$MONGODB_URL = "https://downloads.mongodb.org/win32/mongodb-win32-x86_64-2008plus-ssl-latest.zip" -$OPEN_SSL_URL = "https://indy.fulgan.com/SSL/Archive/openssl-1.0.2l-i386-win32.zip" +$MONGODB_URL = "https://downloads.mongodb.org/win32/mongodb-win32-x86_64-2012plus-v4.2-latest.zip" +$OPEN_SSL_URL = "https://indy.fulgan.com/SSL/openssl-1.0.2u-x64_86-win64.zip" $CPP_URL = "https://go.microsoft.com/fwlink/?LinkId=746572" -$NPM_URL = "https://nodejs.org/dist/v10.13.0/node-v10.13.0-x64.msi" -$PYWIN32_URL = "https://github.com/mhammond/pywin32/releases/download/b225/pywin32-225.win-amd64-py3.7.exe" +$NPM_URL = "https://nodejs.org/dist/v12.14.1/node-v12.14.1-x64.msi" $MK32_DLL_URL = "https://github.com/guardicore/mimikatz/releases/download/1.1.0/mk32.zip" $MK64_DLL_URL = "https://github.com/guardicore/mimikatz/releases/download/1.1.0/mk64.zip" -$UPX_URL = "https://github.com/upx/upx/releases/download/v3.94/upx394w.zip" +$UPX_URL = "https://github.com/upx/upx/releases/download/v3.96/upx-3.96-win64.zip" diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh index 834d811a7..65fdd48e6 100755 --- a/deployment_scripts/deploy_linux.sh +++ b/deployment_scripts/deploy_linux.sh @@ -1,10 +1,54 @@ #!/bin/bash -source config exists() { command -v "$1" >/dev/null 2>&1 } +is_root() { + return $(id -u) +} + +has_sudo() { + # 0 true, 1 false + timeout 1 sudo id && return 0 || return 1 +} + +handle_error() { + echo "Fix the errors above and rerun the script" + exit 1 +} + +log_message() { + echo -e "\n\n" + echo -e "DEPLOYMENT SCRIPT: $1" +} + +config_branch=${2:-"develop"} +config_url="https://raw.githubusercontent.com/guardicore/monkey/${config_branch}/deployment_scripts/config" + +if (! exists curl) && (! exists wget); then + log_message 'Your system does not have curl or wget, exiting' + exit 1 +fi + +file=$(mktemp) +# shellcheck disable=SC2086 +if exists wget; then + # shellcheck disable=SC2086 + wget --output-document=$file "$config_url" +else + # shellcheck disable=SC2086 + curl -s -o $file "$config_url" +fi + +log_message "downloaded configuration" +# shellcheck source=deployment_scripts/config +# shellcheck disable=SC2086 +source $file +log_message "loaded configuration" +# shellcheck disable=SC2086 +# rm $file + # Setup monkey either in dir required or current dir monkey_home=${1:-$(pwd)} if [[ $monkey_home == $(pwd) ]]; then @@ -13,26 +57,19 @@ fi # We can set main paths after we know the home dir ISLAND_PATH="$monkey_home/monkey/monkey_island" -MONKEY_COMMON_PATH="$monkey_home/monkey/common/" MONGO_PATH="$ISLAND_PATH/bin/mongodb" ISLAND_BINARIES_PATH="$ISLAND_PATH/cc/binaries" INFECTION_MONKEY_DIR="$monkey_home/monkey/infection_monkey" MONKEY_BIN_DIR="$INFECTION_MONKEY_DIR/bin" -handle_error() { - echo "Fix the errors above and rerun the script" +if is_root; then + log_message "Please don't run this script as root" exit 1 -} +fi -log_message() { - echo -e "\n\n-------------------------------------------" - echo -e "DEPLOYMENT SCRIPT: $1" - echo -e "-------------------------------------------\n" -} - -sudo -v -if [[ $? != 0 ]]; then - echo "You need root permissions for some of this script operations. Quiting." +HAS_SUDO=$(has_sudo) +if [[ ! $HAS_SUDO ]]; then + log_message "You need root permissions for some of this script operations. Quiting." exit 1 fi @@ -41,15 +78,10 @@ if [[ ! -d ${monkey_home} ]]; then fi if ! exists git; then - echo "Please install git and re-run this script" + log_message "Please install git and re-run this script" exit 1 fi -if ! exists wget; then - echo 'Your system does have wget, please install and re-run this script' - exit 1 -fi - log_message "Cloning files from git" branch=${2:-"develop"} if [[ ! -d "$monkey_home/monkey" ]]; then # If not already cloned @@ -59,7 +91,7 @@ fi # Create folders log_message "Creating island dirs under $ISLAND_PATH" -mkdir -p "${MONGO_PATH}" +mkdir -p "${MONGO_PATH}" || handle_error mkdir -p "${ISLAND_BINARIES_PATH}" || handle_error # Detecting command that calls python 3.7 @@ -78,87 +110,114 @@ if [[ ${python_cmd} == "" ]]; then log_message "Python 3.7 command not found. Installing python 3.7." sudo add-apt-repository ppa:deadsnakes/ppa sudo apt-get update - sudo apt install python3.7 + sudo apt install python3.7 python3.7-dev log_message "Python 3.7 is now available with command 'python3.7'." python_cmd="python3.7" fi -log_message "Updating package list" -sudo apt-get update +log_message "Installing build-essential" +sudo apt install build-essential -log_message "Installing pip" -sudo apt install python3-pip -${python_cmd} -m pip install pip - -log_message "Install python3.7-dev" -sudo apt-get install python3.7-dev +log_message "Installing or updating pip" +# shellcheck disable=SC2086 +pip_url=https://bootstrap.pypa.io/get-pip.py +if exists wget; then + wget --output-document=get-pip.py $pip_url +else + curl $pip_url -o get-pip.py +fi +${python_cmd} get-pip.py +rm get-pip.py log_message "Installing island requirements" -requirements="$ISLAND_PATH/requirements.txt" -${python_cmd} -m pip install --user --upgrade -r ${requirements} || handle_error +requirements_island="$ISLAND_PATH/requirements.txt" +${python_cmd} -m pip install -r "${requirements_island}" --user --upgrade || handle_error log_message "Installing monkey requirements" sudo apt-get install libffi-dev upx libssl-dev libc++1 -cd "${monkey_home}"/monkey/infection_monkey || handle_error -${python_cmd} -m pip install -r requirements.txt --user --upgrade || handle_error +requirements_monkey="$INFECTION_MONKEY_DIR/requirements.txt" +${python_cmd} -m pip install -r "${requirements_monkey}" --user --upgrade || handle_error + +agents=${3:-true} # Download binaries -log_message "Downloading binaries" -wget -c -N -P ${ISLAND_BINARIES_PATH} ${LINUX_32_BINARY_URL} -wget -c -N -P ${ISLAND_BINARIES_PATH} ${LINUX_64_BINARY_URL} -wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_32_BINARY_URL} -wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_64_BINARY_URL} +if [ "$agents" = true ] ; then + log_message "Downloading binaries" + if exists wget; then + wget -c -N -P ${ISLAND_BINARIES_PATH} ${LINUX_32_BINARY_URL} + wget -c -N -P ${ISLAND_BINARIES_PATH} ${LINUX_64_BINARY_URL} + wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_32_BINARY_URL} + wget -c -N -P ${ISLAND_BINARIES_PATH} ${WINDOWS_64_BINARY_URL} + else + curl -o ${ISLAND_BINARIES_PATH}\monkey-linux-32 ${LINUX_32_BINARY_URL} + curl -o ${ISLAND_BINARIES_PATH}\monkey-linux-64 ${LINUX_64_BINARY_URL} + curl -o ${ISLAND_BINARIES_PATH}\monkey-windows-32.exe ${WINDOWS_32_BINARY_URL} + curl -o ${ISLAND_BINARIES_PATH}\monkey-windows-64.exe ${WINDOWS_64_BINARY_URL} + fi +fi + # Allow them to be executed chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_32_BINARY_NAME" chmod a+x "$ISLAND_BINARIES_PATH/$LINUX_64_BINARY_NAME" -# Get machine type/kernel version -kernel=$(uname -m) -linux_dist=$(lsb_release -a 2>/dev/null) - # If a user haven't installed mongo manually check if we can install it with our script -log_message "Installing MongoDB" -"${ISLAND_PATH}"/linux/install_mongo.sh ${MONGO_PATH} || handle_error - +if ! exists mongod; then + log_message "Installing MongoDB" + "${ISLAND_PATH}"/linux/install_mongo.sh ${MONGO_PATH} || handle_error +fi log_message "Installing openssl" sudo apt-get install openssl # Generate SSL certificate log_message "Generating certificate" -cd "${ISLAND_PATH}" || { - echo "cd failed" - exit 1 -} -openssl genrsa -out cc/server.key 2048 -openssl req -new -key cc/server.key -out cc/server.csr -subj "/C=GB/ST=London/L=London/O=Global Security/OU=Monkey Department/CN=monkey.com" -openssl x509 -req -days 366 -in cc/server.csr -signkey cc/server.key -out cc/server.crt + +"${ISLAND_PATH}"/linux/create_certificate.sh ${ISLAND_PATH}/cc # Update node -log_message "Installing nodejs" -cd "$ISLAND_PATH/cc/ui" || handle_error -sudo apt-get install curl -curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - -sudo apt-get install -y nodejs +if ! exists npm; then + log_message "Installing nodejs" + node_src=https://deb.nodesource.com/setup_12.x + if exists curl; then + curl -sL $node_src | sudo -E bash - + else + wget -q -O - $node_src | sudo -E bash - + fi + sudo apt-get install -y nodejs +fi + +pushd "$ISLAND_PATH/cc/ui" || handle_error npm install sass-loader node-sass webpack --save-dev npm update log_message "Generating front end" npm run dist +popd || handle_error # Making dir for binaries mkdir "${MONKEY_BIN_DIR}" # Download sambacry binaries log_message "Downloading sambacry binaries" -wget -c -N -P "${MONKEY_BIN_DIR}" "${SAMBACRY_64_BINARY_URL}" -wget -c -N -P "${MONKEY_BIN_DIR}" "${SAMBACRY_32_BINARY_URL}" - +# shellcheck disable=SC2086 +if exists wget; then + wget -c -N -P "${MONKEY_BIN_DIR}" ${SAMBACRY_64_BINARY_URL} + wget -c -N -P "${MONKEY_BIN_DIR}" ${SAMBACRY_32_BINARY_URL} +else + curl -o ${MONKEY_BIN_DIR}/sc_monkey_runner64.so ${SAMBACRY_64_BINARY_URL} + curl -o ${MONKEY_BIN_DIR}/sc_monkey_runner32.so ${SAMBACRY_32_BINARY_URL} +fi # Download traceroute binaries log_message "Downloading traceroute binaries" -wget -c -N -P "${MONKEY_BIN_DIR}" "${TRACEROUTE_64_BINARY_URL}" -wget -c -N -P "${MONKEY_BIN_DIR}" "${TRACEROUTE_32_BINARY_URL}" +# shellcheck disable=SC2086 +if exists wget; then + wget -c -N -P "${MONKEY_BIN_DIR}" ${TRACEROUTE_64_BINARY_URL} + wget -c -N -P "${MONKEY_BIN_DIR}" ${TRACEROUTE_32_BINARY_URL} +else + curl -o ${MONKEY_BIN_DIR}/traceroute64 ${TRACEROUTE_64_BINARY_URL} + curl -o ${MONKEY_BIN_DIR}/traceroute32 ${TRACEROUTE_32_BINARY_URL} +fi -sudo chmod +x "${monkey_home}"/monkey/infection_monkey/build_linux.sh +sudo chmod +x "${INFECTION_MONKEY_DIR}/build_linux.sh" log_message "Deployment script finished." exit 0 diff --git a/deployment_scripts/deploy_windows.ps1 b/deployment_scripts/deploy_windows.ps1 index dd602e199..003fdd061 100644 --- a/deployment_scripts/deploy_windows.ps1 +++ b/deployment_scripts/deploy_windows.ps1 @@ -1,17 +1,40 @@ -function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, [String] $branch = "develop"){ - # Import the config variables - . ./config.ps1 - "Config variables from config.ps1 imported" - - # If we want monkey in current dir we need to create an empty folder for source files - if ( (Join-Path $monkey_home '') -eq (Join-Path (Get-Item -Path ".\").FullName '') ){ - $monkey_home = Join-Path -Path $monkey_home -ChildPath $MONKEY_FOLDER_NAME - } +param( + [Parameter(Mandatory = $false, Position = 0)] + [String] $monkey_home = (Get-Item -Path ".\").FullName, + [Parameter(Mandatory = $false, Position = 1)] + [System.String] + $branch = "develop", + [Parameter(Mandatory = $false, Position = 2)] + [Bool] + $agents = $true +) +function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, [String] $branch = "develop") +{ + Write-Output "Downloading to $monkey_home" + Write-Output "Branch $branch" # Set variables for script execution [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $webClient = New-Object System.Net.WebClient + + # Import the config variables + $config_filename = New-TemporaryFile + $config_filename = "config.ps1" + $config_url = "https://raw.githubusercontent.com/guardicore/monkey/" + $branch + "/deployment_scripts/config.ps1" + $webClient.DownloadFile($config_url, $config_filename) + . ./config.ps1 + "Config variables from config.ps1 imported" + Remove-Item $config_filename + + + # If we want monkey in current dir we need to create an empty folder for source files + if ((Join-Path $monkey_home '') -eq (Join-Path (Get-Item -Path ".\").FullName '')) + { + $monkey_home = Join-Path -Path $monkey_home -ChildPath $MONKEY_FOLDER_NAME + } + + # We check if git is installed try { @@ -25,15 +48,22 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, } # Download the monkey - $output = cmd.exe /c "git clone --single-branch -b $branch $MONKEY_GIT_URL $monkey_home 2>&1" + $command = "git clone --single-branch -b $branch $MONKEY_GIT_URL $monkey_home 2>&1" + Write-Output $command + $output = cmd.exe /c $command $binDir = (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "\bin") - if ( $output -like "*already exists and is not an empty directory.*"){ + if ($output -like "*already exists and is not an empty directory.*") + { "Assuming you already have the source directory. If not, make sure to set an empty directory as monkey's home directory." - } elseif ($output -like "fatal:*"){ + } + elseif ($output -like "fatal:*") + { "Error while cloning monkey from the repository:" $output return - } else { + } + else + { "Monkey cloned from the repository" # Create bin directory New-Item -ItemType directory -path $binDir @@ -44,9 +74,12 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, try { $version = cmd.exe /c '"python" --version 2>&1' - if ( $version -like 'Python 3.*' ) { + if ($version -like 'Python 3.*') + { "Python 3.* was found, installing dependencies" - } else { + } + else + { throw System.Management.Automation.CommandNotFoundException } } @@ -56,11 +89,12 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, "Select 'add to PATH' when installing" $webClient.DownloadFile($PYTHON_URL, $TEMP_PYTHON_INSTALLER) Start-Process -Wait $TEMP_PYTHON_INSTALLER -ErrorAction Stop - $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") Remove-Item $TEMP_PYTHON_INSTALLER # Check if installed correctly $version = cmd.exe /c '"python" --version 2>&1' - if ( $version -like '* is not recognized*' ) { + if ($version -like '* is not recognized*') + { "Python is not found in PATH. Add it to PATH and relaunch the script." return } @@ -69,7 +103,8 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, "Upgrading pip..." $output = cmd.exe /c 'python -m pip install --user --upgrade pip 2>&1' $output - if ( $output -like '*No module named pip*' ) { + if ($output -like '*No module named pip*') + { "Make sure pip module is installed and re-run this script." return } @@ -83,20 +118,24 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, $user_python_dir = cmd.exe /c 'py -m site --user-site' $user_python_dir = Join-Path (Split-Path $user_python_dir) -ChildPath "\Scripts" - if(!($ENV:PATH | Select-String -SimpleMatch $user_python_dir)){ + if (!($ENV:Path | Select-String -SimpleMatch $user_python_dir)) + { "Adding python scripts path to user's env" - $env:Path += ";"+$user_python_dir - [Environment]::SetEnvironmentVariable("Path",$env:Path,"User") + $env:Path += ";" + $user_python_dir + [Environment]::SetEnvironmentVariable("Path", $env:Path, "User") } # Download mongodb - if(!(Test-Path -Path (Join-Path -Path $binDir -ChildPath "mongodb") )){ + if (!(Test-Path -Path (Join-Path -Path $binDir -ChildPath "mongodb"))) + { "Downloading mongodb ..." $webClient.DownloadFile($MONGODB_URL, $TEMP_MONGODB_ZIP) "Unzipping mongodb" Expand-Archive $TEMP_MONGODB_ZIP -DestinationPath $binDir # Get unzipped folder's name - $mongodb_folder = Get-ChildItem -Path $binDir | Where-Object -FilterScript {($_.Name -like "mongodb*")} | Select-Object -ExpandProperty Name + $mongodb_folder = Get-ChildItem -Path $binDir | Where-Object -FilterScript { + ($_.Name -like "mongodb*") + } | Select-Object -ExpandProperty Name # Move all files from extracted folder to mongodb folder New-Item -ItemType directory -Path (Join-Path -Path $binDir -ChildPath "mongodb") New-Item -ItemType directory -Path (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "db") @@ -127,23 +166,30 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, . .\windows\create_certificate.bat Pop-Location - # Adding binaries - "Adding binaries" - $binaries = (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "\cc\binaries") - New-Item -ItemType directory -path $binaries -ErrorAction SilentlyContinue - $webClient.DownloadFile($LINUX_32_BINARY_URL, (Join-Path -Path $binaries -ChildPath $LINUX_32_BINARY_PATH)) - $webClient.DownloadFile($LINUX_64_BINARY_URL, (Join-Path -Path $binaries -ChildPath $LINUX_64_BINARY_PATH)) - $webClient.DownloadFile($WINDOWS_32_BINARY_URL, (Join-Path -Path $binaries -ChildPath $WINDOWS_32_BINARY_PATH)) - $webClient.DownloadFile($WINDOWS_64_BINARY_URL, (Join-Path -Path $binaries -ChildPath $WINDOWS_64_BINARY_PATH)) + if ($agents) + { + # Adding binaries + "Adding binaries" + $binaries = (Join-Path -Path $monkey_home -ChildPath $MONKEY_ISLAND_DIR | Join-Path -ChildPath "\cc\binaries") + New-Item -ItemType directory -path $binaries -ErrorAction SilentlyContinue + $webClient.DownloadFile($LINUX_32_BINARY_URL, (Join-Path -Path $binaries -ChildPath $LINUX_32_BINARY_PATH)) + $webClient.DownloadFile($LINUX_64_BINARY_URL, (Join-Path -Path $binaries -ChildPath $LINUX_64_BINARY_PATH)) + $webClient.DownloadFile($WINDOWS_32_BINARY_URL, (Join-Path -Path $binaries -ChildPath $WINDOWS_32_BINARY_PATH)) + $webClient.DownloadFile($WINDOWS_64_BINARY_URL, (Join-Path -Path $binaries -ChildPath $WINDOWS_64_BINARY_PATH)) + } + # Check if NPM installed "Installing npm" try { $version = cmd.exe /c '"npm" --version 2>&1' - if ( $version -like "*is not recognized*"){ + if ($version -like "*is not recognized*") + { throw System.Management.Automation.CommandNotFoundException - } else { + } + else + { "Npm already installed" } } @@ -152,7 +198,7 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, "Downloading npm ..." $webClient.DownloadFile($NPM_URL, $TEMP_NPM_INSTALLER) Start-Process -Wait $TEMP_NPM_INSTALLER - $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") Remove-Item $TEMP_NPM_INSTALLER } @@ -162,18 +208,13 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, & npm run dist Pop-Location - # Install pywin32 - "Downloading pywin32" - $webClient.DownloadFile($PYWIN32_URL, $TEMP_PYWIN32_INSTALLER) - Start-Process -Wait $TEMP_PYWIN32_INSTALLER -ErrorAction Stop - Remove-Item $TEMP_PYWIN32_INSTALLER - # Create infection_monkey/bin directory if not already present $binDir = (Join-Path -Path $monkey_home -ChildPath $MONKEY_DIR | Join-Path -ChildPath "\bin") New-Item -ItemType directory -path $binaries -ErrorAction SilentlyContinue # Download upx - if(!(Test-Path -Path (Join-Path -Path $binDir -ChildPath "upx.exe") )){ + if (!(Test-Path -Path (Join-Path -Path $binDir -ChildPath "upx.exe"))) + { "Downloading upx ..." $webClient.DownloadFile($UPX_URL, $TEMP_UPX_ZIP) "Unzipping upx" @@ -187,12 +228,14 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, # Download mimikatz binaries $mk32_path = Join-Path -Path $binDir -ChildPath $MK32_DLL - if(!(Test-Path -Path $mk32_path )){ + if (!(Test-Path -Path $mk32_path)) + { "Downloading mimikatz 32 binary" $webClient.DownloadFile($MK32_DLL_URL, $mk32_path) } $mk64_path = Join-Path -Path $binDir -ChildPath $MK64_DLL - if(!(Test-Path -Path $mk64_path )){ + if (!(Test-Path -Path $mk64_path)) + { "Downloading mimikatz 64 binary" $webClient.DownloadFile($MK64_DLL_URL, $mk64_path) } @@ -200,12 +243,14 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, # Download sambacry binaries $samba_path = Join-Path -Path $monkey_home -ChildPath $SAMBA_BINARIES_DIR $samba32_path = Join-Path -Path $samba_path -ChildPath $SAMBA_32_BINARY_NAME - if(!(Test-Path -Path $samba32_path )){ + if (!(Test-Path -Path $samba32_path)) + { "Downloading sambacry 32 binary" $webClient.DownloadFile($SAMBA_32_BINARY_URL, $samba32_path) } $samba64_path = Join-Path -Path $samba_path -ChildPath $SAMBA_64_BINARY_NAME - if(!(Test-Path -Path $samba64_path )){ + if (!(Test-Path -Path $samba64_path)) + { "Downloading sambacry 64 binary" $webClient.DownloadFile($SAMBA_64_BINARY_URL, $samba64_path) } @@ -213,3 +258,4 @@ function Deploy-Windows([String] $monkey_home = (Get-Item -Path ".\").FullName, "Script finished" } +Deploy-Windows -monkey_home $monkey_home -branch $branch diff --git a/deployment_scripts/run_script.bat b/deployment_scripts/run_script.bat deleted file mode 100644 index 3dcd62760..000000000 --- a/deployment_scripts/run_script.bat +++ /dev/null @@ -1,8 +0,0 @@ -SET command=. .\deploy_windows.ps1; Deploy-Windows -if NOT "%~1" == "" ( - SET "command=%command% -monkey_home %~1" -) -if NOT "%~2" == "" ( - SET "command=%command% -branch %~2" -) -powershell -ExecutionPolicy ByPass -Command %command% \ No newline at end of file diff --git a/monkey/codecov.yml b/monkey/codecov.yml new file mode 100644 index 000000000..8d5127230 --- /dev/null +++ b/monkey/codecov.yml @@ -0,0 +1,20 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "50...90" + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,tree" + behavior: default + require_changes: no diff --git a/monkey/common/BUILD b/monkey/common/BUILD new file mode 100644 index 000000000..90012116c --- /dev/null +++ b/monkey/common/BUILD @@ -0,0 +1 @@ +dev \ No newline at end of file diff --git a/monkey/common/cloud/all_instances.py b/monkey/common/cloud/all_instances.py new file mode 100644 index 000000000..6387730f6 --- /dev/null +++ b/monkey/common/cloud/all_instances.py @@ -0,0 +1,12 @@ +from typing import List + +from common.cloud.aws.aws_instance import AwsInstance +from common.cloud.azure.azure_instance import AzureInstance +from common.cloud.gcp.gcp_instance import GcpInstance +from common.cloud.instance import CloudInstance + +all_cloud_instances = [AwsInstance(), AzureInstance(), GcpInstance()] + + +def get_all_cloud_instances() -> List[CloudInstance]: + return all_cloud_instances diff --git a/monkey/common/cloud/aws/__init__.py b/monkey/common/cloud/aws/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/common/cloud/aws_instance.py b/monkey/common/cloud/aws/aws_instance.py similarity index 92% rename from monkey/common/cloud/aws_instance.py rename to monkey/common/cloud/aws/aws_instance.py index 4339fbcf4..03c5482ba 100644 --- a/monkey/common/cloud/aws_instance.py +++ b/monkey/common/cloud/aws/aws_instance.py @@ -6,6 +6,9 @@ import logging __author__ = 'itay.mizeretz' +from common.cloud.environment_names import Environment +from common.cloud.instance import CloudInstance + AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS = "169.254.169.254" AWS_LATEST_METADATA_URI_PREFIX = 'http://{0}/latest/'.format(AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS) ACCOUNT_ID_KEY = "accountId" @@ -13,10 +16,15 @@ ACCOUNT_ID_KEY = "accountId" logger = logging.getLogger(__name__) -class AwsInstance(object): +class AwsInstance(CloudInstance): """ Class which gives useful information about the current instance you're on. """ + def is_instance(self): + return self.instance_id is not None + + def get_cloud_provider_name(self) -> Environment: + return Environment.AWS def __init__(self): self.instance_id = None @@ -57,9 +65,6 @@ class AwsInstance(object): def get_region(self): return self.region - def is_aws_instance(self): - return self.instance_id is not None - @staticmethod def _extract_account_id(instance_identity_document_response): """ diff --git a/monkey/common/cloud/aws_service.py b/monkey/common/cloud/aws/aws_service.py similarity index 98% rename from monkey/common/cloud/aws_service.py rename to monkey/common/cloud/aws/aws_service.py index 6ef385542..a42c2e1dd 100644 --- a/monkey/common/cloud/aws_service.py +++ b/monkey/common/cloud/aws/aws_service.py @@ -4,7 +4,7 @@ import boto3 import botocore from botocore.exceptions import ClientError -from common.cloud.aws_instance import AwsInstance +from common.cloud.aws.aws_instance import AwsInstance __author__ = ['itay.mizeretz', 'shay.nehmad'] diff --git a/monkey/common/cloud/aws_service_test.py b/monkey/common/cloud/aws/aws_service_test.py similarity index 100% rename from monkey/common/cloud/aws_service_test.py rename to monkey/common/cloud/aws/aws_service_test.py diff --git a/monkey/common/cloud/azure/__init__.py b/monkey/common/cloud/azure/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/common/cloud/azure/azure_instance.py b/monkey/common/cloud/azure/azure_instance.py new file mode 100644 index 000000000..ec910fb98 --- /dev/null +++ b/monkey/common/cloud/azure/azure_instance.py @@ -0,0 +1,55 @@ +import logging +import requests + +from common.cloud.environment_names import Environment +from common.cloud.instance import CloudInstance + +LATEST_AZURE_METADATA_API_VERSION = "2019-04-30" +AZURE_METADATA_SERVICE_URL = "http://169.254.169.254/metadata/instance?api-version=%s" % LATEST_AZURE_METADATA_API_VERSION + +logger = logging.getLogger(__name__) + + +class AzureInstance(CloudInstance): + """ + Access to useful information about the current machine if it's an Azure VM. + Based on Azure metadata service: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service + """ + def is_instance(self): + return self.on_azure + + def get_cloud_provider_name(self) -> Environment: + return Environment.AZURE + + def __init__(self): + """ + Determines if on Azure and if so, gets some basic metadata on this instance. + """ + self.instance_name = None + self.instance_id = None + self.location = None + self.on_azure = False + + try: + response = requests.get(AZURE_METADATA_SERVICE_URL, headers={"Metadata": "true"}) + self.on_azure = True + + # If not on cloud, the metadata URL is non-routable and the connection will fail. + # If on AWS, should get 404 since the metadata service URL is different, so bool(response) will be false. + if response: + logger.debug("On Azure. Trying to parse metadata.") + self.try_parse_response(response) + else: + logger.warning("On Azure, but metadata response not ok: {}".format(response.status_code)) + except requests.RequestException: + logger.debug("Failed to get response from Azure metadata service: This instance is not on Azure.") + self.on_azure = False + + def try_parse_response(self, response): + try: + response_data = response.json() + self.instance_name = response_data["compute"]["name"] + self.instance_id = response_data["compute"]["vmId"] + self.location = response_data["compute"]["location"] + except KeyError: + logger.exception("Error while parsing response from Azure metadata service.") diff --git a/monkey/common/cloud/environment_names.py b/monkey/common/cloud/environment_names.py new file mode 100644 index 000000000..945d438ce --- /dev/null +++ b/monkey/common/cloud/environment_names.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class Environment(Enum): + UNKNOWN = "Unknown" + ON_PREMISE = "On Premise" + AZURE = "Azure" + AWS = "AWS" + GCP = "GCP" + ALIBABA = "Alibaba Cloud" + IBM = "IBM Cloud" + DigitalOcean = "Digital Ocean" + + +ALL_ENVIRONMENTS_NAMES = [x.value for x in Environment] diff --git a/monkey/common/cloud/gcp/__init__.py b/monkey/common/cloud/gcp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/common/cloud/gcp/gcp_instance.py b/monkey/common/cloud/gcp/gcp_instance.py new file mode 100644 index 000000000..184465bf5 --- /dev/null +++ b/monkey/common/cloud/gcp/gcp_instance.py @@ -0,0 +1,43 @@ +import logging +import requests + +from common.cloud.environment_names import Environment +from common.cloud.instance import CloudInstance + +logger = logging.getLogger(__name__) + + +GCP_METADATA_SERVICE_URL = "http://metadata.google.internal/" + + +class GcpInstance(CloudInstance): + """ + Used to determine if on GCP. See https://cloud.google.com/compute/docs/storing-retrieving-metadata#runninggce + """ + def is_instance(self): + return self.on_gcp + + def get_cloud_provider_name(self) -> Environment: + return Environment.GCP + + def __init__(self): + self.on_gcp = False + + try: + # If not on GCP, this domain shouldn't resolve. + response = requests.get(GCP_METADATA_SERVICE_URL) + + if response: + logger.debug("Got ok metadata response: on GCP") + self.on_gcp = True + + if "Metadata-Flavor" not in response.headers: + logger.warning("Got unexpected GCP Metadata format") + else: + if not response.headers["Metadata-Flavor"] == "Google": + logger.warning("Got unexpected Metadata flavor: {}".format(response.headers["Metadata-Flavor"])) + else: + logger.warning("On GCP, but metadata response not ok: {}".format(response.status_code)) + except requests.RequestException: + logger.debug("Failed to get response from GCP metadata service: This instance is not on GCP") + self.on_gcp = False diff --git a/monkey/common/cloud/instance.py b/monkey/common/cloud/instance.py new file mode 100644 index 000000000..abe0c7910 --- /dev/null +++ b/monkey/common/cloud/instance.py @@ -0,0 +1,14 @@ +from common.cloud.environment_names import Environment + + +class CloudInstance(object): + """ + This is an abstract class which represents a cloud instance. + + The current machine can be a cloud instance (for example EC2 instance or Azure VM). + """ + def is_instance(self) -> bool: + raise NotImplementedError() + + def get_cloud_provider_name(self) -> Environment: + raise NotImplementedError() diff --git a/monkey/common/cmd/aws/aws_cmd_runner.py b/monkey/common/cmd/aws/aws_cmd_runner.py index 459a42129..1ab680c4d 100644 --- a/monkey/common/cmd/aws/aws_cmd_runner.py +++ b/monkey/common/cmd/aws/aws_cmd_runner.py @@ -1,6 +1,6 @@ import logging -from common.cloud.aws_service import AwsService +from common.cloud.aws.aws_service import AwsService from common.cmd.aws.aws_cmd_result import AwsCmdResult from common.cmd.cmd_runner import CmdRunner from common.cmd.cmd_status import CmdStatus diff --git a/monkey/common/data/system_info_collectors_names.py b/monkey/common/data/system_info_collectors_names.py new file mode 100644 index 000000000..831bbe142 --- /dev/null +++ b/monkey/common/data/system_info_collectors_names.py @@ -0,0 +1,4 @@ +AWS_COLLECTOR = "AwsCollector" +HOSTNAME_COLLECTOR = "HostnameCollector" +ENVIRONMENT_COLLECTOR = "EnvironmentCollector" +PROCESS_LIST_COLLECTOR = "ProcessListCollector" diff --git a/monkey/common/network/network_range.py b/monkey/common/network/network_range.py index e39da1d03..15e04f893 100644 --- a/monkey/common/network/network_range.py +++ b/monkey/common/network/network_range.py @@ -46,6 +46,7 @@ class NetworkRange(object, metaclass=ABCMeta): def get_range_obj(address_str): if not address_str: # Empty string return None + address_str = address_str.strip() if NetworkRange.check_if_range(address_str): return IpRange(ip_range=address_str) if -1 != address_str.find('/'): diff --git a/monkey/common/version.py b/monkey/common/version.py new file mode 100644 index 000000000..9d60e636c --- /dev/null +++ b/monkey/common/version.py @@ -0,0 +1,25 @@ +# To get the version from shell, run `python ./version.py` (see `python ./version.py -h` for details). +import argparse +from pathlib import Path + +MAJOR = "1" +MINOR = "8" +PATCH = "0" +build_file_path = Path(__file__).parent.joinpath("BUILD") +with open(build_file_path, "r") as build_file: + BUILD = build_file.read() + + +def get_version(build=BUILD): + return f"{MAJOR}.{MINOR}.{PATCH}+{build}" + + +def print_version(): + parser = argparse.ArgumentParser() + parser.add_argument("-b", "--build", default=BUILD, help="Choose the build string for this version.", type=str) + args = parser.parse_args() + print(get_version(args.build)) + + +if __name__ == '__main__': + print_version() diff --git a/monkey/infection_monkey/build_linux.sh b/monkey/infection_monkey/build_linux.sh index fcaf4c75d..68abd4758 100644 --- a/monkey/infection_monkey/build_linux.sh +++ b/monkey/infection_monkey/build_linux.sh @@ -1,2 +1,17 @@ #!/bin/bash + +# Allow custom build ID +# If the first argument is not empty... +if [[ -n "$1" ]] +then + # Validate argument is a valid build string + if [[ "$1" =~ ^[\da-zA-Z]*$ ]] + then + # And put it in the BUILD file + echo "$1" > ../common/BUILD + else + echo "Build ID $1 invalid!" + fi +fi + pyinstaller -F --log-level=DEBUG --clean monkey.spec diff --git a/monkey/infection_monkey/build_windows.bat b/monkey/infection_monkey/build_windows.bat index f763bda6b..93e4e4a42 100644 --- a/monkey/infection_monkey/build_windows.bat +++ b/monkey/infection_monkey/build_windows.bat @@ -1 +1,12 @@ +REM Check if build ID was passed to the build script. +if "%1"=="" GOTO START_BUILD + +REM Validate build ID +echo %1|findstr /r "^[0-9a-zA-Z]*$" +if %errorlevel% neq 0 (exit /b %errorlevel%) + +REM replace build ID +echo %1> ../common/BUILD + +:START_BUILD pyinstaller -F --log-level=DEBUG --clean --upx-dir=.\bin monkey.spec diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py index 1a3cfc78c..5c5b5a392 100644 --- a/monkey/infection_monkey/config.py +++ b/monkey/infection_monkey/config.py @@ -1,6 +1,6 @@ import hashlib -import os import json +import os import sys import uuid from abc import ABCMeta @@ -125,6 +125,7 @@ class Configuration(object): finger_classes = [] exploiter_classes = [] + system_info_collectors_classes = [] # how many victims to look for in a single scan iteration victims_max_find = 100 diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py index 928425535..21871d857 100644 --- a/monkey/infection_monkey/main.py +++ b/monkey/infection_monkey/main.py @@ -12,6 +12,7 @@ from infection_monkey.config import WormConfiguration, EXTERNAL_CONFIG_FILE from infection_monkey.dropper import MonkeyDrops from infection_monkey.model import MONKEY_ARG, DROPPER_ARG from infection_monkey.monkey import InfectionMonkey +from common.version import get_version # noinspection PyUnresolvedReferences import infection_monkey.post_breach # dummy import for pyinstaller @@ -117,6 +118,8 @@ def main(): LOG.info(">>>>>>>>>> Initializing monkey (%s): PID %s <<<<<<<<<<", monkey_cls.__name__, os.getpid()) + LOG.info(f"version: {get_version()}") + monkey = monkey_cls(monkey_args) monkey.initialize() diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py index 80d2d8642..6b8803a9f 100644 --- a/monkey/infection_monkey/monkey.py +++ b/monkey/infection_monkey/monkey.py @@ -30,13 +30,20 @@ from infection_monkey.network.tools import get_interface_to_target from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError, FailedExploitationError from infection_monkey.telemetry.attack.t1106_telem import T1106Telem from common.utils.attack_utils import ScanStatus, UsageEnum +from common.version import get_version from infection_monkey.exploit.HostExploiter import HostExploiter +MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, shutting down" + __author__ = 'itamar' LOG = logging.getLogger(__name__) +class PlannedShutdownException(Exception): + pass + + class InfectionMonkey(object): def __init__(self, args): self._keep_running = False @@ -87,143 +94,158 @@ class InfectionMonkey(object): LOG.debug("Default server: %s is already in command servers list" % self._default_server) def start(self): - LOG.info("Monkey is running...") + try: + LOG.info("Monkey is starting...") - # Sets island's IP and port for monkey to communicate to - if not self.set_default_server(): - return - self.set_default_port() + LOG.debug("Starting the setup phase.") + # Sets island's IP and port for monkey to communicate to + self.set_default_server() + self.set_default_port() - # Create a dir for monkey files if there isn't one - create_monkey_dir() + # Create a dir for monkey files if there isn't one + create_monkey_dir() - if WindowsUpgrader.should_upgrade(): - self._upgrading_to_64 = True - self._singleton.unlock() - LOG.info("32bit monkey running on 64bit Windows. Upgrading.") - WindowsUpgrader.upgrade(self._opts) - return + self.upgrade_to_64_if_needed() - ControlClient.wakeup(parent=self._parent) - ControlClient.load_control_config() + ControlClient.wakeup(parent=self._parent) + ControlClient.load_control_config() - if is_windows_os(): - T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() + if is_windows_os(): + T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send() - if not WormConfiguration.alive: - LOG.info("Marked not alive from configuration") - return + self.shutdown_by_not_alive_config() - if firewall.is_enabled(): - firewall.add_firewall_rule() + if firewall.is_enabled(): + firewall.add_firewall_rule() - monkey_tunnel = ControlClient.create_control_tunnel() - if monkey_tunnel: - monkey_tunnel.start() + monkey_tunnel = ControlClient.create_control_tunnel() + if monkey_tunnel: + monkey_tunnel.start() - StateTelem(is_done=False).send() - TunnelTelem().send() + StateTelem(is_done=False, version=get_version()).send() + TunnelTelem().send() + LOG.debug("Starting the post-breach phase.") + self.collect_system_info_if_configured() + PostBreach().execute_all_configured() + + LOG.debug("Starting the propagation phase.") + self.shutdown_by_max_depth_reached() + + for iteration_index in range(WormConfiguration.max_iterations): + ControlClient.keepalive() + ControlClient.load_control_config() + + self._network.initialize() + + self._fingerprint = HostFinger.get_instances() + + self._exploiters = HostExploiter.get_classes() + + if not self._keep_running or not WormConfiguration.alive: + break + + machines = self._network.get_victim_machines(max_find=WormConfiguration.victims_max_find, + stop_callback=ControlClient.check_for_stop) + is_empty = True + for machine in machines: + if ControlClient.check_for_stop(): + break + + is_empty = False + for finger in self._fingerprint: + LOG.info("Trying to get OS fingerprint from %r with module %s", + machine, finger.__class__.__name__) + finger.get_host_fingerprint(machine) + + ScanTelem(machine).send() + + # skip machines that we've already exploited + if machine in self._exploited_machines: + LOG.debug("Skipping %r - already exploited", + machine) + continue + elif machine in self._fail_exploitation_machines: + if WormConfiguration.retry_failed_explotation: + LOG.debug("%r - exploitation failed before, trying again", machine) + else: + LOG.debug("Skipping %r - exploitation failed before", machine) + continue + + if monkey_tunnel: + monkey_tunnel.set_tunnel_for_host(machine) + if self._default_server: + if self._network.on_island(self._default_server): + machine.set_default_server(get_interface_to_target(machine.ip_addr) + + (':' + self._default_server_port if self._default_server_port else '')) + else: + machine.set_default_server(self._default_server) + LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server)) + + # Order exploits according to their type + if WormConfiguration.should_exploit: + self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) + host_exploited = False + for exploiter in [exploiter(machine) for exploiter in self._exploiters]: + if self.try_exploiting(machine, exploiter): + host_exploited = True + VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send() + break + if not host_exploited: + self._fail_exploitation_machines.add(machine) + VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send() + if not self._keep_running: + break + + if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1): + time_to_sleep = WormConfiguration.timeout_between_iterations + LOG.info("Sleeping %d seconds before next life cycle iteration", time_to_sleep) + time.sleep(time_to_sleep) + + if self._keep_running and WormConfiguration.alive: + LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations) + elif not WormConfiguration.alive: + LOG.info("Marked not alive from configuration") + + # if host was exploited, before continue to closing the tunnel ensure the exploited host had its chance to + # connect to the tunnel + if len(self._exploited_machines) > 0: + time_to_sleep = WormConfiguration.keep_tunnel_open_time + LOG.info("Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep) + time.sleep(time_to_sleep) + + if monkey_tunnel: + monkey_tunnel.stop() + monkey_tunnel.join() + except PlannedShutdownException: + LOG.info("A planned shutdown of the Monkey occurred. Logging the reason and finishing execution.") + LOG.exception("Planned shutdown, reason:") + + def shutdown_by_max_depth_reached(self): + if 0 == WormConfiguration.depth: + TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send() + raise PlannedShutdownException(MAX_DEPTH_REACHED_MESSAGE) + else: + LOG.debug("Running with depth: %d" % WormConfiguration.depth) + + def collect_system_info_if_configured(self): if WormConfiguration.collect_system_info: LOG.debug("Calling system info collection") system_info_collector = SystemInfoCollector() system_info = system_info_collector.get_info() SystemInfoTelem(system_info).send() - # Executes post breach actions - PostBreach().execute() + def shutdown_by_not_alive_config(self): + if not WormConfiguration.alive: + raise PlannedShutdownException("Marked 'not alive' from configuration.") - if 0 == WormConfiguration.depth: - TraceTelem("Reached max depth, shutting down").send() - return - else: - LOG.debug("Running with depth: %d" % WormConfiguration.depth) - - for iteration_index in range(WormConfiguration.max_iterations): - ControlClient.keepalive() - ControlClient.load_control_config() - - self._network.initialize() - - self._fingerprint = HostFinger.get_instances() - - self._exploiters = HostExploiter.get_classes() - - if not self._keep_running or not WormConfiguration.alive: - break - - machines = self._network.get_victim_machines(max_find=WormConfiguration.victims_max_find, - stop_callback=ControlClient.check_for_stop) - is_empty = True - for machine in machines: - if ControlClient.check_for_stop(): - break - - is_empty = False - for finger in self._fingerprint: - LOG.info("Trying to get OS fingerprint from %r with module %s", - machine, finger.__class__.__name__) - finger.get_host_fingerprint(machine) - - ScanTelem(machine).send() - - # skip machines that we've already exploited - if machine in self._exploited_machines: - LOG.debug("Skipping %r - already exploited", - machine) - continue - elif machine in self._fail_exploitation_machines: - if WormConfiguration.retry_failed_explotation: - LOG.debug("%r - exploitation failed before, trying again", machine) - else: - LOG.debug("Skipping %r - exploitation failed before", machine) - continue - - if monkey_tunnel: - monkey_tunnel.set_tunnel_for_host(machine) - if self._default_server: - if self._network.on_island(self._default_server): - machine.set_default_server(get_interface_to_target(machine.ip_addr) + - (':' + self._default_server_port if self._default_server_port else '')) - else: - machine.set_default_server(self._default_server) - LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server)) - - # Order exploits according to their type - if WormConfiguration.should_exploit: - self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) - host_exploited = False - for exploiter in [exploiter(machine) for exploiter in self._exploiters]: - if self.try_exploiting(machine, exploiter): - host_exploited = True - VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send() - break - if not host_exploited: - self._fail_exploitation_machines.add(machine) - VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send() - if not self._keep_running: - break - - if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1): - time_to_sleep = WormConfiguration.timeout_between_iterations - LOG.info("Sleeping %d seconds before next life cycle iteration", time_to_sleep) - time.sleep(time_to_sleep) - - if self._keep_running and WormConfiguration.alive: - LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations) - elif not WormConfiguration.alive: - LOG.info("Marked not alive from configuration") - - # if host was exploited, before continue to closing the tunnel ensure the exploited host had its chance to - # connect to the tunnel - if len(self._exploited_machines) > 0: - time_to_sleep = WormConfiguration.keep_tunnel_open_time - LOG.info("Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep) - time.sleep(time_to_sleep) - - if monkey_tunnel: - monkey_tunnel.stop() - monkey_tunnel.join() + def upgrade_to_64_if_needed(self): + if WindowsUpgrader.should_upgrade(): + self._upgrading_to_64 = True + self._singleton.unlock() + LOG.info("32bit monkey running on 64bit Windows. Upgrading.") + WindowsUpgrader.upgrade(self._opts) + raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.") def cleanup(self): LOG.info("Monkey cleanup started") @@ -233,7 +255,7 @@ class InfectionMonkey(object): InfectionMonkey.close_tunnel() firewall.close() else: - StateTelem(is_done=True).send() # Signal the server (before closing the tunnel) + StateTelem(is_done=True, version=get_version()).send() # Signal the server (before closing the tunnel) InfectionMonkey.close_tunnel() firewall.close() if WormConfiguration.send_log_to_server: @@ -346,9 +368,11 @@ class InfectionMonkey(object): self._default_server_port = '' def set_default_server(self): + """ + Sets the default server for the Monkey to communicate back to. + :raises PlannedShutdownException if couldn't find the server. + """ if not ControlClient.find_server(default_tunnel=self._default_tunnel): - LOG.info("Monkey couldn't find server. Going down.") - return False + raise PlannedShutdownException("Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel)) self._default_server = WormConfiguration.current_server LOG.debug("default server set to: %s" % self._default_server) - return True diff --git a/monkey/infection_monkey/monkey.spec b/monkey/infection_monkey/monkey.spec index 9c5fa9a18..e5873c9c5 100644 --- a/monkey/infection_monkey/monkey.spec +++ b/monkey/infection_monkey/monkey.spec @@ -1,5 +1,6 @@ # -*- mode: python -*- import os +import sys import platform @@ -18,7 +19,9 @@ def main(): hookspath=['./pyinstaller_hooks'], runtime_hooks=None, binaries=None, - datas=None, + datas=[ + ("../common/BUILD", "/common") + ], excludes=None, win_no_prefer_redirects=None, win_private_assemblies=None, @@ -48,7 +51,7 @@ def is_windows(): def is_32_bit(): - return platform.architecture()[0] == "32bit" + return sys.maxsize <= 2**32 def get_bin_folder(): @@ -93,7 +96,18 @@ def get_traceroute_binaries(): def get_monkey_filename(): - return 'monkey.exe' if is_windows() else 'monkey' + name = 'monkey-' + if is_windows(): + name = name+"windows-" + else: + name = name+"linux-" + if is_32_bit(): + name = name+"32" + else: + name = name+"64" + if is_windows(): + name = name+".exe" + return name def get_exe_strip(): diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py index 7474c8ef1..d700bac62 100644 --- a/monkey/infection_monkey/post_breach/post_breach_handler.py +++ b/monkey/infection_monkey/post_breach/post_breach_handler.py @@ -20,7 +20,7 @@ class PostBreach(object): self.os_is_linux = not is_windows_os() self.pba_list = self.config_to_pba_list() - def execute(self): + def execute_all_configured(self): """ Executes all post breach actions. """ diff --git a/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py new file mode 100644 index 000000000..97cf81bfb --- /dev/null +++ b/monkey/infection_monkey/pyinstaller_hooks/hook-infection_monkey.system_info.collectors.py @@ -0,0 +1,6 @@ +from PyInstaller.utils.hooks import collect_submodules, collect_data_files + +# Import all actions as modules +hiddenimports = collect_submodules('infection_monkey.system_info.collectors') +# Add action files that we enumerate +datas = (collect_data_files('infection_monkey.system_info.collectors', include_py_files=True)) diff --git a/monkey/infection_monkey/readme.md b/monkey/infection_monkey/readme.md index d6e17acdb..da865c35f 100644 --- a/monkey/infection_monkey/readme.md +++ b/monkey/infection_monkey/readme.md @@ -34,7 +34,7 @@ The monkey is composed of three separate parts. 6. To build the final exe: - `cd monkey\infection_monkey` - `build_windows.bat` - - `output is placed under dist\monkey.exe` + - output is placed under `dist\monkey32.exe` or `dist\monkey64.exe` depending on your version of Python ## Linux @@ -55,18 +55,18 @@ Tested on Ubuntu 16.04. 3. Build Sambacry binaries - Build/Download according to sections at the end of this readme. - - Place the binaries under [code location]\infection_monkey\bin, under the names 'sc_monkey_runner32.so', 'sc_monkey_runner64.so' + - Place the binaries under [code location]/infection_monkey/bin, under the names 'sc_monkey_runner32.so', 'sc_monkey_runner64.so' 4. Build Traceroute binaries - Build/Download according to sections at the end of this readme. - - Place the binaries under [code location]\infection_monkey\bin, under the names 'traceroute32', 'traceroute64' + - Place the binaries under [code location]/infection_monkey/bin, under the names 'traceroute32', 'traceroute64' 5. To build, run in terminal: - `cd [code location]/infection_monkey` - `chmod +x build_linux.sh` - `./build_linux.sh` - output is placed under dist/monkey + output is placed under `dist/monkey32` or `dist/monkey64` depending on your version of python ### Sambacry diff --git a/monkey/infection_monkey/system_info/__init__.py b/monkey/infection_monkey/system_info/__init__.py index 7d4395af7..d9107e7bf 100644 --- a/monkey/infection_monkey/system_info/__init__.py +++ b/monkey/infection_monkey/system_info/__init__.py @@ -6,9 +6,9 @@ import psutil from enum import IntEnum from infection_monkey.network.info import get_host_subnets -from infection_monkey.system_info.aws_collector import AwsCollector from infection_monkey.system_info.azure_cred_collector import AzureCollector from infection_monkey.system_info.netstat_collector import NetstatCollector +from infection_monkey.system_info.system_info_collectors_handler import SystemInfoCollectorsHandler LOG = logging.getLogger(__name__) @@ -61,50 +61,12 @@ class InfoCollector(object): self.info = {} def get_info(self): - self.get_hostname() - self.get_process_list() + # Collect all hardcoded self.get_network_info() self.get_azure_info() - self.get_aws_info() - def get_hostname(self): - """ - Adds the fully qualified computer hostname to the system information. - :return: None. Updates class information - """ - LOG.debug("Reading hostname") - self.info['hostname'] = socket.getfqdn() - - def get_process_list(self): - """ - Adds process information from the host to the system information. - Currently lists process name, ID, parent ID, command line - and the full image path of each process. - :return: None. Updates class information - """ - LOG.debug("Reading process list") - processes = {} - for process in psutil.process_iter(): - try: - processes[process.pid] = {"name": process.name(), - "pid": process.pid, - "ppid": process.ppid(), - "cmdline": " ".join(process.cmdline()), - "full_image_path": process.exe(), - } - except (psutil.AccessDenied, WindowsError): - # we may be running as non root - # and some processes are impossible to acquire in Windows/Linux - # in this case we'll just add what we can - processes[process.pid] = {"name": "null", - "pid": process.pid, - "ppid": process.ppid(), - "cmdline": "ACCESS DENIED", - "full_image_path": "null", - } - continue - - self.info['process_list'] = processes + # Collect all plugins + SystemInfoCollectorsHandler().execute_all_configured() def get_network_info(self): """ @@ -150,11 +112,3 @@ class InfoCollector(object): except Exception: # If we failed to collect azure info, no reason to fail all the collection. Log and continue. LOG.error("Failed collecting Azure info.", exc_info=True) - - def get_aws_info(self): - # noinspection PyBroadException - try: - self.info['aws'] = AwsCollector().get_aws_info() - except Exception: - # If we failed to collect aws info, no reason to fail all the collection. Log and continue. - LOG.error("Failed collecting AWS info.", exc_info=True) diff --git a/monkey/infection_monkey/system_info/aws_collector.py b/monkey/infection_monkey/system_info/aws_collector.py deleted file mode 100644 index df90e5913..000000000 --- a/monkey/infection_monkey/system_info/aws_collector.py +++ /dev/null @@ -1,29 +0,0 @@ -import logging - -from common.cloud.aws_instance import AwsInstance - -__author__ = 'itay.mizeretz' - -LOG = logging.getLogger(__name__) - - -class AwsCollector(object): - """ - Extract info from AWS machines - """ - - @staticmethod - def get_aws_info(): - LOG.info("Collecting AWS info") - aws = AwsInstance() - info = {} - if aws.is_aws_instance(): - LOG.info("Machine is an AWS instance") - info = \ - { - 'instance_id': aws.get_instance_id() - } - else: - LOG.info("Machine is NOT an AWS instance") - - return info diff --git a/monkey/infection_monkey/system_info/collectors/__init__.py b/monkey/infection_monkey/system_info/collectors/__init__.py new file mode 100644 index 000000000..f5b7166e9 --- /dev/null +++ b/monkey/infection_monkey/system_info/collectors/__init__.py @@ -0,0 +1,3 @@ +""" +This package holds all the dynamic (plugin) collectors +""" diff --git a/monkey/infection_monkey/system_info/collectors/aws_collector.py b/monkey/infection_monkey/system_info/collectors/aws_collector.py new file mode 100644 index 000000000..68d125279 --- /dev/null +++ b/monkey/infection_monkey/system_info/collectors/aws_collector.py @@ -0,0 +1,31 @@ +import logging + +from common.cloud.aws.aws_instance import AwsInstance +from common.data.system_info_collectors_names import AWS_COLLECTOR +from infection_monkey.system_info.system_info_collector import SystemInfoCollector + + +logger = logging.getLogger(__name__) + + +class AwsCollector(SystemInfoCollector): + """ + Extract info from AWS machines. + """ + def __init__(self): + super().__init__(name=AWS_COLLECTOR) + + def collect(self) -> dict: + logger.info("Collecting AWS info") + aws = AwsInstance() + info = {} + if aws.is_instance(): + logger.info("Machine is an AWS instance") + info = \ + { + 'instance_id': aws.get_instance_id() + } + else: + logger.info("Machine is NOT an AWS instance") + + return info diff --git a/monkey/infection_monkey/system_info/collectors/environment_collector.py b/monkey/infection_monkey/system_info/collectors/environment_collector.py new file mode 100644 index 000000000..100d23175 --- /dev/null +++ b/monkey/infection_monkey/system_info/collectors/environment_collector.py @@ -0,0 +1,24 @@ +from common.cloud.all_instances import get_all_cloud_instances +from common.cloud.environment_names import Environment +from common.data.system_info_collectors_names import ENVIRONMENT_COLLECTOR +from infection_monkey.system_info.system_info_collector import SystemInfoCollector + + +def get_monkey_environment() -> str: + """ + Get the Monkey's running environment. + :return: One of the cloud providers if on cloud; otherwise, assumes "on premise". + """ + for instance in get_all_cloud_instances(): + if instance.is_instance(): + return instance.get_cloud_provider_name().value + + return Environment.ON_PREMISE.value + + +class EnvironmentCollector(SystemInfoCollector): + def __init__(self): + super().__init__(name=ENVIRONMENT_COLLECTOR) + + def collect(self) -> dict: + return {"environment": get_monkey_environment()} diff --git a/monkey/infection_monkey/system_info/collectors/hostname_collector.py b/monkey/infection_monkey/system_info/collectors/hostname_collector.py new file mode 100644 index 000000000..21d03aac7 --- /dev/null +++ b/monkey/infection_monkey/system_info/collectors/hostname_collector.py @@ -0,0 +1,16 @@ +import logging +import socket + +from common.data.system_info_collectors_names import HOSTNAME_COLLECTOR +from infection_monkey.system_info.system_info_collector import SystemInfoCollector + + +logger = logging.getLogger(__name__) + + +class HostnameCollector(SystemInfoCollector): + def __init__(self): + super().__init__(name=HOSTNAME_COLLECTOR) + + def collect(self) -> dict: + return {"hostname": socket.getfqdn()} diff --git a/monkey/infection_monkey/system_info/collectors/process_list_collector.py b/monkey/infection_monkey/system_info/collectors/process_list_collector.py new file mode 100644 index 000000000..c0610cc74 --- /dev/null +++ b/monkey/infection_monkey/system_info/collectors/process_list_collector.py @@ -0,0 +1,50 @@ +import logging +import psutil + +from common.data.system_info_collectors_names import PROCESS_LIST_COLLECTOR +from infection_monkey.system_info.system_info_collector import SystemInfoCollector + +logger = logging.getLogger(__name__) + +# Linux doesn't have WindowsError +try: + WindowsError +except NameError: + # noinspection PyShadowingBuiltins + WindowsError = psutil.AccessDenied + + +class ProcessListCollector(SystemInfoCollector): + def __init__(self): + super().__init__(name=PROCESS_LIST_COLLECTOR) + + def collect(self) -> dict: + """ + Adds process information from the host to the system information. + Currently lists process name, ID, parent ID, command line + and the full image path of each process. + """ + logger.debug("Reading process list") + processes = {} + for process in psutil.process_iter(): + try: + processes[process.pid] = { + "name": process.name(), + "pid": process.pid, + "ppid": process.ppid(), + "cmdline": " ".join(process.cmdline()), + "full_image_path": process.exe(), + } + except (psutil.AccessDenied, WindowsError): + # we may be running as non root and some processes are impossible to acquire in Windows/Linux. + # In this case we'll just add what we know. + processes[process.pid] = { + "name": "null", + "pid": process.pid, + "ppid": process.ppid(), + "cmdline": "ACCESS DENIED", + "full_image_path": "null", + } + continue + + return {'process_list': processes} diff --git a/monkey/infection_monkey/system_info/system_info_collector.py b/monkey/infection_monkey/system_info/system_info_collector.py new file mode 100644 index 000000000..8c0b6aa65 --- /dev/null +++ b/monkey/infection_monkey/system_info/system_info_collector.py @@ -0,0 +1,38 @@ +from infection_monkey.config import WormConfiguration +from infection_monkey.utils.plugins.plugin import Plugin +from abc import ABCMeta, abstractmethod + +import infection_monkey.system_info.collectors + + +class SystemInfoCollector(Plugin, metaclass=ABCMeta): + """ + ABC for system info collection. See system_info_collector_handler for more info. Basically, to implement a new system info + collector, inherit from this class in an implementation in the infection_monkey.system_info.collectors class, and override + the 'collect' method. Don't forget to parse your results in the Monkey Island and to add the collector to the configuration + as well - see monkey_island.cc.services.processing.system_info_collectors for examples. + + See the Wiki page "How to add a new System Info Collector to the Monkey?" for a detailed guide. + """ + def __init__(self, name="unknown"): + self.name = name + + @staticmethod + def should_run(class_name) -> bool: + return class_name in WormConfiguration.system_info_collectors_classes + + @staticmethod + def base_package_file(): + return infection_monkey.system_info.collectors.__file__ + + @staticmethod + def base_package_name(): + return infection_monkey.system_info.collectors.__package__ + + @abstractmethod + def collect(self) -> dict: + """ + Collect the relevant information and return it in a dictionary. + To be implemented by each collector. + """ + raise NotImplementedError() diff --git a/monkey/infection_monkey/system_info/system_info_collectors_handler.py b/monkey/infection_monkey/system_info/system_info_collectors_handler.py new file mode 100644 index 000000000..cc007ff86 --- /dev/null +++ b/monkey/infection_monkey/system_info/system_info_collectors_handler.py @@ -0,0 +1,33 @@ +import logging +from typing import Sequence + +from infection_monkey.system_info.system_info_collector import SystemInfoCollector +from infection_monkey.telemetry.system_info_telem import SystemInfoTelem + +LOG = logging.getLogger(__name__) + + +class SystemInfoCollectorsHandler(object): + def __init__(self): + self.collectors_list = self.config_to_collectors_list() + + def execute_all_configured(self): + successful_collections = 0 + system_info_telemetry = {} + for collector in self.collectors_list: + try: + LOG.debug("Executing system info collector: '{}'".format(collector.name)) + collected_info = collector.collect() + system_info_telemetry[collector.name] = collected_info + successful_collections += 1 + except Exception as e: + # If we failed one collector, no need to stop execution. Log and continue. + LOG.error("Collector {} failed. Error info: {}".format(collector.name, e)) + LOG.info("All system info collectors executed. Total {} executed, out of which {} collected successfully.". + format(len(self.collectors_list), successful_collections)) + + SystemInfoTelem({"collectors": system_info_telemetry}).send() + + @staticmethod + def config_to_collectors_list() -> Sequence[SystemInfoCollector]: + return SystemInfoCollector.get_instances() diff --git a/monkey/infection_monkey/telemetry/state_telem.py b/monkey/infection_monkey/telemetry/state_telem.py index 3bd63d2f9..4d4224288 100644 --- a/monkey/infection_monkey/telemetry/state_telem.py +++ b/monkey/infection_monkey/telemetry/state_telem.py @@ -5,15 +5,19 @@ __author__ = "itay.mizeretz" class StateTelem(BaseTelem): - def __init__(self, is_done): + def __init__(self, is_done, version="Unknown"): """ Default state telemetry constructor :param is_done: Whether the state of monkey is done. """ super(StateTelem, self).__init__() self.is_done = is_done + self.version = version telem_category = 'state' def get_data(self): - return {'done': self.is_done} + return { + 'done': self.is_done, + 'version': self.version + } diff --git a/monkey/monkey_island/cc/environment/__init__.py b/monkey/monkey_island/cc/environment/__init__.py index 26d33f78c..ec7c7a0f4 100644 --- a/monkey/monkey_island/cc/environment/__init__.py +++ b/monkey/monkey_island/cc/environment/__init__.py @@ -26,8 +26,6 @@ class Environment(object, metaclass=ABCMeta): def testing(self, value): self._testing = value - _MONKEY_VERSION = "1.7.0" - def __init__(self): self.config = None self._testing = False # Assume env is not for unit testing. @@ -58,9 +56,6 @@ class Environment(object, metaclass=ABCMeta): def is_develop(self): return self.get_deployment() == 'develop' - def get_version(self): - return self._MONKEY_VERSION + ('-dev' if self.is_develop() else '') - def _get_from_config(self, key, default_value=None): val = default_value if self.config is not None: diff --git a/monkey/monkey_island/cc/environment/aws.py b/monkey/monkey_island/cc/environment/aws.py index 18db5c376..5608bddcd 100644 --- a/monkey/monkey_island/cc/environment/aws.py +++ b/monkey/monkey_island/cc/environment/aws.py @@ -1,6 +1,6 @@ import monkey_island.cc.auth from monkey_island.cc.environment import Environment -from common.cloud.aws_instance import AwsInstance +from common.cloud.aws.aws_instance import AwsInstance __author__ = 'itay.mizeretz' diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py index 508479287..f02aebafa 100644 --- a/monkey/monkey_island/cc/main.py +++ b/monkey/monkey_island/cc/main.py @@ -26,6 +26,7 @@ from monkey_island.cc.utils import local_ip_addresses from monkey_island.cc.environment.environment import env from monkey_island.cc.database import is_db_server_up, get_db_version from monkey_island.cc.resources.monkey_download import MonkeyDownload +from common.version import get_version from monkey_island.cc.bootloader_server import BootloaderHttpServer @@ -66,8 +67,9 @@ def start_island_server(): def log_init_info(): - logger.info( - 'Monkey Island Server is running. Listening on the following URLs: {}'.format( + logger.info('Monkey Island Server is running!') + logger.info(f"version: {get_version()}") + logger.info('Listening on the following URLs: {}'.format( ", ".join(["https://{}:{}".format(x, env.get_island_port()) for x in local_ip_addresses()]) ) ) diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py index 07b5ba3fe..f658a3d06 100644 --- a/monkey/monkey_island/cc/models/monkey.py +++ b/monkey/monkey_island/cc/models/monkey.py @@ -9,6 +9,7 @@ from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_docu from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS from monkey_island.cc.models.command_control_channel import CommandControlChannel from monkey_island.cc.utils import local_ip_addresses +from common.cloud import environment_names MAX_MONKEYS_AMOUNT_TO_CACHE = 100 @@ -42,6 +43,10 @@ class Monkey(Document): ttl_ref = ReferenceField(MonkeyTtl) tunnel = ReferenceField("self") command_control_channel = EmbeddedDocumentField(CommandControlChannel) + + # Environment related fields + environment = StringField(default=environment_names.Environment.UNKNOWN.value, + choices=environment_names.ALL_ENVIRONMENTS_NAMES) aws_instance_id = StringField(required=False) # This field only exists when the monkey is running on an AWS # instance. See https://github.com/guardicore/monkey/issues/426. @@ -55,7 +60,8 @@ class Monkey(Document): raise MonkeyNotFoundError("info: {0} | id: {1}".format(ex, str(db_id))) @staticmethod - def get_single_monkey_by_guid(monkey_guid): + # See https://www.python.org/dev/peps/pep-0484/#forward-references + def get_single_monkey_by_guid(monkey_guid) -> 'Monkey': try: return Monkey.objects.get(guid=monkey_guid) except DoesNotExist as ex: diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py index c41699add..98d3694bf 100644 --- a/monkey/monkey_island/cc/resources/remote_run.py +++ b/monkey/monkey_island/cc/resources/remote_run.py @@ -6,7 +6,7 @@ import flask_restful from monkey_island.cc.auth import jwt_required from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService -from common.cloud.aws_service import AwsService +from common.cloud.aws.aws_service import AwsService CLIENT_ERROR_FORMAT = "ClientError, error message: '{}'. Probably, the IAM role that has been associated with the " \ "instance doesn't permit SSM calls. " diff --git a/monkey/monkey_island/cc/resources/version_update.py b/monkey/monkey_island/cc/resources/version_update.py index b1fbfdf82..a88f8830c 100644 --- a/monkey/monkey_island/cc/resources/version_update.py +++ b/monkey/monkey_island/cc/resources/version_update.py @@ -1,7 +1,7 @@ import flask_restful import logging -from monkey_island.cc.environment.environment import env +from common.version import get_version from monkey_island.cc.services.version_update import VersionUpdateService __author__ = 'itay.mizeretz' @@ -17,7 +17,7 @@ class VersionUpdate(flask_restful.Resource): # even when not authenticated def get(self): return { - 'current_version': env.get_version(), + 'current_version': get_version(), 'newer_version': VersionUpdateService.get_newer_version(), 'download_link': VersionUpdateService.get_download_link() } diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index fd2ed5b8d..96c59cad6 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -153,9 +153,18 @@ class ConfigService: def ssh_key_exists(keys, user, ip): return [key for key in keys if key['user'] == user and key['ip'] == ip] + def _filter_none_values(data): + if isinstance(data, dict): + return {k: ConfigService._filter_none_values(v) for k, v in data.items() if k is not None and v is not None} + elif isinstance(data, list): + return [ConfigService._filter_none_values(item) for item in data if item is not None] + else: + return data + @staticmethod def update_config(config_json, should_encrypt): # PBA file upload happens on pba_file_upload endpoint and corresponding config options are set there + config_json = ConfigService._filter_none_values(config_json) monkey_island.cc.services.post_breach_files.set_config_PBA_files(config_json) if should_encrypt: try: diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py index 32ee13b12..3d0220ee2 100644 --- a/monkey/monkey_island/cc/services/config_schema.py +++ b/monkey/monkey_island/cc/services/config_schema.py @@ -1,3 +1,6 @@ +from common.data.system_info_collectors_names \ + import AWS_COLLECTOR, ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR, PROCESS_LIST_COLLECTOR + WARNING_SIGN = " \u26A0" SCHEMA = { @@ -99,6 +102,44 @@ SCHEMA = { } ] }, + "system_info_collectors_classes": { + "title": "System Information Collectors", + "type": "string", + "anyOf": [ + { + "type": "string", + "enum": [ + ENVIRONMENT_COLLECTOR + ], + "title": "Collect which environment this machine is on (on prem/cloud)", + "attack_techniques": [] + }, + { + "type": "string", + "enum": [ + AWS_COLLECTOR + ], + "title": "If on AWS, collect more information about the instance", + "attack_techniques": [] + }, + { + "type": "string", + "enum": [ + HOSTNAME_COLLECTOR + ], + "title": "Collect the machine's hostname", + "attack_techniques": [] + }, +{ + "type": "string", + "enum": [ + PROCESS_LIST_COLLECTOR + ], + "title": "Collect running processes on the machine", + "attack_techniques": [] + }, + ], + }, "post_breach_acts": { "title": "Post breach actions", "type": "string", @@ -433,6 +474,21 @@ SCHEMA = { "attack_techniques": ["T1003"], "description": "Determines whether to use Mimikatz" }, + "system_info_collectors_classes": { + "title": "System info collectors", + "type": "array", + "uniqueItems": True, + "items": { + "$ref": "#/definitions/system_info_collectors_classes" + }, + "default": [ + ENVIRONMENT_COLLECTOR, + AWS_COLLECTOR, + HOSTNAME_COLLECTOR, + PROCESS_LIST_COLLECTOR + ], + "description": "Determines which system information collectors will collect information." + }, } }, "life_cycle": { diff --git a/monkey/monkey_island/cc/services/remote_run_aws.py b/monkey/monkey_island/cc/services/remote_run_aws.py index 9627bf74c..dfaa0e327 100644 --- a/monkey/monkey_island/cc/services/remote_run_aws.py +++ b/monkey/monkey_island/cc/services/remote_run_aws.py @@ -1,7 +1,7 @@ import logging -from common.cloud.aws_instance import AwsInstance -from common.cloud.aws_service import AwsService +from common.cloud.aws.aws_instance import AwsInstance +from common.cloud.aws.aws_service import AwsService from common.cmd.aws.aws_cmd_runner import AwsCmdRunner from common.cmd.cmd import Cmd from common.cmd.cmd_runner import CmdRunner @@ -54,7 +54,7 @@ class RemoteRunAwsService: @staticmethod def is_running_on_aws(): - return RemoteRunAwsService.aws_instance.is_aws_instance() + return RemoteRunAwsService.aws_instance.is_instance() @staticmethod def update_aws_region_authless(): @@ -130,7 +130,8 @@ class RemoteRunAwsService: return r"[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {" \ r"$true}; (New-Object System.Net.WebClient).DownloadFile('https://" + island_ip + \ r":5000/api/monkey/download/monkey-windows-" + bit_text + r".exe','.\\monkey.exe'); " \ - r";Start-Process -FilePath '.\\monkey.exe' -ArgumentList 'm0nk3y -s " + island_ip + r":5000'; " + r";Start-Process -FilePath '.\\monkey.exe' " \ + r"-ArgumentList 'm0nk3y -s " + island_ip + r":5000'; " @staticmethod def _get_run_monkey_cmd_line(is_linux, is_64bit, island_ip): diff --git a/monkey/monkey_island/cc/services/reporting/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py index 1df12e2eb..86486b9ba 100644 --- a/monkey/monkey_island/cc/services/reporting/aws_exporter.py +++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py @@ -5,7 +5,7 @@ from datetime import datetime import boto3 from botocore.exceptions import UnknownServiceError -from common.cloud.aws_instance import AwsInstance +from common.cloud.aws.aws_instance import AwsInstance from monkey_island.cc.environment.environment import load_server_configuration_from_file from monkey_island.cc.services.reporting.exporter import Exporter diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py index 6a44679a4..97e8fa4f1 100644 --- a/monkey/monkey_island/cc/services/reporting/report.py +++ b/monkey/monkey_island/cc/services/reporting/report.py @@ -386,10 +386,11 @@ class ReportService: @staticmethod def get_monkey_subnets(monkey_guid): network_info = mongo.db.telemetry.find_one( - {'telem_category': 'system_info', 'monkey_guid': monkey_guid}, + {'telem_category': 'system_info', + 'monkey_guid': monkey_guid}, {'data.network_info.networks': 1} ) - if network_info is None: + if network_info is None or not network_info["data"]: return [] return \ diff --git a/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py b/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py index 9025ff68f..1a041bb3b 100644 --- a/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py +++ b/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py @@ -15,28 +15,34 @@ __regular_report_generating_lock = threading.Semaphore() def safe_generate_reports(): # Entering the critical section; Wait until report generation is available. __report_generating_lock.acquire() - report = safe_generate_regular_report() - attack_report = safe_generate_attack_report() - # Leaving the critical section. - __report_generating_lock.release() + try: + report = safe_generate_regular_report() + attack_report = safe_generate_attack_report() + finally: + # Leaving the critical section. + __report_generating_lock.release() return report, attack_report def safe_generate_regular_report(): # Local import to avoid circular imports from monkey_island.cc.services.reporting.report import ReportService - __regular_report_generating_lock.acquire() - report = ReportService.generate_report() - __regular_report_generating_lock.release() + try: + __regular_report_generating_lock.acquire() + report = ReportService.generate_report() + finally: + __regular_report_generating_lock.release() return report def safe_generate_attack_report(): # Local import to avoid circular imports from monkey_island.cc.services.attack.attack_report import AttackReportService - __attack_report_generating_lock.acquire() - attack_report = AttackReportService.generate_new_report() - __attack_report_generating_lock.release() + try: + __attack_report_generating_lock.acquire() + attack_report = AttackReportService.generate_new_report() + finally: + __attack_report_generating_lock.release() return attack_report diff --git a/monkey/monkey_island/cc/services/telemetry/processing/state.py b/monkey/monkey_island/cc/services/telemetry/processing/state.py index 4e164e900..b7e341483 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/state.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/state.py @@ -1,9 +1,14 @@ +import logging + from monkey_island.cc.models import Monkey from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import \ test_passed_findings_for_unreached_segments +logger = logging.getLogger(__name__) + + def process_state_telemetry(telemetry_json): monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']) NodeService.add_communication_info(monkey, telemetry_json['command_control_channel']) @@ -15,3 +20,6 @@ def process_state_telemetry(telemetry_json): if telemetry_json['data']['done']: current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) test_passed_findings_for_unreached_segments(current_monkey) + + if telemetry_json['data']['version']: + logger.info(f"monkey {telemetry_json['monkey_guid']} has version {telemetry_json['data']['version']}") diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py index e43581c43..75c806ea5 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py @@ -1,25 +1,23 @@ import logging -from monkey_island.cc.database import mongo -from monkey_island.cc.models import Monkey -from monkey_island.cc.services import mimikatz_utils -from monkey_island.cc.services.node import NodeService -from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.telemetry.zero_trust_tests.antivirus_existence import test_antivirus_existence -from monkey_island.cc.services.wmi_handler import WMIHandler from monkey_island.cc.encryptor import encryptor +from monkey_island.cc.services import mimikatz_utils +from monkey_island.cc.services.config import ConfigService +from monkey_island.cc.services.node import NodeService +from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import \ + SystemInfoTelemetryDispatcher +from monkey_island.cc.services.wmi_handler import WMIHandler logger = logging.getLogger(__name__) def process_system_info_telemetry(telemetry_json): + dispatcher = SystemInfoTelemetryDispatcher() telemetry_processing_stages = [ process_ssh_info, process_credential_info, process_mimikatz_and_wmi_info, - process_aws_data, - update_db_with_new_hostname, - test_antivirus_existence, + dispatcher.dispatch_collector_results_to_relevant_processors ] # Calling safe_process_telemetry so if one of the stages fail, we log and move on instead of failing the rest of @@ -34,7 +32,7 @@ def safe_process_telemetry(processing_function, telemetry_json): processing_function(telemetry_json) except Exception as err: logger.error( - "Error {} while in {} stage of processing telemetry.".format(str(err), processing_function.func_name), + "Error {} while in {} stage of processing telemetry.".format(str(err), processing_function.__name__), exc_info=True) @@ -104,14 +102,3 @@ def process_mimikatz_and_wmi_info(telemetry_json): wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets) wmi_handler.process_and_handle_wmi_info() - -def process_aws_data(telemetry_json): - if 'aws' in telemetry_json['data']: - if 'instance_id' in telemetry_json['data']['aws']: - monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id') - mongo.db.monkey.update_one({'_id': monkey_id}, - {'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}}) - - -def update_db_with_new_hostname(telemetry_json): - Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']).set_hostname(telemetry_json['data']['hostname']) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/__init__.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/aws.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/aws.py new file mode 100644 index 000000000..2b4d8085e --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/aws.py @@ -0,0 +1,15 @@ +import logging + +from monkey_island.cc.models.monkey import Monkey + +logger = logging.getLogger(__name__) + + +def process_aws_telemetry(collector_results, monkey_guid): + relevant_monkey = Monkey.get_single_monkey_by_guid(monkey_guid) + + if "instance_id" in collector_results: + instance_id = collector_results["instance_id"] + relevant_monkey.aws_instance_id = instance_id + relevant_monkey.save() + logger.debug("Updated Monkey {} with aws instance id {}".format(str(relevant_monkey), instance_id)) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/environment.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/environment.py new file mode 100644 index 000000000..4c685a01b --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/environment.py @@ -0,0 +1,12 @@ +import logging + +from monkey_island.cc.models.monkey import Monkey + +logger = logging.getLogger(__name__) + + +def process_environment_telemetry(collector_results, monkey_guid): + relevant_monkey = Monkey.get_single_monkey_by_guid(monkey_guid) + relevant_monkey.environment = collector_results["environment"] + relevant_monkey.save() + logger.debug("Updated Monkey {} with env {}".format(str(relevant_monkey), collector_results)) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py new file mode 100644 index 000000000..e2de4519c --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/hostname.py @@ -0,0 +1,9 @@ +import logging + +from monkey_island.cc.models.monkey import Monkey + +logger = logging.getLogger(__name__) + + +def process_hostname_telemetry(collector_results, monkey_guid): + Monkey.get_single_monkey_by_guid(monkey_guid).set_hostname(collector_results["hostname"]) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py new file mode 100644 index 000000000..b5f2d24ea --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/system_info_telemetry_dispatcher.py @@ -0,0 +1,64 @@ +import logging +import typing + +from common.data.system_info_collectors_names \ + import AWS_COLLECTOR, ENVIRONMENT_COLLECTOR, HOSTNAME_COLLECTOR, PROCESS_LIST_COLLECTOR +from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import process_aws_telemetry +from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import process_environment_telemetry +from monkey_island.cc.services.telemetry.processing.system_info_collectors.hostname import process_hostname_telemetry +from monkey_island.cc.services.telemetry.zero_trust_tests.antivirus_existence import test_antivirus_existence + +logger = logging.getLogger(__name__) + +SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = { + AWS_COLLECTOR: [process_aws_telemetry], + ENVIRONMENT_COLLECTOR: [process_environment_telemetry], + HOSTNAME_COLLECTOR: [process_hostname_telemetry], + PROCESS_LIST_COLLECTOR: [test_antivirus_existence] +} + + +class SystemInfoTelemetryDispatcher(object): + def __init__(self, collector_to_parsing_functions: typing.Mapping[str, typing.List[typing.Callable]] = None): + """ + :param collector_to_parsing_functions: Map between collector names and a list of functions that process the output of + that collector. If `None` is supplied, uses the default one; This should be the normal flow, overriding the + collector->functions mapping is useful mostly for testing. + """ + if collector_to_parsing_functions is None: + collector_to_parsing_functions = SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS + self.collector_to_processing_functions = collector_to_parsing_functions + + def dispatch_collector_results_to_relevant_processors(self, telemetry_json): + """ + If the telemetry has collectors' results, dispatches the results to the relevant processing functions. + :param telemetry_json: Telemetry sent from the Monkey + """ + if "collectors" in telemetry_json["data"]: + self.dispatch_single_result_to_relevant_processor(telemetry_json) + + def dispatch_single_result_to_relevant_processor(self, telemetry_json): + relevant_monkey_guid = telemetry_json['monkey_guid'] + + for collector_name, collector_results in telemetry_json["data"]["collectors"].items(): + self.dispatch_result_of_single_collector_to_processing_functions( + collector_name, + collector_results, + relevant_monkey_guid) + + def dispatch_result_of_single_collector_to_processing_functions( + self, + collector_name, + collector_results, + relevant_monkey_guid): + if collector_name in self.collector_to_processing_functions: + for processing_function in self.collector_to_processing_functions[collector_name]: + # noinspection PyBroadException + try: + processing_function(collector_results, relevant_monkey_guid) + except Exception as e: + logger.error( + "Error {} while processing {} system info telemetry".format(str(e), collector_name), + exc_info=True) + else: + logger.warning("Unknown system info collector name: {}".format(collector_name)) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py new file mode 100644 index 000000000..f85b2b88c --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_environment.py @@ -0,0 +1,31 @@ +import uuid + +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import \ + SystemInfoTelemetryDispatcher +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + + +class TestEnvironmentTelemetryProcessing(IslandTestCase): + def test_process_environment_telemetry(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + # Arrange + monkey_guid = str(uuid.uuid4()) + a_monkey = Monkey(guid=monkey_guid) + a_monkey.save() + dispatcher = SystemInfoTelemetryDispatcher() + + on_premise = "On Premise" + telem_json = { + "data": { + "collectors": { + "EnvironmentCollector": {"environment": on_premise}, + } + }, + "monkey_guid": monkey_guid + } + dispatcher.dispatch_collector_results_to_relevant_processors(telem_json) + + self.assertEqual(Monkey.get_single_monkey_by_guid(monkey_guid).environment, on_premise) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py new file mode 100644 index 000000000..c5cc7aca2 --- /dev/null +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info_collectors/test_system_info_telemetry_dispatcher.py @@ -0,0 +1,57 @@ +import uuid + +from monkey_island.cc.models import Monkey +from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import \ + SystemInfoTelemetryDispatcher +from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import \ + process_aws_telemetry +from monkey_island.cc.testing.IslandTestCase import IslandTestCase + +TEST_SYS_INFO_TO_PROCESSING = { + "AwsCollector": [process_aws_telemetry], +} + + +class SystemInfoTelemetryDispatcherTest(IslandTestCase): + def test_dispatch_to_relevant_collector_bad_inputs(self): + self.fail_if_not_testing_env() + + dispatcher = SystemInfoTelemetryDispatcher(TEST_SYS_INFO_TO_PROCESSING) + + # Bad format telem JSONs - throws + bad_empty_telem_json = {} + self.assertRaises(KeyError, dispatcher.dispatch_collector_results_to_relevant_processors, bad_empty_telem_json) + bad_no_data_telem_json = {"monkey_guid": "bla"} + self.assertRaises(KeyError, dispatcher.dispatch_collector_results_to_relevant_processors, bad_no_data_telem_json) + bad_no_monkey_telem_json = {"data": {"collectors": {"AwsCollector": "Bla"}}} + self.assertRaises(KeyError, dispatcher.dispatch_collector_results_to_relevant_processors, bad_no_monkey_telem_json) + + # Telem JSON with no collectors - nothing gets dispatched + good_telem_no_collectors = {"monkey_guid": "bla", "data": {"bla": "bla"}} + good_telem_empty_collectors = {"monkey_guid": "bla", "data": {"bla": "bla", "collectors": {}}} + + dispatcher.dispatch_collector_results_to_relevant_processors(good_telem_no_collectors) + dispatcher.dispatch_collector_results_to_relevant_processors(good_telem_empty_collectors) + + def test_dispatch_to_relevant_collector(self): + self.fail_if_not_testing_env() + self.clean_monkey_db() + + a_monkey = Monkey(guid=str(uuid.uuid4())) + a_monkey.save() + + dispatcher = SystemInfoTelemetryDispatcher() + + # JSON with results - make sure functions are called + instance_id = "i-0bd2c14bd4c7d703f" + telem_json = { + "data": { + "collectors": { + "AwsCollector": {"instance_id": instance_id}, + } + }, + "monkey_guid": a_monkey.guid + } + dispatcher.dispatch_collector_results_to_relevant_processors(telem_json) + + self.assertEquals(Monkey.get_single_monkey_by_guid(a_monkey.guid).aws_instance_id, instance_id) diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py index ddc1af65b..1916291e2 100644 --- a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py +++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py @@ -7,36 +7,36 @@ from monkey_island.cc.models.zero_trust.event import Event from monkey_island.cc.services.telemetry.zero_trust_tests.known_anti_viruses import ANTI_VIRUS_KNOWN_PROCESS_NAMES -def test_antivirus_existence(telemetry_json): - current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']) - if 'process_list' in telemetry_json['data']: - process_list_event = Event.create_event( - title="Process list", - message="Monkey on {} scanned the process list".format(current_monkey.hostname), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL) - events = [process_list_event] +def test_antivirus_existence(process_list_json, monkey_guid): + current_monkey = Monkey.get_single_monkey_by_guid(monkey_guid) - av_processes = filter_av_processes(telemetry_json) + process_list_event = Event.create_event( + title="Process list", + message="Monkey on {} scanned the process list".format(current_monkey.hostname), + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL) + events = [process_list_event] - for process in av_processes: - events.append(Event.create_event( - title="Found AV process", - message="The process '{}' was recognized as an Anti Virus process. Process " - "details: {}".format(process[1]['name'], json.dumps(process[1])), - event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL - )) + av_processes = filter_av_processes(process_list_json["process_list"]) - if len(av_processes) > 0: - test_status = zero_trust_consts.STATUS_PASSED - else: - test_status = zero_trust_consts.STATUS_FAILED - AggregateFinding.create_or_add_to_existing( - test=zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, status=test_status, events=events - ) + for process in av_processes: + events.append(Event.create_event( + title="Found AV process", + message="The process '{}' was recognized as an Anti Virus process. Process " + "details: {}".format(process[1]['name'], json.dumps(process[1])), + event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL + )) + + if len(av_processes) > 0: + test_status = zero_trust_consts.STATUS_PASSED + else: + test_status = zero_trust_consts.STATUS_FAILED + AggregateFinding.create_or_add_to_existing( + test=zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, status=test_status, events=events + ) -def filter_av_processes(telemetry_json): - all_processes = list(telemetry_json['data']['process_list'].items()) +def filter_av_processes(process_list): + all_processes = list(process_list.items()) av_processes = [] for process in all_processes: process_name = process[1]['name'] diff --git a/monkey/monkey_island/cc/services/version_update.py b/monkey/monkey_island/cc/services/version_update.py index c1dab52a9..ddd60d5c0 100644 --- a/monkey/monkey_island/cc/services/version_update.py +++ b/monkey/monkey_island/cc/services/version_update.py @@ -2,6 +2,7 @@ import logging import requests +from common.version import get_version from monkey_island.cc.environment.environment import env __author__ = "itay.mizeretz" @@ -39,7 +40,7 @@ class VersionUpdateService: Checks if newer monkey version is available :return: False if not, version in string format ('1.6.2') otherwise """ - url = VersionUpdateService.VERSION_SERVER_CHECK_NEW_URL % (env.get_deployment(), env.get_version()) + url = VersionUpdateService.VERSION_SERVER_CHECK_NEW_URL % (env.get_deployment(), get_version()) reply = requests.get(url, timeout=15) @@ -53,4 +54,4 @@ class VersionUpdateService: @staticmethod def get_download_link(): - return VersionUpdateService.VERSION_SERVER_DOWNLOAD_URL % (env.get_deployment(), env.get_version()) + return VersionUpdateService.VERSION_SERVER_DOWNLOAD_URL % (env.get_deployment(), get_version()) diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js index 2c54822a5..b4325e55c 100644 --- a/monkey/monkey_island/cc/ui/src/components/Main.js +++ b/monkey/monkey_island/cc/ui/src/components/Main.js @@ -30,7 +30,7 @@ let infectionMonkeyImage = require('../images/infection-monkey.svg'); let guardicoreLogoImage = require('../images/guardicore-logo.png'); let notificationIcon = require('../images/notification-logo-512x512.png'); -const reportZeroTrustRoute = '/report/zero_trust'; +const reportZeroTrustRoute = '/report/zeroTrust'; class AppComponent extends AuthComponent { updateStatus = () => { 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 6d9325487..a49e198a6 100644 --- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js +++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js @@ -30,7 +30,7 @@ class ConfigurePageComponent extends AuthComponent { lastAction: 'none', sections: [], selectedSection: 'attack', - allMonkeysAreDead: true, + monkeysRan: false, PBAwinFile: [], PBAlinuxFile: [], showAttackAlert: false @@ -363,13 +363,7 @@ class ConfigurePageComponent extends AuthComponent { this.authFetch('/api') .then(res => res.json()) .then(res => { - // This check is used to prevent unnecessary re-rendering - let allMonkeysAreDead = (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']); - if (allMonkeysAreDead !== this.state.allMonkeysAreDead) { - this.setState({ - allMonkeysAreDead: allMonkeysAreDead - }); - } + this.setState({monkeysRan: res['completed_steps']['run_monkey']}); }); }; @@ -470,15 +464,15 @@ class ConfigurePageComponent extends AuthComponent { ) }; - renderRunningMonkeysWarning = () => { + renderConfigWontChangeWarning = () => { return (
- {this.state.allMonkeysAreDead ? - '' : + {this.state.monkeysRan ?
- Some monkeys are currently running. Note that changing the configuration will only apply to new - infections. + Changed configuration will only apply to new infections. + "Start over" to run again with different configuration.
+ : '' }
) }; @@ -520,7 +514,7 @@ class ConfigurePageComponent extends AuthComponent { {this.renderAttackAlertModal()}

Monkey Configuration

{this.renderNav()} - {this.renderRunningMonkeysWarning()} + {this.renderConfigWontChangeWarning()} {content}
- -
- - - ) - - }; - render() { return ( - {this.renderCleanDialogModal()} +

Start Over

@@ -104,7 +75,7 @@ class StartOverPageComponent extends AuthComponent { this.setState({ cleaned: false }); - this.authFetch('/api?action=reset') + return this.authFetch('/api?action=reset') .then(res => res.json()) .then(res => { if (res['status'] === 'OK') { @@ -112,8 +83,14 @@ class StartOverPageComponent extends AuthComponent { cleaned: true }); } - }); - } + }).then(this.updateMonkeysRunning()); + }; + + closeModal = () => { + this.setState({ + showCleanDialog: false + }) + }; } export default StartOverPageComponent; diff --git a/monkey/monkey_island/cc/ui/src/components/ui-components/StartOverModal.js b/monkey/monkey_island/cc/ui/src/components/ui-components/StartOverModal.js new file mode 100644 index 000000000..5694d463a --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/components/ui-components/StartOverModal.js @@ -0,0 +1,74 @@ +import {Modal} from 'react-bootstrap'; +import React from 'react'; +import {GridLoader} from 'react-spinners'; + + +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 ( + this.props.onClose()}> + +

+
Reset environment
+

+

+ Are you sure you want to reset the environment? +

+ { + !this.state.allMonkeysAreDead ? +
+ + Some monkeys are still running. It's advised to kill all monkeys before resetting. +
+ : +
+ } + { + this.state.loading ?
: this.showModalButtons() + } + + + ) + }; + + showModalButtons() { + return (
+ + +
) + } + + modalVerificationOnClick = async () => { + this.setState({loading: true}); + this.props.onVerify() + .then(() => {this.setState({loading: false}); + this.props.onClose();}) + + } +} + +export default StartOverModal; diff --git a/monkey/monkey_island/cc/ui/src/styles/StartOverPage.scss b/monkey/monkey_island/cc/ui/src/styles/StartOverPage.scss new file mode 100644 index 000000000..ee4ab65ea --- /dev/null +++ b/monkey/monkey_island/cc/ui/src/styles/StartOverPage.scss @@ -0,0 +1,9 @@ +$yellow: #ffcc00; + +.modalLoader div{ + margin-left: auto; + margin-right: auto; +} +.modalLoader div>div{ + background-color: $yellow; +} diff --git a/monkey/monkey_island/deb-package/DEBIAN/postinst b/monkey/monkey_island/deb-package/DEBIAN/postinst index 93053a76e..fe62a6100 100644 --- a/monkey/monkey_island/deb-package/DEBIAN/postinst +++ b/monkey/monkey_island/deb-package/DEBIAN/postinst @@ -6,7 +6,7 @@ PYTHON_FOLDER=/var/monkey/monkey_island/bin/python # Prepare python virtualenv pip3 install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER -virtualenv -p python3 ${PYTHON_FOLDER} +python3 -m virtualenv -p python3 ${PYTHON_FOLDER} # install pip requirements ${PYTHON_FOLDER}/bin/python -m pip install -r $MONKEY_FOLDER/monkey_island/requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER @@ -22,7 +22,7 @@ if [ -d "/etc/systemd/network" ]; then systemctl enable monkey-island fi -${MONKEY_FOLDER}/monkey_island/create_certificate.sh +${MONKEY_FOLDER}/monkey_island/create_certificate.sh ${MONKEY_FOLDER}/monkey_island/ service monkey-island start diff --git a/monkey/monkey_island/deb-package/DEBIAN/control b/monkey/monkey_island/deb-package/DEBIAN_MONGO/control similarity index 66% rename from monkey/monkey_island/deb-package/DEBIAN/control rename to monkey/monkey_island/deb-package/DEBIAN_MONGO/control index 88723149c..a47371005 100644 --- a/monkey/monkey_island/deb-package/DEBIAN/control +++ b/monkey/monkey_island/deb-package/DEBIAN_MONGO/control @@ -1,7 +1,7 @@ Package: gc-monkey-island Architecture: amd64 -Maintainer: Guardicore -Homepage: http://www.guardicore.com +Maintainer: Guardicore +Homepage: https://www.infectionmonkey.com Priority: optional Version: 1.0 Description: Guardicore Infection Monkey Island installation package diff --git a/monkey/monkey_island/deb-package/DEBIAN_MONGO/postinst b/monkey/monkey_island/deb-package/DEBIAN_MONGO/postinst index 42dc2d5a2..f79a71913 100644 --- a/monkey/monkey_island/deb-package/DEBIAN_MONGO/postinst +++ b/monkey/monkey_island/deb-package/DEBIAN_MONGO/postinst @@ -6,7 +6,7 @@ PYTHON_FOLDER=/var/monkey/monkey_island/bin/python # Prepare python virtualenv pip3 install virtualenv --no-index --find-links file://$INSTALLATION_FOLDER -virtualenv -p python3 ${PYTHON_FOLDER} +python3 -m virtualenv -p python3 ${PYTHON_FOLDER} # install pip requirements ${PYTHON_FOLDER}/bin/python -m pip install -r $MONKEY_FOLDER/monkey_island/requirements.txt --no-index --find-links file://$INSTALLATION_FOLDER @@ -25,7 +25,7 @@ if [ -d "/etc/systemd/network" ]; then systemctl enable monkey-island fi -${MONKEY_FOLDER}/monkey_island/create_certificate.sh +${MONKEY_FOLDER}/monkey_island/create_certificate.sh ${MONKEY_FOLDER}/monkey_island/ service monkey-island start service monkey-mongo start diff --git a/monkey/monkey_island/deb-package/service/systemd/start_server.sh b/monkey/monkey_island/deb-package/service_mongo/systemd/start_server.sh similarity index 100% rename from monkey/monkey_island/deb-package/service/systemd/start_server.sh rename to monkey/monkey_island/deb-package/service_mongo/systemd/start_server.sh diff --git a/monkey/monkey_island/linux/create_certificate.sh b/monkey/monkey_island/linux/create_certificate.sh index 72aace118..7e306a822 100644 --- a/monkey/monkey_island/linux/create_certificate.sh +++ b/monkey/monkey_island/linux/create_certificate.sh @@ -1,6 +1,9 @@ #!/bin/bash -openssl genrsa -out ./cc/server.key 2048 -openssl req -new -key ./cc/server.key -out ./cc/server.csr -subj "/OU=Monkey Department/CN=monkey.com" -openssl x509 -req -days 366 -in ./cc/server.csr -signkey ./cc/server.key -out ./cc/server.crt +server_root=${1:-"./cc"} + + +openssl genrsa -out "$server_root"/server.key 2048 +openssl req -new -key "$server_root"/server.key -out "$server_root"/server.csr -subj "/C=GB/ST=London/L=London/O=Global Security/OU=Monkey Department/CN=monkey.com" +openssl x509 -req -days 366 -in "$server_root"/server.csr -signkey "$server_root"/server.key -out $server_root/server.crt diff --git a/monkey/monkey_island/linux/install_mongo.sh b/monkey/monkey_island/linux/install_mongo.sh index 51091d144..36203b298 100755 --- a/monkey/monkey_island/linux/install_mongo.sh +++ b/monkey/monkey_island/linux/install_mongo.sh @@ -10,16 +10,25 @@ MONGODB_DIR=$1 # If using deb, this should be: /var/monkey/monkey_island/bin/mon if [[ ${os_version_monkey} == "Ubuntu 16.04"* ]]; then echo Detected Ubuntu 16.04 - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.6.12.tgz" + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-4.2.3.tgz" elif [[ ${os_version_monkey} == "Ubuntu 18.04"* ]]; then echo Detected Ubuntu 18.04 - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-4.2.0.tgz" + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-4.2.3.tgz" +elif [[ ${os_version_monkey} == "Ubuntu 19.10"* ]]; then + echo Detected Ubuntu 19.10 + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1804-4.2.3.tgz" elif [[ ${os_version_monkey} == "Debian GNU/Linux 8"* ]]; then echo Detected Debian 8 - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian81-3.6.12.tgz" + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian81-4.0.16.tgz" elif [[ ${os_version_monkey} == "Debian GNU/Linux 9"* ]]; then echo Detected Debian 9 - export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian92-3.6.12.tgz" + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian92-4.2.3.tgz" +elif [[ ${os_version_monkey} == "Debian GNU/Linux 10"* ]]; then + echo Detected Debian 10 + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian10-4.2.3.tgz" +elif [[ ${os_version_monkey} == "Kali GNU/Linux"* ]]; then + echo Detected Kali Linux + export tgz_url="https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-debian10-4.2.3.tgz" else echo Unsupported OS exit 1 @@ -32,7 +41,7 @@ pushd "${TEMP_MONGO}" || { } if exists wget; then - wget ${tgz_url} -O mongodb.tgz + wget -q ${tgz_url} -O mongodb.tgz else if exists curl; then curl --output mongodb.tgz ${tgz_url} diff --git a/monkey/monkey_island/monkey_island.spec b/monkey/monkey_island/monkey_island.spec index 342df5ab3..ef6d9c2d3 100644 --- a/monkey/monkey_island/monkey_island.spec +++ b/monkey/monkey_island/monkey_island.spec @@ -15,7 +15,9 @@ def main(): hookspath=None, runtime_hooks=None, binaries=None, - datas=None, + datas=[ + ("../common/BUILD", "/common") + ], excludes=None, win_no_prefer_redirects=None, win_private_assemblies=None, @@ -35,6 +37,7 @@ def main(): debug=False, strip=get_exe_strip(), upx=True, + upx_exclude=['vcruntime140.dll'], console=True, icon=get_exe_icon()) @@ -44,7 +47,7 @@ def is_windows(): def is_32_bit(): - return platform.architecture()[0] == "32bit" + return sys.maxsize <= 2**32 def process_datas(orig_datas): diff --git a/monkey/monkey_island/readme.md b/monkey/monkey_island/readme.md index e0c04e0bb..c16679b61 100644 --- a/monkey/monkey_island/readme.md +++ b/monkey/monkey_island/readme.md @@ -103,4 +103,4 @@ #### How to run -1. When your current working directory is monkey, run ./monkey_island/linux/run.sh (located under /linux) +1. When your current working directory is monkey, run `chmod 755 ./monkey_island/linux/run.sh` followed by `./monkey_island/linux/run.sh` (located under /linux) diff --git a/monkey/monkey_island/requirements.txt b/monkey/monkey_island/requirements.txt index 77ff9a620..3c59bcc47 100644 --- a/monkey/monkey_island/requirements.txt +++ b/monkey/monkey_island/requirements.txt @@ -3,10 +3,6 @@ bson python-dateutil tornado werkzeug -jinja2 -markupsafe -itsdangerous -click flask Flask-Pymongo Flask-Restful diff --git a/monkey/pytest.ini b/monkey/pytest.ini index 3d355a4ac..3596bf5f6 100644 --- a/monkey/pytest.ini +++ b/monkey/pytest.ini @@ -4,3 +4,4 @@ log_cli_level = DEBUG log_cli_format = %(asctime)s [%(levelname)s] %(module)s.%(funcName)s.%(lineno)d: %(message)s log_cli_date_format=%H:%M:%S addopts = -v --capture=sys +norecursedirs = node_modules dist