Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 76 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,13 @@
---


## 🆕 Latest Updates (v0.8.55)
## 🆕 Latest Updates (v0.8.56)

- **Feature: Add 13 missing parameters to devicesummary endpoint** - Added support for `device_name`, `ip_address`, `end_timestamp`, `start_timestamp`, `devicesummary_by`, `devicesummary_by_value`, `device_type`, `network_location`, `network_location_id`, `peer_id`, `source`, and `status` parameters to align with Darktrace API specification
- **Documentation: Update devicesummary documentation** - Added examples and parameter descriptions for new filtering options
- **Note: devicesummary HTTP 500 limitation confirmed** - Documentation updated to clarify that all devicesummary parameters return HTTP 500 with API token authentication (Darktrace backend limitation, not SDK bug)
- **Feature: Configurable request timeouts** - Added `timeout` parameter to `DarktraceClient` and all endpoint methods. Supports client-level defaults and per-request overrides. Use for long-running queries like advanced search.
- **Security: Enable SSL certificate verification by default (fixes #47)** - Changed `verify_ssl` default from `False` to `True` for secure-by-default behavior. All endpoint modules now inherit the client's SSL verification setting.
- **Documentation: Add SSL certificate setup guide** - Added instructions for using self-signed certificates with `verify_ssl=True` via system trust store or environment variable.

## 📝 Previous Updates (v0.8.54)

- **Fix: Multi-parameter devicesearch query format (fixes #45)** - Changed query parameter joining from explicit ' AND ' to space separation per Darktrace API specification
- **Fix: ensure host URL includes protocol (default to https if missing)**
> For previous updates, see [GitHub Releases](https://github.com/LegendEvent/darktrace-sdk/releases).

---

Expand All @@ -32,6 +29,7 @@
- **Modular & Maintainable**: Each endpoint group is a separate Python module/class.
- **Easy Authentication**: Secure HMAC-SHA1 signature generation and token management.
- **SSL Verification**: SSL certificate verification is enabled by default for secure connections.
- **Configurable Timeouts**: Client-level and per-request timeout support for long-running queries.
- **Async-Ready**: Designed for easy extension to async workflows.
- **Type Hints & Docstrings**: Full typing and documentation for all public methods.
- **Comprehensive Documentation**: Detailed documentation for every module and endpoint.
Expand All @@ -55,6 +53,76 @@ client = DarktraceClient(

> ⚠️ **Warning**: Disabling SSL verification exposes your connection to man-in-the-middle attacks. Never disable in production environments.

### Using Self-Signed Certificates with verify_ssl=True

For production environments with self-signed certificates, add the certificate to your system trust store instead of disabling verification:

```bash
# 1. Get the certificate from your Darktrace instance
openssl s_client -showcerts -connect your-darktrace-instance:443 </dev/null 2>/dev/null | openssl x509 -outform PEM > ~/darktrace-cert.pem

# 2. Copy to system CA store (Linux/Ubuntu/Debian)
sudo cp ~/darktrace-cert.pem /usr/local/share/ca-certificates/darktrace-cert.crt
sudo update-ca-certificates

# 3. Now verify_ssl=True will work
```

**Alternative (no sudo required):**
```bash
# Create a custom CA bundle and set environment variable
cat /etc/ssl/certs/ca-certificates.crt ~/darktrace-cert.pem > ~/.custom-ca-bundle.pem
export REQUESTS_CA_BUNDLE=~/.custom-ca-bundle.pem
```

---

## ⏱️ Request Timeouts

The SDK supports configurable request timeouts at both client and per-request levels.

### Client-Level Timeout

Set a default timeout for all requests:

```python
from darktrace import DarktraceClient

# 30 second timeout for all requests
client = DarktraceClient(
host="https://your-darktrace-instance",
public_token="YOUR_PUBLIC_TOKEN",
private_token="YOUR_PRIVATE_TOKEN",
timeout=30
)
```

### Per-Request Timeout

Override the timeout for specific requests (e.g., long-running advanced searches):

```python
# Client default: 30 seconds
client = DarktraceClient(host="...", public_token="...", private_token="...", timeout=30)

# Override for slow query (5 minutes)
results = client.advanced_search.search(query, timeout=300)

# Override with tuple format (connect_timeout, read_timeout)
results = client.advanced_search.search(query, timeout=(5, 300))
```

### Timeout Format

| Format | Description |
|--------|-------------|
| (not passed) | Uses client default timeout |
| `timeout=None` | No timeout (disables client default) |
| `timeout=30` | 30 seconds total |
| `timeout=(5, 30)` | 5 seconds connect, 30 seconds read |

> **Note**: Advanced search queries can take 5-10 minutes for complex queries. Consider using per-request timeouts for these endpoints.

---

## 📦 Installation
Expand Down
5 changes: 3 additions & 2 deletions darktrace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from .dt_subnets import Subnets
from .dt_summarystatistics import SummaryStatistics
from .dt_tags import Tags
from .dt_utils import debug_print
from .dt_utils import debug_print, TimeoutType
from .dt_components import Components
from .dt_cves import CVEs
from .dt_details import Details
Expand Down Expand Up @@ -61,5 +61,6 @@
'DeviceSearch',
'ModelBreaches',
'AdvancedSearch',
'debug_print'
'debug_print',
'TimeoutType',
]
28 changes: 24 additions & 4 deletions darktrace/client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from typing import TYPE_CHECKING

from .auth import DarktraceAuth
from .dt_antigena import Antigena
from .dt_analyst import Analyst
from .dt_breaches import ModelBreaches
from .dt_devices import Devices
from .dt_email import DarktraceEmail
from .dt_utils import debug_print
from .dt_utils import debug_print, TimeoutType
from .dt_advanced_search import AdvancedSearch
from .dt_components import Components
from .dt_cves import CVEs
Expand All @@ -28,8 +30,6 @@
from .dt_summarystatistics import SummaryStatistics
from .dt_tags import Tags

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .dt_antigena import Antigena
from .dt_analyst import Analyst
Expand Down Expand Up @@ -65,6 +65,7 @@ class DarktraceClient:
auth: DarktraceAuth
debug: bool
verify_ssl: bool
timeout: TimeoutType
advanced_search: 'AdvancedSearch'
antigena: 'Antigena'
analyst: 'Analyst'
Expand Down Expand Up @@ -93,7 +94,15 @@ class DarktraceClient:
summarystatistics: 'SummaryStatistics'
tags: 'Tags'

def __init__(self, host: str, public_token: str, private_token: str, debug: bool = False, verify_ssl: bool = True) -> None:
def __init__(
self,
host: str,
public_token: str,
private_token: str,
debug: bool = False,
verify_ssl: bool = True,
timeout: TimeoutType = None
) -> None:
Comment on lines +97 to +105
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description states the change is fully backwards compatible and focused on timeouts, but verify_ssl is added here with a default of True, which changes behavior for users relying on the previous default (notably self-signed cert setups). Please update the PR description/release notes to call out this behavioral change explicitly (or consider deferring it to a separate PR).

Copilot uses AI. Check for mistakes.
"""
Initialize the Darktrace API client.

Expand All @@ -104,6 +113,8 @@ def __init__(self, host: str, public_token: str, private_token: str, debug: bool
debug (bool, optional): Enable debug logging. Defaults to False.
verify_ssl (bool, optional): Enable SSL certificate verification. Defaults to True.
Set to False only for development/testing with self-signed certificates.
timeout (float|tuple, optional): Request timeout in seconds. Can be a single float
or a tuple of (connect_timeout, read_timeout). None means no timeout (default).

Example:
>>> client = DarktraceClient(
Expand All @@ -112,6 +123,14 @@ def __init__(self, host: str, public_token: str, private_token: str, debug: bool
... private_token="your_private_token",
... debug=True
... )

>>> # With timeout
>>> client = DarktraceClient(
... host="https://your-instance.darktrace.com",
... public_token="your_public_token",
... private_token="your_private_token",
... timeout=30 # 30 second timeout for all requests
... )
"""

# Ensure host has a protocol
Expand All @@ -123,6 +142,7 @@ def __init__(self, host: str, public_token: str, private_token: str, debug: bool
self.auth = DarktraceAuth(public_token, private_token)
self.debug = debug
self.verify_ssl = verify_ssl
self.timeout = timeout

# Endpoint groups
self.advanced_search = AdvancedSearch(self)
Expand Down
22 changes: 13 additions & 9 deletions darktrace/dt_advanced_search.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import requests
import json
from typing import Dict, Any
from .dt_utils import debug_print, BaseEndpoint, encode_query
from typing import Dict, Any, Optional, Union, Tuple
from .dt_utils import debug_print, BaseEndpoint, encode_query, _UNSET

class AdvancedSearch(BaseEndpoint):
def __init__(self, client):
super().__init__(client)

def search(self, query: Dict[str, Any], post_request: bool = False):
def search(self, query: Dict[str, Any], post_request: bool = False, timeout: Optional[Union[float, Tuple[float, float]]] = _UNSET): # type: ignore[assignment]
"""Perform Advanced Search query.

Parameters:
Expand Down Expand Up @@ -59,7 +59,8 @@ def search(self, query: Dict[str, Any], post_request: bool = False):
headers, sorted_params = self._get_headers(endpoint, json_body=body)
headers['Content-Type'] = 'application/json'
self.client._debug(f"POST {url} body={body}")
response = requests.post(url, headers=headers, data=json.dumps(body, separators=(',', ':')), verify=self.client.verify_ssl)
resolved_timeout = self._resolve_timeout(timeout)
response = requests.post(url, headers=headers, data=json.dumps(body, separators=(',', ':')), verify=self.client.verify_ssl, timeout=resolved_timeout)
self.client._debug(f"Response status: {response.status_code}")
self.client._debug(f"Response text: {response.text}")
response.raise_for_status()
Expand Down Expand Up @@ -89,28 +90,31 @@ def search(self, query: Dict[str, Any], post_request: bool = False):
url = f"{self.client.host}{endpoint}/{encoded_query}"
headers, sorted_params = self._get_headers(f"{endpoint}/{encoded_query}")
self.client._debug(f"GET {url}")
response = requests.get(url, headers=headers, params=sorted_params, verify=self.client.verify_ssl)
resolved_timeout = self._resolve_timeout(timeout)
response = requests.get(url, headers=headers, params=sorted_params, verify=self.client.verify_ssl, timeout=resolved_timeout)
response.raise_for_status()
return response.json()

def analyze(self, field: str, analysis_type: str, query: Dict[str, Any]):
def analyze(self, field: str, analysis_type: str, query: Dict[str, Any], timeout: Optional[Union[float, Tuple[float, float]]] = _UNSET): # type: ignore[assignment]
"""Analyze field data."""
encoded_query = encode_query(query)
endpoint = f'/advancedsearch/api/analyze/{field}/{analysis_type}/{encoded_query}'
url = f"{self.client.host}{endpoint}"
headers, sorted_params = self._get_headers(endpoint)
self.client._debug(f"GET {url}")
response = requests.get(url, headers=headers, params=sorted_params, verify=self.client.verify_ssl)
resolved_timeout = self._resolve_timeout(timeout)
response = requests.get(url, headers=headers, params=sorted_params, verify=self.client.verify_ssl, timeout=resolved_timeout)
response.raise_for_status()
return response.json()

def graph(self, graph_type: str, interval: int, query: Dict[str, Any]):
def graph(self, graph_type: str, interval: int, query: Dict[str, Any], timeout: Optional[Union[float, Tuple[float, float]]] = _UNSET): # type: ignore[assignment]
"""Get graph data."""
encoded_query = encode_query(query)
endpoint = f'/advancedsearch/api/graph/{graph_type}/{interval}/{encoded_query}'
url = f"{self.client.host}{endpoint}"
headers, sorted_params = self._get_headers(endpoint)
self.client._debug(f"GET {url}")
response = requests.get(url, headers=headers, params=sorted_params, verify=self.client.verify_ssl)
resolved_timeout = self._resolve_timeout(timeout)
response = requests.get(url, headers=headers, params=sorted_params, verify=self.client.verify_ssl, timeout=resolved_timeout)
response.raise_for_status()
return response.json()
Loading