diff --git a/.gitignore b/.gitignore index 5f2dbe1..1e4f950 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ buildNumber.properties # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) !/.mvn/wrapper/maven-wrapper.jar + +node_modules/ +node/ +build/ +cache/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2a6c8f4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,34 @@ +dist: trusty +sudo: required +language: java +jdk: +- openjdk8 +addons: + apt: + packages: + - rpm +install: +- git clone --branch 2.4 --depth=1 --no-single-branch https://github.com/Graylog2/graylog2-server ../graylog2-server +- (cd ../graylog2-server && mvn -DskipTests=true compile -B -V) +- mvn install -DskipTests=true -Dmaven.javadoc.skip=true -Dskip.web.build=true -B -V +script: +- mvn package -B +before_deploy: +- mvn jdeb:jdeb && export RELEASE_DEB_FILE=$(ls target/*.deb) +- mvn rpm:rpm && export RELEASE_RPM_FILE=$(find target/ -name '*.rpm' | tail -1) +- rm -f target/original-*.jar +- export RELEASE_PKG_FILE=$(ls target/*.jar) +- echo "Deploying release to GitHub releases" +deploy: + provider: releases + api_key: + secure: L0SCYpBQTuuXmzrl68iF8y80jjXRXtjQzdq+gbf0+hPGZuRxcl/UwJcEz4pOBNAmLy+zh+xz/IZPNNm/KnYLNrrJk69a1LsJ58H3xpis8ycdzjIQvXuJn6i7Pffrd5nER9TpsiokERzBlT7hiEJaB36pvxQGUVSbhoCxciQ8x+9Dnh4Xx6HFtbzqJlB69hQMA5r4ojOoinbbaM4qBr7/ypAydwuRiYiXFPWinL2JJHTLeDb5XSNyDcHqLOY3l9hPQ/mCuz1ufgyjbXBhVAd8zxsksjvVR5O6XUlSUxM/TWfSmmTgRnU23jJuOV1bSZWlMBeK6WNw2y6lxzR2BsEDa1c+HOAg33FNo9laMW7OTumI3Xsdy3vtCaPnb1cHKQH6cXSvu2vRzVNST8edwQjTs/jT842/Eodo0X7WH9Jdn+dI/L4zCk8O4OEeTMBBapBdujqhuRB0eHIAZxy4arEGFwKx/v1UtL9UgzWT+T1hX+T3Zk1JsnCg1IU9L0/J5OWrMB6jyFeyx5kU4s2/b83DDisyNq2LC8waML7Z7giyj8PMr5QKEjhF5/Fe21eGjBJaHjnzQwHlaFBkhHGbrmzQvDsh3KX0pxQKIdy0frsT1pvoxRqz7zvnUTvpF+DqmbQldh9ALGpdbJONLgIqmclui+NgshEU4dKPhMsOg6+dpI0= + file: + - "${RELEASE_PKG_FILE}" + - "${RELEASE_DEB_FILE}" + - "${RELEASE_RPM_FILE}" + skip_cleanup: true + draft: true + on: + repo: airbus-cyber/graylog-plugin-logging-alert + jdk: openjdk8 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cbbc801 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [1.0.0](https://github.com/airbus-cyber/graylog-plugin-logging-alert/tree/1.0.0) (2018-12-19) + +* First release diff --git a/README.md b/README.md index 392c5aa..060e0a9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Logging Alert Plugin for Graylog +[![Build Status](https://travis-ci.org/airbus-cyber/graylog-plugin-logging-alert.svg?branch=master)](https://travis-ci.org/airbus-cyber/graylog-plugin-logging-alert) [![License](https://img.shields.io/badge/license-GPL--3.0-orange.svg)](https://www.gnu.org/licenses/gpl-3.0.txt) +[![GitHub Release](https://img.shields.io/badge/release-v1.0.0-blue.svg)](https://github.com/airbus-cyber/graylog-plugin-logging-alert/releases) #### Alert notification plugin for Graylog to generate log messages from alerts @@ -39,7 +41,15 @@ Then, in the popup that occurs, you can configure the **Title** of the notificat You can configure the **Alert Severity**. You have the choice between 4 levels of severity. -You can also configure the **Log Content** to log the information you want. +You can also configure the **Log Content** to log the information you want. Some plugin-specific fields values can be added to the log content. + +| Plugin-specific Fields | Description | +| -------------------------- | ------------------------------------------------------- | +| logging_alert.id | ID of the alert | +| logging_alert.severity | Severity of the alert | +| logging_alert.detect_time | Timestamp of the first message that triggered the alert | +| logging_alert.alert_url | URI of the Graylog alert | +| logging_alert.messages_url | URI to the retrieve messages that triggered the alert | The parameter **Split Fields** allow you to split the alert based on message field values. Thus, a different alert id is generated for each value of one or more message fields. @@ -55,7 +65,7 @@ Make sure you also configured alert conditions for the stream so that the alerts Click on **Configure** in the **System / Configurations** section to update the plugin configuration. -In the popup that occurs, you can configure the default value of the parameters that are set when adding a new notification : **Default Alert Severity**, **Default Log Content** and **Default Aggregation Time Range**. +In the popup that occurs, you can configure the default value of the parameters that are set when adding a new notification: **Default Alert Severity**, **Default Log Content** and **Default Aggregation Time Range**. You can define a **Line Break Substitution** of the log content in order to help parsing log fields and their values. Thus a separator can be inserted between the fields of the log content. diff --git a/build.config.js b/build.config.js new file mode 100755 index 0000000..c494fd4 --- /dev/null +++ b/build.config.js @@ -0,0 +1,6 @@ +const path = require('path'); + +module.exports = { + // Make sure that this is the correct path to the web interface part of the Graylog server repository. + web_src_path: path.resolve(__dirname, '../graylog2-server/graylog2-web-interface'), +}; diff --git a/package.json b/package.json new file mode 100755 index 0000000..9053184 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "LoggingAlert", + "version": "1.0.0", + "description": "Graylog plugin LoggingAlert Web Interface", + "repository": { + "type": "git", + "url": "https://github.com/airbus-cyber/graylog-plugin-logging-alert" + }, + "scripts": { + "build": "webpack --bail" + }, + "keywords": [ + "graylog" + ], + "author": "Airbus CyberSecurity", + "license": "GPL-3.0", + "dependencies": {}, + "devDependencies": { + "graylog-web-plugin": "file:../graylog2-server/graylog2-web-interface/packages/graylog-web-plugin", + "react": "^0.14.9", + "react-addons-test-utils": "^0.14.8", + "react-dom": "^0.14.9", + "webpack": "^3.4.1" + } +} diff --git a/pom.xml b/pom.xml new file mode 100755 index 0000000..2c5a373 --- /dev/null +++ b/pom.xml @@ -0,0 +1,236 @@ + + + 4.0.0 + + 3.1 + + + + org.graylog.plugins + graylog-plugin-web-parent + 2.4.3 + + + com.airbus-cyber-security.graylog + graylog-plugin-logging-alert + 1.0.0 + jar + ${project.artifactId} + Graylog ${project.artifactId} plugin. + https://www.graylog.org + + + UTF-8 + 1.8 + 1.8 + 2.4.3 + /usr/share/graylog-server/plugin + 1.5.1 + + + 3.8.0 + 2.0.0 + 4.12 + 2.8.9 + 1.7.0 + 1.1.0 + + + + + org.graylog2 + graylog2-server + provided + + + org.apache.logging.log4j + log4j-slf4j-impl + + + + + com.google.auto.value + auto-value + ${auto-value.version} + provided + + + + + uk.org.lidalia + slf4j-test + ${slf4j-test.version} + test + + + junit + junit + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + org.assertj + assertj-joda-time + ${assertj-joda-time.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.powermock + powermock-api-mockito2 + ${powermock.version} + test + + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + + + + + + + build + + + src/main/resources + true + + + + + maven-assembly-plugin + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + + com.google.auto.value.processor.AutoValueProcessor + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + + ${project.groupId}.${project.artifactId} + + + target + + + + org.apache.maven.plugins + maven-shade-plugin + + false + + + com.airbus-cyber-security.graylog:* + + + + + + package + + shade + + + + + + + + + + + org.apache.maven.plugins + maven-release-plugin + + true + forked-path + @{project.version} + clean test + package + + + + jdeb + org.vafer + + ${project.build.directory}/${project.artifactId}-${project.version}.deb + + + ${project.build.directory}/${project.build.finalName}.jar + file + + perm + ${graylog2.plugin-dir} + 644 + root + root + + + + + + + + org.codehaus.mojo + rpm-maven-plugin + + Application/Internet + + /usr + + + _unpackaged_files_terminate_build 0 + _binaries_in_noarch_packages_terminate_build 0 + + 644 + 755 + root + root + + + ${graylog2.plugin-dir} + + + ${project.build.directory}/ + + ${project.build.finalName}.jar + + + + + + + + + + diff --git a/src/deb/control/control b/src/deb/control/control new file mode 100644 index 0000000..cf33fb9 --- /dev/null +++ b/src/deb/control/control @@ -0,0 +1,8 @@ +Package: [[name]] +Version: [[version]] +Architecture: all +Maintainer: Airbus CyberSecurity +Section: web +Priority: optional +Depends: graylog-server +Description: [[description]] diff --git a/src/main/java/com/airbus_cyber_security/graylog/LoggingAlert.java b/src/main/java/com/airbus_cyber_security/graylog/LoggingAlert.java new file mode 100755 index 0000000..10597ee --- /dev/null +++ b/src/main/java/com/airbus_cyber_security/graylog/LoggingAlert.java @@ -0,0 +1,446 @@ +package com.airbus_cyber_security.graylog; + +import java.util.*; +import java.util.stream.Collectors; + +import org.graylog2.alerts.Alert; +import org.graylog2.alerts.AlertService; +import org.graylog2.indexer.IndexSetRegistry; +import org.graylog2.indexer.indices.Indices; +import org.graylog2.indexer.results.SearchResult; +import org.graylog2.indexer.searches.Searches; +import org.graylog2.indexer.searches.Sorting; +import org.graylog2.plugin.Message; +import org.graylog2.plugin.MessageSummary; +import org.graylog2.plugin.Tools; +import org.graylog2.plugin.alarms.AlertCondition; +import org.graylog2.plugin.alarms.AlertCondition.CheckResult; +import org.graylog2.plugin.alarms.callbacks.AlarmCallback; +import org.graylog2.plugin.alarms.callbacks.AlarmCallbackConfigurationException; +import org.graylog2.plugin.alarms.callbacks.AlarmCallbackException; +import org.graylog2.plugin.cluster.ClusterConfigService; +import org.graylog2.plugin.configuration.Configuration; +import org.graylog2.plugin.configuration.ConfigurationException; +import org.graylog2.plugin.configuration.ConfigurationRequest; +import org.graylog2.plugin.configuration.fields.ConfigurationField; +import org.graylog2.plugin.configuration.fields.DropdownField; +import org.graylog2.plugin.configuration.fields.ListField; +import org.graylog2.plugin.configuration.fields.NumberField; +import org.graylog2.plugin.configuration.fields.TextField; +import org.graylog2.plugin.indexer.searches.timeranges.AbsoluteRange; +import org.graylog2.plugin.indexer.searches.timeranges.InvalidRangeParametersException; +import org.graylog2.plugin.indexer.searches.timeranges.RelativeRange; +import org.graylog2.plugin.streams.Stream; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.airbus_cyber_security.graylog.config.LoggingAlertConfig; +import com.airbus_cyber_security.graylog.config.SeverityType; +import com.floreysoft.jmte.Engine; +import com.google.common.collect.Maps; +import com.google.inject.Inject; + +/** + * This is the plugin. Your class should implement one of the existing plugin + * interfaces. (i.e. AlarmCallback, MessageInput, MessageOutput) + */ +public class LoggingAlert implements AlarmCallback{ + + private final Logger LOGGER; + private final Logger LOGGER_OVERFLOW; + + private static final String FIELD_SEVERITY = "severity"; + private static final String FIELD_BODY= "content"; + private static final String FIELD_SPLIT = "split_fields"; + private static final String FIELD_COMMENT = "comment"; + private static final String FIELD_AGGREGATION_TIME = "aggregation_time"; + + private static final String SEPARATOR_TEMPLATE = "\n"; + + private static final String MSGS_URL_BEGIN = "/search?rangetype=absolute&from="; + private static final String MSGS_URL_TO = "&to="; + private static final String MSGS_URL_STREAM = "streams%3A"; + private static final int SIZE_STREAM = 24; + + + private final Engine templateEngine = new Engine(); + private final ClusterConfigService clusterConfigService; + private Configuration configs; + private final Indices indices; + private final IndexSetRegistry indexSetRegistry; + private final AlertService alertService; + private final Searches searches; + private final String fieldAlertID; + private final String aggregationStreamID; + private final int limitOverflow; + private final String separator; + + @Inject + public LoggingAlert(final ClusterConfigService clusterConfigService, Indices indices, IndexSetRegistry indexSetRegistry, + AlertService alertService, Searches searches) { + this.clusterConfigService = clusterConfigService; + this.indices = indices; + this.indexSetRegistry = indexSetRegistry; + this.alertService = alertService; + this.searches = searches; + + final LoggingAlertConfig configGeneral = clusterConfigService.getOrDefault(LoggingAlertConfig.class, + LoggingAlertConfig.createDefault()); + + aggregationStreamID = configGeneral.accessAggregationStream(); + fieldAlertID = configGeneral.accessFieldAlertId(); + limitOverflow = configGeneral.accessLimitOverflow(); + separator = configGeneral.accessSeparator(); + + LOGGER = LoggerFactory.getLogger(configGeneral.accessAlertTag()); + LOGGER_OVERFLOW = LoggerFactory.getLogger(configGeneral.accessOverflowTag()); + } + + private Map getModel(Stream stream, AlertCondition.CheckResult checkResult, + Message message, LoggingAlertFields loggingAlertFields) { + Map model = new HashMap<>(); + model.put("message", message); + model.put("stream", stream); + model.put("check_result", checkResult); + model.put("alertCondition", checkResult.getTriggeredCondition()); + model.put("logging_alert", loggingAlertFields); + + return model; + } + + String buildBody(Stream stream, AlertCondition.CheckResult checkResult, Message message, LoggingAlertFields loggingAlertFields) { + Map model = getModel(stream, checkResult, message, loggingAlertFields); + return this.templateEngine.transform(configs.getString(FIELD_BODY).replace(SEPARATOR_TEMPLATE, separator), model); + } + + + private String getAggregationAlertID(Stream stream, DateTime date, String sufixID) { + try { + RelativeRange relativeRange = RelativeRange.create(configs.getInt(FIELD_AGGREGATION_TIME) * 60); + final AbsoluteRange range = AbsoluteRange.create(relativeRange.getFrom(), relativeRange.getTo()); + + final String filter = "streams:" + aggregationStreamID; + List listAlert = alertService.loadRecentOfStream(stream.getId(), + date.minusMinutes(configs.getInt(FIELD_AGGREGATION_TIME)), 300); + if(!listAlert.isEmpty()) { + Iterator it = listAlert.iterator(); + StringBuilder bldStringsearchQuery = new StringBuilder(fieldAlertID+": "+it.next().getId()+sufixID); + while (it.hasNext()) { + bldStringsearchQuery.append(" OR "+fieldAlertID+": "+it.next().getId()+sufixID); + } + + final SearchResult backlogResult = searches.search(bldStringsearchQuery.toString(), filter, + range, 10, 0, new Sorting(Message.FIELD_TIMESTAMP, Sorting.Direction.DESC)); + + if(!backlogResult.getResults().isEmpty()) { + return backlogResult.getResults().get(0).getMessage().getField(fieldAlertID).toString(); + } + } + } catch (InvalidRangeParametersException e) { + e.printStackTrace(); + } + return null; + } + + private String getGraylogID(Stream stream, CheckResult result) { + Optional optionalAlert = alertService.getLastTriggeredAlert(stream.getId(), result.getTriggeredCondition().getId()); + if(optionalAlert.isPresent()) { + return optionalAlert.get().getId(); + }else { + return null; + } + } + + private String getNewAlertID(Stream stream, CheckResult result) { + String graylogID = getGraylogID(stream, result); + if(graylogID != null) { + return graylogID; + }else { + return UUID.randomUUID().toString(); + } + } + + private String getAlertID(Stream stream, CheckResult result, String sufixID) { + String loggingAlertID = null; + + if(configs.intIsSet(FIELD_AGGREGATION_TIME) && configs.getInt(FIELD_AGGREGATION_TIME) > 0 && + aggregationStreamID != null && !aggregationStreamID.isEmpty()) { + loggingAlertID = getAggregationAlertID(stream, result.getTriggeredAt(), sufixID); + } + + if(loggingAlertID == null || loggingAlertID.isEmpty()) { + loggingAlertID = getNewAlertID(stream, result) + sufixID; + } + return loggingAlertID; + } + + private String getValuesAggregationField(MessageSummary messageSummary) { + StringBuilder valuesAggregationField = new StringBuilder(); + for (String field : configs.getList(FIELD_SPLIT, Collections.emptyList())) { + valuesAggregationField.append(messageSummary.getField(field)); + } + return valuesAggregationField.toString(); + } + + private String getAlertUrl(Stream stream, CheckResult result) + { + Optional optionalAlert = alertService.getLastTriggeredAlert(stream.getId(), result.getTriggeredCondition().getId()); + if(optionalAlert.isPresent()) { + return "/alerts/"+optionalAlert.get().getId(); + } + return ""; + } + + private String getPreviousMessagesURL(String streamID, DateTime timeBegin, DateTime timeEnd) { + final String filter = "streams:" + streamID; + final AbsoluteRange range = AbsoluteRange.create(timeBegin, timeEnd); + final SearchResult backlogResult = searches.search("*", filter, + range, 10, 0, new Sorting(Message.FIELD_TIMESTAMP, Sorting.Direction.DESC)); + + if(!backlogResult.getResults().isEmpty()) { + return backlogResult.getResults().get(0).getMessage().getField("messages_url").toString(); + } + + return null; + } + + private DateTime getTimeFrom(String msgsURL, DateTimeFormatter timeFormatter) { + int indexBegin = msgsURL.indexOf(MSGS_URL_BEGIN); + int indexEnd = msgsURL.indexOf(MSGS_URL_TO); + if(indexBegin > 0 && indexEnd > 0) { + String date = msgsURL.substring(indexBegin+MSGS_URL_BEGIN.length(), indexEnd); + try { + return DateTime.parse(date, timeFormatter); + }catch(Exception e) { + /* Invalid date */ + } + } + + return null; + } + + private String getQuery(String msgsURL) { + StringBuilder query = new StringBuilder(); + int indexBegin = msgsURL.indexOf(MSGS_URL_STREAM); + while(indexBegin > 0) { + int indexEnd = indexBegin + MSGS_URL_STREAM.length() + SIZE_STREAM; + query.append("+OR+" + msgsURL.substring(indexBegin, indexEnd)); + indexBegin = msgsURL.indexOf(MSGS_URL_STREAM, indexEnd); + } + + return query.toString(); + } + + private String getMessagesUrl(Stream stream, CheckResult result, MessageSummary messageSummary, DateTime timeBeginSearch) + { + Optional optionalAlert = alertService.getLastTriggeredAlert(stream.getId(), result.getTriggeredCondition().getId()); + if(optionalAlert.isPresent()) { + DateTimeFormatter timeFormatter = DateTimeFormat.forPattern("yyy-MM-dd'T'HH'%3A'mm'%3A'ss.SSS'Z'"); + + DateTime endTime; + /* If the alert is interval and resolved */ + if(optionalAlert.get().isInterval() && optionalAlert.get().getResolvedAt() != null) { + endTime = optionalAlert.get().getResolvedAt().plusMinutes(1); + }else { + endTime = optionalAlert.get().getTriggeredAt().plusMinutes(1); + } + + /* when the alert is unresolved and the repeat notification is active */ + int timeRange = Tools.getNumber(result.getTriggeredCondition().getParameters().get("time"), 1).intValue(); + if(endTime.isBefore(timeBeginSearch.plusMinutes(timeRange))) { + endTime = timeBeginSearch.plusMinutes(timeRange); + } + + DateTime beginTime = timeBeginSearch; + + String search = "&q=streams%3A" + stream.getId(); + Map conditionParameters = optionalAlert.get().getConditionParameters(); + if(conditionParameters.containsKey("additional_stream")) { + String additionalStreamID = (String) conditionParameters.get("additional_stream"); + + String previousMsgsURL = getPreviousMessagesURL(additionalStreamID, beginTime, endTime); + if(previousMsgsURL!= null && !previousMsgsURL.isEmpty()) { + DateTime timeFromMsgsUrl = getTimeFrom(previousMsgsURL, timeFormatter); + if(timeFromMsgsUrl != null && timeFromMsgsUrl.isBefore(beginTime)){ + beginTime = timeFromMsgsUrl; + } + search = "&q=(+streams%3A" + stream.getId() + getQuery(previousMsgsURL) + "+)"; + } + } + + StringBuilder searchFields = new StringBuilder(); + for (String field : configs.getList(FIELD_SPLIT, Collections.emptyList())) { + String valueAggregationField = (String) messageSummary.getField(field); + if(valueAggregationField != null && !valueAggregationField.isEmpty()) { + searchFields.append("+AND+" + field + "%3A\"" + valueAggregationField + "\""); + } + } + + return MSGS_URL_BEGIN + + beginTime.toString(timeFormatter) + MSGS_URL_TO + + endTime.toString(timeFormatter) + + search + + searchFields.toString(); + } + + return ""; + } + + private String getHashFromString(String value) { + int hash = value.hashCode(); + if(hash < 0) { + return "a"+Math.abs(hash); + } + return String.valueOf(hash); + } + + private Map getListOfloggingAlertField(Stream stream, CheckResult result, DateTime date) { + String graylogId = getGraylogID(stream, result); + String alertUrl = getAlertUrl(stream, result); + Map listOfloggingAlertField = Maps.newHashMap(); + + for (MessageSummary messageSummary : result.getMatchingMessages()) { + String valuesAggregationField = getValuesAggregationField(messageSummary); + String messagesUrl = getMessagesUrl(stream, result, messageSummary, date); + + if(messageSummary.hasField(fieldAlertID)) { + listOfloggingAlertField.put(valuesAggregationField, new LoggingAlertFields((String) messageSummary.getField(fieldAlertID), + graylogId, configs.getString(FIELD_SEVERITY), date, alertUrl, messagesUrl)); + }else { + if(!listOfloggingAlertField.containsKey(valuesAggregationField)) { + /* Add hash code if split field */ + String alertID = null; + if(valuesAggregationField.equals("")) { + alertID = getAlertID(stream, result, ""); + }else { + alertID = getAlertID(stream, result, "-"+getHashFromString(valuesAggregationField)); + } + listOfloggingAlertField.put(valuesAggregationField, + new LoggingAlertFields(alertID, graylogId, configs.getString(FIELD_SEVERITY), date, alertUrl, messagesUrl)); + } + } + } + + return listOfloggingAlertField; + } + + @Override + public void call(Stream stream, CheckResult result) + throws AlarmCallbackException { + + /* Get the time of the first message */ + List listMsgSummary = result.getMatchingMessages(); + DateTime date = result.getTriggeredAt(); + for (MessageSummary messageSummary : listMsgSummary) { + if(messageSummary.getTimestamp().isBefore(date)) + date = messageSummary.getTimestamp(); + } + + /* Get the list of messages to log */ + Set listMessagesToLog= new LinkedHashSet<>(); + if(listMsgSummary.isEmpty()) { + LoggingAlertFields loggingAlertFields= new LoggingAlertFields( getAlertID(stream, result, ""), + getGraylogID(stream, result), configs.getString(FIELD_SEVERITY), date, getAlertUrl(stream, result), ""); + String messageToLog=buildBody(stream, result, new Message("Empty message", "LoggingAlert", date), loggingAlertFields); + listMessagesToLog.add(messageToLog); + }else { + Map listOfloggingAlertField = getListOfloggingAlertField(stream, result, date); + for (MessageSummary messageSummary : listMsgSummary) { + String valuesAggregationField = getValuesAggregationField(messageSummary); + String messageToLog=buildBody(stream, result, messageSummary.getRawMessage(), + listOfloggingAlertField.get(valuesAggregationField)); + listMessagesToLog.add(messageToLog); + } + } + + /* Log each messages */ + int iter = 0; + for (String message : listMessagesToLog) { + if(limitOverflow <= 0 || iter < limitOverflow) { + LOGGER.info(message); + } else { + LOGGER_OVERFLOW.info(message); + } + iter++; + } + } + + @Override + public void checkConfiguration() throws ConfigurationException { + + } + + @Override + public Map getAttributes() { + return configs.getSource(); + } + + @Override + public String getName() { + return "Logging Alert Notification"; + } + + @Override + public ConfigurationRequest getRequestedConfiguration() { + final ConfigurationRequest configurationRequest = new ConfigurationRequest(); + final LoggingAlertConfig configGeneral = clusterConfigService.getOrDefault(LoggingAlertConfig.class, + LoggingAlertConfig.createDefault()); + final String[] writeIndexWildcards = indexSetRegistry.getIndexWildcards(); + final Set listFields = indices.getAllMessageFields(writeIndexWildcards); + Map mapFields = listFields.stream().collect(Collectors.toMap(x -> x, x -> x)); + + LinkedHashMap severity = new LinkedHashMap<>(); + severity.put(SeverityType.HIGH.getType(), "high"); + severity.put(SeverityType.MEDIUM.getType(), "medium"); + severity.put(SeverityType.LOW.getType(), "low"); + severity.put(SeverityType.INFO.getType(), "info"); + + configurationRequest.addField(new DropdownField(FIELD_SEVERITY, + "Alert Severity", + severity.get(configGeneral.accessSeverity().getType()), + severity, + "The severity of logged alerts", + ConfigurationField.Optional.NOT_OPTIONAL)); + + configurationRequest.addField(new TextField(FIELD_BODY, + "Log Content", + configGeneral.accessLogBody(), + "The template to generate the log content from", + ConfigurationField.Optional.NOT_OPTIONAL, + TextField.Attribute.TEXTAREA)); + + configurationRequest.addField(new ListField(FIELD_SPLIT, + "Split Fields", + Collections.emptyList(), + mapFields.entrySet().stream().sorted(Map.Entry.comparingByValue()). + collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,(e1, e2) -> e1, LinkedHashMap::new)), + "Fields that should be checked to split the alert according to each value by generating a different alert id for each value", + ConfigurationField.Optional.OPTIONAL, + ListField.Attribute.ALLOW_CREATE)); + + configurationRequest.addField(new NumberField(FIELD_AGGREGATION_TIME, + "Aggregation Time Range", + configGeneral.accessAggregationTime(), + "Aggregate alerts received in the given number of minutes by logging alerts with the same alert id", + ConfigurationField.Optional.OPTIONAL, + NumberField.Attribute.ONLY_POSITIVE)); + + configurationRequest.addField(new TextField(FIELD_COMMENT, + "Comment", + "", + "Comment about the configuration", + ConfigurationField.Optional.OPTIONAL)); + + return configurationRequest; + } + + @Override + public void initialize(Configuration arg0) + throws AlarmCallbackConfigurationException { + configs = new Configuration(arg0.getSource()); + } + +} diff --git a/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertFields.java b/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertFields.java new file mode 100755 index 0000000..76e9903 --- /dev/null +++ b/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertFields.java @@ -0,0 +1,47 @@ +package com.airbus_cyber_security.graylog; + +import org.joda.time.DateTime; + +public class LoggingAlertFields { + + private final String id; + private final String graylog_id; + private final String severity; + private final DateTime detect_time; + private final String alert_url; + private final String messages_url; + + public LoggingAlertFields(String id, String graylog_id, String severity, DateTime detectTime, String alert_url, String messages_url) { + this.id = id; + this.graylog_id = graylog_id; + this.severity = severity; + this.detect_time = detectTime; + this.alert_url = alert_url; + this.messages_url = messages_url; + } + + public String getId() { + return id; + } + + public String getSeverity() { + return severity; + } + + public DateTime getDetect_time() { + return detect_time; + } + + public String getGraylog_id() { + return graylog_id; + } + + public String getAlert_url() { + return alert_url; + } + + public String getMessages_url() { + return messages_url; + } + +} diff --git a/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertMetaData.java b/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertMetaData.java new file mode 100755 index 0000000..6206633 --- /dev/null +++ b/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertMetaData.java @@ -0,0 +1,56 @@ +package com.airbus_cyber_security.graylog; + +import org.graylog2.plugin.PluginMetaData; +import org.graylog2.plugin.ServerStatus; +import org.graylog2.plugin.Version; + +import java.net.URI; +import java.util.Collections; +import java.util.Set; + +/** + * Implement the PluginMetaData interface here. + */ +public class LoggingAlertMetaData implements PluginMetaData { + private static final String PLUGIN_PROPERTIES = "com.airbus-cyber-security.graylog.graylog-plugin-logging-alert/graylog-plugin.properties"; + + @Override + public String getUniqueId() { + return "com.airbus-cyber-security.graylog.LoggingAlertPlugin"; + } + + @Override + public String getName() { + return "Logging Alert Notification"; + } + + @Override + public String getAuthor() { + return "Airbus CyberSecurity"; + } + + @Override + public URI getURL() { + return URI.create("https://www.airbus-cyber-security.com"); + } + + @Override + public Version getVersion() { + return Version.fromPluginProperties(getClass(), PLUGIN_PROPERTIES, "version", Version.from(0, 0, 1, "unknown")); + } + + @Override + public String getDescription() { + return "This notification generates a log message when an alert is triggered."; + } + + @Override + public Version getRequiredVersion() { + return Version.fromPluginProperties(getClass(), PLUGIN_PROPERTIES, "graylog.version", Version.from(2, 4, 0)); + } + + @Override + public Set getRequiredCapabilities() { + return Collections.emptySet(); + } +} diff --git a/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertModule.java b/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertModule.java new file mode 100755 index 0000000..640f632 --- /dev/null +++ b/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertModule.java @@ -0,0 +1,28 @@ +package com.airbus_cyber_security.graylog; + +import org.graylog2.plugin.PluginConfigBean; +import org.graylog2.plugin.PluginModule; + + +import java.util.Collections; +import java.util.Set; + +/** + * Extend the PluginModule abstract class here to add you plugin to the system. + */ +public class LoggingAlertModule extends PluginModule { + /** + * Returns all configuration beans required by this plugin. + * + * Implementing this method is optional. The default method returns an empty {@link Set}. + */ + @Override + public Set getConfigBeans() { + return Collections.emptySet(); + } + + @Override + protected void configure() { + addAlarmCallback(LoggingAlert.class); + } +} diff --git a/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertPlugin.java b/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertPlugin.java new file mode 100755 index 0000000..49c3333 --- /dev/null +++ b/src/main/java/com/airbus_cyber_security/graylog/LoggingAlertPlugin.java @@ -0,0 +1,23 @@ +package com.airbus_cyber_security.graylog; + +import org.graylog2.plugin.Plugin; +import org.graylog2.plugin.PluginMetaData; +import org.graylog2.plugin.PluginModule; + +import java.util.Arrays; +import java.util.Collection; + +/** + * Implement the Plugin interface here. + */ +public class LoggingAlertPlugin implements Plugin { + @Override + public PluginMetaData metadata() { + return new LoggingAlertMetaData(); + } + + @Override + public Collection modules () { + return Arrays.asList(new LoggingAlertModule()); + } +} diff --git a/src/main/java/com/airbus_cyber_security/graylog/config/LoggingAlertConfig.java b/src/main/java/com/airbus_cyber_security/graylog/config/LoggingAlertConfig.java new file mode 100755 index 0000000..6f8f29a --- /dev/null +++ b/src/main/java/com/airbus_cyber_security/graylog/config/LoggingAlertConfig.java @@ -0,0 +1,117 @@ +package com.airbus_cyber_security.graylog.config; + +import javax.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.auto.value.AutoValue; + +@JsonAutoDetect +@AutoValue +public abstract class LoggingAlertConfig { + + private static final String FIELD_ALERT_ID = "id"; + private static final String SEPARATOR_TEMPLATE = "\n"; + private static final String BODY_TEMPLATE = + "type: alert" + SEPARATOR_TEMPLATE + + FIELD_ALERT_ID+ ": ${logging_alert.id}" + SEPARATOR_TEMPLATE + + "severity: ${logging_alert.severity}" + SEPARATOR_TEMPLATE + + "app: graylog" + SEPARATOR_TEMPLATE + + "subject: ${alertCondition.title}" + SEPARATOR_TEMPLATE + + "body: ${check_result.resultDescription}" + SEPARATOR_TEMPLATE + + "src: ${message.fields.src_ip}" + SEPARATOR_TEMPLATE + + "src_category: ${message.fields.src_category}" + SEPARATOR_TEMPLATE + + "dest: ${message.fields.dest_ip}" + SEPARATOR_TEMPLATE + + "dest_category: ${message.fields.dest_category}"; + + + @JsonProperty("severity") + public abstract SeverityType accessSeverity(); + + @JsonProperty("separator") + public abstract String accessSeparator(); + + @JsonProperty("log_body") + public abstract String accessLogBody(); + + @JsonProperty("aggregation_stream") + @Nullable + public abstract String accessAggregationStream(); + + @JsonProperty("aggregation_time") + public abstract int accessAggregationTime(); + + @JsonProperty("limit_overflow") + public abstract int accessLimitOverflow(); + + @JsonProperty("field_alert_id") + public abstract String accessFieldAlertId(); + + @JsonProperty("alert_tag") + public abstract String accessAlertTag(); + + @JsonProperty("overflow_tag") + public abstract String accessOverflowTag(); + + @JsonCreator + public static LoggingAlertConfig create( + @JsonProperty("severity") SeverityType severity, + @JsonProperty("separator") String separator, + @JsonProperty("log_body") String logBody, + @JsonProperty("aggregation_stream") String aggregationStream, + @JsonProperty("aggregation_time") int aggregationTime, + @JsonProperty("limit_overflow") int limitOverflow, + @JsonProperty("field_alert_id") String fieldAlertId, + @JsonProperty("alert_tag") String alertTag, + @JsonProperty("overflow_tag") String overflowTag){ + return builder() + .accessSeverity(severity) + .accessSeparator(separator) + .accessLogBody(logBody) + .accessAggregationStream(aggregationStream) + .accessAggregationTime(aggregationTime) + .accessLimitOverflow(limitOverflow) + .accessFieldAlertId(fieldAlertId) + .accessAlertTag(alertTag) + .accessOverflowTag(overflowTag) + .build(); + } + + public static LoggingAlertConfig createDefault() { + return builder() + .accessSeverity(SeverityType.LOW) + .accessSeparator(" | ") + .accessLogBody(BODY_TEMPLATE) + .accessAggregationStream("*") + .accessAggregationTime(0) + .accessLimitOverflow(0) + .accessFieldAlertId(FIELD_ALERT_ID) + .accessAlertTag("LoggingAlert") + .accessOverflowTag("LoggingOverflow") + .build(); + } + + public static Builder builder() { + return new AutoValue_LoggingAlertConfig.Builder(); + } + + + public abstract Builder toBuilder(); + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder accessSeverity(SeverityType accessSeverity); + public abstract Builder accessSeparator(String accessSeparator); + public abstract Builder accessLogBody(String accessLogBody); + public abstract Builder accessAggregationStream(String accessAggregationStream); + public abstract Builder accessAggregationTime(int accessAggregationTime); + public abstract Builder accessLimitOverflow(int accessLimitOverflow); + public abstract Builder accessFieldAlertId(String accessFieldAlertId); + public abstract Builder accessAlertTag(String accessAlertTag); + public abstract Builder accessOverflowTag(String accessOverflowTag); + + public abstract LoggingAlertConfig build(); + } +} diff --git a/src/main/java/com/airbus_cyber_security/graylog/config/SeverityType.java b/src/main/java/com/airbus_cyber_security/graylog/config/SeverityType.java new file mode 100755 index 0000000..31ff234 --- /dev/null +++ b/src/main/java/com/airbus_cyber_security/graylog/config/SeverityType.java @@ -0,0 +1,19 @@ +package com.airbus_cyber_security.graylog.config; + +public enum SeverityType { + INFO("info"), + LOW("low"), + MEDIUM("medium"), + HIGH("high"); + + private final String type; + + + SeverityType(String type){ + this.type = type; + } + + public String getType(){ + return type; + } +} diff --git a/src/main/resources/META-INF/services/org.graylog2.plugin.Plugin b/src/main/resources/META-INF/services/org.graylog2.plugin.Plugin new file mode 100755 index 0000000..ef7901d --- /dev/null +++ b/src/main/resources/META-INF/services/org.graylog2.plugin.Plugin @@ -0,0 +1 @@ +com.airbus_cyber_security.graylog.LoggingAlertPlugin diff --git a/src/main/resources/com.airbus-cyber-security.graylog.graylog-plugin-logging-alert/graylog-plugin.properties b/src/main/resources/com.airbus-cyber-security.graylog.graylog-plugin-logging-alert/graylog-plugin.properties new file mode 100755 index 0000000..fcebb30 --- /dev/null +++ b/src/main/resources/com.airbus-cyber-security.graylog.graylog-plugin-logging-alert/graylog-plugin.properties @@ -0,0 +1,12 @@ +# The plugin version +version=${project.version} + +# The required Graylog server version +graylog.version=${graylog.version} + +# When set to true (the default) the plugin gets a separate class loader +# when loading the plugin. When set to false, the plugin shares a class loader +# with other plugins that have isolated=false. +# +# Do not disable this unless this plugin depends on another plugin! +isolated=false diff --git a/src/test/java/com/airbus_cyber_security/graylog/LoggingAlertTest.java b/src/test/java/com/airbus_cyber_security/graylog/LoggingAlertTest.java new file mode 100755 index 0000000..e1ad95b --- /dev/null +++ b/src/test/java/com/airbus_cyber_security/graylog/LoggingAlertTest.java @@ -0,0 +1,1228 @@ + +package com.airbus_cyber_security.graylog; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.Collections; +import org.graylog2.alerts.AbstractAlertCondition; +import org.graylog2.alerts.Alert; +import org.graylog2.alerts.AlertService; +import org.graylog2.alerts.types.DummyAlertCondition; +import org.graylog2.indexer.IndexSetRegistry; +import org.graylog2.indexer.indices.Indices; +import org.graylog2.indexer.results.ResultMessage; +import org.graylog2.indexer.results.SearchResult; +import org.graylog2.indexer.searches.Searches; +import org.graylog2.indexer.searches.Sorting; +import org.graylog2.plugin.Message; +import org.graylog2.plugin.MessageSummary; +import org.graylog2.plugin.alarms.AlertCondition; +import org.graylog2.plugin.alarms.callbacks.AlarmCallbackConfigurationException; +import org.graylog2.plugin.alarms.callbacks.AlarmCallbackException; +import org.graylog2.plugin.cluster.ClusterConfigService; +import org.graylog2.plugin.configuration.Configuration; +import org.graylog2.plugin.indexer.searches.timeranges.TimeRange; +import org.graylog2.plugin.streams.Stream; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import com.airbus_cyber_security.graylog.LoggingAlert; +import com.airbus_cyber_security.graylog.config.LoggingAlertConfig; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import uk.org.lidalia.slf4jtest.TestLogger; +import uk.org.lidalia.slf4jtest.TestLoggerFactory; + +import static org.mockito.Mockito.mock; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static uk.org.lidalia.slf4jext.Level.INFO; + +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; + +@PrepareForTest({ LoggingAlert.class }) +@RunWith(PowerMockRunner.class) +public class LoggingAlertTest { + + private static final String SEPARATOR_TEMPLATE = "\n"; + private static final String BODY_TEMPLATE = + "alert_id: ${logging_alert.id}" + SEPARATOR_TEMPLATE + + "alert_title: ${alertCondition.title}" + SEPARATOR_TEMPLATE + + "alert_description: ${check_result.resultDescription}" + SEPARATOR_TEMPLATE + + "severity: ${logging_alert.severity}" + SEPARATOR_TEMPLATE + + "create_time: ${check_result.triggeredAt}" + SEPARATOR_TEMPLATE + + "detect_time: ${logging_alert.detect_time}" + SEPARATOR_TEMPLATE + + "analyzer: Graylog" + SEPARATOR_TEMPLATE + + "analyzer_time: ${message.timestamp}" + SEPARATOR_TEMPLATE + + "sensor: ${message.fields.sensor}" + SEPARATOR_TEMPLATE + + "classification: ${message.fields.classification}" + SEPARATOR_TEMPLATE + + "source_command: ${message.fields.cmd_src}" + SEPARATOR_TEMPLATE + + "source_file_name: ${message.fields.file_src}" + SEPARATOR_TEMPLATE + + "source_host_name: ${message.fields.host_src}" + SEPARATOR_TEMPLATE + + "source_ip_address: ${message.fields.ip_src}" + SEPARATOR_TEMPLATE + + "source_mac_address: ${message.fields.mac_src}" + SEPARATOR_TEMPLATE + + "source_port: ${message.fields.port_src}" + SEPARATOR_TEMPLATE + + "source_process: ${message.fields.process_src}" + SEPARATOR_TEMPLATE + + "source_service_name: ${message.fields.service_src}" + SEPARATOR_TEMPLATE + + "source_tool: ${message.fields.tool_src}" + SEPARATOR_TEMPLATE + + "source_url: ${message.fields.url_src}" + SEPARATOR_TEMPLATE + + "source_user_name: ${message.fields.user_src}" + SEPARATOR_TEMPLATE + + "source_user_privileges: ${message.fields.user_role_src}" + SEPARATOR_TEMPLATE + + "source_user_unique_identifier: ${message.fields.uid_src}" + SEPARATOR_TEMPLATE + + "target_command: ${message.fields.cmd_dst}" + SEPARATOR_TEMPLATE + + "target_file_name: ${message.fields.file_dst}" + SEPARATOR_TEMPLATE + + "target_host_name: ${message.fields.host_dst}" + SEPARATOR_TEMPLATE + + "target_ip_address: ${message.fields.ip_dst}" + SEPARATOR_TEMPLATE + + "target_mac_address: ${message.fields.mac_dst}" + SEPARATOR_TEMPLATE + + "target_port: ${message.fields.port_dst}" + SEPARATOR_TEMPLATE + + "target_process: ${message.fields.process_dst}" + SEPARATOR_TEMPLATE + + "target_service_name: ${message.fields.service_dst}" + SEPARATOR_TEMPLATE + + "target_tool: ${message.fields.tool_dst}" + SEPARATOR_TEMPLATE + + "target_url: ${message.fields.url_dst}" + SEPARATOR_TEMPLATE + + "target_user_name: ${message.fields.user_dst}" + SEPARATOR_TEMPLATE + + "target_user_privileges: ${message.fields.user_role_dst}" + SEPARATOR_TEMPLATE + + "target_user_unique_identifier: ${message.fields.uid_dst}" + SEPARATOR_TEMPLATE + + "file_name: ${message.fields.filename}" + SEPARATOR_TEMPLATE + + "file_hash: ${message.fields.filehash}" + SEPARATOR_TEMPLATE + + "file_size: ${message.fields.filesize}" + SEPARATOR_TEMPLATE + + "file_type: ${message.fields.filetype}" + SEPARATOR_TEMPLATE + + "alert_url: http://localhost:8080${logging_alert.alert_url}" + SEPARATOR_TEMPLATE + + "messages_url: http://localhost:8080${logging_alert.messages_url}"; + + private static final String BODY_TEMPLATE_MSG_URL = + "alert_id: ${logging_alert.id}" + SEPARATOR_TEMPLATE + + "alert_title: ${alertCondition.title}" + SEPARATOR_TEMPLATE + + "create_time: ${check_result.triggeredAt}" + SEPARATOR_TEMPLATE + + "detect_time: ${logging_alert.detect_time}" + SEPARATOR_TEMPLATE + + "alert_url: http://localhost:8080${logging_alert.alert_url}" + SEPARATOR_TEMPLATE + + "messages_url: http://localhost:8080${logging_alert.messages_url}"; + + private static final TestLogger TEST_LOGGER = TestLoggerFactory.getTestLogger("LoggingAlert"); + private static final TestLogger TEST_LOGGER_OVERFLOW = TestLoggerFactory.getTestLogger("LoggingOverflow"); + + private static final String CONDITION_ID = "condition-id"; + private static final String CONDITION_TITLE = "Alert Condition Title"; + private static final String USER = "user"; + + private AlertService alertService; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + private LoggingAlert alarmCallback; + private Searches searches; + private LoggingAlertConfig configGeneral; + + @Before + public void setUp() throws Exception { + alertService = mock(AlertService.class); + searches = mock(Searches.class); + configGeneral = mock(LoggingAlertConfig.class); + when(configGeneral.accessAggregationStream()).thenReturn("*"); + when(configGeneral.accessFieldAlertId()).thenReturn("alert_id"); + when(configGeneral.accessSeparator()).thenReturn(" | "); + when(configGeneral.accessAlertTag()).thenReturn("LoggingAlert"); + when(configGeneral.accessOverflowTag()).thenReturn("LoggingOverflow"); + } + + private Map getConfigMap(String severity, String body, + List aggregationField, int aggregationTime, int limitOverflow) { + Map parameters = Maps.newHashMap(); + parameters.put("severity", severity); + parameters.put("content", body); + parameters.put("split_fields", aggregationField); + parameters.put("aggregation_time", aggregationTime); + parameters.put("limit_overflow", limitOverflow); + return parameters; + } + + private void initializeConfiguration(Map configMap) throws AlarmCallbackConfigurationException { + final Configuration configuration = new Configuration(configMap); + + final ClusterConfigService clusterConfigService= mock(ClusterConfigService.class); + final Indices indices = mock(Indices.class); + final IndexSetRegistry indexSetRegistry = mock(IndexSetRegistry.class); + when(clusterConfigService.getOrDefault(LoggingAlertConfig.class, LoggingAlertConfig.createDefault())).thenReturn(configGeneral); + alarmCallback = new LoggingAlert(clusterConfigService, indices, indexSetRegistry, alertService, searches); + + alarmCallback.initialize(configuration); + } + + private void initializeSimpleConfiguration() throws AlarmCallbackConfigurationException { + initializeConfiguration(getConfigMap("info", BODY_TEMPLATE, Collections.emptyList(), 0, 0)); + } + + @Test + public void checkConfigurationSucceedsWithValidConfiguration() throws Exception { + initializeSimpleConfiguration(); + alarmCallback.checkConfiguration(); + } + + @Test + public void callWithNoAdditionalField() throws AlarmCallbackException, AlarmCallbackConfigurationException { + initializeSimpleConfiguration(); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", new Message("Test message 1", "source1", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC))), + new MessageSummary("graylog_2", new Message("Test message 2", "source2", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC))) + ); + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID uuid = UUID.randomUUID(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(uuid); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").contains( + tuple(INFO, "alert_id: "+uuid.toString()+" | alert_title: Alert Condition Title | alert_description: Result Description | " + + "severity: info | create_time: 2017-09-06T17:00:00.000Z | detect_time: 2017-09-06T17:00:00.000Z | " + + "analyzer: Graylog | analyzer_time: 2017-09-06T17:00:00.000Z | sensor: | classification: | " + + "source_command: | source_file_name: | source_host_name: | source_ip_address: | source_mac_address: | " + + "source_port: | source_process: | source_service_name: | source_tool: | source_url: | source_user_name: | " + + "source_user_privileges: | source_user_unique_identifier: | target_command: | target_file_name: | " + + "target_host_name: | target_ip_address: | target_mac_address: | target_port: | target_process: | " + + "target_service_name: | target_tool: | target_url: | target_user_name: | target_user_privileges: | " + + "target_user_unique_identifier: | file_name: | file_hash: | file_size: | file_type: | " + + "alert_url: http://localhost:8080 | messages_url: http://localhost:8080")); + } + + @Test + public void callWithAdditionalField() throws AlarmCallbackException, AlarmCallbackConfigurationException { + initializeSimpleConfiguration(); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message = new Message("Test message 1", "source1", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message.addField("sensor", "sensor"); + message.addField("classification", "classification"); + message.addField("cmd_src", "cmd_src"); + message.addField("file_src", "file_src"); + message.addField("host_src", "host_src"); + message.addField("ip_src", "ip_src"); + message.addField("mac_src", "mac_src"); + message.addField("port_src", "port_src"); + message.addField("process_src", "process_src"); + message.addField("service_src", "service_src"); + message.addField("tool_src", "tool_src"); + message.addField("url_src", "url_src"); + message.addField("user_src", "user_src"); + message.addField("user_role_src", "user_role_src"); + message.addField("uid_src", "uid_src"); + message.addField("cmd_dst", "cmd_dst"); + message.addField("file_dst", "file_dst"); + message.addField("host_dst", "host_dst"); + message.addField("ip_dst", "ip_dst"); + message.addField("mac_dst", "mac_dst"); + message.addField("port_dst", "port_dst"); + message.addField("process_dst", "process_dst"); + message.addField("service_dst", "service_dst"); + message.addField("tool_dst", "tool_dst"); + message.addField("url_dst", "url_dst"); + message.addField("user_dst", "user_dst"); + message.addField("user_role_dst", "user_role_dst"); + message.addField("uid_dst ", "uid_dst"); + message.addField("filename", "filename"); + message.addField("filehash", "filehash"); + message.addField("filesize", "filesize"); + message.addField("filetype", "filetype"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message)); + + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID uuid = UUID.randomUUID(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(uuid); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").contains( + tuple(INFO, "alert_id: "+uuid.toString()+" | alert_title: Alert Condition Title | alert_description: Result Description | " + + "severity: info | create_time: 2017-09-06T17:00:00.000Z | detect_time: 2017-09-06T17:00:00.000Z | " + + "analyzer: Graylog | analyzer_time: 2017-09-06T17:00:00.000Z | sensor: sensor | classification: classification | " + + "source_command: cmd_src | source_file_name: file_src | source_host_name: host_src | source_ip_address: ip_src | " + + "source_mac_address: mac_src | source_port: port_src | source_process: process_src | source_service_name: service_src | " + + "source_tool: tool_src | source_url: url_src | source_user_name: user_src | source_user_privileges: user_role_src | " + + "source_user_unique_identifier: uid_src | target_command: cmd_dst | target_file_name: file_dst | " + + "target_host_name: host_dst | target_ip_address: ip_dst | target_mac_address: mac_dst | target_port: port_dst | " + + "target_process: process_dst | target_service_name: service_dst | target_tool: tool_dst | target_url: url_dst | " + + "target_user_name: user_dst | target_user_privileges: user_role_dst | target_user_unique_identifier: uid_dst | " + + "file_name: filename | file_hash: filehash | file_size: filesize | file_type: filetype | " + + "alert_url: http://localhost:8080 | messages_url: http://localhost:8080")); + + } + + @Test + public void testAggregationWith1Field() throws AlarmCallbackException, AlarmCallbackConfigurationException { + List listAggegationFields = Collections.singletonList(USER); + initializeConfiguration(getConfigMap("info", "alert_id: ${logging_alert.id}" + SEPARATOR_TEMPLATE + + "user: ${message.fields.user}"+SEPARATOR_TEMPLATE+"ip_src: ${message.fields.ip_src}", listAggegationFields, 0, 0)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message1.addField(USER, "admin"); + message1.addField("ip_src", "127.0.0.1"); + Message message2 = new Message("Test message 2", "source2", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message2.addField(USER, "root"); + message2.addField("ip_src", "127.0.0.1"); + Message message3 = new Message("Test message 3", "source3", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message3.addField(USER, "admin"); + message3.addField("ip_src", "127.0.0.2"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1), + new MessageSummary("graylog_2", message2), + new MessageSummary("graylog_3", message3) + + ); + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID randomUuid = UUID.randomUUID(); + String valuesAggregationField="admin"; + String alertID1 = randomUuid + "-" + valuesAggregationField.hashCode(); + String valuesAggregationField2="root"; + String alertID2 = randomUuid + "-" + valuesAggregationField2.hashCode(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(randomUuid); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple(INFO, "alert_id: "+alertID1+" | user: admin | ip_src: 127.0.0.1"), + tuple(INFO, "alert_id: "+alertID2+" | user: root | ip_src: 127.0.0.1"), + tuple(INFO, "alert_id: "+alertID1+" | user: admin | ip_src: 127.0.0.2")); + + } + + @Test + public void testDeduped() throws AlarmCallbackException, AlarmCallbackConfigurationException { + List listAggegationFields = Collections.singletonList(USER); + initializeConfiguration(getConfigMap("info", "alert_id: ${logging_alert.id}" + SEPARATOR_TEMPLATE + + "user: ${message.fields.user}", listAggegationFields, 0, 0)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message1.addField(USER, "admin"); + Message message2 = new Message("Test message 2", "source2", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message2.addField(USER, "root"); + Message message3 = new Message("Test message 3", "source3", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message3.addField(USER, "admin"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1), + new MessageSummary("graylog_2", message2), + new MessageSummary("graylog_3", message3) + + ); + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID randomUuid = UUID.randomUUID(); + String valuesAggregationField="admin"; + String alertID1 = randomUuid +"-" + valuesAggregationField.hashCode(); + String valuesAggregationField2="root"; + String alertID2 = randomUuid +"-" + valuesAggregationField2.hashCode(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(randomUuid); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple(INFO, "alert_id: "+alertID1+" | user: admin"), + tuple(INFO, "alert_id: "+alertID2+" | user: root")); + + } + + @Test + public void testAggregationWithMultipleField() throws AlarmCallbackException, AlarmCallbackConfigurationException { + List listAggegationFields = Arrays.asList(USER,"ip_src"); + initializeConfiguration(getConfigMap("info", "alert_id: ${logging_alert.id}" + SEPARATOR_TEMPLATE + + "user: ${message.fields.user}" +SEPARATOR_TEMPLATE+"ip_src: ${message.fields.ip_src}", listAggegationFields, 0, 0)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message1.addField(USER, "admin"); + message1.addField("ip_src", "127.0.0.1"); + Message message2 = new Message("Test message 2", "source2", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message2.addField(USER, "root"); + message2.addField("ip_src", "127.0.0.1"); + Message message3 = new Message("Test message 3", "source3", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message3.addField(USER, "admin"); + message3.addField("ip_src", "127.0.0.2"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1), + new MessageSummary("graylog_2", message2), + new MessageSummary("graylog_3", message3) + + ); + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID randomUuid = UUID.randomUUID(); + String valuesAggregationField="admin127.0.0.1"; + String alertID1 = randomUuid +"-" + valuesAggregationField.hashCode(); + String valuesAggregationField2="root127.0.0.1"; + String alertID2 = randomUuid +"-" + valuesAggregationField2.hashCode(); + String valuesAggregationField3="admin127.0.0.2"; + String alertID3 = randomUuid +"-" + valuesAggregationField3.hashCode(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(randomUuid); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple(INFO, "alert_id: "+alertID1+" | user: admin | ip_src: 127.0.0.1"), + tuple(INFO, "alert_id: "+alertID2+" | user: root | ip_src: 127.0.0.1"), + tuple(INFO, "alert_id: "+alertID3+" | user: admin | ip_src: 127.0.0.2")); + + } + + @Test + public void testWithAggregationTime() throws AlarmCallbackException, AlarmCallbackConfigurationException { + List listAggegationFields = Collections.singletonList(USER); + initializeConfiguration(getConfigMap("info", "alert_id: ${logging_alert.id}" + SEPARATOR_TEMPLATE + + "user: ${message.fields.user}"+SEPARATOR_TEMPLATE+"ip_src: ${message.fields.ip_src}", listAggegationFields, 15, 0)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + "", + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message1.addField(USER, "admin"); + message1.addField("ip_src", "127.0.0.1"); + Message message2 = new Message("Test message 2", "source2", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message2.addField(USER, "root"); + message2.addField("ip_src", "127.0.0.1"); + Message message3 = new Message("Test message 3", "source3", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message3.addField(USER, "admin"); + message3.addField("ip_src", "127.0.0.2"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1), + new MessageSummary("graylog_2", message2), + new MessageSummary("graylog_3", message3) + + ); + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + messageSummaries + ); + + when(stream.getId()).thenReturn("001"); + List listAlert = new ArrayList<>(); + Alert alert = mock(Alert.class); + when(alert.getId()).thenReturn("002"); + listAlert.add(alert); + when(alertService.loadRecentOfStream(stream.getId(), new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC).minusMinutes(15), 300)).thenReturn(listAlert); + + SearchResult backlogResult = mock(SearchResult.class); + Message message = new Message("alert_id: alertId","test",new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message.addField("alert_id", "alertID"); + ResultMessage resultMessage = mock(ResultMessage.class); + List listResultMessage = new ArrayList<>(); + listResultMessage.add(resultMessage); + when(resultMessage.getMessage()).thenReturn(message); + when(backlogResult.getResults()).thenReturn(listResultMessage); + + when(searches.search(anyString(), anyString(), any(TimeRange.class), eq(10), eq(0), any(Sorting.class))).thenReturn(backlogResult); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple(INFO, "alert_id: "+"alertID"+" | user: admin | ip_src: 127.0.0.1"), + tuple(INFO, "alert_id: "+"alertID"+" | user: root | ip_src: 127.0.0.1"), + tuple(INFO, "alert_id: "+"alertID"+" | user: admin | ip_src: 127.0.0.2")); + + } + + @Test + public void testWithFieldAlerId() throws AlarmCallbackException, AlarmCallbackConfigurationException { + List listAggegationFields = Collections.singletonList("user"); + initializeConfiguration(getConfigMap("info", "alert_id: ${logging_alert.id}" + SEPARATOR_TEMPLATE + + "user: ${message.fields.user}"+SEPARATOR_TEMPLATE+"ip_src: ${message.fields.ip_src}",listAggegationFields, 0, 0)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + "condition-id", + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + "user", + ImmutableMap.of(), + "Alert Condition Title" + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message1.addField("user", "admin"); + message1.addField("ip_src", "127.0.0.1"); + Message message2 = new Message("Test message 2", "source2", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message2.addField("user", "root"); + message2.addField("ip_src", "127.0.0.1"); + Message message3 = new Message("Test message 3", "source3", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message3.addField("user", "admin"); + message3.addField("ip_src", "127.0.0.2"); + message3.addField("alert_id", "1"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1), + new MessageSummary("graylog_2", message2), + new MessageSummary("graylog_3", message3) + + ); + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID randomUuid = UUID.randomUUID(); + String valuesAggregationField2="root"; + String alertID2 = randomUuid +"-" + valuesAggregationField2.hashCode(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(randomUuid); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple(INFO, "alert_id: "+1+" | user: admin | ip_src: 127.0.0.1"), + tuple(INFO, "alert_id: "+alertID2+" | user: root | ip_src: 127.0.0.1"), + tuple(INFO, "alert_id: "+1+" | user: admin | ip_src: 127.0.0.2")); + } + + @Test + public void callWithUrlAlertisInterval() throws AlarmCallbackException, AlarmCallbackConfigurationException { + initializeSimpleConfiguration(); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 00, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", new Message("Test message 1", "source1", new DateTime(2018, 4, 19, 00, 0, DateTimeZone.UTC))), + new MessageSummary("graylog_2", new Message("Test message 2", "source2", new DateTime(2018, 4, 19, 00, 0, DateTimeZone.UTC))) + ); + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2018, 4, 19, 00, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID uuid = UUID.randomUUID(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(uuid); + + Alert alert = mock(Alert.class); + when(alert.getId()).thenReturn("002"); + when(alert.isInterval()).thenReturn(true); + when(alert.getTriggeredAt()).thenReturn(new DateTime(2018, 04, 19, 00, 01, 27, DateTimeZone.UTC)); + when(alert.getResolvedAt()).thenReturn(new DateTime(2018, 04, 19, 00, 02, 27, DateTimeZone.UTC)); + Optional optAlert = Optional.of(alert); + + when(stream.getId()).thenReturn("001"); + when(alertService.getLastTriggeredAlert(anyString(), anyString())).thenReturn(optAlert); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").contains( + tuple(INFO, "alert_id: 002 | alert_title: Alert Condition Title | alert_description: Result Description | " + + "severity: info | create_time: 2018-04-19T00:00:00.000Z | detect_time: 2018-04-19T00:00:00.000Z | " + + "analyzer: Graylog | analyzer_time: 2018-04-19T00:00:00.000Z | sensor: | classification: | " + + "source_command: | source_file_name: | source_host_name: | source_ip_address: | source_mac_address: | " + + "source_port: | source_process: | source_service_name: | source_tool: | source_url: | source_user_name: | " + + "source_user_privileges: | source_user_unique_identifier: | target_command: | target_file_name: | " + + "target_host_name: | target_ip_address: | target_mac_address: | target_port: | target_process: | " + + "target_service_name: | target_tool: | target_url: | target_user_name: | target_user_privileges: | " + + "target_user_unique_identifier: | file_name: | file_hash: | file_size: | file_type: | " + + "alert_url: http://localhost:8080/alerts/002 | messages_url: http://localhost:8080" + + "/search?rangetype=absolute&from=" + + "2018-04-19T00%3A00%3A00.000Z&to=2018-04-19T00%3A03%3A27.000Z&q=streams%3A001")); + } + + @Test + public void callWithUrlAlertisNotInterval() throws AlarmCallbackException, AlarmCallbackConfigurationException { + initializeSimpleConfiguration(); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", new Message("Test message 1", "source1", new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC))), + new MessageSummary("graylog_2", new Message("Test message 2", "source2", new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC))) + ); + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID uuid = UUID.randomUUID(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(uuid); + + Alert alert = mock(Alert.class); + when(alert.getId()).thenReturn("002"); + when(alert.isInterval()).thenReturn(false); + when(alert.getTriggeredAt()).thenReturn(new DateTime(2018, 04, 19, 14, 01, 27, DateTimeZone.UTC)); + when(alert.getResolvedAt()).thenReturn(new DateTime(2018, 04, 19, 14, 02, 27, DateTimeZone.UTC)); + Optional optAlert = Optional.of(alert); + + when(stream.getId()).thenReturn("001"); + when(alertService.getLastTriggeredAlert(anyString(), anyString())).thenReturn(optAlert); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").contains( + tuple(INFO, "alert_id: 002 | alert_title: Alert Condition Title | alert_description: Result Description | " + + "severity: info | create_time: 2018-04-19T14:00:00.000Z | detect_time: 2018-04-19T14:00:00.000Z | " + + "analyzer: Graylog | analyzer_time: 2018-04-19T14:00:00.000Z | sensor: | classification: | " + + "source_command: | source_file_name: | source_host_name: | source_ip_address: | source_mac_address: | " + + "source_port: | source_process: | source_service_name: | source_tool: | source_url: | source_user_name: | " + + "source_user_privileges: | source_user_unique_identifier: | target_command: | target_file_name: | " + + "target_host_name: | target_ip_address: | target_mac_address: | target_port: | target_process: | " + + "target_service_name: | target_tool: | target_url: | target_user_name: | target_user_privileges: | " + + "target_user_unique_identifier: | file_name: | file_hash: | file_size: | file_type: | " + + "alert_url: http://localhost:8080/alerts/002 | messages_url: http://localhost:8080" + + "/search?rangetype=absolute&from=" + + "2018-04-19T14%3A00%3A00.000Z&to=2018-04-19T14%3A02%3A27.000Z&q=streams%3A001")); + } + + @Test + public void testLimitOverflow() throws AlarmCallbackException, AlarmCallbackConfigurationException { + when(configGeneral.accessLimitOverflow()).thenReturn(2); + + List listAggegationFields = Collections.singletonList(USER); + initializeConfiguration(getConfigMap("info", "alert_id: ${logging_alert.id}" + SEPARATOR_TEMPLATE + + "user: ${message.fields.user}"+SEPARATOR_TEMPLATE+"ip_src: ${message.fields.ip_src}", listAggegationFields, 0, 2)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message1.addField(USER, "admin"); + message1.addField("ip_src", "127.0.0.1"); + Message message2 = new Message("Test message 2", "source2", new DateTime(2017, 9, 6, 17, 1, DateTimeZone.UTC)); + message2.addField(USER, "admin"); + message2.addField("ip_src", "127.0.0.2"); + Message message3 = new Message("Test message 3", "source3", new DateTime(2017, 9, 6, 17, 2, DateTimeZone.UTC)); + message3.addField(USER, "admin"); + message3.addField("ip_src", "127.0.0.3"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1), + new MessageSummary("graylog_2", message2), + new MessageSummary("graylog_3", message3) + ); + + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID randomUuid = UUID.randomUUID(); + String valuesAggregationField="admin"; + String alertID1 = randomUuid + "-" + valuesAggregationField.hashCode(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(randomUuid); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple(INFO, "alert_id: "+alertID1+" | user: admin | ip_src: 127.0.0.1"), + tuple(INFO, "alert_id: "+alertID1+" | user: admin | ip_src: 127.0.0.2")); + + assertThat(TEST_LOGGER_OVERFLOW.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple(INFO, "alert_id: "+alertID1+" | user: admin | ip_src: 127.0.0.3")); + + + } + + @Test + public void callWithSplitFieldTestMessageUrl() throws AlarmCallbackException, AlarmCallbackConfigurationException { + List listAggegationFields = Collections.singletonList(USER); + initializeConfiguration(getConfigMap("info", BODY_TEMPLATE, listAggegationFields, 0, 0)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC)); + message1.addField(USER, "admin"); + message1.addField("ip_src", "127.0.0.1"); + Message message2 = new Message("Test message 2", "source2", new DateTime(2018, 4, 19, 14, 1, DateTimeZone.UTC)); + message2.addField(USER, "admin"); + message2.addField("ip_src", "127.0.0.2"); + Message message3 = new Message("Test message 3", "source3", new DateTime(2018, 4, 19, 14, 2, DateTimeZone.UTC)); + message3.addField(USER, "user1"); + message3.addField("ip_src", "127.0.0.3"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1), + new MessageSummary("graylog_2", message2), + new MessageSummary("graylog_3", message3) + ); + + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID uuid = UUID.randomUUID(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(uuid); + + Alert alert = mock(Alert.class); + when(alert.getId()).thenReturn("002"); + when(alert.isInterval()).thenReturn(true); + when(alert.getTriggeredAt()).thenReturn(new DateTime(2018, 04, 19, 14, 01, 27, DateTimeZone.UTC)); + when(alert.getResolvedAt()).thenReturn(new DateTime(2018, 04, 19, 14, 02, 27, DateTimeZone.UTC)); + Optional optAlert = Optional.of(alert); + + when(stream.getId()).thenReturn("001"); + when(alertService.getLastTriggeredAlert(anyString(), anyString())).thenReturn(optAlert); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple(INFO, "alert_id: 002-92668751 | alert_title: Alert Condition Title | alert_description: Result Description " + + "| severity: info | create_time: 2018-04-19T14:00:00.000Z | detect_time: 2018-04-19T14:00:00.000Z " + + "| analyzer: Graylog | analyzer_time: 2018-04-19T14:00:00.000Z | sensor: | classification: " + + "| source_command: | source_file_name: | source_host_name: | source_ip_address: 127.0.0.1 " + + "| source_mac_address: | source_port: | source_process: | source_service_name: | source_tool: " + + "| source_url: | source_user_name: | source_user_privileges: | source_user_unique_identifier: " + + "| target_command: | target_file_name: | target_host_name: | target_ip_address: " + + "| target_mac_address: | target_port: | target_process: | target_service_name: | target_tool: " + + "| target_url: | target_user_name: | target_user_privileges: | target_user_unique_identifier: " + + "| file_name: | file_hash: | file_size: | file_type: | alert_url: http://localhost:8080/alerts/002 " + + "| messages_url: http://localhost:8080/search?rangetype=absolute" + + "&from=2018-04-19T14%3A00%3A00.000Z&to=2018-04-19T14%3A03%3A27.000Z&q=streams%3A001+AND+user%3A\"admin\""), + tuple(INFO, "alert_id: 002-92668751 | alert_title: Alert Condition Title | alert_description: Result Description " + + "| severity: info | create_time: 2018-04-19T14:00:00.000Z | detect_time: 2018-04-19T14:00:00.000Z " + + "| analyzer: Graylog | analyzer_time: 2018-04-19T14:01:00.000Z | sensor: | classification: " + + "| source_command: | source_file_name: | source_host_name: | source_ip_address: 127.0.0.2 " + + "| source_mac_address: | source_port: | source_process: | source_service_name: | source_tool: " + + "| source_url: | source_user_name: | source_user_privileges: | source_user_unique_identifier: " + + "| target_command: | target_file_name: | target_host_name: | target_ip_address: " + + "| target_mac_address: | target_port: | target_process: | target_service_name: | target_tool: " + + "| target_url: | target_user_name: | target_user_privileges: | target_user_unique_identifier: " + + "| file_name: | file_hash: | file_size: | file_type: | alert_url: http://localhost:8080/alerts/002 " + + "| messages_url: http://localhost:8080/search?rangetype=absolute" + + "&from=2018-04-19T14%3A00%3A00.000Z&to=2018-04-19T14%3A03%3A27.000Z&q=streams%3A001+AND+user%3A\"admin\""), + tuple(INFO, "alert_id: 002-111578566 | alert_title: Alert Condition Title | alert_description: Result Description " + + "| severity: info | create_time: 2018-04-19T14:00:00.000Z | detect_time: 2018-04-19T14:00:00.000Z " + + "| analyzer: Graylog | analyzer_time: 2018-04-19T14:02:00.000Z | sensor: | classification: " + + "| source_command: | source_file_name: | source_host_name: | source_ip_address: 127.0.0.3 " + + "| source_mac_address: | source_port: | source_process: | source_service_name: | source_tool: " + + "| source_url: | source_user_name: | source_user_privileges: | source_user_unique_identifier: " + + "| target_command: | target_file_name: | target_host_name: | target_ip_address: " + + "| target_mac_address: | target_port: | target_process: | target_service_name: | target_tool: " + + "| target_url: | target_user_name: | target_user_privileges: | target_user_unique_identifier: " + + "| file_name: | file_hash: | file_size: | file_type: | alert_url: http://localhost:8080/alerts/002 " + + "| messages_url: http://localhost:8080/search?rangetype=absolute" + + "&from=2018-04-19T14%3A00%3A00.000Z&to=2018-04-19T14%3A03%3A27.000Z&q=streams%3A001+AND+user%3A\"user1\"")); + + + } + + public void callWithSeverityHigh() throws AlarmCallbackException, AlarmCallbackConfigurationException { + List listAggegationFields = Collections.singletonList(USER); + initializeConfiguration(getConfigMap("high", "Body", listAggegationFields, 0, 0)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC)); + message1.addField(USER, "admin"); + message1.addField("ip_src", "127.0.0.1"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1)); + + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID uuid = UUID.randomUUID(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(uuid); + Alert alert = mock(Alert.class); + when(alert.getId()).thenReturn("002"); + when(alert.isInterval()).thenReturn(true); + when(alert.getTriggeredAt()).thenReturn(new DateTime(2018, 04, 19, 14, 01, 27, DateTimeZone.UTC)); + when(alert.getResolvedAt()).thenReturn(new DateTime(2018, 04, 19, 14, 02, 27, DateTimeZone.UTC)); + Optional optAlert = Optional.of(alert); + when(stream.getId()).thenReturn("001"); + when(alertService.getLastTriggeredAlert(anyString(), anyString())).thenReturn(optAlert); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple("ERROR", "body")); + + } + + public void callWithSeverityMedium() throws AlarmCallbackException, AlarmCallbackConfigurationException { + List listAggegationFields = Collections.singletonList(USER); + initializeConfiguration(getConfigMap("medium", "Body", listAggegationFields, 0, 0)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC)); + message1.addField(USER, "admin"); + message1.addField("ip_src", "127.0.0.1"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1)); + + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID uuid = UUID.randomUUID(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(uuid); + Alert alert = mock(Alert.class); + when(alert.getId()).thenReturn("002"); + when(alert.isInterval()).thenReturn(true); + when(alert.getTriggeredAt()).thenReturn(new DateTime(2018, 04, 19, 14, 01, 27, DateTimeZone.UTC)); + when(alert.getResolvedAt()).thenReturn(new DateTime(2018, 04, 19, 14, 02, 27, DateTimeZone.UTC)); + Optional optAlert = Optional.of(alert); + when(stream.getId()).thenReturn("001"); + when(alertService.getLastTriggeredAlert(anyString(), anyString())).thenReturn(optAlert); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple("ERROR", "body")); + + } + + public void callWithSeverityLow() throws AlarmCallbackException, AlarmCallbackConfigurationException { + List listAggegationFields = Collections.singletonList(USER); + initializeConfiguration(getConfigMap("low", "Body", listAggegationFields, 0, 0)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC)); + message1.addField(USER, "admin"); + message1.addField("ip_src", "127.0.0.1"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1)); + + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID uuid = UUID.randomUUID(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(uuid); + Alert alert = mock(Alert.class); + when(alert.getId()).thenReturn("002"); + when(alert.isInterval()).thenReturn(true); + when(alert.getTriggeredAt()).thenReturn(new DateTime(2018, 04, 19, 14, 01, 27, DateTimeZone.UTC)); + when(alert.getResolvedAt()).thenReturn(new DateTime(2018, 04, 19, 14, 02, 27, DateTimeZone.UTC)); + Optional optAlert = Optional.of(alert); + when(stream.getId()).thenReturn("001"); + when(alertService.getLastTriggeredAlert(anyString(), anyString())).thenReturn(optAlert); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple("WARN", "body")); + + } + + @Test + public void testMsgURLWithPreviousMsgsURL() throws AlarmCallbackException, AlarmCallbackConfigurationException { + List listAggegationFields = Collections.singletonList(USER); + initializeConfiguration(getConfigMap("info", BODY_TEMPLATE_MSG_URL, listAggegationFields, 15, 0)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message1.addField(USER, "admin"); + message1.addField("ip_src", "127.0.0.1"); + Message message2 = new Message("Test message 2", "source2", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message2.addField(USER, "root"); + message2.addField("ip_src", "127.0.0.1"); + Message message3 = new Message("Test message 3", "source3", new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message3.addField(USER, "admin"); + message3.addField("ip_src", "127.0.0.2"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1), + new MessageSummary("graylog_2", message2), + new MessageSummary("graylog_3", message3) + + ); + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + messageSummaries + ); + + when(stream.getId()).thenReturn("001"); + List listAlert = new ArrayList<>(); + Alert alert = mock(Alert.class); + when(alert.getId()).thenReturn("002"); + when(alert.getTriggeredAt()).thenReturn(new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + when(alert.getResolvedAt()).thenReturn(new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + Map conditionParameters = Maps.newHashMap(); + conditionParameters.put("additional_stream", "005"); + when(alert.getConditionParameters()).thenReturn(conditionParameters); + Optional optAlert = Optional.of(alert); + when(alertService.getLastTriggeredAlert(anyString(), anyString())).thenReturn(optAlert); + listAlert.add(alert); + when(alertService.loadRecentOfStream(stream.getId(), new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC).minusMinutes(15), 300)).thenReturn(listAlert); + + String dateFromPreviousMsg = "2017-09-06T16%3A00%3A00.000Z"; + String streamIdPreviousMsg = "012345678901234567890123"; + + SearchResult backlogResult = mock(SearchResult.class); + Message message = new Message("alert_id: alertId","test",new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC)); + message.addField("alert_id", "alertID"); + message.addField("messages_url", "http://localhost:8080/search?rangetype=absolute&from=" + + dateFromPreviousMsg + + "&to=2017-09-06T17%3A01%3A00.000Z&q=streams%3A" + +streamIdPreviousMsg +"AND+user%3A\"admin\""); + ResultMessage resultMessage = mock(ResultMessage.class); + List listResultMessage = new ArrayList<>(); + listResultMessage.add(resultMessage); + when(resultMessage.getMessage()).thenReturn(message); + when(backlogResult.getResults()).thenReturn(listResultMessage); + + when(searches.search(anyString(), anyString(), any(TimeRange.class), eq(10), eq(0), any(Sorting.class))).thenReturn(backlogResult); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple(INFO, "alert_id: "+"alertID"+" | alert_title: Alert Condition Title | " + + "create_time: 2017-09-06T17:00:00.000Z | " + + "detect_time: 2017-09-06T17:00:00.000Z | " + + "alert_url: http://localhost:8080/alerts/002 | " + + "messages_url: http://localhost:8080/search?rangetype=absolute&" + + "from=" + dateFromPreviousMsg + + "&to=2017-09-06T17%3A01%3A00.000Z&q=" + + "(+streams%3A001+OR+streams%3A" + streamIdPreviousMsg + + "+)+AND+user%3A\"admin\""), + tuple(INFO, "alert_id: "+"alertID"+" | alert_title: Alert Condition Title | " + + "create_time: 2017-09-06T17:00:00.000Z | " + + "detect_time: 2017-09-06T17:00:00.000Z | " + + "alert_url: http://localhost:8080/alerts/002 | " + + "messages_url: http://localhost:8080/search?rangetype=absolute&" + + "from=" + dateFromPreviousMsg + + "&to=2017-09-06T17%3A01%3A00.000Z&q=" + + "(+streams%3A001+OR+streams%3A" + streamIdPreviousMsg + + "+)+AND+user%3A\"root\"")); + + } + + @Test + public void callWithSplitFieldNotPresent() throws AlarmCallbackException, AlarmCallbackConfigurationException { + List listAggegationFields = Collections.singletonList(USER); + initializeConfiguration(getConfigMap("info", BODY_TEMPLATE, listAggegationFields, 0, 0)); + + Stream stream = mock(Stream.class); + + final AlertCondition alertCondition = new DummyAlertCondition( + stream, + CONDITION_ID, + new DateTime(2017, 9, 6, 17, 0, DateTimeZone.UTC), + USER, + ImmutableMap.of(), + CONDITION_TITLE + ); + + Message message1 = new Message("Test message 1", "source1", new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC)); + message1.addField(USER, "admin"); + message1.addField("ip_src", "127.0.0.1"); + Message message2 = new Message("Test message 2", "source2", new DateTime(2018, 4, 19, 14, 1, DateTimeZone.UTC)); + message2.addField("ip_src", "127.0.0.2"); + Message message3 = new Message("Test message 3", "source3", new DateTime(2018, 4, 19, 14, 2, DateTimeZone.UTC)); + message3.addField(USER, "user1"); + message3.addField("ip_src", "127.0.0.3"); + + final List messageSummaries = ImmutableList.of( + new MessageSummary("graylog_1", message1), + new MessageSummary("graylog_2", message2), + new MessageSummary("graylog_3", message3) + ); + + final AlertCondition.CheckResult checkResult = new AbstractAlertCondition.CheckResult( + true, + alertCondition, + "Result Description", + new DateTime(2018, 4, 19, 14, 0, DateTimeZone.UTC), + messageSummaries + ); + + UUID uuid = UUID.randomUUID(); + mockStatic(UUID.class); + when(UUID.randomUUID()).thenReturn(uuid); + + Alert alert = mock(Alert.class); + when(alert.getId()).thenReturn("002"); + when(alert.isInterval()).thenReturn(true); + when(alert.getTriggeredAt()).thenReturn(new DateTime(2018, 04, 19, 14, 01, 27, DateTimeZone.UTC)); + when(alert.getResolvedAt()).thenReturn(new DateTime(2018, 04, 19, 14, 02, 27, DateTimeZone.UTC)); + Optional optAlert = Optional.of(alert); + + when(stream.getId()).thenReturn("001"); + when(alertService.getLastTriggeredAlert(anyString(), anyString())).thenReturn(optAlert); + + alarmCallback.call(stream,checkResult); + + assertThat(TEST_LOGGER.getLoggingEvents()).extracting("level", "message").containsExactlyInAnyOrder( + tuple(INFO, "alert_id: 002-92668751 | alert_title: Alert Condition Title | alert_description: Result Description " + + "| severity: info | create_time: 2018-04-19T14:00:00.000Z | detect_time: 2018-04-19T14:00:00.000Z " + + "| analyzer: Graylog | analyzer_time: 2018-04-19T14:00:00.000Z | sensor: | classification: " + + "| source_command: | source_file_name: | source_host_name: | source_ip_address: 127.0.0.1 " + + "| source_mac_address: | source_port: | source_process: | source_service_name: | source_tool: " + + "| source_url: | source_user_name: | source_user_privileges: | source_user_unique_identifier: " + + "| target_command: | target_file_name: | target_host_name: | target_ip_address: " + + "| target_mac_address: | target_port: | target_process: | target_service_name: | target_tool: " + + "| target_url: | target_user_name: | target_user_privileges: | target_user_unique_identifier: " + + "| file_name: | file_hash: | file_size: | file_type: | alert_url: http://localhost:8080/alerts/002 " + + "| messages_url: http://localhost:8080/search?rangetype=absolute" + + "&from=2018-04-19T14%3A00%3A00.000Z&to=2018-04-19T14%3A03%3A27.000Z&q=streams%3A001+AND+user%3A\"admin\""), + tuple(INFO, "alert_id: 002-3392903 | alert_title: Alert Condition Title | alert_description: Result Description " + + "| severity: info | create_time: 2018-04-19T14:00:00.000Z | detect_time: 2018-04-19T14:00:00.000Z " + + "| analyzer: Graylog | analyzer_time: 2018-04-19T14:01:00.000Z | sensor: | classification: " + + "| source_command: | source_file_name: | source_host_name: | source_ip_address: 127.0.0.2 " + + "| source_mac_address: | source_port: | source_process: | source_service_name: | source_tool: " + + "| source_url: | source_user_name: | source_user_privileges: | source_user_unique_identifier: " + + "| target_command: | target_file_name: | target_host_name: | target_ip_address: " + + "| target_mac_address: | target_port: | target_process: | target_service_name: | target_tool: " + + "| target_url: | target_user_name: | target_user_privileges: | target_user_unique_identifier: " + + "| file_name: | file_hash: | file_size: | file_type: | alert_url: http://localhost:8080/alerts/002 " + + "| messages_url: http://localhost:8080/search?rangetype=absolute" + + "&from=2018-04-19T14%3A00%3A00.000Z&to=2018-04-19T14%3A03%3A27.000Z&q=streams%3A001"), + tuple(INFO, "alert_id: 002-111578566 | alert_title: Alert Condition Title | alert_description: Result Description " + + "| severity: info | create_time: 2018-04-19T14:00:00.000Z | detect_time: 2018-04-19T14:00:00.000Z " + + "| analyzer: Graylog | analyzer_time: 2018-04-19T14:02:00.000Z | sensor: | classification: " + + "| source_command: | source_file_name: | source_host_name: | source_ip_address: 127.0.0.3 " + + "| source_mac_address: | source_port: | source_process: | source_service_name: | source_tool: " + + "| source_url: | source_user_name: | source_user_privileges: | source_user_unique_identifier: " + + "| target_command: | target_file_name: | target_host_name: | target_ip_address: " + + "| target_mac_address: | target_port: | target_process: | target_service_name: | target_tool: " + + "| target_url: | target_user_name: | target_user_privileges: | target_user_unique_identifier: " + + "| file_name: | file_hash: | file_size: | file_type: | alert_url: http://localhost:8080/alerts/002 " + + "| messages_url: http://localhost:8080/search?rangetype=absolute" + + "&from=2018-04-19T14%3A00%3A00.000Z&to=2018-04-19T14%3A03%3A27.000Z&q=streams%3A001+AND+user%3A\"user1\"")); + + + } + + @After + public void clearLoggers() { + TestLoggerFactory.clear(); + } + +} + diff --git a/src/web/components/LoggingAlertConfig.jsx b/src/web/components/LoggingAlertConfig.jsx new file mode 100755 index 0000000..cf6b55b --- /dev/null +++ b/src/web/components/LoggingAlertConfig.jsx @@ -0,0 +1,312 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button } from 'react-bootstrap'; +import { BootstrapModalForm, Input } from 'components/bootstrap'; +import { IfPermitted, Select, Spinner } from 'components/common'; +import ObjectUtils from 'util/ObjectUtils'; +import naturalSort from 'javascript-natural-sort'; +import StoreProvider from 'injection/StoreProvider'; + +const StreamsStore = StoreProvider.getStore('Streams'); + +const LoggingAlertConfig = React.createClass({ + propTypes: { + config: PropTypes.object, + updateConfig: PropTypes.func.isRequired, + }, + + getDefaultProps() { + return { + config: { + field_alert_id: 'id', + severity: 'LOW', + separator: ' | ', + log_body: 'type: alert' + '\n' + + 'id: ${logging_alert.id}' + '\n' + + 'severity: ${logging_alert.severity}' + '\n' + + 'app: graylog' + '\n' + + 'subject: ${alertCondition.title}' + '\n' + + 'body: ${check_result.resultDescription}' + '\n' + + 'src: ${message.fields.src_ip}' + '\n' + + 'src_category: ${message.fields.src_category}' + '\n' + + 'dest: ${message.fields.dest_ip}' + '\n' + + 'dest_category: ${message.fields.dest_category}', + alert_tag: 'LoggingAlert', + overflow_tag: 'LoggingOverflow', + }, + }; + }, + + getInitialState() { + return { + config: ObjectUtils.clone(this.props.config), + }; + }, + + componentWillReceiveProps(newProps) { + this.setState({ config: ObjectUtils.clone(newProps.config) }); + }, + + componentDidMount() { + StreamsStore.listStreams().then((streams) => { + this.setState({ + streams: streams }); + }); + }, + + _updateConfigField(field, value) { + const update = ObjectUtils.clone(this.state.config); + update[field] = value; + this.setState({ config: update }); + }, + + _onCheckboxClick(field, ref) { + return () => { + this._updateConfigField(field, this.refs[ref].getChecked()); + }; + }, + + _onSelect(field) { + return (selection) => { + this._updateConfigField(field, selection); + }; + }, + + _onUpdate(field) { + return e => { + this._updateConfigField(field, e.target.value); + }; + }, + + _openModal() { + this.refs.loggingAlertConfigModal.open(); + }, + + _closeModal() { + this.refs.loggingAlertConfigModal.close(); + }, + + _resetConfig() { + // Reset to initial state when the modal is closed without saving. + this.setState(this.getInitialState()); + }, + + _saveConfig() { + this.props.updateConfig(this.state.config).then(() => { + this._closeModal(); + }); + }, + + _availableSeverityTypes() { + return [ + {value: 'HIGH', label: 'High'}, + {value: 'MEDIUM', label: 'Medium'}, + {value: 'LOW', label: 'Low'}, + {value: 'INFO', label: 'Info'}, + ]; + }, + + _activeSeverityType(type) { + return this._availableSeverityTypes().filter((t) => t.value === type)[0].label; + }, + + _onSeverityTypeSelect(id) { + const update = ObjectUtils.clone(this.state.config); + update['severity'] = id; + this.setState({ config: update }); + }, + + _onAggregationStreamSelect(id) { + const update = ObjectUtils.clone(this.state.config); + update['aggregation_stream'] = id; + this.setState({ config: update }); + }, + + _formatOption(key, value) { + return { value: value, label: key }; + }, + + render() { + if (!this.state.streams) { + return ; + } + + const formattedStreams = this.state.streams + .map(stream => this._formatOption(stream.title, stream.id)) + .sort((s1, s2) => naturalSort(s1.label.toLowerCase(), s2.label.toLowerCase())); + + return ( +
+

