All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.graylog2.inputs.codecs.SyslogCodec Maven / Gradle / Ivy

There is a newer version: 6.1.4
Show newest version
/*
 * Copyright (C) 2020 Graylog, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Server Side Public License, version 1,
 * as published by MongoDB, Inc.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * Server Side Public License for more details.
 *
 * You should have received a copy of the Server Side Public License
 * along with this program. If not, see
 * .
 */
package org.graylog2.inputs.codecs;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.net.InetAddresses;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import org.apache.commons.lang3.StringUtils;
import org.graylog2.plugin.Message;
import org.graylog2.plugin.ResolvableInetSocketAddress;
import org.graylog2.plugin.Tools;
import org.graylog2.plugin.configuration.Configuration;
import org.graylog2.plugin.configuration.ConfigurationRequest;
import org.graylog2.plugin.configuration.fields.BooleanField;
import org.graylog2.plugin.configuration.fields.ConfigurationField;
import org.graylog2.plugin.configuration.fields.DropdownField;
import org.graylog2.plugin.inputs.annotations.Codec;
import org.graylog2.plugin.inputs.annotations.ConfigClass;
import org.graylog2.plugin.inputs.annotations.FactoryClass;
import org.graylog2.plugin.inputs.codecs.AbstractCodec;
import org.graylog2.plugin.inputs.codecs.CodecAggregator;
import org.graylog2.plugin.inputs.transports.NettyTransport;
import org.graylog2.plugin.journal.RawMessage;
import org.graylog2.syslog4j.server.SyslogServerEventIF;
import org.graylog2.syslog4j.server.impl.event.CiscoSyslogServerEvent;
import org.graylog2.syslog4j.server.impl.event.FortiGateSyslogEvent;
import org.graylog2.syslog4j.server.impl.event.SyslogServerEvent;
import org.graylog2.syslog4j.server.impl.event.structured.StructuredSyslogServerEvent;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import static com.codahale.metrics.MetricRegistry.name;
import static com.google.common.base.Strings.isNullOrEmpty;

@Codec(name = "syslog", displayName = "Syslog")
public class SyslogCodec extends AbstractCodec {
    private static final Logger LOG = LoggerFactory.getLogger(SyslogCodec.class);
    public static final Pattern STRUCTURED_SYSLOG_PATTERN = Pattern.compile("<\\d{1,3}>[0-9]\\d{0,2}\\s.*", Pattern.DOTALL);
    private static final Pattern CISCO_WITH_SEQUENCE_NUMBERS_PATTERN = Pattern.compile("<\\d{1,3}>\\d*:\\s.*", Pattern.DOTALL);
    private static final Pattern FORTIGATE_PATTERN = Pattern.compile("<\\d{1,3}>date=.*", Pattern.DOTALL);

    static final String CK_FORCE_RDNS = "force_rdns";
    static final String CK_ALLOW_OVERRIDE_DATE = "allow_override_date";
    static final String CK_EXPAND_STRUCTURED_DATA = "expand_structured_data";
    static final String CK_STORE_FULL_MESSAGE = "store_full_message";
    static final String CK_TIMEZONE = "timezone";

    private final Timer resolveTime;
    private final Timer decodeTime;

    @AssistedInject
    public SyslogCodec(@Assisted Configuration configuration, MetricRegistry metricRegistry) {
        super(configuration);
        this.resolveTime = metricRegistry.timer(name(SyslogCodec.class, "resolveTime"));
        this.decodeTime = metricRegistry.timer(name(SyslogCodec.class, "decodeTime"));
    }

    @Nullable
    @Override
    public Message decode(@Nonnull RawMessage rawMessage) {
        final String msg = new String(rawMessage.getPayload(), charset);
        try (Timer.Context ignored = this.decodeTime.time()) {
            final ResolvableInetSocketAddress address = rawMessage.getRemoteAddress();
            final InetSocketAddress remoteAddress;
            if (address == null) {
                remoteAddress = null;
            } else {
                remoteAddress = address.getInetSocketAddress();
            }
            return parse(msg, remoteAddress == null ? null : remoteAddress.getAddress(), rawMessage.getTimestamp());
        }
    }

