Skip to content

Commit

Permalink
Microsoft Teams Notification (#1064)
Browse files Browse the repository at this point in the history
Microsoft Teams Notification

Co-authored-by: S srinidhi <[email protected]>
Co-authored-by: Dan Torrey <[email protected]>
  • Loading branch information
3 people authored Jun 14, 2022
1 parent 08a16fe commit 07ca26e
Show file tree
Hide file tree
Showing 15 changed files with 1,538 additions and 6 deletions.
14 changes: 13 additions & 1 deletion src/main/java/org/graylog/integrations/IntegrationsModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
import org.graylog.integrations.aws.resources.KinesisSetupResource;
import org.graylog.integrations.aws.transports.AWSTransport;
import org.graylog.integrations.aws.transports.KinesisTransport;
import org.graylog.integrations.dataadapters.GreyNoiseQuickIPDataAdapter;
import org.graylog.integrations.dataadapters.GreyNoiseCommunityIpLookupAdapter;
import org.graylog.integrations.dataadapters.GreyNoiseQuickIPDataAdapter;
import org.graylog.integrations.inputs.paloalto.PaloAltoCodec;
import org.graylog.integrations.inputs.paloalto.PaloAltoTCPInput;
import org.graylog.integrations.inputs.paloalto9.PaloAlto9xCodec;
Expand All @@ -37,6 +37,9 @@
import org.graylog.integrations.ipfix.transports.IpfixUdpTransport;
import org.graylog.integrations.notifications.types.SlackEventNotification;
import org.graylog.integrations.notifications.types.SlackEventNotificationConfig;
import org.graylog.integrations.notifications.types.microsoftteams.TeamsEventNotification;
import org.graylog.integrations.notifications.types.microsoftteams.TeamsEventNotificationConfig;
import org.graylog.integrations.notifications.types.microsoftteams.TeamsEventNotificationConfigEntity;
import org.graylog.integrations.pagerduty.PagerDutyNotification;
import org.graylog.integrations.pagerduty.PagerDutyNotificationConfig;
import org.graylog2.plugin.PluginConfigBean;
Expand Down Expand Up @@ -104,6 +107,15 @@ private void configureServerOnlyBindings() {
SlackEventNotification.class,
SlackEventNotification.Factory.class);

// Teams Notification
addNotificationType(TeamsEventNotificationConfig.TYPE_NAME,
TeamsEventNotificationConfig.class,
TeamsEventNotification.class,
TeamsEventNotification.Factory.class);
// Adds content pack support for Teams Notification.
registerJacksonSubtype(TeamsEventNotificationConfigEntity.class,
TeamsEventNotificationConfigEntity.TYPE_NAME);

// Pager Duty Notification
addNotificationType(
PagerDutyNotificationConfig.TYPE_NAME,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog.integrations.notifications.types.microsoftteams;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.floreysoft.jmte.Engine;
import com.google.common.annotations.VisibleForTesting;
import org.graylog.events.notifications.EventNotification;
import org.graylog.events.notifications.EventNotificationContext;
import org.graylog.events.notifications.EventNotificationException;
import org.graylog.events.notifications.EventNotificationModelData;
import org.graylog.events.notifications.EventNotificationService;
import org.graylog.events.notifications.PermanentEventNotificationException;
import org.graylog.events.notifications.TemporaryEventNotificationException;
import org.graylog.events.processor.EventDefinitionDto;
import org.graylog.integrations.notifications.types.util.RequestClient;
import org.graylog2.jackson.TypeReferences;
import org.graylog2.notifications.Notification;
import org.graylog2.notifications.NotificationService;
import org.graylog2.plugin.MessageSummary;
import org.graylog2.plugin.system.NodeId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;

import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.Objects.requireNonNull;

public class TeamsEventNotification implements EventNotification {

private static final Logger LOG = LoggerFactory.getLogger(TeamsEventNotification.class);
private final EventNotificationService notificationCallbackService;
private final Engine templateEngine;
private final NotificationService notificationService;
private final ObjectMapper objectMapper;
private final NodeId nodeId;
private final RequestClient requestClient;

@Inject
public TeamsEventNotification(EventNotificationService notificationCallbackService,
ObjectMapper objectMapper,
Engine templateEngine,
NotificationService notificationService,
NodeId nodeId, RequestClient requestClient) {
this.notificationCallbackService = notificationCallbackService;
this.objectMapper = requireNonNull(objectMapper);
this.templateEngine = requireNonNull(templateEngine);
this.notificationService = requireNonNull(notificationService);
this.nodeId = requireNonNull(nodeId);
this.requestClient = requireNonNull(requestClient);
}

/**
* @param ctx
* @throws EventNotificationException is thrown when execute fails
*/
@Override
public void execute(EventNotificationContext ctx) throws EventNotificationException {
final TeamsEventNotificationConfig config = (TeamsEventNotificationConfig) ctx.notificationConfig();
LOG.debug("TeamsEventNotification backlog size in method execute is [{}]", config.backlogSize());

try {
TeamsMessage teamsMessage = createTeamsMessage(ctx, config);
requestClient.send(teamsMessage.getJsonString(), config.webhookUrl());
} catch (TemporaryEventNotificationException exp) {
//scheduler needs to retry a TemporaryEventNotificationException
throw exp;
} catch (PermanentEventNotificationException exp) {
String errorMessage = String.format("Error sending the TeamsEventNotification :: %s", exp.getMessage());
final Notification systemNotification = notificationService.buildNow()
.addNode(nodeId.toString())
.addType(Notification.Type.GENERIC)
.addSeverity(Notification.Severity.URGENT)
.addDetail("title", "TeamsEventNotification Failed")
.addDetail("description", errorMessage);

notificationService.publishIfFirst(systemNotification);
throw exp;

} catch (Exception exp) {
throw new EventNotificationException("There was an exception triggering the TeamsEventNotification", exp);
}
}

/**
* @param ctx
* @param config
* @return
* @throws PermanentEventNotificationException - throws this exception when the custom message template is invalid
*/
TeamsMessage createTeamsMessage(EventNotificationContext ctx, TeamsEventNotificationConfig config) throws PermanentEventNotificationException {
//Note: Link names if notify channel or else the channel tag will be plain text.
String messageTitle = buildDefaultMessage(ctx);
String description = buildMessageDescription(ctx);
JsonNode customMessage = null;
String template = config.customMessage();
if (!isNullOrEmpty(template)) {
customMessage = buildCustomMessage(ctx, config, template);
}

return new TeamsMessage(
config.color(),
config.iconUrl(),
messageTitle,
customMessage,
description
);
}

String buildDefaultMessage(EventNotificationContext ctx) {
String title = ctx.eventDefinition().map(EventDefinitionDto::title).orElse("Unnamed");

// Build Message title
return String.format(Locale.ROOT, "**Alert %s triggered:**\n", title);
}

private String buildMessageDescription(EventNotificationContext ctx) {
String description = ctx.eventDefinition().map(EventDefinitionDto::description).orElse("");
return "_" + description + "_";
}


JsonNode buildCustomMessage(EventNotificationContext ctx, TeamsEventNotificationConfig config, String template) throws PermanentEventNotificationException {
final List<MessageSummary> backlog = getMessageBacklog(ctx, config);
Map<String, Object> model = getCustomMessageModel(ctx, config.type(), backlog);
try {
String facts = templateEngine.transform(template, model);
JsonNode factsNode = getMessageDetails(facts);
LOG.debug("customMessage: template = {} model = {}", template, model);
return factsNode;
} catch (Exception e) {
String error = "Invalid Custom Message template.";
LOG.error("{} [{}]", error, e.toString());
throw new PermanentEventNotificationException(error + e, e.getCause());
}
}

public JsonNode getMessageDetails(String eventFields) {
String[] fields = eventFields.split("\\r?\\n");
List<Map<String, String>> event = new ArrayList<>();
for (String field : fields) {
Map<String, String> facts = new HashMap<>();
String[] factFields = field.split(":");
facts.put("name", factFields[0]);
facts.put("value", factFields.length == 1 ? "" : factFields[1].trim());
event.add(facts);
}
LOG.debug("Created list of facts");
return objectMapper.convertValue(event, JsonNode.class);

}

@VisibleForTesting
List<MessageSummary> getMessageBacklog(EventNotificationContext ctx, TeamsEventNotificationConfig config) {
List<MessageSummary> backlog = notificationCallbackService.getBacklogForEvent(ctx);
if (config.backlogSize() > 0 && backlog != null) {
return backlog.stream().limit(config.backlogSize()).collect(Collectors.toList());
}
return backlog;
}


@VisibleForTesting
Map<String, Object> getCustomMessageModel(EventNotificationContext ctx, String type, List<MessageSummary> backlog) {
EventNotificationModelData modelData = EventNotificationModelData.of(ctx, backlog);

LOG.debug("the custom message model data is {}", modelData);
Map<String, Object> objectMap = objectMapper.convertValue(modelData, TypeReferences.MAP_STRING_OBJECT);
objectMap.put("type", type);

return objectMap;
}

public interface Factory extends EventNotification.Factory<TeamsEventNotification> {
@Override
TeamsEventNotification create();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog.integrations.notifications.types.microsoftteams;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.auto.value.AutoValue;
import org.graylog.events.contentpack.entities.EventNotificationConfigEntity;
import org.graylog.events.event.EventDto;
import org.graylog.events.notifications.EventNotificationConfig;
import org.graylog.events.notifications.EventNotificationExecutionJob;
import org.graylog.scheduler.JobTriggerData;
import org.graylog2.contentpacks.EntityDescriptorIds;
import org.graylog2.contentpacks.model.entities.references.ValueReference;
import org.graylog2.plugin.rest.ValidationResult;

import javax.annotation.Nullable;
import javax.validation.constraints.NotBlank;
import java.net.URI;
import java.util.regex.Pattern;

@AutoValue
@JsonTypeName(TeamsEventNotificationConfig.TYPE_NAME)
@JsonDeserialize(builder = TeamsEventNotificationConfig.Builder.class)
public abstract class TeamsEventNotificationConfig implements EventNotificationConfig {

public static final String TYPE_NAME = "teams-notification-v1";

private static final Pattern TEAMS_PATTERN = Pattern.compile("https://.*.webhook.office.com/");
private static final String DEFAULT_HEX_COLOR = "#ff0500";
private static final String DEFAULT_CUSTOM_MESSAGE = "Graylog Teams Notification";
private static final long DEFAULT_BACKLOG_SIZE = 0;

static final String INVALID_BACKLOG_ERROR_MESSAGE = "Backlog size cannot be less than zero";
static final String INVALID_WEBHOOK_ERROR_MESSAGE = "Specified Webhook URL is not a valid URL";
static final String INVALID_TEAMS_URL_ERROR_MESSAGE = "Specified Webhook URL is not a valid Teams URL";
static final String WEB_HOOK_URL = "https://teams.webhook.office.com/services/xxxx/xxxxxxxxxxxxxxxxxxx";
static final String FIELD_WEBHOOK_URL = "webhook_url";
static final String TEAMS_CUSTOM_MESSAGE = "custom_message";
static final String TEAMS_ICON_URL = "icon_url";
static final String TEAMS_BACKLOG_SIZE = "backlog_size";
static final String TEAMS_COLOR = "color";

@JsonProperty(TEAMS_BACKLOG_SIZE)
public abstract long backlogSize();

@JsonProperty(TEAMS_COLOR)
@NotBlank
public abstract String color();

@JsonProperty(FIELD_WEBHOOK_URL)
@NotBlank
public abstract String webhookUrl();

@JsonProperty(TEAMS_CUSTOM_MESSAGE)
public abstract String customMessage();

@JsonProperty(TEAMS_ICON_URL)
@Nullable
public abstract String iconUrl();

@Override
@JsonIgnore
public JobTriggerData toJobTriggerData(EventDto dto) {
return EventNotificationExecutionJob.Data.builder().eventDto(dto).build();
}

public static Builder builder() {
return Builder.create();
}

@Override
@JsonIgnore
public ValidationResult validate() {
ValidationResult validation = new ValidationResult();

URI webhookUri;
try {
webhookUri = new URI(webhookUrl());
if (webhookUri.getHost().toLowerCase().contains("office")) {
if (!TEAMS_PATTERN.matcher(webhookUrl()).find()) {
validation.addError(FIELD_WEBHOOK_URL, INVALID_TEAMS_URL_ERROR_MESSAGE);
}
}

} catch (Exception ex) {
validation.addError(FIELD_WEBHOOK_URL, INVALID_WEBHOOK_ERROR_MESSAGE);
}

if (backlogSize() < 0) {
validation.addError(TEAMS_BACKLOG_SIZE, INVALID_BACKLOG_ERROR_MESSAGE);
}

return validation;
}

@AutoValue.Builder
public static abstract class Builder implements EventNotificationConfig.Builder<Builder> {
@JsonCreator
public static Builder create() {

return new AutoValue_TeamsEventNotificationConfig.Builder()
.type(TYPE_NAME)
.color(DEFAULT_HEX_COLOR)
.webhookUrl(WEB_HOOK_URL)
.customMessage(DEFAULT_CUSTOM_MESSAGE)
.backlogSize(DEFAULT_BACKLOG_SIZE);
}

@JsonProperty(TEAMS_COLOR)
public abstract Builder color(String color);

@JsonProperty(FIELD_WEBHOOK_URL)
public abstract Builder webhookUrl(String webhookUrl);

@JsonProperty(TEAMS_CUSTOM_MESSAGE)
public abstract Builder customMessage(String customMessage);

@JsonProperty(TEAMS_ICON_URL)
public abstract Builder iconUrl(String iconUrl);

@JsonProperty(TEAMS_BACKLOG_SIZE)
public abstract Builder backlogSize(long backlogSize);

public abstract TeamsEventNotificationConfig build();
}

@Override
public EventNotificationConfigEntity toContentPackEntity(EntityDescriptorIds entityDescriptorIds) {
return TeamsEventNotificationConfigEntity.builder()
.color(ValueReference.of(color()))
.webhookUrl(ValueReference.of(webhookUrl()))
.customMessage(ValueReference.of(customMessage()))
.iconUrl(ValueReference.of(iconUrl()))
.build();
}
}
Loading

0 comments on commit 07ca26e

Please sign in to comment.