Create a Slack/Mattermost notification when a client Flow (with artifacts of interest) has finished. Cancelled collections and collections with artifacts that don’t satisfy preconditions do not create notifications when they are stopped.
name: Server.Alerts.Mattermost
description: |
Create a Slack/Mattermost notification when a client Flow (with artifacts of interest) has finished. Cancelled collections and collections with artifacts that don't satisfy preconditions do not create notifications when they are stopped.
type: SERVER_EVENT
author: Andreas Misje – @misje
parameters:
- name: WebhookURL
description: |
Webhook used to for posting the notification. If empty, the server metadata variable "MattermostWebhookURL" will be used.
- name: VelociraptorServerURL
description: |
The Velociraptor server URL, e.g. "https://velociraptor.example.org", used to build links to flows and clients in the notification payload. If empty, the server metadata variable "VelociraptorServerURL" is used. If that variable is also empty, no links will be created.
- name: Decorate
description: |
Whether the notification payload should be "decorated" using the legacy "secondary attachments" format, supported by both Slack and Mattermost. If false, a single string will be sent.
type: bool
default: Y
- name: ArtifactsToAlertOn
description: |
Notifications will only be created for finished flows with artifact names matching this regex.
default: .
type: regex
- name: ArtifactsToIgnore
description: |
Notifications will not be created for finished flows with artifact names matching this regex.
default: ^Generic.Client.Info
- name: NotifyHunts
description: |
Create notifications for finished flows that are part of a hunt. This may produce a lot of notifications, depending on the number of clients that will take part in the hunt.
type: bool
- name: DelayThreshold
description: |
Only create notifications if the flow has not finished within a certain number of seconds since it was created.
default: 10
sources:
- query: |
LET NotifyUrl = if(
condition=WebhookURL,
then=WebhookURL,
else=server_metadata().MattermostWebhookURL
)
Let ServerUrl = if(
condition=VelociraptorServerURL,
then=VelociraptorServerURL,
else=server_metadata().VelociraptorServerURL
)
// Get basic information about completed flows:
LET CompletedFlows = SELECT timestamp(epoch=Timestamp) AS FlowFinished,
ClientId,
FlowId
FROM watch_monitoring(artifact='System.Flow.Completion')
WHERE Flow.artifacts_with_results
AND ClientId != 'server'
AND NOT Flow.artifacts_with_results =~ ArtifactsToIgnore
AND Flow.artifacts_with_results =~ ArtifactsToAlertOn
// Look up more details about the flows using flows(), since the data returned by watch_monitoring() may be incomplete (like the create_time field):
LET FlowInfo = SELECT ClientId,
client_info(client_id=ClientId).os_info.fqdn AS FQDN,
FlowId,
timestamp(epoch=create_time) AS FlowCreated,
timestamp(epoch=start_time) AS FlowStarted,
FlowFinished,
execution_duration/1000000000 AS Duration,
join(array=artifacts_with_results, sep=', ') AS FlowResults,
total_collected_rows AS CollectedRows,
total_uploaded_files AS UploadedFiles,
total_uploaded_bytes AS UploadedBytes,
state='FINISHED' AS Success,
status AS Error
FROM flows(client_id=ClientId, flow_id=FlowId)
// Filter out flows part of hunts (if enabled) by the trailing ".H" in the ID:
WHERE if(condition=NotifyHunts, then=true, else=not FlowId=~'\.H$')
// Notifications aren't necessarily useful if collections complete close to immediately:
AND FlowFinished.Unix - timestamp(epoch=create_time).Unix >= atoi(string=DelayThreshold)
LET Results = SELECT *
FROM foreach(row=CompletedFlows, query=FlowInfo)
// If ServerUrl is provided, create Markdown links to the client, flows and hunt:
LET ClientLink = if(condition=ServerUrl,
then=format(format='[%v](%v/app/index.html#/host/%v)', args=[
FQDN, ServerUrl, ClientId
]),
else=FQDN
)
LET FlowUrl = format(format='%v/app/index.html#/collected/%v/%v/notebook', args=[
ServerUrl, ClientId, FlowId
])
LET FlowLink = if(condition=ServerUrl,
then=format(format='[%v](%v)', args=[
FlowId, FlowUrl
]),
else=str(str=FlowId)
)
// The HuntId has to be fetched by looking for the FlowId in all hunts:
LET AllHunts = SELECT hunt_id AS HuntId,
hunt_description AS HuntDesc
FROM hunts()
LET OurHunt(Fid) = SELECT *
FROM foreach(
row=AllHunts,
query={SELECT HuntId, HuntDesc FROM hunt_flows(hunt_id=HuntId) WHERE FlowId=Fid}
)
LET HuntLink_ = SELECT HuntDesc, HuntId
FROM OurHunt(Fid=FlowId)
LET HuntLink = if(
condition=ServerUrl AND HuntLink_.HuntId,
then=format(format='[%v](%v/app/index.html#/hunts/%v)', args=[
// There should only ever be one hunt for this flow:
HuntLink_[0].HuntDesc, HuntLink_[0].ServerUrl, HuntLink_[0].HuntId
]),
else=if(condition=HuntLink_.HuntId, then=str(str=HuntLink_[0].HuntId), else='–')
)
LET StateString = if(condition=Success, then='finished collecting', else='FAILED to collect')
LET Message = format(format='Client %v has %v the artifact(s) %v, started at %v, in flow %v', args=[
ClientLink, StateString, FlowResults, FlowStarted.String, FlowLink
])
// Create a more readable notification by using the formatting option called "secondary attachments". It's deemed a legacy format by Slack, but it works in Mattermost (whereas newer formatting options in Slack does not):
LET Decorated = dict(
attachments=[dict(
mrkdwn_in=['text'],
// Use a green colour if the collection succeeded, and red if it failed. The third state "RUNNING" should never be present in flows in this query:
color=if(condition=Success, then='#36a64f', else='#e40303'),
pretext=Message,
title=format(format='Client collection %v', args=[if(condition=Success, then='FINISHED', else='FAILED')]),
title_link=if(condition=ServerUrl, then=FlowUrl, else=null),
fields=[
dict(
title='Collection created',
value=FlowCreated.String,
short=true
),
dict(
title='Collection started',
value=FlowStarted.String,
short=true
),
dict(
title='Error',
value=if(condition=Error, then=Error, else='–'),
short=true
),
dict(
title='Hunt',
value=if(condition=HuntLink, then=HuntLink, else='–'),
short=true
),
dict(
title='Duration',
value=format(format='%.1f s', args=[Duration]),
short=true
),
dict(
title='Collected rows',
value=CollectedRows,
short=true
),
dict(
title='Uploaded files',
value=UploadedFiles,
short=true
),
dict(
title='Uploaded bytes',
value=UploadedBytes,
short=true
),
]
),]
)
LET Payload = if(condition=Decorate, then=Decorated, else=Message)
LET Notify = SELECT Response, Content
FROM http_client(
data=serialize(item=Payload, format='json'),
headers=dict(`Content-Type`='application/json'),
method='POST',
url=NotifyUrl
)
WHERE NotifyUrl
AND if(condition=Response=200,
then=log(level='INFO', message='Notification sent'),
else=log(level='WARN', message=format(format='Failed to send notification: Reponse: %v', args=[Response]))
)
SELECT * FROM foreach(row=Results, query=Notify)