Skip to content

Commit b28f089

Browse files
authored
Watcher add email warning if CSV attachment contains formulas (#44460)
This commit introduces a Warning message to the emails generated by Watcher's reporting action. This change complements Kibana's CSV formula notifications (see elastic/kibana#37930). This is implemented by reading a header (kbn-csv-contains-formulas) provided by Kibana to notify to attach the Warning to the email. The wording of the warning is borrowed from Kibana's UI and may be overridden by a dynamic setting xpack.notification.reporting.warning.kbn-csv-contains-formulas.text. This warning is enabled by default, but may be disabled via a dynamic setting xpack.notification.reporting.warning.enabled.
1 parent bf4b3c6 commit b28f089

File tree

7 files changed

+392
-32
lines changed

7 files changed

+392
-32
lines changed

x-pack/plugin/watcher/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ dependencies {
4444

4545
testCompile 'org.subethamail:subethasmtp:3.1.7'
4646
// needed for subethasmtp, has @GuardedBy annotation
47-
testCompile 'com.google.code.findbugs:jsr305:3.0.1'
47+
testCompile 'com.google.code.findbugs:jsr305:3.0.2'
4848
}
4949

5050
// classes are missing, e.g. com.ibm.icu.lang.UCharacter

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/Watcher.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,8 @@ public Collection<Object> createComponents(Client client, ClusterService cluster
284284
Map<String, EmailAttachmentParser> emailAttachmentParsers = new HashMap<>();
285285
emailAttachmentParsers.put(HttpEmailAttachementParser.TYPE, new HttpEmailAttachementParser(httpClient, templateEngine));
286286
emailAttachmentParsers.put(DataAttachmentParser.TYPE, new DataAttachmentParser());
287-
emailAttachmentParsers.put(ReportingAttachmentParser.TYPE, new ReportingAttachmentParser(settings, httpClient, templateEngine));
287+
emailAttachmentParsers.put(ReportingAttachmentParser.TYPE,
288+
new ReportingAttachmentParser(settings, httpClient, templateEngine, clusterService.getClusterSettings()));
288289
EmailAttachmentsParser emailAttachmentsParser = new EmailAttachmentsParser(emailAttachmentParsers);
289290

290291
// conditions
@@ -470,8 +471,7 @@ public List<Setting<?>> getSettings() {
470471
settings.addAll(HtmlSanitizer.getSettings());
471472
settings.addAll(JiraService.getSettings());
472473
settings.addAll(PagerDutyService.getSettings());
473-
settings.add(ReportingAttachmentParser.RETRIES_SETTING);
474-
settings.add(ReportingAttachmentParser.INTERVAL_SETTING);
474+
settings.addAll(ReportingAttachmentParser.getSettings());
475475

476476
// http settings
477477
settings.addAll(HttpSettings.getSettings());

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/Attachment.java

+18-5
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,26 @@
2424
import java.io.InputStream;
2525
import java.io.OutputStream;
2626
import java.nio.file.Path;
27+
import java.util.Collections;
28+
import java.util.Set;
2729

2830
import static javax.mail.Part.ATTACHMENT;
2931
import static javax.mail.Part.INLINE;
3032

3133
public abstract class Attachment extends BodyPartSource {
3234

3335
private final boolean inline;
36+
private final Set<String> warnings;
3437

3538
protected Attachment(String id, String name, String contentType, boolean inline) {
39+
this(id, name, contentType, inline, Collections.emptySet());
40+
}
41+
42+
protected Attachment(String id, String name, String contentType, boolean inline, Set<String> warnings) {
3643
super(id, name, contentType);
3744
this.inline = inline;
45+
assert warnings != null;
46+
this.warnings = warnings;
3847
}
3948

4049
@Override
@@ -53,6 +62,10 @@ public boolean isInline() {
5362
return inline;
5463
}
5564

65+
public Set<String> getWarnings() {
66+
return warnings;
67+
}
68+
5669
/**
5770
* intentionally not emitting path as it may come as an information leak
5871
*/
@@ -116,15 +129,15 @@ public static class Bytes extends Attachment {
116129
private final byte[] bytes;
117130

118131
public Bytes(String id, byte[] bytes, String contentType, boolean inline) {
119-
this(id, id, bytes, contentType, inline);
132+
this(id, id, bytes, contentType, inline, Collections.emptySet());
120133
}
121134

122135
public Bytes(String id, String name, byte[] bytes, boolean inline) {
123-
this(id, name, bytes, fileTypeMap.getContentType(name), inline);
136+
this(id, name, bytes, fileTypeMap.getContentType(name), inline, Collections.emptySet());
124137
}
125138

126-
public Bytes(String id, String name, byte[] bytes, String contentType, boolean inline) {
127-
super(id, name, contentType, inline);
139+
public Bytes(String id, String name, byte[] bytes, String contentType, boolean inline, Set<String> warnings) {
140+
super(id, name, contentType, inline, warnings);
128141
this.bytes = bytes;
129142
}
130143

@@ -213,7 +226,7 @@ protected XContent(String id, ToXContent content, XContentType type) {
213226
}
214227

215228
protected XContent(String id, String name, ToXContent content, XContentType type) {
216-
super(id, name, bytes(name, content, type), mimeType(type), false);
229+
super(id, name, bytes(name, content, type), mimeType(type), false, Collections.emptySet());
217230
}
218231

219232
static String mimeType(XContentType type) {

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/EmailTemplate.java

+34-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package org.elasticsearch.xpack.watcher.notification.email;
77

88
import org.elasticsearch.ElasticsearchParseException;
9+
import org.elasticsearch.common.Strings;
910
import org.elasticsearch.common.xcontent.ToXContentObject;
1011
import org.elasticsearch.common.xcontent.XContentBuilder;
1112
import org.elasticsearch.common.xcontent.XContentParser;
@@ -16,9 +17,11 @@
1617
import java.io.IOException;
1718
import java.util.ArrayList;
1819
import java.util.Arrays;
20+
import java.util.HashSet;
1921
import java.util.List;
2022
import java.util.Map;
2123
import java.util.Objects;
24+
import java.util.Set;
2225

2326
public class EmailTemplate implements ToXContentObject {
2427

@@ -110,19 +113,46 @@ public Email.Builder render(TextTemplateEngine engine, Map<String, Object> model
110113
if (subject != null) {
111114
builder.subject(engine.render(subject, model));
112115
}
113-
if (textBody != null) {
114-
builder.textBody(engine.render(textBody, model));
115-
}
116+
117+
Set<String> warnings = new HashSet<>(1);
116118
if (attachments != null) {
117119
for (Attachment attachment : attachments.values()) {
118120
builder.attach(attachment);
121+
warnings.addAll(attachment.getWarnings());
119122
}
120123
}
124+
125+
String htmlWarnings = "";
126+
String textWarnings = "";
127+
if(warnings.isEmpty() == false){
128+
StringBuilder textWarningBuilder = new StringBuilder();
129+
StringBuilder htmlWarningBuilder = new StringBuilder();
130+
warnings.forEach(w ->
131+
{
132+
if(Strings.isNullOrEmpty(w) == false) {
133+
textWarningBuilder.append(w).append("\n");
134+
htmlWarningBuilder.append(w).append("<br>");
135+
}
136+
});
137+
textWarningBuilder.append("\n");
138+
htmlWarningBuilder.append("<br>");
139+
htmlWarnings = htmlWarningBuilder.toString();
140+
textWarnings = textWarningBuilder.toString();
141+
}
142+
if (textBody != null) {
143+
builder.textBody(textWarnings + engine.render(textBody, model));
144+
}
145+
121146
if (htmlBody != null) {
122-
String renderedHtml = engine.render(htmlBody, model);
147+
String renderedHtml = htmlWarnings + engine.render(htmlBody, model);
123148
renderedHtml = htmlSanitizer.sanitize(renderedHtml);
124149
builder.htmlBody(renderedHtml);
125150
}
151+
152+
if(htmlBody == null && textBody == null && Strings.isNullOrEmpty(textWarnings) == false){
153+
builder.textBody(textWarnings);
154+
}
155+
126156
return builder;
127157
}
128158

x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/notification/email/attachment/ReportingAttachmentParser.java

+74-5
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88
import org.apache.logging.log4j.LogManager;
99
import org.apache.logging.log4j.Logger;
10+
import org.apache.logging.log4j.message.ParameterizedMessage;
1011
import org.elasticsearch.ElasticsearchException;
1112
import org.elasticsearch.common.ParseField;
1213
import org.elasticsearch.common.Strings;
1314
import org.elasticsearch.common.bytes.BytesReference;
1415
import org.elasticsearch.common.logging.LoggerMessageFormat;
16+
import org.elasticsearch.common.settings.ClusterSettings;
1517
import org.elasticsearch.common.settings.Setting;
1618
import org.elasticsearch.common.settings.Settings;
1719
import org.elasticsearch.common.unit.TimeValue;
@@ -37,22 +39,39 @@
3739
import java.io.IOException;
3840
import java.io.InputStream;
3941
import java.io.UncheckedIOException;
42+
import java.util.ArrayList;
43+
import java.util.Arrays;
44+
import java.util.HashSet;
45+
import java.util.List;
46+
import java.util.Locale;
4047
import java.util.Map;
48+
import java.util.Set;
49+
import java.util.concurrent.ConcurrentHashMap;
4150

4251
public class ReportingAttachmentParser implements EmailAttachmentParser<ReportingAttachment> {
4352

4453
public static final String TYPE = "reporting";
4554

4655
// total polling of 10 minutes happens this way by default
47-
public static final Setting<TimeValue> INTERVAL_SETTING =
56+
static final Setting<TimeValue> INTERVAL_SETTING =
4857
Setting.timeSetting("xpack.notification.reporting.interval", TimeValue.timeValueSeconds(15), Setting.Property.NodeScope);
49-
public static final Setting<Integer> RETRIES_SETTING =
58+
static final Setting<Integer> RETRIES_SETTING =
5059
Setting.intSetting("xpack.notification.reporting.retries", 40, 0, Setting.Property.NodeScope);
5160

61+
static final Setting<Boolean> REPORT_WARNING_ENABLED_SETTING =
62+
Setting.boolSetting("xpack.notification.reporting.warning.enabled", true, Setting.Property.NodeScope, Setting.Property.Dynamic);
63+
64+
static final Setting.AffixSetting<String> REPORT_WARNING_TEXT =
65+
Setting.affixKeySetting("xpack.notification.reporting.warning.", "text",
66+
key -> Setting.simpleString(key, Setting.Property.NodeScope, Setting.Property.Dynamic));
67+
5268
private static final ObjectParser<Builder, AuthParseContext> PARSER = new ObjectParser<>("reporting_attachment");
5369
private static final ObjectParser<KibanaReportingPayload, Void> PAYLOAD_PARSER =
5470
new ObjectParser<>("reporting_attachment_kibana_payload", true, null);
5571

72+
static final Map<String, String> WARNINGS = Map.of("kbn-csv-contains-formulas", "Warning: The attachment [%s] contains " +
73+
"characters which spreadsheet applications may interpret as formulas. Please ensure that the attachment is safe prior to opening.");
74+
5675
static {
5776
PARSER.declareInt(Builder::retries, ReportingAttachment.RETRIES);
5877
PARSER.declareBoolean(Builder::inline, ReportingAttachment.INLINE);
@@ -63,18 +82,52 @@ public class ReportingAttachmentParser implements EmailAttachmentParser<Reportin
6382
PAYLOAD_PARSER.declareString(KibanaReportingPayload::setPath, new ParseField("path"));
6483
}
6584

85+
private static List<Setting<?>> getDynamicSettings() {
86+
return Arrays.asList(REPORT_WARNING_ENABLED_SETTING, REPORT_WARNING_TEXT);
87+
}
88+
89+
private static List<Setting<?>> getStaticSettings() {
90+
return Arrays.asList(INTERVAL_SETTING, RETRIES_SETTING);
91+
}
92+
93+
public static List<Setting<?>> getSettings() {
94+
List<Setting<?>> allSettings = new ArrayList<Setting<?>>(getDynamicSettings());
95+
allSettings.addAll(getStaticSettings());
96+
return allSettings;
97+
}
6698
private final Logger logger;
6799
private final TimeValue interval;
68100
private final int retries;
69101
private HttpClient httpClient;
70102
private final TextTemplateEngine templateEngine;
103+
private boolean warningEnabled = REPORT_WARNING_ENABLED_SETTING.getDefault(Settings.EMPTY);
104+
private final Map<String, String> customWarnings = new ConcurrentHashMap<>(1);
71105

72-
public ReportingAttachmentParser(Settings settings, HttpClient httpClient, TextTemplateEngine templateEngine) {
106+
public ReportingAttachmentParser(Settings settings, HttpClient httpClient, TextTemplateEngine templateEngine,
107+
ClusterSettings clusterSettings) {
73108
this.interval = INTERVAL_SETTING.get(settings);
74109
this.retries = RETRIES_SETTING.get(settings);
75110
this.httpClient = httpClient;
76111
this.templateEngine = templateEngine;
77112
this.logger = LogManager.getLogger(getClass());
113+
clusterSettings.addSettingsUpdateConsumer(REPORT_WARNING_ENABLED_SETTING, this::setWarningEnabled);
114+
clusterSettings.addAffixUpdateConsumer(REPORT_WARNING_TEXT, this::addWarningText, this::warningValidator);
115+
}
116+
117+
void setWarningEnabled(boolean warningEnabled) {
118+
this.warningEnabled = warningEnabled;
119+
}
120+
121+
void addWarningText(String name, String value) {
122+
customWarnings.put(name, value);
123+
}
124+
125+
void warningValidator(String name, String value) {
126+
if (WARNINGS.keySet().contains(name) == false) {
127+
throw new IllegalArgumentException(new ParameterizedMessage(
128+
"Warning [{}] is not supported. Only the following warnings are supported [{}]",
129+
name, String.join(", ", WARNINGS.keySet())).getFormattedMessage());
130+
}
78131
}
79132

80133
@Override
@@ -139,8 +192,24 @@ public Attachment toAttachment(WatchExecutionContext context, Payload payload, R
139192
"method[{}], path[{}], status[{}], body[{}]", context.watch().id(), attachment.id(), request.host(),
140193
request.port(), request.method(), request.path(), response.status(), body);
141194
} else if (response.status() == 200) {
142-
return new Attachment.Bytes(attachment.id(), BytesReference.toBytes(response.body()),
143-
response.contentType(), attachment.inline());
195+
Set<String> warnings = new HashSet<>(1);
196+
if (warningEnabled) {
197+
WARNINGS.forEach((warningKey, defaultWarning) -> {
198+
String[] text = response.header(warningKey);
199+
if (text != null && text.length > 0) {
200+
if (Boolean.valueOf(text[0])) {
201+
String warning = String.format(Locale.ROOT, defaultWarning, attachment.id());
202+
String customWarning = customWarnings.get(warningKey);
203+
if (Strings.isNullOrEmpty(customWarning) == false) {
204+
warning = String.format(Locale.ROOT, customWarning, attachment.id());
205+
}
206+
warnings.add(warning);
207+
}
208+
}
209+
});
210+
}
211+
return new Attachment.Bytes(attachment.id(), attachment.id(), BytesReference.toBytes(response.body()),
212+
response.contentType(), attachment.inline(), warnings);
144213
} else {
145214
String body = response.body() != null ? response.body().utf8ToString() : null;
146215
String message = LoggerMessageFormat.format("", "Watch[{}] reporting[{}] Unexpected status code host[{}], port[{}], " +

0 commit comments

Comments
 (0)