├── .github ├── release.yml └── workflows │ ├── _build-docs.yml │ ├── _build-package.yml │ ├── _check-release-notes.yml │ ├── _deploy-docs.yml │ ├── _integration-tests.yml │ ├── _static-checks.yml │ ├── _upload-package.yml │ └── main-cicd.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── docs ├── examples │ ├── alert.md │ ├── case.md │ └── client.md ├── img │ └── strangebee.png ├── index.md ├── reference.md ├── release-notes.md └── styles │ └── extra.css ├── examples ├── alert │ ├── advanced.py │ ├── case_merge.py │ ├── case_promote.py │ ├── delete_bulk.py │ ├── delete_single.py │ ├── fetch_with_find.py │ ├── fetch_with_get.py │ ├── observable_after_alerting.py │ ├── observable_during_alerting.py │ ├── observable_from_file.py │ ├── simple.py │ ├── update_bulk.py │ └── update_single.py ├── case │ ├── advanced.py │ ├── delete.py │ ├── fetch_with_find.py │ ├── fetch_with_get.py │ ├── merge.py │ ├── minimalistic.py │ ├── obervable_simple.py │ ├── observable_file.py │ ├── pages_after_creation.py │ ├── pages_during_creation.py │ ├── procedures_after_creation.py │ ├── tasks_after_creation.py │ ├── tasks_during_creation.py │ ├── update_bulk.py │ └── update_single.py └── client │ ├── auth_with_apikey.py │ ├── auth_with_username_and_password.py │ ├── org_during_runtime.py │ ├── org_via_constructor.py │ ├── retries.py │ └── ssl.py ├── mkdocs.yml ├── pyproject.toml ├── scripts ├── cd.py ├── ci.py └── linkify_release_notes.py ├── tests ├── __init__.py ├── conftest.py ├── test_alert_endpoint.py ├── test_case_endpoint.py ├── test_case_template_endpoint.py ├── test_comment_endpoint.py ├── test_cortex_endpoint.py ├── test_custom_field_endpoint.py ├── test_init.py ├── test_observable_endpoint.py ├── test_observable_type_endpoint.py ├── test_organisation_endpoint.py ├── test_procedure_endpoint.py ├── test_profile_endpoint.py ├── test_query_endpoint.py ├── test_query_filters.py ├── test_task_endpoint.py ├── test_task_log_endpoint.py ├── test_timeline_endpoint.py ├── test_user_endpoint.py └── utils.py └── thehive4py ├── __init__.py ├── __version__.py ├── client.py ├── endpoints ├── __init__.py ├── _base.py ├── alert.py ├── case.py ├── case_template.py ├── comment.py ├── cortex.py ├── custom_field.py ├── observable.py ├── observable_type.py ├── organisation.py ├── procedure.py ├── profile.py ├── query.py ├── task.py ├── task_log.py ├── timeline.py └── user.py ├── errors.py ├── helpers.py ├── py.typed ├── query ├── __init__.py ├── filters.py ├── page.py └── sort.py ├── session.py └── types ├── __init__.py ├── alert.py ├── attachment.py ├── case.py ├── case_template.py ├── comment.py ├── cortex.py ├── custom_field.py ├── observable.py ├── observable_type.py ├── organisation.py ├── page.py ├── procedure.py ├── profile.py ├── share.py ├── task.py ├── task_log.py ├── timeline.py └── user.py /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - changelog:ignore 5 | categories: 6 | - title: Added 7 | labels: 8 | - changelog:added 9 | - title: Changed 10 | labels: 11 | - changelog:changed 12 | - title: Removed 13 | labels: 14 | - changelog:removed 15 | - title: Deprecated 16 | labels: 17 | - changelog:deprecated 18 | - title: Fixed 19 | labels: 20 | - changelog:fixed 21 | - title: Security 22 | labels: 23 | - changelog:security 24 | - title: Other 25 | labels: 26 | - "*" -------------------------------------------------------------------------------- /.github/workflows/_build-docs.yml: -------------------------------------------------------------------------------- 1 | name: build-docs 2 | on: 3 | workflow_call: 4 | jobs: 5 | build: 6 | name: Build docs 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Set up Python 11 | uses: actions/setup-python@v4 12 | with: 13 | python-version: 3.13 14 | - name: Install build dependencies 15 | run: pip install --no-cache-dir -U pip .['docs'] 16 | - name: Build docs 17 | run: ./scripts/cd.py --build-docs -------------------------------------------------------------------------------- /.github/workflows/_build-package.yml: -------------------------------------------------------------------------------- 1 | name: build-package 2 | on: 3 | workflow_call: 4 | jobs: 5 | build: 6 | name: Build wheel and sdist 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Set up Python 11 | uses: actions/setup-python@v4 12 | with: 13 | python-version: 3.13 14 | - name: Install build dependencies 15 | run: pip install --no-cache-dir -U pip .['build'] 16 | - name: Build package 17 | run: ./scripts/cd.py --build 18 | - name: Upload built distributions 19 | uses: actions/upload-artifact@v4 20 | with: 21 | name: dist 22 | path: dist -------------------------------------------------------------------------------- /.github/workflows/_check-release-notes.yml: -------------------------------------------------------------------------------- 1 | name: check-release-notes 2 | on: 3 | workflow_call: 4 | jobs: 5 | build: 6 | name: Check release notes 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Set up Python 11 | uses: actions/setup-python@v4 12 | with: 13 | python-version: 3.13 14 | - name: Check release notes 15 | run: ./scripts/linkify_release_notes.py --check 16 | - name: Lookup version in release notes 17 | run: | 18 | VERSION=$(grep -Po '(?<=version = ")[^"]*' pyproject.toml) 19 | if ! grep -qF "## $VERSION " "docs/release-notes.md"; then 20 | echo "No release notes found for version '$VERSION'" 21 | exit 1 22 | fi 23 | -------------------------------------------------------------------------------- /.github/workflows/_deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: deploy-docs 2 | on: 3 | workflow_call: 4 | jobs: 5 | upload: 6 | name: Deploy docs to github pages 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Set up Python 11 | uses: actions/setup-python@v4 12 | with: 13 | python-version: 3.13 14 | - name: Install build dependencies 15 | run: pip install --no-cache-dir -U pip .['docs'] 16 | - name: Configure git 17 | run: | 18 | git fetch origin gh-pages --depth=1 19 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 20 | git config --local user.name "github-actions[bot]" 21 | - name: Deploy to github pages 22 | run: ./scripts/cd.py --deploy-docs 23 | -------------------------------------------------------------------------------- /.github/workflows/_integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: integration-tests 2 | on: 3 | workflow_call: 4 | secrets: 5 | CODECOV_TOKEN: 6 | required: true 7 | jobs: 8 | integration-tests: 9 | name: Run integration tests 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: 3.13 17 | - name: Install dependencies 18 | run: pip install --no-cache-dir -U pip .['test'] 19 | - name: Run integration tests 20 | run: scripts/ci.py --test 21 | - name: Upload coverage results 22 | uses: codecov/codecov-action@v4 23 | with: 24 | token: ${{ secrets.CODECOV_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/_static-checks.yml: -------------------------------------------------------------------------------- 1 | name: static-checks 2 | on: 3 | workflow_call: 4 | jobs: 5 | static-checks: 6 | name: Run static checks 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: pip install --no-cache-dir -U pip .['dev'] 19 | - name: Lint check with flake8 20 | run: scripts/ci.py --lint 21 | - name: Format check with black 22 | run: scripts/ci.py --format 23 | - name: Type check with mypy 24 | run: scripts/ci.py --type 25 | - name: CVE check with pip-audit 26 | run: scripts/ci.py --cve 27 | - name: Security check with bandit 28 | run: scripts/ci.py --security 29 | -------------------------------------------------------------------------------- /.github/workflows/_upload-package.yml: -------------------------------------------------------------------------------- 1 | name: upload-package 2 | on: 3 | workflow_call: 4 | secrets: 5 | PYPI_TOKEN: 6 | required: true 7 | jobs: 8 | upload: 9 | name: Upload wheel and sdist 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Compare tag and package version 14 | run: | 15 | TAG=${GITHUB_REF#refs/*/} 16 | VERSION=$(grep -Po '(?<=version = ")[^"]*' pyproject.toml) 17 | if [ "$TAG" != "$VERSION" ]; then 18 | echo "Tag value and package version are different: ${TAG} != ${VERSION}" 19 | exit 1 20 | fi 21 | - name: Download built distributions 22 | uses: actions/download-artifact@v4 23 | with: 24 | name: dist 25 | path: dist 26 | - name: Set up Python 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: 3.13 30 | - name: Install build dependencies 31 | run: pip install --no-cache-dir -U pip .['build'] 32 | - name: Upload to PyPI 33 | run: ./scripts/cd.py --upload 34 | env: 35 | TWINE_REPOSITORY_URL: https://upload.pypi.org/legacy/ 36 | TWINE_USERNAME: __token__ 37 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 38 | -------------------------------------------------------------------------------- /.github/workflows/main-cicd.yml: -------------------------------------------------------------------------------- 1 | name: cicd 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - "*" 8 | pull_request: 9 | jobs: 10 | static-checks: 11 | uses: ./.github/workflows/_static-checks.yml 12 | integration-tests: 13 | uses: ./.github/workflows/_integration-tests.yml 14 | secrets: 15 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 16 | build-package: 17 | uses: ./.github/workflows/_build-package.yml 18 | build-docs: 19 | uses: ./.github/workflows/_build-docs.yml 20 | check-release-notes: 21 | uses: ./.github/workflows/_check-release-notes.yml 22 | deploy-docs: 23 | if: startsWith(github.ref, 'refs/heads/main') 24 | uses: ./.github/workflows/_deploy-docs.yml 25 | needs: [build-docs, build-package, integration-tests, static-checks] 26 | upload-package: 27 | if: startsWith(github.ref, 'refs/tags/') 28 | uses: ./.github/workflows/_upload-package.yml 29 | needs: [build-docs, build-package, integration-tests, static-checks] 30 | secrets: 31 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # generic 2 | __pycache__/ 3 | thehive4py.egg-info/ 4 | build/ 5 | dist/ 6 | 7 | # pytest 8 | .coverage 9 | coverage.xml 10 | 11 | # mkdocs 12 | site/ 13 | 14 | # misc 15 | tmp/ 16 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # experimental (works from the terminal but not from vscode and possibly from other IDEs) 2 | repos: 3 | - repo: local 4 | hooks: 5 | - id: ci-checks 6 | name: ci-checks 7 | entry: scripts/ci.py 8 | language: system 9 | pass_filenames: false 10 | always_run: true 11 | stages: [pre-push] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 StrangeBee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/examples/alert.md: -------------------------------------------------------------------------------- 1 | # Alert 2 | 3 | ## A simple alert 4 | 5 | A new alert requires at least these fields to be defined: 6 | 7 | - `type`: The type of the alert. 8 | - `source`: The source of the alert. 9 | - `sourceRef`: A unique reference for the alert. 10 | - `title`: A descriptive title for the alert. 11 | - `description`: Additional information describing the alert. 12 | 13 | Here's an example that demonstrates how to create the most simplistic alert possible using the [alert.create][thehive4py.endpoints.alert.AlertEndpoint.create] method: 14 | 15 | ```python 16 | --8<-- "examples/alert/simple.py" 17 | ``` 18 | 19 | ## An advanced alert 20 | 21 | The previous example was as simple as it gets and only specified the required alert fields inline in the create method call. 22 | With a more advanced example this can become complicated and hard to read. 23 | Fortunately we can use `thehive4py`'s type hints to the rescue and specify more complex input alerts outside of the method call. 24 | 25 | Here's how: 26 | ```python 27 | --8<-- "examples/alert/advanced.py" 28 | ``` 29 | 30 | In the above snippet `input_alert` is created before the create call and later passed to the `alert` argument. 31 | Finally after the creation of the alert we saved the response in the `output_alert` to be able to use it later. 32 | 33 | !!! note 34 | While the above alert is a bit more advanced it's still far from the most complex example possible. 35 | In case you want to see the what the Alert API offers please check out the [official alert docs](https://docs.strangebee.com/thehive/api-docs/#tag/Alert). 36 | 37 | 38 | ## Alert observables 39 | 40 | In TheHive an observable is a piece of data or evidence (e.g., an IP address, domain, etc.) associated with a security incident, used to provide context and aid in the investigation and response process. 41 | 42 | Let's take a look at different ways of populating alerts with observables, let them be textual or file based observables. 43 | 44 | ### Add observables during alert creation 45 | 46 | We can add observables already during alert creation. This is a great way to combine alert and observable creation in an atomic way: 47 | 48 | Let's create an alert with an `ip` and a `domain` observable: 49 | 50 | ```python 51 | --8<-- "examples/alert/observable_during_alerting.py" 52 | ``` 53 | 54 | ### Add observables to an existing alert 55 | 56 | While it's probably the most convenient way to combine alert and observable creation in a single call, sometimes we don't have all the observables at hand during alert creation time or we have such a large number of observables that we cannot send them all in one single request. 57 | 58 | Fortunately TheHive API supports alert observable creation on already existing alerts. Let's repeat the previous example, but this time add the two observables to an existing alert using the [alert.create_observable][thehive4py.endpoints.alert.AlertEndpoint.create_observable] method: 59 | 60 | 61 | ```python 62 | --8<-- "examples/alert/observable_after_alerting.py" 63 | ``` 64 | 65 | ### Add file based observables 66 | 67 | In the previous examples we've seen how to handle observables without attachments. However sometimes we also want to add attachments to an observable not only textual data. Fortunately that is supported by TheHive. So in the next example let's create a temporary directory with a dummy file and some dummy content that will represent our file based observable and add it to an alert: 68 | 69 | 70 | ```python 71 | --8<-- "examples/alert/observable_from_file.py" 72 | ``` 73 | 74 | As we can see from the above example a file based observable must specify the `attachment` property with a key that links it to the attachment specified in the `attachment_map` dictionary. 75 | 76 | This way TheHive will know which attachment to pair with which observable behind the scenes. 77 | 78 | In our example `attachment_key` is used to specify the relationship between the observable and the actual file. In this case its value is a uuid, however it can be any arbitrary value, though it's important that it should uniquely identify the attachment and the observable we would like to pair in TheHive. 79 | 80 | ## Update single and bulk 81 | 82 | Creating alerts is fun but sometimes an existing alert also needs to be updated. As expected `thehive4py` offers multiple ways to accomplish this task either on a single alert or multiple ones. 83 | 84 | ### Update single 85 | 86 | A single alert can be updated using the [alert.update][thehive4py.endpoints.alert.AlertEndpoint.update] method. The method requires the `alert_id` of the alert to be updated and the `fields` to update. 87 | 88 | ```python 89 | --8<-- "examples/alert/update_single.py" 90 | ``` 91 | 92 | In the above example we've updated the `title` and the `tags` fields. 93 | 94 | Be mindful though, `thehive4py` is a lightweight wrapper around TheHive API and offers no object relationship mapping functionalities, meaning that the `original_alert` won't reflect the changes of the update. 95 | 96 | In order to work with the updated alert we had to fetch the latest version using the [alert.get][thehive4py.endpoints.alert.AlertEndpoint.get] method and store it in the `updated_alert` variable. 97 | 98 | Now the content of `updated_alert` should reflect the changes we made with our update request. 99 | 100 | !!! tip 101 | To see the full list of supported update fields please consult the [official docs](https://docs.strangebee.com/thehive/api-docs/#tag/Alert/operation/Update%20Alert). 102 | 103 | ### Update bulk 104 | 105 | It is also possible to update many alerts at the same time, however there's a constraint: the content of the `fields` property will be applied to all the specified alerts uniformly. With all that said one can use [alert.bulk_update][thehive4py.endpoints.alert.AlertEndpoint.bulk_update] method for bulk updates. 106 | The method accepts the same `fields` dictionary as before but with an additional `ids` field on it, which should contain the list of ids of the alerts to be bulk updated. 107 | 108 | ```python 109 | --8<-- "examples/alert/update_bulk.py" 110 | ``` 111 | 112 | In the example we prepare two alerts for the bulk update, and collect their ids in the `original_alert_ids` list. 113 | Then we update the fields `title` and `tags` on both alerts using the bulk update method. 114 | 115 | ## Get and find 116 | 117 | There are multiple ways to retrieve already existing alerts, we can fetch them one by one or many at once! 118 | 119 | ### Get a single alert 120 | 121 | To get a single alert one can use the [alert.get][thehive4py.endpoints.alert.AlertEndpoint.get] method with the alert's id as follows: 122 | 123 | ```python 124 | --8<-- "examples/alert/fetch_with_get.py" 125 | ``` 126 | 127 | ### Find multiple alerts 128 | 129 | To fetch multiple alerts based on arbitrary conditions one can use the [alert.find][thehive4py.endpoints.alert.AlertEndpoint.find] method which is an abstraction on top of TheHive's Query API. 130 | 131 | In the next example we will create two alerts with different tags. The first alert will get the `antivirus` tag while the second one will get the `phishing` tag. 132 | 133 | Then we will construct query filters in different ways to look for alerts with these tags on them: 134 | 135 | ```python 136 | --8<-- "examples/alert/fetch_with_find.py" 137 | ``` 138 | 139 | The above example demonstrates two ways to construct query filters. 140 | 141 | One is to provide a raw dict based filter which is the plain format of [TheHive's Query API](https://docs.strangebee.com/thehive/api-docs/#tag/Query-and-Export). This is demonstrated in the `raw_filters` variable. 142 | 143 | However this can be cumbersome to remember, that's why `thehive4py` provides filter builders to conveniently build filter expressions on the client side. This alternative approach is demonstrated in the `class_filters` variable. 144 | 145 | These filter expressions can be chained together with different operators, just like we did with the `|` (`or`) operator in the example. 146 | 147 | Currently, the filter classes support the following operators: 148 | 149 | - `&`: Used for the Query API's `_and` construct. 150 | - `|`: Used for the Query API's `_or` construct. 151 | - `~`: Used for the Query API's `_not` construct. 152 | 153 | The full list of the filter builders can be found in the [query.filters][thehive4py.query.filters] module. 154 | 155 | ## Promote and merge into a case 156 | 157 | In TheHive alerts usually represent signals of compromise while cases provide a higher level entity to group these signals into one object. 158 | Therefore we can promote an alert into a case or merge new alerts into an existing case for a more organised investigation. 159 | 160 | ### Promote to case 161 | 162 | To create a case from an alert we can use [alert.promote_to_case][thehive4py.endpoints.alert.AlertEndpoint.promote_to_case] method. 163 | 164 | ```python 165 | --8<-- "examples/alert/case_promote.py" 166 | ``` 167 | 168 | !!! tip 169 | For additional control the method accepts a `fields` argument which can be used to modify properties on the case. 170 | To see all available options please consult the [official docs](https://docs.strangebee.com/thehive/api-docs/#tag/Alert/operation/Create%20Case%20from%20Alert). 171 | 172 | ### Merge into case 173 | 174 | Oftentimes new alerts correspond to an already existing case. Fortunately we have the option to merge such alerts into a parent case using the [alert.merge_into_case][thehive4py.endpoints.alert.AlertEndpoint.merge_into_case] method. 175 | 176 | ```python 177 | --8<-- "examples/alert/case_merge.py" 178 | ``` 179 | 180 | In the above example we prepared a `parent_case` to which we merge the `new_alert` using its id and finally save the updated case in the `updated_parent_case` variable. 181 | 182 | !!! tip 183 | It can happen that multiple new alerts belong to the same parent case. In such situation we can use the [alert.bulk_merge_into_case][thehive4py.endpoints.alert.AlertEndpoint.bulk_merge_into_case] method for a more convenient merge process. 184 | 185 | 186 | ## Delete single and bulk 187 | 188 | `thehive4py` provides two different ways to delete alerts: 189 | 190 | - delete a single alert 191 | - delete alerts in bulk 192 | 193 | ### Delete single 194 | 195 | To delete a single alert the [alert.delete][thehive4py.endpoints.alert.AlertEndpoint.delete] method can be used as follows: 196 | 197 | ```python 198 | --8<-- "examples/alert/delete_single.py" 199 | ``` 200 | 201 | 202 | ### Delete in bulk 203 | 204 | To delete multiple alerts via a single request one can use the [alert.bulk_delete][thehive4py.endpoints.alert.AlertEndpoint.bulk_delete] method as follows: 205 | 206 | ```python 207 | --8<-- "examples/alert/delete_bulk.py" 208 | ``` 209 | 210 | In the above example we created two alerts and saved their ids in the `alert_ids_to_delete` variable just to pass it to the bulk deletion method. 211 | -------------------------------------------------------------------------------- /docs/examples/client.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | ## Authentication 4 | 5 | TheHive API provides two ways to authenticate the client: 6 | 7 | - apikey auth 8 | - username and password auth 9 | 10 | ### Auth with apikey 11 | 12 | ```python 13 | --8<-- "examples/client/auth_with_apikey.py" 14 | ``` 15 | 16 | ### Auth with username and password 17 | 18 | ```python 19 | --8<-- "examples/client/auth_with_username_and_password.py" 20 | ``` 21 | 22 | ## Organisation 23 | 24 | The client will use the default organisation of the user. However in case the user belongs to multiple organisation the client also provides options to specify which organisation to use. 25 | 26 | 27 | ### Specify the organisation during init 28 | 29 | In this example we will instaniate a client with the `admin` organisation explicitly: 30 | 31 | ```python 32 | --8<-- "examples/client/org_via_constructor.py" 33 | ``` 34 | 35 | ### Switch organisations during runtime 36 | 37 | In this example we will instantiate a client without explicitly specifying an organisation and switch to another organisation using the [session_organisation][thehive4py.client.TheHiveApi.session_organisation] property: 38 | 39 | ```python 40 | --8<-- "examples/client/org_during_runtime.py" 41 | ``` 42 | 43 | !!! warning 44 | The [session_organisation][thehive4py.client.TheHiveApi.session_organisation] property is not thread-safe and it's almost always better to instantiate more clients if one wants to work with multiple organisations in parallel. 45 | 46 | 47 | ## SSL Verification 48 | 49 | By default the client verifies if the connection is going through SSL. 50 | In case one needs to pass a custom certificate bundle or directory it's possible via the `verify` argument like: 51 | 52 | ```python 53 | --8<-- "examples/client/ssl.py" 54 | ``` 55 | 56 | !!! note 57 | It's also possible to disable SSL verification completely by setting `verify` to `False`. 58 | However this is greatly discouraged as it's a security bad practice. 59 | 60 | 61 | ## Retries 62 | 63 | The client comes with a sensible retry mechanism by default that will try to cover the most common use cases. 64 | However it's also possible to configure a tailored retry mechanism via the `max_retries` argument. 65 | 66 | The below example will configure a custom retry mechanism with 5 total attempts, a backoff factor of 0.3 seconds on GET methods and 500 status codes: 67 | 68 | ```python 69 | --8<-- "examples/client/retries.py" 70 | ``` 71 | 72 | To learn more about the `urllib3.Retry` object please consult the official documentation [here](https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html#urllib3.util.Retry). 73 | 74 | 75 | -------------------------------------------------------------------------------- /docs/img/strangebee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/TheHive4py/62a3f9714f9c27eb55017fab79ccd00ea8f593f0/docs/img/strangebee.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | TheHive Logo 4 | 5 | thehive4py 6 |

7 |

8 | the de facto Python API client of TheHive 9 |

10 |

11 | 12 | release 13 | 14 | 15 | build 16 | 17 | 18 | codecov 19 | 20 | 21 | pypi 22 | 23 | 24 | license 25 | 26 | 27 | discord 28 | 29 |

30 |
31 | 32 | --- 33 | **Documentation**: https://thehive-project.github.io/TheHive4py/ 34 | 35 | **Source Code**: https://github.com/TheHive-Project/TheHive4py 36 | 37 | --- 38 | 39 | # Introduction 40 | 41 | Welcome to `thehive4py`, the Python library designed to simplify interactions with StrangeBee's TheHive. Whether you're a cybersecurity enthusiast or a developer looking to integrate TheHive into your Python projects, this library has got you covered. 42 | 43 | Feel free to explore the library's capabilities and contribute to its development. We appreciate your support in making TheHive integration in Python more accessible and efficient. 44 | 45 | 46 | # Requirements 47 | `thehive4py` works with all currently supported python versions, at the time of writing `py>=3.8`. One can check the official version support and end of life status [here](https://devguide.python.org/versions/). 48 | 49 | # Installation 50 | The `thehive4py` can be installed with pip like: 51 | 52 | ```bash 53 | pip install "thehive4py>=2.0.0b" 54 | ``` 55 | 56 | 57 | # Quickstart 58 | 59 | ```python 60 | from thehive4py import TheHiveApi 61 | 62 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 63 | ``` 64 | 65 | # Licensing 66 | 67 | This project is licensed under the terms of the MIT license. 68 | 69 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | [//]: # (Style hack due to capitalized h5 headers from mkdocs-material, which corrupts the reference docs display) 2 | [//]: # (More details: https://github.com/squidfunk/mkdocs-material/issues/1522) 3 | 8 | 9 | # API Reference 10 | 11 | ::: thehive4py 12 | options: 13 | show_submodules: true 14 | members: 15 | - client 16 | - session 17 | - endpoints 18 | - types 19 | - query 20 | - errors 21 | - helpers 22 | -------------------------------------------------------------------------------- /docs/styles/extra.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --md-primary-fg-color: #0049d4; 3 | --md-accent-fg-color: #ffc72c; 4 | } 5 | 6 | @keyframes pulse { 7 | 0%, 100% { 8 | transform: scale(1); 9 | } 10 | 50% { 11 | transform: scale(1.25); 12 | } 13 | } 14 | 15 | 16 | .coming-soon { 17 | color: var(--md-primary-fg-color); 18 | font-size: 1.5rem; 19 | font-weight: bold; 20 | transition: 0.75s; 21 | } 22 | 23 | .coming-soon:hover{ 24 | color: var(--md-accent-fg-color); 25 | transition: 0.75s; 26 | } 27 | 28 | .upleftbee { 29 | rotate: -90deg; 30 | } 31 | 32 | .upbee { 33 | rotate: -45deg; 34 | } 35 | 36 | .uprightbee{ 37 | rotate: 0deg; 38 | } 39 | 40 | .downleftbee { 41 | rotate: 180deg; 42 | } 43 | 44 | .downbee { 45 | rotate: 135deg; 46 | } 47 | 48 | .downrightbee{ 49 | rotate: 90deg; 50 | } 51 | 52 | .yellowbee { 53 | color: var(--md-accent-fg-color) 54 | } 55 | 56 | .bluebee { 57 | color: var(--md-primary-fg-color) 58 | } 59 | 60 | .strangebee { 61 | animation: pulse 2s infinite; 62 | font-size: 1.5rem; 63 | vertical-align: top !important; 64 | } 65 | -------------------------------------------------------------------------------- /examples/alert/advanced.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | from thehive4py.types.alert import InputAlert 3 | 4 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 5 | 6 | input_alert: InputAlert = { 7 | "type": "advanced", 8 | "source": "tutorial", 9 | "sourceRef": "should-be-unique", 10 | "title": "an advanced alert", 11 | "description": "a bit more advanced", 12 | "tags": ["advanced", "example"], 13 | "severity": 1, 14 | "caseTemplate": "my-template", 15 | } 16 | 17 | output_alert = hive.alert.create(alert=input_alert) 18 | -------------------------------------------------------------------------------- /examples/alert/case_merge.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thehive4py import TheHiveApi 4 | 5 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 6 | 7 | 8 | parent_case = hive.case.create( 9 | case={"title": "parent case", "description": "a simple parent case"} 10 | ) 11 | 12 | new_alert = hive.alert.create( 13 | alert={ 14 | "type": "merge-into-case", 15 | "source": "tutorial", 16 | "sourceRef": uuid.uuid4().hex, 17 | "title": "alert to merge", 18 | "description": "a single alert to merge into a parent case", 19 | } 20 | ) 21 | 22 | updated_parent_case = hive.alert.merge_into_case( 23 | alert_id=new_alert["_id"], case_id=parent_case["_id"] 24 | ) 25 | -------------------------------------------------------------------------------- /examples/alert/case_promote.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thehive4py import TheHiveApi 4 | 5 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 6 | 7 | alert_to_promote = hive.alert.create( 8 | alert={ 9 | "type": "promote", 10 | "source": "tutorial", 11 | "sourceRef": uuid.uuid4().hex, 12 | "title": "promote to case", 13 | "description": "an alert to promote to case", 14 | } 15 | ) 16 | 17 | case_from_alert = hive.alert.promote_to_case(alert_id=alert_to_promote["_id"]) 18 | -------------------------------------------------------------------------------- /examples/alert/delete_bulk.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thehive4py import TheHiveApi 4 | 5 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 6 | 7 | alert_ids_to_delete = [] 8 | 9 | for i in range(2): 10 | alert_to_delete = hive.alert.create( 11 | alert={ 12 | "type": "delete", 13 | "source": "tutorial", 14 | "sourceRef": uuid.uuid4().hex, 15 | "title": f"delete alert #{i}", 16 | "description": "an alert to delete", 17 | } 18 | ) 19 | alert_ids_to_delete.append(alert_to_delete["_id"]) 20 | 21 | 22 | hive.alert.bulk_delete(ids=alert_ids_to_delete) 23 | -------------------------------------------------------------------------------- /examples/alert/delete_single.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thehive4py import TheHiveApi 4 | 5 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 6 | 7 | alert_to_delete = hive.alert.create( 8 | alert={ 9 | "type": "delete", 10 | "source": "tutorial", 11 | "sourceRef": uuid.uuid4().hex, 12 | "title": "delete alert", 13 | "description": "an alert to delete", 14 | } 15 | ) 16 | 17 | 18 | hive.alert.delete(alert_id=alert_to_delete["_id"]) 19 | -------------------------------------------------------------------------------- /examples/alert/fetch_with_find.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thehive4py import TheHiveApi 4 | from thehive4py.query.filters import Eq 5 | 6 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 7 | 8 | antivirus_alert = hive.alert.create( 9 | alert={ 10 | "type": "find-multiple", 11 | "source": "tutorial", 12 | "sourceRef": uuid.uuid4().hex, 13 | "title": "alert to find", 14 | "description": "an alert to find with others", 15 | "tags": ["antivirus"], 16 | } 17 | ) 18 | 19 | phishing_alert = hive.alert.create( 20 | alert={ 21 | "type": "find-multiple", 22 | "source": "tutorial", 23 | "sourceRef": uuid.uuid4().hex, 24 | "title": "alert to find", 25 | "description": "an alert to find with others", 26 | "tags": ["phishing"], 27 | } 28 | ) 29 | 30 | 31 | raw_filters = { 32 | "_or": [ 33 | {"_eq": {"_field": "tags", "_value": "antivirus"}}, 34 | {"_eq": {"_field": "tags", "_value": "phishing"}}, 35 | ] 36 | } 37 | all_alerts_with_raw_filters = hive.alert.find(filters=raw_filters) 38 | 39 | class_filters = Eq(field="tags", value="antivirus") | Eq(field="tags", value="phishing") 40 | all_alerts_with_class_filters = hive.alert.find(filters=class_filters) 41 | -------------------------------------------------------------------------------- /examples/alert/fetch_with_get.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thehive4py import TheHiveApi 4 | 5 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 6 | 7 | alert_to_get = hive.alert.create( 8 | alert={ 9 | "type": "get-single", 10 | "source": "tutorial", 11 | "sourceRef": uuid.uuid4().hex, 12 | "title": "alert to get", 13 | "description": "a single alert to fetch", 14 | } 15 | ) 16 | 17 | 18 | fetched_alert = hive.alert.get(alert_id=alert_to_get["_id"]) 19 | -------------------------------------------------------------------------------- /examples/alert/observable_after_alerting.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thehive4py import TheHiveApi 4 | from thehive4py.types.alert import InputAlert 5 | from thehive4py.types.observable import InputObservable 6 | 7 | hive = TheHiveApi(url="thehive.example", apikey="h1v3b33") 8 | 9 | input_alert: InputAlert = { 10 | "type": "alert-without-observables", 11 | "source": "example", 12 | "sourceRef": uuid.uuid4().hex, 13 | "title": "alert without observables", 14 | "description": "alert without observables", 15 | } 16 | 17 | output_alert = hive.alert.create(alert=input_alert) 18 | 19 | 20 | input_observables: list[InputObservable] = [ 21 | {"dataType": "ip", "data": "1.2.3.4"}, 22 | {"dataType": "domain", "data": "example.com"}, 23 | ] 24 | 25 | 26 | for input_observable in input_observables: 27 | hive.alert.create_observable( 28 | alert_id=output_alert["_id"], observable=input_observable 29 | ) 30 | -------------------------------------------------------------------------------- /examples/alert/observable_during_alerting.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thehive4py import TheHiveApi 4 | from thehive4py.types.alert import InputAlert 5 | 6 | hive = TheHiveApi(url="thehive.example", apikey="h1v3b33") 7 | 8 | input_alert: InputAlert = { 9 | "type": "alert-with-observables", 10 | "source": "example", 11 | "sourceRef": uuid.uuid4().hex, 12 | "title": "alert with observables", 13 | "description": "alert with observables", 14 | "observables": [ 15 | {"dataType": "ip", "data": "1.2.3.4"}, 16 | {"dataType": "domain", "data": "example.com"}, 17 | ], 18 | } 19 | 20 | hive.alert.create(alert=input_alert) 21 | -------------------------------------------------------------------------------- /examples/alert/observable_from_file.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import tempfile 3 | import uuid 4 | 5 | from thehive4py import TheHiveApi 6 | from thehive4py.types.alert import InputAlert 7 | 8 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 9 | 10 | with tempfile.TemporaryDirectory() as tmpdir: 11 | 12 | observable_filepath = os.path.join(tmpdir, "my-observable.txt") 13 | with open(observable_filepath, "w") as observable_file: 14 | observable_file.write("some observable content") 15 | 16 | attachment_key = uuid.uuid4().hex 17 | attachment_map = {attachment_key: observable_filepath} 18 | input_alert: InputAlert = { 19 | "type": "alert-with-file-observable", 20 | "source": "example", 21 | "sourceRef": uuid.uuid4().hex, 22 | "title": "alert with file observables", 23 | "description": "alert with file observables", 24 | "observables": [ 25 | {"dataType": "file", "attachment": attachment_key}, 26 | ], 27 | } 28 | 29 | hive.alert.create(alert=input_alert, attachment_map=attachment_map) 30 | -------------------------------------------------------------------------------- /examples/alert/simple.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 4 | 5 | simple_alert = hive.alert.create( 6 | alert={ 7 | "type": "simple", 8 | "source": "tutorial", 9 | "sourceRef": "should-be-unique", 10 | "title": "a simple alert", 11 | "description": "a bit too simple", 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /examples/alert/update_bulk.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thehive4py import TheHiveApi 4 | 5 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 6 | 7 | original_alert_ids = [] 8 | for i in range(2): 9 | original_alert = hive.alert.create( 10 | alert={ 11 | "type": "update-bulk", 12 | "source": "tutorial", 13 | "sourceRef": uuid.uuid4().hex, 14 | "title": f"original alert #{i}", 15 | "description": "an alert to update in bulk", 16 | } 17 | ) 18 | 19 | original_alert_ids.append(original_alert["_id"]) 20 | 21 | 22 | hive.alert.bulk_update( 23 | fields={ 24 | "ids": original_alert_ids, 25 | "title": "bulk updated alert", 26 | "tags": ["update-bulk"], 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /examples/alert/update_single.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from thehive4py import TheHiveApi 4 | 5 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 6 | 7 | original_alert = hive.alert.create( 8 | alert={ 9 | "type": "update-single", 10 | "source": "tutorial", 11 | "sourceRef": uuid.uuid4().hex, 12 | "title": "original alert", 13 | "description": "a single alert to update", 14 | } 15 | ) 16 | 17 | 18 | hive.alert.update( 19 | alert_id=original_alert["_id"], 20 | fields={ 21 | "title": "updated alert", 22 | "tags": ["update-single"], 23 | }, 24 | ) 25 | 26 | updated_alert = hive.alert.get(alert_id=original_alert["_id"]) 27 | -------------------------------------------------------------------------------- /examples/case/advanced.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | from thehive4py.types.case import InputCase 3 | 4 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 5 | 6 | input_case: InputCase = { 7 | "title": "an advanced case", 8 | "description": "a bit more advanced case...", 9 | "caseTemplate": "my-template", 10 | "severity": 1, 11 | "status": "New", 12 | "tags": ["advanced", "example"], 13 | } 14 | 15 | output_case = hive.case.create(case=input_case) 16 | -------------------------------------------------------------------------------- /examples/case/delete.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 4 | 5 | case_to_delete = hive.case.create( 6 | case={ 7 | "title": "delete case", 8 | "description": "an case to delete", 9 | } 10 | ) 11 | 12 | 13 | hive.case.delete(case_id=case_to_delete["_id"]) 14 | -------------------------------------------------------------------------------- /examples/case/fetch_with_find.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | from thehive4py.query.filters import Eq 3 | 4 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 5 | 6 | antivirus_case = hive.case.create( 7 | case={ 8 | "title": "case #1 to find", 9 | "description": "a case to find with others", 10 | "tags": ["antivirus"], 11 | } 12 | ) 13 | 14 | phishing_case = hive.case.create( 15 | case={ 16 | "title": "case #2 to find", 17 | "description": "a case to find with others", 18 | "tags": ["phishing"], 19 | } 20 | ) 21 | 22 | 23 | filters = Eq(field="tags", value="antivirus") | Eq(field="tags", value="phishing") 24 | fetched_cases = hive.case.find(filters=filters) 25 | -------------------------------------------------------------------------------- /examples/case/fetch_with_get.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 4 | 5 | case_to_get = hive.case.create( 6 | case={ 7 | "title": "case to get", 8 | "description": "a single case to fetch", 9 | } 10 | ) 11 | 12 | 13 | fetched_case = hive.case.get(case_id=case_to_get["_id"]) 14 | -------------------------------------------------------------------------------- /examples/case/merge.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 4 | 5 | case_ids_to_merge: list[str] = [] 6 | for i in range(2): 7 | case_to_merge = hive.case.create( 8 | case={ 9 | "title": f"original case #{i}", 10 | "description": "a case to merge with another", 11 | } 12 | ) 13 | case_ids_to_merge.append(case_to_merge["_id"]) 14 | 15 | 16 | merge_case = hive.case.merge(case_ids=case_ids_to_merge) 17 | -------------------------------------------------------------------------------- /examples/case/minimalistic.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 4 | 5 | minimalistic_case = hive.case.create( 6 | case={ 7 | "title": "a minimalistic case", 8 | "description": "a bit too minimal", 9 | } 10 | ) 11 | -------------------------------------------------------------------------------- /examples/case/obervable_simple.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | from thehive4py.types.observable import InputObservable 3 | 4 | hive = TheHiveApi(url="thehive.example", apikey="h1v3b33") 5 | 6 | 7 | case_to_enrich = hive.case.create( 8 | case={ 9 | "title": "case to enrich", 10 | "description": "a case to enrich with simple observables", 11 | } 12 | ) 13 | 14 | 15 | observables: list[InputObservable] = [ 16 | {"dataType": "ip", "data": "1.2.3.4"}, 17 | {"dataType": "domain", "data": "example.com"}, 18 | ] 19 | 20 | 21 | for observable in observables: 22 | hive.case.create_observable(case_id=case_to_enrich["_id"], observable=observable) 23 | -------------------------------------------------------------------------------- /examples/case/observable_file.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import tempfile 3 | 4 | from thehive4py import TheHiveApi 5 | 6 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 7 | 8 | case_to_enrich = hive.case.create( 9 | case={ 10 | "title": "case to enrich", 11 | "description": "a case to enrich with a file observable", 12 | } 13 | ) 14 | 15 | with tempfile.TemporaryDirectory() as tmpdir: 16 | 17 | observable_filepath = os.path.join(tmpdir, "my-observable.txt") 18 | with open(observable_filepath) as observable_file: 19 | observable_file.write("some observable content") 20 | 21 | hive.case.create_observable( 22 | case_id=case_to_enrich["_id"], 23 | observable={"dataType": "file", "message": "a file based observable"}, 24 | observable_path=observable_filepath, 25 | ) 26 | -------------------------------------------------------------------------------- /examples/case/pages_after_creation.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | from thehive4py.types.page import InputCasePage 3 | 4 | hive = TheHiveApi(url="thehive.example", apikey="h1v3b33") 5 | 6 | 7 | case_to_enrich = hive.case.create( 8 | case={ 9 | "title": "case to enrich", 10 | "description": "a case to enrich with pages", 11 | } 12 | ) 13 | 14 | case_pages: list[InputCasePage] = [ 15 | { 16 | "title": "Playbooks", 17 | "category": "general", 18 | "content": "Some playbook content", 19 | }, 20 | { 21 | "title": "Resources", 22 | "category": "general", 23 | "content": "Some useful resources", 24 | }, 25 | ] 26 | 27 | 28 | for case_page in case_pages: 29 | hive.case.create_page(case_id=case_to_enrich["_id"], page=case_page) 30 | -------------------------------------------------------------------------------- /examples/case/pages_during_creation.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | from thehive4py.types.page import InputCasePage 3 | 4 | hive = TheHiveApi(url="thehive.example", apikey="h1v3b33") 5 | 6 | 7 | case_pages: list[InputCasePage] = [ 8 | { 9 | "title": "Notes", 10 | "category": "default", 11 | "content": "Some notes to take during case triage.", 12 | }, 13 | { 14 | "title": "Summary", 15 | "category": "default", 16 | "content": "Some summary to wrap up the case triage.", 17 | }, 18 | ] 19 | 20 | case_with_tasks = hive.case.create( 21 | case={ 22 | "title": "case with tasks", 23 | "description": "a case enriched with tasks", 24 | "pages": case_pages, 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /examples/case/procedures_after_creation.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | from thehive4py.helpers import now_to_ts 3 | from thehive4py.types.procedure import InputProcedure 4 | 5 | hive = TheHiveApi(url="thehive.example", apikey="h1v3b33") 6 | 7 | case_to_enrich = hive.case.create( 8 | case={ 9 | "title": "case to enrich", 10 | "description": "a case to enrich with procedures", 11 | } 12 | ) 13 | 14 | case_procedures: list[InputProcedure] = [ 15 | { 16 | "occurDate": now_to_ts(), 17 | "patternId": "T1566", 18 | "tactic": "initial-access", 19 | "description": "Phishing", 20 | }, 21 | { 22 | "occurDate": now_to_ts(), 23 | "patternId": "T1566.001", 24 | "tactic": "initial-access", 25 | "description": "Spearphishing Attachment", 26 | }, 27 | ] 28 | 29 | 30 | for procedure in case_procedures: 31 | hive.case.create_procedure(case_id=case_to_enrich["_id"], procedure=procedure) 32 | -------------------------------------------------------------------------------- /examples/case/tasks_after_creation.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | from thehive4py.types.task import InputTask 3 | 4 | hive = TheHiveApi(url="thehive.example", apikey="h1v3b33") 5 | 6 | 7 | case_to_enrich = hive.case.create( 8 | case={ 9 | "title": "case to enrich", 10 | "description": "a case to enrich with tasks", 11 | } 12 | ) 13 | 14 | case_tasks: list[InputTask] = [ 15 | {"title": "Summarize", "description": "Summarize the investigation"}, 16 | {"title": "Report", "description": "Create a report for the CISO"}, 17 | ] 18 | 19 | 20 | for case_task in case_tasks: 21 | hive.case.create_task(case_id=case_to_enrich["_id"], task=case_task) 22 | -------------------------------------------------------------------------------- /examples/case/tasks_during_creation.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | from thehive4py.types.task import InputTask 3 | 4 | hive = TheHiveApi(url="thehive.example", apikey="h1v3b33") 5 | 6 | 7 | case_tasks: list[InputTask] = [ 8 | {"title": "Triage", "description": "Conduct the initial investigation"}, 9 | {"title": "Respond", "description": "Execute the required actions"}, 10 | ] 11 | 12 | case_with_tasks = hive.case.create( 13 | case={ 14 | "title": "case with tasks", 15 | "description": "a case enriched with tasks", 16 | "tasks": case_tasks, 17 | } 18 | ) 19 | -------------------------------------------------------------------------------- /examples/case/update_bulk.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 4 | 5 | original_case_ids = [] 6 | for i in range(2): 7 | original_case = hive.case.create( 8 | case={ 9 | "title": f"original case #{i}", 10 | "description": "a case to update in bulk", 11 | } 12 | ) 13 | 14 | original_case_ids.append(original_case["_id"]) 15 | 16 | 17 | hive.case.bulk_update( 18 | fields={ 19 | "ids": original_case_ids, 20 | "title": "bulk updated case", 21 | "tags": ["update-bulk"], 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /examples/case/update_single.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33") 4 | 5 | original_case = hive.case.create( 6 | case={ 7 | "title": "original case", 8 | "description": "a single case to update", 9 | } 10 | ) 11 | 12 | 13 | hive.case.update( 14 | case_id=original_case["_id"], 15 | fields={ 16 | "title": "updated case", 17 | "tags": ["update-single"], 18 | }, 19 | ) 20 | 21 | updated_case = hive.case.get(case_id=original_case["_id"]) 22 | -------------------------------------------------------------------------------- /examples/client/auth_with_apikey.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | hive = TheHiveApi( 4 | url="http://localhost:9000", 5 | apikey="h1v3b33", 6 | ) 7 | -------------------------------------------------------------------------------- /examples/client/auth_with_username_and_password.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | hive = TheHiveApi( 4 | url="http://localhost:9000", 5 | username="analyst@thehive", 6 | password="h1v3b33", 7 | ) 8 | -------------------------------------------------------------------------------- /examples/client/org_during_runtime.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | hive = TheHiveApi( 4 | url="http://localhost:9000", 5 | apikey="h1v3b33", 6 | ) 7 | 8 | hive.session_organisation = "other-org" 9 | -------------------------------------------------------------------------------- /examples/client/org_via_constructor.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | client_with_organisation = TheHiveApi( 4 | url="http://localhost:9000", 5 | apikey="h1v3b33", 6 | organisation="admin", 7 | ) 8 | -------------------------------------------------------------------------------- /examples/client/retries.py: -------------------------------------------------------------------------------- 1 | from urllib3 import Retry 2 | 3 | from thehive4py import TheHiveApi 4 | 5 | simple_retries = Retry( 6 | total=5, 7 | backoff_factor=0.5, 8 | allowed_methods=["GET"], 9 | status_forcelist=[500], 10 | ) 11 | 12 | hive = TheHiveApi( 13 | url="http://localhost:9000", 14 | apikey="h1v3b33", 15 | max_retries=simple_retries, 16 | ) 17 | -------------------------------------------------------------------------------- /examples/client/ssl.py: -------------------------------------------------------------------------------- 1 | from thehive4py import TheHiveApi 2 | 3 | hive = TheHiveApi( 4 | url="http://localhost:9000", 5 | apikey="h1v3b33", 6 | verify="/etc/ssl/certs/ca-certificates.crt", 7 | ) 8 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: thehive4py 2 | repo_name: TheHive-Project/TheHive4py 3 | repo_url: https://github.com/TheHive-Project/TheHive4py 4 | nav: 5 | - TheHive4py: index.md 6 | - Examples: 7 | - Client: examples/client.md 8 | - Alert: examples/alert.md 9 | - Case: examples/case.md 10 | - Reference: reference.md 11 | - Release Notes: release-notes.md 12 | theme: 13 | name: material 14 | favicon: img/strangebee.png 15 | logo: img/strangebee.png 16 | features: 17 | - content.code.copy 18 | - navigation.footer 19 | - navigation.tabs 20 | - navigation.tabs.sticky 21 | - toc.follow 22 | palette: 23 | - media: "(prefers-color-scheme)" 24 | toggle: 25 | icon: material/lightbulb-auto 26 | name: Switch to light mode 27 | - media: '(prefers-color-scheme: light)' 28 | scheme: default 29 | primary: custom 30 | accent: custom 31 | toggle: 32 | icon: material/lightbulb 33 | name: Switch to dark mode 34 | - media: '(prefers-color-scheme: dark)' 35 | scheme: slate 36 | primary: custom 37 | accent: custom 38 | toggle: 39 | icon: material/lightbulb-outline 40 | name: Switch to system preference 41 | extra_css: 42 | - styles/extra.css 43 | markdown_extensions: 44 | - admonition 45 | - attr_list 46 | - md_in_html 47 | - pymdownx.emoji: 48 | emoji_index: !!python/name:material.extensions.emoji.twemoji 49 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 50 | - pymdownx.snippets 51 | - pymdownx.superfences 52 | extra: 53 | social: 54 | - icon: simple/discord 55 | link: https://discord.com/invite/XhxG3vzM44 56 | name: Discord 57 | - icon: simple/github 58 | link: https://github.com/TheHive-Project/TheHive4py 59 | name: GitHub 60 | - icon: simple/python 61 | link: https://pypi.org/project/thehive4py/ 62 | name: PyPI 63 | version: 64 | provider: mike 65 | plugins: 66 | - autorefs 67 | - mkdocstrings: 68 | handlers: 69 | python: 70 | import: 71 | - https://docs.python.org/3/objects.inv 72 | - https://requests.readthedocs.io/en/stable/objects.inv 73 | options: 74 | show_root_heading: true 75 | show_root_full_path: true 76 | merge_init_into_class: true 77 | show_source: true 78 | show_if_no_docstring: true 79 | members_order: source 80 | docstring_section_style: table 81 | signature_crossrefs: true 82 | show_symbol_type_heading: true 83 | show_symbol_type_toc: true 84 | watch: 85 | - examples 86 | - thehive4py -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "thehive4py" 7 | description = "Python client for TheHive5" 8 | version = "2.0.0b11" 9 | requires-python = ">=3.9" 10 | dependencies = ["requests~=2.27"] 11 | readme = "README.md" 12 | keywords = ["thehive5", "api", "client"] 13 | license = { text = "MIT" } 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: Developers", 17 | "Intended Audience :: Information Technology", 18 | "Natural Language :: English", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3.13", 24 | "License :: OSI Approved :: MIT License", 25 | ] 26 | authors = [{ name = "Szabolcs Antal", email = "antalszabolcs01@gmail.com" }] 27 | 28 | [project.optional-dependencies] 29 | audit = ["bandit", "pip-audit"] 30 | build = ["build", "twine"] 31 | docs = ["mkdocs", "mkdocs-material", "mkdocstrings-python", "mike"] 32 | lint = ["black", "flake8-pyproject", "mypy", "pre-commit"] 33 | test = ["pytest", "pytest-cov"] 34 | dev = ["thehive4py[audit, lint, test, build, docs]"] 35 | 36 | [tool.setuptools.packages.find] 37 | include = ["thehive4py*"] 38 | 39 | [tool.setuptools.package-data] 40 | thehive4py = ["py.typed"] 41 | 42 | [tool.coverage.run] 43 | omit = ["tests/*", "thehive4py/types/*"] 44 | 45 | [tool.flake8] 46 | max-line-length = 88 47 | per-file-ignores = [ 48 | "thehive4py/__init__.py:F401", 49 | "thehive4py/endpoints/__init__.py:F401", 50 | "thehive4py/query/__init__.py:F401", 51 | ] 52 | -------------------------------------------------------------------------------- /scripts/cd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import subprocess 4 | from typing import Dict 5 | 6 | 7 | def _run_subprocess( 8 | command: str, 9 | quiet=False, 10 | ): 11 | if not quiet: 12 | stdout = stderr = None 13 | else: 14 | stdout = stderr = subprocess.DEVNULL 15 | 16 | try: 17 | subprocess.run(str.split(command), stdout=stdout, stderr=stderr, check=True) 18 | except subprocess.CalledProcessError as err: 19 | error_output = ( 20 | f"ERROR: Execution of command '{command}' returned: {err.returncode}\n" 21 | ) 22 | print(error_output) 23 | exit(err.returncode) 24 | 25 | 26 | def run_build(quiet: bool): 27 | print("Building thehive4py with the build module...") 28 | _run_subprocess( 29 | command="rm -rf build/ dist/", 30 | quiet=quiet, 31 | ) 32 | _run_subprocess( 33 | command="python -m build --sdist --wheel", 34 | quiet=quiet, 35 | ) 36 | print("Successfully built thehive4py!") 37 | 38 | 39 | def run_upload(quiet: bool): 40 | print("Publishing thehive4py with twine...") 41 | _run_subprocess( 42 | command="twine upload dist/*", 43 | quiet=quiet, 44 | ) 45 | print("Successfully published thehive4py!") 46 | 47 | 48 | def run_build_docs(quiet: bool): 49 | print("Building thehive4py docs...") 50 | _run_subprocess( 51 | command="mkdocs build --clean --strict", 52 | quiet=quiet, 53 | ) 54 | print("Successfully built thehive4py docs!") 55 | 56 | 57 | def run_deploy_docs(quiet: bool): 58 | print("Deploying thehive4py docs to gh-pages...") 59 | _run_subprocess( 60 | command="mike deploy main latest -u -p --allow-empty", 61 | quiet=quiet, 62 | ) 63 | print("Successfully deployed thehive4py docs to gh-pages!") 64 | 65 | 66 | def build_run_options() -> Dict[str, dict]: 67 | return { 68 | "build": {"help": "build the package locally", "func": run_build}, 69 | "upload": {"help": "upload the package to pypi", "func": run_upload}, 70 | "build-docs": {"help": "build the docs locally", "func": run_build_docs}, 71 | "deploy-docs": {"help": "deploy the docs to gh-pages", "func": run_deploy_docs}, 72 | } 73 | 74 | 75 | def parse_arguments(run_options: Dict[str, dict]): 76 | parser = argparse.ArgumentParser( 77 | prog="thehive4py-cd", 78 | description="run all cd steps or use options to run cd steps selectively", 79 | ) 80 | parser.add_argument( 81 | "-q", 82 | "--quiet", 83 | action="store_true", 84 | default=False, 85 | help="silence verbose output", 86 | ) 87 | 88 | for run_option_name, run_option_attributes in run_options.items(): 89 | parser.add_argument( 90 | f"--{run_option_name.replace('_', '-')}", 91 | help=run_option_attributes["help"], 92 | action="store_true", 93 | ) 94 | 95 | args = parser.parse_args() 96 | 97 | if not any( 98 | getattr(args, run_option.replace("-", "_")) for run_option in run_options 99 | ): 100 | parser.error(f"provide at least one option from: {list(run_options)}") 101 | 102 | return args 103 | 104 | 105 | def main(): 106 | run_options = build_run_options() 107 | args = parse_arguments(run_options=run_options) 108 | 109 | quiet = args.quiet 110 | 111 | selected_run_funcs = [ 112 | run_option_attributes["func"] 113 | for run_option_name, run_option_attributes in run_options.items() 114 | if getattr(args, run_option_name.replace("-", "_")) 115 | ] 116 | 117 | for run_func in selected_run_funcs: 118 | run_func(quiet=quiet) 119 | print() 120 | 121 | print("Done!") 122 | 123 | 124 | if __name__ == "__main__": 125 | main() 126 | -------------------------------------------------------------------------------- /scripts/ci.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import subprocess 4 | from typing import List 5 | 6 | 7 | def _run_subprocess( 8 | command: str, 9 | quiet=False, 10 | ): 11 | if not quiet: 12 | stdout = stderr = None 13 | else: 14 | stdout = stderr = subprocess.DEVNULL 15 | 16 | try: 17 | subprocess.run(str.split(command), stdout=stdout, stderr=stderr, check=True) 18 | except subprocess.CalledProcessError as err: 19 | error_output = ( 20 | f"ERROR: Execution of command '{command}' returned: {err.returncode}\n" 21 | ) 22 | print(error_output) 23 | exit(err.returncode) 24 | 25 | 26 | def check_all(quiet=False): 27 | print("Run all checks...") 28 | check_lint(quiet=quiet) 29 | check_format(quiet=quiet) 30 | check_type(quiet=quiet) 31 | check_cve(quiet=quiet) 32 | check_security(quiet=quiet) 33 | print("All checks succeeded!") 34 | 35 | 36 | def check_lint(quiet=False): 37 | print("Run lint checks with flake8...") 38 | _run_subprocess( 39 | command="flake8 thehive4py/ tests/", 40 | quiet=quiet, 41 | ) 42 | print("Lint checks succeeded!") 43 | 44 | 45 | def check_format(quiet=False): 46 | print("Run format checks with black...") 47 | _run_subprocess( 48 | command="black --check thehive4py/ tests/", 49 | quiet=quiet, 50 | ) 51 | print("Format checks succeeded!") 52 | 53 | 54 | def check_type(quiet=False): 55 | print("Run type checks with mypy...") 56 | _run_subprocess( 57 | command="mypy --install-types --non-interactive thehive4py/", 58 | quiet=quiet, 59 | ) 60 | print("Type checks succeeded!") 61 | 62 | 63 | def check_cve(quiet=False): 64 | print("Run CVE checks with pip-audit...") 65 | _run_subprocess( 66 | command="pip-audit .", 67 | quiet=quiet, 68 | ) 69 | print("CVE checks succeeded!") 70 | 71 | 72 | def check_security(quiet=False): 73 | print("Run security checks with bandit...") 74 | _run_subprocess( 75 | command="bandit -r thehive4py/", 76 | quiet=quiet, 77 | ) 78 | print("Security checks succeeded!") 79 | 80 | 81 | def check_test(quiet=False): 82 | print("Run integration tests with pytest...") 83 | _run_subprocess( 84 | command="pytest -v --cov", 85 | quiet=quiet, 86 | ) 87 | print("Integration tests succeeded!") 88 | 89 | 90 | def build_check_options() -> List[dict]: 91 | return [ 92 | {"name": "lint", "help": "run lint checks", "check": check_lint}, 93 | {"name": "format", "help": "run format checks", "check": check_format}, 94 | {"name": "type", "help": "run type checks", "check": check_type}, 95 | {"name": "cve", "help": "run cve checks", "check": check_cve}, 96 | {"name": "security", "help": "run security checks", "check": check_security}, 97 | {"name": "test", "help": "run integration tests", "check": check_test}, 98 | ] 99 | 100 | 101 | def parse_arguments(check_options: List[dict]): 102 | parser = argparse.ArgumentParser( 103 | prog="thehive4py-ci", 104 | description=( 105 | "run all ci checks except tests by default, " 106 | "use options to run ci checks selectively" 107 | ), 108 | ) 109 | parser.add_argument( 110 | "-q", 111 | "--quiet", 112 | action="store_true", 113 | default=False, 114 | help="silence verbose output", 115 | ) 116 | for check_option in check_options: 117 | parser.add_argument( 118 | f"--{check_option['name']}", 119 | help=check_option["help"], 120 | action="store_true", 121 | ) 122 | 123 | return parser.parse_args() 124 | 125 | 126 | def main(): 127 | check_options = build_check_options() 128 | args = parse_arguments(check_options=check_options) 129 | 130 | quiet = args.quiet 131 | 132 | selective_checks = [ 133 | check_option["check"] 134 | for check_option in check_options 135 | if getattr(args, check_option["name"]) 136 | ] 137 | 138 | if selective_checks: 139 | for check in selective_checks: 140 | check(quiet=quiet) 141 | else: 142 | check_all(quiet=quiet) 143 | 144 | 145 | if __name__ == "__main__": 146 | main() 147 | -------------------------------------------------------------------------------- /scripts/linkify_release_notes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import re 4 | 5 | 6 | def replace_issue_reference(match: re.Match) -> str: 7 | issue_number = match.groups()[0] 8 | issue_link = ( 9 | f" [#{issue_number}]" 10 | f"(https://github.com/TheHive-Project/TheHive4py/issues/{issue_number})" 11 | ) 12 | print(f"Replace `{match.group()}` by `{issue_link}`") 13 | return issue_link 14 | 15 | 16 | def linkify_issue_references(release_notes: str) -> str: 17 | issue_ref_pattern = r" #(\d+)" 18 | linkified_release_notes = re.sub( 19 | pattern=issue_ref_pattern, 20 | repl=replace_issue_reference, 21 | string=release_notes, 22 | ) 23 | return linkified_release_notes 24 | 25 | 26 | def replace_pr_url(match: re.Match) -> str: 27 | pr_url, pr_number = match.groups() 28 | pr_link = f" [#{pr_number}]({pr_url})" 29 | print(f"Replace `{match.group()}` by `{pr_link}`") 30 | return pr_link 31 | 32 | 33 | def linkify_pr_urls(release_notes: str) -> str: 34 | pr_url_pattern = ( 35 | r" (https:\/\/github\.com\/TheHive-Project\/TheHive4py\/pull\/(\d+))" 36 | ) 37 | 38 | linkified_release_notes = re.sub( 39 | pattern=pr_url_pattern, 40 | repl=replace_pr_url, 41 | string=release_notes, 42 | ) 43 | return linkified_release_notes 44 | 45 | 46 | def replace_contributor_reference(match: re.Match) -> str: 47 | contributor = match.groups()[0] 48 | contributor_link = f" [@{contributor}](https://github.com/{contributor})" 49 | print(f"Replace `{match.group()}` by `{contributor_link}`") 50 | return contributor_link 51 | 52 | 53 | def linkify_contributor_references(release_notes: str) -> str: 54 | contributor_ref_pattern = r" @(\w+)" 55 | 56 | linkified_release_notes = re.sub( 57 | pattern=contributor_ref_pattern, 58 | repl=replace_contributor_reference, 59 | string=release_notes, 60 | ) 61 | return linkified_release_notes 62 | 63 | 64 | def replace_full_changelog_url(match: re.Match) -> str: 65 | changelog_url, changelog_tags = match.groups() 66 | full_changelog_link = f" [{changelog_tags}]({changelog_url})" 67 | print(f"Replace `{match.group()}` by `{full_changelog_link}`") 68 | return full_changelog_link 69 | 70 | 71 | def linkify_full_changelog_urls(release_notes: str) -> str: 72 | full_changelog_url_pattern = ( 73 | r" (https:\/\/github\.com/TheHive-Project\/TheHive4py\/compare\/(.+\.\.\..+))" 74 | ) 75 | 76 | linkified_release_notes = re.sub( 77 | pattern=full_changelog_url_pattern, 78 | repl=replace_full_changelog_url, 79 | string=release_notes, 80 | ) 81 | return linkified_release_notes 82 | 83 | 84 | def parse_arguments(): 85 | parser = argparse.ArgumentParser( 86 | prog="linkify-release-notes", 87 | description=("enhance release notes with markdown links"), 88 | ) 89 | parser.add_argument( 90 | "-c", 91 | "--check", 92 | action="store_true", 93 | default=False, 94 | help="just check for link replacements", 95 | ) 96 | 97 | return parser.parse_args() 98 | 99 | 100 | def main(): 101 | 102 | args = parse_arguments() 103 | 104 | release_notes_path = "docs/release-notes.md" 105 | with open(release_notes_path) as release_notes_fp: 106 | release_notes = release_notes_fp.read() 107 | 108 | print(f"Checking linkification in '{release_notes_path}'") 109 | linkified_release_notes = release_notes 110 | linkified_release_notes = linkify_issue_references( 111 | release_notes=linkified_release_notes 112 | ) 113 | linkified_release_notes = linkify_pr_urls(release_notes=linkified_release_notes) 114 | linkified_release_notes = linkify_contributor_references( 115 | release_notes=linkified_release_notes 116 | ) 117 | linkified_release_notes = linkify_full_changelog_urls( 118 | release_notes=linkified_release_notes 119 | ) 120 | 121 | if linkified_release_notes == release_notes: 122 | print("Nothing to do, release notes are already linkified!") 123 | return 124 | 125 | if args.check: 126 | print("The `--check` flag is active, exiting without replacing!") 127 | exit(1) 128 | 129 | with open(release_notes_path, "w") as linkified_release_notes_fp: 130 | linkified_release_notes_fp.write(linkified_release_notes) 131 | 132 | print(f"Successfully linkified '{release_notes_path}'!") 133 | 134 | 135 | if __name__ == "__main__": 136 | main() 137 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/TheHive4py/62a3f9714f9c27eb55017fab79ccd00ea8f593f0/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | from tests.utils import TestConfig, reset_hive_instance, spawn_hive_container 6 | from thehive4py.client import TheHiveApi 7 | from thehive4py.helpers import now_to_ts 8 | from thehive4py.types.alert import InputAlert, OutputAlert 9 | from thehive4py.types.case import InputCase, OutputCase 10 | from thehive4py.types.case_template import InputCaseTemplate, OutputCaseTemplate 11 | from thehive4py.types.comment import OutputComment 12 | from thehive4py.types.custom_field import OutputCustomField 13 | from thehive4py.types.observable import InputObservable, OutputObservable 14 | from thehive4py.types.observable_type import OutputObservableType 15 | from thehive4py.types.page import OutputCasePage 16 | from thehive4py.types.procedure import OutputProcedure 17 | from thehive4py.types.profile import OutputProfile 18 | from thehive4py.types.task import InputTask, OutputTask 19 | from thehive4py.types.task_log import InputTaskLog, OutputTaskLog 20 | from thehive4py.types.timeline import OutputCustomEvent 21 | from thehive4py.types.user import OutputUser 22 | 23 | 24 | @pytest.fixture(scope="session") 25 | def test_config(): 26 | return TestConfig( 27 | image_name="strangebee/thehive:5.5.2", 28 | container_name="thehive4py-integration-tester", 29 | user="admin@thehive.local", 30 | password="secret", 31 | admin_org="admin", 32 | main_org="main-org", 33 | share_org="share-org", 34 | ) 35 | 36 | 37 | @pytest.fixture(scope="function", autouse=True) 38 | def auto_reset_hive_instance(thehive: TheHiveApi, test_config: TestConfig): 39 | reset_hive_instance(hive_url=thehive.session.hive_url, test_config=test_config) 40 | 41 | 42 | @pytest.fixture(scope="session") 43 | def thehive(test_config: TestConfig): 44 | hive_url = spawn_hive_container(test_config=test_config) 45 | client = TheHiveApi( 46 | url=hive_url, 47 | username=test_config.user, 48 | password=test_config.password, 49 | organisation=test_config.main_org, 50 | ) 51 | return client 52 | 53 | 54 | @pytest.fixture 55 | def thehive_admin(test_config: TestConfig, thehive: TheHiveApi): 56 | default_organisation = thehive.session_organisation 57 | thehive.session_organisation = test_config.admin_org 58 | yield thehive 59 | thehive.session_organisation = default_organisation 60 | 61 | 62 | @pytest.fixture 63 | def test_alert(thehive: TheHiveApi) -> OutputAlert: 64 | return thehive.alert.create( 65 | alert={ 66 | "title": "my first alert", 67 | "description": "...", 68 | "type": "test", 69 | "source": "test", 70 | "sourceRef": "first", 71 | "externalLink": "http://", 72 | "date": now_to_ts(), 73 | "tags": ["whatever"], 74 | } 75 | ) 76 | 77 | 78 | @pytest.fixture 79 | def test_alerts(thehive: TheHiveApi) -> List[OutputAlert]: 80 | alerts: List[InputAlert] = [ 81 | { 82 | "title": "my first alert", 83 | "description": "...", 84 | "type": "test", 85 | "source": "test", 86 | "sourceRef": "first", 87 | "date": now_to_ts(), 88 | }, 89 | { 90 | "title": "my second alert", 91 | "description": "...", 92 | "type": "test", 93 | "source": "test", 94 | "sourceRef": "second", 95 | "date": now_to_ts(), 96 | }, 97 | ] 98 | return [thehive.alert.create(alert=alert) for alert in alerts] 99 | 100 | 101 | @pytest.fixture 102 | def test_case(thehive: TheHiveApi) -> OutputCase: 103 | return thehive.case.create( 104 | case={"title": "my first case", "description": "...", "tags": ["whatever"]} 105 | ) 106 | 107 | 108 | @pytest.fixture 109 | def test_cases(thehive: TheHiveApi) -> List[OutputCase]: 110 | cases: List[InputCase] = [ 111 | {"title": "my first case", "description": "...", "tags": ["whatever"]}, 112 | {"title": "my second case", "description": "...", "tags": ["whatever"]}, 113 | ] 114 | return [thehive.case.create(case=case) for case in cases] 115 | 116 | 117 | @pytest.fixture 118 | def test_case_template(thehive: TheHiveApi) -> OutputCaseTemplate: 119 | name = "my first case template" 120 | return thehive.case_template.create( 121 | case_template={ 122 | "name": name, 123 | "description": "...", 124 | "tags": ["template-tag"], 125 | } 126 | ) 127 | 128 | 129 | @pytest.fixture 130 | def test_case_templates(thehive: TheHiveApi) -> List[OutputCaseTemplate]: 131 | case_templates: List[InputCaseTemplate] = [ 132 | { 133 | "name": "my first case template", 134 | "description": "...", 135 | "tags": ["template-tag-1"], 136 | }, 137 | { 138 | "name": "my second case template", 139 | "description": "...", 140 | "tags": ["template-tag-2"], 141 | }, 142 | ] 143 | return [ 144 | thehive.case_template.create(case_template=case_template) 145 | for case_template in case_templates 146 | ] 147 | 148 | 149 | @pytest.fixture 150 | def test_observable(thehive: TheHiveApi, test_case: OutputCase) -> OutputObservable: 151 | return thehive.observable.create_in_case( 152 | case_id=test_case["_id"], 153 | observable={ 154 | "dataType": "domain", 155 | "data": "example.com", 156 | "message": "test observable", 157 | "tlp": 1, 158 | "pap": 1, 159 | "tags": ["test-tag"], 160 | }, 161 | )[0] 162 | 163 | 164 | @pytest.fixture 165 | def test_observables( 166 | thehive: TheHiveApi, test_case: OutputCase 167 | ) -> List[OutputObservable]: 168 | observables: List[InputObservable] = [ 169 | { 170 | "dataType": "domain", 171 | "data": "example.com", 172 | "message": "test observable", 173 | "tlp": 1, 174 | "pap": 1, 175 | "tags": ["test-tag"], 176 | }, 177 | { 178 | "dataType": "ip", 179 | "data": "127.0.0.1", 180 | "message": "test observable", 181 | "tlp": 2, 182 | "pap": 2, 183 | "tags": ["test-tag"], 184 | }, 185 | ] 186 | return [ 187 | thehive.observable.create_in_case( 188 | case_id=test_case["_id"], observable=observable 189 | )[0] 190 | for observable in observables 191 | ] 192 | 193 | 194 | @pytest.fixture 195 | def test_task(thehive: TheHiveApi, test_case: OutputCase) -> OutputTask: 196 | return thehive.task.create( 197 | case_id=test_case["_id"], 198 | task={"title": "my first task"}, 199 | ) 200 | 201 | 202 | @pytest.fixture 203 | def test_tasks(thehive: TheHiveApi, test_case: OutputCase) -> List[OutputTask]: 204 | tasks: List[InputTask] = [ 205 | {"title": "my first task"}, 206 | {"title": "my second task"}, 207 | ] 208 | return [thehive.task.create(case_id=test_case["_id"], task=task) for task in tasks] 209 | 210 | 211 | @pytest.fixture 212 | def test_task_log(thehive: TheHiveApi, test_task: OutputTask) -> OutputTaskLog: 213 | return thehive.task_log.create( 214 | task_id=test_task["_id"], 215 | task_log={"message": "test log"}, 216 | ) 217 | 218 | 219 | @pytest.fixture 220 | def test_task_logs(thehive: TheHiveApi, test_task: OutputTask) -> List[OutputTaskLog]: 221 | task_logs: List[InputTaskLog] = [ 222 | {"message": "my first log"}, 223 | {"message": "my second log"}, 224 | ] 225 | return [ 226 | thehive.task_log.create(task_id=test_task["_id"], task_log=task_log) 227 | for task_log in task_logs 228 | ] 229 | 230 | 231 | @pytest.fixture 232 | def test_comment(thehive: TheHiveApi, test_case: OutputCase) -> OutputComment: 233 | return thehive.comment.create_in_case( 234 | case_id=test_case["_id"], 235 | comment={"message": "my first comment"}, 236 | ) 237 | 238 | 239 | @pytest.fixture 240 | def test_procedure(thehive: TheHiveApi, test_case: OutputCase) -> OutputProcedure: 241 | return thehive.procedure.create_in_case( 242 | case_id=test_case["_id"], 243 | procedure={ 244 | "occurDate": now_to_ts(), 245 | "patternId": "T1059.006", 246 | "tactic": "execution", 247 | "description": "...", 248 | }, 249 | ) 250 | 251 | 252 | @pytest.fixture 253 | def test_case_page(thehive: TheHiveApi, test_case: OutputCase) -> OutputCasePage: 254 | return thehive.case.create_page( 255 | case_id=test_case["_id"], 256 | page={ 257 | "title": "my case page", 258 | "category": "testing", 259 | "content": "...", 260 | }, 261 | ) 262 | 263 | 264 | @pytest.fixture 265 | def test_timeline_event( 266 | thehive: TheHiveApi, test_case: OutputCase 267 | ) -> OutputCustomEvent: 268 | return thehive.timeline.create_event( 269 | case_id=test_case["_id"], 270 | event={ 271 | "date": now_to_ts(), 272 | "endDate": now_to_ts(), 273 | "title": "test timeline event", 274 | "description": "...", 275 | }, 276 | ) 277 | 278 | 279 | @pytest.fixture 280 | def test_user(test_config: TestConfig, thehive: TheHiveApi) -> OutputUser: 281 | return thehive.user.create( 282 | user={ 283 | "email": "user@example.com", 284 | "name": "test user", 285 | "login": "user@example.com", 286 | "profile": "analyst", 287 | "organisation": test_config.main_org, 288 | } 289 | ) 290 | 291 | 292 | @pytest.fixture 293 | def test_profile(thehive_admin: TheHiveApi) -> OutputProfile: 294 | return thehive_admin.profile.create( 295 | profile={"name": "my-read-only", "permissions": []} 296 | ) 297 | 298 | 299 | @pytest.fixture 300 | def test_custom_field(thehive_admin: TheHiveApi) -> OutputCustomField: 301 | return thehive_admin.custom_field.create( 302 | custom_field={ 303 | "name": "test-field", 304 | "type": "string", 305 | "displayName": "Test Field", 306 | "description": "...", 307 | "group": "default", 308 | "options": [], 309 | } 310 | ) 311 | 312 | 313 | @pytest.fixture 314 | def test_observable_type(thehive_admin: TheHiveApi) -> OutputObservableType: 315 | return thehive_admin.observable_type.create( 316 | observable_type={"name": "my-observable-type"} 317 | ) 318 | -------------------------------------------------------------------------------- /tests/test_case_template_endpoint.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | from thehive4py.client import TheHiveApi 5 | from thehive4py.errors import TheHiveError 6 | from thehive4py.types.case_template import InputCaseTemplate, OutputCaseTemplate 7 | 8 | 9 | class TestCaseTemplateEndpoint: 10 | def test_create_and_get(self, thehive: TheHiveApi): 11 | created_case_template = thehive.case_template.create( 12 | case_template={ 13 | "name": "my first template", 14 | "description": "Template description", 15 | } 16 | ) 17 | fetched_case_template = thehive.case_template.get(created_case_template["_id"]) 18 | assert created_case_template == fetched_case_template 19 | 20 | def test_update(self, thehive: TheHiveApi, test_case_template: OutputCaseTemplate): 21 | case_template_id = test_case_template["_id"] 22 | update_fields: InputCaseTemplate = { 23 | "name": "updated template name", 24 | "description": "updated template description", 25 | } 26 | thehive.case_template.update( 27 | case_template_id=case_template_id, fields=update_fields 28 | ) 29 | updated_case_template = thehive.case_template.get( 30 | case_template_id=case_template_id 31 | ) 32 | 33 | for key, value in update_fields.items(): 34 | assert updated_case_template.get(key) == value 35 | 36 | def test_delete(self, thehive: TheHiveApi, test_case_template: OutputCaseTemplate): 37 | case_template_id = test_case_template["_id"] 38 | thehive.case_template.delete(case_template_id=case_template_id) 39 | with pytest.raises(TheHiveError): 40 | thehive.case_template.get(case_template_id=case_template_id) 41 | 42 | def test_find( 43 | self, 44 | thehive: TheHiveApi, 45 | test_case_templates: List[OutputCaseTemplate], 46 | ): 47 | found_templates = thehive.case_template.find() 48 | original_ids = [template["_id"] for template in test_case_templates] 49 | found_ids = [template["_id"] for template in found_templates] 50 | assert sorted(found_ids) == sorted(original_ids) 51 | -------------------------------------------------------------------------------- /tests/test_comment_endpoint.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thehive4py.client import TheHiveApi 3 | from thehive4py.errors import TheHiveError 4 | from thehive4py.types.alert import OutputAlert 5 | from thehive4py.types.case import OutputCase 6 | from thehive4py.types.comment import InputUpdateComment, OutputComment 7 | 8 | 9 | class TestCommentEndpoint: 10 | def test_create_in_alert_and_get( 11 | self, thehive: TheHiveApi, test_alert: OutputAlert 12 | ): 13 | created_comment = thehive.comment.create_in_alert( 14 | alert_id=test_alert["_id"], 15 | comment={ 16 | "message": "test comment", 17 | }, 18 | ) 19 | 20 | fetched_comment = thehive.comment.get(comment_id=created_comment["_id"]) 21 | assert created_comment == fetched_comment 22 | 23 | def test_create_in_case_and_get(self, thehive: TheHiveApi, test_case: OutputCase): 24 | created_comment = thehive.comment.create_in_case( 25 | case_id=test_case["_id"], 26 | comment={ 27 | "message": "test comment", 28 | }, 29 | ) 30 | 31 | fetched_comment = thehive.comment.get(comment_id=created_comment["_id"]) 32 | assert created_comment == fetched_comment 33 | 34 | def test_delete(self, thehive: TheHiveApi, test_comment: OutputComment): 35 | comment_id = test_comment["_id"] 36 | thehive.comment.delete(comment_id=comment_id) 37 | with pytest.raises(TheHiveError): 38 | thehive.comment.get(comment_id=comment_id) 39 | 40 | def test_update(self, thehive: TheHiveApi, test_comment: OutputComment): 41 | comment_id = test_comment["_id"] 42 | update_fields: InputUpdateComment = { 43 | "message": "updated comment", 44 | } 45 | thehive.comment.update(comment_id=test_comment["_id"], fields=update_fields) 46 | updated_comment = thehive.comment.get(comment_id=comment_id) 47 | 48 | for key, value in update_fields.items(): 49 | assert updated_comment.get(key) == value 50 | -------------------------------------------------------------------------------- /tests/test_cortex_endpoint.py: -------------------------------------------------------------------------------- 1 | from thehive4py.client import TheHiveApi 2 | from thehive4py.types.case import OutputCase 3 | from thehive4py.types.observable import OutputObservable 4 | 5 | 6 | class TestCortexEndpoint: 7 | def test_create_analyzer_job( 8 | self, thehive: TheHiveApi, test_observable: OutputObservable 9 | ): 10 | output_analyzer_job = thehive.cortex.create_analyzer_job( 11 | job={ 12 | "analyzerId": "example", 13 | "cortexId": "cortex", 14 | "artifactId": test_observable["_id"], 15 | } 16 | ) 17 | assert output_analyzer_job["_type"] == "case_artifact_job" 18 | 19 | def test_list_analyzers(self, thehive: TheHiveApi): 20 | analyzers = thehive.cortex.list_analyzers() 21 | assert analyzers == [] 22 | 23 | def test_list_analyzers_by_type(self, thehive: TheHiveApi): 24 | data_type = "mail" 25 | analyzers = thehive.cortex.list_analyzers_by_type(data_type=data_type) 26 | assert analyzers == [] 27 | 28 | def test_list_responders(self, thehive: TheHiveApi, test_case: OutputCase): 29 | responders = thehive.cortex.list_responders( 30 | entity_type="case", entity_id=test_case["_id"] 31 | ) 32 | assert responders == [] 33 | -------------------------------------------------------------------------------- /tests/test_custom_field_endpoint.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from thehive4py.client import TheHiveApi 4 | from thehive4py.errors import TheHiveError 5 | from thehive4py.types.custom_field import InputUpdateCustomField, OutputCustomField 6 | 7 | 8 | class TestCustomFieldEndpoint: 9 | def test_create_and_list(self, thehive_admin: TheHiveApi): 10 | created_custom_field = thehive_admin.custom_field.create( 11 | custom_field={ 12 | "name": "my-field", 13 | "displayName": "My Field", 14 | "description": "...", 15 | "group": "default", 16 | "type": "string", 17 | "options": [], 18 | } 19 | ) 20 | 21 | custom_fields = thehive_admin.custom_field.list() 22 | assert created_custom_field in custom_fields 23 | 24 | def test_delete( 25 | self, thehive_admin: TheHiveApi, test_custom_field: OutputCustomField 26 | ): 27 | thehive_admin.custom_field.delete(custom_field_id=test_custom_field["_id"]) 28 | 29 | with pytest.raises(TheHiveError): 30 | thehive_admin.custom_field.delete(custom_field_id=test_custom_field["_id"]) 31 | 32 | def test_update( 33 | self, thehive_admin: TheHiveApi, test_custom_field: OutputCustomField 34 | ): 35 | custom_field_id = test_custom_field["_id"] 36 | update_fields: InputUpdateCustomField = { 37 | "displayName": "something else ...", 38 | "type": "float", 39 | } 40 | thehive_admin.custom_field.update( 41 | custom_field_id=custom_field_id, fields=update_fields 42 | ) 43 | updated_custom_field = [ 44 | cf 45 | for cf in thehive_admin.custom_field.list() 46 | if cf["_id"] == custom_field_id 47 | ][0] 48 | 49 | for key, value in update_fields.items(): 50 | assert updated_custom_field.get(key) == value 51 | -------------------------------------------------------------------------------- /tests/test_init.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from thehive4py import _warn_old_py_version 6 | 7 | 8 | def test_warn_old_py_version(): 9 | actual_py_version = sys.version_info 10 | dummy_min_py_version = (actual_py_version[0], actual_py_version[1] + 1) 11 | with pytest.deprecated_call(): 12 | _warn_old_py_version(min_py_version=dummy_min_py_version) 13 | -------------------------------------------------------------------------------- /tests/test_observable_endpoint.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | from pathlib import Path 3 | from typing import List 4 | 5 | import pytest 6 | from thehive4py import TheHiveApi 7 | from thehive4py.errors import TheHiveError 8 | from thehive4py.query.sort import Asc 9 | from thehive4py.types.alert import OutputAlert 10 | from thehive4py.types.case import OutputCase 11 | from thehive4py.types.observable import ( 12 | InputBulkUpdateObservable, 13 | InputUpdateObservable, 14 | OutputObservable, 15 | ) 16 | 17 | 18 | class TestObservableEndpoint: 19 | def test_create_in_alert_and_get( 20 | self, thehive: TheHiveApi, test_alert: OutputAlert 21 | ): 22 | created_observable = thehive.observable.create_in_alert( 23 | alert_id=test_alert["_id"], 24 | observable={ 25 | "dataType": "domain", 26 | "data": "example.com", 27 | "message": "test observable", 28 | }, 29 | )[0] 30 | 31 | fetched_observable = thehive.observable.get( 32 | observable_id=created_observable["_id"] 33 | ) 34 | assert created_observable == fetched_observable 35 | 36 | def test_create_in_alert_from_file_and_download_as_zip( 37 | self, thehive: TheHiveApi, test_alert: OutputAlert, tmp_path: Path 38 | ): 39 | observable_content = "observable content" 40 | observable_filename = "alert-observable.txt" 41 | observable_path = str(tmp_path / observable_filename) 42 | with open(observable_path, "w") as observable_fp: 43 | observable_fp.write(observable_content) 44 | 45 | created_observable = thehive.observable.create_in_alert( 46 | alert_id=test_alert["_id"], 47 | observable={ 48 | "dataType": "file", 49 | "message": "file based observable", 50 | }, 51 | observable_path=observable_path, 52 | )[0] 53 | 54 | created_attachment = created_observable.get("attachment") 55 | assert created_attachment 56 | 57 | observable_archive = str(tmp_path / "downloaded-observable.zip") 58 | thehive.observable.download_attachment( 59 | observable_id=created_observable["_id"], 60 | attachment_id=created_attachment["_id"], 61 | observable_path=observable_archive, 62 | as_zip=True, 63 | ) 64 | 65 | with zipfile.ZipFile(observable_archive) as archive_fp: 66 | with archive_fp.open(observable_filename, pwd=b"malware") as downloaded_fp: 67 | assert downloaded_fp.read().decode() == observable_content 68 | 69 | def test_create_in_case_and_get(self, thehive: TheHiveApi, test_case: OutputCase): 70 | created_observable = thehive.observable.create_in_case( 71 | case_id=test_case["_id"], 72 | observable={ 73 | "dataType": "domain", 74 | "data": "example.com", 75 | "message": "test observable", 76 | }, 77 | )[0] 78 | 79 | fetched_observable = thehive.observable.get( 80 | observable_id=created_observable["_id"] 81 | ) 82 | assert created_observable == fetched_observable 83 | 84 | def test_create_in_case_from_file_and_download_as_is( 85 | self, thehive: TheHiveApi, test_case: OutputCase, tmp_path: Path 86 | ): 87 | observable_content = "observable content" 88 | observable_filename = "case-observable.txt" 89 | observable_path = str(tmp_path / observable_filename) 90 | with open(observable_path, "w") as observable_fp: 91 | observable_fp.write(observable_content) 92 | 93 | created_observable = thehive.observable.create_in_case( 94 | case_id=test_case["_id"], 95 | observable={ 96 | "dataType": "file", 97 | "message": "file based observable", 98 | }, 99 | observable_path=observable_path, 100 | )[0] 101 | 102 | created_attachment = created_observable.get("attachment") 103 | assert created_attachment 104 | 105 | downloaded_observable_path = str(tmp_path / "downloaded-observable.zip") 106 | thehive.observable.download_attachment( 107 | observable_id=created_observable["_id"], 108 | attachment_id=created_attachment["_id"], 109 | observable_path=downloaded_observable_path, 110 | as_zip=False, 111 | ) 112 | 113 | with open(downloaded_observable_path) as downloaded_fp: 114 | assert downloaded_fp.read() == observable_content 115 | 116 | def test_delete(self, thehive: TheHiveApi, test_observable: OutputObservable): 117 | observable_id = test_observable["_id"] 118 | thehive.observable.delete(observable_id=observable_id) 119 | with pytest.raises(TheHiveError): 120 | thehive.observable.get(observable_id=observable_id) 121 | 122 | def test_update(self, thehive: TheHiveApi, test_observable: OutputObservable): 123 | observable_id = test_observable["_id"] 124 | update_fields: InputUpdateObservable = { 125 | "dataType": "fqdn", 126 | "message": "updated observable", 127 | } 128 | thehive.observable.update( 129 | observable_id=test_observable["_id"], fields=update_fields 130 | ) 131 | updated_observable = thehive.observable.get(observable_id=observable_id) 132 | 133 | for key, value in update_fields.items(): 134 | assert updated_observable.get(key) == value 135 | 136 | def test_bulk_update( 137 | self, thehive: TheHiveApi, test_observables: List[OutputObservable] 138 | ): 139 | observable_ids = [observable["_id"] for observable in test_observables] 140 | update_fields: InputBulkUpdateObservable = { 141 | "ids": observable_ids, 142 | "dataType": "other", 143 | "message": "updated observable", 144 | } 145 | 146 | thehive.observable.bulk_update(fields=update_fields) 147 | updated_observables = thehive.observable.find() 148 | 149 | expected_fields = { 150 | key: value for key, value in update_fields.items() if key != "ids" 151 | } 152 | for updated_task in updated_observables: 153 | for key, value in expected_fields.items(): 154 | assert updated_task.get(key) == value 155 | 156 | @pytest.mark.skip( 157 | reason="documentation is unclear and implementation might be changed" 158 | ) 159 | def test_share_and_unshare( 160 | self, thehive: TheHiveApi, test_observable: OutputObservable 161 | ): 162 | organisation = "share-org" 163 | 164 | thehive.observable.share( 165 | observable_id=test_observable["_id"], organisations=[organisation] 166 | ) 167 | assert ( 168 | len(thehive.observable.list_shares(observable_id=test_observable["_id"])) 169 | == 1 170 | ) 171 | 172 | thehive.observable.unshare( 173 | observable_id=test_observable["_id"], organisations=[organisation] 174 | ) 175 | assert ( 176 | len(thehive.observable.list_shares(observable_id=test_observable["_id"])) 177 | == 0 178 | ) 179 | 180 | def test_find_and_count( 181 | self, thehive: TheHiveApi, test_observables: List[OutputObservable] 182 | ): 183 | found_observables = thehive.observable.find( 184 | sortby=Asc("_createdAt"), 185 | ) 186 | observable_count = thehive.observable.count() 187 | 188 | assert found_observables == test_observables 189 | assert len(test_observables) == observable_count 190 | -------------------------------------------------------------------------------- /tests/test_observable_type_endpoint.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thehive4py.client import TheHiveApi 3 | from thehive4py.errors import TheHiveError 4 | from thehive4py.query.filters import Eq 5 | from thehive4py.types.observable_type import OutputObservableType 6 | 7 | 8 | class TestObservableTypeEndpoint: 9 | def test_create_and_get(self, thehive_admin: TheHiveApi): 10 | created_observable_type = thehive_admin.observable_type.create( 11 | { 12 | "name": "new-observable-type", 13 | } 14 | ) 15 | 16 | test_observable_type = thehive_admin.observable_type.get( 17 | created_observable_type["_id"] 18 | ) 19 | assert created_observable_type == test_observable_type 20 | 21 | def test_find( 22 | self, thehive: TheHiveApi, test_observable_type: OutputObservableType 23 | ): 24 | found_observable_types = thehive.observable_type.find( 25 | filters=Eq("name", test_observable_type["name"]) 26 | ) 27 | 28 | assert found_observable_types == [test_observable_type] 29 | 30 | def test_delete( 31 | self, thehive: TheHiveApi, test_observable_type: OutputObservableType 32 | ): 33 | observable_type_id = test_observable_type["_id"] 34 | thehive.observable_type.delete(observable_type_id) 35 | with pytest.raises(TheHiveError): 36 | thehive.observable_type.get(observable_type_id) 37 | -------------------------------------------------------------------------------- /tests/test_organisation_endpoint.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from tests.utils import TestConfig 4 | from thehive4py.client import TheHiveApi 5 | from thehive4py.query.filters import Eq 6 | 7 | 8 | class TestOrganisationEndpoint: 9 | 10 | def test_add_and_download_attachment_to_main_org( 11 | self, thehive: TheHiveApi, tmp_path: Path 12 | ): 13 | attachment_paths = [str(tmp_path / f"attachment-{i}.txt") for i in range(2)] 14 | download_attachment_paths = [ 15 | str(tmp_path / f"dl-attachment-{i}.txt") for i in range(2) 16 | ] 17 | 18 | for path in attachment_paths: 19 | with open(path, "w") as attachment_fp: 20 | attachment_fp.write(f"content of {path}") 21 | 22 | added_attachments = thehive.organisation.add_attachment( 23 | attachment_paths=attachment_paths 24 | ) 25 | 26 | for attachment, path in zip(added_attachments, download_attachment_paths): 27 | thehive.organisation.download_attachment( 28 | attachment_id=attachment["_id"], 29 | attachment_path=path, 30 | ) 31 | 32 | for original, downloaded in zip(attachment_paths, download_attachment_paths): 33 | with open(original) as original_fp, open(downloaded) as downloaded_fp: 34 | assert original_fp.read() == downloaded_fp.read() 35 | 36 | def test_add_and_delete_attachment_to_main_org( 37 | self, thehive: TheHiveApi, tmp_path: Path, test_config: TestConfig 38 | ): 39 | attachment_path = str(tmp_path / "my-attachment.txt") 40 | with open(attachment_path, "w") as attachment_fp: 41 | attachment_fp.write("some content...") 42 | 43 | added_attachments = thehive.organisation.add_attachment( 44 | attachment_paths=[attachment_path] 45 | ) 46 | 47 | for attachment in added_attachments: 48 | thehive.organisation.delete_attachment(attachment_id=attachment["_id"]) 49 | 50 | assert thehive.organisation.find_attachments(org_id=test_config.main_org) == [] 51 | 52 | def test_get_organisation(self, thehive_admin: TheHiveApi, test_config: TestConfig): 53 | main_org = thehive_admin.organisation.get(org_id=test_config.main_org) 54 | 55 | assert main_org["name"] == test_config.main_org 56 | 57 | def test_find_organisations( 58 | self, thehive_admin: TheHiveApi, test_config: TestConfig 59 | ): 60 | 61 | organisations = thehive_admin.organisation.find( 62 | filters=Eq("name", test_config.main_org) 63 | ) 64 | 65 | assert len(organisations) == 1 66 | 67 | # most of the other organisation endpoints cannot be tested due to constraints 68 | # as at the moment the community license allows up to maximum 1 organisation 69 | -------------------------------------------------------------------------------- /tests/test_procedure_endpoint.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from thehive4py.client import TheHiveApi 3 | from thehive4py.errors import TheHiveError 4 | from thehive4py.helpers import now_to_ts 5 | from thehive4py.types.alert import OutputAlert 6 | from thehive4py.types.case import OutputCase 7 | from thehive4py.types.procedure import InputUpdateProcedure, OutputProcedure 8 | 9 | 10 | class TestProcedureEndpoint: 11 | def test_create_in_alert_and_get( 12 | self, thehive: TheHiveApi, test_alert: OutputAlert 13 | ): 14 | created_procedure = thehive.procedure.create_in_alert( 15 | alert_id=test_alert["_id"], 16 | procedure={ 17 | "occurDate": now_to_ts(), 18 | "patternId": "T1059.006", 19 | "tactic": "execution", 20 | "description": "...", 21 | }, 22 | ) 23 | 24 | fetched_procedure = thehive.procedure.get(procedure_id=created_procedure["_id"]) 25 | assert created_procedure == fetched_procedure 26 | 27 | def test_create_in_case_and_get(self, thehive: TheHiveApi, test_case: OutputCase): 28 | created_procedure = thehive.procedure.create_in_case( 29 | case_id=test_case["_id"], 30 | procedure={ 31 | "occurDate": now_to_ts(), 32 | "patternId": "T1059.006", 33 | "tactic": "execution", 34 | "description": "...", 35 | }, 36 | ) 37 | 38 | fetched_procedure = thehive.procedure.get(procedure_id=created_procedure["_id"]) 39 | assert created_procedure == fetched_procedure 40 | 41 | def test_delete(self, thehive: TheHiveApi, test_procedure: OutputProcedure): 42 | procedure_id = test_procedure["_id"] 43 | thehive.procedure.delete(procedure_id=procedure_id) 44 | with pytest.raises(TheHiveError): 45 | thehive.procedure.get(procedure_id=procedure_id) 46 | 47 | def test_update(self, thehive: TheHiveApi, test_procedure: OutputProcedure): 48 | procedure_id = test_procedure["_id"] 49 | update_fields: InputUpdateProcedure = { 50 | "description": "updated procedure", 51 | "occurDate": now_to_ts(), 52 | } 53 | thehive.procedure.update( 54 | procedure_id=test_procedure["_id"], fields=update_fields 55 | ) 56 | updated_procedure = thehive.procedure.get(procedure_id=procedure_id) 57 | 58 | for key, value in update_fields.items(): 59 | assert updated_procedure.get(key) == value 60 | -------------------------------------------------------------------------------- /tests/test_profile_endpoint.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from thehive4py.client import TheHiveApi 4 | from thehive4py.errors import TheHiveError 5 | from thehive4py.query.filters import Eq 6 | from thehive4py.types.profile import InputUpdateProfile, OutputProfile 7 | 8 | 9 | class TestProfileEndpoint: 10 | def test_create_and_get(self, thehive_admin: TheHiveApi): 11 | profile = thehive_admin.profile.create( 12 | profile={"name": "test-profile", "permissions": []} 13 | ) 14 | assert profile == thehive_admin.profile.get(profile_id=profile["_id"]) 15 | 16 | def test_create_and_delete( 17 | self, thehive_admin: TheHiveApi, test_profile: OutputProfile 18 | ): 19 | thehive_admin.profile.delete(profile_id=test_profile["_id"]) 20 | 21 | with pytest.raises(TheHiveError): 22 | thehive_admin.profile.get(profile_id=test_profile["_id"]) 23 | 24 | def test_update(self, thehive_admin: TheHiveApi, test_profile: OutputProfile): 25 | update_fields: InputUpdateProfile = { 26 | "name": "updated-test-profile", 27 | "permissions": ["manageAlert/create", "manageCase/create"], 28 | } 29 | thehive_admin.profile.update( 30 | profile_id=test_profile["_id"], fields=update_fields 31 | ) 32 | 33 | updated_profile = thehive_admin.profile.get(profile_id=test_profile["_id"]) 34 | for field, value in update_fields.items(): 35 | assert updated_profile.get(field) == value 36 | 37 | def test_find_and_count( 38 | self, thehive_admin: TheHiveApi, test_profile: OutputProfile 39 | ): 40 | filters = Eq("name", test_profile["name"]) 41 | found_profiles = thehive_admin.profile.find( 42 | filters=filters, 43 | ) 44 | 45 | profile_count = thehive_admin.profile.count(filters=filters) 46 | 47 | assert test_profile in found_profiles 48 | assert len(found_profiles) == profile_count 49 | -------------------------------------------------------------------------------- /tests/test_query_endpoint.py: -------------------------------------------------------------------------------- 1 | from thehive4py.client import TheHiveApi 2 | from thehive4py.types.alert import OutputAlert 3 | 4 | 5 | class TestQueryEndpoint: 6 | def test_simple_query(self, thehive: TheHiveApi, test_alert: OutputAlert): 7 | queried_alerts = thehive.query.run( 8 | query=[{"_name": "getAlert", "idOrName": test_alert["_id"]}], 9 | ) 10 | 11 | assert [test_alert] == queried_alerts 12 | -------------------------------------------------------------------------------- /tests/test_query_filters.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from thehive4py import TheHiveApi 4 | from thehive4py.helpers import now_to_ts 5 | from thehive4py.query.filters import ( 6 | Between, 7 | Contains, 8 | EndsWith, 9 | Eq, 10 | Gt, 11 | Gte, 12 | Has, 13 | Id, 14 | In, 15 | Like, 16 | Lt, 17 | Lte, 18 | Match, 19 | Ne, 20 | StartsWith, 21 | ) 22 | from thehive4py.types.user import OutputUser 23 | 24 | 25 | class TestQueryFilters: 26 | def test_between_contains_has_in(self, thehive: TheHiveApi, test_user: OutputUser): 27 | assert thehive.user.find( 28 | filters=Between(field="_createdAt", start=0, end=now_to_ts()) 29 | ) 30 | with pytest.deprecated_call(): 31 | assert thehive.user.find(filters=Contains(field="login")) 32 | assert thehive.user.find(filters=Has(field="login")) 33 | assert thehive.user.find( 34 | filters=In(field="login", values=["...", "xyz", test_user["login"]]) 35 | ) 36 | 37 | def test_endswith_startswith(self, thehive: TheHiveApi, test_user: OutputUser): 38 | assert thehive.user.find( 39 | filters=EndsWith(field="login", value=test_user["login"]) 40 | ) 41 | assert thehive.user.find( 42 | filters=StartsWith(field="login", value=test_user["login"]) 43 | ) 44 | 45 | def test_eq_ne_id(self, thehive: TheHiveApi, test_user: OutputUser): 46 | assert thehive.user.find(filters=Eq(field="_id", value=test_user["_id"])) 47 | assert thehive.user.find(filters=Ne(field="_id", value=test_user["login"])) 48 | assert thehive.user.find(filters=Id(id=test_user["_id"])) 49 | 50 | def test_gt_gte_lt_lte(self, thehive: TheHiveApi, test_user: OutputUser): 51 | assert thehive.user.find(filters=Gt(field="_createdAt", value=0)) 52 | assert thehive.user.find(filters=Gte(field="_createdAt", value=0)) 53 | assert thehive.user.find(filters=Lt(field="_createdAt", value=now_to_ts())) 54 | assert thehive.user.find(filters=Lte(field="_createdAt", value=now_to_ts())) 55 | 56 | def test_like_match(self, thehive: TheHiveApi, test_user: OutputUser): 57 | assert thehive.user.find(filters=Like(field="login", value=test_user["login"])) 58 | assert thehive.user.find(filters=Match(field="login", value=test_user["login"])) 59 | 60 | def test_and_or_not(self, thehive: TheHiveApi, test_user: OutputUser): 61 | assert thehive.user.find( 62 | filters=Id(id=test_user["_id"]) 63 | & ( 64 | Eq(field="login", value=test_user["login"]) 65 | | Eq(field="login", value="...") 66 | ) 67 | & ~Eq(field="login", value="...") 68 | ) 69 | -------------------------------------------------------------------------------- /tests/test_task_endpoint.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | from tests.utils import TestConfig 6 | from thehive4py.client import TheHiveApi 7 | from thehive4py.errors import TheHiveError 8 | from thehive4py.query.filters import Eq 9 | from thehive4py.query.sort import Asc 10 | from thehive4py.types.case import OutputCase 11 | from thehive4py.types.task import InputBulkUpdateTask, InputUpdateTask, OutputTask 12 | 13 | 14 | class TestTaskEndpoint: 15 | def test_create_and_get(self, thehive: TheHiveApi, test_case: OutputCase): 16 | created_task = thehive.task.create( 17 | case_id=test_case["_id"], task={"title": "test task"} 18 | ) 19 | 20 | fetched_task = thehive.task.get(task_id=created_task["_id"]) 21 | 22 | assert created_task == fetched_task 23 | 24 | def test_delete(self, thehive: TheHiveApi, test_task: OutputTask): 25 | thehive.task.delete(task_id=test_task["_id"]) 26 | 27 | with pytest.raises(TheHiveError): 28 | thehive.task.get(task_id=test_task["_id"]) 29 | 30 | def test_update(self, thehive: TheHiveApi, test_task: OutputTask): 31 | update_fields: InputUpdateTask = {"title": "updated task title"} 32 | thehive.task.update(task_id=test_task["_id"], fields=update_fields) 33 | 34 | updated_task = thehive.task.get(task_id=test_task["_id"]) 35 | 36 | for key, value in update_fields.items(): 37 | assert updated_task.get(key) == value 38 | 39 | def test_bulk_update(self, thehive: TheHiveApi, test_tasks: List[OutputTask]): 40 | task_ids = [task["_id"] for task in test_tasks] 41 | update_fields: InputBulkUpdateTask = { 42 | "ids": task_ids, 43 | "title": "my updated task", 44 | } 45 | 46 | thehive.task.bulk_update(fields=update_fields) 47 | updated_tasks = thehive.task.find() 48 | 49 | expected_fields = { 50 | key: value for key, value in update_fields.items() if key != "ids" 51 | } 52 | for updated_task in updated_tasks: 53 | for key, value in expected_fields.items(): 54 | assert updated_task.get(key) == value 55 | 56 | def test_set_as_required_and_done( 57 | self, test_config: TestConfig, thehive: TheHiveApi, test_task: OutputTask 58 | ): 59 | organisation = test_config.main_org 60 | 61 | thehive.task.set_as_required(task_id=test_task["_id"], org_id=organisation) 62 | actions = thehive.task.get_required_actions(task_id=test_task["_id"]) 63 | assert actions[organisation] is True 64 | 65 | thehive.task.set_as_done(task_id=test_task["_id"], org_id=organisation) 66 | actions = thehive.task.get_required_actions(task_id=test_task["_id"]) 67 | assert actions[organisation] is False 68 | 69 | @pytest.mark.skip( 70 | reason="documentation is unclear and implementation might be changed" 71 | ) 72 | def test_share_and_unshare(self, thehive: TheHiveApi, test_task: OutputTask): 73 | pass 74 | 75 | def test_find_and_count(self, thehive: TheHiveApi, test_tasks: List[OutputTask]): 76 | filters = Eq("title", test_tasks[0]["title"]) | Eq( 77 | "title", test_tasks[1]["title"] 78 | ) 79 | found_tasks = thehive.task.find( 80 | filters=filters, 81 | sortby=Asc("_createdAt"), 82 | ) 83 | 84 | task_count = thehive.task.count(filters=filters) 85 | 86 | assert found_tasks == test_tasks 87 | assert len(test_tasks) == task_count 88 | 89 | def test_create_and_get_logs(self, thehive: TheHiveApi, test_task: OutputTask): 90 | created_task = thehive.task.create_log( 91 | task_id=test_task["_id"], task_log={"message": "my test log"} 92 | ) 93 | task_logs = thehive.task.find_logs(task_id=test_task["_id"]) 94 | assert created_task in task_logs 95 | -------------------------------------------------------------------------------- /tests/test_task_log_endpoint.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from thehive4py.client import TheHiveApi 6 | from thehive4py.errors import TheHiveError 7 | from thehive4py.helpers import now_to_ts 8 | from thehive4py.types.task import OutputTask 9 | from thehive4py.types.task_log import InputUpdateTaskLog, OutputTaskLog 10 | 11 | 12 | class TestTaskLogEndpoint: 13 | def test_create_and_get(self, thehive: TheHiveApi, test_task: OutputTask): 14 | created_log = thehive.task_log.create( 15 | task_id=test_task["_id"], 16 | task_log={ 17 | "message": "My test log", 18 | "includeInTimeline": now_to_ts(), 19 | "startDate": now_to_ts(), 20 | }, 21 | ) 22 | 23 | fetched_log = thehive.task_log.get(task_log_id=created_log["_id"]) 24 | 25 | assert created_log == fetched_log 26 | 27 | def test_create_with_attachment( 28 | self, thehive: TheHiveApi, test_task: OutputTask, tmp_path: Path 29 | ): 30 | 31 | attachment_paths = [str(tmp_path / f"attachment-{i}.txt") for i in range(2)] 32 | 33 | for path in attachment_paths: 34 | with open(path, "w") as attachment_fp: 35 | attachment_fp.write(f"content of {path}") 36 | 37 | log_with_attachment = thehive.task_log.create( 38 | task_id=test_task["_id"], 39 | task_log={ 40 | "message": "My test log", 41 | "includeInTimeline": now_to_ts(), 42 | "startDate": now_to_ts(), 43 | "attachments": attachment_paths, 44 | }, 45 | ) 46 | 47 | assert "attachments" in log_with_attachment 48 | assert len(log_with_attachment["attachments"]) == len(attachment_paths) 49 | 50 | def test_delete(self, thehive: TheHiveApi, test_task_log: OutputTaskLog): 51 | thehive.task_log.delete(task_log_id=test_task_log["_id"]) 52 | 53 | with pytest.raises(TheHiveError): 54 | thehive.task_log.get(task_log_id=test_task_log["_id"]) 55 | 56 | def test_update(self, thehive: TheHiveApi, test_task_log: OutputTask): 57 | update_fields: InputUpdateTaskLog = {"message": "updated task log message"} 58 | thehive.task_log.update(task_log_id=test_task_log["_id"], fields=update_fields) 59 | 60 | updated_task_log = thehive.task_log.get(task_log_id=test_task_log["_id"]) 61 | 62 | for key, value in update_fields.items(): 63 | assert updated_task_log.get(key) == value 64 | 65 | def test_add_and_delete_attachment( 66 | self, thehive: TheHiveApi, test_task_log: OutputTaskLog, tmp_path: Path 67 | ): 68 | attachment_path = str(tmp_path / "my-attachment.txt") 69 | with open(attachment_path, "w") as attachment_fp: 70 | attachment_fp.write("some content...") 71 | 72 | thehive.task_log.add_attachment( 73 | task_log_id=test_task_log["_id"], attachment_paths=[attachment_path] 74 | ) 75 | 76 | attachments = thehive.task_log.get(task_log_id=test_task_log["_id"]).get( 77 | "attachments", [] 78 | ) 79 | 80 | for attachment in attachments: 81 | thehive.task_log.delete_attachment( 82 | task_log_id=test_task_log["_id"], attachment_id=attachment["_id"] 83 | ) 84 | 85 | attachments = thehive.task_log.get(task_log_id=test_task_log["_id"]).get( 86 | "attachments", [] 87 | ) 88 | 89 | assert attachments == [] 90 | 91 | def test_add_and_delete_attachments( 92 | self, thehive: TheHiveApi, test_task_log: OutputTaskLog, tmp_path: Path 93 | ): 94 | attachment_path = str(tmp_path / "my-attachment.txt") 95 | with open(attachment_path, "w") as attachment_fp: 96 | attachment_fp.write("some content...") 97 | 98 | with pytest.deprecated_call(): 99 | thehive.task_log.add_attachments( 100 | task_log_id=test_task_log["_id"], attachment_paths=[attachment_path] 101 | ) 102 | 103 | attachments = thehive.task_log.get(task_log_id=test_task_log["_id"]).get( 104 | "attachments", [] 105 | ) 106 | 107 | for attachment in attachments: 108 | thehive.task_log.delete_attachment( 109 | task_log_id=test_task_log["_id"], attachment_id=attachment["_id"] 110 | ) 111 | 112 | attachments = thehive.task_log.get(task_log_id=test_task_log["_id"]).get( 113 | "attachments", [] 114 | ) 115 | 116 | assert attachments == [] 117 | -------------------------------------------------------------------------------- /tests/test_timeline_endpoint.py: -------------------------------------------------------------------------------- 1 | from thehive4py.client import TheHiveApi 2 | from thehive4py.helpers import now_to_ts 3 | from thehive4py.types.case import OutputCase 4 | from thehive4py.types.timeline import InputUpdateCustomEvent, OutputCustomEvent 5 | 6 | 7 | class TestTimelineEndpoint: 8 | def test_get(self, thehive: TheHiveApi, test_case: OutputCase): 9 | timeline = thehive.timeline.get(case_id=test_case["_id"]) 10 | assert timeline["events"] 11 | 12 | def test_create_and_delete_event(self, thehive: TheHiveApi, test_case: OutputCase): 13 | timeline_event = thehive.timeline.create_event( 14 | case_id=test_case["_id"], 15 | event={"date": now_to_ts(), "title": "custom timeline event"}, 16 | ) 17 | 18 | timeline_events = thehive.timeline.get(case_id=test_case["_id"])["events"] 19 | assert timeline_event["_id"] in [event["entityId"] for event in timeline_events] 20 | 21 | thehive.timeline.delete_event(event_id=timeline_event["_id"]) 22 | timeline_events = thehive.timeline.get(case_id=test_case["_id"])["events"] 23 | assert timeline_event["_id"] not in [ 24 | event["entityId"] for event in timeline_events 25 | ] 26 | 27 | def test_update_event( 28 | self, thehive: TheHiveApi, test_timeline_event: OutputCustomEvent 29 | ): 30 | event_id = test_timeline_event["_id"] 31 | update_fields: InputUpdateCustomEvent = { 32 | "date": now_to_ts(), 33 | "endDate": now_to_ts(), 34 | "title": "updated event", 35 | "description": "updated event description", 36 | } 37 | thehive.timeline.update_event(event_id=event_id, fields=update_fields) 38 | -------------------------------------------------------------------------------- /tests/test_user_endpoint.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import pytest 4 | 5 | from tests.utils import TestConfig 6 | from thehive4py.client import TheHiveApi 7 | from thehive4py.errors import TheHiveError 8 | from thehive4py.query.filters import Eq 9 | from thehive4py.types.user import InputUpdateUser, InputUserOrganisation, OutputUser 10 | 11 | 12 | class TestUserEndpoint: 13 | def test_get_current(self, thehive: TheHiveApi): 14 | # TODO: implement a better control for user checking 15 | current_user = thehive.user.get_current() 16 | assert current_user["login"] == "admin@thehive.local" 17 | 18 | def test_create_and_get(self, thehive: TheHiveApi): 19 | created_user = thehive.user.create( 20 | user={"login": "bailando@sin.ti", "name": "Bailando", "profile": "analyst"} 21 | ) 22 | created_user_2 = thehive.user.create( 23 | user={"login": "bailando@sin.ti", "name": "Bailando", "profile": "analyst"} 24 | ) 25 | 26 | assert created_user == created_user_2 27 | 28 | fetched_user = thehive.user.get(created_user["_id"]) 29 | assert created_user == fetched_user 30 | 31 | def test_update( 32 | self, test_config: TestConfig, thehive: TheHiveApi, test_user: OutputUser 33 | ): 34 | user_id = test_user["_id"] 35 | update_fields: InputUpdateUser = { 36 | "name": "Updated user", 37 | "profile": "read-only", 38 | "email": "whatever@example.com", 39 | "organisation": test_config.main_org, 40 | } 41 | thehive.user.update(user_id=user_id, fields=update_fields) 42 | updated_user = thehive.user.get(user_id=user_id) 43 | 44 | for field in update_fields: 45 | assert updated_user.get(field) == update_fields.get(field) 46 | 47 | def test_lock_and_unlock(self, thehive: TheHiveApi, test_user: OutputUser): 48 | user_id = test_user["_id"] 49 | 50 | thehive.user.lock(user_id=user_id) 51 | locked_user = thehive.user.get(user_id=user_id) 52 | assert locked_user["locked"] is True 53 | 54 | thehive.user.unlock(user_id=user_id) 55 | unlocked_user = thehive.user.get(user_id=user_id) 56 | assert unlocked_user["locked"] is False 57 | 58 | def test_delete(self, thehive: TheHiveApi, test_user: OutputUser): 59 | user_id = test_user["_id"] 60 | thehive.user.delete(user_id=user_id, organisation=test_user["organisation"]) 61 | with pytest.raises(TheHiveError): 62 | thehive.user.get(user_id=user_id) 63 | 64 | @pytest.mark.skip(reason="integrator container only supports a single org ") 65 | def test_set_organisations( 66 | self, test_config: TestConfig, thehive: TheHiveApi, test_user: OutputUser 67 | ): 68 | organisations: List[InputUserOrganisation] = [ 69 | { 70 | "default": True, 71 | "organisation": test_user["organisation"], 72 | "profile": "analyst", 73 | }, 74 | { 75 | "default": False, 76 | "organisation": test_config.share_org, 77 | "profile": "read-only", 78 | }, 79 | ] 80 | user_organisations = thehive.user.set_organisations( 81 | user_id=test_user["_id"], organisations=organisations 82 | ) 83 | assert organisations == user_organisations 84 | 85 | def test_set_password(self, thehive: TheHiveApi, test_user: OutputUser): 86 | assert test_user["hasPassword"] is False 87 | user_id = test_user["_id"] 88 | 89 | password = "super-secruht!" 90 | thehive.user.set_password(user_id=user_id, password=password) 91 | 92 | user_with_password = thehive.user.get(user_id=user_id) 93 | assert user_with_password["hasPassword"] is True 94 | 95 | def test_renew_get_and_remove_apikey( 96 | self, thehive: TheHiveApi, test_user: OutputUser 97 | ): 98 | assert test_user["hasKey"] is False 99 | user_id = test_user["_id"] 100 | 101 | thehive.user.renew_apikey(user_id=user_id) 102 | assert isinstance(thehive.user.get_apikey(user_id=user_id), str) 103 | user_with_apikey = thehive.user.get(user_id=user_id) 104 | assert user_with_apikey["hasKey"] is True 105 | 106 | thehive.user.remove_apikey(user_id=user_id) 107 | user_without_apikey = thehive.user.get(user_id=user_id) 108 | assert user_without_apikey["hasKey"] is False 109 | 110 | def test_find_and_count(self, thehive: TheHiveApi, test_user: OutputUser): 111 | user_filters = Eq("login", test_user["login"]) 112 | found_users = thehive.user.find(filters=user_filters) 113 | user_count = thehive.user.count(filters=user_filters) 114 | 115 | assert [test_user] == found_users 116 | assert len(found_users) == user_count 117 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | import subprocess 3 | import time 4 | from concurrent.futures import ThreadPoolExecutor 5 | from dataclasses import dataclass 6 | 7 | import requests 8 | 9 | from thehive4py.client import TheHiveApi 10 | from thehive4py.helpers import now_to_ts 11 | from thehive4py.query.filters import Eq 12 | 13 | 14 | @dataclass 15 | class TestConfig: 16 | __test__ = False 17 | 18 | image_name: str 19 | container_name: str 20 | 21 | user: str 22 | password: str 23 | 24 | admin_org: str 25 | main_org: str 26 | share_org: str 27 | 28 | 29 | def _is_container_responsive(container_url: str) -> bool: 30 | COOLDOWN = 1.0 31 | TIMEOUT = 120.0 32 | 33 | now = time.time() 34 | end = now + TIMEOUT 35 | 36 | while now < end: 37 | try: 38 | response = requests.get(f"{container_url}/api/status") 39 | if response.ok: 40 | return True 41 | except Exception: 42 | pass 43 | 44 | time.sleep(COOLDOWN) 45 | now = time.time() 46 | return False 47 | 48 | 49 | def _is_container_exist(container_name: str) -> bool: 50 | exist_cmd = subprocess.run( 51 | shlex.split(f"docker ps -aq --filter name={container_name}"), 52 | capture_output=True, 53 | text=True, 54 | ) 55 | return bool(exist_cmd.stdout.strip()) 56 | 57 | 58 | def _get_container_port(container_name: str) -> str: 59 | port_cmd = subprocess.run( 60 | shlex.split(f"docker port {container_name}"), capture_output=True, text=True 61 | ) 62 | port = port_cmd.stdout.strip().split(":")[-1] 63 | return port 64 | 65 | 66 | def _build_container_url(container_name: str) -> str: 67 | port = _get_container_port(container_name) 68 | return f"http://localhost:{port}" 69 | 70 | 71 | def _run_container(container_name: str, container_image: str): 72 | subprocess.run( 73 | shlex.split( 74 | f"docker run -d --rm -p 9000 --name {container_name} {container_image}" 75 | ), 76 | capture_output=True, 77 | text=True, 78 | ) 79 | 80 | 81 | def _destroy_container(container_name: str): 82 | subprocess.run( 83 | shlex.split(f"docker rm -f {container_name}"), 84 | capture_output=True, 85 | text=True, 86 | ) 87 | 88 | 89 | def _reset_hive_org(hive_url: str, test_config: TestConfig, organisation: str) -> None: 90 | client = TheHiveApi( 91 | url=hive_url, 92 | username=test_config.user, 93 | password=test_config.password, 94 | organisation=organisation, 95 | ) 96 | 97 | alerts = client.alert.find() 98 | attachments = client.organisation.find_attachments(org_id=organisation) 99 | cases = client.case.find() 100 | case_templates = client.case_template.find() 101 | 102 | with ThreadPoolExecutor() as executor: 103 | executor.map(client.alert.delete, [alert["_id"] for alert in alerts]) 104 | executor.map( 105 | client.organisation.delete_attachment, 106 | [attachment["_id"] for attachment in attachments], 107 | ) 108 | executor.map(client.case.delete, [case["_id"] for case in cases]) 109 | executor.map( 110 | client.case_template.delete, 111 | [case_template["_id"] for case_template in case_templates], 112 | ) 113 | 114 | 115 | def _reset_hive_admin_org(hive_url: str, test_config: TestConfig) -> None: 116 | client = TheHiveApi( 117 | url=hive_url, 118 | username=test_config.user, 119 | password=test_config.password, 120 | organisation=test_config.admin_org, 121 | ) 122 | 123 | users = client.user.find(filters=~Eq("_createdBy", "system@thehive.local")) 124 | profiles = client.profile.find(filters=~Eq("_createdBy", "system@thehive.local")) 125 | observable_types = client.observable_type.find( 126 | filters=~Eq("_createdBy", "system@thehive.local") 127 | ) 128 | custom_fields = client.custom_field.list() 129 | 130 | with ThreadPoolExecutor() as executor: 131 | executor.map(client.user.delete, [user["_id"] for user in users]) 132 | executor.map(client.profile.delete, [profile["_id"] for profile in profiles]) 133 | executor.map( 134 | client.custom_field.delete, 135 | [custom_field["_id"] for custom_field in custom_fields], 136 | ) 137 | executor.map( 138 | client.observable_type.delete, 139 | [observable_type["_id"] for observable_type in observable_types], 140 | ) 141 | 142 | 143 | def init_hive_instance(url: str, test_config: TestConfig): 144 | hive = TheHiveApi( 145 | url=url, 146 | username=test_config.user, 147 | password=test_config.password, 148 | organisation="admin", 149 | ) 150 | 151 | current_user = hive.user.get_current() 152 | 153 | current_license = hive.session.make_request("GET", "/api/v1/license/current") 154 | if current_license["fallback"]["expiresAt"] < now_to_ts(): 155 | _destroy_container(container_name=test_config.container_name) 156 | spawn_hive_container(test_config=test_config) 157 | 158 | if not len(hive.organisation.find(filters=Eq("name", test_config.main_org))): 159 | hive.organisation.create( 160 | organisation={ 161 | "name": test_config.main_org, 162 | "description": "main organisation for testing", 163 | } 164 | ) 165 | 166 | hive.user.set_organisations( 167 | user_id=current_user["_id"], 168 | organisations=[ 169 | { 170 | "organisation": test_config.main_org, 171 | "profile": "org-admin", 172 | "default": True, 173 | }, 174 | { 175 | "organisation": "admin", 176 | "profile": "admin", 177 | }, 178 | ], 179 | ) 180 | 181 | 182 | def spawn_hive_container(test_config: TestConfig) -> str: 183 | if not _is_container_exist(container_name=test_config.container_name): 184 | _run_container( 185 | container_name=test_config.container_name, 186 | container_image=test_config.image_name, 187 | ) 188 | url = _build_container_url(container_name=test_config.container_name) 189 | 190 | if not _is_container_responsive(container_url=url): 191 | _destroy_container(container_name=test_config.container_name) 192 | raise RuntimeError("Unable to startup test container for TheHive") 193 | 194 | init_hive_instance(url=url, test_config=test_config) 195 | 196 | return url 197 | 198 | 199 | def reset_hive_instance(hive_url: str, test_config: TestConfig) -> None: 200 | # TODO: add back share config reinitialization once the license allows it 201 | with ThreadPoolExecutor() as executor: 202 | for org in [ 203 | test_config.main_org, 204 | # test_config.share_org, 205 | ]: 206 | executor.submit(_reset_hive_org, hive_url, test_config, org) 207 | executor.submit(_reset_hive_admin_org, hive_url, test_config) 208 | -------------------------------------------------------------------------------- /thehive4py/__init__.py: -------------------------------------------------------------------------------- 1 | from thehive4py.client import TheHiveApi 2 | 3 | 4 | def _warn_old_py_version(min_py_version=(3, 9)): 5 | import sys 6 | import warnings 7 | 8 | if sys.version_info < min_py_version: 9 | warnings.warn( 10 | message=( 11 | "thehive4py will drop support for Python versions below " 12 | f"{min_py_version[0]}.{min_py_version[1]}, as they have reached their " 13 | "end of life. Please upgrade to a newer version as soon as possible." 14 | ), 15 | category=DeprecationWarning, 16 | stacklevel=3, 17 | ) 18 | 19 | 20 | _warn_old_py_version() 21 | -------------------------------------------------------------------------------- /thehive4py/__version__.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | __version__ = importlib.metadata.version(__package__ or __name__) 4 | -------------------------------------------------------------------------------- /thehive4py/client.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from thehive4py.endpoints import ( 4 | AlertEndpoint, 5 | CaseEndpoint, 6 | CaseTemplateEndpoint, 7 | CommentEndpoint, 8 | ObservableEndpoint, 9 | OrganisationEndpoint, 10 | ProcedureEndpoint, 11 | ProfileEndpoint, 12 | TaskEndpoint, 13 | TaskLogEndpoint, 14 | TimelineEndpoint, 15 | UserEndpoint, 16 | ) 17 | from thehive4py.endpoints.cortex import CortexEndpoint 18 | from thehive4py.endpoints.custom_field import CustomFieldEndpoint 19 | from thehive4py.endpoints.observable_type import ObservableTypeEndpoint 20 | from thehive4py.endpoints.query import QueryEndpoint 21 | from thehive4py.session import DEFAULT_RETRY, RetryValue, TheHiveSession, VerifyValue 22 | 23 | 24 | class TheHiveApi: 25 | def __init__( 26 | self, 27 | url: str, 28 | apikey: Optional[str] = None, 29 | username: Optional[str] = None, 30 | password: Optional[str] = None, 31 | organisation: Optional[str] = None, 32 | verify: VerifyValue = True, 33 | max_retries: RetryValue = DEFAULT_RETRY, 34 | ): 35 | """Create a client of TheHive API. 36 | 37 | Parameters: 38 | url: TheHive's url. 39 | apikey: TheHive's apikey. It's required if `username` and `password` 40 | is not provided. 41 | username: TheHive's username. It's required if `apikey` is not provided. 42 | Must be specified together with `password`. 43 | password: TheHive's password. It's required if `apikey` is not provided. 44 | Must be specified together with `username`. 45 | organisation: TheHive organisation to use in the session. 46 | verify: Either a boolean, in which case it controls whether we verify 47 | the server's TLS certificate, or a string, in which case it must be a 48 | path to a CA bundle to use. 49 | max_retries: Either `None`, in which case we do not retry failed requests, 50 | or a `Retry` object. 51 | 52 | """ 53 | self.session = TheHiveSession( 54 | url=url, 55 | apikey=apikey, 56 | username=username, 57 | password=password, 58 | verify=verify, 59 | max_retries=max_retries, 60 | ) 61 | self.session_organisation = organisation 62 | 63 | # case management endpoints 64 | self.alert = AlertEndpoint(self.session) 65 | self.case = CaseEndpoint(self.session) 66 | self.case_template = CaseTemplateEndpoint(self.session) 67 | self.comment = CommentEndpoint(self.session) 68 | self.observable = ObservableEndpoint(self.session) 69 | self.procedure = ProcedureEndpoint(self.session) 70 | self.task = TaskEndpoint(self.session) 71 | self.task_log = TaskLogEndpoint(self.session) 72 | self.timeline = TimelineEndpoint(self.session) 73 | 74 | # user management endpoints 75 | self.user = UserEndpoint(self.session) 76 | self.organisation = OrganisationEndpoint(self.session) 77 | self.profile = ProfileEndpoint(self.session) 78 | 79 | # entity endpoints 80 | self.custom_field = CustomFieldEndpoint(self.session) 81 | self.observable_type = ObservableTypeEndpoint(self.session) 82 | 83 | # connector endpoints 84 | self.cortex = CortexEndpoint(self.session) 85 | 86 | # standard endpoints 87 | self.query = QueryEndpoint(self.session) 88 | 89 | @property 90 | def session_organisation(self) -> Optional[str]: 91 | return self.session.headers.get("X-Organisation") # type:ignore 92 | 93 | @session_organisation.setter 94 | def session_organisation(self, organisation: Optional[str] = None): 95 | if organisation: 96 | self.session.headers["X-Organisation"] = organisation 97 | else: 98 | self.session.headers.pop("X-Organisation", None) 99 | -------------------------------------------------------------------------------- /thehive4py/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | from .alert import AlertEndpoint 2 | from .case import CaseEndpoint 3 | from .case_template import CaseTemplateEndpoint 4 | from .comment import CommentEndpoint 5 | from .cortex import CortexEndpoint 6 | from .custom_field import CustomFieldEndpoint 7 | from .observable import ObservableEndpoint 8 | from .organisation import OrganisationEndpoint 9 | from .procedure import ProcedureEndpoint 10 | from .profile import ProfileEndpoint 11 | from .task import TaskEndpoint 12 | from .task_log import TaskLogEndpoint 13 | from .timeline import TimelineEndpoint 14 | from .user import UserEndpoint 15 | -------------------------------------------------------------------------------- /thehive4py/endpoints/_base.py: -------------------------------------------------------------------------------- 1 | import json 2 | import mimetypes 3 | import os 4 | from typing import Optional 5 | 6 | from thehive4py.query import QueryExpr 7 | from thehive4py.query.filters import FilterExpr 8 | from thehive4py.query.page import Paginate 9 | from thehive4py.query.sort import SortExpr 10 | from thehive4py.session import TheHiveSession 11 | from thehive4py.types.observable import InputObservable 12 | 13 | 14 | class EndpointBase: 15 | def __init__(self, session: TheHiveSession): 16 | self._session = session 17 | 18 | def _fileinfo_from_filepath(self, filepath: str) -> tuple: 19 | filename = os.path.split(filepath)[1] 20 | mimetype = mimetypes.guess_type(filepath)[0] 21 | filestream = open(filepath, "rb") 22 | 23 | return (filename, filestream, mimetype) 24 | 25 | def _build_observable_kwargs( 26 | self, observable: InputObservable, observable_path: Optional[str] = None 27 | ) -> dict: 28 | if observable_path: 29 | kwargs = { 30 | "data": {"_json": json.dumps(observable)}, 31 | "files": {"attachment": self._fileinfo_from_filepath(observable_path)}, 32 | } 33 | else: 34 | kwargs = {"json": observable} 35 | 36 | return kwargs 37 | 38 | def _build_subquery( 39 | self, 40 | filters: Optional[FilterExpr] = None, 41 | sortby: Optional[SortExpr] = None, 42 | paginate: Optional[Paginate] = None, 43 | ) -> QueryExpr: 44 | subquery: QueryExpr = [] 45 | if filters: 46 | subquery = [*subquery, {"_name": "filter", **filters}] 47 | if sortby: 48 | subquery = [*subquery, {"_name": "sort", **sortby}] 49 | if paginate: 50 | subquery = [*subquery, {"_name": "page", **paginate}] 51 | 52 | return subquery 53 | -------------------------------------------------------------------------------- /thehive4py/endpoints/case_template.py: -------------------------------------------------------------------------------- 1 | from thehive4py.endpoints._base import EndpointBase 2 | from thehive4py.query import QueryExpr 3 | from thehive4py.query.filters import FilterExpr 4 | from thehive4py.query.page import Paginate 5 | from thehive4py.query.sort import SortExpr 6 | from thehive4py.types.case_template import OutputCaseTemplate, InputCaseTemplate 7 | from typing import List, Optional 8 | 9 | 10 | class CaseTemplateEndpoint(EndpointBase): 11 | def find( 12 | self, 13 | filters: Optional[FilterExpr] = None, 14 | sortby: Optional[SortExpr] = None, 15 | paginate: Optional[Paginate] = None, 16 | ) -> List[OutputCaseTemplate]: 17 | query: QueryExpr = [ 18 | {"_name": "listCaseTemplate"}, 19 | *self._build_subquery(filters=filters, sortby=sortby, paginate=paginate), 20 | ] 21 | 22 | return self._session.make_request( 23 | "POST", 24 | path="/api/v1/query", 25 | json={"query": query}, 26 | params={"name": "caseTemplate"}, 27 | ) 28 | 29 | def get(self, case_template_id: str) -> OutputCaseTemplate: 30 | return self._session.make_request( 31 | "GET", path=f"/api/v1/caseTemplate/{case_template_id}" 32 | ) 33 | 34 | def create(self, case_template: InputCaseTemplate) -> OutputCaseTemplate: 35 | return self._session.make_request( 36 | "POST", path="/api/v1/caseTemplate", json=case_template 37 | ) 38 | 39 | def delete(self, case_template_id: str) -> None: 40 | return self._session.make_request( 41 | "DELETE", path=f"/api/v1/caseTemplate/{case_template_id}" 42 | ) 43 | 44 | def update(self, case_template_id: str, fields: InputCaseTemplate) -> None: 45 | return self._session.make_request( 46 | "PATCH", path=f"/api/v1/caseTemplate/{case_template_id}", json=fields 47 | ) 48 | -------------------------------------------------------------------------------- /thehive4py/endpoints/comment.py: -------------------------------------------------------------------------------- 1 | from thehive4py.endpoints._base import EndpointBase 2 | from thehive4py.errors import TheHiveError 3 | from thehive4py.types.comment import InputComment, InputUpdateComment, OutputComment 4 | 5 | 6 | class CommentEndpoint(EndpointBase): 7 | def create_in_alert(self, alert_id: str, comment: InputComment) -> OutputComment: 8 | return self._session.make_request( 9 | "POST", path=f"/api/v1/alert/{alert_id}/comment", json=comment 10 | ) 11 | 12 | def create_in_case(self, case_id: str, comment: InputComment) -> OutputComment: 13 | return self._session.make_request( 14 | "POST", path=f"/api/v1/case/{case_id}/comment", json=comment 15 | ) 16 | 17 | def get(self, comment_id: str) -> OutputComment: 18 | # TODO: temp implementation until a dedicated get endpoint 19 | comments = self._session.make_request( 20 | "POST", 21 | path="/api/v1/query", 22 | json={"query": [{"_name": "getComment", "idOrName": comment_id}]}, 23 | ) 24 | try: 25 | return comments[0] 26 | except IndexError: 27 | raise TheHiveError("404 - Comment not found") 28 | 29 | def delete(self, comment_id: str) -> None: 30 | return self._session.make_request( 31 | "DELETE", path=f"/api/v1/comment/{comment_id}" 32 | ) 33 | 34 | def update(self, comment_id: str, fields: InputUpdateComment) -> None: 35 | return self._session.make_request( 36 | "PATCH", path=f"/api/v1/comment/{comment_id}", json=fields 37 | ) 38 | -------------------------------------------------------------------------------- /thehive4py/endpoints/cortex.py: -------------------------------------------------------------------------------- 1 | from thehive4py.endpoints._base import EndpointBase 2 | from thehive4py.types.cortex import ( 3 | OutputAnalyzer, 4 | OutputAnalyzerJob, 5 | OutputResponder, 6 | OutputResponderAction, 7 | InputResponderAction, 8 | InputAnalyzerJob, 9 | ) 10 | from typing import Optional, List 11 | 12 | 13 | class CortexEndpoint(EndpointBase): 14 | def create_analyzer_job(self, job: InputAnalyzerJob) -> OutputAnalyzerJob: 15 | return self._session.make_request( 16 | "POST", path="/api/connector/cortex/job", json=job 17 | ) 18 | 19 | def create_responder_action( 20 | self, action: InputResponderAction 21 | ) -> OutputResponderAction: 22 | return self._session.make_request( 23 | "POST", path="/api/connector/cortex/action", json=action 24 | ) 25 | 26 | def list_analyzers(self, range: Optional[str] = None) -> List[OutputAnalyzer]: 27 | params = {"range": range} 28 | return self._session.make_request( 29 | "GET", path="/api/connector/cortex/analyzer", params=params 30 | ) 31 | 32 | def list_analyzers_by_type(self, data_type: str) -> List[OutputAnalyzer]: 33 | return self._session.make_request( 34 | "GET", path=f"/api/connector/cortex/analyzer/type/{data_type}" 35 | ) 36 | 37 | def get_analyzer(self, analyzer_id: str) -> OutputAnalyzer: 38 | return self._session.make_request( 39 | "GET", path=f"/api/connector/cortex/analyzer/{analyzer_id}" 40 | ) 41 | 42 | def get_analyzer_job(self, job_id: str) -> OutputAnalyzerJob: 43 | return self._session.make_request( 44 | "GET", path=f"/api/connector/cortex/job/{job_id}" 45 | ) 46 | 47 | def list_responders( 48 | self, entity_type: str, entity_id: str 49 | ) -> List[OutputResponder]: 50 | return self._session.make_request( 51 | "GET", f"/api/connector/cortex/responder/{entity_type}/{entity_id}" 52 | ) 53 | -------------------------------------------------------------------------------- /thehive4py/endpoints/custom_field.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from thehive4py.endpoints._base import EndpointBase 4 | from thehive4py.types.custom_field import ( 5 | InputCustomField, 6 | InputUpdateCustomField, 7 | OutputCustomField, 8 | ) 9 | 10 | 11 | class CustomFieldEndpoint(EndpointBase): 12 | def create(self, custom_field: InputCustomField) -> OutputCustomField: 13 | return self._session.make_request( 14 | "POST", path="/api/v1/customField", json=custom_field 15 | ) 16 | 17 | def list(self) -> List[OutputCustomField]: 18 | return self._session.make_request("GET", path="/api/v1/customField") 19 | 20 | def delete(self, custom_field_id: str) -> None: 21 | return self._session.make_request( 22 | "DELETE", path=f"/api/v1/customField/{custom_field_id}" 23 | ) 24 | 25 | def update(self, custom_field_id: str, fields: InputUpdateCustomField) -> None: 26 | return self._session.make_request( 27 | "PATCH", path=f"/api/v1/customField/{custom_field_id}", json=fields 28 | ) 29 | -------------------------------------------------------------------------------- /thehive4py/endpoints/observable.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from thehive4py.endpoints._base import EndpointBase 4 | from thehive4py.query import QueryExpr 5 | from thehive4py.query.filters import FilterExpr 6 | from thehive4py.query.page import Paginate 7 | from thehive4py.query.sort import SortExpr 8 | from thehive4py.types.observable import ( 9 | InputBulkUpdateObservable, 10 | InputObservable, 11 | InputUpdateObservable, 12 | OutputObservable, 13 | ) 14 | from thehive4py.types.share import OutputShare 15 | 16 | 17 | class ObservableEndpoint(EndpointBase): 18 | def create_in_alert( 19 | self, 20 | alert_id: str, 21 | observable: InputObservable, 22 | observable_path: Optional[str] = None, 23 | ) -> List[OutputObservable]: 24 | kwargs = self._build_observable_kwargs( 25 | observable=observable, observable_path=observable_path 26 | ) 27 | return self._session.make_request( 28 | "POST", path=f"/api/v1/alert/{alert_id}/observable", **kwargs 29 | ) 30 | 31 | def create_in_case( 32 | self, 33 | case_id: str, 34 | observable: InputObservable, 35 | observable_path: Optional[str] = None, 36 | ) -> List[OutputObservable]: 37 | kwargs = self._build_observable_kwargs( 38 | observable=observable, observable_path=observable_path 39 | ) 40 | return self._session.make_request( 41 | "POST", path=f"/api/v1/case/{case_id}/observable", **kwargs 42 | ) 43 | 44 | def get(self, observable_id: str) -> OutputObservable: 45 | return self._session.make_request( 46 | "GET", path=f"/api/v1/observable/{observable_id}" 47 | ) 48 | 49 | def delete(self, observable_id: str) -> None: 50 | return self._session.make_request( 51 | "DELETE", path=f"/api/v1/observable/{observable_id}" 52 | ) 53 | 54 | def update(self, observable_id: str, fields: InputUpdateObservable) -> None: 55 | return self._session.make_request( 56 | "PATCH", path=f"/api/v1/observable/{observable_id}", json=fields 57 | ) 58 | 59 | def bulk_update(self, fields: InputBulkUpdateObservable) -> None: 60 | return self._session.make_request( 61 | "PATCH", path="/api/v1/observable/_bulk", json=fields 62 | ) 63 | 64 | def share(self, observable_id: str, organisations: List[str]) -> None: 65 | return self._session.make_request( 66 | "POST", 67 | path=f"/api/v1/observable/{observable_id}/shares", 68 | json={"organisations": organisations}, 69 | ) 70 | 71 | def unshare(self, observable_id: str, organisations: List[str]) -> None: 72 | return self._session.make_request( 73 | "DELETE", 74 | path=f"/api/v1/observable/{observable_id}/shares", 75 | json={"organisations": organisations}, 76 | ) 77 | 78 | def list_shares(self, observable_id: str) -> List[OutputShare]: 79 | return self._session.make_request( 80 | "GET", path=f"/api/v1/case/{observable_id}/shares" 81 | ) 82 | 83 | def find( 84 | self, 85 | filters: Optional[FilterExpr] = None, 86 | sortby: Optional[SortExpr] = None, 87 | paginate: Optional[Paginate] = None, 88 | ) -> List[OutputObservable]: 89 | query: QueryExpr = [ 90 | {"_name": "listObservable"}, 91 | *self._build_subquery(filters=filters, sortby=sortby, paginate=paginate), 92 | ] 93 | 94 | return self._session.make_request( 95 | "POST", 96 | path="/api/v1/query", 97 | params={"name": "observables"}, 98 | json={"query": query}, 99 | ) 100 | 101 | def count(self, filters: Optional[FilterExpr] = None) -> int: 102 | query: QueryExpr = [ 103 | {"_name": "listObservable"}, 104 | *self._build_subquery(filters=filters), 105 | {"_name": "count"}, 106 | ] 107 | 108 | return self._session.make_request( 109 | "POST", 110 | path="/api/v1/query", 111 | params={"name": "observable.count"}, 112 | json={"query": query}, 113 | ) 114 | 115 | def download_attachment( 116 | self, 117 | observable_id: str, 118 | attachment_id: str, 119 | observable_path: str, 120 | as_zip=False, 121 | ) -> None: 122 | return self._session.make_request( 123 | "GET", 124 | path=( 125 | f"/api/v1/observable/{observable_id}" 126 | f"/attachment/{attachment_id}/download" 127 | ), 128 | params={"asZip": as_zip}, 129 | download_path=observable_path, 130 | ) 131 | -------------------------------------------------------------------------------- /thehive4py/endpoints/observable_type.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from thehive4py.endpoints._base import EndpointBase 4 | from thehive4py.query import QueryExpr 5 | from thehive4py.types.observable_type import ( 6 | InputObservableType, 7 | OutputObservableType, 8 | ) 9 | from thehive4py.query.filters import FilterExpr 10 | from thehive4py.query.page import Paginate 11 | from thehive4py.query.sort import SortExpr 12 | 13 | 14 | class ObservableTypeEndpoint(EndpointBase): 15 | def create(self, observable_type: InputObservableType) -> OutputObservableType: 16 | return self._session.make_request( 17 | "POST", path="/api/v1/observable/type", json=observable_type 18 | ) 19 | 20 | def get(self, observable_type_id: str) -> OutputObservableType: 21 | return self._session.make_request( 22 | "GET", path=f"/api/v1/observable/type/{observable_type_id}" 23 | ) 24 | 25 | def delete(self, observable_type_id: str) -> None: 26 | return self._session.make_request( 27 | "DELETE", path=f"/api/v1/observable/type/{observable_type_id}" 28 | ) 29 | 30 | def find( 31 | self, 32 | filters: Optional[FilterExpr] = None, 33 | sortby: Optional[SortExpr] = None, 34 | paginate: Optional[Paginate] = None, 35 | ) -> List[OutputObservableType]: 36 | query: QueryExpr = [ 37 | {"_name": "listObservableType"}, 38 | *self._build_subquery(filters=filters, sortby=sortby, paginate=paginate), 39 | ] 40 | 41 | return self._session.make_request( 42 | "POST", 43 | path="/api/v1/query", 44 | params={"name": "observableTypes"}, 45 | json={"query": query}, 46 | ) 47 | -------------------------------------------------------------------------------- /thehive4py/endpoints/organisation.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from thehive4py.endpoints._base import EndpointBase 4 | from thehive4py.query import QueryExpr 5 | from thehive4py.query.filters import FilterExpr 6 | from thehive4py.query.page import Paginate 7 | from thehive4py.query.sort import SortExpr 8 | from thehive4py.types.attachment import OutputAttachment 9 | from thehive4py.types.organisation import ( 10 | InputBulkOrganisationLink, 11 | InputOrganisation, 12 | InputOrganisationLink, 13 | InputUpdateOrganisation, 14 | OutputOrganisation, 15 | OutputOrganisationLink, 16 | OutputSharingProfile, 17 | ) 18 | 19 | 20 | class OrganisationEndpoint(EndpointBase): 21 | def add_attachment( 22 | self, attachment_paths: List[str], can_rename: bool = True 23 | ) -> List[OutputAttachment]: 24 | """Add attachment to organisation. 25 | 26 | Args: 27 | attachment_paths: List of paths to the attachments to create. 28 | can_rename: If set to True, the files can be renamed if they already exist 29 | with the same name 30 | 31 | Returns: 32 | The created attachments. 33 | """ 34 | files = [ 35 | ("attachments", self._fileinfo_from_filepath(attachment_path)) 36 | for attachment_path in attachment_paths 37 | ] 38 | return self._session.make_request( 39 | "POST", "/api/v1/attachment", data={"canRename": can_rename}, files=files 40 | )["attachments"] 41 | 42 | def delete_attachment(self, attachment_id: str) -> None: 43 | """Delete an attachment. 44 | 45 | Args: 46 | attachment_id: The id of the attachment. 47 | 48 | Returns: 49 | N/A 50 | """ 51 | return self._session.make_request( 52 | "DELETE", path=f"/api/v1/attachment/{attachment_id}" 53 | ) 54 | 55 | def download_attachment(self, attachment_id: str, attachment_path: str) -> None: 56 | """Download an attachment. 57 | 58 | Args: 59 | attachment_id: The id of the attachment. 60 | attachment_path: The local path to download the attachment to. 61 | 62 | Returns: 63 | N/A 64 | """ 65 | return self._session.make_request( 66 | "GET", 67 | path=f"/api/v1/attachment/{attachment_id}/download", 68 | download_path=attachment_path, 69 | ) 70 | 71 | def find_attachments( 72 | self, 73 | org_id: str, 74 | filters: Optional[FilterExpr] = None, 75 | sortby: Optional[SortExpr] = None, 76 | paginate: Optional[Paginate] = None, 77 | ) -> List[OutputAttachment]: 78 | """Find attachments related to an organisation. 79 | 80 | Args: 81 | org_id: The id of the organisation. 82 | filters: The filter expressions to apply in the query. 83 | sortby: The sort expressions to apply in the query. 84 | paginate: The pagination experssion to apply in the query. 85 | 86 | Returns: 87 | The list of case attachments matched by the query or an empty list. 88 | """ 89 | query: QueryExpr = [ 90 | {"_name": "getOrganisation", "idOrName": org_id}, 91 | {"_name": "attachments"}, 92 | *self._build_subquery(filters=filters, sortby=sortby, paginate=paginate), 93 | ] 94 | return self._session.make_request( 95 | "POST", 96 | path="/api/v1/query", 97 | params={"name": "organisation-attachments"}, 98 | json={"query": query}, 99 | ) 100 | 101 | def create(self, organisation: InputOrganisation) -> OutputOrganisation: 102 | """Create an organisation. 103 | 104 | Args: 105 | organisation: The body of the organisation. 106 | 107 | Returns: 108 | The created organisation. 109 | """ 110 | return self._session.make_request( 111 | "POST", path="/api/v1/organisation", json=organisation 112 | ) 113 | 114 | def get(self, org_id: str) -> OutputOrganisation: 115 | """Get an organisation. 116 | 117 | Args: 118 | org_id: The id of the organisation. 119 | 120 | Returns: 121 | The organisation specified by the id. 122 | """ 123 | return self._session.make_request("GET", path=f"/api/v1/organisation/{org_id}") 124 | 125 | def update(self, org_id: str, fields: InputUpdateOrganisation) -> None: 126 | """Get an organisation. 127 | 128 | Args: 129 | org_id: The id of the organisation. 130 | fields: The fields of the organisation to update. 131 | 132 | Returns: 133 | N/A 134 | """ 135 | return self._session.make_request( 136 | "PATCH", path=f"/api/v1/organisation/{org_id}", json=fields 137 | ) 138 | 139 | def get_avatar(self, org_id: str, file_hash: str, avatar_path: str) -> None: 140 | """Get an organisaton avatar. 141 | 142 | Args: 143 | org_id: The id of the organisation. 144 | file_hash: The hash of the organisation avatar. 145 | avatar_path: The local path to download the organisation avatar to. 146 | 147 | Returns: 148 | N/A 149 | """ 150 | return self._session.make_request( 151 | "GET", 152 | path=f"/api/v1/organisation/{org_id}/avatar/{file_hash}", 153 | download_path=avatar_path, 154 | ) 155 | 156 | def link(self, org_id: str, other_org_id: str, link: InputOrganisationLink) -> None: 157 | """Link two organisatons. 158 | 159 | Args: 160 | org_id: The id of the organisation. 161 | other_org_id: The id of the other organisation. 162 | link: The type of organisation links. 163 | 164 | Returns: 165 | N/A 166 | """ 167 | return self._session.make_request( 168 | "PUT", path=f"/api/v1/organisation/{org_id}/link/{other_org_id}", json=link 169 | ) 170 | 171 | def unlink(self, org_id: str, other_org_id: str) -> None: 172 | """Unlink two organisatons. 173 | 174 | Args: 175 | org_id: The id of the organisation. 176 | other_org_id: The id of the other organisation. 177 | 178 | Returns: 179 | N/A 180 | """ 181 | return self._session.make_request( 182 | "DELETE", path=f"/api/v1/organisation/{org_id}/link/{other_org_id}" 183 | ) 184 | 185 | def list_links(self, org_id: str) -> List[OutputOrganisationLink]: 186 | """List links of an organisatons. 187 | 188 | Args: 189 | org_id: The id of the organisation. 190 | 191 | Returns: 192 | The list of organisation links. 193 | """ 194 | return self._session.make_request( 195 | "GET", path=f"/api/v1/organisation/{org_id}/links" 196 | ) 197 | 198 | def bulk_link(self, org_id: str, links: List[InputBulkOrganisationLink]) -> None: 199 | """Bulk link organisations. 200 | 201 | Args: 202 | org_id: The id of the organisation. 203 | links: The list of organisation links. 204 | 205 | Returns: 206 | N/A 207 | """ 208 | return self._session.make_request( 209 | "PUT", path=f"/api/v1/organisation/{org_id}/links", json={"links": links} 210 | ) 211 | 212 | def list_sharing_profiles(self) -> List[OutputSharingProfile]: 213 | """List all sharing profiles. 214 | 215 | Returns: 216 | The list of sharing profiles. 217 | """ 218 | return self._session.make_request("GET", path="/api/v1/sharingProfile") 219 | 220 | def find( 221 | self, 222 | filters: Optional[FilterExpr] = None, 223 | sortby: Optional[SortExpr] = None, 224 | paginate: Optional[Paginate] = None, 225 | ) -> List[OutputOrganisation]: 226 | """Find multiple organisations. 227 | 228 | Args: 229 | filters: The filter expressions to apply in the query. 230 | sortby: The sort expressions to apply in the query. 231 | paginate: The pagination experssion to apply in the query. 232 | 233 | Returns: 234 | The list of organisations matched by the query or an empty list. 235 | """ 236 | query: QueryExpr = [ 237 | {"_name": "listOrganisation"}, 238 | *self._build_subquery(filters=filters, sortby=sortby, paginate=paginate), 239 | ] 240 | 241 | return self._session.make_request( 242 | "POST", 243 | path="/api/v1/query", 244 | params={"name": "organisations"}, 245 | json={"query": query}, 246 | ) 247 | 248 | def count(self, filters: Optional[FilterExpr] = None) -> int: 249 | """Count organisations. 250 | 251 | Args: 252 | filters: The filter expressions to apply in the query. 253 | 254 | Returns: 255 | The count of organisations matched by the query. 256 | """ 257 | query: QueryExpr = [ 258 | {"_name": "listOrganisation"}, 259 | *self._build_subquery(filters=filters), 260 | {"_name": "count"}, 261 | ] 262 | 263 | return self._session.make_request( 264 | "POST", 265 | path="/api/v1/query", 266 | params={"name": "organisations.count"}, 267 | json={"query": query}, 268 | ) 269 | -------------------------------------------------------------------------------- /thehive4py/endpoints/procedure.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from thehive4py.endpoints._base import EndpointBase 4 | from thehive4py.errors import TheHiveError 5 | from thehive4py.query import QueryExpr 6 | from thehive4py.query.filters import FilterExpr 7 | from thehive4py.query.page import Paginate 8 | from thehive4py.query.sort import SortExpr 9 | from thehive4py.types.procedure import ( 10 | InputProcedure, 11 | InputUpdateProcedure, 12 | OutputProcedure, 13 | ) 14 | 15 | 16 | class ProcedureEndpoint(EndpointBase): 17 | def create_in_alert( 18 | self, alert_id: str, procedure: InputProcedure 19 | ) -> OutputProcedure: 20 | return self._session.make_request( 21 | "POST", path=f"/api/v1/alert/{alert_id}/procedure", json=procedure 22 | ) 23 | 24 | def create_in_case( 25 | self, case_id: str, procedure: InputProcedure 26 | ) -> OutputProcedure: 27 | return self._session.make_request( 28 | "POST", path=f"/api/v1/case/{case_id}/procedure", json=procedure 29 | ) 30 | 31 | def get(self, procedure_id: str) -> OutputProcedure: 32 | # TODO: temp implementation until a dedicated get endpoint 33 | procedures = self._session.make_request( 34 | "POST", 35 | path="/api/v1/query", 36 | json={"query": [{"_name": "getProcedure", "idOrName": procedure_id}]}, 37 | ) 38 | try: 39 | return procedures[0] 40 | except IndexError: 41 | raise TheHiveError("404 - Procedure not found") 42 | 43 | def delete(self, procedure_id: str) -> None: 44 | return self._session.make_request( 45 | "DELETE", path=f"/api/v1/procedure/{procedure_id}" 46 | ) 47 | 48 | def update(self, procedure_id: str, fields: InputUpdateProcedure) -> None: 49 | return self._session.make_request( 50 | "PATCH", path=f"/api/v1/procedure/{procedure_id}", json=fields 51 | ) 52 | 53 | def find( 54 | self, 55 | filters: Optional[FilterExpr] = None, 56 | sortby: Optional[SortExpr] = None, 57 | paginate: Optional[Paginate] = None, 58 | ) -> List[OutputProcedure]: 59 | query: QueryExpr = [ 60 | {"_name": "listProcedure"}, 61 | *self._build_subquery(filters=filters, sortby=sortby, paginate=paginate), 62 | ] 63 | 64 | return self._session.make_request( 65 | "POST", 66 | path="/api/v1/query", 67 | params={"name": "procedures"}, 68 | json={"query": query}, 69 | ) 70 | -------------------------------------------------------------------------------- /thehive4py/endpoints/profile.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from thehive4py.endpoints._base import EndpointBase 4 | from thehive4py.query import QueryExpr 5 | from thehive4py.query.filters import FilterExpr 6 | from thehive4py.query.page import Paginate 7 | from thehive4py.query.sort import SortExpr 8 | from thehive4py.types.profile import InputProfile, InputUpdateProfile, OutputProfile 9 | 10 | 11 | class ProfileEndpoint(EndpointBase): 12 | def create(self, profile: InputProfile) -> OutputProfile: 13 | return self._session.make_request("POST", path="/api/v1/profile", json=profile) 14 | 15 | def get(self, profile_id: str) -> OutputProfile: 16 | return self._session.make_request("GET", path=f"/api/v1/profile/{profile_id}") 17 | 18 | def delete(self, profile_id: str) -> None: 19 | return self._session.make_request("DELETE", f"/api/v1/profile/{profile_id}") 20 | 21 | def update(self, profile_id: str, fields: InputUpdateProfile) -> None: 22 | return self._session.make_request( 23 | "PATCH", f"/api/v1/profile/{profile_id}", json=fields 24 | ) 25 | 26 | def find( 27 | self, 28 | filters: Optional[FilterExpr] = None, 29 | sortby: Optional[SortExpr] = None, 30 | paginate: Optional[Paginate] = None, 31 | ) -> List[OutputProfile]: 32 | query: QueryExpr = [ 33 | {"_name": "listProfile"}, 34 | *self._build_subquery(filters=filters, sortby=sortby, paginate=paginate), 35 | ] 36 | 37 | return self._session.make_request( 38 | "POST", 39 | path="/api/v1/query", 40 | params={"name": "profiles"}, 41 | json={"query": query}, 42 | ) 43 | 44 | def count(self, filters: Optional[FilterExpr] = None) -> int: 45 | query: QueryExpr = [ 46 | {"_name": "listProfile"}, 47 | *self._build_subquery(filters=filters), 48 | {"_name": "count"}, 49 | ] 50 | 51 | return self._session.make_request( 52 | "POST", 53 | path="/api/v1/query", 54 | params={"name": "profile.count"}, 55 | json={"query": query}, 56 | ) 57 | -------------------------------------------------------------------------------- /thehive4py/endpoints/query.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from thehive4py.endpoints._base import EndpointBase 4 | 5 | 6 | class QueryEndpoint(EndpointBase): 7 | def run( 8 | self, query: typing.List[dict], exclude_fields: typing.List[str] = [] 9 | ) -> typing.Any: 10 | return self._session.make_request( 11 | "POST", 12 | path="/api/v1/query", 13 | json={"query": query, "excludeFields": exclude_fields}, 14 | ) 15 | -------------------------------------------------------------------------------- /thehive4py/endpoints/task.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from thehive4py.endpoints._base import EndpointBase 4 | from thehive4py.query import QueryExpr 5 | from thehive4py.query.filters import FilterExpr 6 | from thehive4py.query.page import Paginate 7 | from thehive4py.query.sort import SortExpr 8 | from thehive4py.types.task import ( 9 | InputBulkUpdateTask, 10 | InputTask, 11 | InputUpdateTask, 12 | OutputTask, 13 | ) 14 | from thehive4py.types.task_log import InputTaskLog, OutputTaskLog 15 | 16 | 17 | class TaskEndpoint(EndpointBase): 18 | def create(self, case_id: str, task: InputTask) -> OutputTask: 19 | return self._session.make_request( 20 | "POST", path=f"/api/v1/case/{case_id}/task", json=task 21 | ) 22 | 23 | def get(self, task_id: str) -> OutputTask: 24 | return self._session.make_request("GET", path=f"/api/v1/task/{task_id}") 25 | 26 | def delete(self, task_id: str) -> None: 27 | return self._session.make_request("DELETE", path=f"/api/v1/task/{task_id}") 28 | 29 | def update(self, task_id: str, fields: InputUpdateTask) -> None: 30 | return self._session.make_request( 31 | "PATCH", path=f"/api/v1/task/{task_id}", json=fields 32 | ) 33 | 34 | def bulk_update(self, fields: InputBulkUpdateTask) -> None: 35 | return self._session.make_request( 36 | "PATCH", path="/api/v1/task/_bulk", json=fields 37 | ) 38 | 39 | def get_required_actions(self, task_id: str) -> dict: 40 | return self._session.make_request( 41 | "GET", path=f"/api/v1/task/{task_id}/actionRequired" 42 | ) 43 | 44 | def set_as_required(self, task_id: str, org_id: str) -> None: 45 | return self._session.make_request( 46 | "PUT", f"/api/v1/task/{task_id}/actionRequired/{org_id}" 47 | ) 48 | 49 | def set_as_done(self, task_id: str, org_id: str) -> None: 50 | return self._session.make_request( 51 | "PUT", f"/api/v1/task/{task_id}/actionDone/{org_id}" 52 | ) 53 | 54 | def share(self): 55 | raise NotImplementedError() 56 | 57 | def list_shares(self): 58 | raise NotImplementedError() 59 | 60 | def unshare(self): 61 | raise NotImplementedError() 62 | 63 | def find( 64 | self, 65 | filters: Optional[FilterExpr] = None, 66 | sortby: Optional[SortExpr] = None, 67 | paginate: Optional[Paginate] = None, 68 | ) -> List[OutputTask]: 69 | query: QueryExpr = [ 70 | {"_name": "listTask"}, 71 | *self._build_subquery(filters=filters, sortby=sortby, paginate=paginate), 72 | ] 73 | 74 | return self._session.make_request( 75 | "POST", 76 | path="/api/v1/query", 77 | params={"name": "tasks"}, 78 | json={"query": query}, 79 | ) 80 | 81 | def count(self, filters: Optional[FilterExpr] = None) -> int: 82 | query: QueryExpr = [ 83 | {"_name": "listTask"}, 84 | *self._build_subquery(filters=filters), 85 | {"_name": "count"}, 86 | ] 87 | 88 | return self._session.make_request( 89 | "POST", 90 | path="/api/v1/query", 91 | params={"name": "task.count"}, 92 | json={"query": query}, 93 | ) 94 | 95 | def create_log(self, task_id: str, task_log: InputTaskLog) -> OutputTaskLog: 96 | return self._session.make_request( 97 | "POST", path=f"/api/v1/task/{task_id}/log", json=task_log 98 | ) 99 | 100 | def find_logs( 101 | self, 102 | task_id: str, 103 | filters: Optional[FilterExpr] = None, 104 | sortby: Optional[SortExpr] = None, 105 | paginate: Optional[Paginate] = None, 106 | ) -> List[OutputTaskLog]: 107 | query: QueryExpr = [ 108 | {"_name": "getTask", "idOrName": task_id}, 109 | {"_name": "logs"}, 110 | *self._build_subquery(filters=filters, sortby=sortby, paginate=paginate), 111 | ] 112 | 113 | return self._session.make_request( 114 | "POST", 115 | path="/api/v1/query", 116 | params={"name": "case-task-logs"}, 117 | json={"query": query}, 118 | ) 119 | -------------------------------------------------------------------------------- /thehive4py/endpoints/task_log.py: -------------------------------------------------------------------------------- 1 | import json as jsonlib 2 | import warnings 3 | from typing import List 4 | 5 | from thehive4py.endpoints._base import EndpointBase 6 | from thehive4py.errors import TheHiveError 7 | from thehive4py.types.task_log import InputTaskLog, InputUpdateTaskLog, OutputTaskLog 8 | 9 | 10 | class TaskLogEndpoint(EndpointBase): 11 | def create( 12 | self, 13 | task_id: str, 14 | task_log: InputTaskLog, 15 | ) -> OutputTaskLog: 16 | """Create a task log. 17 | 18 | Args: 19 | task_id: The id of the task to create the log in. 20 | task_log: The body of the task log. 21 | 22 | Returns: 23 | The created task_log. 24 | """ 25 | if "attachments" in task_log: 26 | files: List[tuple] = [ 27 | ("attachments", self._fileinfo_from_filepath(attachment_path)) 28 | for attachment_path in task_log["attachments"] 29 | ] 30 | files.append(("_json", jsonlib.dumps(task_log))) 31 | kwargs: dict = {"files": files} 32 | else: 33 | kwargs = {"json": task_log} 34 | return self._session.make_request( 35 | "POST", path=f"/api/v1/task/{task_id}/log", **kwargs 36 | ) 37 | 38 | def delete(self, task_log_id: str) -> None: 39 | """Delete a task log. 40 | 41 | Args: 42 | task_log_id: The id of the task log. 43 | 44 | Returns: 45 | N/A 46 | """ 47 | return self._session.make_request("DELETE", path=f"/api/v1/log/{task_log_id}") 48 | 49 | def update(self, task_log_id: str, fields: InputUpdateTaskLog) -> None: 50 | """Update a task log. 51 | 52 | Args: 53 | task_log_id: The id of the task log. 54 | fields: The fields of the task log to update. 55 | 56 | Returns: 57 | N/A 58 | """ 59 | return self._session.make_request( 60 | "PATCH", path=f"/api/v1/log/{task_log_id}", json=fields 61 | ) 62 | 63 | def add_attachment(self, task_log_id: str, attachment_paths: List[str]) -> None: 64 | """Add attachments to a task log. 65 | 66 | Args: 67 | task_log_id: The id of the task log. 68 | attachment_paths: List of paths to the attachments to create. 69 | 70 | Returns: 71 | The created task log attachments. 72 | """ 73 | files = [ 74 | ("attachments", self._fileinfo_from_filepath(attachment_path)) 75 | for attachment_path in attachment_paths 76 | ] 77 | return self._session.make_request( 78 | "POST", f"/api/v1/log/{task_log_id}/attachments", files=files 79 | ) 80 | 81 | def add_attachments(self, task_log_id: str, attachment_paths: List[str]) -> None: 82 | """Add attachments to a task log. 83 | 84 | !!! warning 85 | Deprecated: use [task_log.add_attachment] 86 | [thehive4py.endpoints.task_log.TaskLogEndpoint.add_attachment] 87 | instead 88 | 89 | Args: 90 | task_log_id: The id of the task log. 91 | attachment_paths: List of paths to the attachments to create. 92 | 93 | Returns: 94 | The created task log attachments. 95 | """ 96 | warnings.warn( 97 | message=("Deprecated: use the task_log.add_attachment method instead"), 98 | category=DeprecationWarning, 99 | stacklevel=2, 100 | ) 101 | files = [ 102 | ("attachments", self._fileinfo_from_filepath(attachment_path)) 103 | for attachment_path in attachment_paths 104 | ] 105 | return self._session.make_request( 106 | "POST", f"/api/v1/log/{task_log_id}/attachments", files=files 107 | ) 108 | 109 | def delete_attachment(self, task_log_id: str, attachment_id: str) -> None: 110 | """Delete a task log attachment. 111 | 112 | Args: 113 | task_log_id: The id of the task log. 114 | attachment_id: The id of the task log attachment. 115 | 116 | Returns: 117 | N/A 118 | """ 119 | return self._session.make_request( 120 | "DELETE", f"/api/v1/log/{task_log_id}/attachments/{attachment_id}" 121 | ) 122 | 123 | def get(self, task_log_id: str) -> OutputTaskLog: 124 | """Get a task log by id. 125 | 126 | Args: 127 | task_log_id: The id of the task log. 128 | 129 | Returns: 130 | The task log specified by the id. 131 | """ 132 | # TODO: temp implementation until a dedicated get endpoint [if ever ;)] 133 | logs = self._session.make_request( 134 | "POST", 135 | path="/api/v1/query", 136 | json={"query": [{"_name": "getLog", "idOrName": task_log_id}]}, 137 | ) 138 | 139 | try: 140 | return logs[0] 141 | except IndexError: 142 | raise TheHiveError("404 - Task Log Not Found") 143 | -------------------------------------------------------------------------------- /thehive4py/endpoints/timeline.py: -------------------------------------------------------------------------------- 1 | from thehive4py.endpoints._base import EndpointBase 2 | from thehive4py.types.timeline import ( 3 | InputCustomEvent, 4 | InputUpdateCustomEvent, 5 | OutputCustomEvent, 6 | OutputTimeline, 7 | ) 8 | 9 | 10 | class TimelineEndpoint(EndpointBase): 11 | def get(self, case_id: str) -> OutputTimeline: 12 | return self._session.make_request("GET", f"/api/v1/case/{case_id}/timeline") 13 | 14 | def create_event(self, case_id: str, event: InputCustomEvent) -> OutputCustomEvent: 15 | return self._session.make_request( 16 | "POST", path=f"/api/v1/case/{case_id}/customEvent", json=event 17 | ) 18 | 19 | def delete_event(self, event_id: str) -> None: 20 | return self._session.make_request( 21 | "DELETE", path=f"/api/v1/customEvent/{event_id}" 22 | ) 23 | 24 | def update_event(self, event_id: str, fields: InputUpdateCustomEvent) -> None: 25 | return self._session.make_request( 26 | "PATCH", path=f"/api/v1/customEvent/{event_id}", json=fields 27 | ) 28 | -------------------------------------------------------------------------------- /thehive4py/endpoints/user.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from thehive4py.endpoints._base import EndpointBase 4 | from thehive4py.query import QueryExpr 5 | from thehive4py.query.filters import FilterExpr 6 | from thehive4py.query.page import Paginate 7 | from thehive4py.query.sort import SortExpr 8 | from thehive4py.types.user import ( 9 | InputUpdateUser, 10 | InputUser, 11 | InputUserOrganisation, 12 | OutputUser, 13 | OutputUserOrganisation, 14 | ) 15 | 16 | 17 | class UserEndpoint(EndpointBase): 18 | def create(self, user: InputUser) -> OutputUser: 19 | return self._session.make_request("POST", path="/api/v1/user", json=user) 20 | 21 | def get(self, user_id: str) -> OutputUser: 22 | return self._session.make_request("GET", path=f"/api/v1/user/{user_id}") 23 | 24 | def get_current(self) -> OutputUser: 25 | return self._session.make_request("GET", path="/api/v1/user/current") 26 | 27 | def delete(self, user_id: str, organisation: Optional[str] = None) -> None: 28 | return self._session.make_request( 29 | "DELETE", 30 | path=f"/api/v1/user/{user_id}/force", 31 | params={"organisation": organisation}, 32 | ) 33 | 34 | def update(self, user_id: str, fields: InputUpdateUser) -> None: 35 | return self._session.make_request( 36 | "PATCH", path=f"/api/v1/user/{user_id}", json=fields 37 | ) 38 | 39 | def lock(self, user_id: str) -> None: 40 | return self.update(user_id=user_id, fields={"locked": True}) 41 | 42 | def unlock(self, user_id: str) -> None: 43 | return self.update(user_id=user_id, fields={"locked": False}) 44 | 45 | def set_organisations( 46 | self, user_id: str, organisations: List[InputUserOrganisation] 47 | ) -> List[OutputUserOrganisation]: 48 | return self._session.make_request( 49 | "PUT", 50 | path=f"/api/v1/user/{user_id}/organisations", 51 | json={"organisations": organisations}, 52 | )["organisations"] 53 | 54 | def set_password(self, user_id: str, password: str) -> None: 55 | return self._session.make_request( 56 | "POST", 57 | path=f"/api/v1/user/{user_id}/password/set", 58 | json={"password": password}, 59 | ) 60 | 61 | def get_apikey(self, user_id: str) -> str: 62 | return self._session.make_request("GET", path=f"/api/v1/user/{user_id}/key") 63 | 64 | def remove_apikey(self, user_id: str) -> None: 65 | return self._session.make_request("DELETE", path=f"/api/v1/user/{user_id}/key") 66 | 67 | def renew_apikey(self, user_id: str) -> str: 68 | return self._session.make_request( 69 | "POST", path=f"/api/v1/user/{user_id}/key/renew" 70 | ) 71 | 72 | def get_avatar(self, user_id: str): 73 | # TODO: implement the avatar download 74 | raise NotImplementedError() 75 | 76 | def find( 77 | self, 78 | filters: Optional[FilterExpr] = None, 79 | sortby: Optional[SortExpr] = None, 80 | paginate: Optional[Paginate] = None, 81 | ) -> List[OutputUser]: 82 | query: QueryExpr = [ 83 | {"_name": "listUser"}, 84 | *self._build_subquery(filters=filters, sortby=sortby, paginate=paginate), 85 | ] 86 | 87 | return self._session.make_request( 88 | "POST", 89 | path="/api/v1/query", 90 | params={"name": "users"}, 91 | json={"query": query}, 92 | ) 93 | 94 | def count(self, filters: Optional[FilterExpr] = None) -> int: 95 | query: QueryExpr = [ 96 | {"_name": "listUser"}, 97 | *self._build_subquery(filters=filters), 98 | {"_name": "count"}, 99 | ] 100 | 101 | return self._session.make_request( 102 | "POST", 103 | path="/api/v1/query", 104 | params={"name": "cases.count"}, 105 | json={"query": query}, 106 | ) 107 | -------------------------------------------------------------------------------- /thehive4py/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from requests import Response 4 | 5 | 6 | class TheHiveError(Exception): 7 | 8 | def __init__( 9 | self, message: str, response: Optional[Response] = None, *args, **kwargs 10 | ): 11 | """Base error class of thehive4py. 12 | 13 | Args: 14 | message: The exception message. 15 | response: Either `None`, or a `Response` object of a failed request. 16 | """ 17 | super().__init__(message, *args, **kwargs) 18 | self.message = message 19 | self.response = response 20 | -------------------------------------------------------------------------------- /thehive4py/helpers.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import time 3 | from typing import Optional 4 | 5 | 6 | def now_to_ts() -> int: 7 | """Return now as TheHive timestamp.""" 8 | return int(time.time() * 1000) 9 | 10 | 11 | def dt_to_ts(datetime: dt.datetime) -> int: 12 | """Convert datetime object to TheHive timestamp.""" 13 | return int(datetime.timestamp() * 1000) 14 | 15 | 16 | def ts_to_dt(timestamp: int, tz: Optional[dt.timezone] = None) -> dt.datetime: 17 | """Convert TheHive timestamp to datetime object.""" 18 | return dt.datetime.fromtimestamp(timestamp / 1000.0, tz=tz) 19 | -------------------------------------------------------------------------------- /thehive4py/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/TheHive4py/62a3f9714f9c27eb55017fab79ccd00ea8f593f0/thehive4py/py.typed -------------------------------------------------------------------------------- /thehive4py/query/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | 3 | from .filters import Between, Contains, EndsWith, Eq 4 | from .filters import FilterExpr as _FilterExpr 5 | from .filters import Gt, Gte, Has, Id, In, Like, Lt, Lte, Match, Ne, StartsWith 6 | from .page import Paginate 7 | from .sort import Asc, Desc 8 | from .sort import SortExpr as _SortExpr 9 | 10 | QueryExpr = List[Union[_FilterExpr, _SortExpr, Paginate, dict]] 11 | -------------------------------------------------------------------------------- /thehive4py/query/filters.py: -------------------------------------------------------------------------------- 1 | from collections import UserDict as _UserDict 2 | from typing import Any as _Any 3 | from typing import Union as _Union 4 | import warnings 5 | 6 | FilterExpr = _Union["_FilterBase", dict] 7 | 8 | 9 | class _FilterBase(_UserDict): 10 | """Base class for filters.""" 11 | 12 | def __and__(self, other: "_FilterBase") -> "_FilterBase": 13 | if not isinstance(other, _FilterBase): 14 | self._raise_type_error("&", self, other) 15 | args = self.get("_and", [self]) + other.get("_and", [other]) 16 | return _FilterBase(_and=args) 17 | 18 | def __or__(self, other: "_FilterBase") -> "_FilterBase": # type:ignore 19 | if not isinstance(other, _FilterBase): 20 | self._raise_type_error("|", self, other) 21 | args = self.get("_or", [self]) + other.get("_or", [other]) 22 | return _FilterBase(_or=args) 23 | 24 | def __invert__(self) -> "_FilterBase": 25 | return _FilterBase(_not=self) 26 | 27 | def _raise_type_error(self, operand, first, second): 28 | raise TypeError( 29 | f"unsupported operand type(s) for {operand}: " 30 | f"{type(first)} and {type(second)}" 31 | ) 32 | 33 | 34 | class Lt(_FilterBase): 35 | """Field less than value.""" 36 | 37 | def __init__(self, field: str, value: _Any): 38 | super().__init__(_lt={"_field": field, "_value": value}) 39 | 40 | 41 | class Gt(_FilterBase): 42 | """Field greater than value.""" 43 | 44 | def __init__(self, field: str, value: _Any): 45 | super().__init__(_gt={"_field": field, "_value": value}) 46 | 47 | 48 | class Lte(_FilterBase): 49 | """Field less than or equal value.""" 50 | 51 | def __init__(self, field: str, value: _Any): 52 | super().__init__(_lte={"_field": field, "_value": value}) 53 | 54 | 55 | class Gte(_FilterBase): 56 | """Field less than or equal value.""" 57 | 58 | def __init__(self, field: str, value: _Any): 59 | super().__init__(_gte={"_field": field, "_value": value}) 60 | 61 | 62 | class Ne(_FilterBase): 63 | """Field not equal value.""" 64 | 65 | def __init__(self, field: str, value: _Any): 66 | super().__init__(_ne={"_field": field, "_value": value}) 67 | 68 | 69 | class Eq(_FilterBase): 70 | """Field equal value.""" 71 | 72 | def __init__(self, field: str, value: _Any): 73 | super().__init__(_eq={"_field": field, "_value": value}) 74 | 75 | 76 | class StartsWith(_FilterBase): 77 | """Field starts with value.""" 78 | 79 | def __init__(self, field: str, value: str): 80 | super().__init__(_startsWith={"_field": field, "_value": value}) 81 | 82 | 83 | class EndsWith(_FilterBase): 84 | """Field ends with value.""" 85 | 86 | def __init__(self, field: str, value: str): 87 | super().__init__(_endsWith={"_field": field, "_value": value}) 88 | 89 | 90 | class Id(_FilterBase): 91 | """FIlter by ID.""" 92 | 93 | def __init__(self, id: str): 94 | super().__init__(_id=id) 95 | 96 | 97 | class Between(_FilterBase): 98 | """Field between inclusive from and exclusive to values.""" 99 | 100 | def __init__(self, field: str, start: int, end: int): 101 | super().__init__(_between={"_field": field, "_from": start, "_to": end}) 102 | 103 | 104 | class In(_FilterBase): 105 | """Field is one of the values.""" 106 | 107 | def __init__(self, field: _Any, values: list): 108 | super().__init__(_in={"_field": field, "_values": values}) 109 | 110 | 111 | class Contains(_FilterBase): 112 | """Object contains the field.""" 113 | 114 | def __init__(self, field: str): 115 | warnings.warn( 116 | message="The `Contains` filter has been deprecated. " 117 | "Please use the `Has` filter to prevent breaking " 118 | "changes in the future.", 119 | category=DeprecationWarning, 120 | stacklevel=2, 121 | ) 122 | super().__init__(_contains=field) 123 | 124 | 125 | class Has(_FilterBase): 126 | """Object contains the field.""" 127 | 128 | def __init__(self, field: str): 129 | super().__init__(_has=field) 130 | 131 | 132 | class Like(_FilterBase): 133 | """Field contains the value.""" 134 | 135 | def __init__(self, field: str, value: str): 136 | super().__init__(_like={"_field": field, "_value": value}) 137 | 138 | 139 | class Match(_FilterBase): 140 | """Field contains the value""" 141 | 142 | def __init__(self, field: str, value: str): 143 | super().__init__(_match={"_field": field, "_value": value}) 144 | -------------------------------------------------------------------------------- /thehive4py/query/page.py: -------------------------------------------------------------------------------- 1 | from collections import UserDict 2 | 3 | 4 | class Paginate(UserDict): 5 | def __init__(self, start: int, end: int, extra_data=[]): 6 | super().__init__({"from": start, "to": end, "extraData": extra_data}) 7 | -------------------------------------------------------------------------------- /thehive4py/query/sort.py: -------------------------------------------------------------------------------- 1 | from collections import UserDict 2 | 3 | 4 | class SortExpr(UserDict): 5 | """Base class for sort expressions.""" 6 | 7 | def __and__(self, other: "SortExpr") -> "SortExpr": 8 | return self._concat_expressions("&", self, other) 9 | 10 | def __or__(self, other: "SortExpr") -> "SortExpr": # type:ignore 11 | return self._concat_expressions("|", self, other) 12 | 13 | def _concat_expressions( 14 | self, operand: str, expr1: "SortExpr", expr2: "SortExpr" 15 | ) -> "SortExpr": 16 | if not isinstance(expr1, SortExpr) or not isinstance(expr2, SortExpr): 17 | self._raise_type_error(operand, expr1, expr2) 18 | return SortExpr(_fields=[*expr1["_fields"], *expr2["_fields"]]) 19 | 20 | def _raise_type_error(self, operand, first, second): 21 | raise TypeError( 22 | f"unsupported operand type(s) for {operand}: " 23 | f"{type(first)} and {type(second)}" 24 | ) 25 | 26 | 27 | class Asc(SortExpr): 28 | def __init__(self, field: str): 29 | super().__init__(_fields=[{field: "asc"}]) 30 | 31 | 32 | class Desc(SortExpr): 33 | def __init__(self, field: str): 34 | super().__init__(_fields=[{field: "desc"}]) 35 | -------------------------------------------------------------------------------- /thehive4py/session.py: -------------------------------------------------------------------------------- 1 | import json as jsonlib 2 | from collections import UserDict 3 | from os import PathLike 4 | from typing import Any, Optional, Union 5 | 6 | import requests 7 | import requests.adapters 8 | import requests.auth 9 | from urllib3 import Retry 10 | 11 | from thehive4py.__version__ import __version__ 12 | from thehive4py.errors import TheHiveError 13 | 14 | DEFAULT_RETRY = Retry( 15 | total=5, 16 | backoff_factor=1, 17 | status_forcelist=[500, 502, 503, 504], 18 | allowed_methods=["GET", "POST", "PUT", "PATCH", "DELETE"], 19 | raise_on_status=False, 20 | ) 21 | 22 | 23 | RetryValue = Union[Retry, int, None] 24 | VerifyValue = Union[bool, str] 25 | 26 | 27 | class _SessionJSONEncoder(jsonlib.JSONEncoder): 28 | """Custom JSON encoder class for TheHive session.""" 29 | 30 | def default(self, o: Any): 31 | if isinstance(o, UserDict): 32 | return o.data 33 | return super().default(o) 34 | 35 | 36 | class TheHiveSession(requests.Session): 37 | def __init__( 38 | self, 39 | url: str, 40 | apikey: Optional[str] = None, 41 | username: Optional[str] = None, 42 | password: Optional[str] = None, 43 | verify: VerifyValue = True, 44 | max_retries: RetryValue = DEFAULT_RETRY, 45 | ): 46 | super().__init__() 47 | self.hive_url = self._sanitize_hive_url(url) 48 | self.verify = verify 49 | self.headers["User-Agent"] = f"thehive4py/{__version__}" 50 | self._set_retries(max_retries=max_retries) 51 | 52 | if username and password: 53 | self.headers["Authorization"] = requests.auth._basic_auth_str( 54 | username, password 55 | ) 56 | elif apikey: 57 | self.headers["Authorization"] = f"Bearer {apikey}" 58 | else: 59 | raise TheHiveError( 60 | "Either apikey or the username/password combination must be provided!" 61 | ) 62 | 63 | def _set_retries(self, max_retries: RetryValue): 64 | """Configure the session to retry.""" 65 | retry_adapter = requests.adapters.HTTPAdapter(max_retries=max_retries) 66 | self.mount("http://", retry_adapter) 67 | self.mount("https://", retry_adapter) 68 | 69 | def _sanitize_hive_url(self, hive_url: str) -> str: 70 | """Sanitize the base url for the client.""" 71 | if hive_url.endswith("/"): 72 | return hive_url[:-1] 73 | return hive_url 74 | 75 | def make_request( 76 | self, 77 | method: str, 78 | path: str, 79 | params=None, 80 | data=None, 81 | json=None, 82 | files=None, 83 | download_path: Union[str, PathLike, None] = None, 84 | ) -> Any: 85 | endpoint_url = f"{self.hive_url}{path}" 86 | 87 | headers = {**self.headers} 88 | if json is not None: 89 | data = jsonlib.dumps(json, cls=_SessionJSONEncoder) 90 | headers = {**headers, "Content-Type": "application/json"} 91 | 92 | response = self.request( 93 | method, 94 | url=endpoint_url, 95 | params=params, 96 | data=data, 97 | files=files, 98 | headers=headers, 99 | verify=self.verify, 100 | stream=bool(download_path), 101 | ) 102 | 103 | return self._process_response(response, download_path=download_path) 104 | 105 | def _process_response( 106 | self, 107 | response: requests.Response, 108 | download_path: Union[str, PathLike, None] = None, 109 | ): 110 | if response.ok: 111 | if download_path is None: 112 | return self._process_text_response(response) 113 | else: 114 | self._process_stream_response( 115 | response=response, download_path=download_path 116 | ) 117 | 118 | if not response.ok: 119 | self._process_error_response(response=response) 120 | 121 | def _process_text_response(self, response: requests.Response): 122 | try: 123 | json_data = response.json() 124 | except requests.exceptions.JSONDecodeError: 125 | json_data = None 126 | 127 | if json_data is None: 128 | return response.text 129 | return json_data 130 | 131 | def _process_stream_response( 132 | self, response: requests.Response, download_path: Union[str, PathLike] 133 | ): 134 | with open(download_path, "wb") as download_fp: 135 | for chunk in response.iter_content(chunk_size=4096): 136 | download_fp.write(chunk) 137 | 138 | def _process_error_response(self, response: requests.Response): 139 | try: 140 | json_data = response.json() 141 | except requests.exceptions.JSONDecodeError: 142 | json_data = None 143 | 144 | if isinstance(json_data, dict) and all( 145 | [ 146 | "type" in json_data, 147 | "message" in json_data, 148 | ] 149 | ): 150 | error_text = f"{json_data['type']} - {json_data['message']}" 151 | else: 152 | error_text = response.text 153 | raise TheHiveError(message=error_text, response=response) 154 | -------------------------------------------------------------------------------- /thehive4py/types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheHive-Project/TheHive4py/62a3f9714f9c27eb55017fab79ccd00ea8f593f0/thehive4py/types/__init__.py -------------------------------------------------------------------------------- /thehive4py/types/alert.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypedDict 2 | 3 | from thehive4py.types.custom_field import InputCustomFieldValue, OutputCustomFieldValue 4 | from thehive4py.types.observable import InputObservable 5 | from thehive4py.types.page import InputCasePage 6 | from thehive4py.types.procedure import InputProcedure 7 | from thehive4py.types.share import InputShare 8 | from thehive4py.types.task import InputTask 9 | 10 | 11 | class InputAlertRequired(TypedDict): 12 | type: str 13 | source: str 14 | sourceRef: str 15 | title: str 16 | description: str 17 | 18 | 19 | class InputAlert(InputAlertRequired, total=False): 20 | date: int 21 | externalLink: str 22 | severity: int 23 | tags: List[str] 24 | flag: bool 25 | tlp: int 26 | pap: int 27 | customFields: List[InputCustomFieldValue] 28 | summary: str 29 | status: str 30 | assignee: str 31 | caseTemplate: str 32 | observables: List[InputObservable] 33 | procedures: List[InputProcedure] 34 | 35 | 36 | class OutputAlertRequired(TypedDict): 37 | _id: str 38 | _type: str 39 | _createdBy: str 40 | _createdAt: int 41 | type: str 42 | source: str 43 | sourceRef: str 44 | title: str 45 | description: str 46 | severity: int 47 | severityLabel: str 48 | date: int 49 | tlp: int 50 | tlpLabel: str 51 | pap: int 52 | papLabel: str 53 | follow: bool 54 | observableCount: int 55 | status: str 56 | stage: str 57 | extraData: dict 58 | newDate: int 59 | timeToDetect: int 60 | 61 | 62 | class OutputAlert(OutputAlertRequired, total=False): 63 | _updatedBy: str 64 | _updatedAt: int 65 | externalLink: str 66 | tags: List[str] 67 | customFields: List[OutputCustomFieldValue] 68 | caseTemplate: str 69 | caseId: str 70 | assignee: str 71 | summary: str 72 | inProgressDate: int 73 | closedDate: int 74 | importedDate: int 75 | timeToTriage: int 76 | timeToQualify: int 77 | timeToAcknowledge: int 78 | 79 | 80 | class InputUpdateAlert(TypedDict, total=False): 81 | type: str 82 | source: str 83 | sourceRef: str 84 | externalLink: str 85 | title: str 86 | description: str 87 | severity: int 88 | date: int 89 | lastSyncDate: int 90 | tags: List[str] 91 | tlp: int 92 | pap: int 93 | follow: bool 94 | customFields: List[InputCustomFieldValue] 95 | status: str 96 | summary: str 97 | assignee: str 98 | addTags: List[str] 99 | removeTags: List[str] 100 | 101 | 102 | class InputBulkUpdateAlert(InputUpdateAlert): 103 | ids: List[str] 104 | 105 | 106 | class InputPromoteAlert(TypedDict, total=False): 107 | title: str 108 | description: str 109 | severity: int 110 | startDate: int 111 | endDate: int 112 | tags: List[str] 113 | flag: bool 114 | tlp: int 115 | pap: int 116 | status: str 117 | summary: str 118 | assignee: str 119 | customFields: List[InputCustomFieldValue] 120 | caseTemplate: str 121 | tasks: List[InputTask] 122 | pages: List[InputCasePage] 123 | sharingParameters: List[InputShare] 124 | taskRule: str 125 | observableRule: str 126 | -------------------------------------------------------------------------------- /thehive4py/types/attachment.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypedDict 2 | 3 | 4 | class OutputAttachmentRequired(TypedDict): 5 | _id: str 6 | _type: str 7 | _createdBy: str 8 | _createdAt: int 9 | name: str 10 | size: int 11 | contentType: str 12 | id: str 13 | 14 | 15 | class OutputAttachment(OutputAttachmentRequired, total=False): 16 | _updatedBy: str 17 | _updatedAt: int 18 | hashes: List[str] 19 | -------------------------------------------------------------------------------- /thehive4py/types/case.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal, TypedDict, Union 2 | 3 | from thehive4py.types.observable import OutputObservable 4 | from thehive4py.types.page import InputCasePage 5 | from thehive4py.types.share import InputShare 6 | 7 | from .custom_field import InputCustomFieldValue, OutputCustomFieldValue 8 | from .task import InputTask 9 | 10 | CaseStatusValue = Literal[ 11 | "New", 12 | "InProgress", 13 | "Indeterminate", 14 | "FalsePositive", 15 | "TruePositive", 16 | "Other", 17 | "Duplicated", 18 | ] 19 | 20 | 21 | class CaseStatus: 22 | New: CaseStatusValue = "New" 23 | InProgress: CaseStatusValue = "InProgress" 24 | Indeterminate: CaseStatusValue = "Indeterminate" 25 | FalsePositive: CaseStatusValue = "FalsePositive" 26 | TruePositive: CaseStatusValue = "TruePositive" 27 | Other: CaseStatusValue = "Other" 28 | Duplicated: CaseStatusValue = "Duplicated" 29 | 30 | 31 | ImpactStatusValue = Literal["NotApplicable", "WithImpact", "NoImpact"] 32 | 33 | 34 | class ImpactStatus: 35 | NotApplicable: ImpactStatusValue = "NotApplicable" 36 | WithImpact: ImpactStatusValue = "WithImpact" 37 | NoImpact: ImpactStatusValue = "NoImpact" 38 | 39 | 40 | class InputCaseRequired(TypedDict): 41 | title: str 42 | description: str 43 | 44 | 45 | class InputCase(InputCaseRequired, total=False): 46 | severity: int 47 | startDate: int 48 | endDate: int 49 | tags: List[str] 50 | flag: bool 51 | tlp: int 52 | pap: int 53 | status: CaseStatusValue 54 | summary: str 55 | assignee: str 56 | access: dict 57 | customFields: Union[List[InputCustomFieldValue], dict] 58 | caseTemplate: str 59 | tasks: List[InputTask] 60 | pages: List[InputCasePage] 61 | sharingParameters: List[InputShare] 62 | taskRule: str 63 | observableRule: str 64 | 65 | 66 | class OutputCaseRequired(TypedDict): 67 | _id: str 68 | _type: str 69 | _createdBy: str 70 | _createdAt: int 71 | number: int 72 | title: str 73 | description: str 74 | severity: int 75 | severityLabel: str 76 | startDate: int 77 | flag: bool 78 | tlp: int 79 | tlpLabel: str 80 | pap: int 81 | papLabel: str 82 | status: CaseStatusValue 83 | stage: str 84 | access: dict 85 | extraData: dict 86 | newDate: int 87 | timeToDetect: int 88 | 89 | 90 | class OutputCase(OutputCaseRequired, total=False): 91 | _updatedBy: str 92 | _updatedAt: int 93 | endDate: int 94 | tags: List[str] 95 | summary: str 96 | impactStatus: ImpactStatusValue 97 | assignee: str 98 | customFields: List[OutputCustomFieldValue] 99 | userPermissions: List[str] 100 | inProgressDate: int 101 | closedDate: int 102 | alertDate: int 103 | alertNewDate: int 104 | alertInProgressDate: int 105 | alertImportedDate: int 106 | timeToTriage: int 107 | timeToQualify: int 108 | timeToAcknowledge: int 109 | timeToResolve: int 110 | handlingDuration: int 111 | 112 | 113 | class InputUpdateCase(TypedDict, total=False): 114 | title: str 115 | description: str 116 | severity: int 117 | startDate: int 118 | endDate: int 119 | tags: List[str] 120 | flag: bool 121 | tlp: int 122 | pap: int 123 | status: str 124 | summary: str 125 | assignee: str 126 | impactStatus: str 127 | customFields: Union[List[InputCustomFieldValue], dict] 128 | taskRule: str 129 | observableRule: str 130 | addTags: List[str] 131 | removeTags: List[str] 132 | 133 | 134 | class InputBulkUpdateCase(InputUpdateCase): 135 | ids: List[str] 136 | 137 | 138 | class InputImportCaseRequired(TypedDict): 139 | password: str 140 | 141 | 142 | class InputImportCase(InputImportCaseRequired, total=False): 143 | sharingParameters: List[InputShare] 144 | taskRule: str 145 | observableRule: str 146 | 147 | 148 | class InputApplyCaseTemplateRequired(TypedDict): 149 | ids: List[str] 150 | caseTemplate: str 151 | 152 | 153 | class InputApplyCaseTemplate(InputApplyCaseTemplateRequired, total=False): 154 | updateTitlePrefix: bool 155 | updateDescription: bool 156 | updateTags: bool 157 | updateSeverity: bool 158 | updateFlag: bool 159 | updateTlp: bool 160 | updatePap: bool 161 | updateCustomFields: bool 162 | importTasks: List[str] 163 | importPages: List[str] 164 | 165 | 166 | class OutputCaseObservableMerge(TypedDict): 167 | untouched: int 168 | updated: int 169 | deleted: int 170 | 171 | 172 | class OutputCaseLinkRequired(TypedDict): 173 | linksCount: int 174 | 175 | 176 | class OutputCaseLink(OutputCase, OutputCaseLinkRequired, total=False): 177 | linkedWith: List[OutputObservable] 178 | 179 | 180 | class OutputImportCaseRequired(TypedDict): 181 | case: OutputCase 182 | 183 | 184 | class OutputImportCase(OutputImportCaseRequired, total=False): 185 | observables: List[OutputObservable] 186 | procedures: List[OutputObservable] 187 | errors: List[Any] 188 | 189 | 190 | class InputCaseOwnerOrganisationRequired(TypedDict): 191 | organisation: str 192 | 193 | 194 | class InputCaseOwnerOrganisation(InputCaseOwnerOrganisationRequired, total=False): 195 | keepProfile: str 196 | taskRule: str 197 | observableRule: str 198 | 199 | 200 | class InputCaseAccess(TypedDict): 201 | access: dict # TODO: refine type hint 202 | 203 | 204 | class InputCaseLink(TypedDict): 205 | type: str 206 | caseId: str 207 | 208 | 209 | class InputURLLink(TypedDict): 210 | type: str 211 | url: str 212 | -------------------------------------------------------------------------------- /thehive4py/types/case_template.py: -------------------------------------------------------------------------------- 1 | from typing import List, Literal, TypedDict, Union 2 | 3 | from .custom_field import InputCustomFieldValue 4 | from .task import InputTask, OutputTask 5 | 6 | SeverityValue = Literal[1, 2, 3, 4] 7 | TlpValue = Literal[0, 1, 2, 3, 4] 8 | PapValue = Literal[0, 1, 2, 3] 9 | 10 | 11 | class InputCaseTemplateRequired(TypedDict): 12 | name: str 13 | 14 | 15 | class InputCaseTemplate(InputCaseTemplateRequired, total=False): 16 | displayName: str 17 | titlePrefix: str 18 | description: str 19 | severity: SeverityValue 20 | tags: List[str] 21 | flag: bool 22 | tlp: TlpValue 23 | pap: PapValue 24 | summary: str 25 | tasks: List[InputTask] 26 | pageTemplateIds: List[str] 27 | customFields: Union[dict, List[InputCustomFieldValue]] 28 | 29 | 30 | class OutputCaseTemplateRequired(TypedDict): 31 | _id: str 32 | _type: str 33 | _createdBy: str 34 | _createdAt: int 35 | name: str 36 | 37 | 38 | class OutputCaseTemplate(OutputCaseTemplateRequired, total=False): 39 | _updatedBy: str 40 | _updatedAt: int 41 | displayName: str 42 | titlePrefix: str 43 | description: str 44 | severity: SeverityValue 45 | tags: List[str] 46 | flag: bool 47 | tlp: TlpValue 48 | pap: PapValue 49 | summary: str 50 | tasks: List[OutputTask] 51 | pageTemplateIds: List[str] 52 | customFields: Union[dict, List[InputCustomFieldValue]] 53 | -------------------------------------------------------------------------------- /thehive4py/types/comment.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | 4 | class InputComment(TypedDict): 5 | message: str 6 | 7 | 8 | class OutputCommentRequired(TypedDict): 9 | _id: str 10 | _type: str 11 | createdBy: str 12 | createdAt: int 13 | message: str 14 | isEdited: bool 15 | 16 | 17 | class OutputComment(OutputCommentRequired, total=False): 18 | updatedAt: str 19 | 20 | 21 | class InputUpdateComment(TypedDict): 22 | message: str 23 | -------------------------------------------------------------------------------- /thehive4py/types/cortex.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, TypedDict 2 | 3 | 4 | class OutputAnalyzerRequired(TypedDict): 5 | id: str 6 | name: str 7 | version: str 8 | description: str 9 | 10 | 11 | class OutputAnalyzer(OutputAnalyzerRequired, total=False): 12 | dataTypeList: List[str] 13 | cortexIds: List[str] 14 | 15 | 16 | class OutputResponderRequired(TypedDict): 17 | id: str 18 | name: str 19 | version: str 20 | description: str 21 | 22 | 23 | class OutputResponder(OutputResponderRequired, total=False): 24 | dataTypeList: List[str] 25 | cortexIds: List[str] 26 | 27 | 28 | class OutputAnalyzerJobRequired(TypedDict): 29 | _id: str 30 | _type: str 31 | _createdBy: str 32 | _createdAt: str 33 | analyzerId: str 34 | analyzerName: str 35 | analyzerDefinition: str 36 | status: str 37 | startDate: str 38 | cortexId: str 39 | cortexJobId: str 40 | id: str 41 | operations: str 42 | 43 | 44 | class OutputAnalyzerJob(OutputAnalyzerJobRequired, total=False): 45 | _updatedBy: str 46 | _updatedAt: str 47 | endDate: str 48 | report: Dict[str, Any] 49 | case_artifact: Dict[str, Any] 50 | 51 | 52 | class OutputResponderActionRequired(TypedDict): 53 | _id: str 54 | _type: str 55 | _createdBy: str 56 | _createdAt: str 57 | responderId: str 58 | status: str 59 | startDate: str 60 | cortexId: str 61 | cortexJobId: str 62 | id: str 63 | operations: str 64 | 65 | 66 | class OutputResponderAction(OutputResponderActionRequired, total=False): 67 | _updatedBy: str 68 | _updatedAt: str 69 | endDate: str 70 | report: Dict[str, Any] 71 | responderName: str 72 | responderDefinition: str 73 | 74 | 75 | class InputResponderActionRequired(TypedDict): 76 | objectId: str 77 | objectType: str 78 | responderId: str 79 | 80 | 81 | class InputResponderAction(InputResponderActionRequired, total=False): 82 | parameters: Dict[str, Any] 83 | tlp: int 84 | 85 | 86 | class InputAnalyzerJobRequired(TypedDict): 87 | analyzerId: str 88 | cortexId: str 89 | artifactId: str 90 | 91 | 92 | class InputAnalyzerJob(InputAnalyzerJobRequired, total=False): 93 | parameters: Dict[str, Any] 94 | -------------------------------------------------------------------------------- /thehive4py/types/custom_field.py: -------------------------------------------------------------------------------- 1 | from typing import Any, TypedDict 2 | 3 | 4 | class InputCustomFieldValueRequired(TypedDict): 5 | name: str 6 | 7 | 8 | class InputCustomFieldValue(InputCustomFieldValueRequired, total=False): 9 | value: Any 10 | order: int 11 | 12 | 13 | class OutputCustomFieldValue(TypedDict): 14 | _id: str 15 | name: str 16 | type: str 17 | value: Any 18 | order: int 19 | 20 | 21 | class InputCustomFieldRequired(TypedDict): 22 | name: str 23 | group: str 24 | description: str 25 | type: str 26 | 27 | 28 | class InputCustomField(InputCustomFieldRequired, total=False): 29 | displayName: str 30 | mandatory: bool 31 | options: list 32 | 33 | 34 | class OutputCustomFieldRequired(TypedDict): 35 | _id: str 36 | _type: str 37 | _createdBy: str 38 | _createdAt: int 39 | name: str 40 | displayName: str 41 | group: str 42 | description: str 43 | type: str 44 | mandatory: bool 45 | 46 | 47 | class OutputCustomField(OutputCustomFieldRequired, total=False): 48 | _updatedBy: str 49 | _updatedAt: int 50 | options: list 51 | 52 | 53 | class InputUpdateCustomField(TypedDict, total=False): 54 | displayName: str 55 | group: str 56 | description: str 57 | type: str 58 | options: list 59 | mandatory: bool 60 | -------------------------------------------------------------------------------- /thehive4py/types/observable.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypedDict 2 | 3 | from thehive4py.types.attachment import OutputAttachment 4 | 5 | 6 | class InputObservableRequired(TypedDict): 7 | dataType: str 8 | 9 | 10 | class InputObservable(InputObservableRequired, total=False): 11 | data: str 12 | message: str 13 | startDate: int 14 | tlp: int 15 | pap: int 16 | tags: List[str] 17 | ioc: bool 18 | sighted: bool 19 | sightedAt: int 20 | ignoreSimilarity: bool 21 | isZip: bool 22 | zipPassword: bool 23 | attachment: str 24 | 25 | 26 | class OutputObservableRequired(TypedDict): 27 | _id: str 28 | _type: str 29 | _createdBy: str 30 | _createdAt: int 31 | dataType: str 32 | startDate: int 33 | tlp: int 34 | tlpLabel: str 35 | pap: int 36 | papLabel: str 37 | ioc: bool 38 | sighted: bool 39 | reports: dict 40 | extraData: dict 41 | ignoreSimilarity: bool 42 | 43 | 44 | class OutputObservable(OutputObservableRequired, total=False): 45 | _updatedBy: str 46 | _updatedAt: int 47 | data: str 48 | attachment: OutputAttachment 49 | tags: List[str] 50 | sightedAt: int 51 | message: str 52 | 53 | 54 | class InputUpdateObservable(TypedDict, total=False): 55 | dataType: str 56 | message: str 57 | tlp: int 58 | pap: int 59 | tags: List[str] 60 | ioc: bool 61 | sighted: bool 62 | sightedAt: int 63 | ignoreSimilarity: bool 64 | 65 | 66 | class InputBulkUpdateObservable(InputUpdateObservable): 67 | ids: List[str] 68 | -------------------------------------------------------------------------------- /thehive4py/types/observable_type.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | 4 | class InputObservableTypeRequired(TypedDict): 5 | name: str 6 | 7 | 8 | class InputObservableType(InputObservableTypeRequired, total=False): 9 | isAttachment: bool 10 | 11 | 12 | class OutputObservableTypeRequired(TypedDict): 13 | _id: str 14 | _type: str 15 | _createdBy: str 16 | _createdAt: int 17 | name: str 18 | isAttachment: bool 19 | 20 | 21 | class OutputObservableType(OutputObservableTypeRequired, total=False): 22 | _updatedBy: str 23 | _updatedAt: int 24 | -------------------------------------------------------------------------------- /thehive4py/types/organisation.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypedDict 2 | 3 | 4 | class InputOrganisationLink(TypedDict, total=False): 5 | linkType: str 6 | otherLinkType: str 7 | 8 | 9 | class InputBulkOrganisationLinkRequired(TypedDict): 10 | toOrganisation: str 11 | linkType: str 12 | otherLinkType: str 13 | 14 | 15 | class InputBulkOrganisationLink(InputBulkOrganisationLinkRequired, total=False): 16 | avatar: str 17 | 18 | 19 | class OutputSharingProfile(TypedDict): 20 | name: str 21 | description: str 22 | autoShare: bool 23 | editable: bool 24 | permissionProfile: str 25 | taskRule: str 26 | observableRule: str 27 | 28 | 29 | class InputOrganisationRequired(TypedDict): 30 | name: str 31 | description: str 32 | 33 | 34 | class InputOrganisation(InputOrganisationRequired, total=False): 35 | taskRule: str 36 | observableRule: str 37 | locked: bool 38 | 39 | 40 | class OutputOrganisationRequired(TypedDict): 41 | _id: str 42 | _type: str 43 | _createdBy: str 44 | _createdAt: int 45 | name: str 46 | description: str 47 | taskRule: str 48 | observableRule: str 49 | locked: bool 50 | extraData: dict 51 | 52 | 53 | class OutputOrganisation(OutputOrganisationRequired, total=False): 54 | _updatedBy: str 55 | _updatedAt: int 56 | links: List[InputOrganisationLink] 57 | avatar: str 58 | 59 | 60 | class InputUpdateOrganisation(TypedDict, total=False): 61 | name: str 62 | description: str 63 | taskRule: str 64 | observableRule: str 65 | locked: bool 66 | avatar: str 67 | 68 | 69 | class OutputOrganisationLink(TypedDict): 70 | linkType: str 71 | otherLinkType: str 72 | organisation: OutputOrganisation 73 | -------------------------------------------------------------------------------- /thehive4py/types/page.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | 4 | class InputCasePageRequired(TypedDict): 5 | title: str 6 | content: str 7 | category: str 8 | 9 | 10 | class InputCasePage(InputCasePageRequired, total=False): 11 | order: int 12 | 13 | 14 | class OutputCasePageRequired(TypedDict): 15 | _id: str 16 | id: str 17 | createdBy: str 18 | createdAt: int 19 | title: str 20 | content: str 21 | _type: str 22 | slug: str 23 | order: int 24 | category: str 25 | 26 | 27 | class OutputCasePage(OutputCasePageRequired, total=False): 28 | updatedBy: str 29 | updatedAt: int 30 | 31 | 32 | class InputUpdateCasePage(TypedDict, total=False): 33 | title: str 34 | content: str 35 | category: str 36 | order: int 37 | -------------------------------------------------------------------------------- /thehive4py/types/procedure.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | 4 | class InputProcedureRequired(TypedDict): 5 | occurDate: int 6 | patternId: str 7 | 8 | 9 | class InputProcedure(InputProcedureRequired, total=False): 10 | tactic: str 11 | description: str 12 | 13 | 14 | class OutputProcedureRequired(TypedDict): 15 | _id: str 16 | _createdAt: int 17 | _createdBy: str 18 | occurDate: int 19 | tactic: str 20 | tacticLabel: str 21 | extraData: dict 22 | 23 | 24 | class OutputProcedure(OutputProcedureRequired, total=False): 25 | _updatedAt: int 26 | _updatedBy: str 27 | description: str 28 | patternId: str 29 | patternName: str 30 | 31 | 32 | class InputUpdateProcedure(TypedDict, total=False): 33 | description: str 34 | occurDate: int 35 | -------------------------------------------------------------------------------- /thehive4py/types/profile.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypedDict 2 | 3 | 4 | class InputProfileRequired(TypedDict): 5 | name: str 6 | 7 | 8 | class InputProfile(InputProfileRequired, total=False): 9 | permissions: List[str] 10 | 11 | 12 | class OutputProfileRequired(TypedDict): 13 | _id: str 14 | _type: str 15 | _createdBy: str 16 | _createdAt: int 17 | name: str 18 | editable: bool 19 | isAdmin: bool 20 | 21 | 22 | class OutputProfile(OutputProfileRequired, total=False): 23 | _updatedBy: str 24 | _updatedAt: int 25 | permissions: List[str] 26 | 27 | 28 | class InputUpdateProfile(TypedDict, total=False): 29 | name: str 30 | permissions: List[str] 31 | -------------------------------------------------------------------------------- /thehive4py/types/share.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | 4 | class OutputShareRequired(TypedDict): 5 | _id: str 6 | _type: str 7 | _createdBy: str 8 | _createdAt: int 9 | caseId: str 10 | profileName: str 11 | organisationName: str 12 | owner: bool 13 | taskRule: str 14 | observableRule: str 15 | 16 | 17 | class OutputShare(OutputShareRequired, total=False): 18 | _updatedBy: str 19 | _updatedAt: int 20 | 21 | 22 | class InputShareRequired(TypedDict): 23 | organisation: str 24 | 25 | 26 | class InputShare(InputShareRequired, total=False): 27 | share: bool 28 | profile: str 29 | taskRule: str 30 | observableRule: str 31 | -------------------------------------------------------------------------------- /thehive4py/types/task.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypedDict 2 | 3 | 4 | class InputTaskRequired(TypedDict): 5 | title: str 6 | 7 | 8 | class InputTask(InputTaskRequired, total=False): 9 | group: str 10 | description: str 11 | status: str 12 | flag: bool 13 | startDate: int 14 | endDate: int 15 | order: int 16 | dueDate: int 17 | assignee: str 18 | mandatory: bool 19 | 20 | 21 | class OutputTaskRequired(TypedDict): 22 | _id: str 23 | _type: str 24 | _createdBy: str 25 | _createdAt: int 26 | title: str 27 | group: str 28 | status: str 29 | flag: bool 30 | order: int 31 | mandatory: bool 32 | extraData: dict 33 | 34 | 35 | class OutputTask(OutputTaskRequired, total=False): 36 | _updatedBy: str 37 | _updatedAt: int 38 | description: str 39 | startDate: int 40 | endDate: int 41 | assignee: str 42 | dueDate: int 43 | 44 | 45 | class InputUpdateTask(TypedDict, total=False): 46 | title: str 47 | group: str 48 | description: str 49 | status: str 50 | flag: bool 51 | startDate: int 52 | endDate: int 53 | order: int 54 | dueDate: int 55 | assignee: str 56 | mandatory: bool 57 | 58 | 59 | class InputBulkUpdateTask(InputUpdateTask): 60 | ids: List[str] 61 | -------------------------------------------------------------------------------- /thehive4py/types/task_log.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypedDict 2 | 3 | from thehive4py.types.attachment import OutputAttachment 4 | 5 | 6 | class InputTaskLogRequired(TypedDict): 7 | message: str 8 | 9 | 10 | class InputTaskLog(InputTaskLogRequired, total=False): 11 | startDate: int 12 | includeInTimeline: int 13 | attachments: List[str] 14 | 15 | 16 | class OutputTaskLogRequired(TypedDict): 17 | _id: str 18 | _type: str 19 | _createdBy: str 20 | _createdAt: int 21 | message: str 22 | date: int 23 | owner: str 24 | extraData: dict 25 | 26 | 27 | class OutputTaskLog(OutputTaskLogRequired, total=False): 28 | _updatedBy: str 29 | _updatedAt: int 30 | attachments: List[OutputAttachment] 31 | includeInTimeline: int 32 | 33 | 34 | class InputUpdateTaskLog(TypedDict, total=False): 35 | message: str 36 | includeInTimeline: int 37 | -------------------------------------------------------------------------------- /thehive4py/types/timeline.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypedDict 2 | 3 | 4 | class OutputTimelineEventRequired(TypedDict): 5 | date: int 6 | kind: str 7 | entity: str 8 | entityId: str 9 | details: dict 10 | 11 | 12 | class OutputTimelineEvent(OutputTimelineEventRequired, total=False): 13 | endDate: int 14 | 15 | 16 | class OutputTimeline(TypedDict): 17 | events: List[OutputTimelineEvent] 18 | 19 | 20 | class InputCustomEventRequired(TypedDict): 21 | date: int 22 | title: str 23 | 24 | 25 | class InputCustomEvent(InputCustomEventRequired, total=False): 26 | endDate: int 27 | description: str 28 | 29 | 30 | class OutputCustomEventRequired(TypedDict): 31 | _id: str 32 | _type: str 33 | _createdBy: str 34 | _createdAt: int 35 | date: int 36 | title: str 37 | 38 | 39 | class OutputCustomEvent(OutputCustomEventRequired, total=False): 40 | _updatedBy: str 41 | _updatedAt: int 42 | endDate: int 43 | description: str 44 | 45 | 46 | class InputUpdateCustomEvent(TypedDict, total=False): 47 | date: int 48 | endDate: int 49 | title: str 50 | description: str 51 | -------------------------------------------------------------------------------- /thehive4py/types/user.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypedDict 2 | 3 | 4 | class InputUserRequired(TypedDict): 5 | login: str 6 | name: str 7 | profile: str 8 | 9 | 10 | class InputUser(InputUserRequired, total=False): 11 | email: str 12 | password: str 13 | organisation: str 14 | type: str 15 | 16 | 17 | class OutputOrganisationProfile(TypedDict): 18 | organisationId: str 19 | organisation: str 20 | profile: str 21 | 22 | 23 | class OutputUserRequired(TypedDict): 24 | _id: str 25 | _createdBy: str 26 | _createdAt: int 27 | login: str 28 | name: str 29 | hasKey: bool 30 | hasPassword: bool 31 | hasMFA: bool 32 | locked: bool 33 | profile: str 34 | organisation: str 35 | type: str 36 | extraData: dict 37 | 38 | 39 | class OutputUser(OutputUserRequired, total=False): 40 | _updatedBy: str 41 | _updatedAt: int 42 | email: str 43 | permissions: List[str] 44 | avatar: str 45 | organisations: List[OutputOrganisationProfile] 46 | defaultOrganisation: str 47 | 48 | 49 | class InputUpdateUser(TypedDict, total=False): 50 | name: str 51 | organisation: str 52 | profile: str 53 | locked: bool 54 | avatar: str 55 | email: str 56 | defaultOrganisation: str 57 | 58 | 59 | class InputUserOrganisationRequired(TypedDict): 60 | organisation: str 61 | profile: str 62 | 63 | 64 | class InputUserOrganisation(InputUserOrganisationRequired, total=False): 65 | default: bool 66 | 67 | 68 | class OutputUserOrganisation(TypedDict): 69 | organisation: str 70 | profile: str 71 | default: bool 72 | --------------------------------------------------------------------------------