Server.Alerts.Mattermost

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)