├── tools └── sdk-tests │ ├── python-sdk-root │ ├── Dockerfile │ └── Makefile ├── requirements.txt ├── Gremlin_Python_Icon.png ├── examples ├── team-enable.md ├── envvars.md ├── README.md ├── clients.md ├── orgs.md ├── users.md ├── apikeys.md ├── alfi.md ├── schedules.md ├── companies.md ├── halts.md ├── saml.md ├── kubernetes.md ├── scenarios.md └── team-enable.py ├── .dockerignore ├── pyproject.toml ├── tests ├── __init__.py ├── test_metadata.py ├── test_containers.py ├── test_halts.py ├── test_contracts.py ├── test_providers.py ├── test_metrics.py ├── test_executions.py ├── test_httpclient.py ├── test_apikeys.py ├── test_all.py ├── test_cli.py ├── test_saml.py ├── test_clients.py ├── pytest_orgs.py ├── test_scenario_helpers.py ├── test_alfi.py ├── test_orgs.py ├── test_templates.py ├── test_reports.py ├── test_kubernetes.py ├── util.py ├── test_attacks.py ├── test_oauth.py ├── test_companies.py ├── test_schedules.py ├── test_gremlinapi.py ├── test_scenarios.py └── test_users.py ├── .bumpversion.cfg ├── CONTRIBUTING.md ├── gremlinapi ├── cli │ └── cli.py ├── containers.py ├── metadata.py ├── halts.py ├── contracts.py ├── executions.py ├── providers.py ├── metrics.py ├── exceptions.py ├── apikeys.py ├── saml.py ├── util.py ├── clients.py ├── alfi.py ├── kubernetes.py ├── orgs.py ├── templates.py ├── reports.py ├── config.py ├── attacks.py ├── cli.py ├── gremlinapi.py ├── reliability_tests.py └── http_clients.py ├── tox.ini ├── .github └── workflows │ ├── pypi-prod.yaml │ ├── pypi-test.yaml │ └── unit-test.yml ├── setup.py ├── Dockerfile ├── Makefile └── .gitignore /tools/sdk-tests/python-sdk-root: -------------------------------------------------------------------------------- 1 | ../.. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3>=1.25.8 2 | requests>=2.22.0 -------------------------------------------------------------------------------- /Gremlin_Python_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gremlin/gremlin-python/HEAD/Gremlin_Python_Icon.png -------------------------------------------------------------------------------- /examples/team-enable.md: -------------------------------------------------------------------------------- 1 | # Enabled / disable all Agents for a Team 2 | 3 | ```shell 4 | python team-enable.py 5 | 6 | ``` 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .git 3 | .cache 4 | .env 5 | examples/ 6 | .idea 7 | .github 8 | Dockerfile 9 | .mypy_cache 10 | tmp/ 11 | .tmp -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 40.9.0", 4 | "setuptools_scm >= 1.15.0", 5 | "setuptools_scm_git_archive >= 1.0", 6 | "wheel", 7 | "gremlinapi", 8 | ] 9 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | log = logging.getLogger("GremlinAPI.unit_tests") 8 | log.setLevel(logging.DEBUG) 9 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.18.3 3 | commit = True 4 | tag = False 5 | sign_tags = True 6 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P\d+))? 7 | serialize = {major}.{minor}.{patch}-{build} 8 | 9 | [bumpversion:file:gremlinapi/util.py] 10 | -------------------------------------------------------------------------------- /examples/envvars.md: -------------------------------------------------------------------------------- 1 | # Environment Variables 2 | 3 | The Gremlin Python SDK Supports the following environment variables 4 | 5 | ```shell 6 | 7 | GREMLIN_API_KEY 8 | GREMLIN_BEARER_TOKEN 9 | GREMLIN_COMPANY 10 | GREMLIN_MAX_BEARER_INTERVAL # Default = 86400 11 | GREMLIN_PASSWORD 12 | GREMLIN_PYTHON_API_LOG_LEVEL # Default = WARNING 13 | GREMLIN_TEAM_ID 14 | GREMLIN_USER 15 | GREMLIN_USER_MFA_TOKEN 16 | HTTP_PROXY 17 | HTTPS_PROXY 18 | ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | # Package Version 4 | 5 | Package version control conforms to semantic versioning. 6 | 7 | This package uses [bumpversion](https://github.com/peritus/bumpversion) to manage the version 8 | 9 | Quick usage: 10 | 11 | Major version update: 12 | ```bash 13 | bumpversion --new-version 1.0.0 major 14 | ``` 15 | 16 | Minor version update: 17 | ```bash 18 | bumpversion --new-version 0.4.0 minor 19 | ``` 20 | 21 | Patch version update: 22 | ```bash 23 | bumpversion --new-version 0.3.13 patch 24 | ``` 25 | -------------------------------------------------------------------------------- /gremlinapi/cli/cli.py: -------------------------------------------------------------------------------- 1 | import gremlinapi 2 | from gremlinapi.exceptions import GremlinAuthError 3 | 4 | 5 | class GremlinCLI(object): 6 | def __init__(self): 7 | super.__init__(self) 8 | 9 | def __call__(self): 10 | pass 11 | 12 | def do_action(self): 13 | pass 14 | 15 | def extend_parser(parser): 16 | subparsers = parser.add_subparsers( 17 | title="object", dest="what", help="Object to manipulate." 18 | ) 19 | subparsers.required = True 20 | classes = [] 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = python 3 | minversion = 3.4.0 4 | requires = 5 | setuptools >= 40.9.0 6 | pip >= 19.0.3 7 | isolated_build = true 8 | 9 | [testenv:build-dists] 10 | basepython = python3 11 | isolated_build = true 12 | usedevelop = false 13 | skip_install = true 14 | ignore_outcome=true 15 | deps = 16 | urllib3 >= 1.25.8 17 | requests >= 2.22.0 18 | setenv = 19 | PYPI_UPLOAD = true 20 | commands = 21 | rm -rfv {toxinidir}/dist/ 22 | {envpython} setup.py sdist bdist_wheel 23 | whitelist_externals = 24 | rm 25 | allowlist_externals = 26 | rm -------------------------------------------------------------------------------- /tests/test_metadata.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.metadata import GremlinAPIMetadata 6 | 7 | from .util import mock_json, mock_data 8 | 9 | 10 | class TestMetadata(unittest.TestCase): 11 | @patch("requests.get") 12 | def test_get_metadata_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual(GremlinAPIMetadata.get_metadata(), mock_data) 17 | -------------------------------------------------------------------------------- /tools/sdk-tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gremlin/gremlin-python-api:latest 2 | LABEL maintainer="kyle@gremlin.com" 3 | 4 | # Runtime arguments 5 | ARG BUILD_DATE 6 | ARG IMAGE_NAME 7 | ARG APP_DIR=/opt/gremlin-python 8 | ARG SRC_DIR=. 9 | 10 | # Container Labels 11 | LABEL org.label-schema.schema-version="1.0" 12 | LABEL org.label-schema.build-date=$BUILD_DATE 13 | LABEL org.label-schema.name=$IMAGE_NAME 14 | LABEL org.label-schema.version=$BUILD_VERSION 15 | 16 | WORKDIR ${APP_DIR} 17 | COPY ${SRC_DIR} . 18 | #RUN pip3 install urllib3 19 | #RUN python3 setup.py install 20 | ENTRYPOINT ["/bin/ash"] 21 | CMD ["/bin/ash"] -------------------------------------------------------------------------------- /tests/test_containers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.containers import GremlinAPIContainers 6 | 7 | from .util import mock_json, mock_data 8 | 9 | 10 | class TestContainers(unittest.TestCase): 11 | @patch("requests.get") 12 | def test_list_containers_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual(GremlinAPIContainers.list_containers(), mock_data) 17 | -------------------------------------------------------------------------------- /tests/test_halts.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.halts import GremlinAPIHalts 6 | 7 | from .util import mock_body, mock_data, mock_json 8 | 9 | 10 | class TestHalts(unittest.TestCase): 11 | @patch("requests.post") 12 | def test_halt_all_attacks_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual(GremlinAPIHalts.halt_all_attacks(**mock_body), mock_data) 17 | -------------------------------------------------------------------------------- /tests/test_contracts.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.contracts import GremlinAPIContracts 6 | 7 | from .util import mock_identifier, mock_json, mock_data 8 | 9 | 10 | class TestContracts(unittest.TestCase): 11 | @patch("requests.patch") 12 | def test_update_contract_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual( 17 | GremlinAPIContracts.update_contract(**mock_identifier), mock_data 18 | ) 19 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | See individual .md files 4 | 5 | [ALFI](alfi.md) 6 | 7 | [API Keys](apikeys.md) 8 | 9 | [Attacks](attacks.md) 10 | 11 | [Clients](clients.md) 12 | 13 | [Companies](companies.md) 14 | 15 | [Environment Variables](envvars.md) 16 | 17 | [Halts](halts.md) 18 | 19 | [Kubernetes](kubernetes.md) 20 | 21 | [SAML Authentication](saml.md) 22 | 23 | [Scenarios](scenarios.md) 24 | 25 | [Schedules](schedules.md) 26 | 27 | [Enable / disable all Agents for a Team](team-enable.md) 28 | 29 | [Users](users.md) 30 | 31 | 32 | 33 | ##### Disclaimer 34 | Examples are not complete implementation of code. Please use these as a springboard to understand 35 | the methods this library exposes. 36 | 37 | -------------------------------------------------------------------------------- /tests/test_providers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.providers import GremlinAPIProviders 6 | 7 | from .util import mock_json, mock_data 8 | 9 | 10 | class TestProviders(unittest.TestCase): 11 | @patch("requests.get") 12 | def test_list_providers_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual(GremlinAPIProviders.list_providers(), mock_data) 17 | 18 | @patch("requests.get") 19 | def test_list_aws_services_with_decorator(self, mock_get) -> None: 20 | mock_get.return_value = requests.Response() 21 | mock_get.return_value.status_code = 200 22 | mock_get.return_value.json = mock_json 23 | self.assertEqual(GremlinAPIProviders.list_aws_services(), mock_data) 24 | -------------------------------------------------------------------------------- /examples/clients.md: -------------------------------------------------------------------------------- 1 | # Clients Endpoint Examples 2 | 3 | ### Activate Client 4 | 5 | ```python 6 | from gremlinapi.clients import GremlinAPIClients as clients 7 | team_id = 'TEAM_ID' 8 | client_guid = 'CLIENT_GUID' 9 | 10 | response = clients.activate_client(guid=client_guid, teamId=team_id) 11 | ``` 12 | 13 | ### Deactivate Client 14 | 15 | ```python 16 | from gremlinapi.clients import GremlinAPIClients as clients 17 | team_id = 'TEAM_ID' 18 | client_guid = 'CLIENT_GUID' 19 | 20 | response = clients.deactivate_client(guid=client_guid, teamId=team_id) 21 | ``` 22 | 23 | ### List Clients 24 | 25 | ```python 26 | from gremlinapi.clients import GremlinAPIClients as clients 27 | team_id = 'TEAM_ID' 28 | 29 | client_list = clients.list_clients(teamId=team_id) 30 | ``` 31 | 32 | ### List Active Clients 33 | 34 | ```python 35 | from gremlinapi.clients import GremlinAPIClients as clients 36 | team_id = 'TEAM_ID' 37 | 38 | client_list = clients.list_active_clients(teamId=team_id) 39 | ``` 40 | -------------------------------------------------------------------------------- /examples/orgs.md: -------------------------------------------------------------------------------- 1 | # Organizations 2 | 3 | ### Create Team 4 | ```python 5 | from gremlinapi.orgs import GremlinAPIOrgs as orgs 6 | from gremlinapi.config import GremlinAPIConfig as config 7 | 8 | config.bearer_token = 'BEARER_TOKEN' 9 | 10 | new_team_name = 'Mohawks_Gremlins' 11 | details = orgs.create_org(name=new_team_name) 12 | ``` 13 | 14 | ### List Teams 15 | ```python 16 | from gremlinapi.orgs import GremlinAPIOrgs as orgs 17 | 18 | org_list = orgs.list_orgs() 19 | ``` 20 | 21 | ### Get Team Details 22 | ```python 23 | from gremlinapi.orgs import GremlinAPIOrgs as orgs 24 | 25 | team_id = 'TEAM ID/UUID' 26 | 27 | team_details = orgs.get_org(identifier=team_id) 28 | ``` 29 | 30 | ### Create New Certificates for Team Agents 31 | ```python 32 | from gremlinapi.orgs import GremlinAPIOrgs as orgs 33 | from gremlinapi.config import GremlinAPIConfig as config 34 | 35 | config.bearer_token = 'BEARER_TOKEN' 36 | team_id = 'TEAM ID/UUID' 37 | 38 | new_certs = orgs.new_certificate(teamId=team_id) 39 | ``` -------------------------------------------------------------------------------- /examples/users.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | ### List Users 4 | 5 | ```python 6 | from gremlinapi.users import GremlinAPIUsers as users 7 | 8 | team_id = 'Team ID/UUID' 9 | 10 | user_list = users.list_users(teamId=team_id) 11 | ``` 12 | 13 | ### Invite User 14 | ```python 15 | from gremlinapi.users import GremlinAPIUsers as users 16 | 17 | user_email = 'gizmo@gremlin.com' 18 | 19 | confirmation = users.invite_user(email=user_email) 20 | ``` 21 | 22 | ### Deactivate User 23 | ```python 24 | from gremlinapi.users import GremlinAPIUsers as users 25 | from gremlinapi.config import GremlinAPIConfig as config 26 | 27 | config.bearer_token = 'BEARER TOKEN' 28 | team_id = 'Team ID/UUID' 29 | user_email = 'brain@gremlin.com' 30 | 31 | confirmation = users.deactivate_user(email=user_email, teamId=team_id) 32 | ``` 33 | 34 | ### Revoke Invite 35 | ```python 36 | from gremlinapi.users import GremlinAPIUsers as users 37 | 38 | user_email = 'mohawk@gremlin.com' 39 | 40 | confirmation = users.revoke_user_invite(email=user_email) 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /tests/test_metrics.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.metrics import GremlinAPIMetrics 6 | 7 | from .util import mock_json, mock_data, mock_metrics 8 | 9 | 10 | class TestMetrics(unittest.TestCase): 11 | @patch("requests.get") 12 | def test_get_attack_metrics_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual( 17 | GremlinAPIMetrics.get_attack_metrics(**mock_metrics), mock_data 18 | ) 19 | 20 | @patch("requests.get") 21 | def test_get_scenario_run_metrics_with_decorator(self, mock_get) -> None: 22 | mock_get.return_value = requests.Response() 23 | mock_get.return_value.status_code = 200 24 | mock_get.return_value.json = mock_json 25 | self.assertEqual( 26 | GremlinAPIMetrics.get_scenario_run_metrics(**mock_metrics), mock_data 27 | ) 28 | -------------------------------------------------------------------------------- /tests/test_executions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.executions import GremlinAPIExecutions 6 | 7 | from .util import mock_json, mock_data 8 | 9 | 10 | class TestExecutions(unittest.TestCase): 11 | def test__optional_taskid_endpoint(self) -> None: 12 | test_endpoint = "test-endpoint.com" 13 | test_kwargs = {"taskId": "134567890"} 14 | expected_output = "%s/?taskId=%s" % (test_endpoint, test_kwargs["taskId"]) 15 | self.assertEqual( 16 | GremlinAPIExecutions._optional_taskid_endpoint( 17 | test_endpoint, **test_kwargs 18 | ), 19 | expected_output, 20 | ) 21 | 22 | @patch("requests.get") 23 | def test_list_executions_with_decorator(self, mock_get) -> None: 24 | mock_get.return_value = requests.Response() 25 | mock_get.return_value.status_code = 200 26 | mock_get.return_value.json = mock_json 27 | self.assertEqual(GremlinAPIExecutions.list_executions(), mock_data) 28 | -------------------------------------------------------------------------------- /.github/workflows/pypi-prod.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPi-Prod 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish Python 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | max-parallel: 1 14 | matrix: 15 | python-version: 16 | - 3.8 17 | os: 18 | - ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@master 21 | - name: Setup Python 22 | uses: actions/setup-python@v1 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install tox 26 | run: | 27 | python -m pip install --upgrade tox 28 | - name: Initialize tox env 29 | run: | 30 | python -m tox -e build-dists --parallel auto --notest 31 | - name: Build dists 32 | run: | 33 | python -m tox -e build-dists --parallel 0 34 | - name: Publish 📦 to Prod PyPI 35 | uses: pypa/gh-action-pypi-publish@master 36 | with: 37 | username: __token__ 38 | password: ${{ secrets.Prod_PyPI_token }} -------------------------------------------------------------------------------- /.github/workflows/pypi-test.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPi-Test 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - main 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish Python 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | max-parallel: 1 14 | matrix: 15 | python-version: 16 | - 3.8 17 | os: 18 | - ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@master 21 | - name: Setup Python 22 | uses: actions/setup-python@v1 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install tox 26 | run: | 27 | python -m pip install --upgrade tox 28 | - name: Initialize tox env 29 | run: | 30 | python -m tox -e build-dists --parallel auto --notest 31 | - name: Build dists 32 | run: | 33 | python -m tox -e build-dists --parallel 0 34 | - name: Publish 📦 to Test PyPI 35 | uses: pypa/gh-action-pypi-publish@master 36 | with: 37 | username: __token__ 38 | password: ${{ secrets.Test_PyPI_token }} 39 | repository_url: https://test.pypi.org/legacy/ -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Run and Validate Unit Tests 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - main 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | name: Execute Unit Tests 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | max-parallel: 1 17 | matrix: 18 | python-version: 19 | - 3.8 20 | os: 21 | - ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | - uses: actions/checkout@master 26 | - name: Setup Python 27 | uses: actions/setup-python@v1 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install modules 31 | run: | 32 | python -m pip install --upgrade requests 33 | - name: Install pytest module 34 | run: | 35 | python -m pip install pytest pytest-mock responses 36 | - name: Executes Unit Tests 37 | run: python -m tests.test_all 38 | - name: Execute pytests 39 | run: python -m pytest tests/pytest_*.py 40 | 41 | 42 | -------------------------------------------------------------------------------- /gremlinapi/containers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.exceptions import ( 9 | GremlinParameterError, 10 | ProxyError, 11 | ClientError, 12 | HTTPTimeout, 13 | HTTPError, 14 | ) 15 | 16 | from typing import Type 17 | 18 | from gremlinapi.gremlinapi import GremlinAPI 19 | from gremlinapi.http_clients import get_gremlin_httpclient, GremlinAPIHttpClient 20 | 21 | 22 | log = logging.getLogger("GremlinAPI.client") 23 | 24 | 25 | class GremlinAPIContainers(GremlinAPI): 26 | @classmethod 27 | @register_cli_action("list_containers", ("",), ("teamId",)) 28 | def list_containers( 29 | cls, 30 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 31 | **kwargs, 32 | ) -> dict: 33 | method: str = "GET" 34 | endpoint: str = cls._optional_team_endpoint(f"/containers", **kwargs) 35 | payload: dict = cls._payload(**{"headers": https_client.header()}) 36 | (resp, body) = https_client.api_call(method, endpoint, **payload) 37 | return body 38 | -------------------------------------------------------------------------------- /examples/apikeys.md: -------------------------------------------------------------------------------- 1 | # apikeys endpoint 2 | 3 | 4 | #### Create API Key 5 | 6 | Since you're creating an API key, it's likely you don't have an existing one to use. 7 | Using environment variables is an easy way to provide authentication to the library. 8 | ```bash 9 | #!/bin/bash 10 | export GREMLIN_USER=my-gremlin-username 11 | export GREMLIN_PASSWORD=my-gremlin-password 12 | export GREMLIN_TEAM_ID=my-gremlin-teamid 13 | ``` 14 | 15 | And the python to create the api-key: 16 | ```python 17 | import gremlinapi 18 | from gremlinapi.apikeys import GremlinAPIapikeys as apikeys 19 | 20 | gremlinapi.login() 21 | key_string = apikeys.create_apikey(identifier='my-first-apikey', description='This is our first API key') 22 | print(f'Set the environment variable GREMLIN_API_KEY to {key_string}') 23 | ``` 24 | 25 | 26 | #### List API Keys 27 | 28 | ```python 29 | from gremlinapi.apikeys import GremlinAPIapikeys as apikeys 30 | from pprint import pprint 31 | keys = apikeys.list_apikeys() 32 | pprint(keys) 33 | ``` 34 | 35 | #### Revoke API Keys 36 | 37 | ```python 38 | from gremlinapi.apikeys import GremlinAPIapikeys as apikeys 39 | from pprint import pprint 40 | resp = apikeys.revoke_apikey(identifier='my-api-key-identifier') 41 | pprint(resp) 42 | ``` -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | from distutils.file_util import copy_file 4 | from setuptools import setup, find_packages 5 | 6 | from gremlinapi.util import get_version 7 | 8 | 9 | __version__ = get_version() 10 | 11 | 12 | def getRequires(): 13 | deps = ["requests>=2.22.0", "urllib3>=1.25.8"] 14 | return deps 15 | 16 | 17 | dir_path = os.path.abspath(os.path.dirname(__file__)) 18 | readme = io.open(os.path.join(dir_path, "README.md"), encoding="utf-8").read() 19 | 20 | setup( 21 | name="gremlinapi", 22 | version=str(__version__), 23 | author="Kyle Hultman", 24 | author_email="kyle@gremlin.com", 25 | url="https://github.com/gremlin/gremlin-python/", 26 | packages=find_packages(exclude=["temp*.py", "test"]), 27 | include_package_data=True, 28 | license="Apache 2.0", 29 | description="Gremlin library for Python", 30 | long_description=readme, 31 | long_description_content_type="text/markdown", 32 | install_requires=getRequires(), 33 | python_requires=">=3.7", 34 | entry_points={"console_scripts": ["pgremlin = gremlinapi.cli:main"]}, 35 | classifiers=[ 36 | "Programming Language :: Python :: 3.7", 37 | "Programming Language :: Python :: 3.8", 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /gremlinapi/metadata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.exceptions import ( 9 | GremlinParameterError, 10 | ProxyError, 11 | ClientError, 12 | HTTPTimeout, 13 | HTTPError, 14 | ) 15 | 16 | from gremlinapi.gremlinapi import GremlinAPI 17 | from gremlinapi.http_clients import ( 18 | get_gremlin_httpclient, 19 | GremlinAPIHttpClient, 20 | ) 21 | 22 | from typing import Union, Type 23 | 24 | log = logging.getLogger("GremlinAPI.client") 25 | 26 | 27 | class GremlinAPIMetadata(GremlinAPI): 28 | @classmethod 29 | @register_cli_action("get_metadata", ("",), ("teamId",)) 30 | def get_metadata( 31 | cls, 32 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 33 | *args: tuple, 34 | **kwargs: dict, 35 | ) -> dict: 36 | method: str = "GET" 37 | endpoint: str = cls._optional_team_endpoint("/metadata", **kwargs) 38 | payload: dict = cls._payload(**{"headers": https_client.header()}) 39 | (resp, body) = https_client.api_call(method, endpoint, **payload) 40 | return body 41 | -------------------------------------------------------------------------------- /examples/alfi.md: -------------------------------------------------------------------------------- 1 | # Alfi Examples 2 | 3 | ### Create ALFI Experiments 4 | 5 | Coming Soon 6 | 7 | ### Get ALFI Experiment Details 8 | 9 | ```python 10 | from gremlinapi.alfi import GremlinALFI as alfi 11 | from pprint import pprint 12 | 13 | alfi_attack_id = 'expirment-guid' 14 | experiment_details = alfi.get_alfi_experiment_details(guid=alfi_attack_id) 15 | 16 | pprint(experiment_details) 17 | ``` 18 | 19 | ### Halt ALFI Experiment 20 | 21 | ```python 22 | from gremlinapi.alfi import GremlinALFI as alfi 23 | 24 | alfi_attack_id = 'expirment-guid' 25 | alfi.halt_alfi_experiment(guid=alfi_attack_id) 26 | ``` 27 | 28 | ### Halt _ALL_ ALFI Experiments 29 | 30 | ```python 31 | from gremlinapi.alfi import GremlinALFI as alfi 32 | 33 | alfi.halt_all_alfi_experiments() 34 | ``` 35 | 36 | ### List Active ALFI Experiments 37 | 38 | ```python 39 | from gremlinapi.alfi import GremlinALFI as alfi 40 | from pprint import pprint 41 | 42 | experiment_list = alfi.list_active_alfi_experiments() 43 | 44 | pprint(experiment_list) 45 | ``` 46 | 47 | ### List Completed ALFI Experiments 48 | 49 | ```python 50 | from gremlinapi.alfi import GremlinALFI as alfi 51 | from pprint import pprint 52 | 53 | experiment_list = alfi.list_completed_alfi_experiments() 54 | 55 | pprint(experiment_list) 56 | ``` 57 | -------------------------------------------------------------------------------- /tools/sdk-tests/Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_OWNER=gremlin 2 | DOCKER_NAME=gremlin-python-api-test 3 | DOCKER_IMAGE=$(DOCKER_OWNER)/$(DOCKER_NAME) 4 | BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') 5 | ENV_VARS=.env 6 | IA_SRC_DIR=$(PWD)/../.. 7 | APP_SRC_DIR=python-sdk-root/ 8 | APP_DST_DIR=/opt/gremlin-python 9 | TSV=$(shell date -u '+%Y%j.%H') 10 | 11 | all: docker-build 12 | 13 | docker-build: 14 | docker build --no-cache=true \ 15 | --build-arg BUILD_DATE=$(BUILD_DATE) \ 16 | --build-arg IMAGE_NAME=$(DOCKER_IMAGE) \ 17 | --build-arg APP_DIR=$(APP_DST_DIR) \ 18 | --build-arg SRC_DIR=$(APP_SRC_DIR) \ 19 | -t $(DOCKER_IMAGE):$(TSV) \ 20 | -t $(DOCKER_IMAGE):latest \ 21 | . 22 | 23 | docker-run: 24 | @if ! test -f $(ENV_VARS); then touch $(ENV_VARS); fi 25 | docker run --rm -v ~/.ssh/:/root/.ssh --mount type=bind,source="$(PWD)",target=/opt/gremlin-python \ 26 | --env-file=$(ENV_VARS) --name $(DOCKER_NAME) --entrypoint "/bin/ash" $(DOCKER_IMAGE):latest 27 | 28 | docker-run-interactive: 29 | @if ! test -f $(ENV_VARS); then touch $(ENV_VARS); fi 30 | docker run -it --rm \ 31 | -v $(IA_SRC_DIR):$(APP_DST_DIR) \ 32 | --env-file=$(ENV_VARS) --name $(DOCKER_NAME) --entrypoint "/bin/ash" $(DOCKER_IMAGE):latest -------------------------------------------------------------------------------- /gremlinapi/halts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.exceptions import ( 9 | GremlinParameterError, 10 | ProxyError, 11 | ClientError, 12 | HTTPTimeout, 13 | HTTPError, 14 | ) 15 | 16 | from gremlinapi.gremlinapi import GremlinAPI 17 | from gremlinapi.http_clients import ( 18 | get_gremlin_httpclient, 19 | GremlinAPIHttpClient, 20 | ) 21 | 22 | from typing import Union, Type 23 | 24 | log = logging.getLogger("GremlinAPI.client") 25 | 26 | 27 | class GremlinAPIHalts(GremlinAPI): 28 | @classmethod 29 | @register_cli_action("halt_all_attacks", ("",), ("teamId", "body")) 30 | def halt_all_attacks( 31 | cls, 32 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 33 | **kwargs: dict 34 | ) -> dict: 35 | method: str = "POST" 36 | data: dict = cls._warn_if_not_json_body(**kwargs) 37 | endpoint: str = cls._optional_team_endpoint("/halts", **kwargs) 38 | payload: dict = cls._payload(**{"headers": https_client.header(), "body": data}) 39 | (resp, body) = https_client.api_call(method, endpoint, **payload) 40 | return body 41 | -------------------------------------------------------------------------------- /examples/schedules.md: -------------------------------------------------------------------------------- 1 | # Schedules 2 | 3 | 4 | ### List active schedules 5 | ```python 6 | from gremlinapi.schedules import GremlinAPISchedules as schedules 7 | 8 | schedule_list = schedules.list_active_schedules() 9 | ``` 10 | 11 | ### List attack schedules 12 | ```python 13 | from gremlinapi.schedules import GremlinAPISchedules as schedules 14 | 15 | schedule_list = schedules.list_attack_schedules() 16 | ``` 17 | 18 | ### List Scenario schedules 19 | ```python 20 | from gremlinapi.schedules import GremlinAPISchedules as schedules 21 | 22 | schedule_list = schedules.list_scenario_schedules() 23 | ``` 24 | 25 | 26 | ### Create Attack Schedule 27 | ```python 28 | from gremlinapi.schedules import GremlinAPISchedules as schedules 29 | 30 | team_id = 'TEAM ID/UUID' 31 | 32 | schedule_body = { 33 | 'command': { 34 | 'type': 'cpu', 35 | 'args': ['-l', '60', '-c', '1'] 36 | }, 37 | 'target': { 38 | 'exact': 1, 39 | 'type': 'Random' 40 | }, 41 | 'trigger': { 42 | 'activeDays': ['M', 'T', 'W', 'Th', 'F'], 43 | 'start': '23:00', 44 | 'end': '23:30', 45 | 'timeZone': 'America/New_York', 46 | 'type': 'Random', 47 | 'maxRuns': 1 48 | } 49 | } 50 | 51 | confirmation = schedules.create_attack_schedule(body=schedule_body, teamId=team_id) 52 | ``` -------------------------------------------------------------------------------- /tests/test_httpclient.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import os 6 | import sys 7 | import unittest 8 | 9 | from gremlinapi.config import GremlinAPIConfig as config 10 | from gremlinapi.http_clients import get_gremlin_httpclient, GremlinAPIHttpClient 11 | 12 | from .util import api_key, bearer_token 13 | 14 | parentPath: str = os.path.abspath("..") 15 | if parentPath not in sys.path: 16 | sys.path.insert(0, parentPath) 17 | 18 | 19 | class TestHttpClient(unittest.TestCase): 20 | def test_api_key(self) -> None: 21 | config.api_key = api_key 22 | https_client: GremlinAPIHttpClient = get_gremlin_httpclient() 23 | header: dict = https_client.header() 24 | self.assertIn(api_key, header["Authorization"]) 25 | 26 | def test_bearer_token(self) -> None: 27 | config.bearer_token = bearer_token 28 | https_client: GremlinAPIHttpClient = get_gremlin_httpclient() 29 | header: dict = https_client.header() 30 | self.assertIn(bearer_token, header["Authorization"]) 31 | 32 | def test_base_uri(self) -> None: 33 | https_client: GremlinAPIHttpClient = get_gremlin_httpclient() 34 | t_uri: str = "test" 35 | self.assertEqual(f"{config.base_uri}{t_uri}", https_client.base_uri(t_uri)) 36 | -------------------------------------------------------------------------------- /gremlinapi/contracts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.exceptions import ( 9 | GremlinParameterError, 10 | ProxyError, 11 | ClientError, 12 | HTTPTimeout, 13 | HTTPError, 14 | ) 15 | 16 | from typing import Type 17 | 18 | from gremlinapi.gremlinapi import GremlinAPI 19 | from gremlinapi.http_clients import get_gremlin_httpclient, GremlinAPIHttpClient 20 | 21 | 22 | log = logging.getLogger("GremlinAPI.client") 23 | 24 | 25 | class GremlinAPIContracts(GremlinAPI): 26 | @classmethod 27 | @register_cli_action("update_contract", ("identifier", "body"), ("",)) 28 | def update_contract( 29 | cls, 30 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 31 | **kwargs, 32 | ) -> dict: 33 | method: str = "PATCH" 34 | identifier: str = cls._error_if_not_param("identifier", **kwargs) 35 | data: dict = cls._error_if_not_json_body(**kwargs) 36 | endpoint: str = f"/companies/{identifier}/contracts/current" 37 | payload: dict = cls._payload(**{"headers": https_client.header(), "body": data}) 38 | (resp, body) = https_client.api_call(method, endpoint, **payload) 39 | return body 40 | -------------------------------------------------------------------------------- /tests/test_apikeys.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.apikeys import GremlinAPIapikeys 6 | 7 | from .util import mock_json, mock_data 8 | 9 | 10 | class TestAPIKeys(unittest.TestCase): 11 | @patch("requests.post") 12 | def test_create_apikey_with_decorator(self, mock_get) -> None: 13 | test_kwargs = {"description": "123456790", "identifier": "1234567890"} 14 | mock_get.return_value = requests.Response() 15 | mock_get.return_value.status_code = 200 16 | mock_get.return_value.json = mock_json 17 | self.assertEqual(GremlinAPIapikeys.create_apikey(**test_kwargs), mock_data) 18 | 19 | @patch("requests.get") 20 | def test_list_apikeys_with_decorator(self, mock_get) -> None: 21 | mock_get.return_value = requests.Response() 22 | mock_get.return_value.status_code = 200 23 | mock_get.return_value.json = mock_json 24 | self.assertEqual(GremlinAPIapikeys.list_apikeys(), mock_data) 25 | 26 | @patch("requests.delete") 27 | def test_revoke_apikey_with_decorator(self, mock_get) -> None: 28 | test_kwargs = {"description": "123456790", "identifier": "1234567890"} 29 | mock_get.return_value = requests.Response() 30 | mock_get.return_value.status_code = 200 31 | mock_get.return_value.json = mock_json 32 | self.assertEqual(GremlinAPIapikeys.revoke_apikey(**test_kwargs), mock_data) 33 | -------------------------------------------------------------------------------- /tests/test_all.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (C) 2021 Kyle Bouchard , Gremlin Inc 4 | 5 | import unittest 6 | 7 | from .test_httpclient import TestHttpClient 8 | from .test_attacks import TestAttacks 9 | from .test_alfi import TestAlfi 10 | from .test_apikeys import TestAPIKeys 11 | from .test_attack_helpers import TestAttackHelpers 12 | from .test_cli import TestCLI 13 | from .test_clients import TestClients 14 | from .test_companies import TestCompanies 15 | from .test_containers import TestContainers 16 | from .test_contracts import TestContracts 17 | from .test_executions import TestExecutions 18 | from .test_gremlinapi import TestAPI 19 | from .test_halts import TestHalts 20 | from .test_kubernetes import TestKubernetesAttacks, TestKubernetesTargets 21 | from .test_metadata import TestMetadata 22 | from .test_metrics import TestMetrics 23 | from .test_orgs import TestOrgs 24 | from .test_oauth import TestOAUTH 25 | from .test_providers import TestProviders 26 | from .test_reports import TestReports 27 | from .test_saml import TestSaml 28 | from .test_scenario_graph_helpers import TestScenarioGraphHelpers 29 | from .test_scenarios import TestScenarios 30 | from .test_schedules import TestSchedules 31 | from .test_users import TestUsers 32 | 33 | # from .test_scenario_helpers import TestScenarioHelpers 34 | # from .test_templates import TestTemplates 35 | 36 | 37 | if __name__ == "__main__": 38 | unittest.main() 39 | -------------------------------------------------------------------------------- /gremlinapi/executions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.exceptions import ( 9 | GremlinParameterError, 10 | ProxyError, 11 | ClientError, 12 | HTTPTimeout, 13 | HTTPError, 14 | ) 15 | 16 | from typing import Type 17 | 18 | from gremlinapi.gremlinapi import GremlinAPI 19 | from gremlinapi.http_clients import get_gremlin_httpclient, GremlinAPIHttpClient 20 | 21 | 22 | log = logging.getLogger("GremlinAPI.client") 23 | 24 | 25 | class GremlinAPIExecutions(GremlinAPI): 26 | @classmethod 27 | def _optional_taskid_endpoint(cls, endpoint, **kwargs) -> str: 28 | task_id: str = cls._info_if_not_param("taskId", **kwargs) 29 | if task_id: 30 | endpoint += f"/?taskId={task_id}" 31 | return cls._optional_team_endpoint(endpoint, **kwargs) 32 | 33 | @classmethod 34 | @register_cli_action("list_executions", ("",), ("taskId", "teamId")) 35 | def list_executions( 36 | cls, 37 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 38 | **kwargs, 39 | ) -> dict: 40 | method: str = "GET" 41 | endpoint: str = cls._optional_taskid_endpoint("/executions", **kwargs) 42 | payload: dict = cls._payload(**{"headers": https_client.header()}) 43 | (resp, body) = https_client.api_call(method, endpoint, **payload) 44 | return body 45 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | import gremlinapi.cli as cli 6 | 7 | # from gremlinapi.attacks import GremlinAPIAttacks 8 | # from gremlinapi.attack_helpers import GremlinAttackHelper, GremlinTargetContainers, GremlinLatencyAttack 9 | 10 | from .util import mock_json, mock_data 11 | 12 | 13 | class TestCLI(unittest.TestCase): 14 | def test_register_cli_action(self) -> None: 15 | pass 16 | 17 | def test__base_args(self) -> None: 18 | pass 19 | 20 | def test__get_parser(self) -> None: 21 | pass 22 | 23 | def test__parse_args(self) -> None: 24 | pass 25 | 26 | # def test_list_endpoint(self) -> None: 27 | # test_endpoint = "test-endpoint.com" 28 | # expected_output = "%s/?source=scenario&pageSize=3&" % test_endpoint 29 | # test_kwargs = {"source":"scenario","pageSize":3} 30 | # test_output = GremlinAPIAttacks._list_endpoint(test_endpoint,**test_kwargs) 31 | # self.assertEqual(test_output, expected_output) 32 | # @patch('requests.post') 33 | # def test_create_attack_with_decorator(self, mock_get) -> None: 34 | # expected_output_class = GremlinAttackHelper() 35 | # test_kwargs = {"body":expected_output_class} 36 | # mock_get.return_value = requests.Response() 37 | # mock_get.return_value.status_code = 200 38 | # mock_get.return_value.json = mock_json 39 | # self.assertEqual(GremlinAPIAttacks.create_attack(**test_kwargs), mock_data) 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | LABEL maintainer="kyle@gremlin.com" 3 | 4 | # Runtime arguments 5 | ARG BUILD_DATE 6 | ARG IMAGE_NAME 7 | ARG AWS_IAMAUTH_DOWNLOAD="https://amazon-eks.s3-us-west-2.amazonaws.com/1.14.6/2019-08-22/bin/linux/amd64/aws-iam-authenticator" 8 | ARG GREMLIN_PYTHON_REPO="https://github.com/gremlin/gremlin-python.git" 9 | 10 | # Container Labels 11 | LABEL org.label-schema.schema-version="1.0" 12 | LABEL org.label-schema.build-date=$BUILD_DATE 13 | LABEL org.label-schema.name=$IMAGE_NAME 14 | LABEL org.label-schema.version=$BUILD_VERSION 15 | 16 | RUN apk add --no-cache --update bash ca-certificates cargo curl gcc git go gzip \ 17 | libffi-dev make musl-dev openssh openssl openssl-dev python3 python3-dev rust swig tar 18 | 19 | RUN python3 -m ensurepip && \ 20 | rm -r /usr/lib/python*/ensurepip && \ 21 | pip3 install --upgrade pip setuptools twine wheel && \ 22 | if [ ! -e /usr/bin/pip ]; then ln -s pip3 /usr/bin/pip ; fi && \ 23 | if [[ ! -e /usr/bin/python ]]; then ln -sf /usr/bin/python3 /usr/bin/python; fi && \ 24 | rm -r /root/.cache 25 | 26 | RUN pip --no-cache-dir install --upgrade awscli boto3 requests 27 | 28 | RUN curl -s -L $AWS_IAMAUTH_DOWNLOAD -o /usr/local/bin/aws-iam-authenticator && chmod 755 /usr/local/bin/aws-iam-authenticator 29 | 30 | RUN mkdir -p /opt/gremlin-python 31 | 32 | WORKDIR /opt/gremlin-python 33 | 34 | # RUN git clone $GREMLIN_PYTHON_REPO . 35 | COPY . . 36 | 37 | # uncomment to install the package container wide 38 | # RUN python3 setup.py install 39 | 40 | ENTRYPOINT ["/bin/ash"] 41 | CMD ["/bin/ash"] -------------------------------------------------------------------------------- /tests/test_saml.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.saml import GremlinAPISaml 6 | 7 | from .util import mock_json, mock_data, mock_saml 8 | 9 | 10 | class TestSaml(unittest.TestCase): 11 | @patch("requests.post") 12 | def test_acs_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual(GremlinAPISaml.acs(**mock_saml), mock_get.return_value) 17 | 18 | @patch("requests.get") 19 | def test_samllogin_with_decorator(self, mock_get) -> None: 20 | mock_get.return_value = requests.Response() 21 | mock_get.return_value.status_code = 200 22 | mock_get.return_value.json = mock_json 23 | self.assertEqual(GremlinAPISaml.samllogin(**mock_saml), mock_data) 24 | 25 | @patch("requests.get") 26 | def test_metadata_with_decorator(self, mock_get) -> None: 27 | mock_get.return_value = requests.Response() 28 | mock_get.return_value.status_code = 200 29 | mock_get.return_value.json = mock_json 30 | self.assertEqual(GremlinAPISaml.metadata(), mock_data) 31 | 32 | @patch("requests.post") 33 | def test_sessions_with_decorator(self, mock_get) -> None: 34 | mock_get.return_value = requests.Response() 35 | mock_get.return_value.status_code = 200 36 | mock_get.return_value.json = mock_json 37 | self.assertEqual(GremlinAPISaml.sessions(**mock_saml), mock_data) 38 | -------------------------------------------------------------------------------- /tests/test_clients.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.clients import GremlinAPIClients 6 | 7 | from .util import mock_json, mock_data, mock_guid 8 | 9 | 10 | class TestClients(unittest.TestCase): 11 | @patch("requests.put") 12 | def test_activate_client_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual(GremlinAPIClients.activate_client(**mock_guid), mock_data) 17 | 18 | @patch("requests.delete") 19 | def test_deactivate_client_with_decorator(self, mock_get) -> None: 20 | mock_get.return_value = requests.Response() 21 | mock_get.return_value.status_code = 200 22 | mock_get.return_value.json = mock_json 23 | self.assertEqual(GremlinAPIClients.deactivate_client(**mock_guid), mock_data) 24 | 25 | @patch("requests.get") 26 | def test_list_active_clients_with_decorator(self, mock_get) -> None: 27 | mock_get.return_value = requests.Response() 28 | mock_get.return_value.status_code = 200 29 | mock_get.return_value.json = mock_json 30 | self.assertEqual(GremlinAPIClients.list_active_clients(), mock_data) 31 | 32 | @patch("requests.get") 33 | def test_list_clients_with_decorator(self, mock_get) -> None: 34 | mock_get.return_value = requests.Response() 35 | mock_get.return_value.status_code = 200 36 | mock_get.return_value.json = mock_json 37 | self.assertEqual(GremlinAPIClients.list_clients(), mock_data) 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_OWNER=gremlin 2 | DOCKER_NAME=gremlin-python-api 3 | DOCKER_IMAGE=$(DOCKER_OWNER)/$(DOCKER_NAME) 4 | 5 | BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') 6 | ENV_VARS=.env 7 | 8 | all: docker-build 9 | 10 | install: 11 | python3 setup.py install 12 | 13 | package: 14 | python3 setup.py sdist bdist_wheel 15 | 16 | test: 17 | python3 -m tests.test_all 18 | pytest tests/pytest_* 19 | 20 | lint: typecheck 21 | python3 -m black $(PWD)/gremlinapi 22 | python3 -m black $(PWD)/tests 23 | 24 | typecheck: 25 | mypy gremlinapi 26 | 27 | pypi-test: export TWINE_PASSWORD=$(PYPI_TEST) 28 | pypi-test: package 29 | python3 -m twine upload --non-interactive --config-file ${HOME}/.pypirc --repository testpypi dist/* 30 | 31 | pypi-prod: export TWINE_PASSWORD=$(PYPI_PROD) 32 | pypi-prod: package 33 | python3 -m twine upload --non-interactive --config-file ${HOME}/.pypirc dist/* 34 | 35 | docker-build: 36 | docker build --no-cache=true \ 37 | --build-arg BUILD_DATE=$(BUILD_DATE) \ 38 | --build-arg IMAGE_NAME=$(DOCKER_IMAGE) \ 39 | -t $(DOCKER_IMAGE):latest \ 40 | . 41 | 42 | docker-run: 43 | @if ! test -f $(ENV_VARS); then touch $(ENV_VARS); fi 44 | docker run --rm -v ~/.ssh/:/root/.ssh --mount type=bind,source="$(PWD)",target=/opt/gremlin-python \ 45 | --env-file=$(ENV_VARS) --name $(DOCKER_NAME) --entrypoint "/bin/ash" $(DOCKER_IMAGE):latest 46 | 47 | docker-run-interactive: 48 | @if ! test -f $(ENV_VARS); then touch $(ENV_VARS); fi 49 | docker run -it --rm -v ~/.ssh/:/root/.ssh --mount type=bind,source="$(PWD)",target=/opt/gremlin-python \ 50 | --env-file=$(ENV_VARS) --name $(DOCKER_NAME) --entrypoint "/bin/ash" $(DOCKER_IMAGE):latest 51 | -------------------------------------------------------------------------------- /gremlinapi/providers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.exceptions import ( 9 | GremlinParameterError, 10 | ProxyError, 11 | ClientError, 12 | HTTPTimeout, 13 | HTTPError, 14 | ) 15 | 16 | from gremlinapi.gremlinapi import GremlinAPI 17 | from gremlinapi.http_clients import ( 18 | get_gremlin_httpclient, 19 | GremlinAPIHttpClient, 20 | ) 21 | 22 | from typing import Union, Type 23 | 24 | 25 | log = logging.getLogger("GremlinAPI.client") 26 | 27 | 28 | class GremlinAPIProviders(GremlinAPI): 29 | @classmethod 30 | @register_cli_action("list_providers", ("",), ("",)) 31 | def list_providers( 32 | cls, 33 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 34 | *args: tuple, 35 | **kwargs: dict, 36 | ) -> dict: 37 | method: str = "GET" 38 | endpoint: str = "/providers" 39 | payload: dict = cls._payload(**{"headers": https_client.header()}) 40 | (resp, body) = https_client.api_call(method, endpoint, **payload) 41 | return body 42 | 43 | @classmethod 44 | @register_cli_action("list_aws_services", ("",), ("",)) 45 | def list_aws_services( 46 | cls, 47 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 48 | *args: tuple, 49 | **kwargs: dict, 50 | ) -> dict: 51 | method: str = "GET" 52 | endpoint: str = "/providers/aws" 53 | payload: dict = cls._payload(**{"headers": https_client.header()}) 54 | (resp, body) = https_client.api_call(method, endpoint, **payload) 55 | return body 56 | -------------------------------------------------------------------------------- /examples/companies.md: -------------------------------------------------------------------------------- 1 | # Companies Endpoint 2 | 3 | ### Get Company Details 4 | 5 | ```python 6 | from gremlinapi.companies import GremlinAPICompanies as companies 7 | 8 | company_id = 'COMPANY_GUID' 9 | response = companies.get_company(identifier=company_id) 10 | ``` 11 | 12 | ### List Company Clients 13 | 14 | ```python 15 | from gremlinapi.companies import GremlinAPICompanies as companies 16 | 17 | company_id = 'COMPANY_GUID' 18 | response = companies.list_company_clients(identifier=company_id) 19 | ``` 20 | 21 | ### Invite Company User 22 | 23 | ```python 24 | from gremlinapi.companies import GremlinAPICompanies as companies 25 | 26 | company_id = 'COMPANY_GUID' 27 | user_email_body = { 'email': 'andjohn@gremlin.com' } 28 | response = companies.invite_company_user(identifier=company_id, body=user_email_body) 29 | ``` 30 | 31 | ```python 32 | from gremlinapi.companies import GremlinAPICompanies as companies 33 | 34 | company_id = 'COMPANY_GUID' 35 | user_email_body = [{ 'emails': 'andjohn+1@gremlin.com'}, { 'email': 'andjohn+2@gremlin.com'}] 36 | response = companies.invite_company_user(identifier=company_id, body=user_email_body) 37 | ``` 38 | 39 | ### Delete Company Invite 40 | 41 | ```python 42 | from gremlinapi.companies import GremlinAPICompanies as companies 43 | 44 | company_id = 'COMPANY_GUID' 45 | user_email = 'andjohn@gremlin.com' 46 | response = companies.delete_company_invite(identifier=company_id, body=user_email) 47 | ``` 48 | 49 | ### Update Company MFA Preferences 50 | 51 | ### Update Company Preferences 52 | 53 | ### Update Company SAML Properties 54 | 55 | ### List Company Users 56 | 57 | ```python 58 | from gremlinapi.companies import GremlinAPICompanies as companies 59 | 60 | company_id = 'COMPANY_GUID' 61 | response = companies.list_company_users(identifier=company_id) 62 | ``` 63 | 64 | ### Update Company User Roles 65 | 66 | ### Activate Company User 67 | 68 | ### Deactivate Company User -------------------------------------------------------------------------------- /examples/halts.md: -------------------------------------------------------------------------------- 1 | # Halts 2 | 3 | ### Halt all attacks 4 | 5 | ```python 6 | from gremlinapi.halts import GremlinAPIHalts as halts 7 | team_id = 'TEAM_ID/UUID' 8 | halt_body = {'reason': 'Showing off how to halt attacks', 9 | 'reference': 'Monitoring Dashboard ID'} 10 | 11 | confirmation = halts.halt_all_attacks(teamId=team_id, body=halt_body) 12 | ``` 13 | 14 | ## ALFI Endpoint 15 | ### Halt ALFI Experiment 16 | 17 | ```python 18 | from gremlinapi.alfi import GremlinALFI as alfi 19 | 20 | alfi_attack_id = 'expirment-guid' 21 | alfi.halt_alfi_experiment(guid=alfi_attack_id) 22 | ``` 23 | 24 | ### Halt _ALL_ ALFI Experiments 25 | 26 | ```python 27 | from gremlinapi.alfi import GremlinALFI as alfi 28 | 29 | alfi.halt_all_alfi_experiments() 30 | ``` 31 | 32 | 33 | ## Attacks Endpoint 34 | ### Halt Attack 35 | 36 | ```python 37 | from gremlinapi.attacks import GremlinAPIAttacks as attacks 38 | team_id = 'TEAM_ID/UUID' 39 | attack_id = 'ATTACK_ID' 40 | 41 | confirmation = attacks.halt_attack(guid=attack_id, teamId=team_id) 42 | ``` 43 | 44 | ### Halt _ALL_ Attacks 45 | 46 | ```python 47 | from gremlinapi.attacks import GremlinAPIAttacks as attacks 48 | team_id = 'TEAM_ID/UUID' 49 | 50 | confirmation = attacks.halt_all_attacks(teamId=team_id) 51 | ``` 52 | 53 | ## Kubernetes Endpoint 54 | ### Halt Kubernetes Attack 55 | ```python 56 | from gremlinapi.kubernetes import GremlinAPIKubernetesAttacks as k8attacks 57 | team_id = 'TEAM_ID/UUID' 58 | attack_id = 'ATTACK_ID' 59 | 60 | confirmation = k8attacks.halt_kubernetes_attack(guid=attack_id, teamId=team_id) 61 | ``` 62 | 63 | ### Halt All Kubernetes Attacks 64 | ```python 65 | from gremlinapi.kubernetes import GremlinAPIKubernetesAttacks as k8attacks 66 | team_id = 'TEAM_ID/UUID' 67 | 68 | confirmation = k8attacks.halt_all_kubernetes_attacks(teamId=team_id) 69 | ``` 70 | 71 | ## Scenarios 72 | ### Halt Scenario 73 | ```python 74 | from gremlinapi.scenarios import GremlinAPIScenarios as scenarios 75 | team_id = 'TEAM_ID/UUID' 76 | scenario_id = 'SCENARIO_ID' 77 | scenario_run = 'SCENARIO_RUN_ID' 78 | 79 | confirmation = scenarios.halt_scenario(teamId=team_id, guid=scenario_id, runNumber=scenario_run) 80 | ``` -------------------------------------------------------------------------------- /examples/saml.md: -------------------------------------------------------------------------------- 1 | # Logging in with SAML 2 | 3 | #### Example 4 | 5 | This test obtains a SAML Assertion from OneLogin, and uses it to obtain a Gremlin bearer token 6 | 7 | ```python 8 | 9 | import gremlinapi 10 | import requests 11 | import json 12 | 13 | from gremlinapi.attacks import GremlinAPIAttacks 14 | 15 | gremlin_team_id = '' 16 | gremlin_company_name = '' 17 | 18 | ol_api = "https://api.us.onelogin.com" 19 | 20 | ol_client_id = "" 21 | ol_client_sc = "" 22 | 23 | ol_user = "" 24 | ol_pass = "" 25 | ol_app_id = "" 26 | ol_subdomain = "" 27 | 28 | ol_relay_state = f'{gremlin_company_name}|||https://app.gremlin.com/users/sso/saml/acs|||/' 29 | 30 | content_type = "application/json" 31 | 32 | ### BEGIN GET SAML ASSERTION ### 33 | ol_bearer_response = requests.post(f'{ol_api}/auth/oauth2/v2/token', 34 | data=json.dumps({"grant_type": "client_credentials"}), 35 | headers={"Authorization": f'client_id:{ol_client_id}, client_secret:{ol_client_sc}', 36 | "Content-Type": content_type}) 37 | ol_bearer_data = ol_bearer_response.json() 38 | ol_bearer_token = ol_bearer_data['access_token'] 39 | 40 | ol_saml_response = requests.post(f'{ol_api}/api/2/saml_assertion', 41 | data=json.dumps({ 42 | "username_or_email": ol_user, 43 | "password": ol_pass, 44 | "app_id": ol_app_id, 45 | "subdomain": ol_subdomain 46 | }), 47 | headers={"Authorization": f'bearer:{bearer_token}', "Content-Type": content_type}) 48 | ol_saml_data = ol_saml_response.json() 49 | saml_assertion = ol_saml_data['data'] 50 | ### END GET SAML ASSERTION ### 51 | 52 | ### LOG INTO GREMLIN API ### 53 | gremlinapi.saml_login(email=ol_user, saml_assertion=saml_assertion, relay_state=ol_relay_state) 54 | 55 | ### Ensure we can retrieve data 56 | GremlinAPIAttacks.list_attacks(teamId=gremlin_team_id) 57 | 58 | ``` -------------------------------------------------------------------------------- /gremlinapi/metrics.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.exceptions import ( 9 | GremlinParameterError, 10 | ProxyError, 11 | ClientError, 12 | HTTPTimeout, 13 | HTTPError, 14 | ) 15 | 16 | from gremlinapi.gremlinapi import GremlinAPI 17 | from gremlinapi.http_clients import ( 18 | get_gremlin_httpclient, 19 | GremlinAPIHttpClient, 20 | ) 21 | 22 | from typing import Union, Type 23 | 24 | log = logging.getLogger("GremlinAPI.client") 25 | 26 | 27 | class GremlinAPIMetrics(GremlinAPI): 28 | @classmethod 29 | @register_cli_action("get_attack_metrics", ("attackId",), ("teamId",)) 30 | def get_attack_metrics( 31 | cls, 32 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 33 | *args: tuple, 34 | **kwargs: dict, 35 | ) -> dict: 36 | method: str = "GET" 37 | attack_id: str = cls._error_if_not_param("attackId", **kwargs) 38 | endpoint: str = cls._optional_team_endpoint( 39 | f"/metrics/attacks/{attack_id}", **kwargs 40 | ) 41 | payload: dict = cls._payload(**{"headers": https_client.header()}) 42 | (resp, body) = https_client.api_call(method, endpoint, **payload) 43 | return body 44 | 45 | @classmethod 46 | @register_cli_action( 47 | "get_scenario_run_metrics", ("scenarioId", "scenarioRunNumber"), ("teamId",) 48 | ) 49 | def get_scenario_run_metrics( 50 | cls, 51 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 52 | *args: tuple, 53 | **kwargs: dict, 54 | ) -> dict: 55 | method: str = "GET" 56 | scenario_id: str = cls._error_if_not_param("scenarioId", **kwargs) 57 | scenario_run_number: str = cls._error_if_not_param( 58 | "scenarioRunNumber", **kwargs 59 | ) 60 | endpoint: str = cls._optional_team_endpoint( 61 | f"/metrics/scenarios/{scenario_id}/runs/{scenario_run_number}", **kwargs 62 | ) 63 | payload: dict = cls._payload(**{"headers": https_client.header()}) 64 | (resp, body) = https_client.api_call(method, endpoint, **payload) 65 | return body 66 | -------------------------------------------------------------------------------- /tests/pytest_orgs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import responses 3 | 4 | from gremlinapi.config import GremlinAPIConfig as config 5 | from gremlinapi.orgs import GremlinAPIOrgs 6 | from .test_base import ( 7 | TestTeamIdBase, 8 | TestRequiredParamBase, 9 | TestBase, 10 | TestBasicMethod, 11 | TestOptionalParamBase 12 | ) 13 | from gremlinapi.exceptions import GremlinParameterError 14 | import requests 15 | import os 16 | import re 17 | from .util import ( 18 | api_key as mock_api_key, 19 | mock_base_url, 20 | mock_org_id, 21 | ) 22 | 23 | config.api_key = mock_api_key 24 | 25 | class TestListOrgs(): 26 | 27 | class BasicMethod(TestBasicMethod): 28 | __test__ = True 29 | method = GremlinAPIOrgs.list_orgs 30 | urls = { 31 | 'GET': f'{mock_base_url}/orgs', 32 | } 33 | 34 | class TestGetOrg(): 35 | 36 | class BasicMethod(TestBasicMethod): 37 | __test__ = True 38 | method = GremlinAPIOrgs.get_org 39 | urls = { 40 | 'GET': [ 41 | mock_base_url + r'/orgs*' 42 | ] 43 | } 44 | kwargs = { 45 | 'identifier': mock_org_id 46 | } 47 | 48 | class RequiredParamIdentifier(TestRequiredParamBase): 49 | __test__ = True 50 | method = GremlinAPIOrgs.get_org 51 | urls = { 52 | 'GET': [ 53 | mock_base_url + r'/orgs*' 54 | ] 55 | } 56 | kwargs = { 57 | 'identifier': mock_org_id 58 | } 59 | param_name = 'identifier' 60 | 61 | class OptionalParamAddUser(TestOptionalParamBase): 62 | __test__ = True 63 | method = GremlinAPIOrgs.get_org 64 | urls = { 65 | 'GET': [ 66 | mock_base_url + r'/orgs*' 67 | ] 68 | } 69 | kwargs = { 70 | 'identifier': mock_org_id, 71 | 'add_user': True 72 | } 73 | param_name = 'add_user' 74 | 75 | class TestCreateOrg(): 76 | 77 | class BasicMethod(TestBasicMethod): 78 | __test__ = True 79 | method = GremlinAPIOrgs.create_org 80 | urls = { 81 | 'POST': [ 82 | f'{mock_base_url}/orgs' 83 | ] 84 | } 85 | kwargs = { 86 | 'name': 'test_org', 87 | } 88 | 89 | class TestNewCertificate(): 90 | pass -------------------------------------------------------------------------------- /tests/test_scenario_helpers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | import json 6 | import uuid 7 | 8 | from gremlinapi.scenario_helpers import GremlinScenarioHelper, GremlinILFIStep 9 | 10 | from .util import mock_json, mock_data, mock_scenario, mock_scenario_step 11 | 12 | 13 | class TestScenarioHelpers(unittest.TestCase): 14 | def test_gremlin_scenario_helper_add_step(self) -> None: 15 | helper = GremlinScenarioHelper(**mock_scenario) 16 | helper_step = GremlinILFIStep(**mock_scenario_step) 17 | helper_step_2 = GremlinILFIStep(**mock_scenario_step) 18 | 19 | self.assertEqual(len(helper.steps), 0) 20 | helper.add_step(helper_step) 21 | self.assertEqual(len(helper.steps), 1) 22 | helper.add_step(helper_step_2) 23 | self.assertEqual(len(helper.steps), 2) 24 | 25 | self.assertEqual(helper.steps[0], json.loads(str(helper_step))) 26 | self.assertEqual(helper.steps[1], json.loads(str(helper_step_2))) 27 | 28 | def test_gremlin_scenario_helper_repr_model(self) -> None: 29 | helper = GremlinScenarioHelper(**mock_scenario) 30 | helper_step = GremlinILFIStep(**mock_scenario_step) 31 | helper_step_2 = GremlinILFIStep(**mock_scenario_step) 32 | 33 | helper.add_step(helper_step) 34 | helper.add_step(helper_step_2) 35 | 36 | expected_output = { 37 | "description": mock_scenario["description"], 38 | "hypothesis": mock_scenario["hypothesis"], 39 | "name": mock_scenario["name"], 40 | "steps": [json.loads(str(helper_step)), json.loads(str(helper_step_2))], 41 | } 42 | 43 | self.assertEqual(repr(helper), json.dumps(expected_output)) 44 | 45 | def test_step_and_ilfi_step_repr_model(self) -> None: 46 | helper = GremlinILFIStep(**mock_scenario_step) 47 | 48 | expected_attacks = [ 49 | { 50 | "attackType": "ILFI", 51 | "impactDefinition": mock_scenario_step["command"].impact_definition(), 52 | "targetDefinition": mock_scenario_step["target"].target_definition(), 53 | } 54 | ] 55 | expected_output = { 56 | "delay": mock_scenario_step["delay"], 57 | "attacks": expected_attacks, 58 | "id": str(uuid.uuid3(uuid.NAMESPACE_X500, str(expected_attacks))), 59 | } 60 | 61 | self.assertEqual(repr(helper), json.dumps(expected_output)) 62 | -------------------------------------------------------------------------------- /tests/test_alfi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2021 Kyle Bouchard , Gremlin Inc 4 | 5 | import unittest 6 | import requests 7 | import logging 8 | from unittest.mock import patch 9 | from gremlinapi.alfi import GremlinALFI 10 | 11 | from .util import mock_json, mock_body, mock_data, mock_guid 12 | 13 | 14 | class TestAlfi(unittest.TestCase): 15 | @patch("requests.post") 16 | def test_create_alfi_experiment_with_decorator(self, mock_get) -> None: 17 | mock_get.return_value = requests.Response() 18 | mock_get.return_value.status_code = 200 19 | mock_get.return_value.json = mock_json 20 | self.assertEqual(GremlinALFI.create_alfi_experiment(**mock_body), mock_data) 21 | 22 | @patch("requests.delete") 23 | def test_halt_all_alfi_experiments_with_decorator(self, mock_get) -> None: 24 | mock_get.return_value = requests.Response() 25 | mock_get.return_value.status_code = 200 26 | mock_get.return_value.json = mock_json 27 | self.assertEqual(GremlinALFI.halt_all_alfi_experiments(), mock_data) 28 | 29 | @patch("requests.get") 30 | def test_get_alfi_experiment_details_with_decorator(self, mock_get) -> None: 31 | mock_get.return_value = requests.Response() 32 | mock_get.return_value.status_code = 200 33 | mock_get.return_value.json = mock_json 34 | self.assertEqual( 35 | GremlinALFI.get_alfi_experiment_details(**mock_guid), mock_data 36 | ) 37 | 38 | @patch("requests.delete") 39 | def test_halt_alfi_experiment_with_decorator(self, mock_get) -> None: 40 | mock_get.return_value = requests.Response() 41 | mock_get.return_value.status_code = 200 42 | mock_get.return_value.json = mock_json 43 | self.assertEqual(GremlinALFI.halt_alfi_experiment(**mock_guid), mock_data) 44 | 45 | @patch("requests.get") 46 | def test_list_active_alfi_experiments_with_decorator(self, mock_get) -> None: 47 | mock_get.return_value = requests.Response() 48 | mock_get.return_value.status_code = 200 49 | mock_get.return_value.json = mock_json 50 | self.assertEqual(GremlinALFI.list_active_alfi_experiments(), mock_data) 51 | 52 | @patch("requests.get") 53 | def test_list_completed_alfi_experiments_with_decorator(self, mock_get) -> None: 54 | mock_get.return_value = requests.Response() 55 | mock_get.return_value.status_code = 200 56 | mock_get.return_value.json = mock_json 57 | self.assertEqual(GremlinALFI.list_completed_alfi_experiments(), mock_data) 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | env.vars 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # IntelliJ local settings 133 | .idea 134 | gremlin-python.iml 135 | 136 | _*.json 137 | _t.py -------------------------------------------------------------------------------- /gremlinapi/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | 8 | log = logging.getLogger("GremlinAPI.client") 9 | 10 | 11 | class GremlinAPIException(Exception): 12 | """ 13 | Base exception class 14 | """ 15 | 16 | 17 | class APIError(GremlinAPIException): 18 | def __init__(self, message: str): 19 | super(APIError, self).__init__(message) 20 | 21 | class GremlinGraphError(GremlinAPIException): 22 | def __init__(self, message: str): 23 | super(GremlinGraphError, self).__init__(message) 24 | 25 | class GremlinAuthError(GremlinAPIException): 26 | def __init__(self, message: str): 27 | super(GremlinAuthError, self).__init__(message) 28 | 29 | 30 | class GremlinIdentifierError(GremlinAPIException): 31 | def __init__(self, message: str): 32 | super(GremlinIdentifierError, self).__init__(message) 33 | 34 | 35 | class GremlinParameterError(GremlinAPIException): 36 | def __init__(self, message: str): 37 | super(GremlinParameterError, self).__init__(message) 38 | 39 | 40 | class GremlinCommandTargetError(GremlinAPIException): 41 | def __init__(self, message: str): 42 | super(GremlinCommandTargetError, self).__init__(message) 43 | 44 | 45 | class ProxyError(GremlinAPIException): 46 | def __init__(self, uri: str, method: str, **kwargs: dict): 47 | message: str = f"Error for {method} to {uri}, please verify proxy configuration" 48 | super(ProxyError, self).__init__(message) 49 | 50 | 51 | class ClientError(GremlinAPIException): 52 | def __init__(self, uri: str, method: str, **kwargs: dict): 53 | message: str = ( 54 | f"Error for {method} to {uri}, please check your network configuration" 55 | ) 56 | super(ClientError, self).__init__(message) 57 | 58 | 59 | class HTTPTimeout(GremlinAPIException): 60 | def __init__(self, uri: str, method: str, timeout: str, **kwargs: dict): 61 | message: str = f"{method} to {uri} timed out after {timeout}" 62 | super(HTTPTimeout, self).__init__(message) 63 | 64 | 65 | class HTTPError(GremlinAPIException): 66 | def __init__(self, status_code: str = None, reason: str = None): 67 | message: str = f"Request returned status code {status_code} {reason}" 68 | super(HTTPError, self).__init__(message) 69 | 70 | 71 | class HTTPBadHeader(GremlinAPIException): 72 | def __init__(self, reason: str = None): 73 | message: str = f"Failed to create HTTPHeader: {reason}" 74 | super(HTTPBadHeader, self).__init__(message) 75 | -------------------------------------------------------------------------------- /tests/test_orgs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.orgs import GremlinAPIOrgs 6 | 7 | from .util import mock_json, mock_data, mock_identifier 8 | 9 | 10 | class TestOrgs(unittest.TestCase): 11 | @patch("requests.get") 12 | def test_list_orgs_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual(GremlinAPIOrgs.list_orgs(), mock_data) 17 | 18 | @patch("requests.get") 19 | def test_get_org_with_decorator(self, mock_get) -> None: 20 | mock_get.return_value = requests.Response() 21 | mock_get.return_value.status_code = 200 22 | mock_get.return_value.json = mock_json 23 | self.assertEqual(GremlinAPIOrgs.get_org(**mock_identifier), mock_data) 24 | 25 | @patch("requests.post") 26 | def test_create_org_with_decorator(self, mock_get) -> None: 27 | mock_get.return_value = requests.Response() 28 | mock_get.return_value.status_code = 200 29 | mock_get.return_value.json = mock_json 30 | self.assertEqual(GremlinAPIOrgs.create_org(**mock_identifier), mock_data) 31 | 32 | @patch("requests.post") 33 | def test_new_certificate_with_decorator(self, mock_get) -> None: 34 | mock_get.return_value = requests.Response() 35 | mock_get.return_value.status_code = 200 36 | mock_get.return_value.json = mock_json 37 | self.assertEqual(GremlinAPIOrgs.new_certificate(), mock_data) 38 | 39 | @patch("requests.delete") 40 | def test_delete_certificate_with_decorator(self, mock_get) -> None: 41 | mock_get.return_value = requests.Response() 42 | mock_get.return_value.status_code = 200 43 | mock_get.return_value.json = mock_json 44 | self.assertEqual(GremlinAPIOrgs.delete_certificate(), mock_data) 45 | 46 | @patch("requests.delete") 47 | def test_delete_old_certificate_with_decorator(self, mock_get) -> None: 48 | mock_get.return_value = requests.Response() 49 | mock_get.return_value.status_code = 200 50 | mock_get.return_value.json = mock_json 51 | self.assertEqual(GremlinAPIOrgs.delete_old_certificate(), mock_data) 52 | 53 | @patch("requests.post") 54 | def test_reset_secret_with_decorator(self, mock_get) -> None: 55 | mock_get.return_value = requests.Response() 56 | mock_get.return_value.status_code = 200 57 | mock_get.return_value.json = mock_json 58 | self.assertEqual(GremlinAPIOrgs.reset_secret(**mock_identifier), mock_data) 59 | -------------------------------------------------------------------------------- /tests/test_templates.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.templates import GremlinAPITemplates 6 | 7 | from .util import mock_json, mock_data, mock_body, mock_guid 8 | 9 | 10 | class TestTemplates(unittest.TestCase): 11 | @patch("requests.get") 12 | def test_list_templates_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual(GremlinAPITemplates.list_templates(), mock_data) 17 | 18 | @patch("requests.post") 19 | def test_create_template_with_decorator(self, mock_get) -> None: 20 | mock_get.return_value = requests.Response() 21 | mock_get.return_value.status_code = 200 22 | mock_get.return_value.json = mock_json 23 | self.assertEqual(GremlinAPITemplates.create_template(**mock_body), mock_data) 24 | 25 | @patch("requests.get") 26 | def test_get_template_with_decorator(self, mock_get) -> None: 27 | mock_get.return_value = requests.Response() 28 | mock_get.return_value.status_code = 200 29 | mock_get.return_value.json = mock_json 30 | self.assertEqual(GremlinAPITemplates.get_template(**mock_guid), mock_data) 31 | 32 | @patch("requests.delete") 33 | def test_delete_template_with_decorator(self, mock_get) -> None: 34 | mock_get.return_value = requests.Response() 35 | mock_get.return_value.status_code = 200 36 | mock_get.return_value.json = mock_json 37 | self.assertEqual(GremlinAPITemplates.delete_template(**mock_guid), mock_data) 38 | 39 | @patch("requests.get") 40 | def test_list_command_templates_with_decorator(self, mock_get) -> None: 41 | mock_get.return_value = requests.Response() 42 | mock_get.return_value.status_code = 200 43 | mock_get.return_value.json = mock_json 44 | self.assertEqual(GremlinAPITemplates.list_command_templates(), mock_data) 45 | 46 | @patch("requests.get") 47 | def test_list_target_templates_with_decorator(self, mock_get) -> None: 48 | mock_get.return_value = requests.Response() 49 | mock_get.return_value.status_code = 200 50 | mock_get.return_value.json = mock_json 51 | self.assertEqual(GremlinAPITemplates.list_target_templates(), mock_data) 52 | 53 | @patch("requests.get") 54 | def test_list_trigger_templates_with_decorator(self, mock_get) -> None: 55 | mock_get.return_value = requests.Response() 56 | mock_get.return_value.status_code = 200 57 | mock_get.return_value.json = mock_json 58 | self.assertEqual(GremlinAPITemplates.list_trigger_templates(), mock_data) 59 | -------------------------------------------------------------------------------- /tests/test_reports.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.reports import GremlinAPIReports, GremlinAPIReportsSecurity 6 | 7 | from .util import mock_json, mock_data, mock_report 8 | 9 | 10 | class TestReports(unittest.TestCase): 11 | @patch("requests.get") 12 | def test_report_attacks_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual(GremlinAPIReports.report_attacks(**mock_report), mock_data) 17 | 18 | @patch("requests.get") 19 | def test_report_clients_with_decorator(self, mock_get) -> None: 20 | mock_get.return_value = requests.Response() 21 | mock_get.return_value.status_code = 200 22 | mock_get.return_value.json = mock_json 23 | self.assertEqual(GremlinAPIReports.report_clients(**mock_report), mock_data) 24 | 25 | @patch("requests.get") 26 | def test_report_companies_with_decorator(self, mock_get) -> None: 27 | mock_get.return_value = requests.Response() 28 | mock_get.return_value.status_code = 200 29 | mock_get.return_value.json = mock_json 30 | self.assertEqual(GremlinAPIReports.report_companies(**mock_report), mock_data) 31 | 32 | @patch("requests.get") 33 | def test_report_pricing_with_decorator(self, mock_get) -> None: 34 | mock_get.return_value = requests.Response() 35 | mock_get.return_value.status_code = 200 36 | mock_get.return_value.json = mock_json 37 | self.assertEqual(GremlinAPIReports.report_pricing(**mock_report), mock_data) 38 | 39 | @patch("requests.get") 40 | def test_report_teams_with_decorator(self, mock_get) -> None: 41 | mock_get.return_value = requests.Response() 42 | mock_get.return_value.status_code = 200 43 | mock_get.return_value.json = mock_json 44 | self.assertEqual(GremlinAPIReports.report_teams(**mock_report), mock_data) 45 | 46 | @patch("requests.get") 47 | def test_report_users_with_decorator(self, mock_get) -> None: 48 | mock_get.return_value = requests.Response() 49 | mock_get.return_value.status_code = 200 50 | mock_get.return_value.json = mock_json 51 | self.assertEqual(GremlinAPIReports.report_users(**mock_report), mock_data) 52 | 53 | @patch("requests.get") 54 | def test_report_security_access_with_decorator(self, mock_get) -> None: 55 | mock_get.return_value = requests.Response() 56 | mock_get.return_value.status_code = 200 57 | mock_get.return_value.json = mock_json 58 | self.assertEqual( 59 | GremlinAPIReportsSecurity.report_security_access(**mock_report), mock_data 60 | ) 61 | -------------------------------------------------------------------------------- /gremlinapi/apikeys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.exceptions import ( 9 | GremlinParameterError, 10 | ClientError, 11 | HTTPTimeout, 12 | HTTPError, 13 | ) 14 | 15 | from gremlinapi.gremlinapi import GremlinAPI 16 | from gremlinapi.http_clients import ( 17 | get_gremlin_httpclient, 18 | GremlinAPIHttpClient, 19 | ) 20 | from typing import Union, Type 21 | 22 | 23 | log = logging.getLogger("GremlinAPI.client") 24 | 25 | 26 | class GremlinAPIapikeys(GremlinAPI): 27 | @classmethod 28 | @register_cli_action( 29 | "create_apikey", 30 | ( 31 | "description", 32 | "identifier", 33 | ), 34 | ("teamId",), 35 | ) 36 | def create_apikey( 37 | cls, 38 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 39 | *args: tuple, 40 | **kwargs: dict, 41 | ) -> dict: 42 | method: str = "POST" 43 | data: dict = { 44 | "description": cls._error_if_not_param("description", **kwargs), 45 | "identifier": cls._error_if_not_param("identifier", **kwargs), 46 | } 47 | endpoint: str = cls._optional_team_endpoint(f"/apikeys", **kwargs) 48 | payload: dict = cls._payload(**{"headers": https_client.header(), "body": data}) 49 | (resp, body) = https_client.api_call(method, endpoint, **payload) 50 | return body 51 | 52 | @classmethod 53 | @register_cli_action("list_apikeys", ("",), ("teamId",)) 54 | def list_apikeys( 55 | cls, 56 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 57 | *args: tuple, 58 | **kwargs: dict, 59 | ) -> dict: 60 | method: str = "GET" 61 | endpoint: str = cls._optional_team_endpoint(f"/apikeys", **kwargs) 62 | payload: dict = cls._payload(**{"headers": https_client.header()}) 63 | (resp, body) = https_client.api_call(method, endpoint, **payload) 64 | return body 65 | 66 | @classmethod 67 | @register_cli_action("revoke_apikey", ("identifier",), ("teamId",)) 68 | def revoke_apikey( 69 | cls, 70 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 71 | *args: tuple, 72 | **kwargs: dict, 73 | ) -> dict: 74 | method: str = "DELETE" 75 | identifier: str = cls._error_if_not_param("identifier", **kwargs) 76 | endpoint: str = cls._optional_team_endpoint(f"/apikeys/{identifier}", **kwargs) 77 | payload: dict = cls._payload(**{"headers": https_client.header()}) 78 | (resp, body) = https_client.api_call(method, endpoint, **payload) 79 | return body 80 | -------------------------------------------------------------------------------- /tests/test_kubernetes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.kubernetes import ( 6 | GremlinAPIKubernetesAttacks, 7 | GremlinAPIKubernetesTargets, 8 | ) 9 | 10 | from .util import mock_json, mock_data, mock_uid, mock_body 11 | 12 | 13 | class TestKubernetesAttacks(unittest.TestCase): 14 | @patch("requests.get") 15 | def test_list_all_kubernetes_attacks_with_decorator(self, mock_get) -> None: 16 | mock_get.return_value = requests.Response() 17 | mock_get.return_value.status_code = 200 18 | mock_get.return_value.json = mock_json 19 | self.assertEqual( 20 | GremlinAPIKubernetesAttacks.list_all_kubernetes_attacks(), mock_data 21 | ) 22 | 23 | @patch("requests.get") 24 | def test_get_kubernetes_attack_with_decorator(self, mock_get) -> None: 25 | mock_get.return_value = requests.Response() 26 | mock_get.return_value.status_code = 200 27 | mock_get.return_value.json = mock_json 28 | self.assertEqual( 29 | GremlinAPIKubernetesAttacks.get_kubernetes_attack(**mock_uid), mock_data 30 | ) 31 | 32 | @patch("requests.post") 33 | def test_halt_kubernetes_attack_with_decorator(self, mock_get) -> None: 34 | mock_get.return_value = requests.Response() 35 | mock_get.return_value.status_code = 200 36 | mock_get.return_value.json = mock_json 37 | self.assertEqual( 38 | GremlinAPIKubernetesAttacks.halt_kubernetes_attack(**mock_uid), mock_data 39 | ) 40 | 41 | @patch("requests.post") 42 | def test_halt_all_kubernetes_attack_with_decorator(self, mock_get) -> None: 43 | mock_get.return_value = requests.Response() 44 | mock_get.return_value.status_code = 200 45 | mock_get.return_value.json = mock_json 46 | self.assertEqual( 47 | GremlinAPIKubernetesAttacks.halt_all_kubernetes_attacks(), mock_data 48 | ) 49 | 50 | @patch("requests.post") 51 | def test_new_kubernetes_attack_with_decorator(self, mock_get) -> None: 52 | mock_get.return_value = requests.Response() 53 | mock_get.return_value.status_code = 200 54 | mock_get.return_value.json = mock_json 55 | self.assertEqual( 56 | GremlinAPIKubernetesAttacks.new_kubernetes_attack(**mock_body), mock_data 57 | ) 58 | 59 | 60 | class TestKubernetesTargets(unittest.TestCase): 61 | @patch("requests.get") 62 | def test_list_kubernetes_targets_with_decorator(self, mock_get) -> None: 63 | mock_get.return_value = requests.Response() 64 | mock_get.return_value.status_code = 200 65 | mock_get.return_value.json = mock_json 66 | self.assertEqual( 67 | GremlinAPIKubernetesTargets.list_kubernetes_targets(), mock_data 68 | ) 69 | -------------------------------------------------------------------------------- /examples/kubernetes.md: -------------------------------------------------------------------------------- 1 | # Kubernetes 2 | 3 | ### List All Kubernetes Attacks 4 | ```python 5 | from gremlinapi.kubernetes import GremlinAPIKubernetesAttacks as k8attacks 6 | team_id = 'TEAM_ID/UUID' 7 | 8 | attack_list = k8attacks.list_all_kubernetes_attacks(teamId=team_id) 9 | ``` 10 | 11 | ### Get attack details 12 | ```python 13 | from gremlinapi.kubernetes import GremlinAPIKubernetesAttacks as k8attacks 14 | team_id = 'TEAM_ID/UUID' 15 | attack_id = 'ATTACK_UID' 16 | 17 | attack_details = k8attacks.get_kubernetes_attack(uid=attack_id, teamId=team_id) 18 | ``` 19 | 20 | ### Halt Kubernetes Attack 21 | ```python 22 | from gremlinapi.kubernetes import GremlinAPIKubernetesAttacks as k8attacks 23 | team_id = 'TEAM_ID/UUID' 24 | attack_id = 'ATTACK_ID' 25 | 26 | confirmation = k8attacks.halt_kubernetes_attack(uid=attack_id, teamId=team_id) 27 | ``` 28 | 29 | ### Halt All Kubernetes Attacks 30 | ```python 31 | from gremlinapi.kubernetes import GremlinAPIKubernetesAttacks as k8attacks 32 | team_id = 'TEAM_ID/UUID' 33 | 34 | confirmation = k8attacks.halt_all_kubernetes_attacks(teamId=team_id) 35 | ``` 36 | 37 | ### Attack 50% of a Kubernetes Deployment 38 | ```python 39 | from gremlinapi.kubernetes import GremlinAPIKubernetesAttacks as k8s_attacks 40 | from kubernetes_attack_helpers import GremlinKubernetesAttackTarget, GremlinKubernetesAttackTargetHelper, GremlinKubernetesAttackHelper 41 | from gremlinapi.attack_helpers import GremlinBlackholeAttack 42 | 43 | config.api_key = 44 | config.team_id = 45 | 46 | k8s_attacks.new_kubernetes_attack( 47 | body = GremlinKubernetesAttackHelper( 48 | command = GremlinBlackholeAttack(), 49 | target = GremlinKubernetesAttackTargetHelper( 50 | targets = [ 51 | GremlinKubernetesAttackTarget( 52 | cluster_id = "", 53 | namespace = "", 54 | kind = "DEPLOYMENT", 55 | name = " 56 | ) 57 | ], 58 | percentage = 50 59 | ) 60 | ) 61 | ) 62 | ``` 63 | 64 | ### Attack 3 pods of a Kubernetes ReplicaSet 65 | ```python 66 | from gremlinapi.kubernetes import GremlinAPIKubernetesAttacks as k8s_attacks 67 | from kubernetes_attack_helpers import GremlinKubernetesAttackTarget, GremlinKubernetesAttackTargetHelper, GremlinKubernetesAttackHelper 68 | from gremlinapi.attack_helpers import GremlinBlackholeAttack 69 | 70 | config.api_key = 71 | config.team_id = 72 | 73 | k8s_attacks.new_kubernetes_attack( 74 | body = GremlinKubernetesAttackHelper( 75 | command = GremlinBlackholeAttack(), 76 | target = GremlinKubernetesAttackTargetHelper( 77 | targets = [ 78 | GremlinKubernetesAttackTarget( 79 | cluster_id = "", 80 | namespace = "", 81 | kind = "REPLICASET", 82 | name = " 83 | ) 84 | ], 85 | count = 3 86 | ) 87 | ) 88 | ) 89 | ``` -------------------------------------------------------------------------------- /gremlinapi/saml.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.config import GremlinAPIConfig 9 | from gremlinapi.exceptions import ( 10 | GremlinParameterError, 11 | GremlinAuthError, 12 | ProxyError, 13 | ClientError, 14 | HTTPTimeout, 15 | HTTPError, 16 | ) 17 | 18 | from gremlinapi.gremlinapi import GremlinAPI 19 | from gremlinapi.http_clients import ( 20 | get_gremlin_httpclient, 21 | GremlinAPIHttpClient, 22 | ) 23 | 24 | from http.client import HTTPResponse 25 | 26 | from typing import Union, Type, Any 27 | 28 | 29 | log = logging.getLogger("GremlinAPI.client") 30 | 31 | 32 | class GremlinAPISaml(GremlinAPI): 33 | @classmethod 34 | def acs( 35 | cls, 36 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 37 | *args: tuple, 38 | **kwargs: dict, 39 | ) -> Union[HTTPResponse, Any]: 40 | method: str = "POST" 41 | endpoint: str = "/users/auth/saml/acs" 42 | data: dict = { 43 | "SAMLResponse": cls._error_if_not_param("SAMLResponse", **kwargs), 44 | "RelayState": cls._error_if_not_param("RelayState", **kwargs), 45 | } 46 | payload: dict = cls._payload(**{"headers": https_client.header(), "data": data}) 47 | (resp, body) = https_client.api_call(method, endpoint, **payload) 48 | return resp 49 | 50 | @classmethod 51 | @register_cli_action( 52 | "samllogin", ("companyName", "destination", "acsHandler"), ("",) 53 | ) 54 | def samllogin( 55 | cls, 56 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 57 | *args: tuple, 58 | **kwargs: dict, 59 | ) -> dict: 60 | params: list = ["companyName", "destination", "acsHandler"] 61 | method: str = "GET" 62 | endpoint: str = cls._build_query_string_endpoint( 63 | "/users/auth/saml/login", params, **kwargs 64 | ) 65 | payload: dict = cls._payload() 66 | (resp, body) = https_client.api_call(method, endpoint, **payload) 67 | return body 68 | 69 | @classmethod 70 | @register_cli_action("metadata", ("",), ("",)) 71 | def metadata( 72 | cls, 73 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 74 | *args: tuple, 75 | **kwargs: dict, 76 | ) -> dict: 77 | method: str = "GET" 78 | endpoint: str = "/users/auth/saml/metadata" 79 | payload: dict = cls._payload() 80 | (resp, body) = https_client.api_call(method, endpoint, **payload) 81 | return body 82 | 83 | @classmethod 84 | def sessions( 85 | cls, 86 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 87 | *args: tuple, 88 | **kwargs: dict, 89 | ) -> dict: 90 | method: str = "POST" 91 | endpoint: str = "/users/auth/saml/sessions" 92 | data: dict = {"code": cls._error_if_not_param("code", **kwargs)} 93 | payload: dict = cls._payload(**{"headers": https_client.header(), "body": data}) 94 | (resp, body) = https_client.api_call(method, endpoint, **payload) 95 | return body 96 | -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | api_key: str = "api-key-string" 2 | bearer_token: str = "bearer-token-string" 3 | mock_data = {"testkey": "testval"} 4 | hooli_id = "9676868b-60d2-5ebe-aa66-c1de8162ff9d" 5 | from gremlinapi.attack_helpers import ( 6 | GremlinAttackTargetHelper, 7 | GremlinAttackCommandHelper, 8 | ) 9 | 10 | mock_access_token = "asdf976asdf9786" 11 | mock_bearer_token = "kjhg2345kjhg234" 12 | 13 | 14 | def access_token_json(): 15 | return {"access_token": mock_access_token} 16 | 17 | 18 | def bearer_token_json(): 19 | return {"header": mock_bearer_token} 20 | 21 | 22 | def mock_json(): 23 | return mock_data 24 | 25 | mock_base_url = "https://api.gremlin.com/v1" 26 | 27 | mock_org_id = "1234567890a" 28 | mock_team_id = "1234567890a" 29 | mock_body = {"body": mock_data} 30 | mock_guid = {"guid": mock_data} 31 | mock_scenario_guid = { 32 | "guid": mock_data, 33 | "body": mock_data, 34 | "startDate": "1/1/1900", 35 | "endDate": "1/1/2000", 36 | "runNumber": 1, 37 | "staticEndpointName": "not-a-website.comorg", 38 | } 39 | mock_users = { 40 | "role": "mock user role", 41 | "email": "useremail@useremailfakesite123456789.com", 42 | "password": "1234567890poiuytrewqASDFGHJKL", 43 | "orgId": "102928384756z", 44 | "renewToken": "42", 45 | "companyId": "c0mp4ny", 46 | "companyName": "Mocking Co, A Mockery Company", 47 | "provider": "MacinoogleSoft", 48 | "teamId": "h4x0r734m", 49 | "accessToken": "1q2w3e4r5t6y7u8i9o90p", 50 | "token": "1a1f3d4ca3f41fb4d1cb4cd4bfcd14c", 51 | } 52 | mock_identifier = { 53 | "identifier": mock_data, 54 | "email": "testemail@example.com", 55 | "body": mock_data, 56 | "name": "Gremlin", 57 | } 58 | mock_payload = { 59 | "body": mock_data, 60 | "headers": "1234567890", 61 | "data": mock_data, 62 | } 63 | mock_uid = {"body": mock_data, "uid": "1234567890z"} 64 | mock_metrics = { 65 | "attackId": "1234567890", 66 | "scenarioId": "1234567890", 67 | "scenarioRunNumber": "1", 68 | } 69 | mock_report = { 70 | "start": "mock_start", 71 | "end": "mock_end", 72 | "period": "4", 73 | "startDate": "1/1/1900", 74 | "endDate": "1/1/2000", 75 | "trackingPeriod": "6", 76 | } 77 | mock_saml = { 78 | "SAMLResponse": "mock_response", 79 | "RelayState": "mock_state", 80 | "companyName": "Gremlin Mocks", 81 | "destination": "earth", 82 | "acsHandler": "mock_handler", 83 | "code": "12567890", 84 | } 85 | mock_scenario = { 86 | "description": "A mock scenario", 87 | "hypothesis": "to prove test status", 88 | "name": "mock_scenario", 89 | } 90 | mock_scenario_step = { 91 | "delay": 65536, 92 | "command": GremlinAttackCommandHelper(), 93 | "target": GremlinAttackTargetHelper(), 94 | } 95 | mock_ilfi_node = { 96 | "name": "mock_scenario", 97 | "command": GremlinAttackCommandHelper(), 98 | "target": GremlinAttackTargetHelper(), 99 | } 100 | mock_delay_node = {"delay": "42"} 101 | mock_status_check_node = { 102 | "description": "A mock status check node", 103 | "endpoint_url": "definitely-fake-website1234.com", 104 | "endpoint_headers": {"name": "mock header"}, 105 | "evaluation_response_body_evaluation": "mock evaluation", 106 | "evaluation_ok_status_codes": ["24-42"], 107 | "evaluation_ok_latency_max": 999, 108 | } 109 | 110 | # Reliability Tests Mocks 111 | mock_service_id = '83B2D777-ECBA-4A76-9227-95F4E6814FE7' 112 | mock_dependency_id = 'C117C596-03E4-43BA-8300-AF4E9B629DC8' 113 | mock_reliability_test_id = 'scaling-latency-test' -------------------------------------------------------------------------------- /tests/test_attacks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2021 Kyle Bouchard , Gremlin Inc 4 | 5 | import unittest 6 | import requests 7 | import logging 8 | from unittest.mock import patch 9 | from gremlinapi.attacks import GremlinAPIAttacks 10 | from gremlinapi.attack_helpers import ( 11 | GremlinAttackHelper, 12 | GremlinTargetContainers, 13 | GremlinLatencyAttack, 14 | ) 15 | 16 | from .util import mock_json, mock_data 17 | 18 | 19 | class TestAttacks(unittest.TestCase): 20 | def test_list_endpoint(self) -> None: 21 | test_endpoint = "test-endpoint.com" 22 | expected_output = "%s/?source=scenario&pageSize=3&" % test_endpoint 23 | test_kwargs = {"source": "scenario", "pageSize": 3} 24 | test_output = GremlinAPIAttacks._list_endpoint(test_endpoint, **test_kwargs) 25 | self.assertEqual(test_output, expected_output) 26 | 27 | def test_error_if_not_attack_body(self) -> None: 28 | expected_output_class = GremlinAttackHelper() 29 | test_kwargs = {"body": expected_output_class} 30 | test_output = GremlinAPIAttacks._error_if_not_attack_body(**test_kwargs) 31 | self.assertEqual(test_output, str(expected_output_class)) 32 | 33 | @patch("requests.post") 34 | def test_create_attack_with_decorator(self, mock_get) -> None: 35 | expected_output_class = GremlinAttackHelper() 36 | test_kwargs = {"body": expected_output_class} 37 | mock_get.return_value = requests.Response() 38 | mock_get.return_value.status_code = 200 39 | mock_get.return_value.json = mock_json 40 | self.assertEqual(GremlinAPIAttacks.create_attack(**test_kwargs), mock_data) 41 | 42 | @patch("requests.get") 43 | def test_list_active_attacks_with_decorator(self, mock_get) -> None: 44 | mock_get.return_value = requests.Response() 45 | mock_get.return_value.status_code = 200 46 | mock_get.return_value.json = mock_json 47 | self.assertEqual(GremlinAPIAttacks.list_active_attacks(), mock_data) 48 | 49 | @patch("requests.get") 50 | def test_list_attacks_with_decorator(self, mock_get) -> None: 51 | mock_get.return_value = requests.Response() 52 | mock_get.return_value.status_code = 200 53 | mock_get.return_value.json = mock_json 54 | self.assertEqual(GremlinAPIAttacks.list_attacks(), mock_data) 55 | 56 | @patch("requests.get") 57 | def test_list_completed_attacks_with_decorator(self, mock_get) -> None: 58 | mock_get.return_value = requests.Response() 59 | mock_get.return_value.status_code = 200 60 | mock_get.return_value.json = mock_json 61 | self.assertEqual(GremlinAPIAttacks.list_completed_attacks(), mock_data) 62 | 63 | @patch("requests.get") 64 | def test_get_attack_with_decorator(self, mock_get) -> None: 65 | mock_get.return_value = requests.Response() 66 | mock_get.return_value.status_code = 200 67 | mock_get.return_value.json = mock_json 68 | test_kwargs = {"guid": "1234567890"} 69 | self.assertEqual(GremlinAPIAttacks.get_attack(**test_kwargs), mock_data) 70 | 71 | @patch("requests.delete") 72 | def test_halt_all_attacks_with_decorator(self, mock_get) -> None: 73 | mock_get.return_value = requests.Response() 74 | mock_get.return_value.status_code = 200 75 | mock_get.return_value.json = mock_json 76 | self.assertEqual(GremlinAPIAttacks.halt_all_attacks(), mock_data) 77 | 78 | @patch("requests.delete") 79 | def test_halt_attack_with_decorator(self, mock_get) -> None: 80 | mock_get.return_value = requests.Response() 81 | mock_get.return_value.status_code = 200 82 | mock_get.return_value.json = mock_json 83 | test_kwargs = {"guid": "1234567890"} 84 | self.assertEqual(GremlinAPIAttacks.halt_attack(**test_kwargs), mock_data) 85 | -------------------------------------------------------------------------------- /gremlinapi/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | import functools, warnings, inspect 7 | 8 | log = logging.getLogger("GremlinAPI.client") 9 | 10 | _version = "0.18.3" 11 | 12 | 13 | def get_version(): 14 | return _version 15 | 16 | 17 | string_types = (type(b""), type(""), type(f"")) 18 | 19 | GREMLIN_OAUTH_LOGIN = "https://api.gremlin.com/v1/oauth/login" 20 | GREMLIN_OAUTH_COMPANIES_URI = "https://api.gremlin.com/v1/companies" 21 | GREMLIN_SSO_USER_AUTH = "https://api.gremlin.com/v1/users/auth/sso" 22 | GREMLIN_OAUTH_CALLBACK = "https://api.gremlin.com/v1/oauth/callback" 23 | 24 | MAX_BLAST_RADIUS = 1000 25 | MAX_NODE_COUNT = 50 26 | 27 | def experimental(func): 28 | """ 29 | This is a decorator that will be used on in-progress or 30 | otherwise incomplete functions and objects. 31 | """ 32 | if inspect.isclass(func) or inspect.isfunction(func): 33 | 34 | @functools.wraps(func) 35 | def new_func(*args, **kwargs): 36 | message = "Call to experimental function `{}` ** Please proceed with caution **".format( 37 | func.__name__ 38 | ) 39 | warnings.warn(message) 40 | return func(*args, **kwargs) 41 | 42 | return new_func 43 | 44 | elif isinstance(func, string_types): 45 | 46 | def decorator(func1): 47 | @functools.wraps(func1) 48 | def new_func1(*args, **kwargs): 49 | message = "Call to experimental function `{}` ** %s **" % func 50 | warnings.warn(message) 51 | return func1(*args, **kwargs) 52 | 53 | return new_func1 54 | 55 | return decorator 56 | 57 | 58 | def deprecated(reason): 59 | """ 60 | This is a decorator which can be used to mark functions 61 | as deprecated. It will result in a warning being emitted 62 | when the function is used. 63 | """ 64 | 65 | if inspect.isclass(reason) or inspect.isfunction(reason): 66 | 67 | @functools.wraps(reason) 68 | def new_func(*args, **kwargs): 69 | warnings.simplefilter("always", DeprecationWarning) # turn off filter 70 | warnings.warn( 71 | "Call to deprecated function {}.".format(reason.__name__), 72 | category=DeprecationWarning, 73 | stacklevel=2, 74 | ) 75 | warnings.simplefilter("default", DeprecationWarning) # reset filter 76 | return reason(*args, **kwargs) 77 | 78 | return new_func 79 | 80 | elif isinstance(reason, string_types): 81 | # The @deprecated is used with a 'reason'. 82 | # 83 | # .. code-block:: python 84 | # 85 | # @deprecated("please, use another function") 86 | # def old_function(x, y): 87 | # pass 88 | 89 | def decorator(func1): 90 | 91 | if inspect.isclass(func1): 92 | fmt1 = "Call to deprecated class {name} ({reason})." 93 | else: 94 | fmt1 = "Call to deprecated function {name} ({reason})." 95 | 96 | @functools.wraps(func1) 97 | def new_func1(*args, **kwargs): 98 | warnings.simplefilter("always", DeprecationWarning) 99 | warnings.warn( 100 | fmt1.format(name=func1.__name__, reason=reason), 101 | category=DeprecationWarning, 102 | stacklevel=2, 103 | ) 104 | warnings.simplefilter("default", DeprecationWarning) 105 | return func1(*args, **kwargs) 106 | 107 | return new_func1 108 | 109 | return decorator 110 | 111 | else: 112 | raise TypeError(repr(type(reason))) 113 | -------------------------------------------------------------------------------- /tests/test_oauth.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.oauth import GremlinAPIOAUTH 6 | 7 | from .util import ( 8 | mock_json, 9 | mock_data, 10 | hooli_id, 11 | access_token_json, 12 | mock_access_token, 13 | mock_bearer_token, 14 | bearer_token_json, 15 | ) 16 | 17 | 18 | class TestOAUTH(unittest.TestCase): 19 | @patch("requests.post") 20 | def test_configure_with_decorator(self, mock_get) -> None: 21 | config_body = { 22 | "authorizationUri": "https://oauth.mocklab.io/oauth/authorize", 23 | "tokenUri": "https://oauth.mocklab.io/oauth/token", 24 | "userInfoUri": "https://oauth.mocklab.io/userinfo", 25 | "clientId": "mocklab_oauth2", 26 | "clientSecret": "foo", 27 | "scope": "email", 28 | } 29 | mock_get.return_value = requests.Response() 30 | mock_get.return_value.status_code = 200 31 | mock_get.return_value.json = mock_json 32 | self.assertEqual( 33 | GremlinAPIOAUTH.configure(hooli_id, **config_body), 34 | mock_get.return_value.status_code, 35 | ) 36 | 37 | @patch("requests.get") 38 | def test_initiate_oauth_with_decorator(self, mock_get) -> None: 39 | company_name = "Mock Company" 40 | mock_get.return_value = requests.Response() 41 | mock_get.return_value.status_code = 307 42 | mock_get.return_value.json = mock_json 43 | mock_get.return_value.cookies = {"oauth_state": "mock_oauth_state="} 44 | mock_get.return_value.headers = { 45 | "Location": "mock_uri", 46 | } 47 | state_cookie, oauth_provider_login_url = GremlinAPIOAUTH.initiate_oauth( 48 | company_name 49 | ) 50 | self.assertEqual( 51 | state_cookie, 52 | mock_get.return_value.cookies["oauth_state"], 53 | ) 54 | self.assertEqual( 55 | oauth_provider_login_url, 56 | mock_get.return_value.headers["Location"], 57 | ) 58 | 59 | @patch("requests.post") 60 | def test_get_callback_url_with_decorator(self, mock_post) -> None: 61 | mock_login_uri = "http://example.com/login" 62 | mock_callback_uri = "http:example.com/callback" 63 | auth_body = { 64 | "email": "mock@email.com", 65 | "password": "m0ckp44sw0rd", 66 | "state": "mockstatecookie1234", # obtained in earlier step 67 | "redirectUri": "mockredirect.uri.com", 68 | "clientId": "mock_client_id", 69 | } 70 | mock_post.return_value = requests.Response() 71 | mock_post.return_value.status_code = 200 72 | mock_post.return_value.json = mock_json 73 | mock_post.return_value.headers = { 74 | "Location": mock_callback_uri, 75 | } 76 | self.assertEqual( 77 | GremlinAPIOAUTH.get_callback_url(mock_login_uri, auth_body), 78 | mock_callback_uri, 79 | ) 80 | 81 | @patch("requests.get") 82 | def test_get_access_token_with_decorator(self, mock_get) -> None: 83 | mock_callback_uri = "http:example.com/callback" 84 | mock_state_cookie = "abd3bd14bvd1beb1eabc1bead1badffcb6af1c6bfd6bddcdca6ddc=" 85 | mock_get.return_value = requests.Response() 86 | mock_get.return_value.status_code = 200 87 | mock_get.return_value.json = access_token_json 88 | self.assertEqual( 89 | GremlinAPIOAUTH.get_access_token(mock_state_cookie, mock_callback_uri), 90 | mock_access_token, 91 | ) 92 | 93 | @patch("requests.post") 94 | def test_get_bearer_token_with_decorator(self, mock_get) -> None: 95 | mock_company_name = "Mock Company, Inc." 96 | mock_get.return_value = requests.Response() 97 | mock_get.return_value.status_code = 200 98 | mock_get.return_value.json = bearer_token_json 99 | self.assertEqual( 100 | GremlinAPIOAUTH.get_bearer_token(mock_company_name, mock_access_token), 101 | mock_bearer_token, 102 | ) 103 | -------------------------------------------------------------------------------- /examples/scenarios.md: -------------------------------------------------------------------------------- 1 | # Scenarios 2 | 3 | 4 | ## Create Scenarios 5 | 6 | ### Graph Scenarios 7 | 8 | #### Example 9 | 10 | ```python 11 | from gremlinapi.config import GremlinAPIConfig as config 12 | from gremlinapi.attack_helpers import GremlinBlackholeAttack, GremlinLatencyAttack, GremlinTargetContainers 13 | from gremlinapi.scenario_graph_helpers import (GremlinScenarioGraphHelper, GremlinScenarioILFINode, 14 | GremlinScenarioDelayNode, GremlinScenarioStatusCheckNode) 15 | from gremlinapi.scenarios import GremlinAPIScenarios as scenarios 16 | 17 | my_api_key = "123...xyz" 18 | my_team_guid = 'uuid' 19 | 20 | config.api_key = my_api_key 21 | config.team_guid = my_team_guid 22 | 23 | description = f'This is a test scenario to illustrate the usage of the new scenario graph model' 24 | hypothesis = f'This should create a functional scenario with multiple attack iterations and a status_check' 25 | 26 | blast_radius_steps = [50, 100] # Percents 27 | latency_magnitude_steps = [100, 500, 1000] # Latency in milliseconds 28 | delay_time = 5 # Time to delay between steps in seconds 29 | 30 | # Status check config 31 | status_check_description="Check if Gremlin.com is Still Up" 32 | 33 | endpoint_url = 'https://www.gremlin.com' 34 | endpoint_headers = dict() 35 | 36 | # Optionally you can set specific header attributes 37 | # status_check_api_key = 'Key foo...' 38 | # endpoint_headers = { 39 | # 'Authorization': status_check_api_key 40 | # } 41 | 42 | evaluation_ok_status_codes = ['200-203', '210'] 43 | evaluation_ok_latency_max = 500 44 | evaluation_response_body_evaluation = { 45 | "op": "AND", 46 | "predicates": [] 47 | } 48 | 49 | my_scenario = GremlinScenarioGraphHelper(name='test_scenario_1', description=description, hypothesis=hypothesis) 50 | 51 | # Add the status check to the start of the scenario 52 | status_check_node = GremlinScenarioStatusCheckNode( 53 | description=status_check_description, 54 | endpoint_url=endpoint_url, 55 | endpoint_headers=endpoint_headers, 56 | evaluation_ok_status_codes=evaluation_ok_status_codes, 57 | evaluation_ok_latency_max=evaluation_ok_latency_max, 58 | evaluation_response_body_evaluation=evaluation_response_body_evaluation 59 | ) 60 | my_scenario.add_node(status_check_node) 61 | 62 | # Add a delay step between the status check and the attacks 63 | delay_node = GremlinScenarioDelayNode(delay=delay_time) 64 | 65 | #adds node to graph 66 | my_scenario.add_node(delay_node) 67 | 68 | #adds edge 69 | my_scenario.add_edge(status_check_node, delay_node) 70 | 71 | # Add latency attacks to the scenario 72 | for magnitude_idx, magnitude in enumerate(latency_magnitude_steps): 73 | for blast_radius_idx, blast_radius in enumerate(blast_radius_steps): 74 | print(f'Blast radius {blast_radius_idx+1} of {len(blast_radius_steps)} :: {blast_radius}%') 75 | print(f'Magnitude {magnitude_idx+1} of {len(latency_magnitude_steps)} :: {magnitude}ms') 76 | new_node = GremlinScenarioILFINode( 77 | command=GremlinLatencyAttack(delay=magnitude), 78 | target=GremlinTargetContainers(strategy_type='Random', labels={'owner': 'kyle'}, percent=blast_radius) 79 | ) 80 | # Passing True as the second variable adds an edge from the last node in the graph to the 81 | # node being added 82 | my_scenario.add_node(new_node, True) 83 | # Add a delay step between attacks, skip this if it's the last step in the loop 84 | if not ((magnitude_idx+1 == len(latency_magnitude_steps)) and (blast_radius_idx+1 == len(blast_radius_steps))): 85 | my_scenario.add_node(GremlinScenarioDelayNode(delay=delay_time), True) 86 | 87 | # Let's view the json output 88 | print(my_scenario) 89 | 90 | # Send the scenario to Gremlin 91 | my_scenario_guid = scenarios.create_scenario(body=my_scenario) 92 | ``` 93 | 94 | ## Halt Scenarios 95 | ```python 96 | from gremlinapi.scenarios import GremlinAPIScenarios as scenarios 97 | team_id = 'TEAM_ID/UUID' 98 | scenario_id = 'SCENARIO_ID' 99 | scenario_run = 'SCENARIO_RUN_ID' 100 | 101 | confirmation = scenarios.halt_scenario(teamId=team_id, guid=scenario_id, runNumber=scenario_run) 102 | ``` -------------------------------------------------------------------------------- /gremlinapi/clients.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from typing import Union, Type, Any 8 | 9 | from gremlinapi.cli import register_cli_action 10 | from gremlinapi.exceptions import ( 11 | GremlinParameterError, 12 | ProxyError, 13 | ClientError, 14 | HTTPTimeout, 15 | HTTPError, 16 | ) 17 | from gremlinapi.http_clients import GremlinAPIHttpClient 18 | from gremlinapi.config import GremlinAPIConfig 19 | from gremlinapi.gremlinapi import GremlinAPI 20 | from gremlinapi.http_clients import get_gremlin_httpclient 21 | 22 | log = logging.getLogger("GremlinAPI.client") 23 | 24 | 25 | class GremlinAPIClients(GremlinAPI): 26 | @classmethod 27 | @register_cli_action("activate_client", ("guid",), ("teamId",)) 28 | def activate_client( 29 | cls, 30 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 31 | **kwargs: dict, 32 | ) -> dict: 33 | method: str = "PUT" 34 | guid: str = cls._error_if_not_param("guid", **kwargs) 35 | endpoint: str = cls._optional_team_endpoint( 36 | f"/clients/{guid}/activate", **kwargs 37 | ) 38 | payload: dict = cls._payload(**{"headers": https_client.header()}) 39 | (resp, body) = https_client.api_call(method, endpoint, **payload) 40 | return body 41 | 42 | @classmethod 43 | @register_cli_action("deactivate_client", ("guid",), ("teamId",)) 44 | def deactivate_client( 45 | cls, 46 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 47 | **kwargs: dict, 48 | ) -> dict: 49 | method: str = "DELETE" 50 | guid: str = cls._error_if_not_param("guid", **kwargs) 51 | endpoint: str = cls._optional_team_endpoint(f"/clients/{guid}", **kwargs) 52 | payload: dict = cls._payload(**{"headers": https_client.header()}) 53 | (resp, body) = https_client.api_call(method, endpoint, **payload) 54 | return body 55 | 56 | @classmethod 57 | @register_cli_action("list_active_clients", ("",), ("teamId",)) 58 | def list_active_clients( 59 | cls, 60 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 61 | **kwargs: dict, 62 | ) -> dict: 63 | method: str = "GET" 64 | endpoint: str = cls._optional_team_endpoint(f"/clients/active", **kwargs) 65 | payload: dict = cls._payload(**{"headers": https_client.header()}) 66 | (resp, body) = https_client.api_call(method, endpoint, **payload) 67 | return body 68 | 69 | @classmethod 70 | @register_cli_action("list_clients", ("",), ("teamId",)) 71 | def list_clients( 72 | cls, 73 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 74 | **kwargs: dict, 75 | ) -> dict: 76 | method: str = "GET" 77 | endpoint: str = cls._optional_team_endpoint(f"/clients", **kwargs) 78 | payload: dict = cls._payload(**{"headers": https_client.header()}) 79 | (resp, body) = https_client.api_call(method, endpoint, **payload) 80 | return body 81 | 82 | @classmethod 83 | def get_update_client_target_cache(cls) -> [dict]: 84 | if (not GremlinAPIConfig.client_cache) or (type(GremlinAPIConfig.client_cache) == type(property())): 85 | GremlinAPIConfig.client_cache = GremlinAPIClients.list_clients() 86 | # Collects all containers 87 | total_containers = [] 88 | active_clients = GremlinAPIConfig.client_cache['active'] 89 | for ac in active_clients: 90 | for container in ac['containers']: 91 | total_containers.append(container) 92 | inactive_clients = GremlinAPIConfig.client_cache['inactive'] 93 | for iac in inactive_clients: 94 | for container in iac['containers']: 95 | total_containers.append(container) 96 | idle_clients = GremlinAPIConfig.client_cache['idle'] 97 | for ic in idle_clients: 98 | for container in ic['containers']: 99 | total_containers.append(container) 100 | return total_containers 101 | -------------------------------------------------------------------------------- /gremlinapi/alfi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.exceptions import ( 9 | GremlinParameterError, 10 | ProxyError, 11 | ClientError, 12 | HTTPTimeout, 13 | HTTPError, 14 | ) 15 | 16 | from gremlinapi.gremlinapi import GremlinAPI 17 | from gremlinapi.http_clients import ( 18 | get_gremlin_httpclient, 19 | GremlinAPIHttpClient, 20 | ) 21 | from typing import Union, Type 22 | 23 | log = logging.getLogger("GremlinAPI.client") 24 | 25 | 26 | class GremlinALFI(GremlinAPI): 27 | @classmethod 28 | @register_cli_action("create_alfi_experiment", ("body",), ("teamId",)) 29 | def create_alfi_experiment( 30 | cls, 31 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 32 | *args: tuple, 33 | **kwargs: dict, 34 | ) -> dict: 35 | method: str = "POST" 36 | data: dict = cls._error_if_not_json_body(**kwargs) 37 | endpoint: str = cls._optional_team_endpoint("/experiments", **kwargs) 38 | payload: dict = cls._payload(**{"headers": https_client.header(), "body": data}) 39 | (resp, body) = https_client.api_call(method, endpoint, **payload) 40 | return body 41 | 42 | @classmethod 43 | @register_cli_action("halt_all_alfi_experiments", ("",), ("teamId",)) 44 | def halt_all_alfi_experiments( 45 | cls, 46 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 47 | *args: tuple, 48 | **kwargs: dict, 49 | ) -> dict: 50 | method: str = "DELETE" 51 | endpoint: str = cls._optional_team_endpoint("/experiments", **kwargs) 52 | payload: dict = cls._payload(**{"headers": https_client.header()}) 53 | (resp, body) = https_client.api_call(method, endpoint, **payload) 54 | return body 55 | 56 | @classmethod 57 | @register_cli_action("get_alfi_experiment_details", ("guid",), ("teamId",)) 58 | def get_alfi_experiment_details( 59 | cls, 60 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 61 | *args: tuple, 62 | **kwargs: dict, 63 | ) -> dict: 64 | method: str = "GET" 65 | guid: str = cls._error_if_not_param("guid", **kwargs) 66 | endpoint: str = cls._optional_team_endpoint(f"/experiments/{guid}", **kwargs) 67 | payload: dict = cls._payload(**{"headers": https_client.header()}) 68 | (resp, body) = https_client.api_call(method, endpoint, **payload) 69 | return body 70 | 71 | @classmethod 72 | @register_cli_action("halt_alfi_experiment", ("guid",), ("teamId",)) 73 | def halt_alfi_experiment( 74 | cls, 75 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 76 | *args: tuple, 77 | **kwargs: dict, 78 | ) -> dict: 79 | method: str = "DELETE" 80 | guid: str = cls._error_if_not_param("guid", **kwargs) 81 | endpoint: str = cls._optional_team_endpoint(f"/experiments/{guid}", **kwargs) 82 | payload: dict = cls._payload(**{"headers": https_client.header()}) 83 | (resp, body) = https_client.api_call(method, endpoint, **payload) 84 | return body 85 | 86 | @classmethod 87 | @register_cli_action("list_active_alfi_experiments", ("",), ("teamId",)) 88 | def list_active_alfi_experiments( 89 | cls, 90 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 91 | *args: tuple, 92 | **kwargs: dict, 93 | ) -> dict: 94 | method: str = "GET" 95 | endpoint: str = cls._optional_team_endpoint("/experiments/active", **kwargs) 96 | payload: dict = cls._payload(**{"headers": https_client.header()}) 97 | (resp, body) = https_client.api_call(method, endpoint, **payload) 98 | return body 99 | 100 | @classmethod 101 | @register_cli_action("list_completed_alfi_experiments", ("",), ("teamId",)) 102 | def list_completed_alfi_experiments( 103 | cls, 104 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 105 | *args: tuple, 106 | **kwargs: dict, 107 | ) -> dict: 108 | method: str = "GET" 109 | endpoint: str = cls._optional_team_endpoint("/experiments/completed", **kwargs) 110 | payload: dict = cls._payload(**{"headers": https_client.header()}) 111 | (resp, body) = https_client.api_call(method, endpoint, **payload) 112 | return body 113 | -------------------------------------------------------------------------------- /gremlinapi/kubernetes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.exceptions import ( 9 | GremlinParameterError, 10 | ProxyError, 11 | ClientError, 12 | HTTPTimeout, 13 | HTTPError, 14 | ) 15 | 16 | from gremlinapi.gremlinapi import GremlinAPI 17 | from gremlinapi.http_clients import ( 18 | get_gremlin_httpclient, 19 | GremlinAPIHttpClient, 20 | ) 21 | 22 | from typing import Union, Type 23 | 24 | log = logging.getLogger("GremlinAPI.client") 25 | 26 | 27 | class GremlinAPIKubernetesAttacks(GremlinAPI): 28 | @classmethod 29 | @register_cli_action("list_all_kubernetes_attacks", ("",), ("teamId",)) 30 | def list_all_kubernetes_attacks( 31 | cls, 32 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 33 | *args: tuple, 34 | **kwargs: dict, 35 | ) -> dict: 36 | method: str = "GET" 37 | endpoint: str = cls._optional_team_endpoint("/kubernetes/attacks", **kwargs) 38 | payload: dict = cls._payload(**{"headers": https_client.header()}) 39 | (resp, body) = https_client.api_call(method, endpoint, **payload) 40 | return body 41 | 42 | @classmethod 43 | @register_cli_action("get_kubernetes_attack", ("uid",), ("teamId",)) 44 | def get_kubernetes_attack( 45 | cls, 46 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 47 | *args: tuple, 48 | **kwargs: dict, 49 | ) -> dict: 50 | method: str = "GET" 51 | uid: str = cls._error_if_not_param("uid", **kwargs) 52 | endpoint: str = cls._optional_team_endpoint( 53 | f"/kubernetes/attacks/{uid}", **kwargs 54 | ) 55 | payload: dict = cls._payload(**{"headers": https_client.header()}) 56 | (resp, body) = https_client.api_call(method, endpoint, **payload) 57 | return body 58 | 59 | @classmethod 60 | @register_cli_action("halt_kubernetes_attack", ("uid",), ("teamId",)) 61 | def halt_kubernetes_attack( 62 | cls, 63 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 64 | *args: tuple, 65 | **kwargs: dict, 66 | ) -> dict: 67 | method: str = "POST" 68 | uid: str = cls._error_if_not_param("uid", **kwargs) 69 | endpoint: str = cls._optional_team_endpoint( 70 | f"/kubernetes/attacks/{uid}/halt", **kwargs 71 | ) 72 | payload: dict = cls._payload(**{"headers": https_client.header()}) 73 | (resp, body) = https_client.api_call(method, endpoint, **payload) 74 | return body 75 | 76 | @classmethod 77 | @register_cli_action("halt_all_kubernetes_attacks", ("",), ("teamId",)) 78 | def halt_all_kubernetes_attacks( 79 | cls, 80 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 81 | *args: tuple, 82 | **kwargs: dict, 83 | ) -> dict: 84 | method: str = "POST" 85 | endpoint: str = cls._optional_team_endpoint( 86 | "/kubernetes/attacks/halt", **kwargs 87 | ) 88 | payload: dict = cls._payload(**{"headers": https_client.header()}) 89 | (resp, body) = https_client.api_call(method, endpoint, **payload) 90 | return body 91 | 92 | @classmethod 93 | @register_cli_action("new_kubernetes_attack", ("body",), ("teamId",)) 94 | def new_kubernetes_attack( 95 | cls, 96 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 97 | *args: tuple, 98 | **kwargs: dict, 99 | ) -> dict: 100 | method: str = "POST" 101 | data: dict = cls._error_if_not_json_body(**kwargs) 102 | endpoint: str = cls._optional_team_endpoint("/kubernetes/attacks/new", **kwargs) 103 | payload: dict = cls._payload(**{"headers": https_client.header(), "body": data}) 104 | (resp, body) = https_client.api_call(method, endpoint, **payload) 105 | return body 106 | 107 | 108 | class GremlinAPIKubernetesTargets(GremlinAPI): 109 | @classmethod 110 | @register_cli_action("list_kubernetes_targets", ("",), ("teamId",)) 111 | def list_kubernetes_targets( 112 | cls, 113 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 114 | *args: tuple, 115 | **kwargs: dict, 116 | ) -> dict: 117 | method: str = "GET" 118 | endpoint: str = cls._optional_team_endpoint("/kubernetes/targets", **kwargs) 119 | payload: dict = cls._payload(**{"headers": https_client.header()}) 120 | (resp, body) = https_client.api_call(method, endpoint, **payload) 121 | return body 122 | -------------------------------------------------------------------------------- /gremlinapi/orgs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.cli import register_cli_action 8 | from gremlinapi.exceptions import ( 9 | GremlinParameterError, 10 | ProxyError, 11 | ClientError, 12 | HTTPTimeout, 13 | HTTPError, 14 | ) 15 | 16 | from gremlinapi.gremlinapi import GremlinAPI 17 | from gremlinapi.http_clients import ( 18 | get_gremlin_httpclient, 19 | GremlinAPIHttpClient, 20 | ) 21 | 22 | from typing import Union, Type 23 | 24 | 25 | log = logging.getLogger("GremlinAPI.client") 26 | 27 | 28 | class GremlinAPIOrgs(GremlinAPI): 29 | @classmethod 30 | @register_cli_action("list_orgs", ("",), ("",)) 31 | def list_orgs( 32 | cls, 33 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 34 | *args: tuple, 35 | **kwargs: dict, 36 | ) -> dict: 37 | method: str = "GET" 38 | endpoint: str = "/orgs" 39 | payload: dict = cls._payload(**{"headers": https_client.header()}) 40 | (resp, body) = https_client.api_call(method, endpoint, **payload) 41 | return body 42 | 43 | @classmethod 44 | @register_cli_action("get_org", ("identifier",), ("",)) 45 | def get_org( 46 | cls, 47 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 48 | *args: tuple, 49 | **kwargs: dict, 50 | ) -> dict: 51 | method: str = "GET" 52 | identifier: str = cls._error_if_not_param("identifier", **kwargs) 53 | endpoint: str = f"/orgs/{identifier}" 54 | payload: dict = cls._payload(**{"headers": https_client.header()}) 55 | (resp, body) = https_client.api_call(method, endpoint, **payload) 56 | return body 57 | 58 | @classmethod 59 | @register_cli_action("create_org", ("name",), ("addUser",)) 60 | def create_org( 61 | cls, 62 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 63 | *args: tuple, 64 | **kwargs: dict, 65 | ) -> dict: 66 | method: str = "POST" 67 | add_user: str = cls._info_if_not_param("addUser", True, **kwargs) 68 | endpoint: str = cls._add_query_param("/orgs", "addUser", add_user) 69 | data: dict = {"name": cls._error_if_not_param("name", **kwargs)} 70 | payload: dict = cls._payload(**{"headers": https_client.header(), "data": data}) 71 | (resp, body) = https_client.api_call(method, endpoint, **payload) 72 | return body 73 | 74 | @classmethod 75 | @register_cli_action("new_certificate", ("",), ("teamId",)) 76 | def new_certificate( 77 | cls, 78 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 79 | *args: tuple, 80 | **kwargs: dict, 81 | ) -> dict: 82 | method: str = "POST" 83 | endpoint: str = cls._optional_team_endpoint("/orgs/auth/certificate", **kwargs) 84 | payload: dict = cls._payload(**{"headers": https_client.header()}) 85 | (resp, body) = https_client.api_call(method, endpoint, **payload) 86 | return body 87 | 88 | @classmethod 89 | @register_cli_action("delete_certificate", ("",), ("teamId",)) 90 | def delete_certificate( 91 | cls, 92 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 93 | *args: tuple, 94 | **kwargs: dict, 95 | ) -> dict: 96 | method: str = "DELETE" 97 | endpoint: str = cls._optional_team_endpoint("/orgs/auth/certificate", **kwargs) 98 | payload: dict = cls._payload(**{"headers": https_client.header()}) 99 | (resp, body) = https_client.api_call(method, endpoint, **payload) 100 | return body 101 | 102 | @classmethod 103 | @register_cli_action("delete_old_certificate", ("",), ("teamId",)) 104 | def delete_old_certificate( 105 | cls, 106 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 107 | *args: tuple, 108 | **kwargs: dict, 109 | ) -> dict: 110 | method: str = "DELETE" 111 | endpoint: str = cls._optional_team_endpoint( 112 | "/orgs/auth/certificate/old", **kwargs 113 | ) 114 | payload: dict = cls._payload(**{"headers": https_client.header()}) 115 | (resp, body) = https_client.api_call(method, endpoint, **payload) 116 | return body 117 | 118 | @classmethod 119 | @register_cli_action("reset_secret", ("",), ("identifier", "teamId")) 120 | def reset_secret( 121 | cls, 122 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 123 | *args: tuple, 124 | **kwargs: dict, 125 | ) -> dict: 126 | method: str = "POST" 127 | endpoint: str = cls._optional_team_endpoint("/orgs/auth/secret/reset", **kwargs) 128 | data: dict = dict() 129 | identifier: str = cls._info_if_not_param("identifier", **kwargs) 130 | if identifier: 131 | data["identifier"] = identifier 132 | payload: dict = cls._payload(**{"headers": https_client.header(), "data": data}) 133 | (resp, body) = https_client.api_call(method, endpoint, **payload) 134 | return body 135 | -------------------------------------------------------------------------------- /gremlinapi/templates.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from gremlinapi.util import deprecated 8 | from gremlinapi.cli import register_cli_action 9 | from gremlinapi.exceptions import ( 10 | GremlinParameterError, 11 | ProxyError, 12 | ClientError, 13 | HTTPTimeout, 14 | HTTPError, 15 | ) 16 | 17 | from typing import Type, Union 18 | 19 | from gremlinapi.gremlinapi import GremlinAPI 20 | from gremlinapi.http_clients import ( 21 | get_gremlin_httpclient, 22 | GremlinAPIHttpClient, 23 | ) 24 | 25 | 26 | log = logging.getLogger("GremlinAPI.client") 27 | 28 | 29 | class GremlinAPITemplates(GremlinAPI): 30 | @classmethod 31 | @register_cli_action("list_templates", ("",), ("teamId",)) 32 | @deprecated 33 | def list_templates( 34 | cls, 35 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 36 | *args: tuple, 37 | **kwargs: dict, 38 | ) -> dict: 39 | method: str = "GET" 40 | endpoint: str = cls._optional_team_endpoint(f"/templates", **kwargs) 41 | payload: dict = cls._payload(**{"headers": https_client.header()}) 42 | (resp, body) = https_client.api_call(method, endpoint, **payload) 43 | return body 44 | 45 | @classmethod 46 | @register_cli_action("create_template", ("body",), ("teamId",)) 47 | @deprecated 48 | def create_template( 49 | cls, 50 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 51 | *args: tuple, 52 | **kwargs: dict, 53 | ) -> dict: 54 | method: str = "POST" 55 | data: dict = cls._error_if_not_json_body(**kwargs) 56 | endpoint: str = cls._optional_team_endpoint(f"/templates", **kwargs) 57 | payload: dict = cls._payload(**{"headers": https_client.header(), "body": data}) 58 | (resp, body) = https_client.api_call(method, endpoint, **payload) 59 | return body 60 | 61 | @classmethod 62 | @register_cli_action("get_template", ("guid",), ("teamId",)) 63 | @deprecated 64 | def get_template( 65 | cls, 66 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 67 | *args: tuple, 68 | **kwargs: dict, 69 | ) -> dict: 70 | method: str = "GET" 71 | guid: str = cls._error_if_not_param("guid", **kwargs) 72 | endpoint: str = cls._optional_team_endpoint(f"/templates/{guid}", **kwargs) 73 | payload: dict = cls._payload(**{"headers": https_client.header()}) 74 | (resp, body) = https_client.api_call(method, endpoint, **payload) 75 | return body 76 | 77 | @classmethod 78 | @register_cli_action("delete_template", ("guid",), ("teamId",)) 79 | @deprecated 80 | def delete_template( 81 | cls, 82 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 83 | *args: tuple, 84 | **kwargs: dict, 85 | ) -> dict: 86 | method: str = "DELETE" 87 | guid: str = cls._error_if_not_param("guid", **kwargs) 88 | endpoint: str = cls._optional_team_endpoint(f"/templates/{guid}", **kwargs) 89 | payload: dict = cls._payload(**{"headers": https_client.header()}) 90 | (resp, body) = https_client.api_call(method, endpoint, **payload) 91 | return body 92 | 93 | @classmethod 94 | @register_cli_action("list_command_templates", ("",), ("teamId",)) 95 | @deprecated 96 | def list_command_templates( 97 | cls, 98 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 99 | *args: tuple, 100 | **kwargs: dict, 101 | ) -> dict: 102 | method: str = "GET" 103 | endpoint: str = cls._optional_team_endpoint(f"/templates/command", **kwargs) 104 | payload: dict = cls._payload(**{"headers": https_client.header()}) 105 | (resp, body) = https_client.api_call(method, endpoint, **payload) 106 | return body 107 | 108 | @classmethod 109 | @register_cli_action("list_target_templates", ("",), ("teamId",)) 110 | @deprecated 111 | def list_target_templates( 112 | cls, 113 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 114 | *args: tuple, 115 | **kwargs: dict, 116 | ) -> dict: 117 | method: str = "GET" 118 | endpoint: str = cls._optional_team_endpoint(f"/templates/target", **kwargs) 119 | payload: dict = cls._payload(**{"headers": https_client.header()}) 120 | (resp, body) = https_client.api_call(method, endpoint, **payload) 121 | return body 122 | 123 | @classmethod 124 | @register_cli_action("list_trigger_templates", ("",), ("teamId",)) 125 | @deprecated 126 | def list_trigger_templates( 127 | cls, 128 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 129 | *args: tuple, 130 | **kwargs: dict, 131 | ) -> dict: 132 | method: str = "GET" 133 | endpoint: str = cls._optional_team_endpoint(f"/templates/trigger", **kwargs) 134 | payload: dict = cls._payload(**{"headers": https_client.header()}) 135 | (resp, body) = https_client.api_call(method, endpoint, **payload) 136 | return body 137 | -------------------------------------------------------------------------------- /examples/team-enable.py: -------------------------------------------------------------------------------- 1 | import getpass 2 | from gremlinapi.config import GremlinAPIConfig as config 3 | from gremlinapi.clients import GremlinAPIClients as clients 4 | from gremlinapi.orgs import GremlinAPIOrgs as orgs 5 | import sys 6 | 7 | def enable_all(team_id, targets) -> bool: 8 | success = True 9 | for rover in targets: 10 | if clients.activate_client(teamId=team_id, guid=rover['identifier']) != 'Success': 11 | success = False 12 | return success 13 | 14 | def disable_all(team_id, targets) -> bool: 15 | success = True 16 | for rover in targets: 17 | if clients.deactivate_client(teamId=team_id, guid=rover['identifier']) != 'Success': 18 | success = False 19 | return success 20 | 21 | def confirm_yes() -> bool: 22 | while True: 23 | answer = input("Proceed? Enter \"yes\" or \"no\": ").casefold() 24 | if answer == 'yes': 25 | return True 26 | elif answer == 'no': 27 | return False 28 | else: 29 | print(f"""\"{answer}\" is not valid.""") 30 | 31 | def interactive_enable_agents() -> bool: 32 | print(""" 33 | This application needs your Gremlin API Key. Managing Gremlin API Keys is done at 34 | https://app.gremlin.com/profile/apikeys 35 | 36 | The next prompt, where you enter your Gremlin API Key, does not echo what you type to help keep your 37 | Gremlin API Key protected. Ctrl + Shift + V works to paste in Linux and Windows. Command + V works 38 | to paste in macOS. Your Gremlin API Key can be copied from the webpage by clicking the double 39 | document copy button next to the API Key name then clicking the popup. 40 | """) 41 | config.api_key = getpass.getpass('Enter your Gremlin API Key: ') 42 | all_orgs = orgs.list_orgs() 43 | name_and_id = [(x['name'].casefold(), x['name'], x['identifier']) for x in all_orgs] 44 | name_and_id.sort() 45 | print(f""" 46 | There are {len(name_and_id)} teams accessible. Please enter the number or name of the team you want 47 | to change. 48 | """) 49 | for index, rover in enumerate(name_and_id): 50 | print(f" {index+1:>2} {rover[1]}") 51 | print() 52 | team = input('Enter the team to change: ') 53 | try: 54 | team_index = int(team) - 1 55 | except ValueError: 56 | folded = team.casefold() 57 | for index, rover in enumerate(name_and_id): 58 | if folded == rover[0]: 59 | team_index = index 60 | try: 61 | team_index 62 | team_index_valid = True 63 | except NameError: 64 | team_index_valid = False 65 | if team_index_valid: 66 | if team_index >= len(name_and_id): 67 | print(f""" 68 | {team} is too big. The maximum is {len(name_and_id)}. 69 | """) 70 | return False 71 | if team_index < 0: 72 | print(f""" 73 | {team} is too small. The minimum is 1. 74 | """) 75 | return False 76 | else: 77 | # Try a substring search? Or a prefix search? 78 | print(f""" 79 | {team} is not a valid number nor is it a valid team name. 80 | """) 81 | return False 82 | team_id = name_and_id[team_index][2] 83 | targets = clients.list_clients(teamId=team_id) 84 | print(f""" 85 | The \"{name_and_id[team_index][1]}\" team has {len(targets['active'])+len(targets['idle'])} enabled Agent(s) and {len(targets['inactive'])} disabled Agent(s). 86 | 87 | Do you want all the Agents to be enabled or disabled? 88 | """) 89 | action = input("Enter \"enabled\" or \"disabled\": ").casefold() 90 | success = True 91 | if action == 'enabled': 92 | targets = targets['inactive'] 93 | print() 94 | if len(targets) > 0: 95 | if len(targets) <= 20: 96 | print("The following disabled Agents will be enabled...") 97 | print() 98 | for rover in targets: 99 | print(" ", rover['identifier']) 100 | else: 101 | print(f"""The {len(targets)} disabled Agents will be enabled.""") 102 | print() 103 | if confirm_yes(): 104 | if not enable_all(team_id, targets): 105 | success = False 106 | else: 107 | print("There are no disabled Agents to enable.") 108 | print() 109 | elif action == 'disabled': 110 | targets = targets['active'] + targets['idle'] 111 | print() 112 | if len(targets) > 0: 113 | if len(targets) <= 20: 114 | print("The following enabled Agents will be disabled...") 115 | print() 116 | for rover in targets: 117 | print(" ", rover['identifier']) 118 | else: 119 | print(f"""The {len(targets)} enabled Agents will be disabled.""") 120 | print() 121 | if confirm_yes(): 122 | if not disable_all(team_id, targets): 123 | success = False 124 | else: 125 | print("There are no enabled Agents to disable.") 126 | print() 127 | else: 128 | print(f""" 129 | {action} is not a valid action. 130 | """) 131 | success = False 132 | return success 133 | 134 | if __name__ == '__main__': 135 | sys.exit(interactive_enable_agents()) 136 | -------------------------------------------------------------------------------- /tests/test_companies.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.companies import GremlinAPICompanies 6 | 7 | from .util import mock_json, mock_data, mock_identifier, hooli_id 8 | 9 | 10 | class TestCompanies(unittest.TestCase): 11 | @patch("requests.get") 12 | def test_get_company_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual(GremlinAPICompanies.get_company(**mock_identifier), mock_data) 17 | 18 | @patch("requests.get") 19 | def test_list_company_clients_with_decorator(self, mock_get) -> None: 20 | mock_get.return_value = requests.Response() 21 | mock_get.return_value.status_code = 200 22 | mock_get.return_value.json = mock_json 23 | self.assertEqual( 24 | GremlinAPICompanies.list_company_clients(**mock_identifier), mock_data 25 | ) 26 | 27 | @patch("requests.post") 28 | def test_invite_company_user_with_decorator(self, mock_get) -> None: 29 | mock_get.return_value = requests.Response() 30 | mock_get.return_value.status_code = 200 31 | mock_get.return_value.json = mock_json 32 | self.assertEqual( 33 | GremlinAPICompanies.invite_company_user(**mock_identifier), mock_data 34 | ) 35 | 36 | @patch("requests.delete") 37 | def test_delete_company_invite_with_decorator(self, mock_get) -> None: 38 | mock_get.return_value = requests.Response() 39 | mock_get.return_value.status_code = 200 40 | mock_get.return_value.json = mock_json 41 | self.assertEqual( 42 | GremlinAPICompanies.delete_company_invite(**mock_identifier), mock_data 43 | ) 44 | 45 | @patch("requests.post") 46 | def test_company_mfa_prefs_with_decorator(self, mock_get) -> None: 47 | mock_get.return_value = requests.Response() 48 | mock_get.return_value.status_code = 200 49 | mock_get.return_value.json = mock_json 50 | self.assertEqual( 51 | GremlinAPICompanies.company_mfa_prefs(**mock_identifier), mock_data 52 | ) 53 | 54 | @patch("requests.post") 55 | def test_update_company_prefs_with_decorator(self, mock_get) -> None: 56 | mock_get.return_value = requests.Response() 57 | mock_get.return_value.status_code = 200 58 | mock_get.return_value.json = mock_json 59 | self.assertEqual( 60 | GremlinAPICompanies.update_company_prefs(**mock_identifier), mock_data 61 | ) 62 | 63 | @patch("requests.post") 64 | def test_update_company_saml_props_with_decorator(self, mock_get) -> None: 65 | mock_get.return_value = requests.Response() 66 | mock_get.return_value.status_code = 200 67 | mock_get.return_value.json = mock_json 68 | self.assertEqual( 69 | GremlinAPICompanies.update_company_saml_props(**mock_identifier), mock_data 70 | ) 71 | 72 | @patch("requests.get") 73 | def test_list_company_users_with_decorator(self, mock_get) -> None: 74 | mock_get.return_value = requests.Response() 75 | mock_get.return_value.status_code = 200 76 | mock_get.return_value.json = mock_json 77 | self.assertEqual( 78 | GremlinAPICompanies.list_company_users(**mock_identifier), mock_data 79 | ) 80 | 81 | @patch("requests.put") 82 | def test_update_company_user_role_with_decorator(self, mock_get) -> None: 83 | mock_get.return_value = requests.Response() 84 | mock_get.return_value.status_code = 200 85 | mock_get.return_value.json = mock_json 86 | self.assertEqual( 87 | GremlinAPICompanies.update_company_user_role(**mock_identifier), mock_data 88 | ) 89 | 90 | @patch("requests.post") 91 | def test_activate_company_user_with_decorator(self, mock_get) -> None: 92 | mock_get.return_value = requests.Response() 93 | mock_get.return_value.status_code = 200 94 | mock_get.return_value.json = mock_json 95 | self.assertEqual( 96 | GremlinAPICompanies.activate_company_user(**mock_identifier), mock_data 97 | ) 98 | 99 | @patch("requests.delete") 100 | def test_deactivate_company_user_with_decorator(self, mock_get) -> None: 101 | mock_get.return_value = requests.Response() 102 | mock_get.return_value.status_code = 200 103 | mock_get.return_value.json = mock_json 104 | self.assertEqual( 105 | GremlinAPICompanies.deactivate_company_user(**mock_identifier), mock_data 106 | ) 107 | 108 | @patch("requests.post") 109 | def test_auth_toggles_with_decorator(self, mock_get) -> None: 110 | toggles_body = { 111 | "companyId": hooli_id, 112 | "passwordEnabled": True, 113 | "mfaRequired": False, 114 | "googleEnabled": True, 115 | "oauthEnabled": True, 116 | "samlEnabled": True, 117 | "claimsRequired": False, 118 | } 119 | mock_get.return_value = requests.Response() 120 | mock_get.return_value.status_code = 200 121 | mock_get.return_value.json = mock_json 122 | self.assertEqual( 123 | GremlinAPICompanies.auth_toggles(**toggles_body), 124 | mock_get.return_value.status_code, 125 | ) 126 | -------------------------------------------------------------------------------- /gremlinapi/reports.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import logging 6 | 7 | from datetime import date 8 | 9 | from gremlinapi.cli import register_cli_action 10 | from gremlinapi.exceptions import ( 11 | GremlinParameterError, 12 | ProxyError, 13 | ClientError, 14 | HTTPTimeout, 15 | HTTPError, 16 | ) 17 | 18 | from gremlinapi.gremlinapi import GremlinAPI 19 | from gremlinapi.http_clients import ( 20 | get_gremlin_httpclient, 21 | GremlinAPIHttpClient, 22 | ) 23 | 24 | from typing import Union, Type 25 | 26 | 27 | log = logging.getLogger("GremlinAPI.client") 28 | 29 | 30 | class GremlinAPIReports(GremlinAPI): 31 | @classmethod 32 | @register_cli_action("report_attacks", ("",), ("start", "end", "period", "teamId")) 33 | def report_attacks( 34 | cls, 35 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 36 | *args: tuple, 37 | **kwargs: dict, 38 | ) -> dict: 39 | method: str = "GET" 40 | params: list = ["start", "end", "period"] 41 | endpoint: str = cls._build_query_string_option_team_endpoint( 42 | "/reports/attacks", params, **kwargs 43 | ) 44 | payload: dict = cls._payload(**{"headers": https_client.header()}) 45 | (resp, body) = https_client.api_call(method, endpoint, **payload) 46 | return body 47 | 48 | @classmethod 49 | @register_cli_action("report_clients", ("",), ("start", "end", "period", "teamId")) 50 | def report_clients( 51 | cls, 52 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 53 | *args: tuple, 54 | **kwargs: dict, 55 | ) -> dict: 56 | method: str = "GET" 57 | params: list = ["start", "end", "period"] 58 | endpoint: str = cls._build_query_string_option_team_endpoint( 59 | "/reports/clients", params, **kwargs 60 | ) 61 | payload: dict = cls._payload(**{"headers": https_client.header()}) 62 | (resp, body) = https_client.api_call(method, endpoint, **payload) 63 | return body 64 | 65 | @classmethod 66 | @register_cli_action( 67 | "report_companies", ("",), ("start", "end", "period", "teamId") 68 | ) 69 | def report_companies( 70 | cls, 71 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 72 | *args: tuple, 73 | **kwargs: dict, 74 | ) -> dict: 75 | method: str = "GET" 76 | params: list = ["startDate", "endDate"] 77 | endpoint: str = cls._build_query_string_endpoint( 78 | "/reports/companies", params, **kwargs 79 | ) 80 | payload: dict = cls._payload(**{"headers": https_client.header()}) 81 | (resp, body) = https_client.api_call(method, endpoint, **payload) 82 | return body 83 | 84 | @classmethod 85 | def report_pricing( 86 | cls, 87 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 88 | *args: tuple, 89 | **kwargs: dict, 90 | ) -> dict: 91 | method: str = "GET" 92 | params: list = ["startDate", "endDate", "trackingPeriod"] 93 | endpoint: str = cls._build_query_string_endpoint( 94 | "/reports/pricing", params, **kwargs 95 | ) 96 | payload: dict = cls._payload(**{"headers": https_client.header()}) 97 | (resp, body) = https_client.api_call(method, endpoint, **payload) 98 | return body 99 | 100 | @classmethod 101 | @register_cli_action("report_teams", ("",), ("start", "end", "period", "teamId")) 102 | def report_teams( 103 | cls, 104 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 105 | *args: tuple, 106 | **kwargs: dict, 107 | ) -> dict: 108 | method: str = "GET" 109 | params: list = ["startDate", "endDate"] 110 | endpoint: str = cls._build_query_string_option_team_endpoint( 111 | "/reports/teams", params, **kwargs 112 | ) 113 | payload: dict = cls._payload(**{"headers": https_client.header()}) 114 | (resp, body) = https_client.api_call(method, endpoint, **payload) 115 | return body 116 | 117 | @classmethod 118 | @register_cli_action("report_users", ("",), ("start", "end", "period", "teamId")) 119 | def report_users( 120 | cls, 121 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 122 | *args: tuple, 123 | **kwargs: dict, 124 | ) -> dict: 125 | method: str = "GET" 126 | params: list = ["start", "end", "period"] 127 | endpoint: str = cls._build_query_string_option_team_endpoint( 128 | "/reports/users", params, **kwargs 129 | ) 130 | payload: dict = cls._payload(**{"headers": https_client.header()}) 131 | (resp, body) = https_client.api_call(method, endpoint, **payload) 132 | return body 133 | 134 | 135 | class GremlinAPIReportsSecurity(GremlinAPI): 136 | @classmethod 137 | @register_cli_action("report_security_access", ("start", "end"), ("",)) 138 | def report_security_access( 139 | cls, 140 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 141 | *args: tuple, 142 | **kwargs: dict, 143 | ) -> dict: 144 | method: str = "GET" 145 | params: list = ["start", "end"] 146 | endpoint: str = cls._build_query_string_endpoint( 147 | "/reports/security/access", params, **kwargs 148 | ) 149 | payload: dict = cls._payload(**{"headers": https_client.header()}) 150 | (resp, body) = https_client.api_call(method, endpoint, **payload) 151 | return body 152 | -------------------------------------------------------------------------------- /tests/test_schedules.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.schedules import GremlinAPISchedules 6 | 7 | from .util import mock_json, mock_data, mock_body, mock_guid, mock_scenario_guid 8 | 9 | 10 | class TestSchedules(unittest.TestCase): 11 | @patch("requests.post") 12 | def test_create_schedule_with_decorator(self, mock_get) -> None: 13 | mock_get.return_value = requests.Response() 14 | mock_get.return_value.status_code = 200 15 | mock_get.return_value.json = mock_json 16 | self.assertEqual(GremlinAPISchedules.create_schedule(**mock_body), mock_data) 17 | 18 | @patch("requests.get") 19 | def test_get_schedule_with_decorator(self, mock_get) -> None: 20 | mock_get.return_value = requests.Response() 21 | mock_get.return_value.status_code = 200 22 | mock_get.return_value.json = mock_json 23 | self.assertEqual(GremlinAPISchedules.get_schedule(**mock_guid), mock_data) 24 | 25 | @patch("requests.delete") 26 | def test_delete_schedule_with_decorator(self, mock_get) -> None: 27 | mock_get.return_value = requests.Response() 28 | mock_get.return_value.status_code = 200 29 | mock_get.return_value.json = mock_json 30 | self.assertEqual(GremlinAPISchedules.delete_schedule(**mock_guid), mock_data) 31 | 32 | @patch("requests.get") 33 | def test_list_active_schedules_with_decorator(self, mock_get) -> None: 34 | mock_get.return_value = requests.Response() 35 | mock_get.return_value.status_code = 200 36 | mock_get.return_value.json = mock_json 37 | self.assertEqual(GremlinAPISchedules.list_active_schedules(), mock_data) 38 | 39 | @patch("requests.get") 40 | def test_list_attack_schedules_with_decorator(self, mock_get) -> None: 41 | mock_get.return_value = requests.Response() 42 | mock_get.return_value.status_code = 200 43 | mock_get.return_value.json = mock_json 44 | self.assertEqual(GremlinAPISchedules.list_attack_schedules(), mock_data) 45 | 46 | @patch("requests.post") 47 | def test_create_attack_schedule_with_decorator(self, mock_get) -> None: 48 | mock_get.return_value = requests.Response() 49 | mock_get.return_value.status_code = 200 50 | mock_get.return_value.json = mock_json 51 | self.assertEqual( 52 | GremlinAPISchedules.create_attack_schedule(**mock_body), mock_data 53 | ) 54 | 55 | @patch("requests.get") 56 | def test_get_attack_schedule_with_decorator(self, mock_get) -> None: 57 | mock_get.return_value = requests.Response() 58 | mock_get.return_value.status_code = 200 59 | mock_get.return_value.json = mock_json 60 | self.assertEqual( 61 | GremlinAPISchedules.get_attack_schedule(**mock_guid), mock_data 62 | ) 63 | 64 | @patch("requests.delete") 65 | def test_delete_attack_schedule_with_decorator(self, mock_get) -> None: 66 | mock_get.return_value = requests.Response() 67 | mock_get.return_value.status_code = 200 68 | mock_get.return_value.json = mock_json 69 | self.assertEqual( 70 | GremlinAPISchedules.delete_attack_schedule(**mock_guid), mock_data 71 | ) 72 | 73 | @patch("requests.get") 74 | def test_list_scenario_schedules_with_decorator(self, mock_get) -> None: 75 | mock_get.return_value = requests.Response() 76 | mock_get.return_value.status_code = 200 77 | mock_get.return_value.json = mock_json 78 | self.assertEqual(GremlinAPISchedules.list_scenario_schedules(), mock_data) 79 | 80 | @patch("requests.post") 81 | def test_create_scenario_schedule_with_decorator(self, mock_get) -> None: 82 | mock_get.return_value = requests.Response() 83 | mock_get.return_value.status_code = 200 84 | mock_get.return_value.json = mock_json 85 | self.assertEqual( 86 | GremlinAPISchedules.create_scenario_schedule(**mock_body), mock_data 87 | ) 88 | 89 | @patch("requests.get") 90 | def test_get_scenario_schedule_with_decorator(self, mock_get) -> None: 91 | mock_get.return_value = requests.Response() 92 | mock_get.return_value.status_code = 200 93 | mock_get.return_value.json = mock_json 94 | self.assertEqual( 95 | GremlinAPISchedules.get_scenario_schedule(**mock_guid), mock_data 96 | ) 97 | 98 | @patch("requests.put") 99 | def test_update_scenario_schedule_with_decorator(self, mock_get) -> None: 100 | mock_get.return_value = requests.Response() 101 | mock_get.return_value.status_code = 200 102 | mock_get.return_value.json = mock_json 103 | self.assertEqual( 104 | GremlinAPISchedules.update_scenario_schedule(**mock_scenario_guid), 105 | mock_data, 106 | ) 107 | 108 | @patch("requests.delete") 109 | def test_delete_scenario_schedule_with_decorator(self, mock_get) -> None: 110 | mock_get.return_value = requests.Response() 111 | mock_get.return_value.status_code = 200 112 | mock_get.return_value.json = mock_json 113 | self.assertEqual( 114 | GremlinAPISchedules.delete_scenario_schedule(**mock_guid), mock_data 115 | ) 116 | 117 | @patch("requests.post") 118 | def test_enable_scenario_schedule_with_decorator(self, mock_get) -> None: 119 | mock_get.return_value = requests.Response() 120 | mock_get.return_value.status_code = 200 121 | mock_get.return_value.json = mock_json 122 | self.assertEqual( 123 | GremlinAPISchedules.enable_scenario_schedule(**mock_guid), mock_data 124 | ) 125 | 126 | @patch("requests.delete") 127 | def test_disable_scenario_schedule_with_decorator(self, mock_get) -> None: 128 | mock_get.return_value = requests.Response() 129 | mock_get.return_value.status_code = 200 130 | mock_get.return_value.json = mock_json 131 | self.assertEqual( 132 | GremlinAPISchedules.disable_scenario_schedule(**mock_guid), mock_data 133 | ) 134 | -------------------------------------------------------------------------------- /gremlinapi/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | 6 | import logging 7 | 8 | from datetime import datetime, timezone 9 | 10 | from typing import Optional 11 | 12 | log = logging.getLogger("GremlinAPI.client") 13 | 14 | 15 | class GremlinAPIConfig(object): 16 | def __init__(self): 17 | self._api_key = None 18 | self._base_uri = None 19 | self._bearer_expires = None 20 | self._bearer_timestamp = None 21 | self._bearer_token = None 22 | self._client_cache = {} 23 | self._company_name = None 24 | self._http_proxy = False 25 | self._https_proxy = False 26 | self._max_bearer_interval = None 27 | self._override_blast_radius = None 28 | self._override_node_count = None 29 | self._password = None 30 | self._team_id = None 31 | self._user = None 32 | self._user_mfa_token_value = None 33 | 34 | @property 35 | def api_key(self) -> Optional[str]: 36 | if not self._api_key: 37 | return None 38 | return self._api_key 39 | 40 | @api_key.setter 41 | def api_key(self, api_key: str): 42 | self._api_key = api_key 43 | return self.api_key 44 | 45 | @property 46 | def base_uri(self) -> str: 47 | return self._base_uri 48 | 49 | @base_uri.setter 50 | def base_uri(self, base_uri: str): 51 | self._base_uri = base_uri 52 | return self.base_uri 53 | 54 | @property 55 | def bearer_expires(self) -> datetime: 56 | return self._bearer_expires 57 | 58 | @bearer_expires.setter 59 | def bearer_expires(self, bearer_expires: datetime) -> None: 60 | """ 61 | :param bearer_expires: 62 | :return: 63 | """ 64 | self._bearer_expires = bearer_expires 65 | 66 | @property 67 | def bearer_timestamp(self) -> str: 68 | return self._bearer_timestamp 69 | 70 | @bearer_timestamp.setter 71 | def bearer_timestamp(self, bearer_timestamp: str) -> None: 72 | self._bearer_timestamp = bearer_timestamp 73 | 74 | @property 75 | def bearer_token(self) -> str: 76 | """Bearer token for API authorization""" 77 | return self._bearer_token 78 | 79 | @bearer_token.setter 80 | def bearer_token(self, bearer_token: str) -> str: 81 | self._bearer_token = bearer_token 82 | return self.bearer_token 83 | 84 | @property 85 | def client_cache(self) -> dict: 86 | if not self._client_cache: 87 | return {} 88 | return self._client_cache 89 | 90 | @client_cache.setter 91 | def client_cache(self, client_cache: dict) -> dict: 92 | self._client_cache = client_cache 93 | return self.client_cache 94 | 95 | @property 96 | def company_name(self) -> str: 97 | """Company Name for login""" 98 | return self._company_name 99 | 100 | @company_name.setter 101 | def company_name(self, company_name: str) -> str: 102 | self._company_name = company_name 103 | return self.company_name 104 | 105 | @property 106 | def http_proxy(self) -> str: 107 | return self._http_proxy 108 | 109 | @http_proxy.setter 110 | def http_proxy(self, http_proxy: str) -> str: 111 | self._http_proxy = http_proxy 112 | return self.http_proxy 113 | 114 | @property 115 | def https_proxy(self) -> str: 116 | return self._https_proxy 117 | 118 | @https_proxy.setter 119 | def https_proxy(self, https_proxy: str) -> str: 120 | self._https_proxy = https_proxy 121 | return self.https_proxy 122 | 123 | @property 124 | def max_bearer_interval(self) -> int: 125 | return self._max_bearer_interval 126 | 127 | @max_bearer_interval.setter 128 | def max_bearer_interval(self, max_bearer_interval: int) -> int: 129 | self._max_bearer_interval = max_bearer_interval 130 | return self.max_bearer_interval 131 | 132 | @property 133 | def override_blast_radius(self) -> bool: 134 | if not self._override_blast_radius: 135 | return False 136 | return self._override_blast_radius 137 | 138 | @override_blast_radius.setter 139 | def override_blast_radius(self, override_blast_radius: bool) -> bool: 140 | self._override_blast_radius = override_blast_radius 141 | return self.override_blast_radius 142 | 143 | @property 144 | def override_node_count(self) -> bool: 145 | if not self._override_node_count: 146 | return False 147 | return self._override_node_count 148 | 149 | @override_node_count.setter 150 | def override_node_count(self, override_node_count: bool) -> bool: 151 | self._override_node_count = override_node_count 152 | return self.override_node_count 153 | 154 | @property 155 | def password(self) -> str: 156 | """Password for login""" 157 | return self._password 158 | 159 | @password.setter 160 | def password(self, password: str) -> str: 161 | self._password = password 162 | return self.password 163 | 164 | @property 165 | def team_id(self) -> str: 166 | return self._team_id 167 | 168 | @team_id.setter 169 | def team_id(self, team_id: str) -> str: 170 | self._team_id = team_id 171 | return self.team_id 172 | 173 | @property 174 | def user(self) -> str: 175 | """Username for login""" 176 | return self._user 177 | 178 | @user.setter 179 | def user(self, user: str) -> str: 180 | self._user = user 181 | return self.user 182 | 183 | @property 184 | def user_mfa_token_value(self) -> str: 185 | return self._user_mfa_token_value 186 | 187 | @user_mfa_token_value.setter 188 | def user_mfa_token_value(self, user_mfa_token_value: str) -> str: 189 | self._user_mfa_token_value = user_mfa_token_value 190 | return self.user_mfa_token_value 191 | 192 | @classmethod 193 | def is_bearer_expired(cls) -> bool: 194 | """ 195 | Built-in to let the user check in the bearer token is expired 196 | Primarily used by the login function 197 | :return: bool 198 | """ 199 | return datetime.now(timezone.utc) >= cls.bearer_expires # type: ignore 200 | -------------------------------------------------------------------------------- /gremlinapi/attacks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import json 6 | import logging 7 | 8 | from gremlinapi.cli import register_cli_action 9 | from gremlinapi.exceptions import ( 10 | GremlinParameterError, 11 | ProxyError, 12 | ClientError, 13 | HTTPTimeout, 14 | HTTPError, 15 | ) 16 | 17 | from gremlinapi.gremlinapi import GremlinAPI 18 | from gremlinapi.attack_helpers import GremlinAttackHelper 19 | from gremlinapi.http_clients import ( 20 | get_gremlin_httpclient, 21 | GremlinAPIHttpClient, 22 | ) 23 | from typing import Union, Type 24 | 25 | log = logging.getLogger("GremlinAPI.client") 26 | 27 | 28 | class GremlinAPIAttacks(GremlinAPI): 29 | @classmethod 30 | def _list_endpoint(cls, endpoint: str, *args: tuple, **kwargs: dict) -> str: 31 | if not endpoint: 32 | error_msg: str = f"endpoint not passed correctly: {args} :: {kwargs}" 33 | log.error(error_msg) 34 | raise GremlinParameterError(error_msg) 35 | source: str = cls._info_if_not_param("source", **kwargs) 36 | page_size: str = cls._info_if_not_param("pageSize", **kwargs) 37 | if source or page_size: 38 | endpoint += "/?" 39 | if source and (source.lower() == "adhoc" or source.lower() == "scenario"): 40 | endpoint += f"source={source}&" 41 | if page_size and isinstance(page_size, int): 42 | endpoint += f"pageSize={page_size}&" 43 | return cls._optional_team_endpoint(endpoint, **kwargs) 44 | 45 | @classmethod 46 | def _error_if_not_attack_body(cls, **kwargs: dict) -> str: 47 | body: str = cls._error_if_not_param("body", **kwargs) 48 | if issubclass(type(body), GremlinAttackHelper): 49 | return str(body) 50 | else: 51 | error_msg: str = f"Body present but not of type {type(GremlinAttackHelper)}" 52 | log.warning(error_msg) 53 | return body 54 | 55 | @classmethod 56 | @register_cli_action("create_attack", ("body",), ("teamId",)) 57 | def create_attack( 58 | cls, 59 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 60 | *args: tuple, 61 | **kwargs: dict, 62 | ) -> dict: 63 | method: str = "POST" 64 | data: str = cls._error_if_not_attack_body(**kwargs) 65 | endpoint: str = cls._optional_team_endpoint("/attacks/new", **kwargs) 66 | i_payload: dict = {"headers": https_client.header(), "body": data} 67 | payload: dict = cls._payload(**i_payload) 68 | (resp, body) = https_client.api_call(method, endpoint, **payload) 69 | return body 70 | 71 | @classmethod 72 | @register_cli_action("list_active_attacks", ("",), ("source", "pageSize", "teamId")) 73 | def list_active_attacks( 74 | cls, 75 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 76 | *args: tuple, 77 | **kwargs: dict, 78 | ) -> dict: 79 | method: str = "GET" 80 | endpoint: str = cls._list_endpoint("/attacks/active", **kwargs) 81 | payload: dict = cls._payload(**{"headers": https_client.header()}) 82 | (resp, body) = https_client.api_call(method, endpoint, **payload) 83 | return body 84 | 85 | @classmethod 86 | @register_cli_action("list_attacks", ("",), ("source", "pageSize", "teamId")) 87 | def list_attacks( 88 | cls, 89 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 90 | *args: tuple, 91 | **kwargs: dict, 92 | ) -> dict: 93 | """ 94 | :param https_client: 95 | :param kwargs: { source(adhoc or scenario, query), pageSize(int32, query), teamId(string, query) } 96 | :return: 97 | """ 98 | method: str = "GET" 99 | endpoint: str = cls._list_endpoint("/attacks", **kwargs) 100 | payload: dict = cls._payload(**{"headers": https_client.header()}) 101 | (resp, body) = https_client.api_call(method, endpoint, **payload) 102 | return body 103 | 104 | @classmethod 105 | @register_cli_action( 106 | "list_complete_attacks", ("",), ("source", "pageSize", "teamId") 107 | ) 108 | def list_completed_attacks( 109 | cls, 110 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 111 | *args: tuple, 112 | **kwargs: dict, 113 | ) -> dict: 114 | """ 115 | :param https_client: 116 | :param kwargs: { source(adhoc or scenario, query), pageSize(int32, query), teamId(string, query) } 117 | :return: 118 | """ 119 | method: str = "GET" 120 | endpoint: str = cls._list_endpoint("/attacks/completed", **kwargs) 121 | payload: dict = cls._payload(**{"headers": https_client.header()}) 122 | (resp, body) = https_client.api_call(method, endpoint, **payload) 123 | return body 124 | 125 | @classmethod 126 | @register_cli_action("get_attack", ("guid",), ("teamId",)) 127 | def get_attack( 128 | cls, 129 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 130 | *args: tuple, 131 | **kwargs: dict, 132 | ) -> dict: 133 | method: str = "GET" 134 | guid: str = cls._error_if_not_param("guid", **kwargs) 135 | endpoint: str = cls._optional_team_endpoint(f"/attacks/{guid}", **kwargs) 136 | payload: dict = cls._payload(**{"headers": https_client.header()}) 137 | (resp, body) = https_client.api_call(method, endpoint, **payload) 138 | return body 139 | 140 | @classmethod 141 | @register_cli_action("halt_all_attacks", ("",), ("teamId",)) 142 | def halt_all_attacks( 143 | cls, 144 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 145 | *args: tuple, 146 | **kwargs: dict, 147 | ) -> dict: 148 | method: str = "DELETE" 149 | endpoint: str = cls._optional_team_endpoint("/attacks", **kwargs) 150 | payload: dict = cls._payload(**{"headers": https_client.header()}) 151 | (resp, body) = https_client.api_call(method, endpoint, **payload) 152 | return body 153 | 154 | @classmethod 155 | @register_cli_action("halt_attack", ("guid",), ("teamId",)) 156 | def halt_attack( 157 | cls, 158 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 159 | *args: tuple, 160 | **kwargs: dict, 161 | ) -> dict: 162 | method: str = "DELETE" 163 | guid: str = cls._error_if_not_param("guid", **kwargs) 164 | endpoint: str = cls._optional_team_endpoint(f"/attacks/{guid}", **kwargs) 165 | payload: dict = cls._payload(**{"headers": https_client.header()}) 166 | (resp, body) = https_client.api_call(method, endpoint, **payload) 167 | return body 168 | -------------------------------------------------------------------------------- /tests/test_gremlinapi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import unittest 6 | from unittest.mock import patch 7 | 8 | # import logging 9 | import requests 10 | from gremlinapi.gremlinapi import GremlinAPI 11 | import gremlinapi.exceptions as g_exceptions 12 | 13 | from .util import ( 14 | mock_data, 15 | mock_team_id, 16 | mock_body, 17 | mock_identifier, 18 | mock_payload, 19 | hooli_id, 20 | mock_json, 21 | ) 22 | 23 | test_param = "myparam" 24 | test_value = "paramval" 25 | test_params = ["myparam", "anotherparam", "team_id"] 26 | test_kwargs = { 27 | "myparam": "param1val", 28 | "anotherparam": "param2val", 29 | "team_id": mock_team_id, 30 | } 31 | 32 | test_base_endpoint = "test-endpoint.com" 33 | 34 | 35 | class TestAPI(unittest.TestCase): 36 | def test__add_query_param(self) -> None: 37 | test_endpoint = "%s/?&dummy=yes" % test_base_endpoint 38 | expected_output = "%s&%s=%s" % (test_endpoint, test_param, test_value) 39 | self.assertEqual( 40 | GremlinAPI._add_query_param(test_endpoint, test_param, test_value), 41 | expected_output, 42 | ) 43 | 44 | test_endpoint = "%s/?&dummy=yes&" % test_base_endpoint 45 | expected_output = "%s%s=%s" % (test_endpoint, test_param, test_value) 46 | self.assertEqual( 47 | GremlinAPI._add_query_param(test_endpoint, test_param, test_value), 48 | expected_output, 49 | ) 50 | 51 | test_endpoint = "%s" % test_base_endpoint 52 | expected_output = "%s/?%s=%s" % (test_endpoint, test_param, test_value) 53 | self.assertEqual( 54 | GremlinAPI._add_query_param(test_endpoint, test_param, test_value), 55 | expected_output, 56 | ) 57 | 58 | def test__build_query_string_endpoint(self) -> None: 59 | test_endpoint = "%s" % test_base_endpoint 60 | expected_output = "%s/?%s=%s&%s=%s&%s=%s" % ( 61 | test_endpoint, 62 | test_params[0], 63 | test_kwargs[test_params[0]], 64 | test_params[1], 65 | test_kwargs[test_params[1]], 66 | "teamId", 67 | test_kwargs[test_params[2]], 68 | ) 69 | self.assertEqual( 70 | GremlinAPI._build_query_string_endpoint( 71 | test_endpoint, test_params, **test_kwargs 72 | ), 73 | expected_output, 74 | ) 75 | 76 | def test__build_query_string_option_team_endpoint(self) -> None: 77 | test_endpoint = "%s" % test_base_endpoint 78 | expected_output = "%s/?%s=%s&%s=%s&%s=%s" % ( 79 | test_endpoint, 80 | test_params[0], 81 | test_kwargs[test_params[0]], 82 | test_params[1], 83 | test_kwargs[test_params[1]], 84 | "teamId", 85 | test_kwargs[test_params[2]], 86 | ) 87 | self.assertEqual( 88 | GremlinAPI._build_query_string_option_team_endpoint( 89 | test_endpoint, test_params, **test_kwargs 90 | ), 91 | expected_output, 92 | ) 93 | 94 | def test__build_query_string_required_team_endpoint(self) -> None: 95 | test_endpoint = "%s" % test_base_endpoint 96 | expected_output = "%s/?%s=%s&%s=%s&%s=%s" % ( 97 | test_endpoint, 98 | test_params[0], 99 | test_kwargs[test_params[0]], 100 | test_params[1], 101 | test_kwargs[test_params[1]], 102 | "teamId", 103 | test_kwargs[test_params[2]], 104 | ) 105 | self.assertEqual( 106 | GremlinAPI._build_query_string_required_team_endpoint( 107 | test_endpoint, test_params, **test_kwargs 108 | ), 109 | expected_output, 110 | ) 111 | 112 | def test__optional_team_endpoint(self) -> None: 113 | test_endpoint = "%s" % test_base_endpoint 114 | 115 | expected_output = "%s/?teamId=%s" % ( 116 | test_endpoint, 117 | test_kwargs[test_params[2]], 118 | ) 119 | self.assertEqual( 120 | GremlinAPI._optional_team_endpoint(test_endpoint, **test_kwargs), 121 | expected_output, 122 | ) 123 | 124 | expected_output = "%s" % (test_endpoint) 125 | self.assertEqual( 126 | GremlinAPI._optional_team_endpoint(test_endpoint), 127 | expected_output, 128 | ) 129 | 130 | def test__required_team_endpoint(self) -> None: 131 | test_endpoint = "%s" % test_base_endpoint 132 | 133 | try: 134 | GremlinAPI._required_team_endpoint(test_endpoint) 135 | self.assertTrue( 136 | False, 137 | "Endpoint not provided a team_id but _required_team_endpoint did not throw GremlinParameterError", 138 | ) 139 | except g_exceptions.GremlinParameterError as gpe: 140 | self.assertEqual( 141 | "Endpoint requires a team_id or teamId, none supplied", str(gpe) 142 | ) 143 | 144 | def test__error_if_not_json_body(self) -> None: 145 | try: 146 | GremlinAPI._error_if_not_json_body() 147 | self.assertTrue(False) 148 | except g_exceptions.GremlinParameterError as gpe: 149 | self.assertEqual("JSON Body not supplied: {}", str(gpe)) 150 | 151 | self.assertEqual(GremlinAPI._error_if_not_json_body(**mock_body), mock_data) 152 | 153 | def test__error_if_not_email(self) -> None: 154 | try: 155 | GremlinAPI._error_if_not_json_body() 156 | self.assertTrue(False) 157 | except g_exceptions.GremlinParameterError as gpe: 158 | self.assertEqual("JSON Body not supplied: {}", str(gpe)) 159 | 160 | self.assertEqual( 161 | GremlinAPI._error_if_not_email(**mock_identifier), mock_identifier["email"] 162 | ) 163 | 164 | def test__info_if_not_param(self) -> None: 165 | test_param = "email" 166 | self.assertEqual( 167 | GremlinAPI._info_if_not_param(test_param, **mock_identifier), 168 | mock_identifier[test_param], 169 | ) 170 | self.assertEqual( 171 | GremlinAPI._info_if_not_param("fakeparam", test_param, **mock_identifier), 172 | test_param, 173 | ) 174 | 175 | def test_payload(self) -> None: 176 | expected_output = { 177 | "headers": mock_payload["headers"], 178 | "body": mock_payload["body"], 179 | "data": mock_payload["data"], 180 | } 181 | self.assertEqual( 182 | GremlinAPI._payload(**mock_payload), 183 | expected_output, 184 | ) 185 | 186 | def test__warn_if_not_json_body(self) -> None: 187 | self.assertEqual(GremlinAPI._warn_if_not_json_body(**mock_body), mock_data) 188 | 189 | def test__warn_if_not_param(self) -> None: 190 | test_param = "email" 191 | self.assertEqual( 192 | GremlinAPI._warn_if_not_param(test_param, **mock_identifier), 193 | mock_identifier[test_param], 194 | ) 195 | self.assertEqual( 196 | GremlinAPI._warn_if_not_param("fakeparam", test_param, **mock_identifier), 197 | test_param, 198 | ) 199 | -------------------------------------------------------------------------------- /gremlinapi/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import argparse 6 | from argparse import ArgumentParser 7 | import functools 8 | import importlib 9 | import logging 10 | import os 11 | import sys 12 | 13 | from gremlinapi.config import GremlinAPIConfig 14 | from gremlinapi.exceptions import GremlinAuthError 15 | 16 | from typing import Callable, Union 17 | 18 | log = logging.getLogger("GremlinAPI.client") 19 | 20 | cli_actions: dict = dict() 21 | 22 | 23 | def register_cli_action( 24 | cls_names: str, required: tuple = tuple(), optional: tuple = tuple() 25 | ) -> Callable: 26 | def wrap(f) -> Callable: 27 | @functools.wraps(f) 28 | def wrapped_f(*args: tuple, **kwargs: dict) -> Callable: 29 | return f(*args, **kwargs) 30 | 31 | in_obj = True 32 | classes: Union[str, tuple] = cls_names 33 | if type(classes) != tuple: 34 | classes = (classes,) 35 | 36 | for cls_name in classes: 37 | if cls_name not in cli_actions: 38 | cli_actions[cls_name] = {} 39 | action = f.__name__.replace("_", "-") 40 | cli_actions[cls_name][action] = (required, optional, in_obj) 41 | 42 | return wrapped_f 43 | 44 | return wrap 45 | 46 | 47 | # class GremlinArgs(ArgumentParser): 48 | # def __init__(self, *args, **kwargs) -> None: 49 | # super().__init__(*args, **kwargs) 50 | # self.gremlin_user = None 51 | # self.gremlin_password = None 52 | # self.gremlin_bearer = None 53 | # self.gremlin_api_key = None 54 | # self.bearer = None 55 | 56 | 57 | def _base_args() -> ArgumentParser: 58 | p: ArgumentParser = ArgumentParser(description="Gremlin API Command Line Interface") 59 | p.add_argument( 60 | "--version", 61 | help="Display the version.", 62 | type=bool, 63 | action="store", 64 | dest="version", 65 | default=False, 66 | ) 67 | p.add_argument( 68 | "-v", 69 | "--verbose", 70 | help="Verbose Mode", 71 | type=bool, 72 | action="store", 73 | dest="verbose", 74 | default=False, 75 | ) 76 | p.add_argument( 77 | "-d", 78 | "--debug", 79 | help="Debug mode (may expose authentication credentials to logs!)", 80 | type=bool, 81 | action="store", 82 | dest="debug", 83 | default=False, 84 | ) 85 | auth = p.add_argument_group("Authentication Configuration Options") 86 | auth.add_argument( 87 | "-a", 88 | "--apikey", 89 | help="User provided API key", 90 | type=str, 91 | action="store", 92 | dest="gremlin_api_key", 93 | default=os.getenv("GREMLIN_API_KEY", None), 94 | ) 95 | auth.add_argument( 96 | "-b", 97 | "--bearer", 98 | help="User provided bearer token", 99 | type=str, 100 | action="store", 101 | dest="gremlin_bearer", 102 | default=os.getenv("GREMLIN_BEARER_TOKEN", None), 103 | ) 104 | auth.add_argument( 105 | "-i", 106 | "--bearer-interval", 107 | help="Maximum bearer token age in seconds if using login methods", 108 | type=int, 109 | action="store", 110 | dest="max_bearer_interval", 111 | default=os.getenv("GREMLIN_MAX_BEARER_INTERVAL", 86400), 112 | ) 113 | auth.add_argument( 114 | "-u", 115 | "--user", 116 | help="Gremlin user (email) to use for login functions", 117 | type=str, 118 | dest="gremlin_user", 119 | default=os.getenv("GREMLIN_USER", ""), 120 | ) 121 | auth.add_argument( 122 | "-p", 123 | "--password", 124 | help="Password to use for login functions (will be exposed in logs if debug mode is enabled!)", 125 | type=str, 126 | action="store", 127 | dest="gremlin_password", 128 | default=os.getenv("GREMLIN_PASSWORD", ""), 129 | ) 130 | auth.add_argument( 131 | "-m", 132 | "--mfa", 133 | help="MFA OTP Token for login functions", 134 | type=int, 135 | action="store", 136 | dest="gremlin_user_mfa_token", 137 | default=os.getenv("GREMLIN_USER_MFA_TOKEN", None), 138 | ) 139 | auth.add_argument( 140 | "-c", 141 | "--company", 142 | help="Company Name, as it appears in the Gremlin UI or API, for user login functions", 143 | type=str, 144 | action="store", 145 | dest="gremlin_company", 146 | default=os.getenv("GREMLIN_COMPANY", ""), 147 | ) 148 | auth.add_argument( 149 | "-t", 150 | "--team_id", 151 | help="Gremlin Team ID, required for RBAC enabled accounts", 152 | type=str, 153 | action="store", 154 | dest="gremlin_team_id", 155 | default=os.getenv("GREMLIN_TEAM_ID", ""), 156 | ) 157 | return p 158 | 159 | 160 | def _get_parser(cli_module): 161 | parser = _base_args() 162 | return cli_module.extend_parser(parser) 163 | 164 | 165 | def _parse_args() -> None: 166 | parser = _base_args() 167 | # args = parser.parse_known_args(sys.argv[1:]) 168 | (options, args) = parser.parse_known_args(sys.argv) 169 | cli_module = importlib.import_module("gremlinapi.cli.cli") 170 | parser = _get_parser(cli_module) 171 | try: 172 | import argcomplete # type: ignore 173 | 174 | argcomplete.autocomplete(parser) 175 | except Exception: 176 | pass 177 | # Sanity check input 178 | try: 179 | if not (args.gremlin_user and args.gremlin_password) and not ( # type: ignore 180 | args.gremlin_bearer or args.gremlin_api_key # type: ignore 181 | ): 182 | error_msg: str = f"No form of API authentication provided: {args}" 183 | log.error(error_msg) 184 | raise GremlinAuthError(error_msg) 185 | elif args.gremlin_api_key: # type: ignore 186 | if log.getEffectiveLevel() == logging.DEBUG: 187 | log.debug(f"API authentication supplied: key {args.gremlin_api_key}") # type: ignore 188 | elif args.bearer: # type: ignore 189 | if log.getEffectiveLevel() == logging.DEBUG: 190 | log.debug(f"Bearer supplied at CLI runtime: {args.bearer}") # type: ignore 191 | GremlinAPIConfig.bearer_token = args.bearer # type: ignore 192 | elif args.gremlin_user and args.gremlin_password: # type: ignore 193 | if log.getEffectiveLevel() == logging.DEBUG: 194 | log.debug(f"User authentication provided for user: {args.gremlin_user}") # type: ignore 195 | GremlinAPIConfig.user = args.gremlin_user # type: ignore 196 | GremlinAPIConfig.password = args.gremlin_password # type: ignore 197 | if args.gremlin_user_mfa_token: # type: ignore 198 | if log.getEffectiveLevel() == logging.DEBUG: 199 | log.debug(f"MFA token provided for user {args.gremlin_user}") # type: ignore 200 | GremlinAPIConfig.user_mfa_token_value = args.gremlin_user_mfa_token # type: ignore 201 | else: 202 | error_msg = f"Unexpected state, authentication logic fallthrough: {args}" 203 | log.error(error_msg) 204 | raise GremlinAuthError(error_msg) 205 | except GremlinAuthError: 206 | parser.print_help() 207 | 208 | 209 | def main(): 210 | _parse_args() 211 | -------------------------------------------------------------------------------- /gremlinapi/gremlinapi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import json 6 | import logging 7 | import time 8 | 9 | from gremlinapi.config import GremlinAPIConfig as config 10 | 11 | from gremlinapi.exceptions import ( 12 | APIError, 13 | GremlinParameterError, 14 | ProxyError, 15 | ClientError, 16 | HTTPTimeout, 17 | HTTPError, 18 | ) 19 | 20 | from typing import Optional, Dict, Any, Union, Type 21 | 22 | from gremlinapi.http_clients import get_gremlin_httpclient, GremlinAPIHttpClient 23 | from gremlinapi.exceptions import GremlinParameterError 24 | 25 | log = logging.getLogger("GremlinAPI.client") 26 | 27 | 28 | class GremlinAPI(object): 29 | def __init__(self): 30 | pass 31 | 32 | @classmethod 33 | def param_remap(cls, param): 34 | """ 35 | Remaps parameters into API format from pythonic format. 36 | 37 | e.g. 38 | 39 | "pythonic_parameter" -> "pythonicParameter" 40 | "mixed_pythonicParameter" -> "mixedPythonicParameter" 41 | "apiParameter" -> "apiParameter" 42 | "APIParameter" -> "APIParameter" 43 | 44 | """ 45 | split_param = param.split("_") 46 | new_param = "" 47 | for s in split_param: 48 | if s is split_param[0]: 49 | new_param += s 50 | else: 51 | new_param += s.capitalize() 52 | return new_param 53 | 54 | @classmethod 55 | def _add_query_param(cls, endpoint: str, param_name: str, param_value: str) -> str: 56 | if endpoint and param_name and param_value: 57 | if "/?" in endpoint and not ( 58 | str(endpoint).endswith("?") or str(endpoint).endswith("&") 59 | ): 60 | endpoint += f"&{param_name}={param_value}" 61 | elif "/?" in endpoint and ( 62 | str(endpoint).endswith("?") or str(endpoint).endswith("&") 63 | ): 64 | endpoint += f"{param_name}={param_value}" 65 | elif "/?" not in endpoint: 66 | endpoint += f"/?{param_name}={param_value}" 67 | return endpoint 68 | 69 | @classmethod 70 | def _build_query_string_endpoint( 71 | cls, endpoint: str, params: list, **kwargs: dict 72 | ) -> str: 73 | if not endpoint: 74 | error_msg: str = "expected endpoint, received nothing" 75 | log.error(error_msg) 76 | raise GremlinParameterError(error_msg) 77 | if not params or type(params) != type(list()): 78 | error_msg = f"Expected list of params, received {type(params)}" 79 | log.error(error_msg) 80 | raise (GremlinParameterError(error_msg)) 81 | for param_name in params: 82 | endpoint = cls._add_query_param( 83 | endpoint, 84 | cls.param_remap(param_name), 85 | cls._error_if_not_param(param_name, **kwargs), 86 | ) 87 | return endpoint 88 | 89 | @classmethod 90 | def _build_query_string_option_team_endpoint( 91 | cls, endpoint: str, params: list, **kwargs: dict 92 | ) -> str: 93 | endpoint = cls._build_query_string_endpoint(endpoint, params, **kwargs) 94 | if ("team_id" not in params) and ("teamId" not in params): 95 | endpoint = cls._optional_team_endpoint(endpoint, **kwargs) 96 | return endpoint 97 | 98 | @classmethod 99 | def _build_query_string_required_team_endpoint( 100 | cls, endpoint: str, params: list, **kwargs: dict 101 | ) -> str: 102 | endpoint = cls._build_query_string_endpoint(endpoint, params, **kwargs) 103 | if ("team_id" not in params) and ("teamId" not in params): 104 | endpoint = cls._required_team_endpoint(endpoint, **kwargs) 105 | return endpoint 106 | 107 | @classmethod 108 | def _optional_team_endpoint(cls, endpoint: str, **kwargs: dict) -> str: 109 | if "teamId" in kwargs: 110 | team_id: str = cls._info_if_not_param("teamId", **kwargs) 111 | else: 112 | team_id = cls._info_if_not_param("team_id", **kwargs) 113 | 114 | if not team_id and type(config.team_id) is str: 115 | team_id = config.team_id # type: ignore 116 | if team_id: 117 | endpoint = cls._add_query_param(endpoint, "teamId", team_id) 118 | return endpoint 119 | 120 | @classmethod 121 | def _required_team_endpoint(cls, endpoint: str, **kwargs: dict) -> str: 122 | if "teamId" in kwargs: 123 | team_id: str = cls._info_if_not_param("teamId", **kwargs) 124 | elif 'team_id' in kwargs: 125 | team_id: str = cls._info_if_not_param("team_id", **kwargs) 126 | elif type(config.team_id) is str: 127 | team_id: str = config.team_id # type: ignore 128 | else: 129 | error_msg: str = f"Endpoint requires a team_id or teamId, none supplied" 130 | log.error(error_msg) 131 | raise GremlinParameterError(error_msg) 132 | endpoint = cls._add_query_param(endpoint, "teamId", team_id) 133 | return endpoint 134 | 135 | @classmethod 136 | def _error_if_not_json_body(cls, **kwargs: dict) -> dict: 137 | body: dict = cls._warn_if_not_json_body(**kwargs) 138 | if not body: 139 | error_msg: str = f"JSON Body not supplied: {kwargs}" 140 | log.error(error_msg) 141 | raise GremlinParameterError(error_msg) 142 | return body 143 | 144 | @classmethod 145 | def _error_if_not_email(cls, **kwargs: dict) -> str: 146 | email: str = cls._info_if_not_param("email", **kwargs) 147 | if not email: 148 | error_msg: str = f"email address not passed: {kwargs}" 149 | log.error(error_msg) 150 | raise GremlinParameterError(error_msg) 151 | # Do some email regex validation here... 152 | return email 153 | 154 | @classmethod 155 | def _error_if_not_param(cls, parameter_name: str, **kwargs: dict) -> str: 156 | param: str = cls._info_if_not_param(parameter_name, **kwargs) 157 | if not param: 158 | error_msg: str = f"{parameter_name} not supplied: {kwargs}" 159 | log.error(error_msg) 160 | raise GremlinParameterError(error_msg) 161 | return param 162 | 163 | @classmethod 164 | def _info_if_not_param(cls, parameter_name: str, default="", **kwargs: dict) -> str: 165 | param: str = kwargs.get(parameter_name, "") # type: ignore 166 | if not param: 167 | error_msg: str = f"{parameter_name} not found in arguments: {kwargs}" 168 | log.info(error_msg) 169 | param = default 170 | return param 171 | 172 | @classmethod 173 | def _payload(cls, **kwargs: dict) -> dict: 174 | headers = kwargs.get("headers", None) # type: ignore 175 | body = kwargs.get("body", None) # type: ignore 176 | data = kwargs.get("data", None) # type: ignore 177 | payload: dict = {"headers": headers, "data": data, "body": body} 178 | payload = {k: v for k, v in payload.items() if v is not None} 179 | return payload 180 | 181 | @classmethod 182 | def _warn_if_not_json_body(cls, **kwargs: dict) -> dict: 183 | body: dict = cls._info_if_not_param("body", **kwargs) # type: ignore 184 | if not body: 185 | error_msg: str = f"JSON Body not supplied: {kwargs}" 186 | log.warning(error_msg) 187 | # Do some json validation 188 | return body 189 | 190 | @classmethod 191 | def _warn_if_not_param( 192 | cls, parameter_name: str, default=None, **kwargs: dict 193 | ) -> str: 194 | param: str = cls._info_if_not_param(parameter_name, **kwargs) # type: ignore 195 | if not param: 196 | error_msg: str = f"{parameter_name} not found in arguments: {kwargs}" 197 | log.warning(error_msg) 198 | param = default 199 | return param 200 | -------------------------------------------------------------------------------- /gremlinapi/reliability_tests.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | 4 | from gremlinapi.cli import register_cli_action 5 | from gremlinapi.exceptions import ( 6 | GremlinParameterError, 7 | ProxyError, 8 | ClientError, 9 | HTTPTimeout, 10 | HTTPError 11 | ) 12 | 13 | from typing import Union, Type 14 | 15 | from gremlinapi.gremlinapi import GremlinAPI 16 | from gremlinapi.http_clients import ( 17 | get_gremlin_httpclient, 18 | GremlinAPIHttpClient 19 | ) 20 | 21 | log = logging.getLogger("GremlinAPI.client") 22 | 23 | 24 | class GremlinAPIReliabilityTests(GremlinAPI): 25 | 26 | 27 | @classmethod 28 | def __validate_reliability_test_id( 29 | cls, 30 | reliability_test_id: str, 31 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient() 32 | ) -> None: 33 | ''' 34 | Ensure that a reliablity test ID is valid 35 | ''' 36 | all_tests = [ x['guid'] for x in cls.list_reliability_test_types()['global'] ] 37 | if reliability_test_id not in all_tests: 38 | raise GremlinParameterError(f'Reliability test ID {reliability_test_id} is not valid.') 39 | 40 | @classmethod 41 | def validate_reliability_test_id(cls, 42 | reliability_test_id: str 43 | ): 44 | ''' 45 | ''' 46 | cls.__validate_reliability_test_id(reliability_test_id) 47 | 48 | @classmethod 49 | def __reliability_test_id_requires_dependency_id( 50 | cls, 51 | reliability_test_id: str 52 | ) -> bool: 53 | if reliability_test_id in ('blackhole-test','latency-test','certificate-expiry'): 54 | return True 55 | 56 | 57 | @classmethod 58 | def list_reliability_test_types( 59 | cls, 60 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 61 | *args: tuple, 62 | **kwargs: dict, 63 | ) -> dict: 64 | """ 65 | List all types of reliability tests. 66 | These types represent the different types of tests/experiments 67 | that can be run against a service in Gremlin. 68 | """ 69 | method = "GET" 70 | endpoint = cls._required_team_endpoint("/reliability-tests", **kwargs) 71 | payload = cls._payload(**{"headers": https_client.header()}) 72 | (resp, body) = https_client.api_call(method, endpoint, **payload) 73 | return body 74 | 75 | 76 | @classmethod 77 | def list_service_reliability_test_runs( 78 | cls, 79 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 80 | *args: tuple, 81 | **kwargs: dict, 82 | ) -> dict: 83 | ''' 84 | List all reliability tests that have been run for a particular service ID 85 | ''' 86 | method = "GET" 87 | service_id = cls._error_if_not_param("service_id", **kwargs) 88 | endpoint = cls._required_team_endpoint( 89 | f'/reliability-tests/runs/?serviceId={service_id}', **kwargs 90 | ) 91 | payload = cls._payload(**{"headers": https_client.header()}) 92 | (resp, body) = https_client.api_call(method, endpoint, **payload) 93 | return body 94 | 95 | 96 | @classmethod 97 | def list_service_reliability_test_runs_by_type( 98 | cls, 99 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 100 | *args: tuple, 101 | **kwargs: dict, 102 | ) -> dict: 103 | ''' 104 | List all reliability tests of a specific type that have been run for a particular service ID 105 | ''' 106 | method = "GET" 107 | service_id = cls._error_if_not_param("service_id", **kwargs) 108 | reliability_test_id = cls._error_if_not_param("reliability_test_id", **kwargs) 109 | # TODO: Removing page_size for now, since it's not fully working. 110 | #page_size = cls._info_if_not_param("pageSize", **kwargs) 111 | #if not page_size: 112 | # endpoint = cls._required_team_endpoint( 113 | # f'/reliability-tests/{reliability_test_id}/runs/?serviceId={service_id}', **kwargs) 114 | #else: 115 | # endpoint = cls._required_team_endpoint( 116 | # f'/reliability-tests/{reliability_test_id}/runs/?serviceId={service_id}&pageSize={page_size}', **kwargs) 117 | endpoint = cls._required_team_endpoint( 118 | f'/reliability-tests/{reliability_test_id}/runs/?serviceId={service_id}', **kwargs) 119 | payload = cls._payload(**{"headers": https_client.header()}) 120 | (resp, body) = https_client.api_call(method, endpoint, **payload) 121 | return body 122 | 123 | 124 | @classmethod 125 | def list_reliability_test_notifications( 126 | cls, 127 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 128 | *args: tuple, 129 | **kwargs: dict, 130 | ) -> dict: 131 | ''' 132 | List all reliability test notifications for this team 133 | ''' 134 | method = "GET" 135 | endpoint = cls._required_team_endpoint("/reliability-tests/notifications", **kwargs) 136 | payload = cls._payload(**{"headers": https_client.header()}) 137 | (resp, body) = https_client.api_call(method, endpoint, **payload) 138 | return body 139 | 140 | 141 | @classmethod 142 | def run_single_reliability_test( 143 | cls, 144 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 145 | *args: tuple, 146 | **kwargs: dict, 147 | ) -> dict: 148 | ''' 149 | Run a single reliability test against a service 150 | ''' 151 | method = "POST" 152 | 153 | reliability_test_id = cls._error_if_not_param("reliability_test_id", **kwargs) 154 | cls.__validate_reliability_test_id(reliability_test_id) 155 | 156 | service_id = cls._error_if_not_param("service_id", **kwargs) 157 | data = { 158 | 'serviceId': service_id 159 | } 160 | 161 | if cls.__reliability_test_id_requires_dependency_id(reliability_test_id): 162 | dependency_id = cls._error_if_not_param("dependency_id", **kwargs) 163 | data['dependencyId'] = dependency_id 164 | else: 165 | if 'dependency_id' in kwargs or 'dependencyId' in kwargs: 166 | raise GremlinParameterError( 167 | 'The reliability test you are trying to run does not require a dependency_id' 168 | ) 169 | 170 | endpoint = cls._required_team_endpoint(f"/reliability-tests/{reliability_test_id}/runs", **kwargs) 171 | payload = cls._payload(**{"headers": https_client.header(), "data": json.dumps(data)}) 172 | (resp, body) = https_client.api_call(method, endpoint, **payload) 173 | return body 174 | 175 | 176 | @classmethod 177 | def run_all_reliability_tests( 178 | cls, 179 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 180 | *args: tuple, 181 | **kwargs: dict, 182 | ) -> dict: 183 | ''' 184 | Run all reliability tests for a service 185 | ''' 186 | method = "POST" 187 | service_id = cls._error_if_not_param("service_id", **kwargs) 188 | endpoint = cls._required_team_endpoint(f"/services/{service_id}/baseline", **kwargs) 189 | payload = cls._payload(**{"headers": https_client.header()}) 190 | data = { 191 | 'startBaselineRequest': '' 192 | } 193 | payload = cls._payload(**{"headers": https_client.header(), "data": json.dumps(data)}) 194 | (resp, body) = https_client.api_call(method, endpoint, **payload) 195 | return body 196 | 197 | @classmethod 198 | def get_service_reliability_score( 199 | cls, 200 | https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), 201 | *args: tuple, 202 | **kwargs: dict, 203 | ) -> int: 204 | ''' 205 | Retrieve current score for a service 206 | ''' 207 | method = "GET" 208 | service_id = cls._error_if_not_param("service_id", **kwargs) 209 | endpoint = cls._required_team_endpoint(f"/reliability-management/services/{service_id}", **kwargs) 210 | payload = cls._payload(**{"headers": https_client.header()}) 211 | (resp, body) = https_client.api_call(method, endpoint, **payload) 212 | return body['score'] -------------------------------------------------------------------------------- /gremlinapi/http_clients.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright (C) 2020 Kyle Hultman , Gremlin Inc 4 | 5 | import json 6 | import logging 7 | 8 | from gremlinapi.exceptions import ( 9 | ProxyError, 10 | ClientError, 11 | HTTPTimeout, 12 | HTTPError, 13 | HTTPBadHeader, 14 | ) 15 | 16 | from gremlinapi.config import GremlinAPIConfig 17 | from gremlinapi.util import get_version 18 | 19 | from typing import Tuple, Union, Optional, Any, Dict, Callable, Type 20 | 21 | import requests # type: ignore 22 | import urllib3 # type: ignore 23 | 24 | # try: 25 | # import requests 26 | # import requests.adapters 27 | # except ImportError: 28 | # del requests 29 | # import urllib3 # type: ignore 30 | 31 | log: logging.Logger = logging.getLogger("GremlinAPI.client") 32 | 33 | 34 | class GremlinAPIHttpClient(object): 35 | @classmethod 36 | def api_call( 37 | cls, method: str, endpoint: str, *args: tuple, **kwargs: dict 38 | ) -> Tuple[Union[requests.Response, urllib3.HTTPResponse], dict]: 39 | if requests: 40 | return GremlinAPIRequestsClient.api_call(method, endpoint, *args, **kwargs) 41 | else: 42 | return GremlinAPIurllibClient.api_call(method, endpoint, *args, **kwargs) 43 | 44 | @classmethod 45 | def base_uri(cls, uri: str) -> str: 46 | if not uri.startswith("http") and str(GremlinAPIConfig.base_uri) not in uri: 47 | uri = f"{GremlinAPIConfig.base_uri}{uri}" 48 | return uri 49 | 50 | @classmethod 51 | def header(cls, *args: tuple, **kwargs: dict) -> dict: 52 | api_key: Union[dict, str] = kwargs.get("api_key", "") 53 | bearer_token: Union[dict, str] = kwargs.get("bearer_token", "") 54 | header: dict = dict() 55 | if not (api_key and bearer_token): 56 | if GremlinAPIConfig.bearer_token: 57 | bearer_token = str(GremlinAPIConfig.bearer_token) 58 | if GremlinAPIConfig.api_key: 59 | api_key = str(GremlinAPIConfig.api_key) 60 | if api_key and not bearer_token: 61 | if "Key" in api_key: 62 | header["Authorization"] = api_key 63 | else: 64 | header["Authorization"] = f"Key {api_key}" 65 | elif bearer_token: 66 | if "Bearer" in bearer_token: 67 | header["Authorization"] = bearer_token 68 | else: 69 | header["Authorization"] = f"Bearer {bearer_token}" 70 | else: 71 | error_msg: str = f"Missing API Key or Bearer Token, none supplied: {api_key}, {bearer_token}" 72 | log.error(error_msg) 73 | # raise HTTPBadHeader(error_msg) 74 | header["X-Gremlin-Agent"] = f"gremlin-sdk-python/{get_version()}" 75 | return header 76 | 77 | @classmethod 78 | def proxies(cls) -> dict: 79 | if requests: 80 | return GremlinAPIRequestsClient.proxies() 81 | else: 82 | error_message: str = ( 83 | f"This function is not implemented, proxiea not supported for urllib" 84 | ) 85 | log.error(error_message) 86 | raise NotImplementedError(error_message) 87 | 88 | 89 | class GremlinAPIRequestsClient(GremlinAPIHttpClient): 90 | @classmethod 91 | def proxies(cls) -> dict: 92 | proxies: dict = dict() 93 | if GremlinAPIConfig.http_proxy and type(GremlinAPIConfig.http_proxy) is str: 94 | proxies["http"] = GremlinAPIConfig.http_proxy 95 | if GremlinAPIConfig.https_proxy and type(GremlinAPIConfig.https_proxy) is str: 96 | proxies["https"] = GremlinAPIConfig.https_proxy 97 | return proxies 98 | 99 | @classmethod 100 | def api_call( 101 | cls, method: str, endpoint: str, *args: tuple, **kwargs: dict 102 | ) -> Tuple[requests.Response, dict]: 103 | request_methods: Dict[str, Callable] = { 104 | "HEAD": requests.head, 105 | "GET": requests.get, 106 | "POST": requests.post, 107 | "PUT": requests.put, 108 | "DELETE": requests.delete, 109 | "PATCH": requests.patch, 110 | } 111 | uri: str = cls.base_uri(endpoint) 112 | client: Union[Callable, Any] = request_methods.get(method.upper()) 113 | raw_content: dict = kwargs.pop("raw_content", {}) 114 | data: Union[dict, str] = {} 115 | if "data" in kwargs: 116 | data = kwargs.pop("data") 117 | elif "body" in kwargs: 118 | if "Content-Type" not in kwargs["headers"]: 119 | kwargs["headers"]["Content-Type"] = "application/json" 120 | data = kwargs.pop("body") 121 | if not isinstance(data, str): 122 | data = json.dumps(data) 123 | if log.getEffectiveLevel() == logging.DEBUG: 124 | log.debug(f"body: {data}") 125 | 126 | kwargs["proxies"] = cls.proxies() 127 | if log.getEffectiveLevel() == logging.DEBUG: 128 | log.debug(f"httpd client kwargs: {kwargs}") 129 | 130 | if data: 131 | resp: requests.Response = client( 132 | uri, data=data, allow_redirects=False, **kwargs 133 | ) 134 | else: 135 | resp = client(uri, allow_redirects=False, **kwargs) 136 | 137 | if resp.status_code >= 400: 138 | if resp.reason: 139 | error_msg: str = f"error {resp.status_code} : {resp.reason} - {resp.text}" 140 | log.warning(error_msg) 141 | if log.getEffectiveLevel() == logging.DEBUG: 142 | log.debug(f"{uri}\n{data}\n{kwargs}") 143 | raise HTTPError(error_msg) 144 | body: Any = None 145 | if raw_content: 146 | body = resp.content 147 | else: 148 | try: 149 | body = resp.json() 150 | except ValueError: 151 | # No JSON in response 152 | try: 153 | body = str(resp.content, resp.encoding) 154 | except TypeError: 155 | # Response must be empty, return something nice 156 | body = "Success" 157 | 158 | return resp, body 159 | 160 | 161 | class GremlinAPIurllibClient(GremlinAPIHttpClient): 162 | """Fallback library in the event requests library is unavailable.""" 163 | 164 | import urllib3 165 | 166 | @classmethod 167 | def api_call( 168 | cls, method: str, endpoint: str, *args: tuple, **kwargs: dict 169 | ) -> Tuple[urllib3.HTTPResponse, dict]: 170 | 171 | log.warning( 172 | f"The request to {endpoint} is using the urllib3 library. Consider installing th requests library." 173 | ) 174 | 175 | if "data" in kwargs: 176 | form_data: dict = kwargs.pop("data") 177 | elif "body" in kwargs: 178 | if "Content-Type" not in kwargs["headers"]: 179 | kwargs["headers"]["Content-Type"] = "application/json" 180 | request_body: str = json.dumps(kwargs.pop("body")) 181 | 182 | uri: str = f"{GremlinAPIConfig.base_uri}{endpoint}" 183 | 184 | if GremlinAPIConfig.https_proxy and type(GremlinAPIConfig.https_proxy) is str: 185 | http_client: urllib3.ProxyManager = urllib3.ProxyManager( 186 | GremlinAPIConfig.https_proxy 187 | ) 188 | elif GremlinAPIConfig.http_proxy and type(GremlinAPIConfig.http_proxy) is str: 189 | http_client = urllib3.ProxyManager(GremlinAPIConfig.http_proxy) 190 | else: 191 | http_client = urllib3.PoolManager() 192 | 193 | if form_data: 194 | resp: urllib3.HTTPResponse = http_client.request( 195 | method, uri, fields=form_data, **kwargs 196 | ) 197 | elif request_body: 198 | resp = http_client.request(method, uri, body=request_body, **kwargs) 199 | else: 200 | resp = http_client.request(method, uri, **kwargs) 201 | 202 | if log.getEffectiveLevel() == logging.DEBUG: 203 | log.debug(resp) 204 | 205 | if resp.status >= 400: 206 | if log.getEffectiveLevel() == logging.DEBUG: 207 | log.debug( 208 | f"Failed response: {resp.status}\n{http_client}\n{uri}\n{kwargs}\n{resp}" 209 | ) 210 | raise HTTPError(resp) 211 | 212 | body: dict = json.loads(resp.data) 213 | return resp, body 214 | 215 | 216 | def get_gremlin_httpclient() -> Type[GremlinAPIHttpClient]: 217 | return GremlinAPIHttpClient 218 | -------------------------------------------------------------------------------- /tests/test_scenarios.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.scenarios import GremlinAPIScenarios, GremlinAPIScenariosRecommended 6 | 7 | from .util import mock_json, mock_data, mock_scenario, mock_payload, mock_scenario_guid 8 | 9 | 10 | class TestScenarios(unittest.TestCase): 11 | def test__error_if_not_scenario_body(self) -> None: 12 | test_output = GremlinAPIScenarios._error_if_not_scenario_body(**mock_payload) 13 | self.assertEqual(test_output, str(mock_data)) 14 | 15 | @patch("requests.get") 16 | def test_list_scenarios_with_decorator(self, mock_get) -> None: 17 | mock_get.return_value = requests.Response() 18 | mock_get.return_value.status_code = 200 19 | mock_get.return_value.json = mock_json 20 | self.assertEqual(GremlinAPIScenarios.list_scenarios(), mock_data) 21 | 22 | @patch("requests.post") 23 | def test_create_scenario_with_decorator(self, mock_get) -> None: 24 | mock_get.return_value = requests.Response() 25 | mock_get.return_value.status_code = 200 26 | mock_get.return_value.json = mock_json 27 | self.assertEqual(GremlinAPIScenarios.create_scenario(**mock_payload), mock_data) 28 | 29 | @patch("requests.get") 30 | def test_get_scenario_with_decorator(self, mock_get) -> None: 31 | mock_get.return_value = requests.Response() 32 | mock_get.return_value.status_code = 200 33 | mock_get.return_value.json = mock_json 34 | self.assertEqual( 35 | GremlinAPIScenarios.get_scenario(**mock_scenario_guid), mock_data 36 | ) 37 | 38 | @patch("requests.put") 39 | def test_update_scenario_with_decorator(self, mock_get) -> None: 40 | mock_get.return_value = requests.Response() 41 | mock_get.return_value.status_code = 200 42 | mock_get.return_value.json = mock_json 43 | self.assertEqual( 44 | GremlinAPIScenarios.update_scenario(**mock_scenario_guid), mock_data 45 | ) 46 | 47 | @patch("requests.post") 48 | def test_archive_scenario_with_decorator(self, mock_get) -> None: 49 | mock_get.return_value = requests.Response() 50 | mock_get.return_value.status_code = 200 51 | mock_get.return_value.json = mock_json 52 | self.assertEqual( 53 | GremlinAPIScenarios.archive_scenario(**mock_scenario_guid), mock_data 54 | ) 55 | 56 | @patch("requests.post") 57 | def test_restore_scenario_with_decorator(self, mock_get) -> None: 58 | mock_get.return_value = requests.Response() 59 | mock_get.return_value.status_code = 200 60 | mock_get.return_value.json = mock_json 61 | self.assertEqual( 62 | GremlinAPIScenarios.restore_scenario(**mock_scenario_guid), mock_data 63 | ) 64 | 65 | @patch("requests.get") 66 | def test_list_scenario_runs_with_decorator(self, mock_get) -> None: 67 | mock_get.return_value = requests.Response() 68 | mock_get.return_value.status_code = 200 69 | mock_get.return_value.json = mock_json 70 | self.assertEqual( 71 | GremlinAPIScenarios.list_scenario_runs(**mock_scenario_guid), mock_data 72 | ) 73 | 74 | @patch("requests.get") 75 | def test_list_scenarios_runs_with_decorator(self, mock_get) -> None: 76 | mock_get.return_value = requests.Response() 77 | mock_get.return_value.status_code = 200 78 | mock_get.return_value.json = mock_json 79 | self.assertEqual( 80 | GremlinAPIScenarios.list_scenarios_runs(**mock_scenario_guid), mock_data 81 | ) 82 | 83 | @patch("requests.post") 84 | def test_run_scenario_with_decorator(self, mock_get) -> None: 85 | mock_get.return_value = requests.Response() 86 | mock_get.return_value.status_code = 200 87 | mock_get.return_value.json = mock_json 88 | self.assertEqual( 89 | GremlinAPIScenarios.run_scenario(**mock_scenario_guid), mock_data 90 | ) 91 | 92 | @patch("requests.get") 93 | def test_get_scenario_run_details_with_decorator(self, mock_get) -> None: 94 | mock_get.return_value = requests.Response() 95 | mock_get.return_value.status_code = 200 96 | mock_get.return_value.json = mock_json 97 | self.assertEqual( 98 | GremlinAPIScenarios.get_scenario_run_details(**mock_scenario_guid), 99 | mock_data, 100 | ) 101 | 102 | @patch("requests.put") 103 | def test_update_scenario_result_flags_with_decorator(self, mock_get) -> None: 104 | mock_get.return_value = requests.Response() 105 | mock_get.return_value.status_code = 200 106 | mock_get.return_value.json = mock_json 107 | self.assertEqual( 108 | GremlinAPIScenarios.update_scenario_result_flags(**mock_scenario_guid), 109 | mock_data, 110 | ) 111 | 112 | @patch("requests.put") 113 | def test_update_scenario_result_notes_with_decorator(self, mock_get) -> None: 114 | mock_get.return_value = requests.Response() 115 | mock_get.return_value.status_code = 200 116 | mock_get.return_value.json = mock_json 117 | self.assertEqual( 118 | GremlinAPIScenarios.update_scenario_result_notes(**mock_scenario_guid), 119 | mock_data, 120 | ) 121 | 122 | @patch("requests.get") 123 | def test_list_scenario_schedules_with_decorator(self, mock_get) -> None: 124 | mock_get.return_value = requests.Response() 125 | mock_get.return_value.status_code = 200 126 | mock_get.return_value.json = mock_json 127 | self.assertEqual( 128 | GremlinAPIScenarios.list_scenario_schedules(**mock_scenario_guid), mock_data 129 | ) 130 | 131 | @patch("requests.get") 132 | def test_list_active_scenarios_with_decorator(self, mock_get) -> None: 133 | mock_get.return_value = requests.Response() 134 | mock_get.return_value.status_code = 200 135 | mock_get.return_value.json = mock_json 136 | self.assertEqual( 137 | GremlinAPIScenarios.list_active_scenarios(**mock_scenario_guid), mock_data 138 | ) 139 | 140 | @patch("requests.get") 141 | def test_list_archived_scenarios_with_decorator(self, mock_get) -> None: 142 | mock_get.return_value = requests.Response() 143 | mock_get.return_value.status_code = 200 144 | mock_get.return_value.json = mock_json 145 | self.assertEqual( 146 | GremlinAPIScenarios.list_archived_scenarios(**mock_scenario_guid), mock_data 147 | ) 148 | 149 | @patch("requests.get") 150 | def test_list_draft_scenarios_with_decorator(self, mock_get) -> None: 151 | mock_get.return_value = requests.Response() 152 | mock_get.return_value.status_code = 200 153 | mock_get.return_value.json = mock_json 154 | self.assertEqual( 155 | GremlinAPIScenarios.list_draft_scenarios(**mock_scenario_guid), mock_data 156 | ) 157 | 158 | @patch("requests.post") 159 | def test_halt_scenario_with_decorator(self, mock_get) -> None: 160 | mock_get.return_value = requests.Response() 161 | mock_get.return_value.status_code = 200 162 | mock_get.return_value.json = mock_json 163 | self.assertEqual( 164 | GremlinAPIScenarios.halt_scenario(**mock_scenario_guid), mock_data 165 | ) 166 | 167 | @patch("requests.get") 168 | def test_list_recommended_scenarios_with_decorator(self, mock_get) -> None: 169 | mock_get.return_value = requests.Response() 170 | mock_get.return_value.status_code = 200 171 | mock_get.return_value.json = mock_json 172 | self.assertEqual( 173 | GremlinAPIScenariosRecommended.list_recommended_scenarios( 174 | **mock_scenario_guid 175 | ), 176 | mock_data, 177 | ) 178 | 179 | @patch("requests.get") 180 | def test_get_recommended_scenario_with_decorator(self, mock_get) -> None: 181 | mock_get.return_value = requests.Response() 182 | mock_get.return_value.status_code = 200 183 | mock_get.return_value.json = mock_json 184 | self.assertEqual( 185 | GremlinAPIScenariosRecommended.get_recommended_scenario( 186 | **mock_scenario_guid 187 | ), 188 | mock_data, 189 | ) 190 | 191 | @patch("requests.get") 192 | def test_get_recommended_scenario_static_with_decorator(self, mock_get) -> None: 193 | mock_get.return_value = requests.Response() 194 | mock_get.return_value.status_code = 200 195 | mock_get.return_value.json = mock_json 196 | self.assertEqual( 197 | GremlinAPIScenariosRecommended.get_recommended_scenario_static( 198 | **mock_scenario_guid 199 | ), 200 | mock_data, 201 | ) 202 | -------------------------------------------------------------------------------- /tests/test_users.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | import logging 4 | import requests 5 | from gremlinapi.users import ( 6 | GremlinAPIUsers, 7 | GremlinAPIUsersAuth, 8 | GremlinAPIUsersAuthMFA, 9 | ) 10 | 11 | from .util import mock_json, mock_data, mock_users, mock_body 12 | 13 | 14 | class TestUsers(unittest.TestCase): 15 | # GremlinAPIUsers 16 | def test__error_if_not_valid_role_statement(self) -> None: 17 | test_output = GremlinAPIUsers._error_if_not_valid_role_statement(**mock_users) 18 | self.assertEqual(test_output, mock_users["role"]) 19 | 20 | @patch("requests.get") 21 | def test_list_users_with_decorator(self, mock_get) -> None: 22 | mock_get.return_value = requests.Response() 23 | mock_get.return_value.status_code = 200 24 | mock_get.return_value.json = mock_json 25 | self.assertEqual(GremlinAPIUsers.list_users(), mock_data) 26 | 27 | @patch("requests.post") 28 | def test_add_user_to_team_with_decorator(self, mock_get) -> None: 29 | mock_get.return_value = requests.Response() 30 | mock_get.return_value.status_code = 200 31 | mock_get.return_value.json = mock_json 32 | self.assertEqual(GremlinAPIUsers.add_user_to_team(**mock_body), mock_data) 33 | 34 | @patch("requests.put") 35 | def test_update_user_with_decorator(self, mock_get) -> None: 36 | mock_get.return_value = requests.Response() 37 | mock_get.return_value.status_code = 200 38 | mock_get.return_value.json = mock_json 39 | self.assertEqual(GremlinAPIUsers.update_user(**mock_users), mock_data) 40 | 41 | @patch("requests.delete") 42 | def test_deactivate_user_with_decorator(self, mock_get) -> None: 43 | mock_get.return_value = requests.Response() 44 | mock_get.return_value.status_code = 200 45 | mock_get.return_value.json = mock_json 46 | self.assertEqual(GremlinAPIUsers.deactivate_user(**mock_users), mock_data) 47 | 48 | @patch("requests.get") 49 | def test_list_active_users_with_decorator(self, mock_get) -> None: 50 | mock_get.return_value = requests.Response() 51 | mock_get.return_value.status_code = 200 52 | mock_get.return_value.json = mock_json 53 | self.assertEqual(GremlinAPIUsers.list_active_users(), mock_data) 54 | 55 | @patch("requests.post") 56 | def test_invite_user_with_decorator(self, mock_get) -> None: 57 | mock_get.return_value = requests.Response() 58 | mock_get.return_value.status_code = 200 59 | mock_get.return_value.json = mock_json 60 | self.assertEqual(GremlinAPIUsers.invite_user(**mock_users), mock_data) 61 | 62 | @patch("requests.delete") 63 | def test_revoke_user_invite_with_decorator(self, mock_get) -> None: 64 | mock_get.return_value = requests.Response() 65 | mock_get.return_value.status_code = 200 66 | mock_get.return_value.json = mock_json 67 | self.assertEqual(GremlinAPIUsers.revoke_user_invite(**mock_users), mock_data) 68 | 69 | @patch("requests.get") 70 | def test_renew_user_authorization_with_decorator(self, mock_get) -> None: 71 | mock_get.return_value = requests.Response() 72 | mock_get.return_value.status_code = 200 73 | mock_get.return_value.json = mock_json 74 | self.assertEqual( 75 | GremlinAPIUsers.renew_user_authorization(**mock_users), mock_data 76 | ) 77 | 78 | @patch("requests.get") 79 | def test_renew_user_authorization_rbac_with_decorator(self, mock_get) -> None: 80 | mock_get.return_value = requests.Response() 81 | mock_get.return_value.status_code = 200 82 | mock_get.return_value.json = mock_json 83 | self.assertEqual( 84 | GremlinAPIUsers.renew_user_authorization_rbac(**mock_users), mock_data 85 | ) 86 | 87 | @patch("requests.get") 88 | def test_get_user_self_with_decorator(self, mock_get) -> None: 89 | mock_get.return_value = requests.Response() 90 | mock_get.return_value.status_code = 200 91 | mock_get.return_value.json = mock_json 92 | self.assertEqual(GremlinAPIUsers.get_user_self(), mock_data) 93 | 94 | @patch("requests.patch") 95 | def test_update_user_self_with_decorator(self, mock_get) -> None: 96 | mock_get.return_value = requests.Response() 97 | mock_get.return_value.status_code = 200 98 | mock_get.return_value.json = mock_json 99 | self.assertEqual(GremlinAPIUsers.update_user_self(**mock_body), mock_data) 100 | 101 | @patch("requests.get") 102 | def test_get_user_session_with_decorator(self, mock_get) -> None: 103 | mock_get.return_value = requests.Response() 104 | mock_get.return_value.status_code = 200 105 | mock_get.return_value.json = mock_json 106 | self.assertEqual(GremlinAPIUsers.get_user_session(), mock_data) 107 | 108 | # GremlinAPIUsersAuth 109 | @patch("requests.post") 110 | def test_auth_user_with_decorator(self, mock_get) -> None: 111 | mock_get.return_value = requests.Response() 112 | mock_get.return_value.status_code = 200 113 | mock_get.return_value.json = mock_json 114 | self.assertEqual(GremlinAPIUsersAuth.auth_user(**mock_users), mock_data) 115 | 116 | @patch("requests.post") 117 | def test_auth_user_sso_with_decorator(self, mock_get) -> None: 118 | mock_get.return_value = requests.Response() 119 | mock_get.return_value.status_code = 200 120 | mock_get.return_value.json = mock_json 121 | self.assertEqual(GremlinAPIUsersAuth.auth_user_sso(**mock_users), mock_data) 122 | 123 | @patch("requests.delete") 124 | def test_invalidate_session_with_decorator(self, mock_get) -> None: 125 | mock_get.return_value = requests.Response() 126 | mock_get.return_value.status_code = 200 127 | mock_get.return_value.json = mock_json 128 | self.assertEqual(GremlinAPIUsersAuth.invalidate_session(), mock_data) 129 | 130 | @patch("requests.get") 131 | def test_get_company_affiliations_with_decorator(self, mock_get) -> None: 132 | mock_get.return_value = requests.Response() 133 | mock_get.return_value.status_code = 200 134 | mock_get.return_value.json = mock_json 135 | self.assertEqual( 136 | GremlinAPIUsersAuth.get_company_affiliations(**mock_users), mock_data 137 | ) 138 | 139 | @patch("requests.get") 140 | def test_get_saml_metadata_with_decorator(self, mock_get) -> None: 141 | mock_get.return_value = requests.Response() 142 | mock_get.return_value.status_code = 200 143 | mock_get.return_value.json = mock_json 144 | self.assertEqual(GremlinAPIUsersAuth.get_saml_metadata(), mock_data) 145 | 146 | # GremlinAPIUsersAuthMFA 147 | @patch("requests.post") 148 | def test_auth_user_mfa_with_decorator(self, mock_get) -> None: 149 | mock_get.return_value = requests.Response() 150 | mock_get.return_value.status_code = 200 151 | mock_get.return_value.json = mock_json 152 | self.assertEqual(GremlinAPIUsersAuthMFA.auth_user(**mock_users), mock_data) 153 | 154 | @patch("requests.get") 155 | def test_get_mfa_status_with_decorator(self, mock_get) -> None: 156 | mock_get.return_value = requests.Response() 157 | mock_get.return_value.status_code = 200 158 | mock_get.return_value.json = mock_json 159 | self.assertEqual(GremlinAPIUsersAuthMFA.get_mfa_status(**mock_users), mock_data) 160 | 161 | @patch("requests.get") 162 | def test_get_user_mfa_status_with_decorator(self, mock_get) -> None: 163 | mock_get.return_value = requests.Response() 164 | mock_get.return_value.status_code = 200 165 | mock_get.return_value.json = mock_json 166 | self.assertEqual(GremlinAPIUsersAuthMFA.get_user_mfa_status(), mock_data) 167 | 168 | @patch("requests.post") 169 | def test_disable_mfa_with_decorator(self, mock_get) -> None: 170 | mock_get.return_value = requests.Response() 171 | mock_get.return_value.status_code = 200 172 | mock_get.return_value.json = mock_json 173 | self.assertEqual(GremlinAPIUsersAuthMFA.disable_mfa(**mock_users), mock_data) 174 | 175 | @patch("requests.post") 176 | def test_force_disable_mfa_with_decorator(self, mock_get) -> None: 177 | mock_get.return_value = requests.Response() 178 | mock_get.return_value.status_code = 200 179 | mock_get.return_value.json = mock_json 180 | self.assertEqual( 181 | GremlinAPIUsersAuthMFA.force_disable_mfa(**mock_users), mock_data 182 | ) 183 | 184 | @patch("requests.post") 185 | def test_enable_mfa_with_decorator(self, mock_get) -> None: 186 | mock_get.return_value = requests.Response() 187 | mock_get.return_value.status_code = 200 188 | mock_get.return_value.json = mock_json 189 | self.assertEqual(GremlinAPIUsersAuthMFA.enable_mfa(**mock_users), mock_data) 190 | 191 | @patch("requests.post") 192 | def test_validate_token_with_decorator(self, mock_get) -> None: 193 | mock_get.return_value = requests.Response() 194 | mock_get.return_value.status_code = 200 195 | mock_get.return_value.json = mock_json 196 | self.assertEqual(GremlinAPIUsersAuthMFA.validate_token(**mock_users), mock_data) 197 | --------------------------------------------------------------------------------