    private Message parse(String msg, InetAddress remoteAddress, DateTime receivedTimestamp) {
        /*
         * ZOMG funny 80s neckbeard protocols. We are now deciding if to parse
         * structured (RFC5424) or unstructured (classic BSD, RFC3164) syslog
         * by checking if there is a VERSION after the PRI. Sorry.
         *
         *                            ._.                                  _
         *    R-O-F-L-R-O-F-L-R-O-F-L-IOI-R-O-F-L-R-O-F-L-R-O-F-L         / l
         *                ___________/LOL\____                           /: ]
         *            .__/°         °\___/°   \                         / ::\
         *           /^^ \            °  °     \_______.__________.____/: OO:\
         *      .__./     j      ________             _________________ ::OO::|
         *    ./ ^^ j____/°     [\______/]      .____/                 \__:__/
         *  ._|____/°    °       <{(OMG{<       /                         ::
         * /  °    °              (OMFG{       /
         * |°  loooooooooooooooooooooooooooooooool
         *         °L|                   L|
         *          ()                   ()
         *
         *
         *  http://open.spotify.com/track/2ZtQKBB8wDTtPPqDZhy7xZ
         *
         */
        String timeZoneConfig = configuration.getString(CK_TIMEZONE);
        final SyslogServerEventIF e;
        boolean isDefaultTimeZoneSet = StringUtils.isNotBlank(timeZoneConfig) && !DropdownField.NOT_CONFIGURED.equals(timeZoneConfig);
        DateTimeZone defaultTimeZone = isDefaultTimeZoneSet ? DateTimeZone.forID(timeZoneConfig) : null;
        if (STRUCTURED_SYSLOG_PATTERN.matcher(msg).matches()) {
            e = new StructuredSyslogServerEvent(msg, remoteAddress, defaultTimeZone);
        } else if (CISCO_WITH_SEQUENCE_NUMBERS_PATTERN.matcher(msg).matches()) {
            e = new CiscoSyslogServerEvent(msg, remoteAddress, defaultTimeZone);
        } else if (FORTIGATE_PATTERN.matcher(msg).matches()) {
            e = new FortiGateSyslogEvent(msg, defaultTimeZone);
        } else {
            e = new SyslogServerEvent(msg, remoteAddress, defaultTimeZone);
        }

        // If the message is a structured one, we do not want the message ID and the structured data in the
        // message string. See: https://github.com/Graylog2/graylog2-server/issues/845#issuecomment-69499719
        final String syslogMessage;
        if (e instanceof StructuredSyslogServerEvent) {
            final String structMessage = ((StructuredSyslogServerEvent) e).getStructuredMessage().getMessage();
            syslogMessage = isNullOrEmpty(structMessage) ? e.getMessage() : structMessage;
        } else {
            syslogMessage = e.getMessage();
        }

        final Message m = new Message(syslogMessage, parseHost(e, remoteAddress), parseDate(e, receivedTimestamp));
        m.addField("facility", Tools.syslogFacilityToReadable(e.getFacility()));
        m.addField("level", e.getLevel());
        m.addField("facility_num", e.getFacility());

        // I can haz pattern matching?
        if (e instanceof CiscoSyslogServerEvent) {
            m.addField("sequence_number", ((CiscoSyslogServerEvent) e).getSequenceNumber());
        }
        if (e instanceof FortiGateSyslogEvent) {
            final HashMap fields = new HashMap<>(((FortiGateSyslogEvent) e).getFields());
            // The FortiGate "level" field is a string, Graylog requires a numeric value.
            fields.remove("level");
            m.addFields(fields);
        }

        // Store full message if configured.
        if (configuration.getBoolean(CK_STORE_FULL_MESSAGE)) {
            m.addField("full_message", new String(e.getRaw(), charset));
        }


        final boolean expandStructuredData = configuration.getBoolean(CK_EXPAND_STRUCTURED_DATA);
        m.addFields(parseAdditionalData(e, expandStructuredData));

        return m;
    }

    private Map parseAdditionalData(SyslogServerEventIF msg, boolean expand) {

        // Structured syslog has more data we can parse.
        if (msg instanceof StructuredSyslogServerEvent) {
            final StructuredSyslogServerEvent sMsg = (StructuredSyslogServerEvent) msg;
            final Map structuredData = new HashMap<>(extractFields(sMsg, expand));

            if (!isNullOrEmpty(sMsg.getApplicationName())) {
                structuredData.put("application_name", sMsg.getApplicationName());
            }

            if (!isNullOrEmpty(sMsg.getProcessId())) {
                structuredData.put("process_id", sMsg.getProcessId());
            }

            return structuredData;
        } else {
            return Collections.emptyMap();
        }
    }

    private String parseHost(SyslogServerEventIF msg, InetAddress remoteAddress) {
        if (remoteAddress != null && configuration.getBoolean(CK_FORCE_RDNS)) {
            try (Timer.Context ignored = this.resolveTime.time()) {
                return Tools.rdnsLookup(remoteAddress);
            } catch (UnknownHostException e) {
                LOG.warn("Reverse DNS lookup failed. Falling back to parsed hostname.", e);
            }
        }

        final String host = msg.getHost();
        return isNullOrEmpty(host) && remoteAddress != null ? InetAddresses.toAddrString(remoteAddress) : host;
    }

