Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Structured audit logging #31931

Merged
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
dfa711f
StructuredMapMessage
albertzaharovits Jul 10, 2018
0511e09
WIP tests assertMessage
albertzaharovits Jul 10, 2018
2607d19
Merge branch 'master' into structured-audit-logging
albertzaharovits Jul 11, 2018
8e4cb49
test helper methods in place, still WIP
albertzaharovits Jul 12, 2018
08187bf
Merge branch 'master' into structured-audit-logging
albertzaharovits Jul 12, 2018
d54c05f
Merge branch 'master' into structured-audit-logging
albertzaharovits Aug 7, 2018
69c201a
Consistent renames
albertzaharovits Aug 7, 2018
fa579e7
WIP until LoggingAuditTrailTests#accessGranted
albertzaharovits Aug 7, 2018
1098bc2
WIP should really stop splitting on spaces
albertzaharovits Aug 8, 2018
138612a
damn regexps
albertzaharovits Aug 9, 2018
732f021
LoggingAuditTrailTests doneee!
albertzaharovits Aug 9, 2018
3e38556
Merge branch 'master' into structured-audit-logging
albertzaharovits Aug 9, 2018
863b003
Nothing to see here
albertzaharovits Aug 10, 2018
1c14cc7
Merge branch 'master' into structured-audit-logging
albertzaharovits Aug 10, 2018
e8c4c81
Merge branch 'master' into structured-audit-logging
albertzaharovits Aug 10, 2018
fe0ba08
AuditTrailServiceTests done!
albertzaharovits Aug 12, 2018
723e25e
Checkstyle fixes
albertzaharovits Aug 12, 2018
1c7f947
PatternLayout with JSON format
albertzaharovits Aug 13, 2018
8fdefae
JSON format from PatterLayout to preserve order
albertzaharovits Aug 13, 2018
3688615
Checkstyle
albertzaharovits Aug 14, 2018
62ff075
Merge branch 'master' into structured-audit-logging
albertzaharovits Aug 14, 2018
f48ac57
Some QA tests work
albertzaharovits Aug 14, 2018
4c7f335
SQL X-PACK
albertzaharovits Aug 14, 2018
3a9e4db
Charsets forbbided error in PatternLayout
albertzaharovits Aug 14, 2018
be1a6a2
Clear log appender before test
albertzaharovits Aug 15, 2018
9e66119
debug message for asert failures
albertzaharovits Aug 16, 2018
ef1d6b6
Don't burry Thread.currentThread().getStackTrace()
albertzaharovits Aug 17, 2018
e92fb3b
Revert "debug message for asert failures"
albertzaharovits Aug 17, 2018
131f669
Merge branch 'master' into structured-audit-logging
albertzaharovits Aug 19, 2018
5c95ec9
Merge branch 'master' into structured-audit-logging
albertzaharovits Aug 26, 2018
eb8cb15
Rename from _access.log to _audit.log
albertzaharovits Aug 26, 2018
59d3b50
Indices and Roles are arrays; no tests
albertzaharovits Aug 27, 2018
b640d4f
Roles and indeces are arrays
albertzaharovits Aug 27, 2018
c3644e8
roles and indices are arrays
albertzaharovits Aug 27, 2018
b3c70ca
Merge branch 'master' into structured-audit-logging
albertzaharovits Aug 27, 2018
56fbeaa
timestamp -> @timestamp
albertzaharovits Aug 27, 2018
40e98a1
Empty arrays for roles and indices
albertzaharovits Aug 28, 2018
76650aa
URI.path URI.query encoded
albertzaharovits Aug 28, 2018
feb2529
Merge branch 'master' into structured-audit-logging
albertzaharovits Aug 30, 2018
49af157
Merge branch 'master' into structured-audit-logging
albertzaharovits Sep 4, 2018
efbeefc
Merge branch 'master' into structured-audit-logging
albertzaharovits Sep 5, 2018
a0103d2
Merge branch 'master' into structured-audit-logging
albertzaharovits Sep 5, 2018
2a3857d
Use the actual log4j2properties file in tests
albertzaharovits Sep 6, 2018
9ffda34
Feedback
albertzaharovits Sep 6, 2018
a569107
log4j2.properties comments about event field meaning
albertzaharovits Sep 6, 2018
8b8a8b8
LogEntryBuilder
albertzaharovits Sep 6, 2018
3d6d886
Merge branch 'master' into structured-audit-logging
albertzaharovits Sep 6, 2018
ad942f1
Merge branch 'master' into structured-audit-logging
albertzaharovits Sep 13, 2018
a6aa256
`host.ip` no port and `origin.address` has port
albertzaharovits Sep 13, 2018
cc9084f
added node.id and disabled node.name
albertzaharovits Sep 13, 2018
5f87530
Removed "event.category": "elasticsearch-audit"
albertzaharovits Sep 13, 2018
633ef9c
Add parantheses
albertzaharovits Sep 13, 2018
8882486
Merge branch 'master' into structured-audit-logging
albertzaharovits Sep 13, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions x-pack/plugin/core/src/main/config/log4j2.properties
Original file line number Diff line number Diff line change
@@ -1,9 +1,62 @@
appender.audit_rolling.type = RollingFile
appender.audit_rolling.name = audit_rolling
appender.audit_rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_access.log
appender.audit_rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_audit.log
albertzaharovits marked this conversation as resolved.
Show resolved Hide resolved
appender.audit_rolling.layout.type = PatternLayout
appender.audit_rolling.layout.pattern = [%d{ISO8601}] %m%n
appender.audit_rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_access-%d{yyyy-MM-dd}.log
appender.audit_rolling.layout.pattern = {\
"@timestamp":"%d{ISO8601}"\
%varsNotEmpty{, "node.name":"%enc{%map{node.name}}{JSON}"}\
%varsNotEmpty{, "host.name":"%enc{%map{host.name}}{JSON}"}\
%varsNotEmpty{, "host.ip":"%enc{%map{host.ip}}{JSON}"}\
%varsNotEmpty{, "event.type":"%enc{%map{event.type}}{JSON}"}\
%varsNotEmpty{, "event.action":"%enc{%map{event.action}}{JSON}"}\
%varsNotEmpty{, "user.name":"%enc{%map{user.name}}{JSON}"}\
%varsNotEmpty{, "user.run_by.name":"%enc{%map{user.run_by.name}}{JSON}"}\
%varsNotEmpty{, "user.run_as.name":"%enc{%map{user.run_as.name}}{JSON}"}\
%varsNotEmpty{, "user.realm":"%enc{%map{user.realm}}{JSON}"}\
%varsNotEmpty{, "user.run_by.realm":"%enc{%map{user.run_by.realm}}{JSON}"}\
%varsNotEmpty{, "user.run_as.realm":"%enc{%map{user.run_as.realm}}{JSON}"}\
%varsNotEmpty{, "user.roles":%map{user.roles}}\
%varsNotEmpty{, "origin.type":"%enc{%map{origin.type}}{JSON}"}\
%varsNotEmpty{, "origin.address":"%enc{%map{origin.address}}{JSON}"}\
%varsNotEmpty{, "realm":"%enc{%map{realm}}{JSON}"}\
%varsNotEmpty{, "url.path":"%enc{%map{url.path}}{JSON}"}\
%varsNotEmpty{, "url.query":"%enc{%map{url.query}}{JSON}"}\
%varsNotEmpty{, "request.body":"%enc{%map{request.body}}{JSON}"}\
%varsNotEmpty{, "action":"%enc{%map{action}}{JSON}"}\
%varsNotEmpty{, "request.name":"%enc{%map{request.name}}{JSON}"}\
%varsNotEmpty{, "indices":%map{indices}}\
%varsNotEmpty{, "opaque_id":"%enc{%map{opaque_id}}{JSON}"}\
%varsNotEmpty{, "transport.profile":"%enc{%map{transport.profile}}{JSON}"}\
%varsNotEmpty{, "rule":"%enc{%map{rule}}{JSON}"}\
%varsNotEmpty{, "event.category":"%enc{%map{event.category}}{JSON}"}\
}%n
Copy link
Contributor Author

