From fd2b1474486a660a557e32184733d1689b80359d Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Mon, 3 Apr 2017 12:08:36 +0300 Subject: [PATCH 01/38] PKG_INFO --- PKG-INFO | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 PKG-INFO diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..87a9f8f --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,32 @@ +Metadata-Version: 1.1.3 +Name: mazerunner_sdk_python +Version: 1.1.3 +Summary: MazeRunner python SDK +Home-page: https://github.com/Cymmetria/mazerunner_sdk_python +Author: Cymmetria +License: BSD-3 +Download-URL: https://github.com/Cymmetria/mazerunner_sdk_python/archive/1.1.3.tar.gz +Description: This library implements a convenient client for MazeRunner™ API for Python. + Using this library, you will be able to easily configure and manipulate the key features + of MazeRunner, such as the creation of a deception campaign, turning decoys on or off, deployment on + remote endpoints, and inspecting alerts with their attached evidence. + + See the documentation at https://community.cymmetria.com/api + + Fork us at https://github.com/Cymmetria/mazerunner_sdk_python + +Keywords: mazerunner, sdk, python, deception +Requires: argparse +Requires: mohawk +Requires: requests +Requires: requests-hawk +Requires: six +Requires: wsgiref +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.7 +Classifier: Topic :: Software Development :: Libraries From f0b6d04b59846b95d58a05596a8b67aee109b51d Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Mon, 24 Apr 2017 20:52:39 +0300 Subject: [PATCH 02/38] Improve delete everything example --- mazerunner/samples/delete_everything.py | 39 +++++++++++++++++-------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/mazerunner/samples/delete_everything.py b/mazerunner/samples/delete_everything.py index 56397aa..4b614e1 100644 --- a/mazerunner/samples/delete_everything.py +++ b/mazerunner/samples/delete_everything.py @@ -21,7 +21,8 @@ def get_args(): parser.add_argument('api_key', type=str, help="The API key") parser.add_argument('api_secret', type=str, help="The API secret") parser.add_argument('certificate', - type=str, help="The file path to the SSL certificate of the MazeRunner management server") + type=str, help="The file path to the SSL certificate of the " + "MazeRunner management server") return parser.parse_args() @@ -33,33 +34,47 @@ def main(): * Create MazeRunner connection. * Get a collection of all breadcrumbs. * Delete all elements in the collection. - * Same for services. - * Same for decoys. + * Same for deployment groups, decoys, services, endpoints, cidr mappings, background tasks """ args = get_args() - client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, args.certificate, False) + client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, args.certificate) - # delete breadcrumbs: + # Delete breadcrumbs: breadcrumbs = client.breadcrumbs - print 'deleting %d breadcrumbs' % len(breadcrumbs) + print 'Deleting %d breadcrumbs' % len(breadcrumbs) _delete_items_in_collection(breadcrumbs) - # delete deployment groups: + # Delete deployment groups: deployment_groups = client.deployment_groups - print 'deleting %d deployment groups' % len(deployment_groups) + print 'Deleting %d deployment groups' % len(deployment_groups) _delete_items_in_collection(deployment_groups, exclude_persist=True) - # delete services: + # Delete services: services = client.services - print 'deleting %d services' % len(services) + print 'Deleting %d services' % len(services) _delete_items_in_collection(services) - # delete decoys: + # Delete decoys: decoys = client.decoys - print 'deleting %d decoys' % len(decoys) + print 'Deleting %d decoys' % len(decoys) _delete_items_in_collection(decoys) + # Delete CIDR mappings: + cidr_mappings = client.cidr_mappings + print 'Deleting %d cidr mappings' % len(cidr_mappings) + _delete_items_in_collection(cidr_mappings) + + # Delete endpoints: + endpoints = client.endpoints + print 'Deleting %d endpoints' % len(endpoints) + _delete_items_in_collection(endpoints) + + # Acknowledge all complete background tasks: + print 'Acknowledging all complete background tasks' + client.background_tasks.acknowledge_all_complete() + + if __name__ == '__main__': main() From b5e02448327b4a87b329d640449171363f8d612f Mon Sep 17 00:00:00 2001 From: gal_singer Date: Tue, 20 Jun 2017 14:24:24 +0000 Subject: [PATCH 03/38] Merged in bugfix/test_dns (pull request #20) MAZ-2718 --- mazerunner/api_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index 6cee850..78ff24c 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -1,3 +1,4 @@ +import json from httplib import NO_CONTENT import shutil from numbers import Number @@ -258,7 +259,9 @@ def test_dns(self): try: return self._api_client.api_request("{}{}".format(self.url, 'test_dns/'), 'post') except ValidationError as e: - if str(e) == '{"non_field_errors":["Failed to resolve address for decoy"]}': + data = json.loads(e.message) + errors = data.get("non_field_errors", []) + if len(errors) == 1 and errors[0].startswith("Failed to resolve address for decoy"): return False raise From c5bdf00b85c52ca79fa7fcf57b058ce87f2d9277 Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Sun, 25 Jun 2017 17:16:10 +0000 Subject: [PATCH 04/38] Adaptations for the new MazeRunner --- .gitignore | 2 +- README.md | 4 +- makefile | 16 +++ mazerunner/samples/assign_group_to_cidr.py | 80 -------------- ...loyment_group_with_all_breadcrumb_types.py | 98 +++++++++++------- mazerunner/samples/create_smb_breadcrumbs.py | 24 +++-- mazerunner/samples/deploy_to_linux.py | 31 ++++-- .../syslog_server_active_soc_reporter.py | 3 +- mazerunner/samples/track_live_alerts.py | 11 +- source/index.rst | 2 +- Makefile => sphinx_makefile | 0 test/test_site.zip | Bin 418 -> 0 bytes testing_requirements.txt | 1 + 13 files changed, 127 insertions(+), 145 deletions(-) create mode 100644 makefile delete mode 100755 mazerunner/samples/assign_group_to_cidr.py rename Makefile => sphinx_makefile (100%) delete mode 100644 test/test_site.zip diff --git a/.gitignore b/.gitignore index 580e444..be2a033 100755 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,6 @@ mazerunner_sdk.egg-info/ dist .coverage .cache -sdk-venv +.sdk htmlcov test_deployments diff --git a/README.md b/README.md index bb3c467..9705c06 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ Structure of the json_credentials file: ###Generate documentation files: ~~~~ -export PYTHONPATH=`pwd` -make html +make dev-env +make docs ~~~~ ###See documentation at [https://community.cymmetria.com/api](https://community.cymmetria.com/api) diff --git a/makefile b/makefile new file mode 100644 index 0000000..f04a687 --- /dev/null +++ b/makefile @@ -0,0 +1,16 @@ +SHELL := /bin/bash + +.PHONY: dev-env +dev-env: + sudo apt-get install -y texlive-latex-extra texlive-fonts-recommended python-sphinx; \ + virtualenv .sdk; \ + source .sdk/bin/activate; \ + pip install -r requirements.txt; \ + pip install -r testing_requirements.txt; + +.PHONY: docs +docs: export PYTHONPATH = $(shell pwd):$(shell pwd)/mazerunner:$(shell pwd)/mazerunner/samples +docs: + source .sdk/bin/activate; \ + make -f sphinx_makefile html latexpdf + cp build/latex/MazeRunnerSDK.pdf build/html/sdk.pdf diff --git a/mazerunner/samples/assign_group_to_cidr.py b/mazerunner/samples/assign_group_to_cidr.py deleted file mode 100755 index 4310653..0000000 --- a/mazerunner/samples/assign_group_to_cidr.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python2 - -import argparse -from socket import gethostbyaddr -from netaddr import IPNetwork -import mazerunner - - -def get_hostname_for_ip(ip_address): - name = None - try: - name, alias, addresslist = gethostbyaddr(ip_address) - except Exception: - pass - return name - - -def find_deployment_group(client, group_name): - for group in client.deployment_groups: - if group.name == group_name: - return group - - raise ValueError('Deployment group "{}" could not be found'.format(group_name)) - - -def find_endpoint(endpoints, hostname): - hostname = hostname.lower() - for x in endpoints: - if (x.dns and x.dns.lower() == hostname) or \ - (x.hostname and x.hostname.lower() == hostname): - return x - return None - - -def assign_group_to_cidr(deployment_group, cidr, connection_params): - network = IPNetwork(cidr) - - client = mazerunner.connect(**connection_params) - group = find_deployment_group(client, deployment_group) - - for address in network.iter_hosts(): - name = get_hostname_for_ip(str(address)) - if not name: - print "Could not resolve hostname for {}".format(address) - continue - # Find the endpoint object - endpoints = client.endpoints.filter(name) - e = find_endpoint(endpoints, name) - if not e: - print "Could not find endpoint object for {}".format(name) - continue - try: - client.endpoints.reassign_to_group(group, [e]) - except mazerunner.exceptions.ValidationError: - # Workaround... - pass - - print "Endpoint at {} ({}) assigned to group {}".format(address, name, deployment_group) - - -def main(): - parser = argparse.ArgumentParser("Assign deployment group to all endpoints in a CIDR range") - parser.add_argument('mazerunner', help="IP address of MazeRunner management server") - parser.add_argument('api_key', help="API key") - parser.add_argument('api_secret', help="API secert key") - parser.add_argument('certificate', help="Path to MazeRunner's SSL certificate") - parser.add_argument('deployment_group', help="Name of the deployment group to assign to") - parser.add_argument('cidr', help='CIDR range to assign (e.g. 192.168.0.0/24)') - - args = parser.parse_args() - - connection_params = dict(ip_address=args.mazerunner, - api_key=args.api_key, - api_secret=args.api_secret, - certificate=args.certificate) - assign_group_to_cidr(args.deployment_group, args.cidr, connection_params) - - -if __name__ == '__main__': - main() diff --git a/mazerunner/samples/create_deployment_group_with_all_breadcrumb_types.py b/mazerunner/samples/create_deployment_group_with_all_breadcrumb_types.py index ab7359d..b97c3f6 100644 --- a/mazerunner/samples/create_deployment_group_with_all_breadcrumb_types.py +++ b/mazerunner/samples/create_deployment_group_with_all_breadcrumb_types.py @@ -1,7 +1,7 @@ -''' -This sample script creates a deployment group with each of the available breadcrumb types +""" +This sample script creates a deployment group with the requested breadcrumb type and downloads the deployment package -''' +""" import random import argparse import zipfile @@ -10,6 +10,9 @@ import time import mazerunner +from mazerunner.exceptions import ValidationError + +COMMON_SAMPLE_PASSWORD = ['password', '12345678', 'xyzvbnm,', 'qwertyui'] def _create_temp_file(): @@ -61,7 +64,7 @@ def _create_dummy_zip_file(): 'requires_username': False, 'requires_password': False, 'args': {'browser': 'chrome', 'subservice': 'phpmyadmin'}, - 'required_service':{'type': 'http', 'decoy': 'DMZ Server', 'args': { + 'required_service': {'type': 'http', 'decoy': 'DMZ Server', 'args': { 'web_apps': ['phpmyadmin'], 'zip_file_path': _create_dummy_zip_file() }} }, @@ -69,7 +72,7 @@ def _create_dummy_zip_file(): 'requires_username': True, 'requires_password': True, 'args': {}, - 'required_service':{'type': 'smb', 'decoy': 'Windows Server', 'args': { + 'required_service': {'type': 'smb', 'decoy': 'Windows Server', 'args': { 'share_name': 'transfer_files', 'zip_file_path': _create_dummy_zip_file() }} }, @@ -77,7 +80,7 @@ def _create_dummy_zip_file(): 'requires_username': True, 'requires_password': True, 'args': {}, - 'required_service':{'type': 'git', 'decoy': 'Development Server', 'args': { + 'required_service': {'type': 'git', 'decoy': 'Development Server', 'args': { 'repository_name': 'backend', 'zip_file_path': _create_dummy_zip_file() }} }, @@ -90,8 +93,8 @@ def _create_dummy_zip_file(): 'netshare': { 'requires_username': True, 'requires_password': True, - 'args': {}, - 'required_service':{'type': 'smb', 'decoy': 'Windows Server', 'args': { + 'args': {'persistence': 'non_persistent'}, + 'required_service': {'type': 'smb', 'decoy': 'Windows Server', 'args': { 'share_name': 'transfer_files', 'zip_file_path': _create_dummy_zip_file() }} }, @@ -99,7 +102,7 @@ def _create_dummy_zip_file(): 'requires_username': True, 'requires_password': True, 'args': {'network_name': 'office'}, - 'required_service':{'type': 'openvpn', 'decoy': 'DMZ Server', 'args': { + 'required_service': {'type': 'openvpn', 'decoy': 'DMZ Server', 'args': { 'cert_country': 'US', 'cert_state': 'CA', 'cert_city': 'San Francisco', @@ -111,19 +114,19 @@ def _create_dummy_zip_file(): 'requires_username': True, 'requires_password': True, 'args': {}, - 'required_service':{'type': 'rdp', 'decoy': 'Windows Server', 'args': {}} + 'required_service': {'type': 'rdp', 'decoy': 'Windows Server', 'args': {}} }, 'ssh': { 'requires_username': True, 'requires_password': True, 'args': {}, - 'required_service':{'type': 'ssh', 'decoy': 'DMZ Server', 'args': {}} + 'required_service': {'type': 'ssh', 'decoy': 'DMZ Server', 'args': {}} }, 'ssh_privatekey': { 'requires_username': True, 'requires_password': False, - 'args': {'deploy_for': 'last_login', 'installation_type': 'alias'}, - 'required_service':{'type': 'ssh', 'decoy': 'DMZ Server', 'args': {}} + 'args': {'deploy_for': 'root', 'installation_type': 'alias'}, + 'required_service': {'type': 'ssh', 'decoy': 'DMZ Server', 'args': {}} }, } @@ -143,12 +146,24 @@ def get_args(): type=str, help='The file path to the SSL certificate of the ' 'MazeRunner management server') - parser.add_argument('deployment_group_name', type=str, help='The name of the deployment group to be created') - parser.add_argument('username', type=str, help='The username that will be shared among the breadcrumbs') - parser.add_argument('os', type=str, choices=['Windows', 'Linux'], - help='The OS for which we want to get a deployment script pack') - parser.add_argument('-p', '--passwords', required=False, type=str, help='The file path to a list of passwords') - parser.add_argument('-f', '--format', required=False, type=str, choices=['ZIP', 'EXE', 'MSI'], default='ZIP', + parser.add_argument('deployment_group_name', + type=str, + help='The name of the deployment group to be created') + parser.add_argument('username', + type=str, + help='The username that will be shared among the breadcrumbs') + parser.add_argument('breadcrumb_type', type=str, choices=BREADCRUMB_DATA.keys()) + parser.add_argument('-p', + '--passwords', + required=False, + type=str, + help='The file path to a list of passwords') + parser.add_argument('-f', + '--format', + required=False, + type=str, + choices=['ZIP', 'EXE', 'MSI'], + default='ZIP', help='The file format for the deployment script pack') return parser.parse_args() @@ -162,7 +177,7 @@ def _get_password(passwords_file): data = f.read() passwords = [p.strip() for p in data.split('\n') if p.strip()] else: - passwords = ['password', '12345678', 'xyzvbnm,', 'qwertyui'] # default common passwords + passwords = COMMON_SAMPLE_PASSWORD return random.choice(passwords) @@ -216,7 +231,8 @@ def create_service_if_needed(client, service_data): args['name'] = service_name service = client.services.create(**args) - if 'zip_file_path' in service_data['args'] and os.path.exists(service_data['args']['zip_file_path']): + if 'zip_file_path' in service_data['args'] and \ + os.path.exists(service_data['args']['zip_file_path']): os.remove(service_data['args']['zip_file_path']) decoy = create_decoy_if_needed(client, service_data['decoy']) @@ -272,23 +288,22 @@ def main(): """ args = get_args() - client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, args.certificate, False) + client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, args.certificate) password = _get_password(args.passwords) deployment_group = client.deployment_groups.create(name=args.deployment_group_name) - for breadcrumb_type in BREADCRUMB_DATA: - breadcrumb = create_breadcrumb( - client, - breadcrumb_type, - BREADCRUMB_DATA[breadcrumb_type], - args.username, - password, - args.deployment_group_name) - breadcrumb.add_to_group(deployment_group.id) + breadcrumb = create_breadcrumb( + client, + args.breadcrumb_type, + BREADCRUMB_DATA[args.breadcrumb_type], + args.username, + password, + args.deployment_group_name) + breadcrumb.add_to_group(deployment_group.id) - print "Waiting for deployment group to be available - this takes a few minutes" + print "Waiting for deployment group to become available - this may take a few minutes" deployment_group.load() while not deployment_group.is_active: @@ -296,13 +311,20 @@ def main(): deployment_group.load() save_to = '%s' % args.deployment_group_name - deployment_group.deploy( - location_with_name=save_to, - os=args.os, - download_type="install", - download_format=args.format) - print "Deployment package saved to %s.%s" % (save_to, args.format.lower()) + for operating_system in ['Windows', 'Linux']: + try: + deployment_group.deploy( + location_with_name='%s_%s' % (save_to, operating_system), + os=operating_system, + download_type="install", + download_format=args.format) + except ValidationError: + print 'This breadcrumb is not supported on %s, skipping' % operating_system + + print "%s deployment package saved to %s.%s" % (operating_system, + save_to, + args.format.lower()) if __name__ == '__main__': main() diff --git a/mazerunner/samples/create_smb_breadcrumbs.py b/mazerunner/samples/create_smb_breadcrumbs.py index 833e069..51ac240 100644 --- a/mazerunner/samples/create_smb_breadcrumbs.py +++ b/mazerunner/samples/create_smb_breadcrumbs.py @@ -11,6 +11,7 @@ import mazerunner + def _create_temp_file(): """ this function creates an empty temporary file and returns a path to the file @@ -44,9 +45,12 @@ def get_args(): parser.add_argument('api_key', type=str, help="The API key") parser.add_argument('api_secret', type=str, help="The API secret") parser.add_argument('certificate', - type=str, help="The file path to the SSL certificate of the MazeRunner management server") + type=str, + help="The file path to the SSL certificate " + "of the MazeRunner management server") parser.add_argument('usernames_file', type=str, help="The file path to a list of usernames") - parser.add_argument('-p', '--passwords', required=False, type=str, help="The file path to a list of passwords") + parser.add_argument('-p', '--passwords', required=False, type=str, + help="The file path to a list of passwords") return parser.parse_args() @@ -70,14 +74,15 @@ def main(): # Connect to the MazeRunner API # The client object holds your session data and allows you to access the MazeRunner resources - client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, args.certificate, False) - + client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, args.certificate) + # Create a decoy # This will create a decoy virtual machine inside MazeRunner. # The decoy acts as a trap, # Any action done on the decoy is monitored and will generate an alert on MazeRunner print "Creating Decoy" - decoy = client.decoys.create(name="Backup Server Decoy", hostname="nas-backup-02", os="Ubuntu_1404", vm_type="KVM") + decoy = client.decoys.create(name="Backup Server Decoy", hostname="nas-backup-02", + os="Ubuntu_1404", vm_type="KVM") while decoy.machine_status != "not_seen": # make sure the decoy was created time.sleep(5) decoy.load() @@ -110,14 +115,17 @@ def main(): print "Creating Breadcrumbs" for idx, username in enumerate(usernames): # Create breadcrumb - # Breadcrumbs can be deployed on endpoints to trick an attacker to interact with a decoy and generate an alert + # Breadcrumbs can be deployed on endpoints to trick + # an attacker to interact with a decoy and generate an alert breadcrumb = client.breadcrumbs.create( name="SMB Breadcrumb %d" % idx, breadcrumb_type="netshare", username=username, - password=random.choice(passwords)) + password=random.choice(passwords), + persistence='persistent', + registry_entry_name='test registry entry') - # Connect the breadrumb to the service + # Connect the breadcrumb to the service # When a breadcrumb is connected to a service, the credential and other information # found in the breadcrumb will be usable with the service breadcrumb.connect_to_service(service.id) diff --git a/mazerunner/samples/deploy_to_linux.py b/mazerunner/samples/deploy_to_linux.py index baaf8be..0f6e397 100644 --- a/mazerunner/samples/deploy_to_linux.py +++ b/mazerunner/samples/deploy_to_linux.py @@ -1,6 +1,7 @@ """ -This sample script deploys (install/uninstall) a specific Deployment Group on linux endpoint[s] supplied by the user. -A unique endpoint can be provided from the command line, or use a csv file to deploy on multiple endpoints. +This sample script deploys (install/uninstall) a specific Deployment Group on linux endpoint[s] +supplied by the user. A unique endpoint can be provided from the command line, or use a csv file to +deploy on multiple endpoints. """ import tempfile import mazerunner @@ -26,13 +27,18 @@ def get_args(): parser.add_argument('ip_address', type=str, help="IP address of MazeRunner management server.") parser.add_argument('api_key', type=str, help="The API key.") parser.add_argument('api_secret', type=str, help="The API secret.") - parser.add_argument('deployment_type', type=str, help='Specify which deployment we want to do - install/uninstall.', + parser.add_argument('deployment_type', type=str, + help='Specify which deployment we want to do - install/uninstall.', choices=['install', 'uninstall']) - parser.add_argument('deployment_group', type=str, help='The name of the Deployment Group in MazeRunner.') - parser.add_argument('--certificate', - type=str, help="The file path to the SSL certificate of the MazeRunner management server.") + parser.add_argument('deployment_group', type=str, + help='The name of the Deployment Group in MazeRunner.') + parser.add_argument( + '--certificate', + type=str, + help="The file path to the SSL certificate of the MazeRunner management server.") parser.add_argument('--ip', type=str, help='ip of a linux endpoint to deploy.') - parser.add_argument('--port', type=int, help='ssh port of a linux endpoint - default to 22.', default='22') + parser.add_argument('--port', type=int, help='ssh port of a linux endpoint - default to 22.', + default='22') parser.add_argument('--user', type=str, help='username of the linux endpoint to deploy.') parser.add_argument('--passwd', type=str, help='password of the linux endpoint to deploy.') parser.add_argument('--csv', type=str, help='Name of a CSV file, each line of the CSV should ' @@ -58,6 +64,7 @@ def init_ssh_client(host, port, user, passwd): sftpclient = paramiko.SFTPClient.from_transport(transport) return sshclient, sftpclient + def run_cmd(ssh, cmd): """ Tun a command on an existing ssh connection. @@ -116,6 +123,10 @@ def deploy_zip_on_endpoints(zipfile, endpoints, deploy_type, deployment_group): ) run_cmd(ssh, run_setup_cmd) + # Delete the original zip file + rm_file_cmd = 'sudo rm -rf {} {}'.format(zipfile, zipfile[:-4]) + run_cmd(ssh, rm_file_cmd) + print("MazeRunner deployment group '{}' {}ed successfully on '{}'.".format( deployment_group, deploy_type, @@ -178,7 +189,7 @@ def main(): raise Exception("No endpoints found to work on.") # Init the MazeRunner client - client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, args.certificate, False) + client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, args.certificate) # Get a new tempfile name dep_file = tempfile.mkdtemp() @@ -195,7 +206,9 @@ def main(): deployment_group.deploy(dep_file, 'Linux', args.deployment_type, 'ZIP') # Deploy the file on all endpoints we have - deploy_zip_on_endpoints(dep_file_full, linux_endpoints_to_deploy, args.deployment_type, args.deployment_group) + deploy_zip_on_endpoints(dep_file_full, linux_endpoints_to_deploy, args.deployment_type, + args.deployment_group) + if __name__ == "__main__": main() diff --git a/mazerunner/samples/syslog_server_active_soc_reporter.py b/mazerunner/samples/syslog_server_active_soc_reporter.py index 2731234..0b7898b 100644 --- a/mazerunner/samples/syslog_server_active_soc_reporter.py +++ b/mazerunner/samples/syslog_server_active_soc_reporter.py @@ -130,8 +130,7 @@ def main(): client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, - args.certificate, - False) + args.certificate) try: server = SocketServer.UDPServer((HOST, PORT), get_syslog_handler(client)) diff --git a/mazerunner/samples/track_live_alerts.py b/mazerunner/samples/track_live_alerts.py index ba2e5cf..fc77e64 100644 --- a/mazerunner/samples/track_live_alerts.py +++ b/mazerunner/samples/track_live_alerts.py @@ -6,7 +6,7 @@ import time import mazerunner -WAIT_TIME = 3 # Time to wait in seconds +WAIT_TIME = 3 # Time to wait in seconds DISPLAY_FORMAT = '''Got a new alert! Decoy: {decoy_name} Alert Type: {alert_type}''' @@ -21,7 +21,9 @@ def get_args(): parser.add_argument('api_key', type=str, help="The API key") parser.add_argument('api_secret', type=str, help="The API secret") parser.add_argument('certificate', - type=str, help="The file path to the SSL certificate of the MazeRunner management server") + type=str, + help="The file path to the SSL certificate of the " + "MazeRunner management server") parser.add_argument('-m', '--show-muted', action='store_true', help="Show mute-level alerts as well as alert-level alerts") return parser.parse_args() @@ -41,12 +43,13 @@ def main(): """ args = get_args() - client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, args.certificate, False) + client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, args.certificate) # Get alerts alerts = client.alerts alert_types = alerts.params()['alert_type'] - filtered_alerts = alerts.filter(filter_enabled=True, only_alerts=not args.show_muted, alert_types=alert_types) + filtered_alerts = alerts.filter(filter_enabled=True, only_alerts=not args.show_muted, + alert_types=alert_types) print "Showing all alerts live. Press Ctrl+C to exit." last_length = len(list(filtered_alerts)) try: diff --git a/source/index.rst b/source/index.rst index 396d51d..79ae2fd 100644 --- a/source/index.rst +++ b/source/index.rst @@ -17,4 +17,4 @@ Fork us at https://github.com/Cymmetria/mazerunner_sdk_python installation api_client samples - Download as PDF + Download as PDF diff --git a/Makefile b/sphinx_makefile similarity index 100% rename from Makefile rename to sphinx_makefile diff --git a/test/test_site.zip b/test/test_site.zip deleted file mode 100644 index 75c33ced409e4f0503c102347de5d9c28190ba59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 418 zcmWIWW@h1H00F0>QZFzAN^mjAFqEVgm&6xmmZa*3hHx@4|B<{HAuDw;qO^jWfsy3} zGXn#d2mqQP0@TF;HbZl|kgx}kmkq=cC}x07D@aUF)ypW!&4HT;G8N6l&4E5A{q Date: Wed, 28 Jun 2017 13:17:05 +0300 Subject: [PATCH 05/38] Add missing test file --- test/test_site.zip | Bin 0 -> 418 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/test_site.zip diff --git a/test/test_site.zip b/test/test_site.zip new file mode 100644 index 0000000000000000000000000000000000000000..75c33ced409e4f0503c102347de5d9c28190ba59 GIT binary patch literal 418 zcmWIWW@h1H00F0>QZFzAN^mjAFqEVgm&6xmmZa*3hHx@4|B<{HAuDw;qO^jWfsy3} zGXn#d2mqQP0@TF;HbZl|kgx}kmkq=cC}x07D@aUF)ypW!&4HT;G8N6l&4E5A{q Date: Sun, 16 Jul 2017 10:01:24 +0000 Subject: [PATCH 06/38] fixed MAZ-3149 - bug in handling query params that caused long iterations on alerts to fail --- mazerunner/api_client.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index 78ff24c..41882b9 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -1,4 +1,5 @@ import json +import urlparse from httplib import NO_CONTENT import shutil from numbers import Number @@ -14,7 +15,7 @@ class BaseCollection(object): def __init__(self, api_client, obj_class=None): """ - :param api_client: The connection instance. + :param api_client: The connection instance. :param obj_class: The class, instance of which all the members should be. """ self._api_client = api_client @@ -1417,6 +1418,22 @@ def api_request(self, if not url.startswith("http"): url = self._base_url + url + parsed = urlparse.urlparse(url) + parsed_no_query = urlparse.ParseResult( + scheme=parsed.scheme, + netloc=parsed.netloc, + path=parsed.path, + params=parsed.params, + query='', + fragment=parsed.fragment) + url = urlparse.urlunparse(parsed_no_query) + query = {query_param_name: set(query_param_value) + for query_param_name, query_param_value + in urlparse.parse_qs(parsed.query).items()} + if query_params: + query.update(query_params) + query_params = query + request_args = dict( method=method, url=url, From 286c87f2db13778ffb5f8a89e4308f558c071d81 Mon Sep 17 00:00:00 2001 From: Imri Goldberg Date: Sun, 16 Jul 2017 11:30:02 +0000 Subject: [PATCH 07/38] Merged in bugfix/speed_increase (pull request #24) * small fix that significantly speeds up work with alerts Approved-by: Yochai Blumenfeld --- mazerunner/api_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index 41882b9..7ff6aae 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -9,6 +9,7 @@ from mazerunner.exceptions import ValidationError, ServerError, BadParamError, \ InvalidInstallMethodError +ALERTS_PER_PAGE = 500 class BaseCollection(object): MODEL_CLASS = None @@ -869,7 +870,8 @@ def __init__(self, api_client, filter_enabled=False, only_alerts=False, alert_ty def _get_query_params(self): return dict(filter_enabled=self.filter_enabled, only_alerts=self.only_alerts, - alert_types=self.alert_types) + alert_types=self.alert_types, + per_page=ALERTS_PER_PAGE) def filter(self, filter_enabled=False, only_alerts=False, alert_types=None): """ From bda38add8a1f2e50535c6f06a34d8df6aa31946b Mon Sep 17 00:00:00 2001 From: Nick Bartosh Date: Sun, 16 Jul 2017 16:19:14 +0000 Subject: [PATCH 08/38] Merged in feature/cuckoo_integration (pull request #19) New Sample file: Upload files to Cuckoo * New Sample file: Upload files to Cuckoo * New Sample file: Upload files to Cuckoo * New Sample file: Upload files to Cuckoo * Updates to script recommended by Imri * Updates * Updates to the files based on comments from Yochai and Imri * Updates to fix ambigious wording. Also brought creation of API URL into the code and a few minor edits recommended by Yochai. * Updated code to include a date variable that limits which alerts are sent to Cuckoo. Streamlined validation checking * Revert "Updated code to include a date variable that limits which alerts are sent to Cuckoo. Streamlined validation checking" This reverts commit d69052aceeda2439124c2a42e6febca5315a8efd. * Added back --skip-verification flag to args. Approved-by: Yochai Blumenfeld --- .../samples/send_bin_files_to_cuckoo.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 mazerunner/samples/send_bin_files_to_cuckoo.py diff --git a/mazerunner/samples/send_bin_files_to_cuckoo.py b/mazerunner/samples/send_bin_files_to_cuckoo.py new file mode 100644 index 0000000..0d6ed9f --- /dev/null +++ b/mazerunner/samples/send_bin_files_to_cuckoo.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python2 + +import argparse +import requests +import os +import tempfile +import shutil + +import mazerunner + + +def retrieve_files_from_mazerunner(client, dirpath): + + const_bin = ".bin" + + # Create a list to store our files in + alert_files = [] + + # Grab Code Execution Alerts + filter_params = dict( + filter_enabled = True, + only_alerts = False, + alert_types = ["code","unsigned_code"] + ) + alerts = client.alerts.filter(**filter_params) + + # Iterate through our alerts and download the bin files if one exists + + for alert in alerts: + + # Remove the extension as its added when we download the file from MazeRunner + dest_file_name = alert.image_file_name + + # Local filepath where we will store our executable + dest_file_path = os.path.join(dirpath, "%s_%s" % (alert.decoy["hostname"], dest_file_name)) + + try: + alert.download_image_file(dest_file_path[:-len(const_bin)]) + + # Store path for later use + alert_files.append(dest_file_path) + + except mazerunner.exceptions.ValidationError: + # No file to download + pass + + return alert_files + + +def send_files_to_cuckoo(cuckoo_api, verify_ssl, alert_files): + + # Iterate through our file list and send them to Cuckoo + for current_file in alert_files: + fname = os.path.basename(current_file) + cuckoo_url = 'https://%s/tasks/create/file' % cuckoo_api + + try: + with open(current_file, "rb") as sample: + files = {"file": (fname, sample)} + r = requests.post(cuckoo_url, files=files, verify=verify_ssl) + + # Ensure we successfully sent our file to Cuckoo + if r.status_code != 200: + print "Error uploading file:" + fname + continue + except IOError: + print current_file + " could not be opened for reading." + pass + + +def transfer_files_to_cuckoo(cuckoo_api, verify_ssl, connection_params): + + # Create a temporary directory to store our bin files in + dirpath = tempfile.mkdtemp() + + try: + # Instantiate our MazeRunner connection + client = mazerunner.connect(**connection_params) + + # Download files from MazeRunner, Upload files to Cuckoo + alert_files = retrieve_files_from_mazerunner(client, dirpath) + send_files_to_cuckoo(cuckoo_api, verify_ssl, alert_files) + + finally: + # Clean up after our work + shutil.rmtree(dirpath) + + print "Process complete" + + +def main(): + parser = argparse.ArgumentParser("Send code execution files from MazeRunner to a Cuckoo Instance") + parser.add_argument('mazerunner', help="IP address of MazeRunner management server") + parser.add_argument('api_key', help="API key") + parser.add_argument('api_secret', help="API secert key") + parser.add_argument('certificate', help="Path to MazeRunner's SSL certificate") + parser.add_argument('cuckoo_api', help="IP or FQDN of Cuckoo (192.168.1.10:4343 or cuckoo.yourdomain.com:4343") + parser.add_argument('--skip-verification', dest='verify_ssl', action='store_false', help='Skip SSL verification') + + args = parser.parse_args() + + connection_params = dict(ip_address=args.mazerunner, + api_key=args.api_key, + api_secret=args.api_secret, + certificate=args.certificate + ) + + transfer_files_to_cuckoo(args.cuckoo_api, args.verify_ssl, connection_params) + + +if __name__ == '__main__': + main() \ No newline at end of file From 3f4d7533f52095698a051d49bd8536e23498f4db Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Mon, 17 Jul 2017 16:09:01 +0000 Subject: [PATCH 09/38] Adapt the ElasticSearch integration sample to the new UI --- .../elasticsearch_responder_monitor.py | 78 ++++++------------- 1 file changed, 23 insertions(+), 55 deletions(-) diff --git a/mazerunner/samples/elasticsearch_responder_monitor.py b/mazerunner/samples/elasticsearch_responder_monitor.py index 3e62b66..3e7145a 100644 --- a/mazerunner/samples/elasticsearch_responder_monitor.py +++ b/mazerunner/samples/elasticsearch_responder_monitor.py @@ -1,38 +1,6 @@ """ -This script should be used together with MazeRunner's Responder feature. -The Responder feature generates "net use" requests in the network, which will -function as the breadcrumbs. -When an attacker runs the Responder.py attack tool, they will -"catch" that username, and when they make a failed attempt to authenticate using that username, -an event will be logged by the hacked endpoint. -From there, the logged event will be sent to Elasticsearch. - -This script will find the event in Elasticsearch and send it to MazeRunner. For other products, -like Splunk, MazeRunner already has a built-in integration. - -In order to use this script, please configure the following: - -1. Open the configuration screen in MazeRunner and go to the SOC tab. -Under "SOC Interfaces", click "Add", choose "SOC via MazeRunner API", and -give it a name (we named it "elasticsearch_responder" in the example below). - -2. Still on the SOC tab, turn on the Responder, and provide credentials for the Responder to -access the endpoints and run the "net use" commands. - -3. At the bottom of the SOC tab, in the 'Map Responder fields' section, click 'Add' and create a -mapping of the required fields to their names in your Elasticsearch system. - -4. On the Manage API keys screen, generate an API key-secret pair, and download -the certificate to your computer. - -5. On the Campaign screen, create a nested decoy, an SMB service, -and a "Responder - Pass the Hash" breadcrumb, and connect them all together. - -6. Create a deployment group and connect the Responder breadcrumb and several endpoints to that -deployment group. - -7. Create a configuration file and pass its name as the first parameter for this -script. Use the -h option for displaying a sample configuration JSON file. +This script will allow you to integrate the Responder feature in MazeRunner with ElasticSearch. +For usage information, run the script with no params. """ @@ -279,30 +247,30 @@ def get_example(): if __name__ == '__main__': parser = argparse.ArgumentParser( - usage='This script should be used together with the Responder feature of MazeRunner.\n' - 'The Responder generates "net use" requests in the network, which will\n' - 'function as the breadcrumbs, using which we want the attacker to try to\n' - 'authenticate. As soon as the attacker tries to login using this user,\n' - 'the event will be logged and sent to elastic search. This script will find\n' + usage='\n' + 'This script should be used together with the Responder feature of MazeRunner.\n' + 'The Responder breadcrumb and service detect Responder.py tool usage in your ' + 'network.\n' + 'When MazeRunner detects such tool it feeds it with the credentials you configured\n' + 'in the breadcrumb.\n' + 'As soon as the attacker tries to use these stolen credentials and log in\n' + 'to one of the monitored assets in your network,\n' + 'the event will be logged and sent to ElasticSearch. This script will find\n' 'the event in ElasticSearch and send it to MazeRunner. For other products,\n' 'like Splunk, MazeRunner already has a built-in integration.\n\n' 'In order to use this script, please configure the following:\n\n' - '1. Open the configuration screen in MazeRunner and go to the SOC tab.\n' - 'Under "SOC Interfaces" click "add", choose "SOC via MazeRunner API" and\n\n' - 'give it a name (we named it "elasticsearch_responder" in the example below).\n' - '2. Still on the SOC tab, turn on the Responder, and provide it credentials using ' - 'which it can access the endpoints and run the "net use" commands.\n\n' - '3. At the bottom of the SOC tab, in the "Map Responder fields" section, click ' - '"Add" and create a mapping of the required fields to their names in your ' - 'Elasticsearch system.\n\n' - '4. In the Manage API Keys screen, generate an api key-secret pair, and download \n' - 'the certificate to your computer.\n\n' - '5. In the campaign screen, create a nested decoy, an SMB service,\n' - 'and a "Responder - Pass the Hash" breadcrumb, and connect them all together.\n\n' - '6. Create a deployment group and connect to it the responder breadcrumb and ' - 'several endpoints.\n\n' - '7. Create a configuration file and pass its name as the first parameter for this\n' - 'script. Here is an example for the file:\n\n %s' % Config.get_example()) + '1. Open the ActiveSOC tab in MazeRunner.\n' + 'Under "SOC Interfaces" click "add", choose "SOC via MazeRunner API" and\n' + 'give it a name (we named it "elasticsearch_responder" in the example below).\n\n' + '2. In the Settings tab, go to the "API Keys" sub-tab. There, generate an api \n' + 'key-secret pair, and download the certificate to your computer.\n\n' + '3. Create a responder campaign. For more information about this step,\n' + 'see the "ActiveSoc and responder monitor activity feed" section in the user ' + 'manual.\n\n' + '4. Under the ActiveSOC tab, go to "Responder Monitor", and make sure ' + 'that "Responder monitor SOC integration" is activated.\n\n' + '5. Create a configuration file and pass the file name as the first parameter for \n' + 'this script. Here is an example for the file:\n\n %s' % Config.get_example()) parser.add_argument('config_file') args = parser.parse_args() From a6d8be08d072ff86d60612ff124f42020854575d Mon Sep 17 00:00:00 2001 From: Nadav Lev Date: Mon, 31 Jul 2017 14:50:58 +0000 Subject: [PATCH 10/38] Merged in bugfix/test_check_conflicts (pull request #25) fix test_check_conflicts --- test/test_sdk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_sdk.py b/test/test_sdk.py index 9972a79..052d8f8 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -457,7 +457,7 @@ def test_check_conflicts(self): assert dep_group.check_conflicts('Windows') == [ { u'error': u"Conflict between breadcrumbs ssh1 and ssh2: " - u"Two SSH breadcrumb can't point to the same " + u"Two SSH breadcrumbs can't point to the same " u"user/decoy combination on the same endpoint" } ] From fd00fd087db1b52ca0e8069a14fdd5364c6cd78b Mon Sep 17 00:00:00 2001 From: Nuni Date: Thu, 10 Aug 2017 15:22:21 +0300 Subject: [PATCH 11/38] MAZ-3319 --- mazerunner/api_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index 7ff6aae..9b9871f 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -599,9 +599,10 @@ def auto_deploy_groups(self, deployment_groups_ids, install_method, run_method, domain=domain, deploy_on=deploy_on ) - self._api_client.api_request(url="{}{}".format(self._get_url(), "auto_deploy_groups/"), - method='post', - data=data) + url = "{}1/".format(self._get_url().replace(self._obj_class.NAME, 'deploy')) + return self._api_client.api_request(url=url, + method='put', + data=data) def deploy_all(self, location_with_name, os, download_format="ZIP"): """ From 9d971b843e9f34a721673daa6f1c3635b7991e94 Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Sun, 13 Aug 2017 16:12:19 +0300 Subject: [PATCH 12/38] change url --- mazerunner/api_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index 9b9871f..fbef7cb 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -599,10 +599,10 @@ def auto_deploy_groups(self, deployment_groups_ids, install_method, run_method, domain=domain, deploy_on=deploy_on ) - url = "{}1/".format(self._get_url().replace(self._obj_class.NAME, 'deploy')) + url = "{}{}/".format(self._get_url(), 'deploy') return self._api_client.api_request(url=url, - method='put', - data=data) + method='post', + data=data) def deploy_all(self, location_with_name, os, download_format="ZIP"): """ From b9aa801dc53b4484578e776fdf67fa781494bf2c Mon Sep 17 00:00:00 2001 From: gal_singer Date: Wed, 16 Aug 2017 12:36:19 +0000 Subject: [PATCH 13/38] Merged in (pull request #26) Endpoint creation from SDK MAZ-3196 --- mazerunner/api_client.py | 26 ++++++++++++++++--- test/test_sdk.py | 54 +++++++++++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index fbef7cb..b68b9b7 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -85,7 +85,7 @@ def create_item(self, data, files=None): :param files: Relevant file paths to upload for the element. """ response = self._api_client.api_request(self._get_url(), 'post', data=data, files=files) - return self._obj_class(self._api_client, response) + return self._obj_class(self._api_client, response).load() class UnpaginatedEditableCollection(EditableCollection): @@ -146,8 +146,9 @@ def _update_related_fields(self): self._param_dict.get(key, []))) for key, field_type in self.RELATED_FIELDS.iteritems(): - if key in self._param_dict: - setattr(self, key, field_type(self._api_client, self._param_dict[key])) + value = self._param_dict.get(key, None) + if value: + setattr(self, key, field_type(self._api_client, value)) def __getattr__(self, item): if item not in self._param_dict: @@ -941,6 +942,10 @@ class Endpoint(Entity): NAME = 'endpoint' + RELATED_FIELDS = { + 'deployment_group': DeploymentGroup, + } + def delete(self): """ Delete the endpoint. @@ -951,7 +956,7 @@ def delete(self): self._api_client.api_request(url, 'post', data=data) -class EndpointCollection(Collection): +class EndpointCollection(EditableCollection): """ A subset of the endpoints in the system. @@ -985,6 +990,19 @@ def __init__(self, self.statuses = statuses self.deploy_groups = deploy_groups + def create(self, ip_address=None, dns=None, hostname=None, deployment_group_id=None): + """ + Create an Endpoint. + + Pass at least ip_address or dns or hostname as a parameter + :param deployment_group_id: id of the deployment group + :param ip_address: address of the endpoint + :param dns: fqdn of the endpoint + :param hostname: hostname of the endpoint + """ + data = dict(ip_address=ip_address, dns=dns, hostname=hostname, deployment_group=deployment_group_id) + return self.create_item(data=data) + def _get_query_params(self): return { 'filter_enabled': self.filter_enabled, diff --git a/test/test_sdk.py b/test/test_sdk.py index 052d8f8..c1a3dfc 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -732,10 +732,12 @@ def test_params(self): class TestEntity(APITest): def test_repr(self): service = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh") - assert str(service) == "".format( - serv=self.mazerunner_ip_address, - service_id=service.id) + + str_service = "".format( + serv=self.mazerunner_ip_address, service_id=service.id) + assert str(service) == str_service def test_get_attribute(self): service = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh") @@ -871,7 +873,7 @@ def _test_import_endpoint(): cidr_mapping = self.cidr_mappings.create( cidr_block='%s/30' % self.lab_endpoint_ip, - deployment_group=1, + deployment_group_id=1, comments='no comments', active=True ) @@ -958,7 +960,7 @@ def _test_stop_import(): self.cidr_mappings.create( cidr_block='%s/24' % self.lab_endpoint_ip, - deployment_group=1, + deployment_group_id=1, comments='no comments', active=True ) @@ -979,3 +981,43 @@ def _test_stop_import(): _test_delete() _test_data() _test_stop_import() + + def test_create_endpoint(self): + for params in [ + dict(ip_address='1.1.1.1'), + dict(dns='endpoint_address.endpoint.local'), + dict(hostname='hostname'), + dict(dns='endpoint_address.endpoint.local', ip_address='1.1.1.1'), + ]: + endpoint = self.endpoints.create(**params) + assert endpoint + for key, value in params.iteritems(): + assert getattr(endpoint, key) == value + assert len(self.endpoints) == 1 + endpoint.delete() + + def test_create_endpoint_with_deployment_group(self): + ip_address = "1.1.1.1" + endpoint = self.endpoints.create(ip_address=ip_address, deployment_group_id=1) + assert endpoint.ip_address == ip_address + assert endpoint.deployment_group.name == "All Breadcrumbs" + endpoint.delete() + + def test_create_invalid_endpoint(self): + for params, expected_error_message in [ + (dict(ip_address='1.1.1.1.1'), "Enter a valid IPv4 address."), + (dict(dns='A'*256), "Ensure this field has no more than 255 characters."), + (dict(hostname='A'*15), "Ensure this field has no more than 15 characters."), + (dict(), "require dns or hostname or ip_address"), + ]: + try: + self.endpoints.create(**params) + raise AssertionError, "Creation of the endpoint should raise an exception" + except ValidationError as e: + error = json.loads(e.message) + if params: + for key in params: + assert key in error + assert error[key] == [expected_error_message] + else: + assert error["non_field_errors"] == [expected_error_message] From f4d9f9650945aacecef5bf5a29e9e23acf495c57 Mon Sep 17 00:00:00 2001 From: Gal Singer Date: Wed, 16 Aug 2017 16:15:02 +0300 Subject: [PATCH 14/38] english fixes for description in create endpoint function --- mazerunner/api_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index b68b9b7..06d8bb2 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -994,7 +994,7 @@ def create(self, ip_address=None, dns=None, hostname=None, deployment_group_id=N """ Create an Endpoint. - Pass at least ip_address or dns or hostname as a parameter + Pass at least one of the following parameters: ip_address, dns, or hostname :param deployment_group_id: id of the deployment group :param ip_address: address of the endpoint :param dns: fqdn of the endpoint From 2fb996e6ab986923b29f936b1c4f780d94030531 Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Mon, 21 Aug 2017 05:21:07 +0000 Subject: [PATCH 15/38] Add the new forensic puller features --- mazerunner/api_client.py | 104 +++++++++++++++++++++++++++++++++++++++ test/test_sdk.py | 2 + 2 files changed, 106 insertions(+) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index 06d8bb2..339f554 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -11,6 +11,7 @@ ALERTS_PER_PAGE = 500 + class BaseCollection(object): MODEL_CLASS = None @@ -772,6 +773,94 @@ def create(self, name, breadcrumb_type, **kwargs): return self.create_item(data) +class AlertProcessDLL(BaseEntity): + """ + A DLL file that was used by an attacker's process. + """ + def download_file(self, destination_path): + """ + Download the DLL file to the local disk + + :param destination_path: Location on the disk, where you want to save the file. + """ + self._api_client.request_and_download( + url='{url}{download_file_suffix}/'.format(url=self.url, + download_file_suffix='download_file'), + destination_path=destination_path) + + +class AlertProcessDLLCollection(Collection): + """ + DLL files associated with a specific binary attack tool, fetched by the Forensic Puller. + + This entity will be returned by calling :py:meth:`AlertProcess.get_dlls` + """ + + MODEL_CLASS = AlertProcessDLL + URL_SUFFIX = 'dll' + + def __init__(self, api_client, alert_process, obj_class=None): + super(AlertProcessDLLCollection, self).__init__(api_client, obj_class) + self.alert_process = alert_process + + def _get_url(self): + return '{process_url}{suffix}/'.format(process_url=self.alert_process.url, + suffix=self.URL_SUFFIX) + + +class AlertProcess(BaseEntity): + """ + A suspicious process that has been detected on an attacker's endpoint by the Forensic Puller. + + For more information about the Forensic Puller, navigate to User Menu > User Manual. + """ + + def get_dlls(self): + """ + Get a generator of the DLL files that were used by the process. + """ + return AlertProcessDLLCollection(self._api_client, alert_process=self) + + def download_file(self, destination_path): + """ + Download the attacker's tool to your local disk + + :param destination_path: Location on the disk, where you want to save the file. + """ + self._api_client.request_and_download( + url='{url}{download_file_suffix}/'.format(url=self.url, + download_file_suffix='download_file'), + destination_path=destination_path) + + def download_minidump(self, destination_path): + """ + Download minidump of the attacker's process + + :param destination_path: Location on the disk, where you want to save the file. + """ + self._api_client.request_and_download( + url='{url}{download_file_suffix}/'.format(url=self.url, + download_file_suffix='download_minidump'), + destination_path='{}.dump'.format(destination_path)) + + +class AlertProcessCollection(Collection): + """ + A subset of the processes associated with a specific alert. + + This entity will be returned by calling :py:meth:`api_client.Alert.get_processes` + """ + MODEL_CLASS = AlertProcess + URL_SUFFIX = 'process' + + def __init__(self, api_client, alert, obj_class=None): + super(AlertProcessCollection, self).__init__(api_client, obj_class) + self.alert = alert + + def _get_url(self): + return '{alert_url}{suffix}/'.format(alert_url=self.alert.url, suffix=self.URL_SUFFIX) + + class Alert(BaseEntity): """ An alert is automatically generated by the system every time an attacker interacts with the @@ -782,6 +871,7 @@ class Alert(BaseEntity): """ NAME = 'alert' + PROCESSES_URL_SUFFIX = 'processes' def delete(self): """ @@ -847,6 +937,14 @@ def download_stix_file(self, location_with_name): response.raw.decode_content = True shutil.copyfileobj(response.raw, f) + def get_processes(self): + """ + Get a generator of all the processes associated with the alert. + + Supported since MazeRunner 1.7.0. + """ + return AlertProcessCollection(api_client=self._api_client, alert=self) + class AlertCollection(Collection): """ @@ -1502,6 +1600,12 @@ def api_request(self, "Bad response: Not json.\nContent:\n{content}".format(content=resp.content) ) + def request_and_download(self, url, destination_path): + data = self.api_request(url, stream=True) + with open(destination_path, 'wb') as f: + data.raw.decode_content = True + shutil.copyfileobj(data.raw, f) + @property def decoys(self): """ diff --git a/test/test_sdk.py b/test/test_sdk.py index c1a3dfc..210d5ad 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -144,6 +144,8 @@ def _get_items(group): with suppress(ServerError): entity.delete() + wait_until(self._assert_clean_system, exc_list=[AssertionError], check_return_value=False) + def teardown_method(self, method): logger.debug("teardown_method called") From 18f3e4c3c8cf747f6efe2880bef0fa469df4550d Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Mon, 21 Aug 2017 11:57:14 +0000 Subject: [PATCH 16/38] New alert filters in the SDK --- mazerunner/api_client.py | 67 +++++++++++++++++++------ mazerunner/samples/track_live_alerts.py | 29 ++++++----- test/test_sdk.py | 28 ++++++----- 3 files changed, 84 insertions(+), 40 deletions(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index 339f554..cb0c5d0 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -955,25 +955,50 @@ class AlertCollection(Collection): MODEL_CLASS = Alert - def __init__(self, api_client, filter_enabled=False, only_alerts=False, alert_types=None): + def __init__(self, api_client, filter_enabled=False, only_alerts=False, alert_types=None, + start_date=None, end_date=None, id_greater_than=None, username=None, source=None, + keywords=None, decoy_name=None): """ :param api_client: The connection instance. :param filter_enabled: Whether this is a filtered collection. :param only_alerts: Filter out alerts in status 'Ignore' and 'Mute'. :param alert_types: E.g., code, HTTP, etc. See params() for the full list. + :param start_date: The beginning of the date range, formatted dd/mm/yyyy. + :param end_date: The end of the date range, formatted dd/mm/yyyy. + :param id_greater_than: Filter alerts to see only alerts that occur after this ID. + :param username: The breadcrumb's username, which the attacker used to log in. + :param source: The IP or hostname of the attacker's endpoint. + :param keywords: Search main fields for these keywords. + :param decoy_name: The name of the decoy that was attacked. """ super(AlertCollection, self).__init__(api_client) self.filter_enabled = filter_enabled self.only_alerts = only_alerts self.alert_types = alert_types + self.start_date = start_date + self.end_date = end_date + self.id_greater_than = id_greater_than + self.username = username + self.source = source + self.keywords = keywords + self.decoy_name = decoy_name def _get_query_params(self): return dict(filter_enabled=self.filter_enabled, only_alerts=self.only_alerts, alert_types=self.alert_types, - per_page=ALERTS_PER_PAGE) - - def filter(self, filter_enabled=False, only_alerts=False, alert_types=None): + per_page=ALERTS_PER_PAGE, + start_date=self.start_date, + end_date=self.end_date, + id_gt=self.id_greater_than, + username=self.username, + source=self.source, + keywords=self.keywords, + decoy_name=self.decoy_name) + + def filter(self, filter_enabled=False, only_alerts=False, alert_types=None, + start_date=None, end_date=None, id_greater_than=None, username=None, source=None, + keywords=None, decoy_name=None): """ Get alerts by query. @@ -981,11 +1006,27 @@ def filter(self, filter_enabled=False, only_alerts=False, alert_types=None): :param only_alerts: Only take alerts in 'Alert' status (exclude those in 'Mute' and \ 'Ignore' status). :param alert_types: A list of alert types. + :param start_date: The beginning of the date range, formatted dd/mm/yyyy. + :param end_date: The end of the date range, formatted dd/mm/yyyy. + :param id_greater_than: Filter alerts to see only alerts that occur after this ID. + :param username: The breadcrumb's username, which the attacker used to log in. + :param source: The IP or hostname of the attacker's endpoint. + :param keywords: Search main fields for these keywords. + :param decoy_name: The name of the decoy that was attacked. :return: A filtered :class:`api_client.AlertCollection`. """ formatted_alert_types = " ".join(alert_types) if alert_types else "" - return AlertCollection(self._api_client, filter_enabled=filter_enabled, - only_alerts=only_alerts, alert_types=formatted_alert_types) + return AlertCollection(self._api_client, + filter_enabled=filter_enabled, + only_alerts=only_alerts, + alert_types=formatted_alert_types, + start_date=start_date, + end_date=end_date, + id_greater_than=id_greater_than, + username=username, + source=source, + keywords=keywords, + decoy_name=decoy_name) def export(self, location_with_name): """ @@ -1161,7 +1202,9 @@ def clean_filtered(self, data={ 'clean_all_filtered': True, 'username': username, - 'password': password, + 'password': { + 'value': password + }, 'domain': domain, 'run_method': self._get_run_method(install_method), 'install_method': install_method @@ -1187,7 +1230,9 @@ def clean_by_endpoints_ids(self, data={ 'selected_endpoints_ids': endpoints_ids, 'username': username, - 'password': password, + 'password': { + 'value': password + }, 'domain': domain, 'run_method': self._get_run_method(install_method), 'install_method': install_method @@ -1230,12 +1275,6 @@ def filter_data(self): """ return self._api_client.api_request(url='{}{}'.format(self._get_url(), 'filter_data/')) - def status_dashboard(self): - """ - Get a list of all endpoints and their statuses. - """ - return self._api_client.api_request(url='{}{}'.format(self._get_url(), 'status_dashboard/')) - def params(self): raise NotImplementedError diff --git a/mazerunner/samples/track_live_alerts.py b/mazerunner/samples/track_live_alerts.py index fc77e64..398988a 100644 --- a/mazerunner/samples/track_live_alerts.py +++ b/mazerunner/samples/track_live_alerts.py @@ -46,22 +46,25 @@ def main(): client = mazerunner.connect(args.ip_address, args.api_key, args.api_secret, args.certificate) # Get alerts - alerts = client.alerts - alert_types = alerts.params()['alert_type'] - filtered_alerts = alerts.filter(filter_enabled=True, only_alerts=not args.show_muted, - alert_types=alert_types) - print "Showing all alerts live. Press Ctrl+C to exit." - last_length = len(list(filtered_alerts)) + alert_types = client.alerts.params()['alert_type'] + last_seen_id = 0 + + print("Showing all alerts live. Press Ctrl+C to exit.") + try: while True: time.sleep(WAIT_TIME) - current_length = len(list(filtered_alerts)) - if current_length > last_length: - alert_list = list(filtered_alerts) - for i in range(last_length, current_length): - print DISPLAY_FORMAT.format( - decoy_name=alert_list[i].decoy['name'], alert_type=alert_list[i].alert_type) - last_length = current_length + alert_filter = client.alerts.filter(filter_enabled=True, + only_alerts=not args.show_muted, + alert_types=alert_types, + id_greater_than=last_seen_id) + + for alert in alert_filter: + print(DISPLAY_FORMAT.format(decoy_name=alert.decoy['name'], + alert_type=alert.alert_type)) + + if alert.id > last_seen_id: + last_seen_id = alert.id except KeyboardInterrupt: sys.exit() diff --git a/test/test_sdk.py b/test/test_sdk.py index 210d5ad..4776edf 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -343,7 +343,11 @@ def test_ova(self): # Download decoy: download_file_path = "mazerunner/ova_image" - ova_decoy.download(location_with_name=download_file_path) + + # Wait until the decoy becomes available and download the file + wait_until(ova_decoy.download, location_with_name=download_file_path, + check_return_value=False, exc_list=[ValidationError]) + self.file_paths_for_cleanup.append("{}.ova".format(download_file_path)) def test_decoy_update(self): @@ -504,13 +508,12 @@ def _test_manual_deployment(): os.remove(TEST_DEPLOYMENTS_FILE_PATH) def _test_auto_deployment(): - with pytest.raises(ValidationError): - dep_group.auto_deploy(username=None, - password=None, - install_method='PS_EXEC', - run_method='EXE_DEPLOY', - domain='', - deploy_on="all") + dep_group.auto_deploy(username=None, + password=None, + install_method='PS_EXEC', + run_method='EXE_DEPLOY', + domain='', + deploy_on="all") # Since this runs asynchronously and it has nothing to deploy on, we only want to see # that the request was accepted @@ -875,7 +878,7 @@ def _test_import_endpoint(): cidr_mapping = self.cidr_mappings.create( cidr_block='%s/30' % self.lab_endpoint_ip, - deployment_group_id=1, + deployment_group=1, comments='no comments', active=True ) @@ -953,7 +956,6 @@ def _test_data(): ]) assert isinstance(self.endpoints.filter_data(), dict) - assert isinstance(self.endpoints.status_dashboard(), list) def _test_stop_import(): _destroy_elements() @@ -962,7 +964,7 @@ def _test_stop_import(): self.cidr_mappings.create( cidr_block='%s/24' % self.lab_endpoint_ip, - deployment_group_id=1, + deployment_group=1, comments='no comments', active=True ) @@ -1009,8 +1011,8 @@ def test_create_invalid_endpoint(self): for params, expected_error_message in [ (dict(ip_address='1.1.1.1.1'), "Enter a valid IPv4 address."), (dict(dns='A'*256), "Ensure this field has no more than 255 characters."), - (dict(hostname='A'*15), "Ensure this field has no more than 15 characters."), - (dict(), "require dns or hostname or ip_address"), + (dict(hostname='A'*16), "Ensure this field has no more than 15 characters."), + (dict(), "You must provide either dns, hostname, or ip address"), ]: try: self.endpoints.create(**params) From 15b9cd49782fd4239ed06524f696b7fd65bac234 Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Sun, 27 Aug 2017 17:39:17 +0300 Subject: [PATCH 17/38] Documentation fixes --- mazerunner/api_client.py | 25 +++++++++++++------------ source/api_client.rst | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index cb0c5d0..d198d15 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -781,7 +781,7 @@ def download_file(self, destination_path): """ Download the DLL file to the local disk - :param destination_path: Location on the disk, where you want to save the file. + :param destination_path: Location on the disk where you want to save the file. """ self._api_client.request_and_download( url='{url}{download_file_suffix}/'.format(url=self.url, @@ -825,7 +825,7 @@ def download_file(self, destination_path): """ Download the attacker's tool to your local disk - :param destination_path: Location on the disk, where you want to save the file. + :param destination_path: Location on the disk where you want to save the file. """ self._api_client.request_and_download( url='{url}{download_file_suffix}/'.format(url=self.url, @@ -836,7 +836,7 @@ def download_minidump(self, destination_path): """ Download minidump of the attacker's process - :param destination_path: Location on the disk, where you want to save the file. + :param destination_path: Location on the disk where you want to save the file. """ self._api_client.request_and_download( url='{url}{download_file_suffix}/'.format(url=self.url, @@ -941,7 +941,7 @@ def get_processes(self): """ Get a generator of all the processes associated with the alert. - Supported since MazeRunner 1.7.0. + Supported versions: MazeRunner 1.7.0 and above. """ return AlertProcessCollection(api_client=self._api_client, alert=self) @@ -1002,7 +1002,7 @@ def filter(self, filter_enabled=False, only_alerts=False, alert_types=None, """ Get alerts by query. - :param filter_enabled: When False, the only_alerts and alert_types params will be ignored. + :param filter_enabled: When False, all the filtering params will be ignored. :param only_alerts: Only take alerts in 'Alert' status (exclude those in 'Mute' and \ 'Ignore' status). :param alert_types: A list of alert types. @@ -1076,7 +1076,7 @@ def delete(self, selected_alert_ids=None, delete_all_filtered=False): class Endpoint(Entity): """ An endpoint represents a single workstation in the organization, and the status - of the breadcrumbs' deployment on it. + of the breadcrumbs' deployment to it. """ NAME = 'endpoint' @@ -1131,13 +1131,14 @@ def __init__(self, def create(self, ip_address=None, dns=None, hostname=None, deployment_group_id=None): """ - Create an Endpoint. + Create an endpoint. - Pass at least one of the following parameters: ip_address, dns, or hostname - :param deployment_group_id: id of the deployment group - :param ip_address: address of the endpoint - :param dns: fqdn of the endpoint - :param hostname: hostname of the endpoint + Pass at least one of the following parameters: ip_address, dns, or hostname. + + :param deployment_group_id: Id of the deployment group. + :param ip_address: Address of the endpoint. + :param dns: FQDN of the endpoint. + :param hostname: Hostname of the endpoint. """ data = dict(ip_address=ip_address, dns=dns, hostname=hostname, deployment_group=deployment_group_id) return self.create_item(data=data) diff --git a/source/api_client.rst b/source/api_client.rst index d7b5670..8534cc5 100644 --- a/source/api_client.rst +++ b/source/api_client.rst @@ -71,6 +71,24 @@ Alerts :inherited-members: :exclude-members: MODEL_CLASS +.. autoclass:: api_client.AlertProcess + :members: + :inherited-members: + +.. autoclass:: api_client.AlertProcessCollection + :members: + :inherited-members: + :exclude-members: MODEL_CLASS + +.. autoclass:: api_client.AlertProcessDLL + :members: + :inherited-members: + +.. autoclass:: api_client.AlertProcessDLLCollection + :members: + :inherited-members: + :exclude-members: MODEL_CLASS + Endpoints ========= From 5f5e9a84900d26e61b8b30e9a04fbb9ce88abd53 Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Thu, 31 Aug 2017 15:10:28 +0000 Subject: [PATCH 18/38] Make the tests more robust --- conftest.py | 1 + test/test_sdk.py | 91 +++++++++++++++++++++++------------------------- 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/conftest.py b/conftest.py index 3af2105..93d7e8b 100644 --- a/conftest.py +++ b/conftest.py @@ -1,4 +1,5 @@ def pytest_addoption(parser): + parser.addoption("--initial_clean", action="store_true", default=False) parser.addoption("--json_credentials", action="store", default=None, help="Json file with the relevant credentials") parser.addoption("--runslow", action="store_true", default=False, help="Run slow tests") diff --git a/test/test_sdk.py b/test/test_sdk.py index 4776edf..da26043 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -3,10 +3,10 @@ import json import logging import shutil +from collections import namedtuple from stat import S_IRUSR import pytest -from contextlib2 import suppress from retrying import retry from subprocess import Popen @@ -16,7 +16,7 @@ from mazerunner.api_client import DeploymentGroupCollection, \ BreadcrumbCollection, ServiceCollection, DecoyCollection, Service, \ AlertPolicy, CIDRMappingCollection, BackgroundTaskCollection, \ - EndpointCollection + EndpointCollection, Decoy, Breadcrumb, DeploymentGroup, Endpoint, CIDRMapping, BackgroundTask from mazerunner.exceptions import ValidationError, ServerError, BadParamError, \ InvalidInstallMethodError from utils import TimeoutException, wait_until @@ -31,7 +31,17 @@ API_ID_PARAM = 'id' API_SECRET_PARAM = 'secret' MAZERUNNER_CERTIFICATE_PATH_PARAM = 'mazerunner_certificate_path' -INITIAL_DEPLOYMENT_GROUPS = 1 + +ENTITIES_CONFIGURATION = { + Decoy: [], + Service: [], + Breadcrumb: [], + DeploymentGroup: [1], + Endpoint: [], + CIDRMapping: [], + BackgroundTask: [] +} + TEST_DEPLOYMENTS_FILE_PATH = os.path.join(os.path.dirname(__file__), 'test_deployments/dep.zip') TEST_DEPLOYMENTS_FOLDER_PATH = os.path.dirname(TEST_DEPLOYMENTS_FILE_PATH) @@ -60,10 +70,6 @@ class MachineStatus(object): # noinspection PyMethodMayBeStatic,PyAttributeOutsideInit class APITest(object): - DISPOSABLE_TYPES = [ - DecoyCollection, BreadcrumbCollection, DeploymentGroupCollection, - CIDRMappingCollection, BackgroundTaskCollection, EndpointCollection, ServiceCollection - ] runslow = pytest.mark.skipif(not pytest.config.getoption('--runslow'), reason='--runslow not activated') @@ -71,13 +77,13 @@ class APITest(object): reason='--lab_dependent not activated') def _assert_clean_system(self): - assert len(self.decoys) == 0 - assert len(self.services) == 0 - assert len(self.breadcrumbs) == 0 - assert len(self.deployment_groups) == INITIAL_DEPLOYMENT_GROUPS - assert len(self.cidr_mappings) == 0 - assert len(self.endpoints) == 0 - assert len(self.background_tasks) == 0 + for entity_collection in self.disposable_entities: + existing_ids = {entity.id for entity in entity_collection} + expected_ids = set(ENTITIES_CONFIGURATION[entity_collection.MODEL_CLASS]) + + assert existing_ids == expected_ids, 'System must be clean before running this test. ' \ + 'Use the --initial_clean flag to do this ' \ + 'automatically' def _configure_entities_groups(self): self.decoys = self.client.decoys @@ -90,6 +96,16 @@ def _configure_entities_groups(self): self.endpoints = self.client.endpoints self.background_tasks = self.client.background_tasks + self.disposable_entities = [ + self.decoys, + self.services, + self.breadcrumbs, + self.deployment_groups, + self.endpoints, + self.cidr_mappings, + self.background_tasks + ] + def setup_method(self, method): logger.debug("setup_method called") @@ -112,37 +128,23 @@ def setup_method(self, method): certificate=self.mazerunner_certificate_path) self._configure_entities_groups() - self._assert_clean_system() - self._register_existing_elements() + if pytest.config.option.initial_clean: + self._destroy_new_entities() + + self._assert_clean_system() self.file_paths_for_cleanup = [] _clear_deployment_path() - def _register_existing_elements(self): - - def _get_group_elements(group): - return [element.id - for element - in group(self.client)] - - self._existing_elements_by_type = { - group: _get_group_elements(group) - for group - in self.DISPOSABLE_TYPES - } - def _destroy_new_entities(self): - - def _get_items(group): - return list(group(self.client)) - - for entity_group in self.DISPOSABLE_TYPES: - for entity in _get_items(entity_group): - if entity.id not in self._existing_elements_by_type[entity_group]: - with suppress(ServerError): - entity.delete() + for entity_collection in self.disposable_entities: + for entity in list(entity_collection): + initial_ids = ENTITIES_CONFIGURATION[entity_collection.MODEL_CLASS] + if entity.id not in initial_ids: + wait_until(entity.delete, exc_list=[ServerError, ValidationError], + check_return_value=False) wait_until(self._assert_clean_system, exc_list=[AssertionError], check_return_value=False) @@ -226,11 +228,13 @@ def test_api_setup_campaign(self): logger.debug("test_api_setup_campaign called") # Create deployment group: - assert len(self.deployment_groups) == INITIAL_DEPLOYMENT_GROUPS + assert {dg.id for dg in self.deployment_groups} == \ + set(ENTITIES_CONFIGURATION[DeploymentGroup]) deployment_group = self.deployment_groups.create(name=SSH_GROUP_NAME, description="test deployment group") self.assert_entity_name_in_collection(SSH_GROUP_NAME, self.deployment_groups) - assert len(self.deployment_groups) == INITIAL_DEPLOYMENT_GROUPS + 1 + assert {dg.id for dg in self.deployment_groups} == \ + set(ENTITIES_CONFIGURATION[DeploymentGroup] + [deployment_group.id]) # Create breadcrumb: assert len(self.breadcrumbs) == 0 @@ -508,13 +512,6 @@ def _test_manual_deployment(): os.remove(TEST_DEPLOYMENTS_FILE_PATH) def _test_auto_deployment(): - dep_group.auto_deploy(username=None, - password=None, - install_method='PS_EXEC', - run_method='EXE_DEPLOY', - domain='', - deploy_on="all") - # Since this runs asynchronously and it has nothing to deploy on, we only want to see # that the request was accepted From 25fa5645e30de2f80fc99b91add2c64120ff575f Mon Sep 17 00:00:00 2001 From: Nadav Lev Date: Thu, 14 Sep 2017 13:17:39 +0000 Subject: [PATCH 19/38] MAZ-3234 --- mazerunner/api_client.py | 37 +++++++++++++++++++++++++++++++++++++ test/test_sdk.py | 14 ++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index d198d15..3d7b076 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -1073,6 +1073,30 @@ def delete(self, selected_alert_ids=None, delete_all_filtered=False): query_params=query_params) +class ForensicPullerOnDemand(BaseCollection): + """ + Forensic Puller on demand. + + This entity will be returned by :py:attr:`api_client.APIClient.forensic_puller_on_demand`. + """ + URL_EXTENSION = 'forensic-puller-on-demand-run' + + def run_on_ip_list(self, ip_list): + """ + Runs Forensic Puller on a list of IPs. + + :param ip_list: List of IPs. + """ + data = dict(ip_list=ip_list) + self._api_client.api_request( + url=self._get_url(), + method='post', + data=data) + + def _get_url(self): + return self._api_client.api_urls[self.URL_EXTENSION] + + class Endpoint(Entity): """ An endpoint represents a single workstation in the organization, and the status @@ -1725,6 +1749,19 @@ def alerts(self): """ return AlertCollection(self) + @property + def forensic_puller_on_demand(self): + """ + Get an :class:`api_client.ForensicPullerOnDemand` instance, on which you can + perform read and delete operations. + + Example:: + + client = mazerunner.connect(...) + code_alerts = client.forensic_puller_on_demand.run_on_ip_list(ip_list=['192.168.1.1']) + """ + return ForensicPullerOnDemand(self) + @property def endpoints(self): """ diff --git a/test/test_sdk.py b/test/test_sdk.py index da26043..1356600 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -26,6 +26,7 @@ ENDPOINT_PASSWORD_PARAM = 'endpoint_password' CODE_EXECUTION_ALERT_TYPE = 'code' +FORENSIC_DATA_ALERT_TYPE = 'forensic_puller' MAZERUNNER_IP_ADDRESS_PARAM = 'ip_address' API_ID_PARAM = 'id' @@ -534,6 +535,19 @@ def _test_auto_deployment(): _test_manual_deployment() _test_auto_deployment() + def forensic_puller_alert_is_shown(self): + alerts = list(self.alerts.filter(filter_enabled=True, + only_alerts=False, + alert_types=[FORENSIC_DATA_ALERT_TYPE])) + return bool(alerts) + + @pytest.mark.skip("needs auto deploy setting credentials") + @APITest.lab_dependent + def test_forensic_puller_on_demand(self): + ## TODO: Add setting global deployment credentials here. + self.client.forensic_puller_on_demand.run_on_ip_list(ip_list=[self.lab_endpoint_ip]) + wait_until(self.forensic_puller_alert_is_shown) + @APITest.lab_dependent def test_deployment_credentials(self): assert self.client.deployment_groups.test_deployment_credentials( From 3492e80550149257e771228c5adfb9935f04d076 Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Thu, 14 Sep 2017 13:57:08 +0000 Subject: [PATCH 20/38] Allow unassigning deployment group from endpoints --- mazerunner/api_client.py | 23 +++++++++++++++++++++++ test/test_sdk.py | 22 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index 3d7b076..8d28772 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -535,6 +535,7 @@ class DeploymentGroupCollection(EditableCollection): """ MODEL_CLASS = DeploymentGroup + ALL_BREADCRUMBS_DEPLOYMENT_GROUP_ID = 1 def create(self, name, description=None): """ @@ -1118,6 +1119,12 @@ def delete(self): data = {"selected_endpoints_ids": [self.id]} self._api_client.api_request(url, 'post', data=data) + def reassign_to_group(self, deployment_group): + self._api_client.endpoints.reassign_to_group(deployment_group, [self]) + + def clear_deployment_group(self): + self._api_client.endpoints.clear_deployment_group([self]) + class EndpointCollection(EditableCollection): """ @@ -1127,6 +1134,7 @@ class EndpointCollection(EditableCollection): """ MODEL_CLASS = Endpoint + UNASSIGN_FROM_DEPLOYMENT_GROUP = 'unassigned' RUN_METHOD_FOR_INSTALL_METHOD = { 'ZIP': 'CMD_DEPLOY', @@ -1202,6 +1210,21 @@ def reassign_to_group(self, deployment_group, endpoints): method='POST', data=data) + def clear_deployment_group(self, endpoints): + """ + Unassign specified endpoints from all deployment groups. + + :param endpoints: A list of endpoints that should be unassigned. + """ + data = dict( + to_group=self.UNASSIGN_FROM_DEPLOYMENT_GROUP, + selected_endpoints_ids=[ep.id for ep in endpoints]) + + self._api_client.api_request( + "{}{}".format(self._get_url(), 'reassign_selected/'), + method='POST', + data=data) + def _get_run_method(self, install_method): if install_method not in self.RUN_METHOD_FOR_INSTALL_METHOD: raise InvalidInstallMethodError('Invalid install method: %s' % install_method) diff --git a/test/test_sdk.py b/test/test_sdk.py index 1356600..7149bfd 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -940,7 +940,29 @@ def _test_clean(ep): def _test_reassignment(ep): dep_group = self.deployment_groups.create(name='ep1_test', description='test') + + # Assign via collection self.endpoints.reassign_to_group(dep_group, [ep]) + assert self.endpoints.get_item(ep.id).deployment_group.id == dep_group.id + + # Clear via collection + self.endpoints.clear_deployment_group([ep]) + assert self.endpoints.get_item(ep.id).deployment_group is None + + # Assign via entity + all_breadcrumbs_deployment_group = self.deployment_groups.get_item( + self.deployment_groups.ALL_BREADCRUMBS_DEPLOYMENT_GROUP_ID) + endpoint.reassign_to_group(all_breadcrumbs_deployment_group) + assert self.endpoints.get_item(ep.id).deployment_group.id == \ + all_breadcrumbs_deployment_group.id + + # Clear via entity + ep.clear_deployment_group() + assert self.endpoints.get_item(ep.id).deployment_group is None + + # Eventually leave the endpoint with the new deployment group assigned + ep.reassign_to_group(dep_group) + assert self.endpoints.get_item(ep.id).deployment_group.id == dep_group.id def _test_delete(): self.endpoints.filter('no.such.endpoints').delete_filtered() From 8b3af5ef1641e66fbe68ec4e814b9640ff06f95c Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Sun, 8 Oct 2017 08:20:18 +0000 Subject: [PATCH 21/38] Fix how background tasks are cleared in tests --- test/test_sdk.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/test_sdk.py b/test/test_sdk.py index 7149bfd..1e9f04f 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -21,6 +21,8 @@ InvalidInstallMethodError from utils import TimeoutException, wait_until +CLEAR_SYSTEM_ERROR_MESSAGE = 'System must be clean before running this test. Use the '\ + '--initial_clean flag to do this automatically' ENDPOINT_IP_PARAM = 'endpoint_ip' ENDPOINT_USERNAME_PARAM = 'endpoint_username' ENDPOINT_PASSWORD_PARAM = 'endpoint_password' @@ -82,9 +84,9 @@ def _assert_clean_system(self): existing_ids = {entity.id for entity in entity_collection} expected_ids = set(ENTITIES_CONFIGURATION[entity_collection.MODEL_CLASS]) - assert existing_ids == expected_ids, 'System must be clean before running this test. ' \ - 'Use the --initial_clean flag to do this ' \ - 'automatically' + assert existing_ids == expected_ids, CLEAR_SYSTEM_ERROR_MESSAGE + + assert len(self.background_tasks) == 0, CLEAR_SYSTEM_ERROR_MESSAGE def _configure_entities_groups(self): self.decoys = self.client.decoys @@ -103,8 +105,7 @@ def _configure_entities_groups(self): self.breadcrumbs, self.deployment_groups, self.endpoints, - self.cidr_mappings, - self.background_tasks + self.cidr_mappings ] def setup_method(self, method): @@ -147,6 +148,8 @@ def _destroy_new_entities(self): wait_until(entity.delete, exc_list=[ServerError, ValidationError], check_return_value=False) + self.background_tasks.acknowledge_all_complete() + wait_until(self._assert_clean_system, exc_list=[AssertionError], check_return_value=False) def teardown_method(self, method): From f5c0921bab994765f3e34aafce74daaf09ef8c84 Mon Sep 17 00:00:00 2001 From: Nadav Nuni Date: Sun, 8 Oct 2017 13:53:25 +0000 Subject: [PATCH 22/38] Merged in feature/MAZ-3696 (pull request #33) --- mazerunner/api_client.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index 8d28772..dc444b2 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -1094,6 +1094,23 @@ def run_on_ip_list(self, ip_list): method='post', data=data) + +class StorageUsageData(BaseCollection): + """ + Storage usage data. + This entity will be returned by :py:attr:`api_client.APIClient.storage_usage_data`. + """ + URL_EXTENSION = 'storage-usage' + + def __unicode__(self): + return unicode(self.details()) + + def __str__(self): + return str(self.details()) + + def details(self): + return self._api_client.api_request(url=self._get_url(), method='get') + def _get_url(self): return self._api_client.api_urls[self.URL_EXTENSION] @@ -1785,6 +1802,13 @@ def forensic_puller_on_demand(self): """ return ForensicPullerOnDemand(self) + @property + def storage_usage(self): + """ + Get an :class:`api_client.StorageUsageData` + """ + return StorageUsageData(self) + @property def endpoints(self): """ From a9457e27f04a9bd3edd647d841f814d493fc0e5e Mon Sep 17 00:00:00 2001 From: Lev Pachmanov Date: Tue, 17 Oct 2017 15:06:09 +0000 Subject: [PATCH 23/38] Merged in feature/MAZ-1589 (pull request #36) Approved-by: Nadav Lev --- test/test_sdk.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/test_sdk.py b/test/test_sdk.py index 1e9f04f..c14f65f 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -252,7 +252,7 @@ def test_api_setup_campaign(self): # Create service: assert len(self.services) == 0 - service_ssh = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh") + service_ssh = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh", any_user="false") self.assert_entity_name_in_collection(SSH_SERVICE_NAME, self.services) assert len(self.services) == 1 @@ -318,7 +318,7 @@ def test_api_setup_campaign(self): self.assert_entity_name_in_collection(SSH_GROUP_NAME, self.deployment_groups) self.assert_entity_name_not_in_collection(SSH_GROUP_NAME_UPDATE, self.deployment_groups) - service_ssh.update(name=SSH_SERVICE_NAME_UPDATE) + service_ssh.update(name=SSH_SERVICE_NAME_UPDATE, any_user="false") breadcrumb_ssh.detach_from_service(service_ssh.id) self.assert_entity_name_not_in_collection(SSH_SERVICE_NAME, @@ -437,7 +437,7 @@ def test_check_conflicts(self): hostname="decoyssh", os="Ubuntu_1404", vm_type="KVM")) - service_ssh = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh") + service_ssh = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh", any_user="false") service_ssh.connect_to_decoy(decoy_ssh.id) dep_group = self.deployment_groups.create(name='test_check_conflicts') @@ -482,7 +482,7 @@ def test_deployment(self): os="Ubuntu_1404", vm_type="KVM")) - service_ssh = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh") + service_ssh = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh", any_user="false") bc_ssh = self.breadcrumbs.create(name='ssh1', breadcrumb_type="ssh", @@ -672,7 +672,7 @@ def _create_code_exec_alert(): hostname="decoyssh", os="Ubuntu_1404", vm_type="KVM")) - service_ssh = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh") + service_ssh = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh", any_user="false") service_ssh.connect_to_decoy(decoy_ssh.id) bc_ssh1 = self.breadcrumbs.create(name='ssh1', @@ -750,16 +750,16 @@ def test_params(self): class TestEntity(APITest): def test_repr(self): - service = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh") + service = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh", any_user="false") str_service = "".format( - serv=self.mazerunner_ip_address, service_id=service.id) + "is_active=False attached_decoys=[] any_user=u'{any_user}' service_type=u'ssh' id={service_id}>"\ + .format(serv=self.mazerunner_ip_address, service_id=service.id, any_user=service.any_user) assert str(service) == str_service def test_get_attribute(self): - service = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh") + service = self.services.create(name=SSH_SERVICE_NAME, service_type="ssh", any_user="false") assert service.name == SSH_SERVICE_NAME with pytest.raises(AttributeError): From 5ea6be6c55ded125c5e95ad640a27a72a931de9a Mon Sep 17 00:00:00 2001 From: gal_singer Date: Tue, 24 Oct 2017 11:55:36 +0000 Subject: [PATCH 24/38] Merged in pull request #37 wait for background task finish before start new deploy MAZ-3947 --- test/test_sdk.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/test/test_sdk.py b/test/test_sdk.py index c14f65f..b65e758 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -3,7 +3,6 @@ import json import logging import shutil -from collections import namedtuple from stat import S_IRUSR import pytest @@ -13,10 +12,8 @@ import mazerunner import os -from mazerunner.api_client import DeploymentGroupCollection, \ - BreadcrumbCollection, ServiceCollection, DecoyCollection, Service, \ - AlertPolicy, CIDRMappingCollection, BackgroundTaskCollection, \ - EndpointCollection, Decoy, Breadcrumb, DeploymentGroup, Endpoint, CIDRMapping, BackgroundTask +from mazerunner.api_client import Service, AlertPolicy, Decoy, Breadcrumb, \ + DeploymentGroup, Endpoint, CIDRMapping, BackgroundTask from mazerunner.exceptions import ValidationError, ServerError, BadParamError, \ InvalidInstallMethodError from utils import TimeoutException, wait_until @@ -497,6 +494,13 @@ def test_deployment(self): self.power_on_decoy(decoy_ssh) + def _has_complete_bg_tasks(): + return len([bg_task for bg_task in self.background_tasks.filter(running=False)]) > 0 + + def _wait_and_destroy_background_task(): + wait_until(_has_complete_bg_tasks, check_return_value=True) + self.background_tasks.acknowledge_all_complete() + def _test_manual_deployment(): dep_group.deploy(location_with_name=TEST_DEPLOYMENTS_FILE_PATH.replace('.zip', ''), os='Windows', @@ -526,6 +530,8 @@ def _test_auto_deployment(): domain='', deploy_on="all") + _wait_and_destroy_background_task() + self.deployment_groups.auto_deploy_groups( username='some-user', password='some-pass', @@ -535,6 +541,8 @@ def _test_auto_deployment(): domain='', deploy_on="all") + _wait_and_destroy_background_task() + _test_manual_deployment() _test_auto_deployment() From e78dbbdea571248125ac3fa3960ac721c345bbf5 Mon Sep 17 00:00:00 2001 From: gal_singer Date: Mon, 13 Nov 2017 09:22:55 +0000 Subject: [PATCH 25/38] Merged in pull request #38 set a bigger timeout for download ova decoys --- test/test_sdk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_sdk.py b/test/test_sdk.py index b65e758..b947353 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -351,7 +351,7 @@ def test_ova(self): # Wait until the decoy becomes available and download the file wait_until(ova_decoy.download, location_with_name=download_file_path, - check_return_value=False, exc_list=[ValidationError]) + check_return_value=False, exc_list=[ValidationError], total_timeout=60*10) self.file_paths_for_cleanup.append("{}.ova".format(download_file_path)) From b06a26ae10de4a4137337d9732f6e9f3aed18773 Mon Sep 17 00:00:00 2001 From: gal_singer Date: Tue, 5 Dec 2017 09:43:54 +0000 Subject: [PATCH 26/38] Merged in pull request #39 MAZ-4108 add test to create service type that does not exists --- test/test_sdk.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/test_sdk.py b/test/test_sdk.py index b947353..6cd713f 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -328,6 +328,11 @@ def test_api_setup_campaign(self): decoy_ssh.load() assert decoy_ssh.machine_status == MachineStatus.INACTIVE + invalid_service = "invalid_service" + with pytest.raises(ValidationError): + self.services.create(name=invalid_service, service_type=invalid_service) + self.assert_entity_name_not_in_collection(invalid_service, self.services) + class TestDecoy(APITest): DECOY_STATUS_ACTIVE = 'active' From acaf49b914876b366f236c5ac08d4b91bae4f4b6 Mon Sep 17 00:00:00 2001 From: Gal Singer Date: Fri, 29 Dec 2017 15:05:38 +0200 Subject: [PATCH 27/38] https_active=False in test sdk --- test/test_sdk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_sdk.py b/test/test_sdk.py index 6cd713f..a8ce9d8 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -820,7 +820,8 @@ def test_service_with_files(self): self.services.create(name=SSH_BREADCRUMB_NAME, service_type="http", zip_file_path=site_data_file, - web_apps=['phpmyadmin']) + web_apps=['phpmyadmin'], + https_active=False) assert len(self.client.services) == 1 From 22fde133ea7b5efcbc5ece5c36145c4ad3fdaa79 Mon Sep 17 00:00:00 2001 From: gal_singer Date: Wed, 7 Mar 2018 11:20:19 +0000 Subject: [PATCH 28/38] Merged in pull request #40 MAZ-4233 Multiple promisc changes --- mazerunner/api_client.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index dc444b2..87f1a45 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -227,13 +227,14 @@ def update(self, name, chosen_static_ip=None, chosen_subnet=None, chosen_gateway ec2_region=self.ec2_region, ec2_subnet_id=self.ec2_subnet_id, vlan=self.vlan, - chosen_interface=self.chosen_interface, + interface=self.interface, name=name, chosen_static_ip=chosen_static_ip, chosen_subnet=chosen_subnet, chosen_gateway=chosen_gateway, chosen_dns=chosen_dns, - dns_address=dns_address + dns_address=dns_address, + network_type=self.network_type ) non_empty_data = {key: value for key, value in data.iteritems() if value} self._update_item(non_empty_data) @@ -293,11 +294,13 @@ class DecoyCollection(EditableCollection): MODEL_CLASS = Decoy def create(self, os, vm_type, name, hostname, chosen_static_ip=None, chosen_subnet=None, - chosen_gateway=None, chosen_dns=None, chosen_interface=None, vlan=None, - ec2_region=None, ec2_subnet_id=None, account=None, dns_address=''): + chosen_gateway=None, chosen_dns=None, interface=1, vlan=None, + ec2_region=None, ec2_subnet_id=None, account=None, dns_address='', network_type="PROMISC"): """ Create a decoy. + :param network_type: Network type of the decoy. Options : \ + PROMISC, NON_PROMISC, VLAN_TRUNK :param os: OS installed on the server. Options: \ Ubuntu_1404, Windows_7, Windows_Server_2012, Windows_Server_2008. :param vm_type: Server type. KVM for nested (recommended) or OVA for standalone. @@ -308,8 +311,7 @@ def create(self, os, vm_type, name, hostname, chosen_static_ip=None, chosen_subn :param chosen_subnet: Decoy subnet mask. :param chosen_gateway: Decoy default gateway address. :param chosen_dns: The DNS server address (This is NOT the name of the decoy). - :param chosen_interface: The physical interface to which the decoy should be connected. \ - Applicable for non-promiscuous mode only. + :param interface: The physical interface to which the decoy should be connected. \ :param vlan: VLAN to which the decoy will be connected (if applicable). :param ec2_region: EC2 region (e.g., eu-west-1), if applicable. :param ec2_subnet_id: EC2 subnet ID, if applicable. @@ -326,12 +328,13 @@ def create(self, os, vm_type, name, hostname, chosen_static_ip=None, chosen_subn chosen_subnet=chosen_subnet, chosen_gateway=chosen_gateway, chosen_dns=chosen_dns, - chosen_interface=chosen_interface, + interface=interface, vlan=vlan, ec2_region=ec2_region, ec2_subnet_id=ec2_subnet_id, account=account, - dns_address=dns_address + dns_address=dns_address, + network_type=network_type ) non_empty_data = {key: value for key, value in data.iteritems() if value} return self.create_item(non_empty_data) From 2b2d4da3b3d5ea4cb1c6d97c62e463753a8d350a Mon Sep 17 00:00:00 2001 From: Gal Singer Date: Sun, 1 Apr 2018 10:52:10 +0300 Subject: [PATCH 29/38] Fix test_repr --- test/test_sdk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_sdk.py b/test/test_sdk.py index a8ce9d8..2a84f72 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -767,7 +767,8 @@ def test_repr(self): str_service = ""\ + "is_active=False attached_decoys=[] any_user=u'{any_user}' is_delete_enabled=True " \ + "service_type=u'ssh' id={service_id}>"\ .format(serv=self.mazerunner_ip_address, service_id=service.id, any_user=service.any_user) assert str(service) == str_service From 5aa66dc4590e9c403a9d449e578adbff471c8dc1 Mon Sep 17 00:00:00 2001 From: Gal Singer Date: Mon, 30 Apr 2018 18:04:54 +0300 Subject: [PATCH 30/38] Fix boolean formatting in SDK tests --- test/test_sdk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_sdk.py b/test/test_sdk.py index 2a84f72..1caa9c7 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -767,7 +767,7 @@ def test_repr(self): str_service = ""\ .format(serv=self.mazerunner_ip_address, service_id=service.id, any_user=service.any_user) assert str(service) == str_service From 48197b18573da82c1a36d96bad34d893a0adcdb8 Mon Sep 17 00:00:00 2001 From: Gal Singer Date: Wed, 9 May 2018 21:47:08 +0300 Subject: [PATCH 31/38] MAZ-5051 update error message in test to the new error --- test/test_sdk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_sdk.py b/test/test_sdk.py index 1caa9c7..cf71528 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -1061,7 +1061,7 @@ def test_create_endpoint_with_deployment_group(self): def test_create_invalid_endpoint(self): for params, expected_error_message in [ (dict(ip_address='1.1.1.1.1'), "Enter a valid IPv4 address."), - (dict(dns='A'*256), "Ensure this field has no more than 255 characters."), + (dict(dns='A'*256), "Maximum field length is 255 characters"), (dict(hostname='A'*16), "Ensure this field has no more than 15 characters."), (dict(), "You must provide either dns, hostname, or ip address"), ]: From 33b51057e824f334263bd5f5603a2847219bdfd2 Mon Sep 17 00:00:00 2001 From: Gal Singer Date: Thu, 10 May 2018 11:50:58 +0300 Subject: [PATCH 32/38] change other error message as well --- test/test_sdk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_sdk.py b/test/test_sdk.py index cf71528..395b862 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -1062,7 +1062,7 @@ def test_create_invalid_endpoint(self): for params, expected_error_message in [ (dict(ip_address='1.1.1.1.1'), "Enter a valid IPv4 address."), (dict(dns='A'*256), "Maximum field length is 255 characters"), - (dict(hostname='A'*16), "Ensure this field has no more than 15 characters."), + (dict(hostname='A'*16), "Maximum field length is 15 characters"), (dict(), "You must provide either dns, hostname, or ip address"), ]: try: From fe586b61946197671d8815ec8b439b4391c2057a Mon Sep 17 00:00:00 2001 From: Dekel Date: Thu, 31 May 2018 10:17:54 +0000 Subject: [PATCH 33/38] Merged in bugfix/docufix (pull request #41) fix documentation Approved-by: Yochai Blumenfeld --- mazerunner/api_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index 87f1a45..54868d9 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -1725,8 +1725,8 @@ def decoys(self): backup_server_story_decoy = client.decoys.create( name='backup_server_decoy', os='Windows_Server_2012', - hostname: 'backup_server', - vm_type: "KVM") + hostname='backupserver', + vm_type='KVM') old_decoy = client.decoys.get_item(id=5) old_decoy.delete() @@ -1744,7 +1744,7 @@ def services(self): client = mazerunner.connect(...) app_db_service = client.services.create( name='app_db_service', - type='mysql') + service_type='mysql') """ return ServiceCollection(self) From e7e2f0ffad65fabc9b4993053879fcba3f36bb06 Mon Sep 17 00:00:00 2001 From: Frankie Simon Date: Mon, 4 Jun 2018 09:00:16 +0000 Subject: [PATCH 34/38] Merged in MAZ-3456-download-honeydoc-file (pull request #42) Support creating breadcrumbs w/ files + test honeydoc download Approved-by: Nadav Lev Approved-by: Yochai Blumenfeld --- mazerunner/api_client.py | 445 ++++++++++++++++++--------------------- test/sample.docx | Bin 0 -> 13435 bytes test/test_sdk.py | 40 +++- 3 files changed, 244 insertions(+), 241 deletions(-) create mode 100644 test/sample.docx diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index 54868d9..d3429eb 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -28,7 +28,7 @@ class Collection(BaseCollection): def __len__(self): query_params = self._get_query_params() response = self._api_client.api_request(self._get_url(), query_params=query_params) - return response['count'] + return response["count"] def __iter__(self): for chunk in self._iter_chunks(): @@ -38,14 +38,14 @@ def __iter__(self): def _iter_chunks(self): query_params = self._get_query_params() response = self._api_client.api_request(self._get_url(), query_params=query_params) - results = response['results'] + results = response["results"] while True: yield [self._obj_class(self._api_client, obj) for obj in results] # Get the next batch of objects if possible - if response.get('next'): - response = self._api_client.api_request(response['next'], query_params=query_params) - results = response['results'] + if response.get("next"): + response = self._api_client.api_request(response["next"], query_params=query_params) + results = response["results"] else: return @@ -85,7 +85,7 @@ def create_item(self, data, files=None): :param data: Element data. :param files: Relevant file paths to upload for the element. """ - response = self._api_client.api_request(self._get_url(), 'post', data=data, files=files) + response = self._api_client.api_request(self._get_url(), "post", data=data, files=files) return self._obj_class(self._api_client, response).load() @@ -135,10 +135,10 @@ def __init__(self, api_client, param_dict): self._update_related_fields() def __repr__(self): - properties = ' '.join('%s=%s' % (key, repr(value)) + properties = " ".join("%s=%s" % (key, repr(value)) for key, value in self._param_dict.items()) - return '<%s: %s>' % (self.__class__.__name__, properties) + return "<%s: %s>" % (self.__class__.__name__, properties) def _update_related_fields(self): for key, field_type in self.RELATED_COLLECTIONS.iteritems(): @@ -176,14 +176,14 @@ def load(self): class Entity(BaseEntity): def _update_item(self, data, files=None): - response = self._api_client.api_request(self.url, 'put', data=data, files=files) + response = self._api_client.api_request(self.url, "put", data=data, files=files) self._update_entity_data(response) def _partial_update_item(self, data, files=None): non_empty_data = {key: value for key, value in data.iteritems() if value} if non_empty_data: response = self._api_client.api_request(url=self.url, - method='patch', + method="patch", data=non_empty_data, files=files) self._update_entity_data(response) @@ -192,7 +192,7 @@ def delete(self): """ Delete this element. """ - self._api_client.api_request(self.url, 'delete') + self._api_client.api_request(self.url, "delete") class Decoy(Entity): @@ -203,10 +203,10 @@ class Decoy(Entity): or an external machine downloaded as an OVA and manually deployed on an ESX machine. """ - NAME = 'decoy' + NAME = "decoy" def update(self, name, chosen_static_ip=None, chosen_subnet=None, chosen_gateway=None, - chosen_dns=None, dns_address=''): + chosen_dns=None, dns_address=""): """ Change decoy configuration. @@ -243,26 +243,26 @@ def power_on(self): """ Start the decoy machine. """ - self._api_client.api_request("{}{}".format(self.url, 'power_on/'), 'post') + self._api_client.api_request("{}{}".format(self.url, "power_on/"), "post") def recreate(self): """ Recreate the decoy machine. """ - self._api_client.api_request("{}{}".format(self.url, 'recreate/'), 'post') + self._api_client.api_request("{}{}".format(self.url, "recreate/"), "post") def power_off(self): """ Shut down the decoy machine. """ - self._api_client.api_request("{}{}".format(self.url, 'power_off/'), 'post') + self._api_client.api_request("{}{}".format(self.url, "power_off/"), "post") def test_dns(self): """ Check whether the decoy is properly registered in the DNS server. """ try: - return self._api_client.api_request("{}{}".format(self.url, 'test_dns/'), 'post') + return self._api_client.api_request("{}{}".format(self.url, "test_dns/"), "post") except ValidationError as e: data = json.loads(e.message) errors = data.get("non_field_errors", []) @@ -276,12 +276,8 @@ def download(self, location_with_name): :param location_with_name: Destination path. """ - response = self._api_client.api_request("{}{}".format(self.url, 'download/'), stream=True) - - file_path = "{}.{}".format(location_with_name, "ova") - with open(file_path, 'wb') as f: - response.raw.decode_content = True - shutil.copyfileobj(response.raw, f) + self._api_client.request_and_download(url="{}{}".format(self.url, "download/"), + destination_path="{}.{}".format(location_with_name, "ova")) class DecoyCollection(EditableCollection): @@ -295,7 +291,7 @@ class DecoyCollection(EditableCollection): def create(self, os, vm_type, name, hostname, chosen_static_ip=None, chosen_subnet=None, chosen_gateway=None, chosen_dns=None, interface=1, vlan=None, - ec2_region=None, ec2_subnet_id=None, account=None, dns_address='', network_type="PROMISC"): + ec2_region=None, ec2_subnet_id=None, account=None, dns_address="", network_type="PROMISC"): """ Create a decoy. @@ -353,11 +349,11 @@ class Service(Entity): * Remote desktop """ - NAME = 'service' + NAME = "service" RELATED_COLLECTIONS = { - 'attached_decoys': Decoy, - 'available_decoys': Decoy + "attached_decoys": Decoy, + "available_decoys": Decoy } def update(self, name, zip_file_path=None, **kwargs): @@ -368,7 +364,7 @@ def update(self, name, zip_file_path=None, **kwargs): :param zip_file_path: A file to upload, if applicable. :param kwargs: Additional relevant parameters. """ - files = {"zip_file": open(zip_file_path, 'rb')} if zip_file_path else None + files = {"zip_file": open(zip_file_path, "rb")} if zip_file_path else None data = dict( name=name, service_type=self.service_type @@ -383,8 +379,8 @@ def connect_to_decoy(self, decoy_id): :param decoy_id: The ID of the decoy to which the service should be attached. """ data = dict(decoy_id=decoy_id) - self._api_client.api_request(url="{}{}".format(self.url, 'connect_to_decoy/'), - method='post', + self._api_client.api_request(url="{}{}".format(self.url, "connect_to_decoy/"), + method="post", data=data) self.load() @@ -395,8 +391,8 @@ def detach_from_decoy(self, decoy_id): :param decoy_id: Decoy ID from which the service should be detached. """ data = dict(decoy_id=decoy_id) - self._api_client.api_request(url="{}{}".format(self.url, 'detach_from_decoy/'), - method='post', + self._api_client.api_request(url="{}{}".format(self.url, "detach_from_decoy/"), + method="post", data=data) self.load() @@ -420,7 +416,7 @@ def create(self, name, service_type, zip_file_path=None, **kwargs): :param zip_file_path: The path of a ZIP file to upload, if applicable. :param kwargs: Additional relevant parameters. """ - files = {"zip_file": open(zip_file_path, 'rb')} if zip_file_path else None + files = {"zip_file": open(zip_file_path, "rb")} if zip_file_path else None data = dict( name=name, service_type=service_type @@ -443,7 +439,7 @@ class DeploymentGroup(Entity): the deployment group's associated breadcrumbs on the deployment group's associated endpoints. """ - NAME = 'deployment-group' + NAME = "deployment-group" def update(self, name, description): """ @@ -481,7 +477,7 @@ def check_conflicts(self, os): :param os: OS type (Windows/Linux). """ query_dict = dict(os=os) - return self._api_client.api_request(url="{}{}".format(self.url, 'check_conflicts/'), + return self._api_client.api_request(url="{}{}".format(self.url, "check_conflicts/"), query_params=query_dict) def deploy(self, location_with_name, os, download_type, download_format="ZIP"): @@ -494,16 +490,12 @@ def deploy(self, location_with_name, os, download_type, download_format="ZIP"): :param download_format: Installer format (ZIP/MSI/EXE). """ query_dict = dict(os=os, download_type=download_type, download_format=download_format) - response = self._api_client.api_request( - "{}{}".format(self.url, 'deploy/'), - query_params=query_dict, stream=True) - file_path = "{}.{}".format(location_with_name, download_format.lower()) - with open(file_path, 'wb') as f: - response.raw.decode_content = True - shutil.copyfileobj(response.raw, f) + self._api_client.request_and_download(url="{}{}".format(self.url, "deploy/"), + destination_path=file_path, + query_params=query_dict) - def auto_deploy(self, install_method, run_method, username, password, domain='', + def auto_deploy(self, install_method, run_method, username, password, domain="", deploy_on="all"): """ Deploy all the breadcrumbs that are members of this deployment group on all the endpoints @@ -527,7 +519,7 @@ def auto_deploy(self, install_method, run_method, username, password, domain='', domain=domain, deploy_on=deploy_on ) - self._api_client.api_request("{}{}".format(self.url, 'auto_deploy/'), 'post', data=data) + self._api_client.api_request("{}{}".format(self.url, "auto_deploy/"), "post", data=data) class DeploymentGroupCollection(EditableCollection): @@ -577,7 +569,7 @@ def test_deployment_credentials(self, addr, install_method, username, password, ) return self._api_client.api_request(url="{}{}".format(self._get_url(), "test_deployment_credentials/"), - method='post', data=data) + method="post", data=data) def auto_deploy_groups(self, deployment_groups_ids, install_method, run_method, username, password, domain=None, deploy_on="all"): @@ -605,9 +597,9 @@ def auto_deploy_groups(self, deployment_groups_ids, install_method, run_method, domain=domain, deploy_on=deploy_on ) - url = "{}{}/".format(self._get_url(), 'deploy') + url = "{}{}/".format(self._get_url(), "deploy") return self._api_client.api_request(url=url, - method='post', + method="post", data=data) def deploy_all(self, location_with_name, os, download_format="ZIP"): @@ -618,16 +610,10 @@ def deploy_all(self, location_with_name, os, download_format="ZIP"): :param os: OS for which the installation is intended. :param download_format: Installer format (ZIP/MSI/EXE). """ - query_dict = dict(os=os, download_format=download_format) - response = self._api_client.api_request( - url="{}{}".format(self._get_url(), 'deploy_all/'), - query_params=query_dict, - stream=True) - file_path = "{}.{}".format(location_with_name, download_format.lower()) - with open(file_path, 'wb') as f: - response.raw.decode_content = True - shutil.copyfileobj(response.raw, f) + self._api_client.request_and_download(url="{}{}".format(self._get_url(), "deploy_all/"), + destination_path=file_path, + query_params=dict(os=os, download_format=download_format)) def params(self): raise NotImplementedError @@ -657,12 +643,12 @@ class Breadcrumb(Entity): - A cookie with a session token, stored in the endpoint's browser. """ - NAME = 'breadcrumb' + NAME = "breadcrumb" RELATED_COLLECTIONS = { - 'attached_services': Service, - 'available_services': Service, - 'deployment_groups': DeploymentGroup + "attached_services": Service, + "available_services": Service, + "deployment_groups": DeploymentGroup } def update(self, name, **kwargs): @@ -687,8 +673,8 @@ def connect_to_service(self, service_id): :param service_id: The service ID to which the breadcrumb should be attached. """ data = dict(service_id=service_id) - self._api_client.api_request(url="{}{}".format(self.url, 'connect_to_service/'), - method='post', + self._api_client.api_request(url="{}{}".format(self.url, "connect_to_service/"), + method="post", data=data) self.load() @@ -699,8 +685,8 @@ def detach_from_service(self, service_id): :param service_id: Service ID from which the breadcrumbs should be detached. """ data = dict(service_id=service_id) - self._api_client.api_request(url="{}{}".format(self.url, 'detach_from_service/'), - method='post', + self._api_client.api_request(url="{}{}".format(self.url, "detach_from_service/"), + method="post", data=data) self.load() @@ -714,14 +700,10 @@ def deploy(self, location_with_name, os, download_type, download_format="ZIP"): :param download_format: Installer format (ZIP/EXE/MSI). """ query_dict = dict(os=os, download_type=download_type, download_format=download_format) - response = self._api_client.api_request( - "{}{}".format(self.url, 'deploy/'), - query_params=query_dict, stream=True) - file_path = "{}.{}".format(location_with_name, download_format.lower()) - with open(file_path, 'wb') as f: - response.raw.decode_content = True - shutil.copyfileobj(response.raw, f) + self._api_client.request_and_download(url="{}{}".format(self.url, "deploy/"), + destination_path=file_path, + query_params=query_dict) def add_to_group(self, deployment_group_id): """ @@ -731,10 +713,10 @@ def add_to_group(self, deployment_group_id): """ if not isinstance(deployment_group_id, Number): - raise BadParamError('deployment_group_id must be a number') + raise BadParamError("deployment_group_id must be a number") data = dict(deployment_group_id=deployment_group_id) - self._api_client.api_request("{}{}".format(self.url, 'add_to_group/'), 'post', data=data) + self._api_client.api_request("{}{}".format(self.url, "add_to_group/"), "post", data=data) self.load() def remove_from_group(self, deployment_group_id): @@ -744,11 +726,20 @@ def remove_from_group(self, deployment_group_id): :param deployment_group_id: Deployment group ID from which the breadcrumb should be removed. """ data = dict(deployment_group_id=deployment_group_id) - self._api_client.api_request(url="{}{}".format(self.url, 'remove_from_group/'), - method='post', + self._api_client.api_request(url="{}{}".format(self.url, "remove_from_group/"), + method="post", data=data) self.load() + def download_breadcrumb_honeydoc(self, location_with_name): + """ + Download the HoneyDoc file for this breadcrumb (for HoneyDoc breadcrumbs only). + + :param location_with_name: Destination path. + """ + self._api_client.request_and_download(url="{}{}".format(self.url, "download_breadcrumb_honeydoc/"), + destination_path=location_with_name) + class BreadcrumbCollection(EditableCollection): """ @@ -759,22 +750,26 @@ class BreadcrumbCollection(EditableCollection): MODEL_CLASS = Breadcrumb - def create(self, name, breadcrumb_type, **kwargs): + def create(self, name, breadcrumb_type, file_field_name=None, file_path=None, **kwargs): """ Create a new breadcrumb. :param name: An internal name for the breadcrumb. :param breadcrumb_type: The type of breadcrumb. See options for a list \ of available breadcrumb types. + :param file_field_name: For breadcrumbs requiring a file, the name of the + breadcrumb field expected to contain the file content. + :param file_path: For breadcrumbs requiring a file, the path to the file to upload. :param kwargs: Other parameters relevant for the desired breadcrumb type. See options \ for more information. """ + files = {file_field_name: open(file_path, "rb")} if file_field_name else None data = dict( name=name, breadcrumb_type=breadcrumb_type ) data.update(kwargs) - return self.create_item(data) + return self.create_item(data, files=files) class AlertProcessDLL(BaseEntity): @@ -788,8 +783,8 @@ def download_file(self, destination_path): :param destination_path: Location on the disk where you want to save the file. """ self._api_client.request_and_download( - url='{url}{download_file_suffix}/'.format(url=self.url, - download_file_suffix='download_file'), + url="{url}{download_file_suffix}/".format(url=self.url, + download_file_suffix="download_file"), destination_path=destination_path) @@ -801,14 +796,14 @@ class AlertProcessDLLCollection(Collection): """ MODEL_CLASS = AlertProcessDLL - URL_SUFFIX = 'dll' + URL_SUFFIX = "dll" def __init__(self, api_client, alert_process, obj_class=None): super(AlertProcessDLLCollection, self).__init__(api_client, obj_class) self.alert_process = alert_process def _get_url(self): - return '{process_url}{suffix}/'.format(process_url=self.alert_process.url, + return "{process_url}{suffix}/".format(process_url=self.alert_process.url, suffix=self.URL_SUFFIX) @@ -832,8 +827,8 @@ def download_file(self, destination_path): :param destination_path: Location on the disk where you want to save the file. """ self._api_client.request_and_download( - url='{url}{download_file_suffix}/'.format(url=self.url, - download_file_suffix='download_file'), + url="{url}{download_file_suffix}/".format(url=self.url, + download_file_suffix="download_file"), destination_path=destination_path) def download_minidump(self, destination_path): @@ -843,9 +838,9 @@ def download_minidump(self, destination_path): :param destination_path: Location on the disk where you want to save the file. """ self._api_client.request_and_download( - url='{url}{download_file_suffix}/'.format(url=self.url, - download_file_suffix='download_minidump'), - destination_path='{}.dump'.format(destination_path)) + url="{url}{download_file_suffix}/".format(url=self.url, + download_file_suffix="download_minidump"), + destination_path="{}.dump".format(destination_path)) class AlertProcessCollection(Collection): @@ -855,14 +850,14 @@ class AlertProcessCollection(Collection): This entity will be returned by calling :py:meth:`api_client.Alert.get_processes` """ MODEL_CLASS = AlertProcess - URL_SUFFIX = 'process' + URL_SUFFIX = "process" def __init__(self, api_client, alert, obj_class=None): super(AlertProcessCollection, self).__init__(api_client, obj_class) self.alert = alert def _get_url(self): - return '{alert_url}{suffix}/'.format(alert_url=self.alert.url, suffix=self.URL_SUFFIX) + return "{alert_url}{suffix}/".format(alert_url=self.alert.url, suffix=self.URL_SUFFIX) class Alert(BaseEntity): @@ -874,14 +869,14 @@ class Alert(BaseEntity): which query was run on the DB, which SMB shares were accessed, etc. """ - NAME = 'alert' - PROCESSES_URL_SUFFIX = 'processes' + NAME = "alert" + PROCESSES_URL_SUFFIX = "processes" def delete(self): """ Delete the alert """ - self._api_client.api_request(self.url, 'delete') + self._api_client.api_request(self.url, "delete") def download_image_file(self, location_with_name): """ @@ -889,13 +884,8 @@ def download_image_file(self, location_with_name): :param location_with_name: Download destination path. """ - response = self._api_client.api_request(url="{}{}".format(self.url, 'download_image_file/'), - stream=True) - - file_path = "{}.bin".format(location_with_name) - with open(file_path, 'wb') as f: - response.raw.decode_content = True - shutil.copyfileobj(response.raw, f) + self._api_client.request_and_download(url="{}{}".format(self.url, "download_image_file/"), + destination_path="{}.bin".format(location_with_name)) def download_memory_dump_file(self, location_with_name): """ @@ -903,14 +893,8 @@ def download_memory_dump_file(self, location_with_name): :param location_with_name: Download destination path. """ - response = self._api_client.api_request(url="{}{}".format(self.url, - 'download_memory_dump_file/'), - stream=True) - - file_path = "{}.bin".format(location_with_name) - with open(file_path, 'wb') as f: - response.raw.decode_content = True - shutil.copyfileobj(response.raw, f) + self._api_client.request_and_download(url="{}{}".format(self.url, "download_memory_dump_file/"), + destination_path="{}.bin".format(location_with_name)) def download_network_capture_file(self, location_with_name): """ @@ -918,14 +902,8 @@ def download_network_capture_file(self, location_with_name): :param location_with_name: Download destination path. """ - response = self._api_client.api_request(url="{}{}".format(self.url, - 'download_network_capture_file/'), - stream=True) - - file_path = "{}.pcap".format(location_with_name) - with open(file_path, 'wb') as f: - response.raw.decode_content = True - shutil.copyfileobj(response.raw, f) + self._api_client.request_and_download(url="{}{}".format(self.url, "download_network_capture_file/"), + destination_path="{}.pcap".format(location_with_name)) def download_stix_file(self, location_with_name): """ @@ -933,13 +911,8 @@ def download_stix_file(self, location_with_name): :param location_with_name: Download destination path. """ - response = self._api_client.api_request(url="{}{}".format(self.url, 'download_stix_file/'), - stream=True) - - file_path = "{}.xml".format(location_with_name) - with open(file_path, 'wb') as f: - response.raw.decode_content = True - shutil.copyfileobj(response.raw, f) + self._api_client.request_and_download(url="{}{}".format(self.url, "download_stix_file/"), + destination_path="{}.xml".format(location_with_name)) def get_processes(self): """ @@ -1038,15 +1011,9 @@ def export(self, location_with_name): :param location_with_name: Download destination file. """ - query_params = self._get_query_params() - response = self._api_client.api_request(url="{}{}".format(self._get_url(), 'export/'), - stream=True, - query_params=query_params) - - file_path = "{}.csv".format(location_with_name) - with open(file_path, 'wb') as f: - response.raw.decode_content = True - shutil.copyfileobj(response.raw, f) + self._api_client.request_and_download(url="{}{}".format(self._get_url(), "export/"), + destination_path="{}.csv".format(location_with_name), + query_params=self._get_query_params()) def delete(self, selected_alert_ids=None, delete_all_filtered=False): """ @@ -1065,14 +1032,14 @@ def delete(self, selected_alert_ids=None, delete_all_filtered=False): Example 2: Delete alerts by filter:: client = mazerunner.connect(...) - filtered_alerts = client.alerts.filter(alert_types=['share', 'http']) + filtered_alerts = client.alerts.filter(alert_types=["share", "http"]) filtered_alerts.delete(delete_all_filtered=True) """ data = dict(selected_alert_ids=selected_alert_ids, delete_all_filtered=delete_all_filtered) query_params = self._get_query_params() - self._api_client.api_request(url="{}{}".format(self._get_url(), 'delete_selected/'), - method='post', + self._api_client.api_request(url="{}{}".format(self._get_url(), "delete_selected/"), + method="post", data=data, query_params=query_params) @@ -1083,7 +1050,7 @@ class ForensicPullerOnDemand(BaseCollection): This entity will be returned by :py:attr:`api_client.APIClient.forensic_puller_on_demand`. """ - URL_EXTENSION = 'forensic-puller-on-demand-run' + URL_EXTENSION = "forensic-puller-on-demand-run" def run_on_ip_list(self, ip_list): """ @@ -1094,7 +1061,7 @@ def run_on_ip_list(self, ip_list): data = dict(ip_list=ip_list) self._api_client.api_request( url=self._get_url(), - method='post', + method="post", data=data) @@ -1103,7 +1070,7 @@ class StorageUsageData(BaseCollection): Storage usage data. This entity will be returned by :py:attr:`api_client.APIClient.storage_usage_data`. """ - URL_EXTENSION = 'storage-usage' + URL_EXTENSION = "storage-usage" def __unicode__(self): return unicode(self.details()) @@ -1112,7 +1079,7 @@ def __str__(self): return str(self.details()) def details(self): - return self._api_client.api_request(url=self._get_url(), method='get') + return self._api_client.api_request(url=self._get_url(), method="get") def _get_url(self): return self._api_client.api_urls[self.URL_EXTENSION] @@ -1124,10 +1091,10 @@ class Endpoint(Entity): of the breadcrumbs' deployment to it. """ - NAME = 'endpoint' + NAME = "endpoint" RELATED_FIELDS = { - 'deployment_group': DeploymentGroup, + "deployment_group": DeploymentGroup, } def delete(self): @@ -1135,9 +1102,9 @@ def delete(self): Delete the endpoint. """ base_url = self._api_client.api_urls[self.NAME] - url = '%sdelete_selected/?filter_enabled=true' % base_url + url = "%sdelete_selected/?filter_enabled=true" % base_url data = {"selected_endpoints_ids": [self.id]} - self._api_client.api_request(url, 'post', data=data) + self._api_client.api_request(url, "post", data=data) def reassign_to_group(self, deployment_group): self._api_client.endpoints.reassign_to_group(deployment_group, [self]) @@ -1154,18 +1121,18 @@ class EndpointCollection(EditableCollection): """ MODEL_CLASS = Endpoint - UNASSIGN_FROM_DEPLOYMENT_GROUP = 'unassigned' + UNASSIGN_FROM_DEPLOYMENT_GROUP = "unassigned" RUN_METHOD_FOR_INSTALL_METHOD = { - 'ZIP': 'CMD_DEPLOY', - 'EXE': 'EXE_DEPLOY', - 'MSI': 'EXE_DEPLOY' + "ZIP": "CMD_DEPLOY", + "EXE": "EXE_DEPLOY", + "MSI": "EXE_DEPLOY" } def __init__(self, api_client, filter_enabled=False, - keywords='', + keywords="", statuses=None, deploy_groups=None): """ @@ -1197,10 +1164,10 @@ def create(self, ip_address=None, dns=None, hostname=None, deployment_group_id=N def _get_query_params(self): return { - 'filter_enabled': self.filter_enabled, - 'keywords': self.keywords, - 'statuses': self.statuses, - 'deploy_groups': self.deploy_groups + "filter_enabled": self.filter_enabled, + "keywords": self.keywords, + "statuses": self.statuses, + "deploy_groups": self.deploy_groups } def filter(self, keywords=""): @@ -1226,8 +1193,8 @@ def reassign_to_group(self, deployment_group, endpoints): selected_endpoints_ids=[e.id for e in endpoints]) self._api_client.api_request( - "{}{}".format(self._get_url(), 'reassign_selected/'), - method='POST', + "{}{}".format(self._get_url(), "reassign_selected/"), + method="POST", data=data) def clear_deployment_group(self, endpoints): @@ -1241,20 +1208,20 @@ def clear_deployment_group(self, endpoints): selected_endpoints_ids=[ep.id for ep in endpoints]) self._api_client.api_request( - "{}{}".format(self._get_url(), 'reassign_selected/'), - method='POST', + "{}{}".format(self._get_url(), "reassign_selected/"), + method="POST", data=data) def _get_run_method(self, install_method): if install_method not in self.RUN_METHOD_FOR_INSTALL_METHOD: - raise InvalidInstallMethodError('Invalid install method: %s' % install_method) + raise InvalidInstallMethodError("Invalid install method: %s" % install_method) return self.RUN_METHOD_FOR_INSTALL_METHOD[install_method.upper()] def clean_filtered(self, install_method, username, password, - domain=''): + domain=""): """ Uninstall breadcrumbs from all of the endpoints matching the filter. @@ -1264,18 +1231,18 @@ def clean_filtered(self, :param domain: The domain where that user is registered. Leave blank for local user. """ - self._api_client.api_request(url='{}{}'.format(self._get_url(), 'clean_selected/'), - method='POST', + self._api_client.api_request(url="{}{}".format(self._get_url(), "clean_selected/"), + method="POST", query_params=self._get_query_params(), data={ - 'clean_all_filtered': True, - 'username': username, - 'password': { - 'value': password + "clean_all_filtered": True, + "username": username, + "password": { + "value": password }, - 'domain': domain, - 'run_method': self._get_run_method(install_method), - 'install_method': install_method + "domain": domain, + "run_method": self._get_run_method(install_method), + "install_method": install_method }) def clean_by_endpoints_ids(self, @@ -1283,7 +1250,7 @@ def clean_by_endpoints_ids(self, install_method, username, password, - domain=''): + domain=""): """ Uninstall breadcrumbs from all of the specified endpoint IDs. @@ -1293,28 +1260,28 @@ def clean_by_endpoints_ids(self, :param password: Password for that user. :param domain: The domain where that user is registered. Leave blank for local user. """ - self._api_client.api_request(url='{}{}'.format(self._get_url(), 'clean_selected/'), - method='POST', + self._api_client.api_request(url="{}{}".format(self._get_url(), "clean_selected/"), + method="POST", data={ - 'selected_endpoints_ids': endpoints_ids, - 'username': username, - 'password': { - 'value': password + "selected_endpoints_ids": endpoints_ids, + "username": username, + "password": { + "value": password }, - 'domain': domain, - 'run_method': self._get_run_method(install_method), - 'install_method': install_method + "domain": domain, + "run_method": self._get_run_method(install_method), + "install_method": install_method }) def delete_filtered(self): """ Delete all the endpoints matching the filter. """ - self._api_client.api_request(url='{}{}'.format(self._get_url(), 'delete_selected/'), - method='POST', + self._api_client.api_request(url="{}{}".format(self._get_url(), "delete_selected/"), + method="POST", query_params=self._get_query_params(), data={ - 'delete_all_filtered': True + "delete_all_filtered": True }) def delete_by_endpoints_ids(self, endpoints_ids): @@ -1323,17 +1290,17 @@ def delete_by_endpoints_ids(self, endpoints_ids): :param endpoints_ids: List of the endpoint IDs to be deleted. """ - self._api_client.api_request(url='{}{}'.format(self._get_url(), 'delete_selected/'), - method='POST', + self._api_client.api_request(url="{}{}".format(self._get_url(), "delete_selected/"), + method="POST", data={ - 'selected_endpoints_ids': endpoints_ids, + "selected_endpoints_ids": endpoints_ids, }) def export_filtered(self): """ Export all filtered endpoints to CSV. """ - return self._api_client.api_request(url='{}{}'.format(self._get_url(), 'export/'), + return self._api_client.api_request(url="{}{}".format(self._get_url(), "export/"), query_params=self._get_query_params(), expect_json_response=False) @@ -1341,7 +1308,7 @@ def filter_data(self): """ Get the available values for the endpoint filters. """ - return self._api_client.api_request(url='{}{}'.format(self._get_url(), 'filter_data/')) + return self._api_client.api_request(url="{}{}".format(self._get_url(), "filter_data/")) def params(self): raise NotImplementedError @@ -1354,13 +1321,13 @@ class BackgroundTask(BaseEntity): Examples of requests that create background tasks include deployment on endpoints, and importing the organization structure from Active Directory. """ - NAME = 'background-task' + NAME = "background-task" def stop(self): """ Stop task. """ - self._api_client.api_request("{}{}".format(self.url, 'stop/'), 'post') + self._api_client.api_request("{}{}".format(self.url, "stop/"), "post") class BackgroundTaskCollection(Collection): @@ -1397,7 +1364,7 @@ def acknowledge_all_complete(self): """ Acknowledge all tasks with the status 'stopped' or 'complete'. """ - self._api_client.api_request("{}{}".format(self._get_url(), 'acknowledge_all/'), 'post') + self._api_client.api_request("{}{}".format(self._get_url(), "acknowledge_all/"), "post") def params(self): raise NotImplementedError @@ -1412,7 +1379,7 @@ class AlertPolicy(BaseEntity): - 1 = Mute - 2 = Alert """ - NAME = 'alert-policy' + NAME = "alert-policy" def update_to_status(self, to_status): """ @@ -1421,7 +1388,7 @@ def update_to_status(self, to_status): :param to_status: The name of the new 'to_status' of the policy. """ data = dict(to_status=to_status) - response = self._api_client.api_request(self.url, 'put', data=data) + response = self._api_client.api_request(self.url, "put", data=data) self._update_entity_data(response) @@ -1448,7 +1415,7 @@ def reset_all_to_default(self): """ Reset the 'to_status' of all alert policies to their original system default. """ - self._api_client.api_request("{}{}".format(self._get_url(), 'reset_all/'), 'post') + self._api_client.api_request("{}{}".format(self._get_url(), "reset_all/"), "post") class CIDRMapping(BaseEntity): @@ -1460,23 +1427,23 @@ class CIDRMapping(BaseEntity): mapping, the daily CIDR block importer will also assign that deployment group to endpoints that were just imported or did not have one configured. """ - NAME = 'cidr-mapping' + NAME = "cidr-mapping" def generate_endpoints(self): """ Scan the CIDR block and import the endpoints. """ - return self._api_client.api_request('{}{}'.format(self.url, 'generate_endpoints/'), - method='post', + return self._api_client.api_request("{}{}".format(self.url, "generate_endpoints/"), + method="post", data={ - 'reassign': False + "reassign": False }) def delete(self): """ Delete this record. """ - self._api_client.api_request(self.url, 'delete') + self._api_client.api_request(self.url, "delete") class CIDRMappingCollection(UnpaginatedEditableCollection): @@ -1499,21 +1466,21 @@ def create(self, cidr_block, deployment_group, comments, active): :param active: Whether this block should be included in the import. """ return self.create_item({ - 'cidr_block': cidr_block, - 'deployment_group': deployment_group, - 'comments': comments, - 'active': active + "cidr_block": cidr_block, + "deployment_group": deployment_group, + "comments": comments, + "active": active }) def generate_all_endpoints(self): """ Scan all the active CIDR blocks in the system and import all of their endpoints. """ - return self._api_client.api_request('{}{}'.format(self._get_url(), - 'generate_all_endpoints/'), - method='post', + return self._api_client.api_request("{}{}".format(self._get_url(), + "generate_all_endpoints/"), + method="post", data={ - 'reassign': False + "reassign": False }) def params(self): @@ -1525,7 +1492,7 @@ class ActiveSOCEvent(Entity): A message to be sent to a SOC interface. """ - NAME = 'api-soc' + NAME = "api-soc" class ActiveSOCEventCollection(EditableCollection): @@ -1561,7 +1528,7 @@ def create_multiple_events(self, soc_name, events_dicts): source=soc_name, data=events_dicts ) - self._api_client.api_request(self._get_url(), 'post', data=data) + self._api_client.api_request(self._get_url(), "post", data=data) def params(self): raise NotImplementedError @@ -1594,10 +1561,10 @@ class APIClient(object): Example:: - client = mazerunner.connect(ip_address='1.2.3.4', - api_key='my-api-key', - api_secret='my-api-secret', - certificate='/path/to/MazeRunner.crt') + client = mazerunner.connect(ip_address="1.2.3.4", + api_key="my-api-key", + api_secret="my-api-secret", + certificate="/path/to/MazeRunner.crt") my_service = client.services.get_item(id=8) """ @@ -1615,13 +1582,13 @@ def __init__(self, host, api_key, api_secret, certificate): self._certificate = False else: self._certificate = certificate - self._base_url = 'https://%(host)s' % dict(host=host) + self._base_url = "https://%(host)s" % dict(host=host) self._session = requests.Session() - self.api_urls = self.api_request('/api/v1.0/') + self.api_urls = self.api_request("/api/v1.0/") def api_request(self, url, - method='get', + method="get", query_params=None, data=None, files=None, @@ -1650,7 +1617,7 @@ def api_request(self, netloc=parsed.netloc, path=parsed.path, params=parsed.params, - query='', + query="", fragment=parsed.fragment) url = urlparse.urlunparse(parsed_no_query) query = {query_param_name: set(query_param_value) @@ -1667,10 +1634,10 @@ def api_request(self, auth=self._auth ) if not files: - request_args['json'] = data + request_args["json"] = data else: - request_args['data'] = data - request_args['files'] = files + request_args["data"] = data + request_args["files"] = files req = requests.Request(**request_args) resp = self._session.send(req.prepare(), verify=self._certificate, stream=stream) @@ -1685,7 +1652,7 @@ def api_request(self, return resp if method == "delete" or resp.status_code == NO_CONTENT: - return + return None content_type = resp.headers.get("Content-Type", None) @@ -1707,9 +1674,9 @@ def api_request(self, "Bad response: Not json.\nContent:\n{content}".format(content=resp.content) ) - def request_and_download(self, url, destination_path): - data = self.api_request(url, stream=True) - with open(destination_path, 'wb') as f: + def request_and_download(self, url, destination_path, query_params=None): + data = self.api_request(url, stream=True, query_params=query_params) + with open(destination_path, "wb") as f: data.raw.decode_content = True shutil.copyfileobj(data.raw, f) @@ -1723,10 +1690,10 @@ def decoys(self): client = mazerunner.connect(...) backup_server_story_decoy = client.decoys.create( - name='backup_server_decoy', - os='Windows_Server_2012', - hostname='backupserver', - vm_type='KVM') + name="backup_server_decoy", + os="Windows_Server_2012", + hostname="backupserver", + vm_type="KVM") old_decoy = client.decoys.get_item(id=5) old_decoy.delete() @@ -1743,8 +1710,8 @@ def services(self): client = mazerunner.connect(...) app_db_service = client.services.create( - name='app_db_service', - service_type='mysql') + name="app_db_service", + service_type="mysql") """ return ServiceCollection(self) @@ -1758,7 +1725,7 @@ def deployment_groups(self): client = mazerunner.connect(...) hr_deployment_group = client.deployment_groups.create( - name='breadcrumbs_for_hr_machines') + name="breadcrumbs_for_hr_machines") """ return DeploymentGroupCollection(self) @@ -1772,10 +1739,10 @@ def breadcrumbs(self): client = mazerunner.connect(...) mysql_breadcrumb = client.breadcrumbs.create( - breadcrumb_type='mysql', - name='mysql_breadcrumb', - deploy_for='root', - installation_type='mysql_history') + breadcrumb_type="mysql", + name="mysql_breadcrumb", + deploy_for="root", + installation_type="mysql_history") """ return BreadcrumbCollection(self) @@ -1788,7 +1755,7 @@ def alerts(self): Example:: client = mazerunner.connect(...) - code_alerts = client.alerts.filter(alert_types=['code']) + code_alerts = client.alerts.filter(alert_types=["code"]) """ return AlertCollection(self) @@ -1801,7 +1768,7 @@ def forensic_puller_on_demand(self): Example:: client = mazerunner.connect(...) - code_alerts = client.forensic_puller_on_demand.run_on_ip_list(ip_list=['192.168.1.1']) + code_alerts = client.forensic_puller_on_demand.run_on_ip_list(ip_list=["192.168.1.1"]) """ return ForensicPullerOnDemand(self) @@ -1860,12 +1827,12 @@ def active_soc_events(self): Example:: client = mazerunner.connect(...) - self.active_soc_events.create_multiple_events('my-soc-interface-name', [{ - 'ComputerName': 'TEST_ENDPOINT1', - 'EventCode': 4625 + self.active_soc_events.create_multiple_events("my-soc-interface-name", [{ + "ComputerName": "TEST_ENDPOINT1", + "EventCode": 4625 },{ - 'ComputerName': 'TEST_ENDPOINT2', - 'EventCode': 529 + "ComputerName": "TEST_ENDPOINT2", + "EventCode": 529 }]) """ return ActiveSOCEventCollection(self) @@ -1880,9 +1847,9 @@ def cidr_mappings(self): client = mazerunner.connect(...) developers_segment = client.cidr_mapping.create( - cidr_block='192.168.5.0/24', + cidr_block="192.168.5.0/24", deployment_group=5, - comments='R&D', + comments="R&D", active=True) developers_segment.generate_endpoints() """ diff --git a/test/sample.docx b/test/sample.docx new file mode 100644 index 0000000000000000000000000000000000000000..1352bc73510b5e704a49a39227fa4b15f7303490 GIT binary patch literal 13435 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 0 + class TestDecoy(APITest): DECOY_STATUS_ACTIVE = 'active' @@ -992,7 +1028,7 @@ def _test_delete(): endpoints = list(self.endpoints.filter(self.lab_endpoint_ip)) assert len(endpoints) > 0 - self.endpoints.delete_by_endpoints_ids([endpoints[0].id]) + self.endpoints.delete_by_endpoints_ids([curr_endpoint.id for curr_endpoint in endpoints]) assert len(self.endpoints.filter(self.lab_endpoint_ip)) == 0 def _test_data(): From f48c66c019944bbf4c63242a476ba2c534c0e759 Mon Sep 17 00:00:00 2001 From: Shay Ingber Date: Wed, 4 Jul 2018 16:18:23 +0000 Subject: [PATCH 35/38] Merged in feature/MAZ-5445 (pull request #46) Feature/MAZ-5445 Allow access to the AuditLog Via the SDK and the API Also change the date format of some of MazeRunner's dates from dd/mm/yyyy to yyyy-mm-dd for better functionality on the API's side Approved-by: Dekel --- mazerunner/api_client.py | 70 +++++++++++++++++- test/test_sdk.py | 153 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 220 insertions(+), 3 deletions(-) diff --git a/mazerunner/api_client.py b/mazerunner/api_client.py index d3429eb..c81997c 100644 --- a/mazerunner/api_client.py +++ b/mazerunner/api_client.py @@ -9,7 +9,8 @@ from mazerunner.exceptions import ValidationError, ServerError, BadParamError, \ InvalidInstallMethodError -ALERTS_PER_PAGE = 500 +ENTRIES_PER_PAGE = 500 +ISO_TIME_FORMAT = "%Y-%m-%d" class BaseCollection(object): @@ -964,7 +965,7 @@ def _get_query_params(self): return dict(filter_enabled=self.filter_enabled, only_alerts=self.only_alerts, alert_types=self.alert_types, - per_page=ALERTS_PER_PAGE, + per_page=ENTRIES_PER_PAGE, start_date=self.start_date, end_date=self.end_date, id_gt=self.id_greater_than, @@ -1540,6 +1541,67 @@ def get_item(self, id): raise NotImplementedError +class AuditLogLine(Entity): + """ + A message from the server's audit log + """ + NAME = "audit-log" + + +class AuditLogLineCollection(Collection): + """ + Use this to access MazeRunner's audit log. + + This entity will be returned by :py:attr:`api_client.APIClient.audit_log`. + """ + MODEL_CLASS = AuditLogLine + + def __init__(self, api_client, filter_enabled=False, item=None, username=None, event_type=None, keywords=None, + start_date=None, end_date=None, object_ids=None, category=None): + super(AuditLogLineCollection, self).__init__(api_client) + self.filter_enabled = filter_enabled + self.end_date = end_date + self.start_date = start_date + self.username = username + self.category = category + self.event_type = event_type + self.object_ids = object_ids + self.item = item + self.keywords = keywords + + def _get_query_params(self): + return {'filter_enabled': self.filter_enabled, + 'end_date': self.end_date, + 'start_date': self.start_date, + 'per_page': ENTRIES_PER_PAGE, + 'username[]': self.username, + 'category[]': self.category, + 'event_type[]': self.event_type, + 'object_ids': self.object_ids, + 'item': self.item} + + def filter(self, filter_enabled=True, item=None, username=None, event_type=None, + start_date=None, end_date=None, object_ids=None, category=None): + + for param_name, param in dict(username=username, + event_type=event_type, + category=category).iteritems(): + if param and type(param) != list: + raise BadParamError("{} has to be a list!".format(param_name)) + return AuditLogLineCollection(self._api_client, + filter_enabled=filter_enabled, + end_date=end_date, + start_date=start_date, + username=username, + category=category, + event_type=event_type, + object_ids=object_ids, + item=item) + + def delete(self): + self._api_client.api_request("".join([self._get_url(), "delete_audit_log/"]), method="post") + + class APIClient(object): """ This is the starting point for any interaction with MazeRunner. @@ -1854,3 +1916,7 @@ def cidr_mappings(self): developers_segment.generate_endpoints() """ return CIDRMappingCollection(self) + + @property + def audit_log(self): + return AuditLogLineCollection(self) diff --git a/test/test_sdk.py b/test/test_sdk.py index dc1a892..82a2e39 100644 --- a/test/test_sdk.py +++ b/test/test_sdk.py @@ -1,5 +1,6 @@ import StringIO import csv +import datetime import json import logging import shutil @@ -13,7 +14,7 @@ import os from mazerunner.api_client import Service, AlertPolicy, Decoy, Breadcrumb, \ - DeploymentGroup, Endpoint, CIDRMapping, BackgroundTask + DeploymentGroup, Endpoint, CIDRMapping, BackgroundTask, AuditLogLine, ISO_TIME_FORMAT from mazerunner.exceptions import ValidationError, ServerError, BadParamError, \ InvalidInstallMethodError from utils import TimeoutException, wait_until @@ -95,6 +96,7 @@ def _configure_entities_groups(self): self.cidr_mappings = self.client.cidr_mappings self.endpoints = self.client.endpoints self.background_tasks = self.client.background_tasks + self.audit_log = self.client.audit_log self.disposable_entities = [ self.decoys, @@ -1112,3 +1114,152 @@ def test_create_invalid_endpoint(self): assert error[key] == [expected_error_message] else: assert error["non_field_errors"] == [expected_error_message] + + +class TestAuditLog(APITest): + + @staticmethod + def _format_time(date_obj): + return date_obj.strftime(ISO_TIME_FORMAT) + + def _test_time_based_queries(self): + today = datetime.datetime.now() + tomorrow = today + datetime.timedelta(days=1) + a_week_ago = today + datetime.timedelta(days=-7) + two_weeks_ago = today + datetime.timedelta(days=-14) + + # check that today has data - start date + assert len(self.audit_log.filter(start_date=self._format_time(today))) != 0, \ + "No data from today according to start date" + + # and that tomorrow doesn't - start date + assert len(self.audit_log.filter(start_date=self._format_time(tomorrow))) == 0, \ + "Data from tomorrow found!" + + # check that today has data - end date + assert len(self.audit_log.filter(end_date=self._format_time(today))) != 0, \ + "No data from today according to end date" + + # and that last week doesn't - end date + assert len(self.audit_log.filter(end_date=self._format_time(a_week_ago))) == 0, \ + "Data from a week ago found, even though we deleted everything!" + + # test time range + assert len(self.audit_log.filter(start_date=self._format_time(a_week_ago), + end_date=self._format_time(today))) != 0, \ + "No logs from the past week" + + assert len(self.audit_log.filter(start_date=self._format_time(two_weeks_ago), + end_date=self._format_time(a_week_ago))) == 0, \ + "Logs found from two weeks ago." + + def _test_object_ids_queries(self, log_count): + # get obj ids from server + object_ids = [log_line._param_dict.get("object_ids") for log_line in self.audit_log] + # and extract them + object_ids = list(set([object_id[0] if object_id else None for object_id in object_ids])) + + # and make sure you have enough obj ids + assert len(object_ids) >= 2, "No more than 1 object ID in the system" + + # test that you don't get all the alerts when filtering + usable_object_id = [object_id for object_id in object_ids if object_id][0] + + assert len(self.audit_log.filter(object_ids=usable_object_id)) != log_count, \ + "Object ID filter returned the same amount of logs as the full filter" + + def _test_username_queries(self, log_count): + user_id = self.client._auth.credentials['id'] + + # make sure that if the username is right you get data + assert len(self.audit_log.filter(username=[user_id])) != 0, "No logs found for the user" + + # Note: we don't have more than one user in the tests, therefore we don't + # have a test that filters one user's info + + # check that a bad username doesnt provide any data + bad_username = user_id * 2 + assert len(self.audit_log.filter(username=[bad_username])) == 0, \ + "Logs found for the (probably) nonexistent user {}".format(bad_username) + + # test users not list ERR + with pytest.raises(BadParamError): + self.audit_log.filter(username=user_id) + + def _test_category_queries(self, log_count): + # get categories from server + categories = list(set([log_line._param_dict.get("category") for log_line in self.audit_log])) + + # and make sure you have enough + assert len(categories) >= 2, "No more than 1 category in the system" + + # make sure the param is OK + assert len(self.audit_log.filter(category=[categories[0]])) != 0, \ + "No logs for previously existing filter value" + + # test that you don't get all the alerts when filtering + assert len(self.audit_log.filter(category=[categories[0]])) != log_count, \ + "Filtered list returned the same amount of logs as the full filter" + + # test categories not list ERR + with pytest.raises(BadParamError): + self.audit_log.filter(category=categories[0]) + + def _test_event_type_queries(self, log_count): + # get event_types from server + event_types = list(set([log_line._param_dict.get("event_type_label") for log_line in self.audit_log])) + + # and make sure you have enough + assert len(event_types) >= 2, "No more than 1 event type in the system." + + # make sure the param is OK + assert len(self.audit_log.filter(event_type=[event_types[0]])) != 0, \ + "No logs for previously existing filter value" + + # test that you don't get all the alerts when filtering + assert len(self.audit_log.filter(event_type=[event_types[0]])) != log_count, \ + "Filtered list returned the same amount of logs as the full filter" + + # test event type not list ERR + with pytest.raises(BadParamError): + self.audit_log.filter(event_type=event_types[0]) + + def test_audit_log_query(self): + + # test delete (at the start for a clean log) + self.audit_log.delete() + logger.info("Audit log cleared") + + # build all sorts of logs + decoy_ssh = self.create_decoy(dict(name=SSH_DECOY_NAME, + hostname="decoyssh", + os="Ubuntu_1404", + vm_type="KVM")) + + # test query + log_count = self.audit_log + assert len(log_count) != 0, "No logs found" + assert type(list(self.audit_log)[0]) == AuditLogLine, "Invalid output" + + self._test_time_based_queries() + self._test_object_ids_queries(log_count) + self._test_username_queries(log_count) + self._test_category_queries(log_count) + self._test_event_type_queries(log_count) + + # test filter=False with params + assert len(self.audit_log.filter(event_type=["Delete"], filter_enabled=False)) == \ + len(self.audit_log.filter(event_type=["Action"], filter_enabled=False)), \ + "filter_enabled = False should make other filters redundant" + + # test delete (again to make sure the log is actually cleaned) + self.audit_log.delete() + assert len(self.audit_log) != 0, "No delete log!" + assert len(self.audit_log) == 1, "Other logs found" + + # once this issue is fixed we need to use the provided data from the decoy creation to actually filter items + @pytest.mark.xfail(reason="Item filtering is broken on server side") + def test_audit_log_item_filter(self): + # look for an item that doesnt exist + INVALID_ITEM_FILTER = "qweasdzxc" + assert len(self.audit_log.filter(item=INVALID_ITEM_FILTER)) == 0 From a1429909dbecc4440ff1bef4add0e1cbd6bf4244 Mon Sep 17 00:00:00 2001 From: frankie Date: Sun, 15 Jul 2018 17:14:02 +0300 Subject: [PATCH 36/38] Version update to 1.2.1 --- .gitignore | 1 + PKG-INFO | 6 +++--- long_description.txt | 8 ++++++++ setup.py | 8 ++++++-- source/conf.py | 6 +++--- 5 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 long_description.txt diff --git a/.gitignore b/.gitignore index be2a033..b24b011 100755 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ dist .sdk htmlcov test_deployments +venv diff --git a/PKG-INFO b/PKG-INFO index 87a9f8f..2dfb4e7 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,11 +1,11 @@ -Metadata-Version: 1.1.3 +Metadata-Version: 1.2.1 Name: mazerunner_sdk_python -Version: 1.1.3 +Version: 1.2.1 Summary: MazeRunner python SDK Home-page: https://github.com/Cymmetria/mazerunner_sdk_python Author: Cymmetria License: BSD-3 -Download-URL: https://github.com/Cymmetria/mazerunner_sdk_python/archive/1.1.3.tar.gz +Download-URL: https://github.com/Cymmetria/mazerunner_sdk_python/archive/1.2.1.tar.gz Description: This library implements a convenient client for MazeRunner™ API for Python. Using this library, you will be able to easily configure and manipulate the key features of MazeRunner, such as the creation of a deception campaign, turning decoys on or off, deployment on diff --git a/long_description.txt b/long_description.txt new file mode 100644 index 0000000..0e86d56 --- /dev/null +++ b/long_description.txt @@ -0,0 +1,8 @@ +This library implements a convenient client for MazeRunner™ API for Python. +Using this library, you will be able to easily configure and manipulate the key features +of MazeRunner, such as the creation of a deception campaign, turning decoys on or off, deployment on +remote endpoints, and inspecting alerts with their attached evidence. + +See the documentation at https://community.cymmetria.com/api + +Fork us at https://github.com/Cymmetria/mazerunner_sdk_python \ No newline at end of file diff --git a/setup.py b/setup.py index 168161d..4a34675 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,18 @@ from setuptools import setup, find_packages +with open("long_description.txt", "r") as fh: + long_description = fh.read() + setup( name='mazerunner_sdk', packages=find_packages(), - version='1.1.3', + version='1.2.1', description='MazeRunner SDK', + long_description=long_description, author='Cymmetria', author_email='publicapi@cymmetria.com', url='https://github.com/Cymmetria/mazerunner_sdk_python', - download_url='https://github.com/Cymmetria/mazerunner_sdk_python/tarball/1.1.3', + download_url='https://github.com/Cymmetria/mazerunner_sdk_python/tarball/1.2.1', license='BSD 3-Clause', keywords=['cymmetria', 'mazerunner', 'sdk', 'api'], install_requires=["argparse==1.2.1", diff --git a/source/conf.py b/source/conf.py index 7ad13e5..e90b26d 100644 --- a/source/conf.py +++ b/source/conf.py @@ -68,9 +68,9 @@ # built documents. # # The short X.Y version. -version = u'1.1.3' +version = u'1.2.1' # The full version, including alpha/beta/rc tags. -release = u'1.1.3' +release = u'1.2.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -145,7 +145,7 @@ # The name for this set of Sphinx documents. # " v documentation" by default. # -# html_title = u'MazeRunner SDK v1.1.3' +# html_title = u'MazeRunner SDK v1.2.1' # A shorter title for the navigation bar. Default is the same as html_title. # From 966abf7e246a8ffc88574ce8202656eb4024d70d Mon Sep 17 00:00:00 2001 From: Yochai Blumenfeld Date: Tue, 17 Jul 2018 15:50:42 +0000 Subject: [PATCH 37/38] Fix missing documentation --- .gitignore | 1 + source/api_client.rst | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/.gitignore b/.gitignore index b24b011..f4f8fef 100755 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ dist htmlcov test_deployments venv +.pytest_cache diff --git a/source/api_client.rst b/source/api_client.rst index 8534cc5..35c5442 100644 --- a/source/api_client.rst +++ b/source/api_client.rst @@ -148,3 +148,27 @@ ActiveSOC events :members: :inherited-members: :exclude-members: MODEL_CLASS + + +Audit log events +================ +.. autoclass:: api_client.AuditLogLineCollection + :members: + :inherited-members: + :exclude-members: MODEL_CLASS + + +Storage usage data +================== +.. autoclass:: api_client.StorageUsageData + :members: + :inherited-members: + :exclude-members: MODEL_CLASS + + +Forensic puller on demand +========================= +.. autoclass:: api_client.ForensicPullerOnDemand + :members: + :inherited-members: + :exclude-members: MODEL_CLASS From 8350fa0f0b39b17e9758e1ee1b2371d1d8049d22 Mon Sep 17 00:00:00 2001 From: Frankie Simon Date: Tue, 31 Jul 2018 13:52:47 +0000 Subject: [PATCH 38/38] Fix long description issue + update copyright --- PKG-INFO | 6 +++--- long_description.txt | 8 -------- setup.py | 19 +++++++++++++++---- source/conf.py | 8 ++++---- 4 files changed, 22 insertions(+), 19 deletions(-) delete mode 100644 long_description.txt diff --git a/PKG-INFO b/PKG-INFO index 2dfb4e7..93776a9 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,11 +1,11 @@ -Metadata-Version: 1.2.1 +Metadata-Version: 1.2.3 Name: mazerunner_sdk_python -Version: 1.2.1 +Version: 1.2.3 Summary: MazeRunner python SDK Home-page: https://github.com/Cymmetria/mazerunner_sdk_python Author: Cymmetria License: BSD-3 -Download-URL: https://github.com/Cymmetria/mazerunner_sdk_python/archive/1.2.1.tar.gz +Download-URL: https://github.com/Cymmetria/mazerunner_sdk_python/archive/1.2.3.tar.gz Description: This library implements a convenient client for MazeRunner™ API for Python. Using this library, you will be able to easily configure and manipulate the key features of MazeRunner, such as the creation of a deception campaign, turning decoys on or off, deployment on diff --git a/long_description.txt b/long_description.txt deleted file mode 100644 index 0e86d56..0000000 --- a/long_description.txt +++ /dev/null @@ -1,8 +0,0 @@ -This library implements a convenient client for MazeRunner™ API for Python. -Using this library, you will be able to easily configure and manipulate the key features -of MazeRunner, such as the creation of a deception campaign, turning decoys on or off, deployment on -remote endpoints, and inspecting alerts with their attached evidence. - -See the documentation at https://community.cymmetria.com/api - -Fork us at https://github.com/Cymmetria/mazerunner_sdk_python \ No newline at end of file diff --git a/setup.py b/setup.py index 4a34675..29466d6 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + from setuptools import setup, find_packages -with open("long_description.txt", "r") as fh: - long_description = fh.read() +long_description = """ +This library implements a convenient client for MazeRunner™ API for Python. +Using this library, you will be able to easily configure and manipulate the key features +of MazeRunner, such as the creation of a deception campaign, turning decoys on or off, deployment on +remote endpoints, and inspecting alerts with their attached evidence. + +See the documentation at https://community.cymmetria.com/api + +Fork us at https://github.com/Cymmetria/mazerunner_sdk_python +""" setup( name='mazerunner_sdk', packages=find_packages(), - version='1.2.1', + version='1.2.3', description='MazeRunner SDK', long_description=long_description, author='Cymmetria', author_email='publicapi@cymmetria.com', url='https://github.com/Cymmetria/mazerunner_sdk_python', - download_url='https://github.com/Cymmetria/mazerunner_sdk_python/tarball/1.2.1', + download_url='https://github.com/Cymmetria/mazerunner_sdk_python/tarball/1.2.3', license='BSD 3-Clause', keywords=['cymmetria', 'mazerunner', 'sdk', 'api'], install_requires=["argparse==1.2.1", diff --git a/source/conf.py b/source/conf.py index e90b26d..be91abe 100644 --- a/source/conf.py +++ b/source/conf.py @@ -60,7 +60,7 @@ # General information about the project. project = u'MazeRunner™ SDK' -copyright = u'2015-2017, Cymmetria' +copyright = u'2015-2018, Cymmetria' author = u'Cymmetria' # The version info for the project you're documenting, acts as replacement for @@ -68,9 +68,9 @@ # built documents. # # The short X.Y version. -version = u'1.2.1' +version = u'1.2.3' # The full version, including alpha/beta/rc tags. -release = u'1.2.1' +release = u'1.2.3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -145,7 +145,7 @@ # The name for this set of Sphinx documents. # " v documentation" by default. # -# html_title = u'MazeRunner SDK v1.2.1' +# html_title = u'MazeRunner SDK v1.2.3' # A shorter title for the navigation bar. Default is the same as html_title. #