Created
November 12, 2025 10:12
-
-
Save misje/3ab03e6ebdf7e32687137fca82351446 to your computer and use it in GitHub Desktop.
Exchange.Monitor.Alerts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Exchange.Monitor.Alerts | |
| author: Andreas Misje – @misje | |
| description: | | |
| Send an e-mail when an alert is created. | |
| This artifact forwards alerts from Server.Internal.Alerts as e-mails. Alert | |
| context, like client information, original timestamp, artifact name/type and | |
| any other argument passed to `alert()` will be included, either in an HTML- | |
| formatted table or as a clear-text bullet list. | |
| Additional information about the client may be included as extra rows by using | |
| ClientMetadata. This is handy to provide additional context, like serial number, | |
| computer owner or department, if this is saved as client metadata. | |
| Context data that is considered falsy (empty strings, null, 0, empty lists etc.) | |
| are excluded from the details in the e-mail unless KeepEmptyRows is true. | |
| type: SERVER_EVENT | |
| parameters: | |
| - name: Recipients | |
| type: csv | |
| description: | | |
| E-mail addresses that will receive the message. Required unless | |
| NotifyExecutor is true. | |
| default: | | |
| Address | |
| - name: NotifyExecutor | |
| description: | | |
| If the alert originates from a collection, send an e-mail to the user | |
| executing the flow (in addition to Recipients) if the username contains | |
| an '@' character. | |
| type: bool | |
| default: false | |
| - name: Sender | |
| description: | | |
| Sender e-mail address. REQUIRED. | |
| - name: HTML | |
| type: bool | |
| description: | | |
| Send an HTML-formatted message instead of plain text. | |
| default: true | |
| - name: ArtifactsToAlertOn | |
| type: regex | |
| description: | | |
| Only forward alerts from these monitoring artifacts. | |
| default: .+ | |
| - name: ArtifactsToIgnore | |
| type: regex | |
| description: | | |
| Ignore alerts from these monitoring artifacts. | |
| - name: AlertNameInclude | |
| type: regex | |
| description: | | |
| Only forward alerts matching this name. | |
| default: .+ | |
| - name: AlertNameExclude | |
| type: regex | |
| description: | | |
| Ignore alerts matching this name. | |
| - name: ServerSecret | |
| description: | | |
| Secret used to configure server, port, username, password and skip_verify. | |
| Required. | |
| default: notify_mail_secret | |
| - name: SendInterval | |
| type: int | |
| description: | | |
| How long to wait (in seconds) between sending e-mails (throttling). | |
| default: 10 | |
| - name: IncludeFlowDetails | |
| type: bool | |
| description: | | |
| Include information about the flow, like the executor, artifact arguments | |
| etc. | |
| - name: KeepEmptyRows | |
| type: bool | |
| description: | | |
| By default, rows with empty values are removed from the e-mail. In order | |
| to keep the structure of e-mails consistent, empty rows may be kept with this | |
| setting. | |
| - name: ClientMetadata | |
| type: csv | |
| description: | | |
| Include the following client metadata as useful context in the e-mail. If | |
| Alias is set, the metadata key will be renamed to the alias. The metadata | |
| are output as individual rows (considering prefix fields "Client " to make | |
| their meaning clear). | |
| default: | | |
| Field,Alias | |
| - name: SeverityTransforms | |
| type: csv | |
| description: | | |
| A table of event_data members that may be used as a severity value, along | |
| with an optional regex replacement. Used in SeverityThreshold and in e-mail | |
| title and body. Last match is used. | |
| default: | | |
| Member,Regex,Replace | |
| - name: SeverityThreshold | |
| type: json_array | |
| description: | | |
| If severity is defined in SeverityTransforms and in event_data, the value | |
| must be in this list in order for an e-mail to be produced. | |
| default: "[]" | |
| imports: | |
| - Exchange.Server.Alerts.Mail | |
| sources: | |
| - query: | | |
| LET IncludeArtifact = NOT ArtifactsToAlertOn OR Artifact =~ | |
| ArtifactsToAlertOn | |
| LET ExcludeArtifact = ArtifactsToIgnore | |
| AND Artifact =~ ArtifactsToIgnore | |
| LET ArtifactNameMatched = NOT Artifact OR (IncludeArtifact | |
| AND NOT ExcludeArtifact) | |
| LET IncludeName = NOT AlertNameInclude OR Message =~ | |
| AlertNameInclude | |
| LET ExcludeName = AlertNameExclude | |
| AND Message =~ AlertNameExclude | |
| LET NameMatched = IncludeName | |
| AND NOT ExcludeName | |
| LET S = scope() | |
| LET Alerts = SELECT timestamp(epoch=now()) AS ServerTime, | |
| timestamp(epoch=timestamp) AS AlertTime, | |
| S.client_id AS ClientId, | |
| S.flow_id AS FlowId, | |
| name AS Message, | |
| S.artifact AS Artifact, | |
| S.artifact_Type AS ArtifactType, | |
| event_data AS Context | |
| FROM watch_monitoring(artifact='Server.Internal.Alerts') | |
| WHERE ArtifactNameMatched | |
| AND NameMatched | |
| LET CreatorAddrs = if(condition=NotifyExecutor | |
| AND Creator =~ '[^@]+@.+', | |
| then=(request.creator, ), | |
| else=[]) | |
| LET IsHunt = FlowId =~ '\\.H$' | |
| // Get the hunt (if any) this flow is part of: | |
| LET OurHunt = SELECT * | |
| FROM HuntInfo(flow_id=FlowId) | |
| LET OrgInfo <= org() | |
| LET GetSeverity = get(item=Context, member=Member) | |
| LET Severities = SELECT Member AS Name, | |
| if(condition=Regex | |
| AND Replace, | |
| then=regex_replace( | |
| source=GetSeverity, | |
| re=Regex, | |
| replace=Replace), | |
| else=if( | |
| condition=NOT Regex OR | |
| GetSeverity =~ Regex, | |
| then=GetSeverity)) AS Level | |
| FROM SeverityTransforms | |
| // Used to remove the original severity field from the context: | |
| LET SeverityDummyDict = to_dict(item={ | |
| SELECT Severities.Name[-1] AS _key | |
| FROM scope() | |
| }) | |
| LET Title = if( | |
| condition=ClientId = 'server', | |
| then='Server alert', | |
| else=format(format='Alert from client %v', | |
| args=client_info(client_id=ClientId).os_info.hostname)) | |
| LET Boldify(String) = if(condition=HTML, | |
| then=format(format='<b>%v</b>', args=String), | |
| else=String) | |
| LET Summary = format( | |
| format='An alert was sent from %v at %v: %v.', | |
| args=(if(condition=ClientId = 'server', | |
| then='the server', | |
| else=client_info(client_id=ClientId).os_info.hostname), | |
| AlertTime, Boldify(String=Message))) | |
| LET AlertInfo = dict( | |
| `Server time`=if(condition=ServerTime > AlertTime, | |
| then=TimestampString(Timestamp=ServerTime)), | |
| `Alert time`=TimestampString(Timestamp=AlertTime), | |
| `Severity`=Severities.Level[-1], | |
| `Message`=Message, | |
| `Artifact`=if(condition=Artifact, | |
| then=Link(URL=link_to(artifact=Artifact, raw=true), | |
| Name=Artifact, | |
| HTML=HTML)), | |
| `Artifact type`=ArtifactType) + Context - SeverityDummyDict | |
| LET Tables(HTML) = dict(`Alert details`=Table( | |
| Values=AlertInfo, | |
| HTML=HTML, | |
| KeepEmpty=KeepEmptyRows), | |
| `Client details`=if( | |
| condition=ClientId != 'server', | |
| then=Table(Values=ClientInfo(HTML=HTML), | |
| HTML=HTML, | |
| KeepEmpty=KeepEmptyRows), | |
| else=''), | |
| `Flow details`=if( | |
| condition=IncludeFlowDetails | |
| AND FlowId, | |
| then=Table(Values=FlowInfo(HTML=HTML), | |
| HTML=HTML, | |
| KeepEmpty=KeepEmptyRows), | |
| else='')) | |
| LET PlainText = format(format='%v\n\n%v\n\n%v', | |
| args=(Title, Summary, join( | |
| array=items(item=Tables(HTML=false))._value, | |
| sep='\n\n'))) | |
| LET Footer = format(format='Sent from Velociraptor by %v', | |
| args=ArtifactLinks(Artifacts='Exchange.Monitor.Alerts', | |
| HTML=HTML)[0].Link) | |
| LET HTMLBody = MailTemplate(Title=Title, | |
| Tables=Tables(HTML=true), | |
| Summary=Summary, | |
| Footer=Footer, | |
| Warning=true) | |
| LET Results = SELECT *, AlertInfo, | |
| if(condition=IncludeFlowDetails | |
| AND FlowId, | |
| then=FlowInfo[0].Info) AS FlowInfo | |
| FROM Alerts | |
| WHERE NOT Severities OR NOT SeverityThreshold OR | |
| Severities.Level[-1] IN SeverityThreshold | |
| SELECT * | |
| FROM foreach(row=Results, | |
| query={ | |
| SELECT * | |
| FROM Artifact.Custom.Generic.Utils.SendEmail( | |
| Secret=ServerSecret, | |
| Recipients=Unique(Items=Recipients.Address + CreatorAddrs), | |
| Sender=Sender, | |
| HTMLMessage=if(condition=HTML, then=HTMLBody), | |
| PlainTextMessage=PlainText, | |
| Subject=Title, | |
| Period=SendInterval) | |
| }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment