Skip to content

Commit

Permalink
Releasing version 2.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Christiaan van Tienhoven committed Sep 11, 2018
1 parent 1485cd4 commit 2097406
Show file tree
Hide file tree
Showing 11 changed files with 50 additions and 155 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ This scenario is actually very useful in a security context, but with the built-

![](https://github.com/cvtienhoven/graylog-plugin-aggregates/blob/master/images/report.png)

**Define the Aggregates Email Callback**
![](https://github.com/cvtienhoven/graylog-plugin-aggregates/blob/master/images/email_callback.png)

**Email example**
![](https://github.com/cvtienhoven/graylog-plugin-aggregates/blob/master/images/email.png)


Usage
-----
Expand Down Expand Up @@ -83,11 +89,15 @@ Each rule can be configured to be executed on a particular stream. For the latte

**Sending alerts**

Since version 2.0.0, the plugin integrates tightly with the `Notifications` and `Alert Conditions` within Graylog. You can define a notification on a stream as you would normally do. The plugin creates an Alert Condition when creating an Aggregate Rule and it keeps the condition in sync with the rule after updates. In previous versions, you could only send emails, but now you can also use the HTTP Alarm Callback for instance. If you still want to use emails, you'll have to use the Email Alarm Callback. Unfortunately the HTML markup in emails had to be discarded since the Email Alarm Callback sends emails in plain text.
Since version 2.0.0, the plugin integrates tightly with the `Notifications` and `Alert Conditions` within Graylog. You can define a notification on a stream as you would normally do. The plugin creates an Alert Condition when creating an Aggregate Rule and it keeps the condition in sync with the rule after updates. In previous versions, you could only send emails, but now you can also use the HTTP Alarm Callback for instance.

New in version 2.3.0 is the `Aggregates Email Alarm Callback`. This callback works in the same way as the normal Email Alarm Callback, but a table (inserted at the `${matchedTermsTable}` placeholder) is added to the template, as shown in the screenshot. The table contains the found values and their number of occurrences, with links to the respective search query. This callback sends an HTML email, so you can customize the layout by using HTML tags and CSS in the email template.

**_Note 1_**: If you delete the Alert Condition, the plugin re-creates it, except when you disable the rule.<br/>
**_Note 2_**: Enabling the message backlog can inflict a performance penalty, as the backlog has to be assembled from the found terms, using separate searches. Use with care.<br/>
**_Note 3_**: Alert Conditions are created under the user `admin`.
**_Note 3_**: Alert Conditions are created under the user `admin`.<br/>
**_Note_4_**: For the Aggregates Email Alarm Callback, only email receivers can be defined, user receivers are not supported.



**Reporting**
Expand All @@ -107,6 +117,9 @@ When creating or editing a rule, the schedule(s) for generating report(s) can be
- Feature: Added the Aggregates Email Alarm Callback that emails a table with found field values, the # of occurences and a link to the search (#35, #41).
- Bugfix: Removed the extra " AND streams: <id>" from the query, as the stream is already filtered in the Alert Condition.
- Bugfix: Logged the removal of history items on debug level instead of info (#26).
- Bugfix: If the stream is altered for a rule, remove the AlertCondition on the original stream first.
- Bugfix: Altered description for stream title in rule list if user can't see that stream.
- Bugfix: The Aggregates item in the navition bar is only visible when users have aggregate_rules:read permissions (#40)

2.2.4
-----
Expand Down
Binary file added images/email.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/email_callback.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<groupId>org.graylog.plugins.aggregates</groupId>
<artifactId>graylog-plugin-aggregates</artifactId>
<version>2.3.0-SNAPSHOT</version>
<version>2.3.0</version>
<packaging>jar</packaging>

<name>${project.artifactId}</name>
Expand Down
125 changes: 1 addition & 124 deletions src/main/java/org/graylog/plugins/aggregates/Aggregates.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,128 +5,5 @@
* This is the plugin. Your class should implement one of the existing plugin
* interfaces. (i.e. AlarmCallback, MessageInput, MessageOutput)
*/
public class Aggregates{//} extends Periodical {
/*
private int sequence = 0;
private int maxInterval = 1; // max interval detected in rules
private final ClusterConfigService clusterConfigService;
private final Searches searches;
private final Cluster cluster;
private final RuleService ruleService;
private final HistoryItemService historyItemService;
private final AlertConditionFactory alertConditionFactory;
private final StreamService streamService;
private final AlertService alertService;
private static final Logger LOG = LoggerFactory.getLogger(Aggregates.class);
private List<Rule> list;
@Inject
public Aggregates(Searches searches, ClusterConfigService clusterConfigService,
Cluster cluster, RuleService ruleService, HistoryItemService historyItemService, AlertConditionFactory alertConditionFactory,
StreamService streamService, AlertService alertService) {
this.searches = searches;
this.clusterConfigService = clusterConfigService;
this.cluster = cluster;
this.ruleService = ruleService;
this.historyItemService = historyItemService;
this.alertConditionFactory = alertConditionFactory;
this.streamService = streamService;
this.alertService = alertService;
}
@VisibleForTesting
boolean shouldRun() {
return cluster.isHealthy();
}
@Override
public void doRun() {
if (!shouldRun()) {
LOG.warn("Indexer is not running, not checking any rules this run.");
} else {
list = ruleService.all();
if (sequence == maxInterval) {
sequence = 0;
}
sequence++;
}
}
@VisibleForTesting
TimeRange buildRelativeTimeRange(int range) {
try {
return restrictTimeRange(RelativeRange.create(range));
} catch (InvalidRangeParametersException e) {
LOG.warn("Invalid timerange parameters provided, not executing rule");
return null;
}
}
protected org.graylog2.plugin.indexer.searches.timeranges.TimeRange restrictTimeRange(
final org.graylog2.plugin.indexer.searches.timeranges.TimeRange timeRange) {
final DateTime originalFrom = timeRange.getFrom();
final DateTime to = timeRange.getTo();
final DateTime from;
final SearchesClusterConfig config = clusterConfigService.get(SearchesClusterConfig.class);
if (config == null || Period.ZERO.equals(config.queryTimeRangeLimit())) {
from = originalFrom;
} else {
final DateTime limitedFrom = to.minus(config.queryTimeRangeLimit());
from = limitedFrom.isAfter(originalFrom) ? limitedFrom : originalFrom;
}
return AbsoluteRange.create(from, to);
}
@Override
public int getInitialDelaySeconds() {
return 0;
}
@Override
protected Logger getLogger() {
return LOG;
}
@Override
public int getPeriodSeconds() {
return 60;
}
@Override
public boolean isDaemon() {
return true;
}
@Override
public boolean masterOnly() {
return true;
}
@Override
public boolean runsForever() {
return false;
}
@Override
public boolean startOnThisNode() {
return true;
}
@Override
public boolean stopOnGracefulShutdown() {
return true;
}
*/
public class Aggregates{
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public URI getURL() {

@Override
public Version getVersion() {
return new Version(2, 2, 4);
return new Version(2, 3, 0);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ public void call(Stream stream, AlertCondition.CheckResult result) throws AlarmC
}

private EmailRecipients getEmailRecipients() {

return emailRecipientsFactory.create(
configuration.getList(CK_USER_RECEIVERS, Collections.emptyList()),
configuration.getList(CK_EMAIL_RECEIVERS, Collections.emptyList())
Expand Down Expand Up @@ -197,19 +198,12 @@ private ConfigurationRequest getConfigurationRequest(Map<String, String> userNam
ConfigurationField.Optional.OPTIONAL,
TextField.Attribute.TEXTAREA));

configurationRequest.addField(new ListField(CK_USER_RECEIVERS,
"User Receivers",
Collections.emptyList(),
userNames,
"Graylog usernames that should receive this alert",
ConfigurationField.Optional.OPTIONAL));

configurationRequest.addField(new ListField(CK_EMAIL_RECEIVERS,
"E-Mail Receivers",
Collections.emptyList(),
Collections.emptyMap(),
"E-Mail addresses that should receive this alert",
ConfigurationField.Optional.OPTIONAL,
ConfigurationField.Optional.NOT_OPTIONAL,
ListField.Attribute.ALLOW_CREATE));

return configurationRequest;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@

public class FormattedEmailAlertSender implements AggregatesAlertSender {
private static final Logger LOG = LoggerFactory.getLogger(org.graylog2.alerts.FormattedEmailAlertSender.class);
private static final String FONT_SIZE = "14px";
private static final String FONT_FAMILY = "Arial";

public static final String bodyTemplate = "##########\n" +
"Alert Description: ${check_result.resultDescription}\n" +
Expand Down Expand Up @@ -105,7 +107,7 @@ String buildSubject(Stream stream, AlertCondition.CheckResult checkResult, List<

Map<String, Object> model = getModel(stream, checkResult, backlog);

return templateEngine.transform(template, model);
return templateEngine.transform(template, model).replaceAll("(\r\n|\n)", "<br />");
}

@VisibleForTesting
Expand All @@ -118,7 +120,7 @@ String buildBody(Stream stream, AlertCondition.CheckResult checkResult, List<Mes
}
Map<String, Object> model = getModel(stream, checkResult, backlog);

return this.templateEngine.transform(template, model);
return "<html><body style=\"font-family:" + FONT_FAMILY + "; font-size: " + FONT_SIZE + "\">" + this.templateEngine.transform(template, model) + "</body></html>";
}

private Map<String, Object> getModel(Stream stream, AlertCondition.CheckResult checkResult, List<Message> backlog) {
Expand All @@ -138,8 +140,8 @@ private Map<String, Object> getModel(Stream stream, AlertCondition.CheckResult c

private String getMatchedTermsHTMLTable(AlertCondition.CheckResult result, String baseUri){
String field = ((AggregatesAlertCondition)result.getTriggeredCondition()).getField();
String table = "<table width=\"100%\" border=\"1\" align=\"left\">";
table += "<tr><th>Found value</th><th>Occurrences</th></tr>\n";
String table = "<table width=\"100%\" border=\"1\" style=\"font-family:Arial; border-collapse: collapse; text-align: left;font-size: " + FONT_SIZE + ";\">";
table += "<tr><th align=\"left\">Value of field \"" + field + "\"</th><th align=\"left\">Occurrences</th></tr>\n";
for (Map.Entry<String, Long> entry : ((AggregatesAlertCondition.AggregatesCheckResult)result).getMatchedTerms().entrySet())
{
try {
Expand All @@ -150,8 +152,6 @@ private String getMatchedTermsHTMLTable(AlertCondition.CheckResult result, Strin
}
table += "</table>";

LOG.info("table: " + table);

return table;
}

Expand Down Expand Up @@ -226,10 +226,13 @@ private void sendEmail(String emailAddress, Stream stream, AlertCondition.CheckR
} else {
email.setFrom(configuration.getFromEmail());
}

email.setSubject(buildSubject(stream, checkResult, backlog));
email.setMsg(buildBody(stream, checkResult, backlog));
email.addTo(emailAddress);

LOG.debug("Sending email to [{}]", emailAddress);

email.send();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ public Rule create(Rule rule) {
public Rule update(String name, Rule rule) {

if (rule instanceof RuleImpl) {
RuleImpl originalRule = coll.findOne(DBQuery.is("name", name));

if (!originalRule.getStreamId().equals(rule.getStreamId())){
removeAlertCondition(originalRule);
createOrUpdateAlertCondition(rule);
}

final RuleImpl ruleImpl = (RuleImpl) rule;
LOG.debug("Rule to be updated [{}]", ruleImpl);

Expand Down Expand Up @@ -184,6 +191,7 @@ public Rule fromRequest(UpdateRuleRequest request) {
}

public String createOrUpdateAlertCondition(Rule rule){

String query = rule.getQuery();
String streamId = rule.getStreamId();

Expand Down Expand Up @@ -225,20 +233,21 @@ public String createOrUpdateAlertCondition(Rule rule){

}


private void removeAlertCondition(Rule rule){
Stream triggeredStream = null;
try {
triggeredStream = streamService.load(rule.getStreamId());
streamService.removeAlertCondition(triggeredStream,rule.getAlertConditionId());
} catch (NotFoundException e) {
LOG.error("Stream with ID [{}] not found", rule.getStreamId());
}
}


@Override
public int destroy(String ruleName) {
Rule rule = coll.findOne(DBQuery.is("name", ruleName));
Stream triggeredStream = null;
try {
triggeredStream = streamService.load(rule.getStreamId());
streamService.removeAlertCondition(triggeredStream,rule.getAlertConditionId());
} catch (NotFoundException e) {
LOG.error("Stream with ID [{}] not found", rule.getStreamId());
}

removeAlertCondition(rule);
return coll.remove(DBQuery.is("name", ruleName)).getN();
}

Expand Down
2 changes: 1 addition & 1 deletion src/web/aggregates/RulesList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ const RulesList = React.createClass({
</div>
);

let streamTitle = '--No Stream (global search)--';
let streamTitle = 'N/A (No permissions?)';
if (rule.streamId !== '') {
for (let i = 0; i < this.state.streams.length; i++) {
if (this.state.streams[i].id === rule.streamId) {
Expand Down
5 changes: 2 additions & 3 deletions src/web/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,11 @@ import SchedulesPage from 'aggregates/SchedulesPage'
const manifest = new PluginManifest(packageJson, {

routes: [
{ path: '/aggregates', component: AggregatesPage, permissions: 'AGGREGATE_RULES_READ,AGGREGATE_REPORT_SCHEDULES_READ' },
{ path: '/aggregates', component: AggregatesPage, permissions: 'AGGREGATE_RULES_READ' },
{ path: '/aggregates/schedules', component: SchedulesPage, permissions: 'AGGREGATE_REPORT_SCHEDULES_READ' }
],

navigation: [
{ path: '/aggregates', description: 'Aggregates' }
{ path: '/aggregates', description: 'Aggregates', permissions: 'aggregate_rules:read' }
],

systemConfigurations: [
Expand Down

0 comments on commit 2097406

Please sign in to comment.