Get Started with the XDR SDK for Python๐
Note
Use the following region or environment identifiers, depending on your tenant region:
US1orcharlieorproductionfor https://ctpx.secureworks.com/US2ordeltafor https://delta.taegis.secureworks.com/US3orfoxtrotfor https://foxtrot.taegis.secureworks.com/EU1orechofor https://echo.taegis.secureworks.com/EU2orgolffor https://golf.taegis.secureworks.com/
Note
production is useful for partners with child tenants that want to interate API calls over multiple tenants using the Tenants API. The Tenants API uses the production identifier rather than charlie or US1, but this will direct the SDK to the correct region.
Install the Taegis SDK for Python๐
Assuming you have Python 3.8+ already installed, install the Taegis SDK for Python:
Authenticate๐
The install will ask to authenticate with your Secureworksยฎ Taegisโข XDR account. This requires a password and MFA token, unless your organization has registered your organization with SSO authentication. If SSO is enabled, you will be presented with a device code authentication link.
For more information, see Authenticate with Taegis SDK for Python.
Example Usage๐
from taegis_sdk_python import GraphQLService
from pprint import pprint as pp
service = GraphQLService()
results = service.subjects.query.current_subject()
pp(results)
Exploring the SDK๐
The SDK was built around utilizing the Python built-in help. You can use help on any object within the GraphQLService object structure to understand what is available and how to call it. The help menu is a great resource for determining input types. Each service is self contained so that if you need an input, like SearchRequestInput, you will find it under taegis_sdk_python.services.<service>.types.
from taegis_sdk_python import GraphQLService
from taegis_sdk_python.services.alerts.types import SearchRequestInput
service = GraphQLService()
# Find available services (Service Endpoints)
help(service)
# Find available service queries (or mutations or subscriptions)
help(service.alerts.query)
help(service.alerts.mutation)
help(service.alerts.subscription)
# Reference documentation on specific endpoint
help(service.alerts.query.alerts_service_search)
# Help on an Input variable
help(SearchRequestInput)
# service
class GraphQLService(builtins.object)
| GraphQLService(*, environment: Optional[str] = None, tenant_id: Optional[str] = None, environments: Optional[Dict[str, str]] = None, gateway: Optional[str] = None)
...
| agent
| Events Service Endpoint.
|
| alerts
| Alerts2 Service Endpoint.
|
| assets
| Assets
...
# Alerts Query
class TaegisSDKAlertsQuery(builtins.object)
| TaegisS
...
| alerts_service_aggregate_alerts_by_severity(self, in_: 'Optional[AggregateAlertsBySeverityInputInput]' = None) -> 'AlertsAggregateResponse'
| Pull alert severity aggregates based on `group_by` parameters: domain, watchlist, hostname, detector, user..
|
| alerts_service_alerts_dashboard_triage(self, in_: 'Optional[TriageDashboardInputInput]' = None) -> 'TriageDashboardOutput'
| None.
|
| alerts_service_poll(self, in_: 'Optional[PollRequestInput]' = None) -> 'AlertsResponse'
| Poll for results for a specific `search
...
# SearchRequestInput
class SearchRequestInput(builtins.object)
| SearchRequestInput(cql_query: Optional[str] = None, offset: Optional[int] = None, limit: Optional[int] = None) -> None
...
Note: Output has been truncated for verbosity.
Context Manager๐
The service object is also a context manager to help temporarily override default values when making an API call. This can include fields like the environment, tenant_id, output, or access_token.
from taegis_sdk_python import GraphQLService
from taegis_sdk_python.services.alerts.types import SearchRequestInput
service = GraphQLService()
with service(
environment="US2",
tenant_id="00000",
output="""
reason
alerts {
total_results
list {
id
tenant_id
metadata {
title
severity
}
status
}
}
""",
):
result = service.alerts.query.alerts_service_search(SearchRequestInput(
offset=0,
limit=10,
cql_query="""
FROM alert
WHERE
severity >= 0.6
EARLIEST=-1d
"""
))
Change Tenant Context๐
from taegis_sdk_python import GraphQLService
from taegis_sdk_python.services.investigations.types import InvestigationsV2Arguments
service = GraphQLService()
# specify the output fields, and start the service context
with service(tenant_id="00000"):
result = service.investigations2.query.investigations_v2(InvestigationsV2Arguments(
page=1,
per_page=3,
cql="WHERE deleted_at IS NOT NULL EARLIEST=-90d"
))
pp(result)
Change the Environment๐
from taegis_sdk_python import GraphQLService
from taegis_sdk_python.services.investigations.types import InvestigationsV2Arguments
service = GraphQLService()
# specify the output fields, and start the service context
with service(environment="US2"):
result = service.investigations2.query.investigations_v2(InvestigationsV2Arguments(
page=1,
per_page=3,
cql="WHERE deleted_at IS NOT NULL EARLIEST=-90d"
))
pp(result)
Use a Preexisting Access Token๐
from taegis_sdk_python import GraphQLService
from taegis_sdk_python.services.investigations.types import InvestigationsV2Arguments
service = GraphQLService()
# specify the output fields, and start the service context
with service(access_token="<your access token>"):
result = service.investigations2.query.investigations_v2(InvestigationsV2Arguments(
page=1,
per_page=3,
cql="WHERE deleted_at IS NOT NULL EARLIEST=-90d"
))
pp(result)
Pruning the GraphQL Output๐
One of the benefits of using GraphQL is that you can define which fields that you want returned. By default, we assume little to no knowledge of GraphQL to get you started, so we provide all the fields in the API call for exploration reasons. This may be unneeded for specific application or reporting purposes.
To assist with this, we have a utility called build_output_string. This returns a string representation of the output object with all possible fields for the return type. You can use this as reference to build your own, or modify it to remove fields that are not needed.
from taegis_sdk_python import build_output_string
from taegis_sdk_python.services.alerts.types import AlertsResponse
print(build_output_string(AlertsResponse))
reason search_id status alerts { previous_offset total_parts list { reference_details { reference {
description url type } } parent_tenant_id entities { entities relationships { relationship to_entity
from_entity type } } sensor_types suppression_rules { id version } resolution_history { timestamp {
nanos seconds } user_id status num_alerts_affected id reason } enrichment_details {
business_email_compromise { user_name source_address_geo_summary { country { confidence iso_code
geoname_id code } city { confidence locale_names { record { value key } } geoname_id name } location {
timezone latitude longitude us_metro_code radius metro_code gmt_offset } asn { autonomous_system_no
autonomous_system_org } continent { code geoname_id } } source_address }
...
Note: Output has been truncated for verbosity.
The service object can be called as a context manager with output assigned with the new GraphQL output fields. The same object will be returned, but fields not defined will be assigned a None value.
from taegis_sdk_python import GraphQLService
from taegis_sdk_python.services.alerts.types import SearchRequestInput
from pprint import pprint as pp
service = GraphQLService()
with service(output="search_id alerts { list { id status metadata { title severity confidence } } }")
results = service.alerts.query.alerts_service_search(
SearchRequestInput(
offset=0,
limit=10,
cql_query="""
FROM alert
WHERE
severity >= 0.6
EARLIEST=-1d
""",
)
)
pp(results)
Cascading Context๐
The Taegis SDK for Python service can handle a cascading context. Each invocation of the service context manager, now overwrites the context per stack. The main use case for this is to change the output of a single API call within a greater context without needing to exit the context entirely. Any new level will temporarily overwrite any previous context definitions, the previous definitions will be available after exiting the current context.
Note: The following example is for illustration purposes of the context manager. The mix of API calls may not be useful together.
from taegis_sdk_python import GraphQLService
from taegis_sdk_python.services.investigations2.types import InvestigationsV2Arguments
from taegis_sdk_python.services.alerts.types import SearchRequestInput
service = GraphQLService()
with service(environment="US2", tenant_id="00000"):
# Context
# environment: US2
# tenant_id: 00000
with service(output="investigations { id alertsEvidence { id } }")
# Context
# environment: US2
# tenant_id: 00000
# output: investigations { id alertsEvidence { id } }
investigation_results = service.investigations2.query.investigations_v2(InvestigationsV2Arguments(
page=1,
per_page=3,
cql="WHERE deleted_at IS NOT NULL EARLIEST=-90d"
))
# Context
# environment: US2
# tenant_id: 00000
alert_ids = [
alert.id
result for result in investigation_results
alert for alert in result.alerts_evidence
]
with service(output="alerts { list { id metadata { title } status } }"):
# Context
# environment: US2
# tenant_id: 00000
# output: alerts { list { id metadata { title } status } }
alert_results = service.alerts.query.alerts_search_search(SearchRequestInput(
offset=0,
limit=10000,
cql_query=f"FROM alert WHERE resource_id IN ('{'\',\''.join(alert_ids)]}')"
))
# Context
# environment: US2
# tenant_id: 00000
# may be useful for users/applications that have access to a parent/child tenant relationship
with service(tenant_id="00001", output="alerts { list { id metadata { title } status } }"):
# Context
# environment: US2
# tenant_id: 00001
# output: alerts { list { id metadata { title } status } }
alert_results = service.alerts.query.alerts_search_search(SearchRequestInput(
offset=0,
limit=10000,
cql_query=f"FROM alert WHERE resource_id IN ('{'\',\''.join(alert_ids)]}')"
))
with service(environment="US1", output="email"):
# Context
# environment: US1
# tenant_id: 00001
# output: email
user = service.users.query.current_tdruser()
# Context
# environment: US2
# tenant_id: 00001
# output: alerts { list { id metadata { title } status } }
# Context
# environment: US2
# tenant_id: 00000
# Context is now completely cleared
Arbitrary Queries๐
If you would like to run an API call that is different from the provided method or which the SDK does not support, you can craft your own query/mutation/subscription. Certain services may be configured differently; it is recommended to use the service endpoint you want to query against when available.
execute_queryexecute_mutationexecute_subscription
from taegis_sdk_python import GraphQLService
from pprint import pprint as pp
service = GraphQLService()
results = service.alerts.execute_query(
endpoint="alertsServiceSearch",
variables={
"in": {
"limit": 3,
"offset": 0,
"cql_query": """
FROM alert
WHERE
severity >= 0.6
EARLIEST=-1d
"""
}
},
output="""
search_id
alerts {
list {
id
tenant_id
metadata {
title
severity
}
status
}
}
"""
)
pp(results)
Arbitrary Mutation๐
results = service.core.execute_mutation(
"createInvestigation",
variables={
"investigation": {
"description": "SDK Test Investigation",
"key_findings": "This is a test.",
"priority": 1
}
},
output="""
id
created_at
created_by_user {
id
given_name
family_name
}
description
key_findings
"""
)
print(results)
Raw Queries๐
You can also run your own raw GraphQL strings. This provides the most flexibility but least amount of guard rails.
executesubscribe
from taegis_sdk_python import GraphQLService
from pprint import pprint as pp
service = GraphQLService()
results = service.investigations.execute("""
query investigationsStatusCount
{
investigationsStatusCount
{
open closed active awaiting_action suspended total
}
}
""")
pp(results)
Helpers๐
build_output_string Utility๐
There is a utility called build_output_string. This returns a string representation of the output object with all possible fields for the return type.
from taegis_sdk_python import build_output_string
from taegis_sdk_python.services.alerts.types import AlertsResponse
print(build_output_string(AlertsResponse))
_build_output_query Utility๐
If you want some assistance in building a complete GraphQL query string, you can call the service endpoint _build_output_query to help. This does build the query from the schema, so ensure you are pulling the schema from the correct service endpoint.
from taegis_sdk_python import GraphQLService, build_output_string
from taegis_sdk_python.services.investigations.types import InvestigationStatusCountResponse
service = GraphQLService()
schema = service.alerts.get_sync_schema()
print(service.alerts._build_output_query(
operation_type="query",
endpoint="investigationsStatusCount",
graphql_field=schema.query_type.fields.get("investigationsStatusCount"),
output=build_output_string(InvestigationStatusCountResponse)
))
Deprecation Warnings๐
Deprecated input fields, output fields and endpoints are set to log a warning. For more information, see Deprecation in Taegis SDK for Python.
Example:
GraphQL Query `allInvestigations` is deprecated: 'replaced by investigationsSearch'
Output field `activity_logs` is deprecated: 'Not Supported - Use audit logs', removing from default output...
Output field `assignee` is deprecated: 'No longer supported', removing from default output...
Schema Fetching๐
The Taegis SDK for Python caches the schema for 5 minutes. The fetched schema is used to help generate query strings and for error handling. For applications that are time sensitive or have a large number of API calls, this functionality helps amoratize the time needed to fetch the schema for validation and errors. This can be configured with the schema_expiry (in minutes) attribute on the GraphQLService. The schema is fetched per service (alerts will cache a schema separate from investigations). Expiration can be configured per service via context manager on the first API call to the service.
Note
Longer expiration times may have an effect on error handling. If the server side schema updates with a breaking change between caching and an API call, the SDK may build and validate a query string that is inconsistent with the deployed schema.
from taegis_sdk_python import GraphQLService
# all schemas will be cached for 15 minutes
service = GraphQLService(schema_expiry=15)
with service(schema_expiry=30): # users schema will now be cached for 30 minutes
results = service.users.query.current_tdruser()
The schema may be cleared per service using the service.<service>.clear_schema() method.
from taegis_sdk_python import GraphQLService
service = GraphQLService()
user = service.users.query.current_tdruser() # schema will be cached for the users service
service.users.clear_schema() # local schema will be cleared and re-fetched on next call
Subscription Message Size๐
The maxiumum message size for a subscription can be configured with the max_message_size parameter for all subscriptions. Invididual calls can be modified via the context manager. The default is set to 0; which removes the limit. This option only affects subscriptions; queries and mutations are not affected by this.
A ConnectionResetError will be thrown if the message size is reached or exceeded.
from taegis_sdk_python import GraphQLService
service = GraphQLService(max_message_size=4194304) # sets default to 4MB
with service(max_message_size=5242880): # sets specific API call to 5MB
options = EventQueryOptions(
timestamp_ascending=True,
page_size=1000,
max_rows=1000,
skip_cache=True,
aggregation_off=False,
)
results = service.events.subscription.event_query("FROM process EARLIEST=-1d", options=options)
try:
results = service.events.subscription.event_query("FROM process EARLIEST=-1d", options=options)
except ConnectionResetError as exc:
# handle error