@albertzaharovits albertzaharovits Aug 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the format of the PatternLayout that is assigned to the rolling file appender of the audit log trail.
It prints each event on one line in the JSON format.
A field with a missing value is not printed (%varsNotEmpty function).
All values are escaped as JSON, i.e. {, }, " and other special characters are escaped. Consequently there is no need to escape them in the code.
PatternLayout is preferred to the JSONLayout because it allows to define the field order.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add these comments (at least the info in them) in the log4j2.properties file? This is good information and it would be nice to have it next to where this is defined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, will do!

# "node.name" node name from the `elasticsearch.yml` settings
# "host.name" unresolved hostname of the local node
# "host.ip" the local bound ip (i.e. the ip listening for connections)
# "event.type" a received REST request is translated into one or more transport requests. This indicates which processing layer generated the event "rest" or "transport" (internal)
# "event.action" the name of the audited event, eg. "authentication_failed", "access_granted", "run_as_granted", etc.
# "user.name" the subject name as authenticated by a realm
# "user.run_by.name" the original authenticated subject name that is impersonating another one.
# "user.run_as.name" if this "event.action" is of a run_as type, this is the subject name to be impersonated as.
# "user.realm" the name of the realm that authenticated "user.name"
# "user.run_by.realm" the realm name of the impersonating subject ("user.run_by.name")
# "user.run_as.realm" if this "event.action" is of a run_as type, this is the realm name the impersonated user is looked up from
# "user.roles" the roles array of the user; these are the roles that are granting privileges
# "origin.type" it is "rest" if the event is originating (is in relation to) a REST request; possible other values are "transport" and "ip_filter"
# "origin.address" the remote address and port of the first network hop, i.e. a REST proxy or another cluster node
# "realm" name of a realm that has generated an "authentication_failed" or an "authentication_successful"; the subject is not yet authenticated
# "url.path" the URI component between the port and the query string; it is percent (URL) encoded
# "url.query" the URI component after the path and before the fragment; it is percent (URL) encoded
# "request.body" the content of the request body entity, JSON escaped
# "action" an action is the most granular operation that is authorized and this identifies it in a namespaced way (internal)
# "request.name" if the event is in connection to a transport message this is the name of the request class, similar to how rest requests are identified by the url path (internal)
# "indices" the array of indices that the "action" is acting upon
# "opaque_id" opaque value conveyed by the "X-Opaque-Id" request header
# "transport.profile" name of the transport profile in case this is a "connection_granted" or "connection_denied" event
# "rule" name of the applied rulee if the "origin.type" is "ip_filter"
# "event.category" fixed value "elasticsearch-audit"

