diff --git a/changelog/unreleased/issue-20223.toml b/changelog/unreleased/issue-20223.toml new file mode 100644 index 000000000000..17826640a2e0 --- /dev/null +++ b/changelog/unreleased/issue-20223.toml @@ -0,0 +1,5 @@ +type="a" +message="Added new Palo Alto Networks TCP (PAN-OS v11+) input." + +issues=["20223"] +pulls=["20236"] diff --git a/graylog2-server/src/main/java/org/graylog/integrations/IntegrationsModule.java b/graylog2-server/src/main/java/org/graylog/integrations/IntegrationsModule.java index c1ff3378ab57..0e703cbd64f7 100644 --- a/graylog2-server/src/main/java/org/graylog/integrations/IntegrationsModule.java +++ b/graylog2-server/src/main/java/org/graylog/integrations/IntegrationsModule.java @@ -29,6 +29,8 @@ import org.graylog.integrations.dataadapters.GreyNoiseQuickIPDataAdapter; import org.graylog.integrations.inputs.paloalto.PaloAltoCodec; import org.graylog.integrations.inputs.paloalto.PaloAltoTCPInput; +import org.graylog.integrations.inputs.paloalto11.PaloAlto11xCodec; +import org.graylog.integrations.inputs.paloalto11.PaloAlto11xInput; import org.graylog.integrations.inputs.paloalto9.PaloAlto9xCodec; import org.graylog.integrations.inputs.paloalto9.PaloAlto9xInput; import org.graylog.integrations.ipfix.codecs.IpfixCodec; @@ -171,6 +173,11 @@ private void configureUniversalBindings() { addMessageInput(PaloAlto9xInput.class); addCodec(PaloAlto9xCodec.NAME, PaloAlto9xCodec.class); + // Palo Alto Networks 11x + LOG.debug("Registering message input: {}", PaloAlto11xInput.NAME); + addMessageInput(PaloAlto11xInput.class); + addCodec(PaloAlto11xCodec.NAME, PaloAlto11xCodec.class); + // AWS addCodec(AWSCodec.NAME, AWSCodec.class); addCodec(KinesisCloudWatchFlowLogCodec.NAME, KinesisCloudWatchFlowLogCodec.class); diff --git a/graylog2-server/src/main/java/org/graylog/integrations/inputs/paloalto11/PaloAlto11xCodec.java b/graylog2-server/src/main/java/org/graylog/integrations/inputs/paloalto11/PaloAlto11xCodec.java new file mode 100644 index 000000000000..d3e1d4cd92a8 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/integrations/inputs/paloalto11/PaloAlto11xCodec.java @@ -0,0 +1,185 @@ +/* + * 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.graylog.integrations.inputs.paloalto11; + +import com.google.inject.assistedinject.Assisted; +import com.google.inject.assistedinject.AssistedInject; +import org.graylog.integrations.inputs.paloalto.PaloAltoMessageBase; +import org.graylog.integrations.inputs.paloalto.PaloAltoParser; +import org.graylog.schema.EventFields; +import org.graylog.schema.VendorFields; +import org.graylog2.plugin.Message; +import org.graylog2.plugin.MessageFactory; +import org.graylog2.plugin.ResolvableInetSocketAddress; +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.ConfigClass; +import org.graylog2.plugin.inputs.annotations.FactoryClass; +import org.graylog2.plugin.inputs.codecs.Codec; +import org.graylog2.plugin.inputs.codecs.CodecAggregator; +import org.graylog2.plugin.journal.RawMessage; +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 java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +public class PaloAlto11xCodec implements Codec { + private static final Logger LOG = LoggerFactory.getLogger(PaloAlto11xCodec.class); + static final String CK_STORE_FULL_MESSAGE = "store_full_message"; + static final String CK_TIMEZONE = "timezone"; + + public static final String NAME = "PaloAlto11x"; + public static final String EVENT_SOURCE_PRODUCT_NAME = "PAN"; + public static final String UNKNOWN = "unknown"; + + private final Configuration configuration; + private final MessageFactory messageFactory; + private final PaloAltoParser rawMessageParser; + private final DateTimeZone timezone; + + @AssistedInject + public PaloAlto11xCodec(@Assisted Configuration configuration, PaloAltoParser rawMessageParser, + MessageFactory messageFactory) { + this.configuration = configuration; + this.messageFactory = messageFactory; + String timezoneID = configuration.getString(CK_TIMEZONE); + // previously existing PA inputs after updating will not have a Time Zone configured, default to UTC + this.timezone = timezoneID != null ? DateTimeZone.forID(timezoneID) : DateTimeZone.UTC; + LOG.trace("Configured with time zone: {}", timezone); + this.rawMessageParser = rawMessageParser; + } + + @Nullable + @Override + public Message decode(@Nonnull RawMessage rawMessage) { + String rawMessageString = new String(rawMessage.getPayload(), StandardCharsets.UTF_8); + LOG.trace("Received raw message: {}", rawMessageString); + + PaloAltoMessageBase p = null; + try { + p = rawMessageParser.parse(rawMessageString, timezone); + } catch (Exception e) { + LOG.warn("Cannot parse malformed Palo Alto 11x Message. Leaving message unparsed: {}", rawMessageString); + } + + String payload = rawMessageString; + String source = getRawMessageSource(rawMessage); + DateTime timestamp = DateTime.now(DateTimeZone.UTC); + String panType = UNKNOWN; + if (p != null) { + if (p.payload() != null) { + payload = p.payload(); + } + if (p.source() != null) { + source = p.source(); + } + if (p.timestamp() != null) { + timestamp = p.timestamp(); + } + if (p.panType() != null) { + panType = p.panType(); + } + } + + Message message = messageFactory.createMessage(payload, source, timestamp); + message.addField(EventFields.EVENT_SOURCE_PRODUCT, EVENT_SOURCE_PRODUCT_NAME); + message.addField(VendorFields.VENDOR_SUBTYPE, panType); + // Store full message if configured. + if (configuration.getBoolean(CK_STORE_FULL_MESSAGE)) { + message.addField(Message.FIELD_FULL_MESSAGE, new String(rawMessage.getPayload(), StandardCharsets.UTF_8)); + } + LOG.trace("Successfully processed [{}] message with [{}] fields.", panType, message.getFieldCount()); + return message; + } + + private String getRawMessageSource(RawMessage rawMessage) { + final ResolvableInetSocketAddress address = rawMessage.getRemoteAddress(); + final InetSocketAddress remoteAddress; + if (address == null) { + remoteAddress = null; + } else { + remoteAddress = address.getInetSocketAddress(); + } + + return remoteAddress == null ? UNKNOWN : remoteAddress.getAddress().toString(); + } + + @Override + public String getName() { + return NAME; + } + + @Nonnull + @Override + public Configuration getConfiguration() { + return this.configuration; + } + + @FactoryClass + public interface Factory extends Codec.Factory { + @Override + PaloAlto11xCodec create(Configuration configuration); + + @Override + PaloAlto11xCodec.Config getConfig(); + } + + @ConfigClass + public static class Config implements Codec.Config { + @Override + public ConfigurationRequest getRequestedConfiguration() { + final ConfigurationRequest r = new ConfigurationRequest(); + + r.addField(new DropdownField( + CK_TIMEZONE, + "Time Zone", + DateTimeZone.UTC.getID(), + DropdownField.ValueTemplates.timeZones(), + "Time zone of the Palo Alto device", + ConfigurationField.Optional.OPTIONAL)); + + r.addField( + new BooleanField( + CK_STORE_FULL_MESSAGE, + "Store full message?", + false, + "Store the full original Palo Alto message as full_message?" + ) + ); + + return r; + } + + @Override + public void overrideDefaultValues(@Nonnull ConfigurationRequest cr) { + } + } + + @Nullable + @Override + public CodecAggregator getAggregator() { + return null; + } +} diff --git a/graylog2-server/src/main/java/org/graylog/integrations/inputs/paloalto11/PaloAlto11xInput.java b/graylog2-server/src/main/java/org/graylog/integrations/inputs/paloalto11/PaloAlto11xInput.java new file mode 100644 index 000000000000..a61243637382 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/integrations/inputs/paloalto11/PaloAlto11xInput.java @@ -0,0 +1,80 @@ +/* + * 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.graylog.integrations.inputs.paloalto11; + +import com.codahale.metrics.MetricRegistry; +import com.google.inject.assistedinject.Assisted; +import jakarta.inject.Inject; +import org.graylog2.inputs.transports.SyslogTcpTransport; +import org.graylog2.plugin.LocalMetricRegistry; +import org.graylog2.plugin.ServerStatus; +import org.graylog2.plugin.configuration.Configuration; +import org.graylog2.plugin.inputs.MessageInput; +import org.graylog2.plugin.inputs.annotations.ConfigClass; +import org.graylog2.plugin.inputs.annotations.FactoryClass; + +public class PaloAlto11xInput extends MessageInput { + + public static final String NAME = "Palo Alto Networks TCP (PAN-OS v11+)"; + + @Inject + public PaloAlto11xInput(@Assisted Configuration configuration, + MetricRegistry metricRegistry, + SyslogTcpTransport.Factory transport, + LocalMetricRegistry localRegistry, + PaloAlto11xCodec.Factory codec, + PaloAlto11xInput.Config config, + PaloAlto11xInput.Descriptor descriptor, + ServerStatus serverStatus) { + super( + metricRegistry, + configuration, + transport.create(configuration), + localRegistry, + codec.create(configuration), + config, + descriptor, + serverStatus); + } + + @FactoryClass + public interface Factory extends MessageInput.Factory { + @Override + PaloAlto11xInput create(Configuration configuration); + + @Override + PaloAlto11xInput.Config getConfig(); + + @Override + PaloAlto11xInput.Descriptor getDescriptor(); + } + + public static class Descriptor extends MessageInput.Descriptor { + public Descriptor() { + super(NAME, false, ""); + } + } + + @ConfigClass + public static class Config extends MessageInput.Config { + + @Inject + public Config(SyslogTcpTransport.Factory transport, PaloAlto11xCodec.Factory codec) { + super(transport.getConfig(), codec.getConfig()); + } + } +} diff --git a/graylog2-server/src/test/java/org/graylog/integrations/inputs/paloalto11/PaloAlto11xCodecTest.java b/graylog2-server/src/test/java/org/graylog/integrations/inputs/paloalto11/PaloAlto11xCodecTest.java new file mode 100644 index 000000000000..2151f9524f6d --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog/integrations/inputs/paloalto11/PaloAlto11xCodecTest.java @@ -0,0 +1,134 @@ +/* + * 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.graylog.integrations.inputs.paloalto11; + +import com.google.common.collect.ImmutableList; +import org.graylog.integrations.inputs.paloalto.PaloAltoMessageBase; +import org.graylog.integrations.inputs.paloalto.PaloAltoParser; +import org.graylog.schema.EventFields; +import org.graylog.schema.VendorFields; +import org.graylog2.plugin.Message; +import org.graylog2.plugin.MessageFactory; +import org.graylog2.plugin.TestMessageFactory; +import org.graylog2.plugin.configuration.Configuration; +import org.graylog2.plugin.journal.RawMessage; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.nio.charset.StandardCharsets; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + +@RunWith(MockitoJUnitRunner.class) +public class PaloAlto11xCodecTest { + private static final String TEST_SOURCE = "Test Source"; + private static final DateTime TEST_DATE_TIME = DateTime.now(DateTimeZone.UTC); + private static final String TEST_RAW_MESSAGE = "Foo,Bar,Baz,This,That,GLOBALPROTECT"; + private static final ImmutableList TEST_FIELD_LIST = ImmutableList.of("Foo", "Bar", "Baz", "Three", "Four", "GLOBALPROTECT"); + private final MessageFactory messageFactory = new TestMessageFactory(); + + // Code Under Test + PaloAlto11xCodec cut; + + // Mock Objects + @Mock + Configuration mockConfig; + @Mock + PaloAltoParser mockRawParser; + + // Test Objects + RawMessage in; + Message out; + + @Before + public void setUp() throws Exception { + this.cut = new PaloAlto11xCodec(mockConfig, mockRawParser, messageFactory); + } + + @Test + public void decode_runsSuccessfully_whenGoodInput() { + givenGoodInputRawMessage(); + givenRawParserReturnsValidMessage(); + givenStoreFullMessage(false); + whenDecodeIsCalled(); + thenOutputMessageContainsExpectedFields(false); + } + + @Test + public void decode_returnsDefaults_whenRawPaloParseFails() { + givenGoodInputRawMessage(); + givenRawParserReturnsNull(); + whenDecodeIsCalled(); + thenOutputMessageIsDefaults(); + } + + // GIVENs + private void givenGoodInputRawMessage() { + in = new RawMessage(TEST_RAW_MESSAGE.getBytes(StandardCharsets.UTF_8)); + } + + private void givenRawParserReturnsValidMessage() { + PaloAltoMessageBase foo = PaloAltoMessageBase.create(TEST_SOURCE, TEST_DATE_TIME, TEST_RAW_MESSAGE, "SYSTEM", + TEST_FIELD_LIST); + given(mockRawParser.parse(anyString(), any(DateTimeZone.class))).willReturn(foo); + } + + private void givenRawParserReturnsNull() { + given(mockRawParser.parse(anyString(), any(DateTimeZone.class))).willReturn(null); + } + + private void givenStoreFullMessage(boolean storeFullMessage) { + given(mockConfig.getBoolean(PaloAlto11xCodec.CK_STORE_FULL_MESSAGE)).willReturn(storeFullMessage); + } + + // WHENs + private void whenDecodeIsCalled() { + out = cut.decode(in); + } + + private void thenOutputMessageContainsExpectedFields(boolean shouldContainFullMessage) { + assertNotNull(out); + assertThat(out.getField(EventFields.EVENT_SOURCE_PRODUCT), is(PaloAlto11xCodec.EVENT_SOURCE_PRODUCT_NAME)); + assertThat(out.getField(VendorFields.VENDOR_SUBTYPE), is("SYSTEM")); + + if (shouldContainFullMessage) { + assertThat(out.getField(Message.FIELD_FULL_MESSAGE), is(TEST_RAW_MESSAGE)); + } else { + assertThat(out.getField(Message.FIELD_FULL_MESSAGE), nullValue()); + } + } + + private void thenOutputMessageIsDefaults() { + assertNotNull(out); + assertThat(out.getField(EventFields.EVENT_SOURCE_PRODUCT), is(PaloAlto11xCodec.EVENT_SOURCE_PRODUCT_NAME)); + assertThat(out.getField(VendorFields.VENDOR_SUBTYPE), is(PaloAlto11xCodec.UNKNOWN)); + assertThat(out.getField(Message.FIELD_MESSAGE), is(TEST_RAW_MESSAGE)); + assertThat(out.getField(Message.FIELD_TIMESTAMP), notNullValue()); + } +}