Cortex XDR PRO features an amazing workflow capable of correlating all sort of alerts into meninful incidents. It provides support for self-generated alerts (the ones coming from Palo Alto Networks endpoint agents or NGFW's) as well as for third party alerts. These third party alerts can be incorporated into the product by consuming its Insert Parsed Alerts API
This API provides quota enforcement (up to 600 alerts per minute / up to 60 alerts per update). Anyone willing to consume it should thake that into consideration to avoid alerts being lost due to quota exceeding.
In this tutorial we will implement a pipeline with the following features:
- HTTP front end to process POST requests that contain alerts to be be ingested into Cortex XDR PRO
- Pipeline stage to enforce qouta limitation imposed by XDR
- Client to the Cortex XDR insert parsed alerts API
As a project of this characteristics is meant to end up becoming a micro-service the following architecural decisions have been taken in consideration:
- The code must be as compact as possible to run in a container - opted to code in GO
- Runtime footprint and dependencies must be as low as possible to reduce vulnerabilities - opted to use static linking in a distroless image
The project described in this article became a repository of its own and is available in GitHub
There are two main elements to implement here:
- a collection of structs and corresponding methods to create the API payload
- a type to hold secret data capable of implementing the Advanced API key authentication method
The API uses a JSON payload whose main component is an array of alerts with a pre-defined schema.
To produce such a payload we need a corresponding GO struct featuring
We need, as well, the struct to encapsulate these alerts into a
request_data json object
There is no need to export the
xdrPayload struct. But a decision has to be taken regarding whether we expose the
struct in the library for the developer to fill its properties. A deep dive into the API reveals that some properties like
action_status can't contain arbitrary string values but enumerated ones.
With that in mind it looks safer to expose an abstracted struct instead and type aliases for these enumerated values. This way the developer will be forced to use only valid values.
These enumerated values will need to be converted eventually into their string representations.
Thinking on sanity of API calls it look like a good idea, as well, to provide convenience methods to validate
constrained values like
RemoteIP. The following struct method provides validation for IPv4/v6
Semantics for the Advanced API key authentication can be found in the Get Started with Cortex XDR APIs document.
Basically, a random value (nonce) must be created and shared with the XDR endpoint. That value must be used to create a hash composed of the Advanced API Key and timestamp aming to reduce replay attacks.
So let's start by defining a struct that will hold the required secret data as well as the FQDN of the XDR instance it will be interfacing with.
Let's provide, as well, a factory function that computes a valid
nonce (random) value.
Time to implement a hash method that provides the signature that will be used as the
Authentication header value to every API call.
The only missing piece is a method that receives an array of
Alerts as its input and that pushes them into the
Insert Parsed Alerts API filling the http headers as needed to pass the authentication requirements.
So far so good. We have a client implementation capable of receiving an array of
Alerts and pushing
them to the Insert Parsed Alerts API. But, what about XDR quota enforcement? What about these up to 600
alerts per minute or the limit of up to 60 alerts per update?
Although this is a generic problem with tons of implementations available on the Internet we'll share a few highlines of a GO implementation.
We need the following components:
- a Buffered Channel that we'll use to accumulate
- a bucket (counter) that will start with a capacity of 600 units and that will be decremented each time we pull an
Alertfrom the channel
- a 1 minute Ticker to re-fill the bucket to 600 units
- a 2 seconds Ticker to check the channel for alerts and that drives
Let's pack all these components into an engine function meant to be started as a goroutine.
Then we can create a factory function that creates the buffered channel, starts the engine and return a function we can use to push alerts into the pipe.
The last step is to create a web micro-service that can receive an alert from a third party element, parse it into a
Alert struct and then push it into the pipeline.
For that well assume we have a type that implements the following
The following code demonstrate how to create the web server handler minimum structure (error handling should be implemented)
And that's all. We just need to initialize all components and assemble all the pieces in a web server. Check the following code as a basic example
In this tutorial we've covered a typical use case for the XDR Ingest Parsed Alerts API: a pipeline capable of receiving third party alerts, parsing them into valid XDR payloads and pushing the resulting data into the Cortex XDR alerts dataset. These alerts will be aggregated into their corresponding incidents.
Althought it is a generic issue the tutorial covered as well the need for an engine capable of enforcing quota limits imposed, in this case, by the XDR API.
The final example implementation leverages a HTTP micro-service but other implementations are possible like
To reduce vulnerability footprint for the micro-service GO language was chosen and only standard runtime libraries were used. That allows, for example, to pack the whole application in a distroless container image as demonstrated in the following example Dockerfile