diff --git a/.github/workflows/_deploy-docs.yml b/.github/workflows/_deploy-docs.yml
index db6ef5e..0ce0e70 100644
--- a/.github/workflows/_deploy-docs.yml
+++ b/.github/workflows/_deploy-docs.yml
@@ -7,19 +7,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - name: Compare tag and package version
- run: |
- TAG=${GITHUB_REF#refs/*/}
- VERSION=$(grep -Po '(?<=version = ")[^"]*' pyproject.toml)
- if [ "$TAG" != "$VERSION" ]; then
- echo "Tag value and package version are different: ${TAG} != ${VERSION}"
- exit 1
- fi
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.13
- name: Install build dependencies
run: pip install --no-cache-dir -U pip .['docs']
+ - name: Configure git
+ run: |
+ git fetch origin gh-pages --depth=1
+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
+ git config --local user.name "github-actions[bot]"
- name: Deploy to github pages
run: ./scripts/cd.py --deploy-docs
diff --git a/.github/workflows/main-cicd.yml b/.github/workflows/main-cicd.yml
index 1aa2bc1..c3e9729 100644
--- a/.github/workflows/main-cicd.yml
+++ b/.github/workflows/main-cicd.yml
@@ -17,12 +17,13 @@ jobs:
uses: ./.github/workflows/_build-package.yml
build-docs:
uses: ./.github/workflows/_build-docs.yml
+ deploy-docs:
+ if: startsWith(github.ref, 'refs/heads/main')
+ uses: ./.github/workflows/_deploy-docs.yml
+ needs: [build-docs, build-package, integration-tests, static-checks]
upload-package:
if: startsWith(github.ref, 'refs/tags/')
uses: ./.github/workflows/_upload-package.yml
- needs: [static-checks, integration-tests, build-package, build-docs]
+ needs: [build-docs, build-package, integration-tests, static-checks]
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
- deploy-docs:
- uses: ./.github/workflows/_deploy-docs.yml
- needs: [upload-package]
diff --git a/README.md b/README.md
index 19f1980..6e06dc3 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,43 @@
-
-
-
-
- thehive4py - the de facto Python API client of TheHive
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-# thehive4py
+
+
+---
+**Documentation**: https://thehive-project.github.io/TheHive4py
+
+**Source Code**: https://github.com/TheHive-Project/TheHive4py
+
+---
+
+# Introduction
> [!IMPORTANT]
> thehive4py v1.x is not maintained anymore as TheHive v3 and v4 are end of life. thehive4py v2.x is a complete rewrite and is not compatible with thehive4py v1.x. The library is still in beta phase.
diff --git a/docs/examples/alert.md b/docs/examples/alert.md
new file mode 100644
index 0000000..5e8e5f2
--- /dev/null
+++ b/docs/examples/alert.md
@@ -0,0 +1,208 @@
+# Alert
+
+## A simple alert
+
+A new alert requires at least these fields to be defined:
+
+- `type`: The type of the alert.
+- `source`: The source of the alert.
+- `sourceRef`: A unique reference for the alert.
+- `title`: A descriptive title for the alert.
+- `description`: Additional information describing the alert.
+
+Here's an example that demonstrates how to create the most simplistic alert possible using the [alert.create][thehive4py.endpoints.alert.AlertEndpoint.create] method:
+
+```python
+--8<-- "examples/alert/simple.py"
+```
+
+## An advanced alert
+
+In the previous example we really kept things simple and only specified the required alert fields inline in the create method call.
+With a more advanced example this can become complicated and hard to read.
+Fortunately we can use `thehive4py`'s type hints to the rescue and specify more complex input alerts outside of the method call.
+
+Here's how:
+```python
+--8<-- "examples/alert/advanced.py"
+```
+
+In the above snippet `input_alert` is created before the create call and later passed to the `alert` argument.
+Finally after the creation of the alert we saved the response in the `output_alert` to be able to use it later.
+
+!!! note
+ While the above alert is a bit more advanced it's still far from the most complex example possible.
+ 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).
+
+
+## Alert observables
+
+TheHive API provides multiple ways to add observables to alerts, let them be textual or file based observables.
+
+### Add observables during alert creation
+
+We can add observables already during alert creation. This is a great way to combine alert and observable creation in a simple and atomic way:
+
+Let's create an alert with an `ip` and a `domain` observable:
+
+```python
+--8<-- "examples/alert/observable_during_alerting.py"
+```
+
+### Add observables to an existing alert
+
+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.
+
+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:
+
+
+```python
+--8<-- "examples/alert/observable_after_alerting.py"
+```
+
+### Add file based observables
+
+In the previous examples we've seen how to handle simple observables without attachments. Next we will 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:
+
+
+```python
+--8<-- "examples/alert/observable_from_file.py"
+```
+
+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.
+
+This way TheHive will know which attachment to pair with which observable behind the scenes.
+
+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.
+
+## Update single and bulk
+
+Sometimes an existing alert needs to be updated. TheHive offers multiple ways to accomplish this task either with a single alert or multiple ones.
+
+### Update single
+
+A single alert can be updated using [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.
+
+```python
+--8<-- "examples/alert/update_single.py"
+```
+
+In the above example we've updated the `title` and the `tags` fields.
+
+Be mindful though, `thehive4py` is a lightweight wrapper around TheHive API and offers no object relationship mapping functionalities, meaning that the original `original_alert` won't reflect the changes of the update.
+
+To work with the updated alert we fetched the latest version using the [alert.get][thehive4py.endpoints.alert.AlertEndpoint.get] method and stored it in the `updated_alert` variable.
+
+Now the content of `updated_alert` should reflect the changes we made with our update request.
+
+!!! tip
+ 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).
+
+### Update bulk
+
+To update the **same fields** with the **same values** on multiple alerts at the same time, one can use [alert.bulk_update][thehive4py.endpoints.alert.AlertEndpoint.bulk_update] method.
+The method accepts the same `fields` dictionary with an additional `ids` field on it, which should contain the list of ids of the alerts to be bulk updated.
+
+```python
+--8<-- "examples/alert/update_bulk.py"
+```
+
+In the example we prepare two alerts for the bulk update, and collect their ids in the `original_alert_ids` list.
+Then we update the fields `title` and `tags` on both alerts using the bulk update method.
+
+## Get and find
+
+There are multiple ways to retrieve already existing alerts:
+
+### Get a single alert
+
+To get a single alert one can use the [alert.get][thehive4py.endpoints.alert.AlertEndpoint.get] method with the alert's id as follows:
+
+```python
+--8<-- "examples/alert/fetch_with_get.py"
+```
+
+### Find multiple alerts
+
+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.
+
+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.
+
+Then we will construct a query filter that will look for alerts with these tags on them:
+
+```python
+--8<-- "examples/alert/fetch_with_find.py"
+```
+
+The above example demonstrates two ways to construct query filters.
+
+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.
+
+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.
+
+These filter expressions can be chained together with different operators, just like we did with the `|` (`or`) operator in the example.
+
+Currently, the filter classes support the following operators:
+
+- `&`: Used for the Query API's `_and` construct.
+- `|`: Used for the Query API's `_or` construct.
+- `~`: Used for the Query API's `_not` construct.
+
+The full list of the filter builders can be found in the [query.filters][thehive4py.query.filters] module.
+
+## Promote and merge into a case
+
+In TheHive alerts usually represent signals of compromise while cases provide a higher level entity to group these signals into one object.
+Therefore we can promote an alert into a case or merge new alerts into an existing case for a more organised investigation.
+
+### Promote to case
+
+To create a case from an alert we can use [alert.promote_to_case][thehive4py.endpoints.alert.AlertEndpoint.promote_to_case] method.
+
+```python
+--8<-- "examples/alert/case_promote.py"
+```
+
+!!! tip
+ For additional control the method accepts a `fields` argument which can be used to modify properties on the case.
+ To see all available options please consult the [official docs](https://docs.strangebee.com/thehive/api-docs/#tag/Alert/operation/Create%20Case%20from%20Alert).
+
+### Merge into case
+
+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.
+
+```python
+--8<-- "examples/alert/case_merge.py"
+```
+
+In the above example we prepared a `parent_case` to which we merge the `new_alert` using their ids and finally save the updated case in the `updated_parent_case` variable.
+
+!!! tip
+ 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.
+
+
+## Delete single and bulk
+
+`thehive4py` provides two different ways to delete alerts:
+
+- delete a single alert
+- delete alerts in bulk
+
+### Delete single
+
+To delete a single alert the [alert.delete][thehive4py.endpoints.alert.AlertEndpoint.delete] method can be used as follows:
+
+```python
+--8<-- "examples/alert/delete_single.py"
+```
+
+
+### Delete in bulk
+
+To delete multiple alerts via a single request one can use the [alert.bulk_delete][thehive4py.endpoints.alert.AlertEndpoint.bulk_delete] method as follows:
+
+```python
+--8<-- "examples/alert/delete_bulk.py"
+```
+
+In the above example we created two alerts and saved their ids in the `alert_ids_to_delete` variable just to pass it in the bulk deletion method.
diff --git a/docs/examples/client.md b/docs/examples/client.md
new file mode 100644
index 0000000..16c162c
--- /dev/null
+++ b/docs/examples/client.md
@@ -0,0 +1,74 @@
+# Client
+
+## Authentication
+
+TheHive API provides two ways to authenticate the client:
+
+- apikey auth
+- username and password auth
+
+### Auth with apikey
+
+```python
+--8<-- "examples/client/auth_with_apikey.py"
+```
+
+### Auth with username and password
+
+```python
+--8<-- "examples/client/auth_with_username_and_password.py"
+```
+
+## Organisation
+
+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.
+
+
+### Specify the organisation during init
+
+In this example we will instaniate a client with the `admin` organisation explicitly:
+
+```python
+--8<-- "examples/client/org_via_constructor.py"
+```
+
+### Switch organisations during runtime
+
+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:
+
+```python
+--8<-- "examples/client/org_during_runtime.py"
+```
+
+!!! warning
+ 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.
+
+
+## SSL Verification
+
+By default the client verifies if the connection is going through SSL.
+In case one needs to pass a custom certificate bundle or directory it's possible via the `verify` argument like:
+
+```python
+--8<-- "examples/client/ssl.py"
+```
+
+!!! note
+ It's also possible to disable SSL verification completely by setting `verify` to `False`.
+ However this is greatly discouraged as it's a security bad practice.
+
+
+## Retries
+
+The client comes with a sensible retry mechanism by default that will try to cover the most common use cases.
+However it's also possible to configure a tailored retry mechanism via the `max_retries` argument.
+
+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:
+
+```python
+--8<-- "examples/client/retries.py"
+```
+
+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).
+
+
diff --git a/docs/index.md b/docs/index.md
index 65a0ce4..bab1a24 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,33 +1,68 @@
----
-title: Coming Soon!
-hide:
- - toc
- - navigation
----
-#
-
-
- TheHive4py
+
+
thehive4py
-
- :material-bee:{ .strangebee .bluebee .upleftbee }
- :material-bee:{ .strangebee .yellowbee .upbee }
- :material-bee:{ .strangebee .bluebee .uprightbee }
-
-
Coming Soon!
-
- :material-bee:{ .strangebee .yellowbee .downleftbee }
- :material-bee:{ .strangebee .bluebee .downbee }
- :material-bee:{ .strangebee .yellowbee .downrightbee }
-
+
+ the de facto Python API client of TheHive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+**Documentation**:
https://thehive-project.github.io/TheHive4py/
+**Source Code**:
https://github.com/TheHive-Project/TheHive4py
-
Our bees are out buzzing to gather all the docs you need!
-
Check out our Quickstart in the meantime!
-
+---
+
+# Introduction
+
+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.
+
+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.
+
+
+# Requirements
+`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/).
+
+# Installation
+The `thehive4py` can be installed with pip like:
+
+```bash
+pip install "thehive4py>=2.0.0b"
+```
+
+
+# Quickstart
+
+```python
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+```
+
+# Licensing
+This project is licensed under the terms of the MIT license.
diff --git a/docs/reference.md b/docs/reference.md
new file mode 100644
index 0000000..8e70d3e
--- /dev/null
+++ b/docs/reference.md
@@ -0,0 +1,20 @@
+[//]: # (Style hack due to capitalized h5 headers from mkdocs-material, which corrupts the reference docs display)
+[//]: # (More details: https://github.com/squidfunk/mkdocs-material/issues/1522)
+
+
+# API Reference
+
+::: thehive4py
+ options:
+ show_submodules: true
+ members:
+ - client
+ - session
+ - endpoints
+ - types
+ - query
+ - errors
diff --git a/examples/alert/advanced.py b/examples/alert/advanced.py
new file mode 100644
index 0000000..29abfa0
--- /dev/null
+++ b/examples/alert/advanced.py
@@ -0,0 +1,13 @@
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+
+simple_alert = hive.alert.create(
+ alert={
+ "type": "simple",
+ "source": "tutorial",
+ "sourceRef": "should-be-unique",
+ "title": "a simple alert",
+ "description": "a bit too simple",
+ }
+)
diff --git a/examples/alert/case_merge.py b/examples/alert/case_merge.py
new file mode 100644
index 0000000..7f5d0b4
--- /dev/null
+++ b/examples/alert/case_merge.py
@@ -0,0 +1,24 @@
+import uuid
+
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+
+
+parent_case = hive.case.create(
+ case={"title": "parent case", "description": "a simple parent case"}
+)
+
+new_alert = hive.alert.create(
+ alert={
+ "type": "merge-into-case",
+ "source": "tutorial",
+ "sourceRef": uuid.uuid4().hex,
+ "title": "alert to merge",
+ "description": "a single alert to merge into a parent case",
+ }
+)
+
+updated_parent_case = hive.alert.merge_into_case(
+ alert_id=new_alert["_id"], case_id=parent_case["_id"]
+)
diff --git a/examples/alert/case_promote.py b/examples/alert/case_promote.py
new file mode 100644
index 0000000..2b7b5c4
--- /dev/null
+++ b/examples/alert/case_promote.py
@@ -0,0 +1,17 @@
+import uuid
+
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+
+alert_to_promote = hive.alert.create(
+ alert={
+ "type": "promote",
+ "source": "tutorial",
+ "sourceRef": uuid.uuid4().hex,
+ "title": "promote to case",
+ "description": "an alert to promote to case",
+ }
+)
+
+case_from_alert = hive.alert.promote_to_case(alert_id=alert_to_promote["_id"])
diff --git a/examples/alert/delete_bulk.py b/examples/alert/delete_bulk.py
new file mode 100644
index 0000000..f97b46e
--- /dev/null
+++ b/examples/alert/delete_bulk.py
@@ -0,0 +1,22 @@
+import uuid
+
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+
+alert_ids_to_delete = []
+
+for i in range(2):
+ alert_to_delete = hive.alert.create(
+ alert={
+ "type": "delete",
+ "source": "tutorial",
+ "sourceRef": uuid.uuid4().hex,
+ "title": f"delete alert #{i}",
+ "description": "an alert to delete",
+ }
+ )
+ alert_ids_to_delete.append(alert_to_delete["_id"])
+
+
+hive.alert.bulk_delete(ids=alert_ids_to_delete)
diff --git a/examples/alert/delete_single.py b/examples/alert/delete_single.py
new file mode 100644
index 0000000..8be55ff
--- /dev/null
+++ b/examples/alert/delete_single.py
@@ -0,0 +1,18 @@
+import uuid
+
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+
+alert_to_delete = hive.alert.create(
+ alert={
+ "type": "delete",
+ "source": "tutorial",
+ "sourceRef": uuid.uuid4().hex,
+ "title": "delete alert",
+ "description": "an alert to delete",
+ }
+)
+
+
+hive.alert.delete(alert_id=alert_to_delete["_id"])
diff --git a/examples/alert/fetch_with_find.py b/examples/alert/fetch_with_find.py
new file mode 100644
index 0000000..a0c2241
--- /dev/null
+++ b/examples/alert/fetch_with_find.py
@@ -0,0 +1,40 @@
+import uuid
+
+from thehive4py import TheHiveApi
+from thehive4py.query.filters import Eq
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+
+antivirus_alert = hive.alert.create(
+ alert={
+ "type": "find-multiple",
+ "source": "tutorial",
+ "sourceRef": uuid.uuid4().hex,
+ "title": "alert to find",
+ "description": "an alert to find with others",
+ "tags": ["antivirus"],
+ }
+)
+
+phishing_alert = hive.alert.create(
+ alert={
+ "type": "find-multiple",
+ "source": "tutorial",
+ "sourceRef": uuid.uuid4().hex,
+ "title": "alert to find",
+ "description": "an alert to find with others",
+ "tags": ["phishing"],
+ }
+)
+
+
+raw_filters = {
+ "_or": [
+ {"_eq": {"_field": "tags", "_value": "antivirus"}},
+ {"_eq": {"_field": "tags", "_value": "antivirus"}},
+ ]
+}
+all_alerts_with_raw_filters = hive.alert.find(filters=raw_filters)
+
+class_filters = Eq(field="tags", value="antivirus") | Eq(field="tags", value="phishing")
+all_alerts_with_class_filters = hive.alert.find(filters=class_filters)
diff --git a/examples/alert/fetch_with_get.py b/examples/alert/fetch_with_get.py
new file mode 100644
index 0000000..9bf86fb
--- /dev/null
+++ b/examples/alert/fetch_with_get.py
@@ -0,0 +1,18 @@
+import uuid
+
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+
+alert_to_get = hive.alert.create(
+ alert={
+ "type": "get-single",
+ "source": "tutorial",
+ "sourceRef": uuid.uuid4().hex,
+ "title": "alert to get",
+ "description": "a single alert to fetch",
+ }
+)
+
+
+fetched_alert = hive.alert.get(alert_id=alert_to_get["_id"])
diff --git a/examples/alert/observable_after_alerting.py b/examples/alert/observable_after_alerting.py
new file mode 100644
index 0000000..91059b3
--- /dev/null
+++ b/examples/alert/observable_after_alerting.py
@@ -0,0 +1,29 @@
+import uuid
+
+from thehive4py import TheHiveApi
+from thehive4py.types.alert import InputAlert
+from thehive4py.types.observable import InputObservable
+
+hive = TheHiveApi(url="thehive.example", apikey="h1v3b33")
+
+input_alert: InputAlert = {
+ "type": "alert-without-observables",
+ "source": "example",
+ "sourceRef": uuid.uuid4().hex,
+ "title": "alert without observables",
+ "description": "alert without observables",
+}
+
+output_alert = hive.alert.create(alert=input_alert)
+
+
+input_observables: list[InputObservable] = [
+ {"dataType": "ip", "data": "1.2.3.4"},
+ {"dataType": "domain", "data": "example.com"},
+]
+
+
+for input_observable in input_observables:
+ hive.alert.create_observable(
+ alert_id=output_alert["_id"], observable=input_observable
+ )
diff --git a/examples/alert/observable_during_alerting.py b/examples/alert/observable_during_alerting.py
new file mode 100644
index 0000000..9119d85
--- /dev/null
+++ b/examples/alert/observable_during_alerting.py
@@ -0,0 +1,20 @@
+import uuid
+
+from thehive4py import TheHiveApi
+from thehive4py.types.alert import InputAlert
+
+hive = TheHiveApi(url="thehive.example", apikey="h1v3b33")
+
+input_alert: InputAlert = {
+ "type": "alert-with-observables",
+ "source": "example",
+ "sourceRef": uuid.uuid4().hex,
+ "title": "alert with observables",
+ "description": "alert with observables",
+ "observables": [
+ {"dataType": "ip", "data": "1.2.3.4"},
+ {"dataType": "domain", "data": "example.com"},
+ ],
+}
+
+hive.alert.create(alert=input_alert)
diff --git a/examples/alert/observable_from_file.py b/examples/alert/observable_from_file.py
new file mode 100644
index 0000000..9331fcc
--- /dev/null
+++ b/examples/alert/observable_from_file.py
@@ -0,0 +1,29 @@
+import os.path
+import tempfile
+import uuid
+
+from thehive4py import TheHiveApi
+from thehive4py.types.alert import InputAlert
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+
+with tempfile.TemporaryDirectory() as tmpdir:
+
+ observable_filepath = os.path.join(tmpdir, "my-observable.txt")
+ with open(observable_filepath) as observable_file:
+ observable_file.write("some observable content")
+
+ attachment_key = uuid.uuid4().hex
+ attachment_map = {attachment_key: observable_filepath}
+ input_alert: InputAlert = {
+ "type": "alert-with-file-observable",
+ "source": "example",
+ "sourceRef": uuid.uuid4().hex,
+ "title": "alert with file observables",
+ "description": "alert with file observables",
+ "observables": [
+ {"dataType": "file", "attachment": attachment_key},
+ ],
+ }
+
+ hive.alert.create(alert=input_alert, attachment_map=attachment_map)
diff --git a/examples/alert/simple.py b/examples/alert/simple.py
new file mode 100644
index 0000000..29abfa0
--- /dev/null
+++ b/examples/alert/simple.py
@@ -0,0 +1,13 @@
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+
+simple_alert = hive.alert.create(
+ alert={
+ "type": "simple",
+ "source": "tutorial",
+ "sourceRef": "should-be-unique",
+ "title": "a simple alert",
+ "description": "a bit too simple",
+ }
+)
diff --git a/examples/alert/update_bulk.py b/examples/alert/update_bulk.py
new file mode 100644
index 0000000..1d07d38
--- /dev/null
+++ b/examples/alert/update_bulk.py
@@ -0,0 +1,28 @@
+import uuid
+
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+
+original_alert_ids = []
+for i in range(2):
+ original_alert = hive.alert.create(
+ alert={
+ "type": "update-bulk",
+ "source": "tutorial",
+ "sourceRef": uuid.uuid4().hex,
+ "title": f"original alert #{i}",
+ "description": "an alert to update in bulk",
+ }
+ )
+
+ original_alert_ids.append(original_alert["_id"])
+
+
+hive.alert.bulk_update(
+ fields={
+ "ids": original_alert_ids,
+ "title": "bulk updated alert",
+ "tags": ["update-bulk"],
+ },
+)
diff --git a/examples/alert/update_single.py b/examples/alert/update_single.py
new file mode 100644
index 0000000..2730b21
--- /dev/null
+++ b/examples/alert/update_single.py
@@ -0,0 +1,26 @@
+import uuid
+
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(url="http://localhost:9000", apikey="h1v3b33")
+
+original_alert = hive.alert.create(
+ alert={
+ "type": "update-single",
+ "source": "tutorial",
+ "sourceRef": uuid.uuid4().hex,
+ "title": "original alert",
+ "description": "a single alert to update",
+ }
+)
+
+
+hive.alert.update(
+ alert_id=original_alert["_id"],
+ fields={
+ "title": "updated alert",
+ "tags": ["update-single"],
+ },
+)
+
+updated_alert = hive.alert.get(alert_id=original_alert["_id"])
diff --git a/examples/client/auth_with_apikey.py b/examples/client/auth_with_apikey.py
new file mode 100644
index 0000000..00e392a
--- /dev/null
+++ b/examples/client/auth_with_apikey.py
@@ -0,0 +1,6 @@
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(
+ url="http://localhost:9000",
+ apikey="h1v3b33",
+)
diff --git a/examples/client/auth_with_username_and_password.py b/examples/client/auth_with_username_and_password.py
new file mode 100644
index 0000000..dcff340
--- /dev/null
+++ b/examples/client/auth_with_username_and_password.py
@@ -0,0 +1,7 @@
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(
+ url="http://localhost:9000",
+ username="analyst@thehive",
+ password="h1v3b33",
+)
diff --git a/examples/client/org_during_runtime.py b/examples/client/org_during_runtime.py
new file mode 100644
index 0000000..075e677
--- /dev/null
+++ b/examples/client/org_during_runtime.py
@@ -0,0 +1,8 @@
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(
+ url="http://localhost:9000",
+ apikey="h1v3b33",
+)
+
+hive.session_organisation = "other-org"
diff --git a/examples/client/org_via_constructor.py b/examples/client/org_via_constructor.py
new file mode 100644
index 0000000..6aac0da
--- /dev/null
+++ b/examples/client/org_via_constructor.py
@@ -0,0 +1,7 @@
+from thehive4py import TheHiveApi
+
+client_with_organisation = TheHiveApi(
+ url="http://localhost:9000",
+ apikey="h1v3b33",
+ organisation="admin",
+)
diff --git a/examples/client/retries.py b/examples/client/retries.py
new file mode 100644
index 0000000..f302f78
--- /dev/null
+++ b/examples/client/retries.py
@@ -0,0 +1,16 @@
+from urllib3 import Retry
+
+from thehive4py import TheHiveApi
+
+simple_retries = Retry(
+ total=5,
+ backoff_factor=0.5,
+ allowed_methods=["GET"],
+ status_forcelist=[500],
+)
+
+hive = TheHiveApi(
+ url="http://localhost:9000",
+ apikey="h1v3b33",
+ max_retries=simple_retries,
+)
diff --git a/examples/client/ssl.py b/examples/client/ssl.py
new file mode 100644
index 0000000..36b3171
--- /dev/null
+++ b/examples/client/ssl.py
@@ -0,0 +1,7 @@
+from thehive4py import TheHiveApi
+
+hive = TheHiveApi(
+ url="http://localhost:9000",
+ apikey="h1v3b33",
+ verify="/etc/ssl/certs/ca-certificates.crt",
+)
diff --git a/mkdocs.yml b/mkdocs.yml
index 9c0d51e..f914c95 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,10 +1,22 @@
-site_name: TheHive4py
+site_name: thehive4py
+repo_name: TheHive-Project/TheHive4py
+repo_url: https://github.com/TheHive-Project/TheHive4py
+nav:
+ - TheHive4py: index.md
+ - Examples:
+ - Client: examples/client.md
+ - Alert: examples/alert.md
+ - Reference: reference.md
theme:
name: material
favicon: img/strangebee.png
logo: img/strangebee.png
features:
+ - content.code.copy
- navigation.footer
+ - navigation.tabs
+ - navigation.tabs.sticky
+ - toc.follow
palette:
- media: "(prefers-color-scheme)"
toggle:
@@ -26,14 +38,15 @@ theme:
name: Switch to system preference
extra_css:
- styles/extra.css
-repo_name: TheHive-Project/TheHive4py
-repo_url: https://github.com/TheHive-Project/TheHive4py
markdown_extensions:
+ - admonition
- attr_list
- md_in_html
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
+ - pymdownx.snippets
+ - pymdownx.superfences
extra:
social:
- icon: simple/discord
@@ -45,5 +58,27 @@ extra:
- icon: simple/python
link: https://pypi.org/project/thehive4py/
name: PyPI
-
-
+ version:
+ provider: mike
+plugins:
+ - autorefs
+ - mkdocstrings:
+ handlers:
+ python:
+ import:
+ - https://docs.python.org/3/objects.inv
+ - https://requests.readthedocs.io/en/stable/objects.inv
+ options:
+ show_root_heading: true
+ show_root_full_path: true
+ merge_init_into_class: true
+ show_source: true
+ show_if_no_docstring: true
+ members_order: source
+ docstring_section_style: table
+ signature_crossrefs: true
+ show_symbol_type_heading: true
+ show_symbol_type_toc: true
+watch:
+ - examples
+ - thehive4py
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index e0a87f7..c1b55a9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,10 +29,10 @@ authors = [{ name = "Szabolcs Antal", email = "antalszabolcs01@gmail.com" }]
[project.optional-dependencies]
audit = ["bandit", "pip-audit"]
build = ["build", "twine"]
-docs = ["mkdocs", "mkdocs-material"]
+docs = ["mkdocs", "mkdocs-material", "mkdocstrings-python", "mike"]
lint = ["black", "flake8-pyproject", "mypy", "pre-commit"]
test = ["pytest", "pytest-cov"]
-dev = ["thehive4py[audit, lint, test, build]"]
+dev = ["thehive4py[audit, lint, test, build, docs]"]
[tool.setuptools.packages.find]
include = ["thehive4py*"]
diff --git a/scripts/cd.py b/scripts/cd.py
index bf366e0..020f4ef 100755
--- a/scripts/cd.py
+++ b/scripts/cd.py
@@ -57,7 +57,7 @@ def run_build_docs(quiet: bool):
def run_deploy_docs(quiet: bool):
print("Deploying thehive4py docs to gh-pages...")
_run_subprocess(
- command="mkdocs gh-deploy --force",
+ command="mike deploy main latest -u -p --allow-empty",
quiet=quiet,
)
print("Successfully deployed thehive4py docs to gh-pages!")
diff --git a/thehive4py/client.py b/thehive4py/client.py
index f731d63..3a24340 100644
--- a/thehive4py/client.py
+++ b/thehive4py/client.py
@@ -17,7 +17,7 @@
from thehive4py.endpoints.custom_field import CustomFieldEndpoint
from thehive4py.endpoints.observable_type import ObservableTypeEndpoint
from thehive4py.endpoints.query import QueryEndpoint
-from thehive4py.session import DEFAULT_RETRY, RetryValue, TheHiveSession
+from thehive4py.session import DEFAULT_RETRY, RetryValue, TheHiveSession, VerifyValue
class TheHiveApi:
@@ -28,9 +28,27 @@ def __init__(
username: Optional[str] = None,
password: Optional[str] = None,
organisation: Optional[str] = None,
- verify=None,
+ verify: VerifyValue = True,
max_retries: RetryValue = DEFAULT_RETRY,
):
+ """Create a client of TheHive API.
+
+ Parameters:
+ url: TheHive's url.
+ apikey: TheHive's apikey. It's required if `username` and `password`
+ is not provided.
+ username: TheHive's username. It's required if `apikey` is not provided.
+ Must be specified together with `password`.
+ password: TheHive's password. It's required if `apikey` is not provided.
+ Must be specified together with `username`.
+ organisation: TheHive organisation to use in the session.
+ verify: Either a boolean, in which case it controls whether we verify
+ the server's TLS certificate, or a string, in which case it must be a
+ path to a CA bundle to use.
+ max_retries: Either `None`, in which case we do not retry failed requests,
+ or a `Retry` object.
+
+ """
self.session = TheHiveSession(
url=url,
apikey=apikey,
diff --git a/thehive4py/endpoints/alert.py b/thehive4py/endpoints/alert.py
index 9bd1967..543e40d 100644
--- a/thehive4py/endpoints/alert.py
+++ b/thehive4py/endpoints/alert.py
@@ -24,6 +24,15 @@ class AlertEndpoint(EndpointBase):
def create(
self, alert: InputAlert, attachment_map: Optional[Dict[str, str]] = None
) -> OutputAlert:
+ """Create an alert.
+
+ Args:
+ alert: The body of the alert.
+ attachment_map: An optional mapping of observable attachment keys and paths.
+
+ Returns:
+ The created alert.
+ """
if attachment_map:
files: Dict[str, Any] = {
key: self._fileinfo_from_filepath(path)
@@ -36,35 +45,102 @@ def create(
return self._session.make_request("POST", path="/api/v1/alert", **kwargs)
def get(self, alert_id: str) -> OutputAlert:
+ """Get an alert by id.
+
+ Args:
+ alert_id: The id of the alert.
+
+ Returns:
+ The alert specified by the id.
+ """
+
return self._session.make_request("GET", path=f"/api/v1/alert/{alert_id}")
def update(self, alert_id: str, fields: InputUpdateAlert) -> None:
+ """Update an alert.
+
+ Args:
+ alert_id: The id of the alert.
+ fields: The fields of the alert to update.
+
+ Returns:
+ N/A
+ """
return self._session.make_request(
"PATCH", path=f"/api/v1/alert/{alert_id}", json=fields
)
def delete(self, alert_id: str) -> None:
+ """Delete an alert.
+
+ Args:
+ alert_id: The id of the alert.
+
+ Returns:
+ N/A
+ """
return self._session.make_request("DELETE", path=f"/api/v1/alert/{alert_id}")
def bulk_update(self, fields: InputBulkUpdateAlert) -> None:
+ """Update multiple alerts with the same values.
+
+ Args:
+ fields: The ids and the fields of the alerts to update.
+
+ Returns:
+ N/A
+ """
return self._session.make_request(
"PATCH", path="/api/v1/alert/_bulk", json=fields
)
def bulk_delete(self, ids: List[str]) -> None:
+ """Delete multiple alerts.
+
+ Args:
+ ids: The ids of the alerts to delete.
+
+ Returns:
+ N/A
+ """
return self._session.make_request(
"POST", path="/api/v1/alert/delete/_bulk", json={"ids": ids}
)
def follow(self, alert_id: str) -> None:
+ """Follow an alert.
+
+ Args:
+ alert_id: The id of the alert.
+
+ Returns:
+ N/A
+ """
self._session.make_request("POST", path=f"/api/v1/alert/{alert_id}/follow")
def unfollow(self, alert_id: str) -> None:
+ """Unfollow an alert.
+
+ Args:
+ alert_id: The id of the alert.
+
+ Returns:
+ N/A
+ """
self._session.make_request("POST", path=f"/api/v1/alert/{alert_id}/unfollow")
def promote_to_case(
self, alert_id: str, fields: InputPromoteAlert = {}
) -> OutputCase:
+ """Promote an alert into a case.
+
+ Args:
+ alert_id: The id of the alert.
+ fields: Override for the fields of the case created from the alert.
+
+ Returns:
+ The case from the promoted alert.
+ """
return self._session.make_request(
"POST",
path=f"/api/v1/alert/{alert_id}/case",
@@ -77,6 +153,17 @@ def create_observable(
observable: InputObservable,
observable_path: Optional[str] = None,
) -> List[OutputObservable]:
+ """Create an observable in an alert.
+
+ Args:
+ alert_id: The id of the alert.
+ observable: The fields of the observable to create.
+ observable_path: Optional path in case of a file based observable.
+
+ Returns:
+ The created alert observables.
+ """
+
kwargs = self._build_observable_kwargs(
observable=observable, observable_path=observable_path
)
@@ -87,6 +174,15 @@ def create_observable(
def add_attachment(
self, alert_id: str, attachment_paths: List[str]
) -> List[OutputAttachment]:
+ """Create an observable in an alert.
+
+ Args:
+ alert_id: The id of the alert.
+ attachment_paths: List of paths to the attachments to create.
+
+ Returns:
+ The created alert attachments.
+ """
files = [
("attachments", self._fileinfo_from_filepath(attachment_path))
for attachment_path in attachment_paths
@@ -98,6 +194,16 @@ def add_attachment(
def download_attachment(
self, alert_id: str, attachment_id: str, attachment_path: str
) -> None:
+ """Download an alert attachment.
+
+ Args:
+ alert_id: The id of the alert.
+ attachment_id: The id of the alert attachment.
+ attachment_path: The local path to download the attachment to.
+
+ Returns:
+ N/A
+ """
return self._session.make_request(
"GET",
path=f"/api/v1/alert/{alert_id}/attachment/{attachment_id}/download",
@@ -105,16 +211,44 @@ def download_attachment(
)
def delete_attachment(self, alert_id: str, attachment_id: str) -> None:
+ """Delete an alert attachment.
+
+ Args:
+ alert_id: The id of the alert.
+ attachment_id: The id of the alert attachment.
+
+ Returns:
+ N/A
+ """
+
return self._session.make_request(
"DELETE", path=f"/api/v1/alert/{alert_id}/attachment/{attachment_id}"
)
def merge_into_case(self, alert_id: str, case_id: str) -> OutputCase:
+ """Merge an alert into an existing case.
+
+ Args:
+ alert_id: The id of the alert to merge.
+ case_id: The id of the case to merge the alert into.
+
+ Returns:
+ The case into which the alert was merged.
+ """
return self._session.make_request(
"POST", path=f"/api/v1/alert/{alert_id}/merge/{case_id}"
)
def bulk_merge_into_case(self, case_id: str, alert_ids: List[str]) -> OutputCase:
+ """Merge an alert into an existing case.
+
+ Args:
+ case_id: The id of the case to merge the alerts into.
+ alert_ids: The list of alert ids to merge.
+
+ Returns:
+ The case into which the alerts were merged.
+ """
return self._session.make_request(
"POST",
path="/api/v1/alert/merge/_bulk",
@@ -127,6 +261,16 @@ def find(
sortby: Optional[SortExpr] = None,
paginate: Optional[Paginate] = None,
) -> List[OutputAlert]:
+ """Find multiple alerts.
+
+ Args:
+ filters: The filter expressions to apply in the query.
+ sortby: The sort expressions to apply in the query.
+ paginate: The pagination experssion to apply in the query.
+
+ Returns:
+ The list of alerts matched by the query or an empty list.
+ """
query: QueryExpr = [
{"_name": "listAlert"},
*self._build_subquery(filters=filters, sortby=sortby, paginate=paginate),
@@ -140,6 +284,15 @@ def find(
)
def count(self, filters: Optional[FilterExpr] = None) -> int:
+ """Count alerts.
+
+ Args:
+ filters: The filter expressions to apply in the query.
+
+ Returns:
+ The count of alerts matched by the query.
+ """
+
query: QueryExpr = [
{"_name": "listAlert"},
*self._build_subquery(filters=filters),
@@ -160,6 +313,17 @@ def find_observables(
sortby: Optional[SortExpr] = None,
paginate: Optional[Paginate] = None,
) -> List[OutputObservable]:
+ """Find observable related to an alert.
+
+ Args:
+ alert_id: The id of the alert.
+ filters: The filter expressions to apply in the query.
+ sortby: The sort expressions to apply in the query.
+ paginate: The pagination experssion to apply in the query.
+
+ Returns:
+ The list of alert observables matched by the query or an empty list.
+ """
query: QueryExpr = [
{"_name": "getAlert", "idOrName": alert_id},
{"_name": "observables"},
@@ -179,6 +343,17 @@ def find_comments(
sortby: Optional[SortExpr] = None,
paginate: Optional[Paginate] = None,
) -> List[OutputComment]:
+ """Find comments related to an alert.
+
+ Args:
+ alert_id: The id of the alert.
+ filters: The filter expressions to apply in the query.
+ sortby: The sort expressions to apply in the query.
+ paginate: The pagination experssion to apply in the query.
+
+ Returns:
+ The list of alert comments matched by the query or an empty list.
+ """
query: QueryExpr = [
{"_name": "getAlert", "idOrName": alert_id},
{"_name": "comments"},
@@ -194,6 +369,15 @@ def find_comments(
def create_procedure(
self, alert_id: str, procedure: InputProcedure
) -> OutputProcedure:
+ """Create an alert procedure.
+
+ Args:
+ alert_id: The id of the alert.
+ procedure: The fields of the procedure to create.
+
+ Returns:
+ The created alert procedure.
+ """
return self._session.make_request(
"POST", path=f"/api/v1/alert/{alert_id}/procedure", json=procedure
)
@@ -205,6 +389,17 @@ def find_procedures(
sortby: Optional[SortExpr] = None,
paginate: Optional[Paginate] = None,
) -> List[OutputProcedure]:
+ """Find procedures related to an alert.
+
+ Args:
+ alert_id: The id of the alert.
+ filters: The filter expressions to apply in the query.
+ sortby: The sort expressions to apply in the query.
+ paginate: The pagination experssion to apply in the query.
+
+ Returns:
+ The list of alert procedures matched by the query or an empty list.
+ """
query: QueryExpr = [
{"_name": "getAlert", "idOrName": alert_id},
{"_name": "procedures"},
@@ -225,6 +420,17 @@ def find_attachments(
sortby: Optional[SortExpr] = None,
paginate: Optional[Paginate] = None,
) -> List[OutputAttachment]:
+ """Find attachments related to an alert.
+
+ Args:
+ alert_id: The id of the alert.
+ filters: The filter expressions to apply in the query.
+ sortby: The sort expressions to apply in the query.
+ paginate: The pagination experssion to apply in the query.
+
+ Returns:
+ The list of alert attachments matched by the query or an empty list.
+ """
query: QueryExpr = [
{"_name": "getAlert", "idOrName": alert_id},
{"_name": "attachments"},
diff --git a/thehive4py/errors.py b/thehive4py/errors.py
index b009444..bab6f9b 100644
--- a/thehive4py/errors.py
+++ b/thehive4py/errors.py
@@ -4,11 +4,16 @@
class TheHiveError(Exception):
- """Base error class for TheHive API."""
def __init__(
self, message: str, response: Optional[Response] = None, *args, **kwargs
):
+ """Base error class of thehive4py.
+
+ Args:
+ message: The exception message.
+ response: Either `None`, or a `Response` object of a failed request.
+ """
super().__init__(message, *args, **kwargs)
self.message = message
self.response = response
diff --git a/thehive4py/session.py b/thehive4py/session.py
index e3e261d..3ffb2a6 100644
--- a/thehive4py/session.py
+++ b/thehive4py/session.py
@@ -21,9 +21,10 @@
RetryValue = Union[Retry, int, None]
+VerifyValue = Union[bool, str]
-class SessionJSONEncoder(jsonlib.JSONEncoder):
+class _SessionJSONEncoder(jsonlib.JSONEncoder):
"""Custom JSON encoder class for TheHive session."""
def default(self, o: Any):
@@ -39,7 +40,7 @@ def __init__(
apikey: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
- verify=None,
+ verify: VerifyValue = True,
max_retries: RetryValue = DEFAULT_RETRY,
):
super().__init__()
@@ -85,7 +86,7 @@ def make_request(
headers = {**self.headers}
if json:
- data = jsonlib.dumps(json, cls=SessionJSONEncoder)
+ data = jsonlib.dumps(json, cls=_SessionJSONEncoder)
headers = {**headers, "Content-Type": "application/json"}
response = self.request(