Logging Alert Notification Configuration

+ +

+ Base configuration for all plugins the Logging Alert Notification module is providing. Note + that some parameters will be stored in MongoDB without encryption. + Graylog users with required permissions will be able to read them in + the configuration dialog on this page. +

+
+
Alert Severity:
+
+ {this._activeSeverityType(this.state.config.severity)} +
+
+
+
Log Content:
+
+ {this.state.config.log_body ? this.state.config.log_body : '[not set]'} +
+
+
+
Line Break Substitution:
+
+ {this.state.config.separator ? this.state.config.separator : '[not set]'} +
+
+
+
Aggregation Time Range:
+
+ {this.state.config.aggregation_time ? this.state.config.aggregation_time : '[not set]'} +
+
+
+
Alerts Stream:
+
+ {this.state.config.aggregation_stream ? this.state.config.aggregation_stream : '[not set]'} +
+
+
+
Alert ID Field:
+
+ {this.state.config.field_alert_id ? this.state.config.field_alert_id : '[not set]'} +
+
+
+
Overflow Limit:
+
+ {this.state.config.limit_overflow ? this.state.config.limit_overflow : '[not set]'} +
+
+
+
Alert Tag:
+
+ {this.state.config.alert_tag ? this.state.config.alert_tag : '[not set]'} +
+
+
+
Overflow Tag:
+
+ {this.state.config.overflow_tag ? this.state.config.overflow_tag : '[not set]'} +
+
+ + + + + + +
+ + + + + + + + + + +
+
+
+ ); + }, +}); + +export default LoggingAlertConfig; + diff --git a/src/web/index.jsx b/src/web/index.jsx new file mode 100755 index 0000000..9030bee --- /dev/null +++ b/src/web/index.jsx @@ -0,0 +1,12 @@ +import packageJson from '../../package.json'; +import { PluginManifest, PluginStore } from 'graylog-web-plugin/plugin'; +import LoggingAlertConfig from 'components/LoggingAlertConfig'; + +PluginStore.register(new PluginManifest(packageJson, { + systemConfigurations: [ + { + component: LoggingAlertConfig, + configType: 'com.airbus_cyber_security.graylog.config.LoggingAlertConfig', + }, + ], +})); diff --git a/webpack.config.js b/webpack.config.js new file mode 100755 index 0000000..eac11b1 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,8 @@ +const PluginWebpackConfig = require('graylog-web-plugin').PluginWebpackConfig; +const loadBuildConfig = require('graylog-web-plugin').loadBuildConfig; +const path = require('path'); + +// Remember to use the same name here and in `getUniqueId()` in the java MetaData class +module.exports = new PluginWebpackConfig('com.airbus_cyber_security.graylog.LoggingAlertPlugin', loadBuildConfig(path.resolve(__dirname, './build.config')), { + // Here goes your additional webpack configuration. +});