    private DateTime parseDate(SyslogServerEventIF msg, DateTime receivedTimestamp) throws IllegalStateException {
        // Check if date could be parsed.
        Date sysLogDate = msg.getDate();
        if (sysLogDate == null) {
            if (configuration.getBoolean(CK_ALLOW_OVERRIDE_DATE)) {
                LOG.debug("Date could not be parsed. Was set to NOW because {} is true.", CK_ALLOW_OVERRIDE_DATE);
                return receivedTimestamp;
            } else {
                LOG.warn("Syslog message is missing date or date could not be parsed. (Possibly set {} to true) "
                                + "Not further handling. Message was: {}",
                        CK_ALLOW_OVERRIDE_DATE, new String(msg.getRaw(), charset));
                throw new IllegalStateException("Syslog message is missing date or date could not be parsed.");
            }
        }

        return new DateTime(sysLogDate);
    }

    @Nullable
    @Override
    public CodecAggregator getAggregator() {
        return null;
    }

    @FactoryClass
    public interface Factory extends AbstractCodec.Factory {
        @Override
        SyslogCodec create(Configuration configuration);

        @Override
        Config getConfig();

        @Override
        Descriptor getDescriptor();
    }

    @ConfigClass
    public static class Config extends AbstractCodec.Config {
        @Override
        public ConfigurationRequest getRequestedConfiguration() {
            final ConfigurationRequest r = super.getRequestedConfiguration();

            r.addField(
                    new BooleanField(
                            CK_FORCE_RDNS,
                            "Force rDNS?",
                            false,
                            "Force rDNS resolution of hostname? Use if hostname cannot be parsed. (Be careful if you are sending DNS logs into this input because it can cause a feedback loop.)"
                    )
            );

            r.addField(
                    new BooleanField(
                            CK_ALLOW_OVERRIDE_DATE,
                            "Allow overriding date?",
                            true,
                            "Allow to override with current date if date could not be parsed?"
                    )
            );

            r.addField(
                    new BooleanField(
                            CK_STORE_FULL_MESSAGE,
                            "Store full message?",
                            false,
                            "Store the full original syslog message as full_message?"
                    )
            );

            r.addField(
                    new BooleanField(
                            CK_EXPAND_STRUCTURED_DATA,
                            "Expand structured data?",
                            false,
                            "Expand structured data elements by prefixing attributes with their SD-ID?"
                    )
            );

            r.addField(new DropdownField(
                    CK_TIMEZONE,
                    "Time Zone",
                    DropdownField.NOT_CONFIGURED,
                    DropdownField.ValueTemplates.timeZones(true),
                    "Default time zone used when no timezone detected",
                    ConfigurationField.Optional.OPTIONAL));

            return r;
        }

        @Override
        public void overrideDefaultValues(@Nonnull ConfigurationRequest cr) {
            if (cr.containsField(NettyTransport.CK_PORT)) {
                cr.getField(NettyTransport.CK_PORT).setDefaultValue(514);
            }
        }
    }

    public static class Descriptor extends AbstractCodec.Descriptor {
        @Inject
        public Descriptor() {
            super(SyslogCodec.class.getAnnotation(Codec.class).displayName());
        }
    }

    @SuppressWarnings("unchecked")
    @VisibleForTesting
    Map extractFields(final StructuredSyslogServerEvent msg, final boolean expand) {
        try {
            final Map> raw = msg.getStructuredMessage().getStructuredData();

            if (raw != null && !raw.isEmpty()) {
                final Map fields = new HashMap<>();
                for (Map.Entry> entry : raw.entrySet()) {
                    if (expand) {
                        fields.putAll(prefixElements(entry.getKey(), entry.getValue()));
                    } else {
                        fields.putAll(entry.getValue());
                    }
                }
                return fields;
            }
        } catch (Exception e) {
            LOG.debug("Could not extract structured syslog", e);
        }
        return Collections.emptyMap();
    }

    private Map prefixElements(final String prefix, final Map elements) {
        if (elements == null || elements.isEmpty()) {
            return Collections.emptyMap();
        }

        final Map prefixedMap = new HashMap<>(elements.size());
        for (Map.Entry entry : elements.entrySet()) {
            prefixedMap.put(prefix.trim() + "_" + entry.getKey(), entry.getValue());
        }

        return prefixedMap;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy