SyslogFormatter.java
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2013 Cybernetica AS
* Portions copyright 2014-2016 ForgeRock AS.
*/
package org.forgerock.audit.handlers.syslog;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableSet;
import static org.forgerock.audit.events.AuditEventBuilder.*;
import static org.forgerock.audit.events.AuditEventHelper.getAuditEventSchema;
import static org.forgerock.audit.events.AuditEventHelper.jsonPointerToDotNotation;
import static org.forgerock.audit.util.JsonSchemaUtils.generateJsonPointers;
import static org.forgerock.audit.util.JsonValueUtils.extractValueAsString;
import org.forgerock.audit.AuditService;
import org.forgerock.audit.events.EventTopicsMetaData;
import org.forgerock.audit.providers.LocalHostNameProvider;
import org.forgerock.audit.providers.ProductInfoProvider;
import org.forgerock.audit.events.AuditEvent;
import org.forgerock.audit.handlers.syslog.SyslogAuditEventHandlerConfiguration.SeverityFieldMapping;
import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.util.Reject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Responsible for formatting an {@link AuditEvent}'s JSON representation as an RFC-5424 compliant Syslog message.
*
* Objects are immutable and can therefore be freely shared across threads without synchronization.
*
* @see <a href="https://tools.ietf.org/html/rfc5424">RFC-5424</a>
*/
class SyslogFormatter {
private static final Logger logger = LoggerFactory.getLogger(SyslogFormatter.class);
private static final String SYSLOG_SPEC_VERSION = "1";
private static final String NIL_VALUE = "-";
private final Map<String, StructuredDataFormatter> structuredDataFormatters;
private final Map<String, SeverityFieldMapping> severityFieldMappings;
private final String hostname;
private final String appName;
private final String procId;
private final Facility facility;
/**
* Construct a new SyslogFormatter.
*
* @param eventTopicsMetaData Schemas and additional meta-data for known audit event topics.
* @param config Configuration options.
* @param localHostNameProvider Strategy for obtaining hostname of current server.
* @param productInfoProvider Strategy for obtaining name of the hosting application.
*/
public SyslogFormatter(EventTopicsMetaData eventTopicsMetaData, SyslogAuditEventHandlerConfiguration config,
LocalHostNameProvider localHostNameProvider, ProductInfoProvider productInfoProvider) {
Reject.ifNull(localHostNameProvider, "LocalHostNameProvider must not be null");
this.hostname = getLocalHostName(localHostNameProvider);
this.procId = String.valueOf(SyslogFormatter.class.hashCode());
this.appName = getProductName(productInfoProvider);
this.facility = config.getFacility();
this.severityFieldMappings =
createSeverityFieldMappings(config.getSeverityFieldMappings(), eventTopicsMetaData);
this.structuredDataFormatters = Collections.unmodifiableMap(
createStructuredDataFormatters(appName, eventTopicsMetaData));
}
/**
* Translate the provided <code>auditEvent</code> to an RFC-5424 compliant Syslog message.
*
* @param topic The topic of the provided <code>auditEvent</code>.
* @param auditEvent The audit event to be formatted.
*
* @return an RFC-5424 compliant Syslog message.
*
* @throws IllegalArgumentException If this formatter has no meta-data for the specified <code>topic</code>.
*/
public String format(String topic, JsonValue auditEvent) {
Reject.ifFalse(canFormat(topic), "Unknown event topic");
final Severity severity = getSeverityLevel(topic, auditEvent);
final String priority = String.valueOf(calculatePriorityValue(facility, severity));
final String timestamp = auditEvent.get(TIMESTAMP).asString();
final String msgId = auditEvent.get(EVENT_NAME).asString();
final String structuredData = structuredDataFormatters.get(topic).format(auditEvent);
final String msg = "";
return "<" + priority + ">" // https://tools.ietf.org/html/rfc5424#section-6.2.1 PRI
+ SYSLOG_SPEC_VERSION + " " // https://tools.ietf.org/html/rfc5424#section-6.2.2 VERSION
+ timestamp + " " // https://tools.ietf.org/html/rfc5424#section-6.2.3 TIMESTAMP
+ hostname + " " // https://tools.ietf.org/html/rfc5424#section-6.2.4 HOSTNAME
+ appName + " " // https://tools.ietf.org/html/rfc5424#section-6.2.5 APP-NAME
+ procId + " " // https://tools.ietf.org/html/rfc5424#section-6.2.6 PROCID
+ msgId + " " // https://tools.ietf.org/html/rfc5424#section-6.2.7 MSGID
+ structuredData + " " // https://tools.ietf.org/html/rfc5424#section-6.3 STRUCTURED-DATA
+ msg; // https://tools.ietf.org/html/rfc5424#section-6.4 MSG
}
/**
* Returns <code>true</code> if this formatter has been configured to handle events of the specified topic.
*
* @param topic The topic of the <code>auditEvent</code> to be formatted.
*
* @return <code>true</code> if this formatter has been configured to handle events of the specified topic;
* <code>false</code> otherwise.
*/
public boolean canFormat(String topic) {
return structuredDataFormatters.containsKey(topic);
}
private Map<String, SeverityFieldMapping> createSeverityFieldMappings(
List<SeverityFieldMapping> mappings, EventTopicsMetaData eventTopicsMetaData) {
Map<String, SeverityFieldMapping> results = new HashMap<>(mappings.size());
for (SeverityFieldMapping mapping : mappings) {
if (results.containsKey(mapping.getTopic())) {
logger.warn("Multiple Syslog severity field mappings defined for {} topic", mapping.getTopic());
continue;
}
if (!eventTopicsMetaData.containsTopic(mapping.getTopic())) {
logger.warn("Syslog severity field mapping defined for unknown topic {}", mapping.getTopic());
continue;
}
JsonValue auditEventMetaData = eventTopicsMetaData.getSchema(mapping.getTopic());
JsonValue auditEventSchema;
try {
auditEventSchema = getAuditEventSchema(auditEventMetaData);
} catch (ResourceException e) {
logger.warn(e.getMessage());
continue;
}
Set<String> topicFieldPointers = generateJsonPointers(auditEventSchema);
String mappedField = mapping.getField();
if (mappedField != null && !mappedField.startsWith("/")) {
mappedField = "/" + mappedField;
}
if (!topicFieldPointers.contains(mappedField)) {
logger.warn("Syslog severity field mapping for topic {} references unknown field {}",
mapping.getTopic(), mapping.getField());
continue;
}
results.put(mapping.getTopic(), mapping);
}
return results;
}
private Map<String, StructuredDataFormatter> createStructuredDataFormatters(
String productName,
EventTopicsMetaData eventTopicsMetaData) {
final Map<String, StructuredDataFormatter> results = new HashMap<>();
for (String topic : eventTopicsMetaData.getTopics()) {
JsonValue schema = eventTopicsMetaData.getSchema(topic);
results.put(topic, new StructuredDataFormatter(productName, topic, schema));
}
return results;
}
private Severity getSeverityLevel(String topic, JsonValue auditEvent) {
if (severityFieldMappings.containsKey(topic)) {
SeverityFieldMapping severityFieldMapping = severityFieldMappings.get(topic);
String severityField = severityFieldMapping.getField();
if (severityField != null && !severityField.startsWith("/")) {
severityField = "/" + severityField;
}
JsonValue jsonValue = auditEvent.get(new JsonPointer(severityField));
String severityValue = jsonValue == null ? null : jsonValue.asString();
if (severityValue == null) {
logger.debug("{} value not set; defaulting to INFORMATIONAL Syslog SEVERITY level", severityField);
} else {
try {
return Severity.valueOf(severityValue);
} catch (IllegalArgumentException ex) {
logger.debug("{} is not a valid Syslog SEVERITY level; defaulting to INFORMATIONAL", severityValue);
}
}
}
// if no mapping was defined or the value wasn't a valid severity, default to INFORMATIONAL
return Severity.INFORMATIONAL;
}
/**
* Calculates the Syslog message PRI value.
*
* @see <a href="https://tools.ietf.org/html/rfc5424#section-6.2.1">RFC-5424 section 6.2.1</a>
*/
private int calculatePriorityValue(Facility facility, Severity severityLevel) {
return (facility.getCode() * 8) + severityLevel.getCode();
}
/**
* Calculates the Syslog message HOSTNAME value.
*
* @see <a href="https://tools.ietf.org/html/rfc5424#section-6.2.4">RFC-5424 section 6.2.4</a>
*/
private String getLocalHostName(LocalHostNameProvider localHostNameProvider) {
String localHostName = localHostNameProvider.getLocalHostName();
return localHostName != null ? localHostName : NIL_VALUE;
}
/**
* Calculates the Syslog message APP-NAME value.
*
* @see <a href="https://tools.ietf.org/html/rfc5424#section-6.2.5">RFC-5424 section 6.2.5</a>
*/
private String getProductName(ProductInfoProvider productInfoProvider) {
String productName = productInfoProvider.getProductName();
return productName != null ? productName : NIL_VALUE;
}
/**
* Responsible for formatting an {@link AuditEvent}'s JSON representation as an RFC-5424 compliant SD-ELEMENT.
*
* Objects are immutable and can therefore be freely shared across threads without synchronization.
*
* @see <a href="https://tools.ietf.org/html/rfc5424#section-6.3">RFC-5424 section 6.3</a>
*/
private static class StructuredDataFormatter {
private static final String FORGEROCK_IANA_ENTERPRISE_ID = "36733";
/**
* The set of audit event fields that should not be copied to structured-data.
*/
private static final Set<String> IGNORED_FIELDS = unmodifiableSet(
new HashSet<>(asList("_id", TIMESTAMP, EVENT_NAME)));
private final String id;
private final Set<String> fieldNames;
/**
* Construct a new StructuredDataFormatter.
*
* @param productName Name of the ForgeRock product in which the {@link AuditService}
* is executing; the SD-ID of each STRUCTURED-DATA element is derived from the
* <code>productName</code> and <code>topic</code>.
* @param topic Coarse-grained categorisation of the types of audit events that this formatter handles;
* the SD-ID of each STRUCTURED-DATA element is derived from the <code>productName</code>
* and <code>topic</code>.
* @param auditEventMetaData Schema and additional meta-data for the audit event topic.
*/
public StructuredDataFormatter(String productName, String topic, JsonValue auditEventMetaData) {
Reject.ifNull(productName, "Product name required.");
Reject.ifNull(topic, "Audit event topic name required.");
JsonValue auditEventSchema;
try {
auditEventSchema = getAuditEventSchema(auditEventMetaData);
} catch (ResourceException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
id = topic + "." + productName + "@" + FORGEROCK_IANA_ENTERPRISE_ID;
fieldNames = unmodifiableSet(generateJsonPointers(auditEventSchema));
}
/**
* Translate the provided <code>auditEvent</code> to an RFC-5424 compliant SD-ELEMENT.
*
* @param auditEvent The audit event to be formatted.
*
* @return an RFC-5424 compliant SD-ELEMENT.
*/
public String format(JsonValue auditEvent) {
StringBuilder sd = new StringBuilder();
sd.append("[");
sd.append(id);
for (String fieldName : fieldNames) {
String formattedName = formatParamName(fieldName);
if (IGNORED_FIELDS.contains(formattedName)) {
continue;
}
sd.append(" ");
sd.append(formattedName);
sd.append("=\"");
sd.append(formatParamValue(extractValueAsString(auditEvent, fieldName)));
sd.append("\"");
}
sd.append("]");
return sd.toString();
}
private String formatParamName(String name) {
return jsonPointerToDotNotation(name);
}
private String formatParamValue(String value) {
if (value == null) {
return "";
} else {
return value.replaceAll("[\\\\\"\\]]", "\\\\$0");
}
}
}
}