From 07ca26e6a4cf87a5c5d14cdbf8cf5a662214a1e7 Mon Sep 17 00:00:00 2001 From: Zack King <91903901+kingzacko1@users.noreply.github.com> Date: Tue, 14 Jun 2022 09:24:02 -0500 Subject: [PATCH] Microsoft Teams Notification (#1064) Microsoft Teams Notification Co-authored-by: S srinidhi Co-authored-by: Dan Torrey --- .../integrations/IntegrationsModule.java | 14 +- .../TeamsEventNotification.java | 201 ++++++++++++ .../TeamsEventNotificationConfig.java | 154 +++++++++ .../TeamsEventNotificationConfigEntity.java | 91 ++++++ .../types/microsoftteams/TeamsMessage.java | 110 +++++++ .../types/util/RequestClient.java | 69 ++++ .../TeamsEventNotificationConfigTest.java | 149 +++++++++ .../TeamsEventNotificationTest.java | 309 ++++++++++++++++++ .../microsoftteams/TeamsMessageTest.java | 45 +++ .../TeamsNotificationDetails.tsx | 47 +++ .../TeamsNotificationForm.tsx | 190 +++++++++++ .../TeamsNotificationSummary.tsx | 59 ++++ src/web/event-notifications/types.ts | 47 +++ src/web/index.jsx | 21 +- tsconfig.json | 38 +++ 15 files changed, 1538 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotification.java create mode 100644 src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfig.java create mode 100644 src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfigEntity.java create mode 100644 src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsMessage.java create mode 100644 src/main/java/org/graylog/integrations/notifications/types/util/RequestClient.java create mode 100644 src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfigTest.java create mode 100644 src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationTest.java create mode 100644 src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsMessageTest.java create mode 100644 src/web/event-notifications/event-notification-details/TeamsNotificationDetails.tsx create mode 100644 src/web/event-notifications/event-notification-types/TeamsNotificationForm.tsx create mode 100644 src/web/event-notifications/event-notification-types/TeamsNotificationSummary.tsx create mode 100644 src/web/event-notifications/types.ts create mode 100644 tsconfig.json diff --git a/src/main/java/org/graylog/integrations/IntegrationsModule.java b/src/main/java/org/graylog/integrations/IntegrationsModule.java index b8bac8e36..8753c27fd 100644 --- a/src/main/java/org/graylog/integrations/IntegrationsModule.java +++ b/src/main/java/org/graylog/integrations/IntegrationsModule.java @@ -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; @@ -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; @@ -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, diff --git a/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotification.java b/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotification.java new file mode 100644 index 000000000..b1cb3c11a --- /dev/null +++ b/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotification.java @@ -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 + * . + */ +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 backlog = getMessageBacklog(ctx, config); + Map 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> event = new ArrayList<>(); + for (String field : fields) { + Map 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 getMessageBacklog(EventNotificationContext ctx, TeamsEventNotificationConfig config) { + List backlog = notificationCallbackService.getBacklogForEvent(ctx); + if (config.backlogSize() > 0 && backlog != null) { + return backlog.stream().limit(config.backlogSize()).collect(Collectors.toList()); + } + return backlog; + } + + + @VisibleForTesting + Map getCustomMessageModel(EventNotificationContext ctx, String type, List backlog) { + EventNotificationModelData modelData = EventNotificationModelData.of(ctx, backlog); + + LOG.debug("the custom message model data is {}", modelData); + Map objectMap = objectMapper.convertValue(modelData, TypeReferences.MAP_STRING_OBJECT); + objectMap.put("type", type); + + return objectMap; + } + + public interface Factory extends EventNotification.Factory { + @Override + TeamsEventNotification create(); + } + +} diff --git a/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfig.java b/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfig.java new file mode 100644 index 000000000..31c6bc84d --- /dev/null +++ b/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfig.java @@ -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 + * . + */ +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 { + @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(); + } +} diff --git a/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfigEntity.java b/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfigEntity.java new file mode 100644 index 000000000..2ef90a23d --- /dev/null +++ b/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfigEntity.java @@ -0,0 +1,91 @@ +/* + * 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 + * . + */ +package org.graylog.integrations.notifications.types.microsoftteams; + +import com.fasterxml.jackson.annotation.JsonCreator; +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.notifications.EventNotificationConfig; +import org.graylog2.contentpacks.model.entities.EntityDescriptor; +import org.graylog2.contentpacks.model.entities.references.ValueReference; + +import java.util.Map; + +@AutoValue +@JsonTypeName(TeamsEventNotificationConfigEntity.TYPE_NAME) +@JsonDeserialize(builder = TeamsEventNotificationConfigEntity.Builder.class) +public abstract class TeamsEventNotificationConfigEntity implements EventNotificationConfigEntity { + + public static final String TYPE_NAME = "teams-notification-v1"; + + @JsonProperty(TeamsEventNotificationConfig.TEAMS_COLOR) + public abstract ValueReference color(); + + @JsonProperty(TeamsEventNotificationConfig.FIELD_WEBHOOK_URL) + public abstract ValueReference webhookUrl(); + + @JsonProperty(TeamsEventNotificationConfig.TEAMS_CUSTOM_MESSAGE) + public abstract ValueReference customMessage(); + + @JsonProperty(TeamsEventNotificationConfig.TEAMS_ICON_URL) + public abstract ValueReference iconUrl(); + + public static Builder builder() { + return Builder.create(); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + public static abstract class Builder implements EventNotificationConfigEntity.Builder { + + @JsonCreator + public static Builder create() { + return new AutoValue_TeamsEventNotificationConfigEntity.Builder() + .type(TYPE_NAME); + } + + @JsonProperty(TeamsEventNotificationConfig.TEAMS_COLOR) + public abstract Builder color(ValueReference color); + + @JsonProperty(TeamsEventNotificationConfig.FIELD_WEBHOOK_URL) + public abstract Builder webhookUrl(ValueReference webhookUrl); + + @JsonProperty(TeamsEventNotificationConfig.TEAMS_CUSTOM_MESSAGE) + public abstract Builder customMessage(ValueReference customMessage); + + @JsonProperty(TeamsEventNotificationConfig.TEAMS_ICON_URL) + public abstract Builder iconUrl(ValueReference iconUrl); + + public abstract TeamsEventNotificationConfigEntity build(); + } + + @Override + public EventNotificationConfig toNativeEntity(Map parameters, Map nativeEntities) { + return TeamsEventNotificationConfig.builder() + .color(color().asString(parameters)) + .webhookUrl(webhookUrl().asString(parameters)) + .customMessage(customMessage().asString(parameters)) + .customMessage(customMessage().asString(parameters)) + .iconUrl(iconUrl().asString(parameters)) + .build(); + } +} + diff --git a/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsMessage.java b/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsMessage.java new file mode 100644 index 000000000..7f9d4f267 --- /dev/null +++ b/src/main/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsMessage.java @@ -0,0 +1,110 @@ +/* + * 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 + * . + */ +package org.graylog.integrations.notifications.types.microsoftteams; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TeamsMessage { + private String color; + private String iconUrl; + private String messageTitle; + private JsonNode customMessage; + private String description; + + + public TeamsMessage( + String color, + String iconUrl, + String messageTitle, + JsonNode customMessage, + String description + ) { + this.color = color; + this.iconUrl = iconUrl; + this.messageTitle = messageTitle; + this.customMessage = customMessage; + this.description = description; + } + + public TeamsMessage(String messageTitle) { + this.messageTitle = messageTitle; + } + + public String getJsonString() { + + final Map params = new HashMap<>(); + params.put("@type", "MessageCard"); + params.put("@context", "http://schema.org/extensions"); + params.put("themeColor", color); + params.put("text", messageTitle); + + final List Sections = new ArrayList<>(); + if (!customMessage.isNull()) { + final Sections section = new Sections( + description, + iconUrl, + customMessage + ); + + Sections.add(section); + } + + if (!Sections.isEmpty()) { + params.put("sections", Sections); + } + + try { + return new ObjectMapper().writeValueAsString(params); + } catch (JsonProcessingException e) { + throw new RuntimeException("Could not build payload JSON.", e); + } + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Sections { + + @JsonProperty + public String activitySubtitle; + + @JsonProperty + public String activityImage; + + @JsonProperty + public JsonNode facts; + + @JsonCreator + public Sections(String activitySubtitle, String activityImage, JsonNode facts) { + this.activitySubtitle = activitySubtitle; + this.activityImage = activityImage; + this.facts = facts; + + } + } + +} diff --git a/src/main/java/org/graylog/integrations/notifications/types/util/RequestClient.java b/src/main/java/org/graylog/integrations/notifications/types/util/RequestClient.java new file mode 100644 index 000000000..0db170077 --- /dev/null +++ b/src/main/java/org/graylog/integrations/notifications/types/util/RequestClient.java @@ -0,0 +1,69 @@ +/* + * 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 + * . + */ +package org.graylog.integrations.notifications.types.util; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.graylog.events.notifications.PermanentEventNotificationException; +import org.graylog.events.notifications.TemporaryEventNotificationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.IOException; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +public class RequestClient { + private static final Logger LOG = LoggerFactory.getLogger(RequestClient.class); + private final OkHttpClient httpClient; + + @Inject + public RequestClient(OkHttpClient httpClient) { + this.httpClient = httpClient; + } + + /** + * @param message + * @param webhookUrl + * @throws TemporaryEventNotificationException - thrown for network or timeout type issues + * @throws PermanentEventNotificationException - thrown with bad webhook url, authentication error type issues + */ + public void send(String message, String webhookUrl) throws TemporaryEventNotificationException, PermanentEventNotificationException { + + final Request request = new Request.Builder() + .url(webhookUrl) + .post(RequestBody.create(MediaType.parse(APPLICATION_JSON), message)) + .build(); + + LOG.debug("Posting to webhook url <{}> the payload is <{}>", + webhookUrl, + message); + + try (final Response r = httpClient.newCall(request).execute()) { + if (!r.isSuccessful()) { + throw new PermanentEventNotificationException( + "Expected successful HTTP response [2xx] but got [" + r.code() + "]. " + webhookUrl); + } + } catch (IOException e) { + throw new TemporaryEventNotificationException("Unable to send the Message. " + e.getMessage()); + } + } +} diff --git a/src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfigTest.java b/src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfigTest.java new file mode 100644 index 000000000..06a2d881e --- /dev/null +++ b/src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationConfigTest.java @@ -0,0 +1,149 @@ +/* + * 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 + * . + */ +package org.graylog.integrations.notifications.types.microsoftteams; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.graylog.events.event.EventDto; +import org.graylog.events.notifications.EventNotificationExecutionJob; +import org.graylog2.contentpacks.EntityDescriptorIds; +import org.graylog2.plugin.rest.ValidationResult; +import org.joda.time.DateTime; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class TeamsEventNotificationConfigTest { + + @Test + public void validate_succeeds_whenWebhookUrlIsValidUrl() { + TeamsEventNotificationConfig teamsEventNotificationConfig = TeamsEventNotificationConfig.builder() + .webhookUrl("http://graylog.org") + .build(); + ValidationResult result = teamsEventNotificationConfig.validate(); + Map> errors = result.getErrors(); + assertEquals(errors.size(), 0); + } + + @Test + public void validate_succeeds_whenWebhookUrlIsValidTeamsUrl() { + TeamsEventNotificationConfig teamsEventNotificationConfig = TeamsEventNotificationConfig.builder() + .webhookUrl("https://teams.webhook.office.com/webhookb2/d6068ba8-584c-41--bb7e-3b112e9b1ff4/IncomingWebhook/440/846c") + .build(); + ValidationResult result = teamsEventNotificationConfig.validate(); + Map> errors = result.getErrors(); + assertEquals(errors.size(), 0); + } + + @Test + public void validate_failsAndReturnsAnError_whenWebhookUrlIsInvalid() { + TeamsEventNotificationConfig teamsEventNotificationConfig = TeamsEventNotificationConfig.builder() + .webhookUrl("html:/?Thing.foo") + .build(); + ValidationResult result = teamsEventNotificationConfig.validate(); + assertTrue(result.failed()); + Map> errors = result.getErrors(); + assertEquals(errors.size(), 1); + assertEquals(((List) errors.get(TeamsEventNotificationConfig.FIELD_WEBHOOK_URL)).get(0), + TeamsEventNotificationConfig.INVALID_WEBHOOK_ERROR_MESSAGE); + } + + @Test + public void validate_failsAndReturnsAnError_whenWebhookUrlIsInvalidTeamsUrl() { + TeamsEventNotificationConfig teamsEventNotificationConfig = TeamsEventNotificationConfig.builder() + .webhookUrl("https://webhooks.office.com/foo") + .build(); + ValidationResult result = teamsEventNotificationConfig.validate(); + assertTrue(result.failed()); + Map> errors = result.getErrors(); + assertEquals(errors.size(), 1); + assertEquals(((List) errors.get(TeamsEventNotificationConfig.FIELD_WEBHOOK_URL)).get(0), + TeamsEventNotificationConfig.INVALID_TEAMS_URL_ERROR_MESSAGE); + } + + @Test + public void validate_messageBacklog() { + TeamsEventNotificationConfig negativeBacklogSize = TeamsEventNotificationConfig.builder() + .backlogSize(-1) + .build(); + + assertEquals(negativeBacklogSize.webhookUrl(), TeamsEventNotificationConfig.WEB_HOOK_URL); + + Collection expected = new ArrayList(); + expected.add(TeamsEventNotificationConfig.INVALID_BACKLOG_ERROR_MESSAGE); + + assertTrue(negativeBacklogSize.validate().failed()); + Map> errors = negativeBacklogSize.validate().getErrors(); + assertEquals(errors.get("backlog_size"), expected); + + Map> errors1 = negativeBacklogSize.validate().getErrors(); + assertEquals(errors1.get("backlog_size"), expected); + + + TeamsEventNotificationConfig goodBacklogSize = TeamsEventNotificationConfig.builder() + .backlogSize(5) + .build(); + assertFalse(goodBacklogSize.validate().failed()); + + } + + @Test + public void toJobTriggerData() { + + final DateTime now = DateTime.parse("2019-01-01T00:00:00.000Z"); + final ImmutableList keyTuple = ImmutableList.of("a", "b"); + + final EventDto eventDto = EventDto.builder() + .id("01DF119QKMPCR5VWBXS8783799") + .eventDefinitionType("aggregation-v1") + .eventDefinitionId("54e3deadbeefdeadbeefaffe") + .originContext("urn:graylog:message:es:graylog_0:199a616d-4d48-4155-b4fc-339b1c3129b2") + .eventTimestamp(now) + .processingTimestamp(now) + .streams(ImmutableSet.of("000000000000000000000002")) + .sourceStreams(ImmutableSet.of("000000000000000000000001")) + .message("Test message") + .source("source") + .keyTuple(keyTuple) + .key(String.join("|", keyTuple)) + .priority(4) + .alert(false) + .fields(ImmutableMap.of("hello", "world")) + .build(); + + final TeamsEventNotificationConfig teamsEventNotificationConfig = TeamsEventNotificationConfig.builder().build(); + EventNotificationExecutionJob.Data data = (EventNotificationExecutionJob.Data) teamsEventNotificationConfig.toJobTriggerData(eventDto); + assertEquals(data.type(), EventNotificationExecutionJob.TYPE_NAME); + assertEquals(data.eventDto().processingTimestamp(), now); + + } + + @Test(expected = NullPointerException.class) + public void toContentPackEntity() { + final TeamsEventNotificationConfig teamsEventNotificationConfig = TeamsEventNotificationConfig.builder().build(); + teamsEventNotificationConfig.toContentPackEntity(EntityDescriptorIds.empty()); + } +} + diff --git a/src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationTest.java b/src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationTest.java new file mode 100644 index 000000000..873c192da --- /dev/null +++ b/src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsEventNotificationTest.java @@ -0,0 +1,309 @@ +/* + * 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 + * . + */ +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.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import org.graylog.events.event.EventDto; +import org.graylog.events.notifications.EventNotificationContext; +import org.graylog.events.notifications.EventNotificationException; +import org.graylog.events.notifications.EventNotificationService; +import org.graylog.events.notifications.NotificationDto; +import org.graylog.events.notifications.NotificationTestData; +import org.graylog.events.notifications.PermanentEventNotificationException; +import org.graylog.events.notifications.TemporaryEventNotificationException; +import org.graylog.events.notifications.types.HTTPEventNotificationConfig; +import org.graylog.events.processor.EventDefinitionDto; +import org.graylog.integrations.notifications.types.util.RequestClient; +import org.graylog2.notifications.NotificationImpl; +import org.graylog2.notifications.NotificationService; +import org.graylog2.plugin.Message; +import org.graylog2.plugin.MessageSummary; +import org.graylog2.plugin.Tools; +import org.graylog2.plugin.system.NodeId; +import org.graylog2.shared.bindings.providers.ObjectMapperProvider; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TeamsEventNotificationTest { + + //code under test + TeamsEventNotification teamsEventNotification; + + @Mock + NodeId mockNodeId; + + @Mock + NotificationService mockNotificationService; + + @Mock + RequestClient mockrequestClient; + + @Mock + EventNotificationService notificationCallbackService; + + private TeamsEventNotificationConfig teamsEventNotificationConfig; + private EventNotificationContext eventNotificationContext; + + @Before + public void setUp() { + + getDummyTeamsNotificationConfig(); + eventNotificationContext = NotificationTestData.getDummyContext(getHttpNotification(), "ayirp").toBuilder().notificationConfig(teamsEventNotificationConfig).build(); + final ImmutableList messageSummaries = generateMessageSummaries(50); + when(notificationCallbackService.getBacklogForEvent(eventNotificationContext)).thenReturn(messageSummaries); + + teamsEventNotification = new TeamsEventNotification(notificationCallbackService, new ObjectMapperProvider().get(), + Engine.createEngine(), + mockNotificationService, + mockNodeId, mockrequestClient); + } + + private void getDummyTeamsNotificationConfig() { + teamsEventNotificationConfig = TeamsEventNotificationConfig.builder() + .type(TeamsEventNotificationConfig.TYPE_NAME) + .color("#FF2052") + .webhookUrl("axzzzz") + .backlogSize(1) + .customMessage("a custom message") + .build(); + } + + private NotificationDto getHttpNotification() { + return NotificationDto.builder() + .title("Foobar") + .id("1234") + .description("") + .config(HTTPEventNotificationConfig.Builder.create() + .url("http://localhost") + .build()) + .build(); + } + + @Test + public void createTeamsMessage() throws EventNotificationException { + String expected = "{\"themeColor\":\"#FF2052\",\"@type\":\"MessageCard\",\"text\":\"**Alert Event Definition Test Title triggered:**\\n\",\"@context\":\"http://schema.org/extensions\",\"sections\":[{\"activitySubtitle\":\"_Event Definition Test Description_\",\"facts\":[{\"name\":\"a custom message\",\"value\":\"\"}]}]}"; + TeamsMessage message = teamsEventNotification.createTeamsMessage(eventNotificationContext, teamsEventNotificationConfig); + String actual = message.getJsonString(); + assertThat(actual).isEqualTo(expected); + + } + + @After + public void tearDown() { + teamsEventNotification = null; + teamsEventNotificationConfig = null; + eventNotificationContext = null; + } + + @Test + public void buildDefaultMessage() { + String message = teamsEventNotification.buildDefaultMessage(eventNotificationContext); + assertThat(message).isNotEmpty(); + } + + @Test + public void getCustomMessageModel() { + List messageSummaries = generateMessageSummaries(50); + Map customMessageModel = teamsEventNotification.getCustomMessageModel(eventNotificationContext, teamsEventNotificationConfig.type(), messageSummaries); + //there are 9 keys and two asserts needs to be implemented (backlog,event) + assertThat(customMessageModel).isNotNull(); + assertThat(customMessageModel.get("event_definition_description")).isEqualTo("Event Definition Test Description"); + assertThat(customMessageModel.get("event_definition_title")).isEqualTo("Event Definition Test Title"); + assertThat(customMessageModel.get("event_definition_type")).isEqualTo("test-dummy-v1"); + assertThat(customMessageModel.get("type")).isEqualTo("teams-notification-v1"); + assertThat(customMessageModel.get("job_definition_id")).isEqualTo(""); + assertThat(customMessageModel.get("job_trigger_id")).isEqualTo(""); + } + + + @Test(expected = EventNotificationException.class) + public void execute_with_invalid_webhook_url() throws EventNotificationException { + givenGoodNotificationService(); + givenGoodNodeId(); + givenTeamsClientThrowsPermException(); + //when execute is called with a invalid webhook URL, we expect a event notification exception + teamsEventNotification.execute(eventNotificationContext); + } + + + @Test(expected = EventNotificationException.class) + public void execute_with_null_event_timerange() throws EventNotificationException { + EventNotificationContext yetAnotherContext = getEventNotificationContextToSimulateNullPointerException(); + assertThat(yetAnotherContext.event().timerangeStart().isPresent()).isFalse(); + assertThat(yetAnotherContext.event().timerangeEnd().isPresent()).isFalse(); + assertThat(yetAnotherContext.notificationConfig().type()).isEqualTo(TeamsEventNotificationConfig.TYPE_NAME); + teamsEventNotification.execute(yetAnotherContext); + } + + private EventNotificationContext getEventNotificationContextToSimulateNullPointerException() { + final DateTime now = DateTime.parse("2019-01-01T00:00:00.000Z"); + final ImmutableList keyTuple = ImmutableList.of("a", "b"); + + final EventDto eventDto = EventDto.builder() + .id("01DF119QKMPCR5VWBXS8783799") + .eventDefinitionType("aggregation-v1") + .eventDefinitionId("54e3deadbeefdeadbeefaffe") + .originContext("urn:graylog:message:es:graylog_0:199a616d-4d48-4155-b4fc-339b1c3129b2") + .eventTimestamp(now) + .processingTimestamp(now) + .streams(ImmutableSet.of("000000000000000000000002")) + .sourceStreams(ImmutableSet.of("000000000000000000000001")) + .message("Test message") + .source("source") + .keyTuple(keyTuple) + .key(String.join("|", keyTuple)) + .priority(4) + .alert(false) + .fields(ImmutableMap.of("hello", "world")) + .build(); + + //uses the eventDEfinitionDto from NotificationTestData.getDummyContext in the setup method + EventDefinitionDto eventDefinitionDto = eventNotificationContext.eventDefinition().orElseThrow(NullPointerException::new); + return EventNotificationContext.builder() + .notificationId("1234") + .notificationConfig(teamsEventNotificationConfig) + .event(eventDto) + .eventDefinition(eventDefinitionDto) + .build(); + } + + + private void givenGoodNotificationService() { + given(mockNotificationService.buildNow()).willReturn(new NotificationImpl().addTimestamp(Tools.nowUTC())); + } + + private void givenTeamsClientThrowsPermException() throws TemporaryEventNotificationException, PermanentEventNotificationException { + doThrow(PermanentEventNotificationException.class) + .when(mockrequestClient) + .send(any(), anyString()); + + } + + private void givenGoodNodeId() { + when(mockNodeId.toString()).thenReturn("12345"); + assertThat(mockNodeId).isNotNull(); + assertThat(mockNodeId.toString()).isEqualTo("12345"); + } + + + @Test + public void buildCustomMessage() throws PermanentEventNotificationException { + JsonNode expectedCustomMessage = teamsEventNotification.buildCustomMessage(eventNotificationContext, teamsEventNotificationConfig, "test"); + assertThat(expectedCustomMessage).isNotEmpty(); + + } + + @Test(expected = PermanentEventNotificationException.class) + public void buildCustomMessage_with_invalidTemplate() throws EventNotificationException { + teamsEventNotificationConfig = buildInvalidTemplate(); + teamsEventNotification.buildCustomMessage(eventNotificationContext, teamsEventNotificationConfig, "Title: ${does't exist}"); + } + + + @Test + public void test_customMessage() throws PermanentEventNotificationException { + + TeamsEventNotificationConfig TeamsConfig = TeamsEventNotificationConfig.builder() + .backlogSize(5) + .build(); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode message = teamsEventNotification.buildCustomMessage(eventNotificationContext, TeamsConfig, "Title: ${event_definition_title}"); + Map fact = new HashMap<>(); + fact.put("name", "Title"); + fact.put("value", "Event Definition Test Title"); + List> facts = new ArrayList<>(); + facts.add(fact); + + assertThat(message).isEqualTo(objectMapper.convertValue(facts, JsonNode.class)); + } + + + @Test + public void test_backlog_message_limit_when_backlogSize_isFive() { + TeamsEventNotificationConfig TeamsConfig = TeamsEventNotificationConfig.builder() + .backlogSize(5) + .build(); + + //global setting is at N and the message override is 5 then the backlog size = 5 + List messageSummaries = teamsEventNotification.getMessageBacklog(eventNotificationContext, TeamsConfig); + assertThat(messageSummaries.size()).isEqualTo(5); + } + + @Test + public void test_backlog_message_limit_when_backlogSize_isZero() { + TeamsEventNotificationConfig TeamsConfig = TeamsEventNotificationConfig.builder() + .backlogSize(0) + .build(); + + //global setting is at N and the message override is 0 then the backlog size = 50 + List messageSummaries = teamsEventNotification.getMessageBacklog(eventNotificationContext, TeamsConfig); + assertThat(messageSummaries.size()).isEqualTo(50); + } + + @Test + public void test_backlog_message_limit_When_eventNotificationContext_isNull() { + TeamsEventNotificationConfig TeamsConfig = TeamsEventNotificationConfig.builder() + .backlogSize(0) + .build(); + + //global setting is at N and the eventNotificationContext is null then the message summaries is null + List messageSummaries = teamsEventNotification.getMessageBacklog(null, TeamsConfig); + assertThat(messageSummaries).isNull(); + } + + + ImmutableList generateMessageSummaries(int size) { + + List messageSummaries = new ArrayList<>(); + for (int i = 0; i < size; i++) { + MessageSummary summary = new MessageSummary("graylog_" + i, new Message("Test message_" + i, "source" + i, new DateTime(2020, 9, 6, 17, 0, DateTimeZone.UTC))); + messageSummaries.add(summary); + } + return ImmutableList.copyOf(messageSummaries); + } + + TeamsEventNotificationConfig buildInvalidTemplate() { + TeamsEventNotificationConfig.Builder builder = TeamsEventNotificationConfig.builder(); + builder.customMessage("Title"); + return builder.build(); + } +} + diff --git a/src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsMessageTest.java b/src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsMessageTest.java new file mode 100644 index 000000000..aa561e7db --- /dev/null +++ b/src/test/java/org/graylog/integrations/notifications/types/microsoftteams/TeamsMessageTest.java @@ -0,0 +1,45 @@ +/* + * 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 + * . + */ +package org.graylog.integrations.notifications.types.microsoftteams; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TeamsMessageTest { + + ObjectMapper objectMapper = new ObjectMapper(); + @Test + public void validateTitle() throws IOException { + JsonNode customMessage = objectMapper.readTree("{\"name\":\"Type\",\"value\":\"test-dummy-v1\"}"); + TeamsMessage message = new TeamsMessage("#FF0000", "", "\"**Alert Event Definition Test Title triggered:**\"",customMessage,"_Event Definition Test Description_" ); + String expected = message.getJsonString(); + List text = getJsonNodeFieldValue(expected, "text"); + assertThat(text).isNotEmpty(); + } + + List getJsonNodeFieldValue(String expected, String fieldName) throws IOException { + final byte[] bytes = expected.getBytes(); + JsonNode jsonNode = new ObjectMapper().readTree(bytes); + return jsonNode.findValuesAsText(fieldName); + } +} diff --git a/src/web/event-notifications/event-notification-details/TeamsNotificationDetails.tsx b/src/web/event-notifications/event-notification-details/TeamsNotificationDetails.tsx new file mode 100644 index 000000000..a65cdfdf3 --- /dev/null +++ b/src/web/event-notifications/event-notification-details/TeamsNotificationDetails.tsx @@ -0,0 +1,47 @@ +/* + * 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 + * . + */ +import * as React from 'react'; +import styled, { css } from 'styled-components'; + +import { ReadOnlyFormGroup } from 'components/common'; +import { Well } from 'components/bootstrap'; + +import type { TeamsNotificationSummaryType } from '../types'; + +const NewExampleWell = styled(Well)(({ theme }) => css` + margin-bottom: 5px; + font-family: ${theme.fonts.family.monospace}; + font-size: ${theme.fonts.size.body}; + white-space: pre-wrap; + word-wrap: break-word; +`); + +const TeamsNotificationDetails: React.FC = ({ notification }) => ( + <> + + + {notification.config.custom_message || Empty body} + + )} /> + + + +); + +export default TeamsNotificationDetails; diff --git a/src/web/event-notifications/event-notification-types/TeamsNotificationForm.tsx b/src/web/event-notifications/event-notification-types/TeamsNotificationForm.tsx new file mode 100644 index 000000000..d006c0c48 --- /dev/null +++ b/src/web/event-notifications/event-notification-types/TeamsNotificationForm.tsx @@ -0,0 +1,190 @@ +/* + * 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 + * . + */ +import React from 'react'; +import cloneDeep from 'lodash/cloneDeep'; +import get from 'lodash/get'; +import camelCase from 'lodash/camelCase'; + +import { getValueFromInput } from 'util/FormsUtils'; +import { Input, Button, ControlLabel, FormControl, FormGroup, HelpBlock, InputGroup } from 'components/bootstrap'; +import { ColorPickerPopover } from 'components/common'; +import ColorLabel from 'components/sidecars/common/ColorLabel'; + +import type { ConfigType, ValidationType } from '../types'; + +type TeamsNotificationFormType = { + config: ConfigType, + validation: ValidationType + onChange: any +} + +class TeamsNotificationForm extends React.Component { + static defaultConfig = { + color: '#FF0000', + webhook_url: '', + /* eslint-disable no-template-curly-in-string */ + custom_message: '' + + '--- [Event Definition] ----\n' + + 'Title: ${event_definition_title}\n' + + 'Type: ${event_definition_type}\n' + + '--- [Event] ----\n' + + 'Timestamp: ${event.timestamp}\n' + + 'Message: ${event.message}\n' + + 'Source: ${event.source}\n' + + 'Key: ${event.key}\n' + + 'Priority: ${event.priority}\n' + + 'Alert: ${event.alert}\n' + + 'Timestamp Processing: ${event.timestamp}\n' + + 'Timerange Start: ${event.timerange_start}\n' + + 'Timerange End: ${event.timerange_end}\n' + + 'Event Fields:\n' + + '${foreach event.fields field}\n' + + '${field.key}: ${field.value}\n' + + '${end}\n' + + '${if backlog}\n' + + '--- [Backlog] ----------\n' + + 'Last messages accounting for this alert:\n' + + '${foreach backlog message}\n' + + '${message.timestamp} :: ${message.source} :: ${message.message}\n' + + '${message.message}\n' + + '${end}' + + '${end}\n', + /* eslint-enable no-template-curly-in-string */ + icon_url: '', + backlog_size: 0, + + }; + + constructor(props: TeamsNotificationFormType | Readonly) { + super(props); + + const defaultBacklogSize = props.config.backlog_size; + + this.state = { + isBacklogSizeEnabled: defaultBacklogSize > 0, + backlogSize: defaultBacklogSize, + }; + } + + handleBacklogSizeChange = (event: { target: { name: string; }; }) => { + const { name } = event.target; + const value = getValueFromInput(event.target); + + this.setState({ [camelCase(name)]: value }); + this.propagateChange(name, getValueFromInput(event.target)); + }; + + toggleBacklogSize = () => { + const { isBacklogSizeEnabled, backlogSize } = this.state; + + this.setState({ isBacklogSizeEnabled: !isBacklogSizeEnabled }); + this.propagateChange('backlog_size', (isBacklogSizeEnabled ? 0 : backlogSize)); + }; + + propagateChange = (key: string, value: any) => { + const { config, onChange } = this.props; + const nextConfig = cloneDeep(config); + nextConfig[key] = value; + onChange(nextConfig); + }; + + handleColorChange: (color: string, _: any, hidePopover: any) => void = (color, _, hidePopover) => { + hidePopover(); + this.propagateChange('color', color); + }; + + handleChange = (event: { target: { name: any; }; }) => { + const { name } = event.target; + this.propagateChange(name, getValueFromInput(event.target)); + }; + + render() { + const { config, validation } = this.props; + const { isBacklogSizeEnabled, backlogSize } = this.state; + const url = 'https://docs.graylog.org/docs/alerts#notifications'; + const element =

Custom message to be appended below the alert title. See docs for more details.

; + + return ( + <> + + + Configuration color +
+ +
+ Change color} + onChange={this.handleColorChange} /> +
+
+ Choose a color to use for this configuration. +
+ + + + + Message Backlog Limit (optional) + + + + + + + Limit the number of backlog messages sent as part of the Microsoft Teams notification. If set to 0, no limit will be enforced. + + + + + + ); + } +} + +export default TeamsNotificationForm; diff --git a/src/web/event-notifications/event-notification-types/TeamsNotificationSummary.tsx b/src/web/event-notifications/event-notification-types/TeamsNotificationSummary.tsx new file mode 100644 index 000000000..c23bde0b9 --- /dev/null +++ b/src/web/event-notifications/event-notification-types/TeamsNotificationSummary.tsx @@ -0,0 +1,59 @@ +/* + * 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 + * . + */ +import React from 'react'; + +import CommonNotificationSummary from 'components/event-notifications/event-notification-types/CommonNotificationSummary'; + +import type { TeamsNotificationSummaryType } from '../types'; + +function TeamsNotificationSummary({ notification, ...restProps }: TeamsNotificationSummaryType) { + return ( + + + Color + {notification?.config?.color} + + + Webhook URL + {notification.config.webhook_url} + + + + Custom Message + {notification.config.custom_message} + + + Message Backlog Limit + {notification.config.backlog_size} + + + Icon URL + {notification.config.icon_url} + + + Graylog URL + {notification.config.graylog_url} + + + ); +} + +TeamsNotificationSummary.defaultProps = { + notification: {}, +}; + +export default TeamsNotificationSummary; diff --git a/src/web/event-notifications/types.ts b/src/web/event-notifications/types.ts new file mode 100644 index 000000000..8725036d2 --- /dev/null +++ b/src/web/event-notifications/types.ts @@ -0,0 +1,47 @@ +/* + * 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 + * . + */ +export type TeamsNotificationSummaryType = { + type: string, + notification: NotificationType, +} + +export type NotificationType = { + config: ConfigType, +} + +export interface ConfigType { + defaultValue?: any, + graylog_url?: string, + icon_url?: string, + backlog_size?: number, + custom_message: string, + webhook_url?: string, + color?: string, +} + +export type ValidationType = { + failed?: boolean, + errors?: ErrorType, +} + +export interface ErrorType { + webhook_url: string[], + color: string[], + icon_url: string, + backlog_size: number, + custom_message: string, +} diff --git a/src/web/index.jsx b/src/web/index.jsx index f3bae4759..01d658991 100644 --- a/src/web/index.jsx +++ b/src/web/index.jsx @@ -29,14 +29,17 @@ import PagerDutyNotificationSummary from './pager-duty/PagerDutyNotificationSumm import SlackNotificationDetails from './event-notifications/event-notification-details/SlackNotificationDetails'; import SlackNotificationForm from './event-notifications/event-notification-types/SlackNotificationForm'; import SlackNotificationSummary from './event-notifications/event-notification-types/SlackNotificationSummary'; - -import packageJson from '../../package.json'; -import GreyNoiseAdapterFieldSet from "./dataadapters/GreyNoiseAdapterFieldSet"; -import GreyNoiseAdapterSummary from "./dataadapters/GreyNoiseAdapterSummary"; -import GreyNoiseAdapterDocumentation from "./dataadapters/GreyNoiseAdapterDocumentation"; +import TeamsNotificationDetails from './event-notifications/event-notification-details/TeamsNotificationDetails'; +import TeamsNotificationForm from './event-notifications/event-notification-types/TeamsNotificationForm'; +import TeamsNotificationSummary from './event-notifications/event-notification-types/TeamsNotificationSummary'; +import GreyNoiseAdapterFieldSet from './dataadapters/GreyNoiseAdapterFieldSet'; +import GreyNoiseAdapterSummary from './dataadapters/GreyNoiseAdapterSummary'; +import GreyNoiseAdapterDocumentation from './dataadapters/GreyNoiseAdapterDocumentation'; import GreyNoiseCommunityIpLookupAdapterDocumentation from './dataadapters/GreyNoiseCommunityIpLookupAdapterDocumentation'; +import packageJson from '../../package.json'; + const manifest = new PluginManifest(packageJson, { routes: [ { path: Routes.INTEGRATIONS.AWS.CLOUDWATCH.index, component: AWSCloudWatchApp }, @@ -65,6 +68,14 @@ const manifest = new PluginManifest(packageJson, { detailsComponent: SlackNotificationDetails, defaultConfig: SlackNotificationForm.defaultConfig, }, + { + type: 'teams-notification-v1', + displayName: 'Microsoft Teams Notification', + formComponent: TeamsNotificationForm, + summaryComponent: TeamsNotificationSummary, + detailsComponent: TeamsNotificationDetails, + defaultConfig: TeamsNotificationForm.defaultConfig, + }, ], lookupTableAdapters: [ { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..83a10fd9c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": false, + "downlevelIteration": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react", + "baseUrl": ".", + "paths": { + "*": [ + "*", + "./src/web/*", + "./test/web/*", + "../graylog2-server/graylog2-web-interface/src/*", + "../graylog2-server/graylog2-web-interface/test/*", + ] + } + }, + "include": [ + "src", + "../graylog2-server/graylog2-web-interface/src/@types/**/*", + "../graylog2-server/graylog2-web-interface/src/**/*.d.ts" + ] +}