Skip to content

Commit 767f648

Browse files
authored
Watcher add email warning if CSV attachment contains formulas (#44460) (#45557)
* 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 f2241a1 commit 767f648

File tree

7 files changed

+395
-32
lines changed

7 files changed

+395
-32
lines changed

x-pack/plugin/watcher/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ dependencies {
4141

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

4747
// 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
@@ -288,7 +288,8 @@ public Collection<Object> createComponents(Client client, ClusterService cluster
288288
Map<String, EmailAttachmentParser> emailAttachmentParsers = new HashMap<>();
289289
emailAttachmentParsers.put(HttpEmailAttachementParser.TYPE, new HttpEmailAttachementParser(httpClient, templateEngine));
290290
emailAttachmentParsers.put(DataAttachmentParser.TYPE, new DataAttachmentParser());
291-
emailAttachmentParsers.put(ReportingAttachmentParser.TYPE, new ReportingAttachmentParser(settings, httpClient, templateEngine));
291+
emailAttachmentParsers.put(ReportingAttachmentParser.TYPE,
292+
new ReportingAttachmentParser(settings, httpClient, templateEngine, clusterService.getClusterSettings()));
292293
EmailAttachmentsParser emailAttachmentsParser = new EmailAttachmentsParser(emailAttachmentParsers);
293294

294295
// conditions
@@ -487,8 +488,7 @@ public List<Setting<?>> getSettings() {
487488
settings.addAll(HtmlSanitizer.getSettings());
488489
settings.addAll(JiraService.getSettings());
489490
settings.addAll(PagerDutyService.getSettings());
490-
settings.add(ReportingAttachmentParser.RETRIES_SETTING);
491-
settings.add(ReportingAttachmentParser.INTERVAL_SETTING);
491+
settings.addAll(ReportingAttachmentParser.getSettings());
492492

493493
// http settings
494494
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

+75-5
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
*/
66
package org.elasticsearch.xpack.watcher.notification.email.attachment;
77

8+
import com.google.common.collect.ImmutableMap;
89
import org.apache.logging.log4j.LogManager;
910
import org.apache.logging.log4j.Logger;
11+
import org.apache.logging.log4j.message.ParameterizedMessage;
1012
import org.elasticsearch.ElasticsearchException;
1113
import org.elasticsearch.common.ParseField;
1214
import org.elasticsearch.common.Strings;
1315
import org.elasticsearch.common.bytes.BytesReference;
1416
import org.elasticsearch.common.logging.LoggerMessageFormat;
17+
import org.elasticsearch.common.settings.ClusterSettings;
1518
import org.elasticsearch.common.settings.Setting;
1619
import org.elasticsearch.common.settings.Settings;
1720
import org.elasticsearch.common.unit.TimeValue;
@@ -37,22 +40,39 @@
3740
import java.io.IOException;
3841
import java.io.InputStream;
3942
import java.io.UncheckedIOException;
43+
import java.util.ArrayList;
44+
import java.util.Arrays;
45+
import java.util.HashSet;
46+
import java.util.List;
47+
import java.util.Locale;
4048
import java.util.Map;
49+
import java.util.Set;
50+
import java.util.concurrent.ConcurrentHashMap;
4151

4252
public class ReportingAttachmentParser implements EmailAttachmentParser<ReportingAttachment> {
4353

4454
public static final String TYPE = "reporting";
4555

4656
// total polling of 10 minutes happens this way by default
47-
public static final Setting<TimeValue> INTERVAL_SETTING =
57+
static final Setting<TimeValue> INTERVAL_SETTING =
4858
Setting.timeSetting("xpack.notification.reporting.interval", TimeValue.timeValueSeconds(15), Setting.Property.NodeScope);
49-
public static final Setting<Integer> RETRIES_SETTING =
59+
static final Setting<Integer> RETRIES_SETTING =
5060
Setting.intSetting("xpack.notification.reporting.retries", 40, 0, Setting.Property.NodeScope);
5161

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

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

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

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

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

0 commit comments

Comments
 (0)