appender.audit_rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_audit-%d{yyyy-MM-dd}.log
appender.audit_rolling.policies.type = Policies
appender.audit_rolling.policies.time.type = TimeBasedTriggeringPolicy
appender.audit_rolling.policies.time.interval = 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,51 @@
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.StringLayout;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.filter.RegexFilter;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.common.logging.Loggers;

import java.util.ArrayList;
import java.util.List;

/**
* Logger that captures events and appends them to in memory lists, with one
* list for each log level. This works with the global log manager context,
* meaning that there could only be a single logger with the same name.
*/
public class CapturingLogger {

public static Logger newCapturingLogger(final Level level) throws IllegalAccessException {
/**
* Constructs a new {@link CapturingLogger} named as the fully qualified name of
* the invoking method. One name can be assigned to a single logger globally, so
* don't call this method multiple times in the same method.
*
* @param level
* The minimum priority level of events that will be captured.
* @param layout
* Optional parameter allowing to set the layout format of events.
* This is useful because events are captured to be inspected (and
* parsed) later. When parsing, it is useful to be in control of the
* printing format as well. If not specified,
* {@code event.getMessage().getFormattedMessage()} is called to
* format the event.
* @return The new logger.
*/
public static Logger newCapturingLogger(final Level level, @Nullable StringLayout layout) throws IllegalAccessException {
albertzaharovits marked this conversation as resolved.
Show resolved Hide resolved
// careful, don't "bury" this on the call stack, unless you know what you're doing
final StackTraceElement caller = Thread.currentThread().getStackTrace()[2];
final String name = caller.getClassName() + "." + caller.getMethodName() + "." + level.toString();
final Logger logger = ESLoggerFactory.getLogger(name);
Loggers.setLevel(logger, level);
final MockAppender appender = new MockAppender(name);
final MockAppender appender = new MockAppender(name, layout);
appender.start();
Loggers.addAppender(logger, appender);
return logger;
Expand All @@ -40,11 +65,27 @@ private static MockAppender getMockAppender(final String name) {
return (MockAppender) loggerConfig.getAppenders().get(name);
}

/**
* Checks if the logger's appender has captured any events.
*
* @param name
* The unique global name of the logger.
* @return {@code true} if no event has been captured, {@code false} otherwise.
*/
public static boolean isEmpty(final String name) {
final MockAppender appender = getMockAppender(name);
return appender.isEmpty();
}

/**
* Gets the captured events for a logger by its name.
*
* @param name
* The unique global name of the logger.
* @param level
* The priority level of the captured events to be returned.
* @return A list of captured events formated to {@code String}.
*/
public static List<String> output(final String name, final Level level) {
final MockAppender appender = getMockAppender(name);
return appender.output(level);
Expand All @@ -58,8 +99,8 @@ private static class MockAppender extends AbstractAppender {
public final List<String> debug = new ArrayList<>();
public final List<String> trace = new ArrayList<>();

private MockAppender(final String name) throws IllegalAccessException {
super(name, RegexFilter.createFilter(".*(\n.*)*", new String[0], false, null, null), null);
private MockAppender(final String name, StringLayout layout) throws IllegalAccessException {
super(name, RegexFilter.createFilter(".*(\n.*)*", new String[0], false, null, null), layout);
}

@Override
Expand All @@ -68,25 +109,34 @@ public void append(LogEvent event) {
// we can not keep a reference to the event here because Log4j is using a thread
// local instance under the hood
case "ERROR":
error.add(event.getMessage().getFormattedMessage());
error.add(formatMessage(event));
break;
case "WARN":
warn.add(event.getMessage().getFormattedMessage());
warn.add(formatMessage(event));
break;
case "INFO":
info.add(event.getMessage().getFormattedMessage());
info.add(formatMessage(event));
break;
case "DEBUG":
debug.add(event.getMessage().getFormattedMessage());
debug.add(formatMessage(event));
break;
case "TRACE":
trace.add(event.getMessage().getFormattedMessage());
trace.add(formatMessage(event));
break;
default:
throw invalidLevelException(event.getLevel());
}
}

private String formatMessage(LogEvent event) {
final Layout<?> layout = getLayout();
if (layout instanceof StringLayout) {
return ((StringLayout) layout).toSerializable(event);
} else {
return event.getMessage().getFormattedMessage();
}
}

private IllegalArgumentException invalidLevelException(Level level) {
return new IllegalArgumentException("invalid level, expected [ERROR|WARN|INFO|DEBUG|TRACE] but was [" + level + "]");
}
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugin/security/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ artifacts {
}
sourceSets.test.resources {
srcDir '../core/src/test/resources'
srcDir '../core/src/main/config'
}
dependencyLicenses {
mapping from: /java-support|opensaml-.*/, to: 'shibboleth'
Expand Down
Loading