From 29ad48e7b40c8400b0d04521235f32b6b20c7c7e Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Tue, 13 Aug 2019 10:47:53 -0400 Subject: [PATCH 01/39] Update jackson dependencies (#366) * Update jackson dependencies Also updates docker setup to fix the build, and removes non-existent 6.x snapshot test * Update changelog and bump version --- .travis.yml | 1 - CHANGELOG.md | 3 +++ VERSION | 2 +- build.gradle | 8 ++++---- ci/unit/docker-setup.sh | 22 +++++++++++++--------- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 744332f6..6df504c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ matrix: - env: SNAPSHOT=true ELASTIC_STACK_VERSION=8.x - env: SNAPSHOT=true ELASTIC_STACK_VERSION=7.x - env: ELASTIC_STACK_VERSION=7.x - - env: SNAPSHOT=true ELASTIC_STACK_VERSION=6.x - env: ELASTIC_STACK_VERSION=6.x - env: ELASTIC_STACK_VERSION=5.x fast_finish: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 21634696..ad98ff1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.0.1 + - Updated Jackson dependencies [#366](https://github.com/logstash-plugins/logstash-input-beats/pull/366) + ## 6.0.0 - Removed obsolete setting congestion_threshold and target_field_for_codec - Changed default value of `add_hostname` to false diff --git a/VERSION b/VERSION index 09b254e9..5fe60723 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.0 +6.0.1 diff --git a/build.gradle b/build.gradle index 719f0429..954d70ac 100644 --- a/build.gradle +++ b/build.gradle @@ -25,10 +25,10 @@ dependencies { compile 'io.netty:netty-all:4.1.30.Final' compile 'io.netty:netty-tcnative-boringssl-static:2.0.12.Final' compile 'org.javassist:javassist:3.24.0-GA' - compile 'com.fasterxml.jackson.core:jackson-core:2.9.7' - compile 'com.fasterxml.jackson.core:jackson-annotations:2.9.7' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.7' - compile 'com.fasterxml.jackson.module:jackson-module-afterburner:2.9.7' + compile 'com.fasterxml.jackson.core:jackson-core:2.9.9' + compile 'com.fasterxml.jackson.core:jackson-annotations:2.9.9' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.3' + compile 'com.fasterxml.jackson.module:jackson-module-afterburner:2.9.9' compile 'org.apache.logging.log4j:log4j-api:2.11.1' } diff --git a/ci/unit/docker-setup.sh b/ci/unit/docker-setup.sh index b9ec5ae0..5ad8841b 100755 --- a/ci/unit/docker-setup.sh +++ b/ci/unit/docker-setup.sh @@ -24,16 +24,20 @@ if [ "$ELASTIC_STACK_VERSION" ]; then fi echo "Testing against version: $ELASTIC_STACK_VERSION" - if [[ "$ELASTIC_STACK_VERSION" = *"-SNAPSHOT" ]]; then - cd /tmp - wget https://snapshots.elastic.co/docker/logstash-"$ELASTIC_STACK_VERSION".tar.gz - tar xfvz logstash-"$ELASTIC_STACK_VERSION".tar.gz repositories - echo "Loading docker image: " - cat repositories - docker load < logstash-"$ELASTIC_STACK_VERSION".tar.gz - rm logstash-"$ELASTIC_STACK_VERSION".tar.gz - cd - + cd /tmp + + jq=".build.projects.logstash.packages.\"logstash-$ELASTIC_STACK_VERSION-docker-image.tar.gz\".url" + echo "curl --silent https://artifacts-api.elastic.co/v1/versions/$ELASTIC_STACK_VERSION/builds/latest | jq -r $jq)" + result=$(curl --silent https://artifacts-api.elastic.co/v1/versions/$ELASTIC_STACK_VERSION/builds/latest | jq -r $jq) + echo $result + curl $result > logstash-docker-image.tar.gz + tar xfvz logstash-docker-image.tar.gz repositories + echo "Loading docker image: " + cat repositories + docker load < logstash-docker-image.tar.gz + rm logstash-docker-image.tar.gz + cd - fi if [ -f Gemfile.lock ]; then From 777acb6c1944ec43faebb26152e2b7749e38fd15 Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Wed, 11 Sep 2019 11:24:24 -0400 Subject: [PATCH 02/39] Improve handling of invalid compressed content Also adds frame version checks when reading headers. Fixes #368 --- .../java/org/logstash/beats/BeatsParser.java | 56 +++++++++++-------- .../beats/InvalidFrameProtocolException.java | 7 +++ .../java/org/logstash/beats/Protocol.java | 10 ++++ .../org/logstash/beats/BeatsParserTest.java | 29 +++++++++- .../java/org/logstash/beats/ProtocolTest.java | 23 ++++++-- 5 files changed, 92 insertions(+), 33 deletions(-) create mode 100644 src/main/java/org/logstash/beats/InvalidFrameProtocolException.java diff --git a/src/main/java/org/logstash/beats/BeatsParser.java b/src/main/java/org/logstash/beats/BeatsParser.java index 3e4b6bfe..ac87cab7 100644 --- a/src/main/java/org/logstash/beats/BeatsParser.java +++ b/src/main/java/org/logstash/beats/BeatsParser.java @@ -9,6 +9,7 @@ import org.apache.logging.log4j.Logger; +import java.io.IOException; import java.nio.charset.Charset; import java.util.HashMap; import java.util.List; @@ -43,10 +44,14 @@ private enum States { private States currentState = States.READ_HEADER; private int requiredBytes = 0; private int sequence = 0; + private boolean decodingCompressedBuffer = false; @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { if(!hasEnoughBytes(in)) { + if (decodingCompressedBuffer){ + throw new InvalidFrameProtocolException("Insufficient bytes in compressed content to decode: " + currentState); + } return; } @@ -54,15 +59,16 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t case READ_HEADER: { logger.trace("Running: READ_HEADER"); - byte currentVersion = in.readByte(); + int version = Protocol.version(in.readByte()); + if (batch == null) { - if (Protocol.isVersion2(currentVersion)) { + if (version == 2) { batch = new V2Batch(); logger.trace("Frame version 2 detected"); - } else { + } else if (version == 1) { logger.trace("Frame version 1 detected"); batch = new V1Batch(); - } + } else throw new InvalidFrameProtocolException("Invalid frame version detected: " + version); } transition(States.READ_FRAME_TYPE); break; @@ -173,22 +179,19 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t case READ_COMPRESSED_FRAME: { logger.trace("Running: READ_COMPRESSED_FRAME"); // Use the compressed size as the safe start for the buffer. - ByteBuf buffer = ctx.alloc().buffer(requiredBytes); - try ( - ByteBufOutputStream buffOutput = new ByteBufOutputStream(buffer); - InflaterOutputStream inflater = new InflaterOutputStream(buffOutput, new Inflater()) - ) { - in.readBytes(inflater, requiredBytes); - transition(States.READ_HEADER); - try { - while (buffer.readableBytes() > 0) { - decode(ctx, buffer, out); - } - } finally { - buffer.release(); + ByteBuf buffer = inflateCompressedFrame(ctx, in); + transition(States.READ_HEADER); + + decodingCompressedBuffer = true; + try { + while (buffer.readableBytes() > 0) { + decode(ctx, buffer, out); } + } finally { + decodingCompressedBuffer = false; + buffer.release(); + transition(States.READ_HEADER); } - break; } case READ_JSON: { @@ -208,6 +211,17 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t } } + private ByteBuf inflateCompressedFrame(final ChannelHandlerContext ctx, final ByteBuf in) throws IOException { + ByteBuf buffer = ctx.alloc().buffer(requiredBytes); + try ( + ByteBufOutputStream buffOutput = new ByteBufOutputStream(buffer); + InflaterOutputStream inflater = new InflaterOutputStream(buffOutput, new Inflater()) + ) { + in.readBytes(inflater, requiredBytes); + } + return buffer; + } + private boolean hasEnoughBytes(ByteBuf in) { return in.readableBytes() >= requiredBytes; } @@ -230,10 +244,4 @@ private void batchComplete() { batch = null; } - public class InvalidFrameProtocolException extends Exception { - InvalidFrameProtocolException(String message) { - super(message); - } - } - } diff --git a/src/main/java/org/logstash/beats/InvalidFrameProtocolException.java b/src/main/java/org/logstash/beats/InvalidFrameProtocolException.java new file mode 100644 index 00000000..d25187c9 --- /dev/null +++ b/src/main/java/org/logstash/beats/InvalidFrameProtocolException.java @@ -0,0 +1,7 @@ +package org.logstash.beats; + +public class InvalidFrameProtocolException extends Exception { + InvalidFrameProtocolException(String message) { + super(message); + } +} diff --git a/src/main/java/org/logstash/beats/Protocol.java b/src/main/java/org/logstash/beats/Protocol.java index 2efd416b..c53d8e57 100644 --- a/src/main/java/org/logstash/beats/Protocol.java +++ b/src/main/java/org/logstash/beats/Protocol.java @@ -1,5 +1,6 @@ package org.logstash.beats; + /** * Created by ph on 2016-05-16. */ @@ -12,6 +13,15 @@ public class Protocol { public static final byte CODE_COMPRESSED_FRAME = 'C'; public static final byte CODE_FRAME = 'D'; + public static int version(byte versionRead) throws InvalidFrameProtocolException{ + if(Protocol.VERSION_2 == versionRead){ + return 2; + } else if (Protocol.VERSION_1 == versionRead){ + return 1; + } + throw new InvalidFrameProtocolException("Invalid version of beats protocol: " + versionRead); + } + public static boolean isVersion2(byte versionRead) { if(Protocol.VERSION_2 == versionRead){ return true; diff --git a/src/test/java/org/logstash/beats/BeatsParserTest.java b/src/test/java/org/logstash/beats/BeatsParserTest.java index 54ccbd7e..de760132 100644 --- a/src/test/java/org/logstash/beats/BeatsParserTest.java +++ b/src/test/java/org/logstash/beats/BeatsParserTest.java @@ -126,7 +126,7 @@ public void testCompressedEncodingDecodingFields() { @Test public void testShouldNotCrashOnGarbageData() { - thrown.expectCause(isA(BeatsParser.InvalidFrameProtocolException.class)); + thrown.expectCause(isA(InvalidFrameProtocolException.class)); byte[] n = new byte[10000]; new Random().nextBytes(n); @@ -146,6 +146,27 @@ public void testZeroSizeJsonPayloadShouldRaiseAnException() throws JsonProcessin sendInvalidJSonPayload(0); } + + @Test + public void testInvalidCompression() throws JsonProcessingException { + thrown.expectCause(isA(InvalidFrameProtocolException.class)); + thrown.expectMessage("Insufficient bytes in compressed content"); + ByteBuf payload = Unpooled.buffer(); + + payload.writeByte(Protocol.VERSION_2); + payload.writeByte('W'); + payload.writeInt(1); + payload.writeByte(Protocol.VERSION_2); + payload.writeByte('C'); + payload.writeInt(9); + + int[] next = {0x78,0x9c,0x33,0xf2,0x62,0x60,0x60,0x60,0x04,0x62,0x66,0x17,0xff,0x60,0x00,0x07,0xe0,0x01,0x67}; + for (int n : next){ + payload.writeByte(n); + } + sendPayloadToParser(payload); + } + @Test public void testNegativeFieldsCountShouldRaiseAnException() { sendInvalidV1Payload(-1); @@ -157,7 +178,7 @@ public void testZeroFieldsCountShouldRaiseAnException() { } private void sendInvalidV1Payload(int size) { - thrown.expectCause(isA(BeatsParser.InvalidFrameProtocolException.class)); + thrown.expectCause(isA(InvalidFrameProtocolException.class)); thrown.expectMessage("Invalid number of fields, received: " + size); ByteBuf payload = Unpooled.buffer(); @@ -182,7 +203,7 @@ private void sendInvalidV1Payload(int size) { } private void sendInvalidJSonPayload(int size) throws JsonProcessingException { - thrown.expectCause(isA(BeatsParser.InvalidFrameProtocolException.class)); + thrown.expectCause(isA(InvalidFrameProtocolException.class)); thrown.expectMessage("Invalid json length, received: " + size); Map mapData = Collections.singletonMap("message", "hola"); @@ -203,6 +224,8 @@ private void sendInvalidJSonPayload(int size) throws JsonProcessingException { sendPayloadToParser(payload); } + + private void sendPayloadToParser(ByteBuf payload) { EmbeddedChannel channel = new EmbeddedChannel(new BeatsParser()); channel.writeOutbound(payload); diff --git a/src/test/java/org/logstash/beats/ProtocolTest.java b/src/test/java/org/logstash/beats/ProtocolTest.java index beae935c..f1b1eab6 100644 --- a/src/test/java/org/logstash/beats/ProtocolTest.java +++ b/src/test/java/org/logstash/beats/ProtocolTest.java @@ -1,17 +1,28 @@ package org.logstash.beats; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - +import static org.hamcrest.Matchers.isA; +import static org.junit.Assert.assertEquals; /** * Created by ph on 2016-06-01. */ public class ProtocolTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void validVersionTest() throws InvalidFrameProtocolException{ + assertEquals(1, Protocol.version((byte) '1')); + assertEquals(2, Protocol.version((byte) '2')); + } + @Test - public void isVersion2Test() { - assertTrue(Protocol.isVersion2((byte) '2')); - assertFalse(Protocol.isVersion2((byte) '1')); + public void invalidVersionTest() throws InvalidFrameProtocolException{ + thrown.expectCause(isA(InvalidFrameProtocolException.class)); + Protocol.version((byte) '3'); } } From 97930252a5874ae942dff2a4a4e9501f972605de Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Wed, 11 Sep 2019 11:53:58 -0400 Subject: [PATCH 03/39] Fix expected exception unit test Fixes #368 --- src/test/java/org/logstash/beats/ProtocolTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/logstash/beats/ProtocolTest.java b/src/test/java/org/logstash/beats/ProtocolTest.java index f1b1eab6..f65d9548 100644 --- a/src/test/java/org/logstash/beats/ProtocolTest.java +++ b/src/test/java/org/logstash/beats/ProtocolTest.java @@ -22,7 +22,7 @@ public void validVersionTest() throws InvalidFrameProtocolException{ @Test public void invalidVersionTest() throws InvalidFrameProtocolException{ - thrown.expectCause(isA(InvalidFrameProtocolException.class)); + thrown.expect(isA(InvalidFrameProtocolException.class)); Protocol.version((byte) '3'); } } From 90d08d2e9864d9c67073d3ef4d8107bbd78cdc62 Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Wed, 11 Sep 2019 12:05:50 -0400 Subject: [PATCH 04/39] Removed redundant error check Fixes #368 --- src/main/java/org/logstash/beats/BeatsParser.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/logstash/beats/BeatsParser.java b/src/main/java/org/logstash/beats/BeatsParser.java index ac87cab7..fa099e74 100644 --- a/src/main/java/org/logstash/beats/BeatsParser.java +++ b/src/main/java/org/logstash/beats/BeatsParser.java @@ -65,10 +65,10 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t if (version == 2) { batch = new V2Batch(); logger.trace("Frame version 2 detected"); - } else if (version == 1) { + } else { logger.trace("Frame version 1 detected"); batch = new V1Batch(); - } else throw new InvalidFrameProtocolException("Invalid frame version detected: " + version); + } } transition(States.READ_FRAME_TYPE); break; From 78782f391bdc3c05f3ad785f382fa84d0988e976 Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Wed, 11 Sep 2019 12:25:53 -0400 Subject: [PATCH 05/39] Version bump and changelog entry Fixes #368 --- CHANGELOG.md | 3 +++ VERSION | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad98ff1c..77f15c7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.0.2 + - Improved handling of invalid compressed content [#368](https://github.com/logstash-plugins/logstash-input-beats/pull/368) + ## 6.0.1 - Updated Jackson dependencies [#366](https://github.com/logstash-plugins/logstash-input-beats/pull/366) diff --git a/VERSION b/VERSION index 5fe60723..9b9a2442 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.1 +6.0.2 From 2202a5eae54689d4fb23876e0b23c74c1866a8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Duarte?= Date: Wed, 25 Sep 2019 10:34:45 +0100 Subject: [PATCH 06/39] Fix Changelog entries --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f15c7b..3d27c964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ -## 6.0.2 +## 6.0.2 - Improved handling of invalid compressed content [#368](https://github.com/logstash-plugins/logstash-input-beats/pull/368) -## 6.0.1 +## 6.0.1 - Updated Jackson dependencies [#366](https://github.com/logstash-plugins/logstash-input-beats/pull/366) ## 6.0.0 From 8481d1bda64abfe43dbb4905a5987450e1b31b1a Mon Sep 17 00:00:00 2001 From: Karen Metts <35154725+karenzone@users.noreply.github.com> Date: Thu, 3 Oct 2019 15:33:32 -0400 Subject: [PATCH 07/39] [Doc]Update config example and bump to v6.0.3 (#371) * Update config example and bump to v6.0.3 * Added PR number to changelog * Update docs/index.asciidoc Co-Authored-By: DeDe Morton --- CHANGELOG.md | 3 +++ VERSION | 2 +- docs/index.asciidoc | 33 ++++++++++++++++++--------------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d27c964..d4ed4c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.0.3 + - Fixed configuration example in doc [#371](https://github.com/logstash-plugins/logstash-input-beats/pull/371) + ## 6.0.2 - Improved handling of invalid compressed content [#368](https://github.com/logstash-plugins/logstash-input-beats/pull/368) diff --git a/VERSION b/VERSION index 9b9a2442..090ea9da 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.2 +6.0.3 diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 5165b0ca..bc9c1d35 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -27,8 +27,9 @@ https://www.elastic.co/products/beats[Elastic Beats] framework. The following example shows how to configure Logstash to listen on port 5044 for incoming Beats connections and to index into Elasticsearch. -[source,ruby] ------------------------------------------------------------------------------- +[source,logstash] +----- + input { beats { port => 5044 @@ -37,21 +38,23 @@ input { output { elasticsearch { - hosts => "localhost:9200" - manage_template => false - index => "%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}" <1> - document_type => "%{[@metadata][type]}" <2> + hosts => ["http://localhost:9200"] + index => "%{[@metadata][beat]}-%{[@metadata][version]}" <1> } } ------------------------------------------------------------------------------- -<1> Specifies the index to write events to. See <> for -more about this setting. -<2> Starting with Logstash 6.0, the `document_type` option is -deprecated due to the -https://www.elastic.co/guide/en/elasticsearch/reference/6.0/removal-of-types.html[removal of types in Logstash 6.0]. -It will be removed in the next major version of Logstash. If you are running -Logstash 6.0 or later, do not set `document_type` in your configuration because -Logstash sets the type to `doc` by default. +----- +<1> `%{[@metadata][beat]}` sets the first part of the index name to the value +of the `beat` metadata field and `%{[@metadata][version]}` sets the second part to +the Beat's version. For example: +metricbeat-7.4.0. + +Events indexed into Elasticsearch with the Logstash configuration shown here +will be similar to events directly indexed by Beats into Elasticsearch. + +NOTE: If ILM is not being used, set `index` to +`%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}` instead so +Logstash creates an index per day, based on the `@timestamp` value of the events +coming from Beats. IMPORTANT: If you are shipping events that span multiple lines, you need to use the https://www.elastic.co/guide/en/beats/filebeat/current/multiline-examples.html[configuration options available in Filebeat] to handle multiline events From a966ee59e0e7ecd0bb620ec959d3a6427f4a18d3 Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Thu, 9 Jan 2020 14:51:09 -0500 Subject: [PATCH 08/39] Update Jackson Dependencies (#375) --- CHANGELOG.md | 3 +++ VERSION | 2 +- build.gradle | 11 +++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ed4c27..844dbd30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.0.4 + - Updated Jackson dependencies + ## 6.0.3 - Fixed configuration example in doc [#371](https://github.com/logstash-plugins/logstash-input-beats/pull/371) diff --git a/VERSION b/VERSION index 090ea9da..1aa5e414 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.3 +6.0.4 diff --git a/build.gradle b/build.gradle index 954d70ac..71cf8379 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,9 @@ description = "Beats/Lumberjack Netty implementation" sourceCompatibility = 1.8 targetCompatibility = 1.8 +String jacksonVersion = '2.9.10' +String jacksonDatabindVersion = '2.9.10.1' + repositories { mavenCentral() } @@ -25,10 +28,10 @@ dependencies { compile 'io.netty:netty-all:4.1.30.Final' compile 'io.netty:netty-tcnative-boringssl-static:2.0.12.Final' compile 'org.javassist:javassist:3.24.0-GA' - compile 'com.fasterxml.jackson.core:jackson-core:2.9.9' - compile 'com.fasterxml.jackson.core:jackson-annotations:2.9.9' - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.9.3' - compile 'com.fasterxml.jackson.module:jackson-module-afterburner:2.9.9' + compile "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" + compile "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" + compile "com.fasterxml.jackson.core:jackson-databind:${jacksonDatabindVersion}" + compile "com.fasterxml.jackson.module:jackson-module-afterburner:${jacksonVersion}" compile 'org.apache.logging.log4j:log4j-api:2.11.1' } From 707ab469b41a322a73945e54f05ac0f359a9f5b7 Mon Sep 17 00:00:00 2001 From: Karol Bucek Date: Mon, 13 Jan 2020 19:41:22 +0100 Subject: [PATCH 09/39] Security: update netty deps - avoid CBC ciphers (#376) * Refactor: cleanup TLS debug logging a bit * Refactor: get rid of some minor warnings --- CHANGELOG.md | 3 +++ VERSION | 2 +- build.gradle | 4 +-- .../org/logstash/netty/SslSimpleBuilder.java | 26 +++++++------------ 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 844dbd30..aaf12eb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.0.5 + - Security: update netty deps - avoid CBC ciphers [#376](hhttps://github.com/logstash-plugins/logstash-input-beats/pull/376) + ## 6.0.4 - Updated Jackson dependencies diff --git a/VERSION b/VERSION index 1aa5e414..288b2cd9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.4 +6.0.5 diff --git a/build.gradle b/build.gradle index 71cf8379..fe695b19 100644 --- a/build.gradle +++ b/build.gradle @@ -25,8 +25,8 @@ dependencies { testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'org.apache.logging.log4j:log4j-core:2.11.1' - compile 'io.netty:netty-all:4.1.30.Final' - compile 'io.netty:netty-tcnative-boringssl-static:2.0.12.Final' + compile 'io.netty:netty-all:4.1.44.Final' + compile 'io.netty:netty-tcnative-boringssl-static:2.0.28.Final' compile 'org.javassist:javassist:3.24.0-GA' compile "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" compile "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" diff --git a/src/main/java/org/logstash/netty/SslSimpleBuilder.java b/src/main/java/org/logstash/netty/SslSimpleBuilder.java index f38f80d8..97d7e303 100644 --- a/src/main/java/org/logstash/netty/SslSimpleBuilder.java +++ b/src/main/java/org/logstash/netty/SslSimpleBuilder.java @@ -7,11 +7,9 @@ import io.netty.handler.ssl.SslHandler; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.logstash.beats.Server; import javax.net.ssl.SSLEngine; import java.io.*; -import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; @@ -45,14 +43,10 @@ public static enum SslClientVerifyMode { This list require the OpenSSl engine for netty. */ public final static String[] DEFAULT_CIPHERS = new String[] { - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" }; private String[] ciphers = DEFAULT_CIPHERS; @@ -60,11 +54,10 @@ public static enum SslClientVerifyMode { private String[] certificateAuthorities; private String passPhrase; - public SslSimpleBuilder(String sslCertificateFilePath, String sslKeyFilePath, String pass) throws FileNotFoundException { + public SslSimpleBuilder(String sslCertificateFilePath, String sslKeyFilePath, String pass) { sslCertificateFile = new File(sslCertificateFilePath); sslKeyFile = new File(sslKeyFilePath); passPhrase = pass; - ciphers = DEFAULT_CIPHERS; } public SslSimpleBuilder setProtocols(String[] protocols) { @@ -77,7 +70,7 @@ public SslSimpleBuilder setCipherSuites(String[] ciphersSuite) throws IllegalArg if(!OpenSsl.isCipherSuiteAvailable(cipher)) { throw new IllegalArgumentException("Cipher `" + cipher + "` is not available"); } else { - logger.debug("Cipher is supported: " + cipher); + logger.debug("Cipher is supported: {}", cipher); } } @@ -108,12 +101,13 @@ public File getSslCertificateFile() { return sslCertificateFile; } - public SslHandler build(ByteBufAllocator bufferAllocator) throws IOException, NoSuchAlgorithmException, CertificateException { + public SslHandler build(ByteBufAllocator bufferAllocator) throws IOException, CertificateException { SslContextBuilder builder = SslContextBuilder.forServer(sslCertificateFile, sslKeyFile, passPhrase); - if(logger.isDebugEnabled()) - logger.debug("Available ciphers:" + Arrays.toString(OpenSsl.availableOpenSslCipherSuites().toArray())); + if (logger.isDebugEnabled()) { + logger.debug("Available ciphers: " + Arrays.toString(OpenSsl.availableOpenSslCipherSuites().toArray())); logger.debug("Ciphers: " + Arrays.toString(ciphers)); + } builder.ciphers(Arrays.asList(ciphers)); @@ -128,7 +122,7 @@ public SslHandler build(ByteBufAllocator bufferAllocator) throws IOException, No SslContext context = builder.build(); SslHandler sslHandler = context.newHandler(bufferAllocator); - if(logger.isDebugEnabled()) + if (logger.isDebugEnabled()) logger.debug("TLS: " + Arrays.toString(protocols)); SSLEngine engine = sslHandler.engine(); @@ -162,7 +156,7 @@ private X509Certificate[] loadCertificateCollection(String[] certificates) throw for(int i = 0; i < certificates.length; i++) { String certificate = certificates[i]; - logger.debug("Loading certificates from file " + certificate); + logger.debug("Loading certificates from file {}", certificate); try(InputStream in = new FileInputStream(certificate)) { List certificatesChains = (List) certificateFactory.generateCertificates(in); From 37cc60f8909a199b333a4079577c0acdccdcc0b4 Mon Sep 17 00:00:00 2001 From: Joao Duarte Date: Wed, 22 Jan 2020 23:20:38 +0100 Subject: [PATCH 10/39] downgrade netty due to IdleStateHandler holding back acknowledgements (#380) Since https://github.com/netty/netty/commit/51112e2b36 was merged into netty 4.1.35, having the IdleStateHandler in the pipeline causes outbound messages to get stuck there until a write idle event is triggered, or something comes inbound. This causes synchronous clients that perform `send(message); get(ack)` to get stuck, while clients that send more than one message before requesting acks are still able to function. In case of Logstash + beats, beats are able to push out data as long as pipelining is used, but disabling it will cause the producer to stall. See more at https://github.com/elastic/logstash/issues/11517#issuecomment-577538163 Actions: * add integration test for large batches on both filebeat and logstash-forwarder * add integration test for filebeat where pipelining is disabled * downgrade netty-all to 4.1.34 * bump to 6.0.6 --- CHANGELOG.md | 5 ++++- VERSION | 2 +- build.gradle | 2 +- spec/integration/filebeat_spec.rb | 15 +++++++++++++++ spec/integration/logstash_forwarder_spec.rb | 7 ++++++- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf12eb1..9f73bc5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ +## 6.0.6 + - Downgraded netty to 4.1.34 due to an issue in IdleStateHandler [#380](https://github.com/logstash-plugins/logstash-input-beats/pull/380) + ## 6.0.5 - - Security: update netty deps - avoid CBC ciphers [#376](hhttps://github.com/logstash-plugins/logstash-input-beats/pull/376) + - Security: update netty deps - avoid CBC ciphers [#376](https://github.com/logstash-plugins/logstash-input-beats/pull/376) ## 6.0.4 - Updated Jackson dependencies diff --git a/VERSION b/VERSION index 288b2cd9..b7ff1516 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.5 +6.0.6 diff --git a/build.gradle b/build.gradle index fe695b19..672753b1 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ dependencies { testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'org.apache.logging.log4j:log4j-core:2.11.1' - compile 'io.netty:netty-all:4.1.44.Final' + compile 'io.netty:netty-all:4.1.34.Final' compile 'io.netty:netty-tcnative-boringssl-static:2.0.28.Final' compile 'org.javassist:javassist:3.24.0-GA' compile "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" diff --git a/spec/integration/filebeat_spec.rb b/spec/integration/filebeat_spec.rb index f2205112..fdfada6d 100644 --- a/spec/integration/filebeat_spec.rb +++ b/spec/integration/filebeat_spec.rb @@ -79,6 +79,21 @@ # Actuals tests context "Plain TCP" do include_examples "send events" + + context "with large batches" do + let(:number_of_events) { 10_000 } + include_examples "send events" + end + + context "without pipelining" do + let(:filebeat_config) { config = super; config["output"]["logstash"]["pipelining"] = 0; config } + include_examples "send events" + + context "with large batches" do + let(:number_of_events) { 10_000 } + include_examples "send events" + end + end end context "TLS" do diff --git a/spec/integration/logstash_forwarder_spec.rb b/spec/integration/logstash_forwarder_spec.rb index fb40c188..ad064ff5 100644 --- a/spec/integration/logstash_forwarder_spec.rb +++ b/spec/integration/logstash_forwarder_spec.rb @@ -74,7 +74,7 @@ context "TLS" do context "Server Verification" do let(:input_config) do - super.merge({ + super.merge({ "ssl" => true, "ssl_certificate" => certificate_file, "ssl_key" => certificate_key_file, @@ -90,6 +90,11 @@ include_examples "send events" end + context "with large batches" do + let(:number_of_events) { 10_000 } + include_examples "send events" + end + context "invalid CA on the client" do let(:invalid_data) { Flores::PKI.generate } let(:certificate_authorities) { f = Stud::Temporary.file; f.close; f.path } From aaa50a3e177ce057836ba3fbdfcb058f7bdc0d10 Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Tue, 28 Jan 2020 11:54:06 -0500 Subject: [PATCH 11/39] Reinstate CBC ciphers Reinstate CBC ciphers to preserve compatibility with lumberjack output. Until the JRuby-openssl library used internally is updated to include more modern ciphers, the CBC ciphers are required to preserve compatibility with the Lumberjack output commonly used for Logstash->Logstash communication. Note that it is possible to manually remove CBC ciphers, by specifying acceptable ciphers using the `cipher_suites` configuration option. --- src/main/java/org/logstash/netty/SslSimpleBuilder.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/logstash/netty/SslSimpleBuilder.java b/src/main/java/org/logstash/netty/SslSimpleBuilder.java index 97d7e303..ff78ff26 100644 --- a/src/main/java/org/logstash/netty/SslSimpleBuilder.java +++ b/src/main/java/org/logstash/netty/SslSimpleBuilder.java @@ -43,10 +43,14 @@ public static enum SslClientVerifyMode { This list require the OpenSSl engine for netty. */ public final static String[] DEFAULT_CIPHERS = new String[] { - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" }; private String[] ciphers = DEFAULT_CIPHERS; From eab31bae421c85db848309711627ae91699304d0 Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Tue, 28 Jan 2020 14:06:37 -0500 Subject: [PATCH 12/39] Revert changes to Netty and netty-tcnative Newer versions of boringssl do not allow CBC ciphers to be used, this commit rolls back the version to allow for compatibility with Lumberjack output. --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 672753b1..71cf8379 100644 --- a/build.gradle +++ b/build.gradle @@ -25,8 +25,8 @@ dependencies { testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'org.apache.logging.log4j:log4j-core:2.11.1' - compile 'io.netty:netty-all:4.1.34.Final' - compile 'io.netty:netty-tcnative-boringssl-static:2.0.28.Final' + compile 'io.netty:netty-all:4.1.30.Final' + compile 'io.netty:netty-tcnative-boringssl-static:2.0.12.Final' compile 'org.javassist:javassist:3.24.0-GA' compile "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" compile "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" From 7ba9516c44150b95bf238df859023db058a49f23 Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Thu, 30 Jan 2020 13:25:12 -0500 Subject: [PATCH 13/39] Version bump and changelog entry --- CHANGELOG.md | 3 +++ VERSION | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f73bc5e..321a3f81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.0.7 + - Reverted changes to netty and tcnative dependencies and removal of CBC ciphers to preserve compatibility with Lumberjack output [#381](https://github.com/logstash-plugins/logstash-input-beats/pull/381) + ## 6.0.6 - Downgraded netty to 4.1.34 due to an issue in IdleStateHandler [#380](https://github.com/logstash-plugins/logstash-input-beats/pull/380) diff --git a/VERSION b/VERSION index b7ff1516..089b1e69 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.6 +6.0.7 From c8fbae90e8625bb97cfb2bb01e1f56d02d2205c5 Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Tue, 4 Feb 2020 09:11:53 -0500 Subject: [PATCH 14/39] Avoid creating SslContext per-connection Currently an SslContext is being built for every connection, which can be expensive given that the beats input is designed to handle connections from many beats agents. This commit changes this to only create the Context on the creation of the server. Fixes #383 --- lib/logstash/inputs/beats.rb | 20 +- src/main/java/org/logstash/beats/Runner.java | 11 +- src/main/java/org/logstash/beats/Server.java | 40 ++-- .../org/logstash/netty/SslSimpleBuilder.java | 192 ------------------ .../logstash/netty/SslSimpleBuilderTest.java | 20 -- 5 files changed, 38 insertions(+), 245 deletions(-) delete mode 100644 src/main/java/org/logstash/netty/SslSimpleBuilder.java delete mode 100644 src/test/java/org/logstash/netty/SslSimpleBuilderTest.java diff --git a/lib/logstash/inputs/beats.rb b/lib/logstash/inputs/beats.rb index 5d65df93..163cf575 100644 --- a/lib/logstash/inputs/beats.rb +++ b/lib/logstash/inputs/beats.rb @@ -114,7 +114,7 @@ class LogStash::Inputs::Beats < LogStash::Inputs::Base config :tls_max_version, :validate => :number, :default => TLS.max.version # The list of ciphers suite to use, listed by priorities. - config :cipher_suites, :validate => :array, :default => org.logstash.netty.SslSimpleBuilder::DEFAULT_CIPHERS + config :cipher_suites, :validate => :array, :default => org.logstash.netty.SslContextBuilder::DEFAULT_CIPHERS # Close Idle clients after X seconds of inactivity. config :client_inactivity_timeout, :validate => :number, :default => 60 @@ -133,7 +133,7 @@ def register end java_import "org.logstash.beats.Server" - java_import "org.logstash.netty.SslSimpleBuilder" + java_import "org.logstash.netty.SslContextBuilder" java_import "java.io.FileInputStream" java_import "io.netty.handler.ssl.OpenSsl" @@ -167,25 +167,23 @@ def create_server if @ssl begin - ssl_builder = org.logstash.netty.SslSimpleBuilder.new(@ssl_certificate, @ssl_key, @ssl_key_passphrase.nil? ? nil : @ssl_key_passphrase.value) - .setProtocols(convert_protocols) - .setCipherSuites(normalized_ciphers) + ssl_context_builder = org.logstash.netty.SslContextBuilder.new(@ssl_certificate, @ssl_key, @ssl_key_passphrase.nil? ? nil : @ssl_key_passphrase.value) + .setProtocols(convert_protocols) + .setCipherSuites(normalized_ciphers) rescue java.lang.IllegalArgumentException => e raise LogStash::ConfigurationError, e end - ssl_builder.setHandshakeTimeoutMilliseconds(@ssl_handshake_timeout) if client_authentification? if @ssl_verify_mode.upcase == "FORCE_PEER" - ssl_builder.setVerifyMode(org.logstash.netty.SslSimpleBuilder::SslClientVerifyMode::FORCE_PEER) + ssl_context_builder.setVerifyMode(org.logstash.netty.SslContextBuilder::SslClientVerifyMode::FORCE_PEER) elsif @ssl_verify_mode.upcase == "PEER" - ssl_builder.setVerifyMode(org.logstash.netty.SslSimpleBuilder::SslClientVerifyMode::VERIFY_PEER) + ssl_context_builder.setVerifyMode(org.logstash.netty.SslContextBuilder::SslClientVerifyMode::VERIFY_PEER) end - ssl_builder.setCertificateAuthorities(@ssl_certificate_authorities) + ssl_context_builder.setCertificateAuthorities(@ssl_certificate_authorities) end - - server.enableSSL(ssl_builder) + server.enable_ssl(ssl_context_builder.build_context, @ssl_handshake_timeout) end server end diff --git a/src/main/java/org/logstash/beats/Runner.java b/src/main/java/org/logstash/beats/Runner.java index a5eb6fd3..72f98ad6 100644 --- a/src/main/java/org/logstash/beats/Runner.java +++ b/src/main/java/org/logstash/beats/Runner.java @@ -1,8 +1,9 @@ package org.logstash.beats; +import io.netty.handler.ssl.SslContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.logstash.netty.SslSimpleBuilder; +import org.logstash.netty.SslContextBuilder; public class Runner { @@ -30,12 +31,10 @@ static public void main(String[] args) throws Exception { - SslSimpleBuilder sslBuilder = new SslSimpleBuilder(sslCertificate, sslKey, null) + SslContextBuilder sslBuilder = new SslContextBuilder(sslCertificate, sslKey, null) .setProtocols(new String[] { "TLSv1.2" }) - .setCertificateAuthorities(certificateAuthorities) - .setHandshakeTimeoutMilliseconds(10000); - - server.enableSSL(sslBuilder); + .setCertificateAuthorities(certificateAuthorities); + server.enableSsl(sslBuilder.buildContext(), 10000); } server.listen(); diff --git a/src/main/java/org/logstash/beats/Server.java b/src/main/java/org/logstash/beats/Server.java index 74624d60..bc2f32e1 100644 --- a/src/main/java/org/logstash/beats/Server.java +++ b/src/main/java/org/logstash/beats/Server.java @@ -5,17 +5,19 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.concurrent.DefaultEventExecutorGroup; import io.netty.util.concurrent.EventExecutorGroup; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.logstash.netty.SslSimpleBuilder; +import org.logstash.netty.SslContextBuilder; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; +import java.util.concurrent.TimeUnit; public class Server { private final static Logger logger = LogManager.getLogger(Server.class); @@ -25,20 +27,22 @@ public class Server { private final int beatsHeandlerThreadCount; private NioEventLoopGroup workGroup; private IMessageListener messageListener = new MessageListener(); - private SslSimpleBuilder sslBuilder; + private SslContext sslContext; private BeatsInitializer beatsInitializer; + private int handshakeTimeoutMillis; private final int clientInactivityTimeoutSeconds; - public Server(String host, int p, int timeout, int threadCount) { + public Server(String host, int p, int clientInactivityTimeoutSeconds, int threadCount) { this.host = host; port = p; - clientInactivityTimeoutSeconds = timeout; + this.clientInactivityTimeoutSeconds = clientInactivityTimeoutSeconds; beatsHeandlerThreadCount = threadCount; } - public void enableSSL(SslSimpleBuilder builder) { - sslBuilder = builder; + public void enableSsl(SslContext sslContext, int handshakeTimeoutMillis) { + this.handshakeTimeoutMillis = handshakeTimeoutMillis; + this.sslContext = sslContext; } public Server listen() throws InterruptedException { @@ -54,7 +58,7 @@ public Server listen() throws InterruptedException { try { logger.info("Starting server on port: {}", this.port); - beatsInitializer = new BeatsInitializer(isSslEnable(), messageListener, clientInactivityTimeoutSeconds, beatsHeandlerThreadCount); + beatsInitializer = new BeatsInitializer(messageListener, clientInactivityTimeoutSeconds, beatsHeandlerThreadCount); ServerBootstrap server = new ServerBootstrap(); server.group(workGroup) @@ -95,7 +99,7 @@ public void setMessageListener(IMessageListener listener) { } public boolean isSslEnable() { - return this.sslBuilder != null; + return this.sslContext != null; } private class BeatsInitializer extends ChannelInitializer { @@ -112,24 +116,20 @@ private class BeatsInitializer extends ChannelInitializer { private final EventExecutorGroup beatsHandlerExecutorGroup; private final IMessageListener localMessageListener; private final int localClientInactivityTimeoutSeconds; - private final boolean localEnableSSL; - BeatsInitializer(Boolean enableSSL, IMessageListener messageListener, int clientInactivityTimeoutSeconds, int beatsHandlerThread) { + BeatsInitializer(IMessageListener messageListener, int clientInactivityTimeoutSeconds, int beatsHandlerThread) { // Keeps a local copy of Server settings, so they can't be modified once it starts listening - this.localEnableSSL = enableSSL; this.localMessageListener = messageListener; this.localClientInactivityTimeoutSeconds = clientInactivityTimeoutSeconds; idleExecutorGroup = new DefaultEventExecutorGroup(DEFAULT_IDLESTATEHANDLER_THREAD); beatsHandlerExecutorGroup = new DefaultEventExecutorGroup(beatsHandlerThread); - } - public void initChannel(SocketChannel socket) throws IOException, NoSuchAlgorithmException, CertificateException { + public void initChannel(SocketChannel socket){ ChannelPipeline pipeline = socket.pipeline(); - if (localEnableSSL) { - SslHandler sslHandler = sslBuilder.build(socket.alloc()); - pipeline.addLast(SSL_HANDLER, sslHandler); + if (sslContext != null) { + pipeline.addLast(SSL_HANDLER, sslHandlerForChannel(socket)); } pipeline.addLast(idleExecutorGroup, IDLESTATE_HANDLER, new IdleStateHandler(localClientInactivityTimeoutSeconds, IDLESTATE_WRITER_IDLE_TIME_SECONDS, localClientInactivityTimeoutSeconds)); @@ -138,6 +138,8 @@ public void initChannel(SocketChannel socket) throws IOException, NoSuchAlgorith pipeline.addLast(beatsHandlerExecutorGroup, new BeatsParser(), new BeatsHandler(localMessageListener)); } + + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { logger.warn("Exception caught in channel initializer", cause); @@ -157,4 +159,10 @@ public void shutdownEventExecutor() { } } } + + private SslHandler sslHandlerForChannel(final SocketChannel socket) { + SslHandler handler = sslContext.newHandler(socket.alloc()); + handler.setHandshakeTimeoutMillis(handshakeTimeoutMillis); + return handler; + } } diff --git a/src/main/java/org/logstash/netty/SslSimpleBuilder.java b/src/main/java/org/logstash/netty/SslSimpleBuilder.java deleted file mode 100644 index ff78ff26..00000000 --- a/src/main/java/org/logstash/netty/SslSimpleBuilder.java +++ /dev/null @@ -1,192 +0,0 @@ -package org.logstash.netty; - -import io.netty.buffer.ByteBufAllocator; -import io.netty.handler.ssl.OpenSsl; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.SslHandler; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.net.ssl.SSLEngine; -import java.io.*; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -/** - * Created by ph on 2016-05-27. - */ -public class SslSimpleBuilder { - - - public static enum SslClientVerifyMode { - VERIFY_PEER, - FORCE_PEER, - } - private final static Logger logger = LogManager.getLogger(SslSimpleBuilder.class); - - - private File sslKeyFile; - private File sslCertificateFile; - private SslClientVerifyMode verifyMode = SslClientVerifyMode.FORCE_PEER; - - private long handshakeTimeoutMilliseconds = 10000; - - /* - Mordern Ciphers List from - https://wiki.mozilla.org/Security/Server_Side_TLS - This list require the OpenSSl engine for netty. - */ - public final static String[] DEFAULT_CIPHERS = new String[] { - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" - }; - - private String[] ciphers = DEFAULT_CIPHERS; - private String[] protocols = new String[] { "TLSv1.2" }; - private String[] certificateAuthorities; - private String passPhrase; - - public SslSimpleBuilder(String sslCertificateFilePath, String sslKeyFilePath, String pass) { - sslCertificateFile = new File(sslCertificateFilePath); - sslKeyFile = new File(sslKeyFilePath); - passPhrase = pass; - } - - public SslSimpleBuilder setProtocols(String[] protocols) { - this.protocols = protocols; - return this; - } - - public SslSimpleBuilder setCipherSuites(String[] ciphersSuite) throws IllegalArgumentException { - for(String cipher : ciphersSuite) { - if(!OpenSsl.isCipherSuiteAvailable(cipher)) { - throw new IllegalArgumentException("Cipher `" + cipher + "` is not available"); - } else { - logger.debug("Cipher is supported: {}", cipher); - } - } - - ciphers = ciphersSuite; - return this; - } - - public SslSimpleBuilder setCertificateAuthorities(String[] cert) { - certificateAuthorities = cert; - return this; - } - - public SslSimpleBuilder setHandshakeTimeoutMilliseconds(int timeout) { - handshakeTimeoutMilliseconds = timeout; - return this; - } - - public SslSimpleBuilder setVerifyMode(SslClientVerifyMode mode) { - verifyMode = mode; - return this; - } - - public File getSslKeyFile() { - return sslKeyFile; - } - - public File getSslCertificateFile() { - return sslCertificateFile; - } - - public SslHandler build(ByteBufAllocator bufferAllocator) throws IOException, CertificateException { - SslContextBuilder builder = SslContextBuilder.forServer(sslCertificateFile, sslKeyFile, passPhrase); - - if (logger.isDebugEnabled()) { - logger.debug("Available ciphers: " + Arrays.toString(OpenSsl.availableOpenSslCipherSuites().toArray())); - logger.debug("Ciphers: " + Arrays.toString(ciphers)); - } - - - builder.ciphers(Arrays.asList(ciphers)); - - if(requireClientAuth()) { - if (logger.isDebugEnabled()) - logger.debug("Certificate Authorities: " + Arrays.toString(certificateAuthorities)); - - builder.trustManager(loadCertificateCollection(certificateAuthorities)); - } - - SslContext context = builder.build(); - SslHandler sslHandler = context.newHandler(bufferAllocator); - - if (logger.isDebugEnabled()) - logger.debug("TLS: " + Arrays.toString(protocols)); - - SSLEngine engine = sslHandler.engine(); - engine.setEnabledProtocols(protocols); - - - if(requireClientAuth()) { - // server is doing the handshake - engine.setUseClientMode(false); - - if(verifyMode == SslClientVerifyMode.FORCE_PEER) { - // Explicitely require a client certificate - engine.setNeedClientAuth(true); - } else if(verifyMode == SslClientVerifyMode.VERIFY_PEER) { - // If the client supply a client certificate we will verify it. - engine.setWantClientAuth(true); - } - } - - sslHandler.setHandshakeTimeoutMillis(handshakeTimeoutMilliseconds); - - return sslHandler; - } - - private X509Certificate[] loadCertificateCollection(String[] certificates) throws IOException, CertificateException { - logger.debug("Load certificates collection"); - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - - List collections = new ArrayList(); - - for(int i = 0; i < certificates.length; i++) { - String certificate = certificates[i]; - - logger.debug("Loading certificates from file {}", certificate); - - try(InputStream in = new FileInputStream(certificate)) { - List certificatesChains = (List) certificateFactory.generateCertificates(in); - collections.addAll(certificatesChains); - } - } - return collections.toArray(new X509Certificate[collections.size()]); - } - - private boolean requireClientAuth() { - if(certificateAuthorities != null) { - return true; - } - - return false; - } - - private FileInputStream createFileInputStream(String filepath) throws FileNotFoundException { - return new FileInputStream(filepath); - } - - /** - * Get the supported protocols - * @return a defensive copy of the supported protocols - */ - String[] getProtocols() { - return protocols.clone(); - } -} diff --git a/src/test/java/org/logstash/netty/SslSimpleBuilderTest.java b/src/test/java/org/logstash/netty/SslSimpleBuilderTest.java deleted file mode 100644 index 56ab5508..00000000 --- a/src/test/java/org/logstash/netty/SslSimpleBuilderTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.logstash.netty; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Unit test for {@link SslSimpleBuilder} - */ -public class SslSimpleBuilderTest { - @Test - public void setProtocols() throws Exception { - SslSimpleBuilder sslSimpleBuilder = new SslSimpleBuilder("/tmp", "mykeyfile", "mypass"); - assertArrayEquals(new String[]{"TLSv1.2"}, sslSimpleBuilder.getProtocols()); - sslSimpleBuilder.setProtocols(new String[]{"TLSv1.1"}); - assertArrayEquals(new String[]{"TLSv1.1"}, sslSimpleBuilder.getProtocols()); - sslSimpleBuilder.setProtocols(new String[]{"TLSv1.1", "TLSv1.2"}); - assertArrayEquals(new String[]{"TLSv1.1", "TLSv1.2"}, sslSimpleBuilder.getProtocols()); - } -} \ No newline at end of file From 5df61f18d062e84c554064f319cf20410d896b37 Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Tue, 4 Feb 2020 09:24:12 -0500 Subject: [PATCH 15/39] Avoid creating SslContext per-connection Currently an SslContext is being built for every connection, which can be expensive given that the beats input is designed to handle connections from many beats agents. This commit changes this to only create the Context on the creation of the server. Fixes #383 --- .../org/logstash/netty/SslContextBuilder.java | 170 ++++++++++++++++++ .../logstash/netty/SslContextBuilderTest.java | 20 +++ 2 files changed, 190 insertions(+) create mode 100644 src/main/java/org/logstash/netty/SslContextBuilder.java create mode 100644 src/test/java/org/logstash/netty/SslContextBuilderTest.java diff --git a/src/main/java/org/logstash/netty/SslContextBuilder.java b/src/main/java/org/logstash/netty/SslContextBuilder.java new file mode 100644 index 00000000..8384a03d --- /dev/null +++ b/src/main/java/org/logstash/netty/SslContextBuilder.java @@ -0,0 +1,170 @@ +package org.logstash.netty; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslContext; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Created by ph on 2016-05-27. + */ +public class SslContextBuilder { + + + public enum SslClientVerifyMode { + VERIFY_PEER, + FORCE_PEER, + } + private final static Logger logger = LogManager.getLogger(SslContextBuilder.class); + + + private File sslKeyFile; + private File sslCertificateFile; + private SslClientVerifyMode verifyMode = SslClientVerifyMode.FORCE_PEER; + + private long handshakeTimeoutMilliseconds = 10000; + + /* + Mordern Ciphers List from + https://wiki.mozilla.org/Security/Server_Side_TLS + This list require the OpenSSl engine for netty. + */ + public final static String[] DEFAULT_CIPHERS = new String[] { + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" + }; + + private String[] ciphers = DEFAULT_CIPHERS; + private String[] protocols = new String[] { "TLSv1.2" }; + private String[] certificateAuthorities; + private String passPhrase; + + public SslContextBuilder(String sslCertificateFilePath, String sslKeyFilePath, String pass) { + sslCertificateFile = new File(sslCertificateFilePath); + sslKeyFile = new File(sslKeyFilePath); + passPhrase = pass; + } + + public SslContextBuilder setProtocols(String[] protocols) { + this.protocols = protocols; + return this; + } + + public SslContextBuilder setCipherSuites(String[] ciphersSuite) throws IllegalArgumentException { + for(String cipher : ciphersSuite) { + if(!OpenSsl.isCipherSuiteAvailable(cipher)) { + throw new IllegalArgumentException("Cipher `" + cipher + "` is not available"); + } else { + logger.debug("Cipher is supported: {}", cipher); + } + } + + ciphers = ciphersSuite; + return this; + } + + public SslContextBuilder setCertificateAuthorities(String[] cert) { + certificateAuthorities = cert; + return this; + } + + public SslContextBuilder setVerifyMode(SslClientVerifyMode mode) { + verifyMode = mode; + return this; + } + + public File getSslKeyFile() { + return sslKeyFile; + } + + public File getSslCertificateFile() { + return sslCertificateFile; + } + + public SslContext buildContext() throws IOException, CertificateException { + io.netty.handler.ssl.SslContextBuilder builder = io.netty.handler.ssl.SslContextBuilder.forServer(sslCertificateFile, sslKeyFile, passPhrase); + + if (logger.isDebugEnabled()) { + logger.debug("Available ciphers: " + Arrays.toString(OpenSsl.availableOpenSslCipherSuites().toArray())); + logger.debug("Ciphers: " + Arrays.toString(ciphers)); + } + + builder.ciphers(Arrays.asList(ciphers)); + + if(requireClientAuth()) { + if (logger.isDebugEnabled()) + logger.debug("Certificate Authorities: " + Arrays.toString(certificateAuthorities)); + + builder.trustManager(loadCertificateCollection(certificateAuthorities)); + if(verifyMode == SslClientVerifyMode.FORCE_PEER) { + // Explicitly require a client certificate + builder.clientAuth(ClientAuth.REQUIRE); + } else if(verifyMode == SslClientVerifyMode.VERIFY_PEER) { + // If the client supply a client certificate we will verify it. + builder.clientAuth(ClientAuth.OPTIONAL); + } + }else{ + builder.clientAuth(ClientAuth.NONE); + } + builder.protocols(protocols); + return builder.build(); + } + + private X509Certificate[] loadCertificateCollection(String[] certificates) throws IOException, CertificateException { + logger.debug("Load certificates collection"); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + List collections = new ArrayList(); + + for(int i = 0; i < certificates.length; i++) { + String certificate = certificates[i]; + + logger.debug("Loading certificates from file {}", certificate); + + try(InputStream in = new FileInputStream(certificate)) { + List certificatesChains = (List) certificateFactory.generateCertificates(in); + collections.addAll(certificatesChains); + } + } + return collections.toArray(new X509Certificate[collections.size()]); + } + + private boolean requireClientAuth() { + if(certificateAuthorities != null) { + return true; + } + + return false; + } + + private FileInputStream createFileInputStream(String filepath) throws FileNotFoundException { + return new FileInputStream(filepath); + } + + /** + * Get the supported protocols + * @return a defensive copy of the supported protocols + */ + String[] getProtocols() { + return protocols.clone(); + } +} diff --git a/src/test/java/org/logstash/netty/SslContextBuilderTest.java b/src/test/java/org/logstash/netty/SslContextBuilderTest.java new file mode 100644 index 00000000..359d4a81 --- /dev/null +++ b/src/test/java/org/logstash/netty/SslContextBuilderTest.java @@ -0,0 +1,20 @@ +package org.logstash.netty; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Unit test for {@link SslContextBuilder} + */ +public class SslContextBuilderTest { + @Test + public void setProtocols() throws Exception { + SslContextBuilder sslContextBuilder = new SslContextBuilder("/tmp", "mykeyfile", "mypass"); + assertArrayEquals(new String[]{"TLSv1.2"}, sslContextBuilder.getProtocols()); + sslContextBuilder.setProtocols(new String[]{"TLSv1.1"}); + assertArrayEquals(new String[]{"TLSv1.1"}, sslContextBuilder.getProtocols()); + sslContextBuilder.setProtocols(new String[]{"TLSv1.1", "TLSv1.2"}); + assertArrayEquals(new String[]{"TLSv1.1", "TLSv1.2"}, sslContextBuilder.getProtocols()); + } +} \ No newline at end of file From 5faaf46737b0ca6f66b7272e727fec30e4c7e708 Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Tue, 4 Feb 2020 09:13:05 -0500 Subject: [PATCH 16/39] Explicitly call `end()` on Inflater This commit always calls the end method on the Inflater after use - an InflaterOutputStream will only call this method if it is using the default Inflater, and will not call it when the stream is closed. Fixes #383 --- src/main/java/org/logstash/beats/BeatsParser.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/logstash/beats/BeatsParser.java b/src/main/java/org/logstash/beats/BeatsParser.java index fa099e74..9e852e49 100644 --- a/src/main/java/org/logstash/beats/BeatsParser.java +++ b/src/main/java/org/logstash/beats/BeatsParser.java @@ -213,11 +213,14 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t private ByteBuf inflateCompressedFrame(final ChannelHandlerContext ctx, final ByteBuf in) throws IOException { ByteBuf buffer = ctx.alloc().buffer(requiredBytes); + Inflater inflater = new Inflater(); try ( ByteBufOutputStream buffOutput = new ByteBufOutputStream(buffer); - InflaterOutputStream inflater = new InflaterOutputStream(buffOutput, new Inflater()) + InflaterOutputStream inflaterStream = new InflaterOutputStream(buffOutput, inflater) ) { - in.readBytes(inflater, requiredBytes); + in.readBytes(inflaterStream, requiredBytes); + }finally{ + inflater.end(); } return buffer; } From ab05444cf53bbe8d3635d5ae28949a7f4cbb653b Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Mon, 3 Feb 2020 09:31:50 -0500 Subject: [PATCH 17/39] Add SslHandlerProvider class Fixes #383 --- lib/logstash/inputs/beats.rb | 4 +-- src/main/java/org/logstash/beats/Runner.java | 4 ++- src/main/java/org/logstash/beats/Server.java | 31 +++++-------------- .../logstash/netty/SslHandlerProvider.java | 22 +++++++++++++ 4 files changed, 35 insertions(+), 26 deletions(-) create mode 100644 src/main/java/org/logstash/netty/SslHandlerProvider.java diff --git a/lib/logstash/inputs/beats.rb b/lib/logstash/inputs/beats.rb index 163cf575..6cd338cb 100644 --- a/lib/logstash/inputs/beats.rb +++ b/lib/logstash/inputs/beats.rb @@ -134,8 +134,8 @@ def register java_import "org.logstash.beats.Server" java_import "org.logstash.netty.SslContextBuilder" + java_import "org.logstash.netty.SslHandlerProvider" java_import "java.io.FileInputStream" - java_import "io.netty.handler.ssl.OpenSsl" if !@ssl @logger.warn("Beats input: SSL Certificate will not be used") unless @ssl_certificate.nil? @@ -183,7 +183,7 @@ def create_server end ssl_context_builder.setCertificateAuthorities(@ssl_certificate_authorities) end - server.enable_ssl(ssl_context_builder.build_context, @ssl_handshake_timeout) + server.setSslHandlerProvider(org.logstash.netty.SslHandlerProvider.new(ssl_context_builder.build_context, @ssl_handshake_timeout)) end server end diff --git a/src/main/java/org/logstash/beats/Runner.java b/src/main/java/org/logstash/beats/Runner.java index 72f98ad6..9a8813a8 100644 --- a/src/main/java/org/logstash/beats/Runner.java +++ b/src/main/java/org/logstash/beats/Runner.java @@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.logstash.netty.SslContextBuilder; +import org.logstash.netty.SslHandlerProvider; public class Runner { @@ -34,7 +35,8 @@ static public void main(String[] args) throws Exception { SslContextBuilder sslBuilder = new SslContextBuilder(sslCertificate, sslKey, null) .setProtocols(new String[] { "TLSv1.2" }) .setCertificateAuthorities(certificateAuthorities); - server.enableSsl(sslBuilder.buildContext(), 10000); + SslHandlerProvider sslHandlerProvider = new SslHandlerProvider(sslBuilder.buildContext(), 10000); + server.setSslHandlerProvider(sslHandlerProvider); } server.listen(); diff --git a/src/main/java/org/logstash/beats/Server.java b/src/main/java/org/logstash/beats/Server.java index bc2f32e1..5da86c09 100644 --- a/src/main/java/org/logstash/beats/Server.java +++ b/src/main/java/org/logstash/beats/Server.java @@ -5,19 +5,12 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslHandler; import io.netty.handler.timeout.IdleStateHandler; import io.netty.util.concurrent.DefaultEventExecutorGroup; import io.netty.util.concurrent.EventExecutorGroup; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.logstash.netty.SslContextBuilder; - -import java.io.IOException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.util.concurrent.TimeUnit; +import org.logstash.netty.SslHandlerProvider; public class Server { private final static Logger logger = LogManager.getLogger(Server.class); @@ -27,9 +20,8 @@ public class Server { private final int beatsHeandlerThreadCount; private NioEventLoopGroup workGroup; private IMessageListener messageListener = new MessageListener(); - private SslContext sslContext; + private SslHandlerProvider sslHandlerProvider; private BeatsInitializer beatsInitializer; - private int handshakeTimeoutMillis; private final int clientInactivityTimeoutSeconds; @@ -40,9 +32,8 @@ public Server(String host, int p, int clientInactivityTimeoutSeconds, int thread beatsHeandlerThreadCount = threadCount; } - public void enableSsl(SslContext sslContext, int handshakeTimeoutMillis) { - this.handshakeTimeoutMillis = handshakeTimeoutMillis; - this.sslContext = sslContext; + public void setSslHandlerProvider(SslHandlerProvider sslHandlerProvider){ + this.sslHandlerProvider = sslHandlerProvider; } public Server listen() throws InterruptedException { @@ -98,8 +89,8 @@ public void setMessageListener(IMessageListener listener) { messageListener = listener; } - public boolean isSslEnable() { - return this.sslContext != null; + public boolean isSslEnabled() { + return this.sslHandlerProvider != null; } private class BeatsInitializer extends ChannelInitializer { @@ -128,8 +119,8 @@ private class BeatsInitializer extends ChannelInitializer { public void initChannel(SocketChannel socket){ ChannelPipeline pipeline = socket.pipeline(); - if (sslContext != null) { - pipeline.addLast(SSL_HANDLER, sslHandlerForChannel(socket)); + if (isSslEnabled()) { + pipeline.addLast(SSL_HANDLER, sslHandlerProvider.sslHandlerForChannel(socket)); } pipeline.addLast(idleExecutorGroup, IDLESTATE_HANDLER, new IdleStateHandler(localClientInactivityTimeoutSeconds, IDLESTATE_WRITER_IDLE_TIME_SECONDS, localClientInactivityTimeoutSeconds)); @@ -159,10 +150,4 @@ public void shutdownEventExecutor() { } } } - - private SslHandler sslHandlerForChannel(final SocketChannel socket) { - SslHandler handler = sslContext.newHandler(socket.alloc()); - handler.setHandshakeTimeoutMillis(handshakeTimeoutMillis); - return handler; - } } diff --git a/src/main/java/org/logstash/netty/SslHandlerProvider.java b/src/main/java/org/logstash/netty/SslHandlerProvider.java new file mode 100644 index 00000000..c63a6821 --- /dev/null +++ b/src/main/java/org/logstash/netty/SslHandlerProvider.java @@ -0,0 +1,22 @@ +package org.logstash.netty; + +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; + +public class SslHandlerProvider { + + private final SslContext sslContext; + private final int sslHandshakeTimeoutMillis; + + public SslHandlerProvider(SslContext context, int sslHandshakeTimeoutMillis){ + this.sslContext = context; + this.sslHandshakeTimeoutMillis = sslHandshakeTimeoutMillis; + } + + public SslHandler sslHandlerForChannel(final SocketChannel socket) { + SslHandler handler = sslContext.newHandler(socket.alloc()); + handler.setHandshakeTimeoutMillis(sslHandshakeTimeoutMillis); + return handler; + } +} From ce0d3b85b7d316b12d6abfb49ea91dabaade16dd Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Tue, 4 Feb 2020 09:16:09 -0500 Subject: [PATCH 18/39] Version bump and changelog entry Fixes #383 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 321a3f81..523062b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.0.8 + - Fixed issue where an SslContext was unnecessarily being created for each connection [#383](https://github.com/logstash-plugins/logstash-input-beats/pull/383) + - Fixed issue where `end` was not being called when an Inflater was closed [#383](https://github.com/logstash-plugins/logstash-input-beats/pull/383) + ## 6.0.7 - Reverted changes to netty and tcnative dependencies and removal of CBC ciphers to preserve compatibility with Lumberjack output [#381](https://github.com/logstash-plugins/logstash-input-beats/pull/381) diff --git a/VERSION b/VERSION index 089b1e69..4b786f57 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.7 +6.0.8 From 565c1c6f8ed9c3db7a2464ff2a45f69301d02a5e Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Fri, 6 Mar 2020 11:21:47 -0500 Subject: [PATCH 19/39] Remove `java_import`s to avoid breaking tcp input (#388) * Remove `java_import`s to avoid breaking tcp input Since the release of `6.0.8`, when pipelines (or across multiple pipelines) with tcp input and beats input, both using TLS, the TCP input uses org.logstash.netty.SslContextBuilder defined here instead of the desired SslContextBuilder from the Netty library, which causes TCP pipelines to fail with `NoMethodError`. This commit removes the (unnecessary) `java_imports` from the `beats.rb` to avoid this. This commit also pins devutils to `~>1.3` to fix the build. * Bump version, add to changelog --- CHANGELOG.md | 4 ++++ VERSION | 2 +- lib/logstash/inputs/beats.rb | 5 ----- logstash-input-beats.gemspec | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 523062b6..2c355cb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.0.9 + - Fixed issue where calling `java_import` on `org.logstash.netty.SslContextBuilder` was causing the TCP input to pick up the wrong SslContextBuilder class + potentially causing pipeline creation to fail [#388](https://github.com/logstash-plugins/logstash-input-beats/pull/388) + ## 6.0.8 - Fixed issue where an SslContext was unnecessarily being created for each connection [#383](https://github.com/logstash-plugins/logstash-input-beats/pull/383) - Fixed issue where `end` was not being called when an Inflater was closed [#383](https://github.com/logstash-plugins/logstash-input-beats/pull/383) diff --git a/VERSION b/VERSION index 4b786f57..f1bb5eb8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.8 +6.0.9 diff --git a/lib/logstash/inputs/beats.rb b/lib/logstash/inputs/beats.rb index 6cd338cb..84f4a135 100644 --- a/lib/logstash/inputs/beats.rb +++ b/lib/logstash/inputs/beats.rb @@ -132,11 +132,6 @@ def register LogStash::Logger.setup_log4j(@logger) end - java_import "org.logstash.beats.Server" - java_import "org.logstash.netty.SslContextBuilder" - java_import "org.logstash.netty.SslHandlerProvider" - java_import "java.io.FileInputStream" - if !@ssl @logger.warn("Beats input: SSL Certificate will not be used") unless @ssl_certificate.nil? @logger.warn("Beats input: SSL Key will not be used") unless @ssl_key.nil? diff --git a/logstash-input-beats.gemspec b/logstash-input-beats.gemspec index 695056c1..9a226b01 100644 --- a/logstash-input-beats.gemspec +++ b/logstash-input-beats.gemspec @@ -33,7 +33,7 @@ Gem::Specification.new do |s| s.add_development_dependency "stud" s.add_development_dependency "pry" s.add_development_dependency "rspec-wait" - s.add_development_dependency "logstash-devutils" + s.add_development_dependency "logstash-devutils", '~>1.3' s.add_development_dependency "logstash-codec-json" s.add_development_dependency "childprocess" # To make filebeat/LSF integration test easier to write. From df4ce84c365e69de8b89e4ab069c99d2fce56874 Mon Sep 17 00:00:00 2001 From: Karol Bucek Date: Mon, 9 Mar 2020 14:48:40 +0100 Subject: [PATCH 20/39] Test: adjust for devutils 2.0 compat (#389) --- logstash-input-beats.gemspec | 2 +- spec/inputs/beats_spec.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/logstash-input-beats.gemspec b/logstash-input-beats.gemspec index 9a226b01..695056c1 100644 --- a/logstash-input-beats.gemspec +++ b/logstash-input-beats.gemspec @@ -33,7 +33,7 @@ Gem::Specification.new do |s| s.add_development_dependency "stud" s.add_development_dependency "pry" s.add_development_dependency "rspec-wait" - s.add_development_dependency "logstash-devutils", '~>1.3' + s.add_development_dependency "logstash-devutils" s.add_development_dependency "logstash-codec-json" s.add_development_dependency "childprocess" # To make filebeat/LSF integration test easier to write. diff --git a/spec/inputs/beats_spec.rb b/spec/inputs/beats_spec.rb index cb5341fd..f4124877 100644 --- a/spec/inputs/beats_spec.rb +++ b/spec/inputs/beats_spec.rb @@ -1,5 +1,6 @@ # encoding: utf-8 require_relative "../spec_helper" +require "logstash/devutils/rspec/shared_examples" require "stud/temporary" require "logstash/inputs/beats" require "logstash/codecs/plain" From c09bce19dbcf819c3d67b13567bcc769c26569a1 Mon Sep 17 00:00:00 2001 From: Joao Duarte Date: Tue, 17 Mar 2020 15:41:54 +0000 Subject: [PATCH 21/39] [skip ci] updated apache license --- LICENSE | 208 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 198 insertions(+), 10 deletions(-) diff --git a/LICENSE b/LICENSE index 72c342fc..a80a3fd5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,14 +1,202 @@ -Copyright 2012-2018 Jordan Sissel, Elasticsearch and contributors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -http://www.apache.org/licenses/LICENSE-2.0 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + 1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Elastic and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 532fc7f9cba4b827524d301cc5a49593627e22b0 Mon Sep 17 00:00:00 2001 From: Joao Duarte Date: Tue, 24 Mar 2020 17:06:31 +0000 Subject: [PATCH 22/39] update to centralized travis configuration update gradle to 5.6 add custom .ci/Dockerfile due to `openssl` binary needs add custom .ci/run.sh for integration testing --- .ci/Dockerfile | 23 +++ {ci/unit => .ci}/run.sh | 4 +- .travis.yml | 19 +-- build.gradle | 5 - ci/unit/Dockerfile | 17 --- ci/unit/docker-compose.yml | 17 --- ci/unit/docker-run.sh | 5 - ci/unit/docker-setup.sh | 55 ------- gradle/wrapper/gradle-wrapper.jar | Bin 54417 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 22 ++- gradlew.bat | 184 ++++++++++++----------- 12 files changed, 148 insertions(+), 206 deletions(-) create mode 100644 .ci/Dockerfile rename {ci/unit => .ci}/run.sh (97%) delete mode 100644 ci/unit/Dockerfile delete mode 100644 ci/unit/docker-compose.yml delete mode 100755 ci/unit/docker-run.sh delete mode 100755 ci/unit/docker-setup.sh diff --git a/.ci/Dockerfile b/.ci/Dockerfile new file mode 100644 index 00000000..7999e3c0 --- /dev/null +++ b/.ci/Dockerfile @@ -0,0 +1,23 @@ +ARG ELASTIC_STACK_VERSION +FROM docker.elastic.co/logstash/logstash:$ELASTIC_STACK_VERSION +USER root +RUN yum install -y openssl +USER logstash +COPY --chown=logstash:logstash Gemfile /usr/share/plugins/plugin/Gemfile +COPY --chown=logstash:logstash *.gemspec VERSION* version* /usr/share/plugins/plugin/ +RUN cp /usr/share/logstash/logstash-core/versions-gem-copy.yml /usr/share/logstash/versions.yml +ENV PATH="${PATH}:/usr/share/logstash/vendor/jruby/bin" +ENV LOGSTASH_SOURCE="1" +ENV ELASTIC_STACK_VERSION=$ELASTIC_STACK_VERSION +# DISTRIBUTION="default" (by default) or "oss" +ARG DISTRIBUTION +ENV DISTRIBUTION=$DISTRIBUTION +# INTEGRATION="true" while integration testing (false-y by default) +ARG INTEGRATION +ENV INTEGRATION=$INTEGRATION +RUN gem install bundler -v '< 2' +WORKDIR /usr/share/plugins/plugin +RUN bundle install --with test ci +COPY --chown=logstash:logstash . /usr/share/plugins/plugin +RUN bundle exec rake vendor +RUN .ci/setup.sh diff --git a/ci/unit/run.sh b/.ci/run.sh similarity index 97% rename from ci/unit/run.sh rename to .ci/run.sh index 451b5bd6..a0ec21c9 100755 --- a/ci/unit/run.sh +++ b/.ci/run.sh @@ -1,6 +1,8 @@ #!/bin/bash - # This is intended to be run inside the docker container as the command of the docker-compose. + +env + set -ex bundle exec rspec spec diff --git a/.travis.yml b/.travis.yml index 6df504c4..a50fc739 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,2 @@ ---- -sudo: required -services: docker -addons: - apt: - packages: - - docker-ce -matrix: - include: - - env: SNAPSHOT=true ELASTIC_STACK_VERSION=8.x - - env: SNAPSHOT=true ELASTIC_STACK_VERSION=7.x - - env: ELASTIC_STACK_VERSION=7.x - - env: ELASTIC_STACK_VERSION=6.x - - env: ELASTIC_STACK_VERSION=5.x - fast_finish: true -install: ci/unit/docker-setup.sh -script: ci/unit/docker-run.sh +import: +- logstash-plugins/.ci:travis/travis.yml@1.x \ No newline at end of file diff --git a/build.gradle b/build.gradle index 71cf8379..cd84c799 100644 --- a/build.gradle +++ b/build.gradle @@ -97,8 +97,3 @@ publishing { } } } - -task wrapper(type: Wrapper) { - description = 'Install Gradle wrapper' - gradleVersion = '2.14' -} diff --git a/ci/unit/Dockerfile b/ci/unit/Dockerfile deleted file mode 100644 index 4cf01114..00000000 --- a/ci/unit/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -ARG ELASTIC_STACK_VERSION -FROM docker.elastic.co/logstash/logstash:$ELASTIC_STACK_VERSION -USER root -RUN yum -y install openssl -USER logstash -COPY --chown=logstash:logstash Gemfile /usr/share/plugins/plugin/Gemfile -COPY --chown=logstash:logstash *.gemspec /usr/share/plugins/plugin/ -COPY --chown=logstash:logstash VERSION /usr/share/plugins/plugin/ -RUN cp /usr/share/logstash/logstash-core/versions-gem-copy.yml /usr/share/logstash/versions.yml -ENV PATH="${PATH}:/usr/share/logstash/vendor/jruby/bin" -ENV LOGSTASH_SOURCE=1 -RUN gem install bundler -v "~> 1" -WORKDIR /usr/share/plugins/plugin -RUN bundle install -COPY --chown=logstash:logstash . /usr/share/plugins/plugin -RUN bundle exec rake vendor -RUN ./gradlew test diff --git a/ci/unit/docker-compose.yml b/ci/unit/docker-compose.yml deleted file mode 100644 index edc5f6af..00000000 --- a/ci/unit/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: '3' - -# run tests: cd ci/unit; docker-compose up --build --force-recreate -# manual: cd ci/unit; docker-compose run logstash bash -services: - - logstash: - build: - context: ../../ - dockerfile: ci/unit/Dockerfile - args: - - ELASTIC_STACK_VERSION=$ELASTIC_STACK_VERSION - command: /usr/share/plugins/plugin/ci/unit/run.sh - environment: - LS_JAVA_OPTS: "-Xmx256m -Xms256m" - LOGSTASH_SOURCE: 1 - tty: true diff --git a/ci/unit/docker-run.sh b/ci/unit/docker-run.sh deleted file mode 100755 index e73aedfc..00000000 --- a/ci/unit/docker-run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# This is intended to be run inside the docker container as the command of the docker-compose. -set -ex -docker-compose -f ci/unit/docker-compose.yml up --exit-code-from logstash diff --git a/ci/unit/docker-setup.sh b/ci/unit/docker-setup.sh deleted file mode 100755 index 5ad8841b..00000000 --- a/ci/unit/docker-setup.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -# This is intended to be run the plugin's root directory. `ci/unit/docker-test.sh` -# Ensure you have Docker installed locally and set the ELASTIC_STACK_VERSION environment variable. -set -e - -VERSION_URL="https://raw.githubusercontent.com/elastic/logstash/master/ci/logstash_releases.json" - -if [ "$ELASTIC_STACK_VERSION" ]; then - echo "Fetching versions from $VERSION_URL" - VERSIONS=$(curl -s $VERSION_URL) - if [[ "$SNAPSHOT" = "true" ]]; then - ELASTIC_STACK_RETRIEVED_VERSION=$(echo $VERSIONS | jq '.snapshots."'"$ELASTIC_STACK_VERSION"'"') - echo $ELASTIC_STACK_RETRIEVED_VERSION - else - ELASTIC_STACK_RETRIEVED_VERSION=$(echo $VERSIONS | jq '.releases."'"$ELASTIC_STACK_VERSION"'"') - fi - if [[ "$ELASTIC_STACK_RETRIEVED_VERSION" != "null" ]]; then - # remove starting and trailing double quotes - ELASTIC_STACK_RETRIEVED_VERSION="${ELASTIC_STACK_RETRIEVED_VERSION%\"}" - ELASTIC_STACK_RETRIEVED_VERSION="${ELASTIC_STACK_RETRIEVED_VERSION#\"}" - echo "Translated $ELASTIC_STACK_VERSION to ${ELASTIC_STACK_RETRIEVED_VERSION}" - export ELASTIC_STACK_VERSION=$ELASTIC_STACK_RETRIEVED_VERSION - fi - - echo "Testing against version: $ELASTIC_STACK_VERSION" - if [[ "$ELASTIC_STACK_VERSION" = *"-SNAPSHOT" ]]; then - cd /tmp - - jq=".build.projects.logstash.packages.\"logstash-$ELASTIC_STACK_VERSION-docker-image.tar.gz\".url" - echo "curl --silent https://artifacts-api.elastic.co/v1/versions/$ELASTIC_STACK_VERSION/builds/latest | jq -r $jq)" - result=$(curl --silent https://artifacts-api.elastic.co/v1/versions/$ELASTIC_STACK_VERSION/builds/latest | jq -r $jq) - echo $result - curl $result > logstash-docker-image.tar.gz - tar xfvz logstash-docker-image.tar.gz repositories - echo "Loading docker image: " - cat repositories - docker load < logstash-docker-image.tar.gz - rm logstash-docker-image.tar.gz - cd - - fi - - if [ -f Gemfile.lock ]; then - rm Gemfile.lock - fi - - docker-compose -f ci/unit/docker-compose.yml down - docker-compose -f ci/unit/docker-compose.yml build - #docker-compose -f ci/unit/docker-compose.yml up --exit-code-from logstash --force-recreate -else - echo "Please set the ELASTIC_STACK_VERSION environment variable" - echo "For example: export ELASTIC_STACK_VERSION=6.2.4" - exit 1 -fi - diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 758de960ec7947253b058ff79c88ce51f3abe08a..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644 GIT binary patch literal 55616 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 90b27ef8..5028f28f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip -distributionSha256Sum=ce1645ff129d11aad62dab70d63426fdce6cfd646fa309dc5dc5255dd03c7c11 diff --git a/gradlew b/gradlew index cccdd3d5..83f2acfd 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -109,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` diff --git a/gradlew.bat b/gradlew.bat index f9553162..24467a14 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,100 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From e23cbefe984cd589e1c448416c729ffb0668a04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Duarte?= Date: Tue, 12 May 2020 11:15:18 +0100 Subject: [PATCH 23/39] detect if ssl certificate or key can't be read (#394) In the scenario where either the ssl certificate or key files can't be read by the user running Logstash, e.g. due to permissions, an error is generated by io.netty.handler.ssl.SslContextBuilder.forServer communicating that these files don't contain valid content: "java.lang.IllegalArgumentException: File does not contain valid certificates" or "java.lang.IllegalArgumentException: File does not contain valid private key" This is often misleading, causing users to question if their certificates are created incorrectly. So this commit checks first if these files can be read, and throws an exception dedicated to this error. bump to 6.0.10 closes #197 --- CHANGELOG.md | 3 +++ VERSION | 2 +- .../java/org/logstash/netty/SslContextBuilder.java | 10 +++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c355cb9..7977d0c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.0.10 + - Added error handling to detect if ssl certificate or key files can't be read [#394](https://github.com/logstash-plugins/logstash-input-beats/pull/394) + ## 6.0.9 - Fixed issue where calling `java_import` on `org.logstash.netty.SslContextBuilder` was causing the TCP input to pick up the wrong SslContextBuilder class potentially causing pipeline creation to fail [#388](https://github.com/logstash-plugins/logstash-input-beats/pull/388) diff --git a/VERSION b/VERSION index f1bb5eb8..c7d48f04 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.9 +6.0.10 diff --git a/src/main/java/org/logstash/netty/SslContextBuilder.java b/src/main/java/org/logstash/netty/SslContextBuilder.java index 8384a03d..b0bd945d 100644 --- a/src/main/java/org/logstash/netty/SslContextBuilder.java +++ b/src/main/java/org/logstash/netty/SslContextBuilder.java @@ -58,9 +58,17 @@ public enum SslClientVerifyMode { private String[] certificateAuthorities; private String passPhrase; - public SslContextBuilder(String sslCertificateFilePath, String sslKeyFilePath, String pass) { + public SslContextBuilder(String sslCertificateFilePath, String sslKeyFilePath, String pass) throws IllegalArgumentException { sslCertificateFile = new File(sslCertificateFilePath); + if (!sslCertificateFile.canRead()) { + throw new IllegalArgumentException( + String.format("Certificate file cannot be read. Please confirm the user running Logstash has permissions to read: %s", sslCertificateFilePath)); + } sslKeyFile = new File(sslKeyFilePath); + if (!sslKeyFile.canRead()) { + throw new IllegalArgumentException( + String.format("Private key file cannot be read. Please confirm the user running Logstash has permissions to read: %s", sslKeyFilePath)); + } passPhrase = pass; } From d0172897307b282f72585746f7c99cfa2b898f38 Mon Sep 17 00:00:00 2001 From: Rob Bavey Date: Wed, 3 Jun 2020 11:16:05 -0400 Subject: [PATCH 24/39] Update/remove dependencies (#393) * Update/remove dependencies This commit updates the following dependencies: Jackson-databind to `2.9.10.4` Netty to `4.1.49.final` Additionally, this commit removes the dependency on `tcnative` + `boringssl` from #384, which enables the update of netty dependencies while still allowing logstash->logstash communication via the lumberjack output plugin. (`boringssl` removed compatibility with `CBC` ciphers, which are still a prerequisite for lumberjack output plugin support at this time, This commit also handles JCE restrictions While it is been the default for Java since JDK 8u161 released in early 2018, old versions of Java may not have the JCE unlimited strength jurisdiction policy installed. This commit handles this case, warning the user that the policy is not installed, and presenting a reduced set of default ciphers for use. --- .ci/run.sh | 2 +- CHANGELOG.md | 6 +++ VERSION | 2 +- build.gradle | 6 +-- lib/logstash/inputs/beats.rb | 3 +- spec/integration/filebeat_spec.rb | 4 +- .../org/logstash/netty/SslContextBuilder.java | 47 ++++++++++++++++--- 7 files changed, 53 insertions(+), 17 deletions(-) diff --git a/.ci/run.sh b/.ci/run.sh index a0ec21c9..2d9dc683 100755 --- a/.ci/run.sh +++ b/.ci/run.sh @@ -7,4 +7,4 @@ set -ex bundle exec rspec spec bundle exec rake test:integration:setup -bundle exec rspec spec --tag integration +bundle exec rspec spec --tag integration -fd diff --git a/CHANGELOG.md b/CHANGELOG.md index 7977d0c0..60075dcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.0.11 + - Updated jackson databind and Netty dependencies. Additionally, this release removes the dependency on `tcnative` + + `boringssl`, using JVM supplied ciphers instead. This may result in fewer ciphers being available if the JCE + unlimited strength jurisdiction policy is not installed. (This policy is installed by default on versions of the + JDK from u161 onwards)[#393](https://github.com/logstash-plugins/logstash-input-beats/pull/393) + ## 6.0.10 - Added error handling to detect if ssl certificate or key files can't be read [#394](https://github.com/logstash-plugins/logstash-input-beats/pull/394) diff --git a/VERSION b/VERSION index c7d48f04..b619a5bf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.10 +6.0.11 diff --git a/build.gradle b/build.gradle index cd84c799..84cb9211 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ sourceCompatibility = 1.8 targetCompatibility = 1.8 String jacksonVersion = '2.9.10' -String jacksonDatabindVersion = '2.9.10.1' +String jacksonDatabindVersion = '2.9.10.4' repositories { mavenCentral() @@ -24,9 +24,7 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'org.apache.logging.log4j:log4j-core:2.11.1' - - compile 'io.netty:netty-all:4.1.30.Final' - compile 'io.netty:netty-tcnative-boringssl-static:2.0.12.Final' + compile 'io.netty:netty-all:4.1.49.Final' compile 'org.javassist:javassist:3.24.0-GA' compile "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" compile "com.fasterxml.jackson.core:jackson-annotations:${jacksonVersion}" diff --git a/lib/logstash/inputs/beats.rb b/lib/logstash/inputs/beats.rb index 84f4a135..2ea87340 100644 --- a/lib/logstash/inputs/beats.rb +++ b/lib/logstash/inputs/beats.rb @@ -114,8 +114,7 @@ class LogStash::Inputs::Beats < LogStash::Inputs::Base config :tls_max_version, :validate => :number, :default => TLS.max.version # The list of ciphers suite to use, listed by priorities. - config :cipher_suites, :validate => :array, :default => org.logstash.netty.SslContextBuilder::DEFAULT_CIPHERS - + config :cipher_suites, :validate => :array, :default => org.logstash.netty.SslContextBuilder.getDefaultCiphers # Close Idle clients after X seconds of inactivity. config :client_inactivity_timeout, :validate => :number, :default => 60 diff --git a/spec/integration/filebeat_spec.rb b/spec/integration/filebeat_spec.rb index fdfada6d..64244985 100644 --- a/spec/integration/filebeat_spec.rb +++ b/spec/integration/filebeat_spec.rb @@ -166,8 +166,8 @@ end context "when the cipher is not supported" do - let(:beats_cipher) { "ECDHE-RSA-AES-128-GCM-SHA256" } - let(:logstash_cipher) { "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"} + let(:beats_cipher) { "ECDHE-RSA-AES-256-GCM-SHA384" } + let(:logstash_cipher) { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} include_examples "doesn't send events" end diff --git a/src/main/java/org/logstash/netty/SslContextBuilder.java b/src/main/java/org/logstash/netty/SslContextBuilder.java index b0bd945d..92e64bc1 100644 --- a/src/main/java/org/logstash/netty/SslContextBuilder.java +++ b/src/main/java/org/logstash/netty/SslContextBuilder.java @@ -1,16 +1,18 @@ package org.logstash.netty; import io.netty.handler.ssl.ClientAuth; -import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.SslContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import javax.crypto.Cipher; +import javax.net.ssl.SSLServerSocketFactory; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; @@ -40,9 +42,8 @@ public enum SslClientVerifyMode { /* Mordern Ciphers List from https://wiki.mozilla.org/Security/Server_Side_TLS - This list require the OpenSSl engine for netty. */ - public final static String[] DEFAULT_CIPHERS = new String[] { + private final static String[] DEFAULT_CIPHERS = new String[] { "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", @@ -53,6 +54,18 @@ public enum SslClientVerifyMode { "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" }; + /* + Reduced set of ciphers available when JCE Unlimited Strength Jurisdiction Policy is not installed. + */ + private final static String[] DEFAULT_CIPHERS_LIMITED = new String[] { + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256" + }; + + private String[] supportedCiphers = ((SSLServerSocketFactory)SSLServerSocketFactory + .getDefault()).getSupportedCipherSuites(); private String[] ciphers = DEFAULT_CIPHERS; private String[] protocols = new String[] { "TLSv1.2" }; private String[] certificateAuthorities; @@ -79,10 +92,13 @@ public SslContextBuilder setProtocols(String[] protocols) { public SslContextBuilder setCipherSuites(String[] ciphersSuite) throws IllegalArgumentException { for(String cipher : ciphersSuite) { - if(!OpenSsl.isCipherSuiteAvailable(cipher)) { - throw new IllegalArgumentException("Cipher `" + cipher + "` is not available"); - } else { + if(Arrays.asList(supportedCiphers).contains(cipher)) { logger.debug("Cipher is supported: {}", cipher); + }else{ + if (!isUnlimitedJCEAvailable()) { + logger.warn("JCE Unlimited Strength Jurisdiction Policy not installed"); + } + throw new IllegalArgumentException("Cipher `" + cipher + "` is not available"); } } @@ -90,6 +106,23 @@ public SslContextBuilder setCipherSuites(String[] ciphersSuite) throws IllegalAr return this; } + public static String[] getDefaultCiphers(){ + if (isUnlimitedJCEAvailable()){ + return DEFAULT_CIPHERS; + } else { + logger.warn("JCE Unlimited Strength Jurisdiction Policy not installed - max key length is 128 bits"); + return DEFAULT_CIPHERS_LIMITED; + } + } + + public static boolean isUnlimitedJCEAvailable(){ + try { + return (Cipher.getMaxAllowedKeyLength("AES") > 128); + } catch (NoSuchAlgorithmException e) { + logger.warn("AES not available", e); + return false; + } + } public SslContextBuilder setCertificateAuthorities(String[] cert) { certificateAuthorities = cert; return this; @@ -112,7 +145,7 @@ public SslContext buildContext() throws IOException, CertificateException { io.netty.handler.ssl.SslContextBuilder builder = io.netty.handler.ssl.SslContextBuilder.forServer(sslCertificateFile, sslKeyFile, passPhrase); if (logger.isDebugEnabled()) { - logger.debug("Available ciphers: " + Arrays.toString(OpenSsl.availableOpenSslCipherSuites().toArray())); + logger.debug("Available ciphers: " + Arrays.toString(supportedCiphers)); logger.debug("Ciphers: " + Arrays.toString(ciphers)); } From ad6ec6f97eb1f3daffc4fa09a4fdada9c81dbaee Mon Sep 17 00:00:00 2001 From: andsel Date: Thu, 1 Oct 2020 14:39:42 +0200 Subject: [PATCH 25/39] Add explicitly the path to find the JVM bundled with the Docker image --- .ci/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 7999e3c0..db5c59e8 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -6,7 +6,7 @@ USER logstash COPY --chown=logstash:logstash Gemfile /usr/share/plugins/plugin/Gemfile COPY --chown=logstash:logstash *.gemspec VERSION* version* /usr/share/plugins/plugin/ RUN cp /usr/share/logstash/logstash-core/versions-gem-copy.yml /usr/share/logstash/versions.yml -ENV PATH="${PATH}:/usr/share/logstash/vendor/jruby/bin" +ENV PATH="${PATH}:/usr/share/logstash/vendor/jruby/bin:/usr/share/logstash/jdk/bin" ENV LOGSTASH_SOURCE="1" ENV ELASTIC_STACK_VERSION=$ELASTIC_STACK_VERSION # DISTRIBUTION="default" (by default) or "oss" From 2ab55f647d28fff900300e1002a2878b60375472 Mon Sep 17 00:00:00 2001 From: Karol Bucek Date: Tue, 1 Dec 2020 18:05:51 +0100 Subject: [PATCH 26/39] Fix: log error when SSL context building fails (#402) There's several changes to make sure plugin provides better user-experience when it comes to logging. The main motivator for the change set are key issues such as `java.lang.IllegalArgumentException: File does not contain valid private key` which are only logged upstream at a debug level (see #400). Thus to improve the logger experience there's 3 set of changes: - log when plugin's SSL context builder fails (user can easily spot key format errors) - make sure we log configuration errors before raising them (due https://github.com/elastic/logstash/issues/12484) - on SSL hand-shake failures extract the real-cause as an attempt to log a better info message e.g. Co-authored-by: Rob Bavey --- CHANGELOG.md | 4 + VERSION | 2 +- lib/logstash/inputs/beats.rb | 102 ++++++++++++------ spec/inputs/beats_spec.rb | 49 ++++++--- spec/support/logstash_test.rb | 7 ++ .../java/org/logstash/beats/BeatsHandler.java | 17 ++- 6 files changed, 130 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60075dcf..f01cece1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.0.12 + - Fix: log error when SSL context building fails [#402](https://github.com/logstash-plugins/logstash-input-beats/pull/402). + We've also made sure to log messages on configuration errors as LS 7.8/7.9 only prints details when level set to debug. + ## 6.0.11 - Updated jackson databind and Netty dependencies. Additionally, this release removes the dependency on `tcnative` + `boringssl`, using JVM supplied ciphers instead. This may result in fewer ciphers being available if the JCE diff --git a/VERSION b/VERSION index b619a5bf..9eaadd7b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.11 +6.0.12 diff --git a/lib/logstash/inputs/beats.rb b/lib/logstash/inputs/beats.rb index 2ea87340..9f491e44 100644 --- a/lib/logstash/inputs/beats.rb +++ b/lib/logstash/inputs/beats.rb @@ -131,27 +131,32 @@ def register LogStash::Logger.setup_log4j(@logger) end - if !@ssl - @logger.warn("Beats input: SSL Certificate will not be used") unless @ssl_certificate.nil? - @logger.warn("Beats input: SSL Key will not be used") unless @ssl_key.nil? - elsif !ssl_configured? - raise LogStash::ConfigurationError, "Certificate or Certificate Key not configured" - end + if @ssl + if @ssl_key.nil? || @ssl_key.empty? + configuration_error "ssl_key => is a required setting when ssl => true is configured" + end + if @ssl_certificate.nil? || @ssl_certificate.empty? + configuration_error "ssl_certificate => is a required setting when ssl => true is configured" + end - if @ssl && require_certificate_authorities? && !client_authentification? - raise LogStash::ConfigurationError, "Using `verify_mode` set to PEER or FORCE_PEER, requires the configuration of `certificate_authorities`" - end + if require_certificate_authorities? && !client_authentification? + configuration_error "ssl_certificate_authorities => is a required setting when ssl_verify_mode => '#{@ssl_verify_mode}' is configured" + end - if client_authentication_metadata? && !require_certificate_authorities? - raise LogStash::ConfigurationError, "Enabling `peer_metadata` requires using `verify_mode` set to PEER or FORCE_PEER" + if client_authentication_metadata? && !require_certificate_authorities? + configuration_error "Configuring ssl_peer_metadata => true requires ssl_verify_mode => to be configured with 'peer' or 'force_peer'" + end + else + @logger.warn("configured ssl_certificate => #{@ssl_certificate.inspect} will not be used") if @ssl_certificate + @logger.warn("configured ssl_key => #{@ssl_key.inspect} will not be used") if @ssl_key end # Logstash 6.x breaking change (introduced with 4.0.0 of this gem) if @codec.kind_of? LogStash::Codecs::Multiline - raise LogStash::ConfigurationError, "Multiline codec with beats input is not supported. Please refer to the beats documentation for how to best manage multiline data. See https://www.elastic.co/guide/en/beats/filebeat/current/multiline-examples.html" + configuration_error "Multiline codec with beats input is not supported. Please refer to the beats documentation for how to best manage multiline data. See https://www.elastic.co/guide/en/beats/filebeat/current/multiline-examples.html" end - @logger.info("Beats inputs: Starting input listener", :address => "#{@host}:#{@port}") + @logger.info("Starting input listener", :address => "#{@host}:#{@port}") @server = create_server end # def register @@ -159,37 +164,20 @@ def register def create_server server = org.logstash.beats.Server.new(@host, @port, @client_inactivity_timeout, @executor_threads) if @ssl - - begin - ssl_context_builder = org.logstash.netty.SslContextBuilder.new(@ssl_certificate, @ssl_key, @ssl_key_passphrase.nil? ? nil : @ssl_key_passphrase.value) - .setProtocols(convert_protocols) - .setCipherSuites(normalized_ciphers) - rescue java.lang.IllegalArgumentException => e - raise LogStash::ConfigurationError, e - end - - + ssl_context_builder = new_ssl_context_builder if client_authentification? - if @ssl_verify_mode.upcase == "FORCE_PEER" + if @ssl_verify_mode == "force_peer" ssl_context_builder.setVerifyMode(org.logstash.netty.SslContextBuilder::SslClientVerifyMode::FORCE_PEER) - elsif @ssl_verify_mode.upcase == "PEER" + elsif @ssl_verify_mode == "peer" ssl_context_builder.setVerifyMode(org.logstash.netty.SslContextBuilder::SslClientVerifyMode::VERIFY_PEER) end ssl_context_builder.setCertificateAuthorities(@ssl_certificate_authorities) end - server.setSslHandlerProvider(org.logstash.netty.SslHandlerProvider.new(ssl_context_builder.build_context, @ssl_handshake_timeout)) + server.setSslHandlerProvider(new_ssl_handshake_provider(ssl_context_builder)) end server end - def ssl_configured? - !(@ssl_certificate.nil? || @ssl_key.nil?) - end - - def target_codec_on_field? - !@target_codec_on_field.empty? - end - def run(output_queue) message_listener = MessageListener.new(output_queue, self) @server.setMessageListener(message_listener) @@ -200,6 +188,14 @@ def stop @server.stop unless @server.nil? end + def ssl_configured? + !(@ssl_certificate.nil? || @ssl_key.nil?) + end + + def target_codec_on_field? + !@target_codec_on_field.empty? + end + def client_authentification? @ssl_certificate_authorities && @ssl_certificate_authorities.size > 0 end @@ -216,6 +212,32 @@ def require_certificate_authorities? @ssl_verify_mode == "force_peer" || @ssl_verify_mode == "peer" end + private + + def new_ssl_handshake_provider(ssl_context_builder) + begin + org.logstash.netty.SslHandlerProvider.new(ssl_context_builder.build_context, @ssl_handshake_timeout) + rescue java.lang.IllegalArgumentException => e + @logger.error("SSL configuration invalid", error_details(e)) + raise LogStash::ConfigurationError, e + rescue java.security.GeneralSecurityException => e + @logger.error("SSL configuration failed", error_details(e, true)) + raise e + end + end + + def new_ssl_context_builder + passphrase = @ssl_key_passphrase.nil? ? nil : @ssl_key_passphrase.value + begin + org.logstash.netty.SslContextBuilder.new(@ssl_certificate, @ssl_key, passphrase) + .setProtocols(convert_protocols) + .setCipherSuites(normalized_ciphers) + rescue java.lang.IllegalArgumentException => e + @logger.error("SSL configuration invalid", error_details(e)) + raise LogStash::ConfigurationError, e + end + end + def normalized_ciphers @cipher_suites.map(&:upcase) end @@ -223,4 +245,16 @@ def normalized_ciphers def convert_protocols TLS.get_supported(@tls_min_version..@tls_max_version).map(&:name) end + + def configuration_error(message) + @logger.error message + raise LogStash::ConfigurationError, message + end + + def error_details(e, trace = false) + error_details = { :exception => e.class, :message => e.message } + error_details[:backtrace] = e.backtrace if trace || @logger.debug? + error_details + end + end diff --git a/spec/inputs/beats_spec.rb b/spec/inputs/beats_spec.rb index f4124877..51aec7ea 100644 --- a/spec/inputs/beats_spec.rb +++ b/spec/inputs/beats_spec.rb @@ -13,11 +13,19 @@ let(:certificate) { BeatsInputTest.certificate } let(:port) { BeatsInputTest.random_port } let(:queue) { Queue.new } - let(:config) { { "port" => 0, "ssl_certificate" => certificate.ssl_cert, "ssl_key" => certificate.ssl_key, "type" => "example", "tags" => "beats"} } + let(:config) do + { + "port" => 0, + "ssl_certificate" => certificate.ssl_cert, + "ssl_key" => certificate.ssl_key, + "type" => "example", + "tags" => "beats" + } + end context "#register" do context "host related configuration" do - let(:config) { super.merge!({ "host" => host, "port" => port, "client_inactivity_timeout" => client_inactivity_timeout, "executor_threads" => threads }) } + let(:config) { super.merge("host" => host, "port" => port, "client_inactivity_timeout" => client_inactivity_timeout, "executor_threads" => threads) } let(:host) { "192.168.1.20" } let(:port) { 9000 } let(:client_inactivity_timeout) { 400 } @@ -38,38 +46,55 @@ context "with ssl enabled" do context "without certificate configuration" do - let(:config) {{ "port" => 0, "ssl" => true, "ssl_key" => certificate.ssl_key, "type" => "example", "tags" => "beats" }} + let(:config) { { "port" => 0, "ssl" => true, "ssl_key" => certificate.ssl_key, "type" => "example" } } it "should fail to register the plugin with ConfigurationError" do plugin = LogStash::Inputs::Beats.new(config) - expect {plugin.register}.to raise_error(LogStash::ConfigurationError) + expect { plugin.register }.to raise_error(LogStash::ConfigurationError) end end context "without key configuration" do - let(:config) { { "port" => 0, "ssl" => true, "ssl_certificate" => certificate.ssl_cert, "type" => "example", "tags" => "Beats"} } + let(:config) { { "port" => 0, "ssl" => true, "ssl_certificate" => certificate.ssl_cert, "type" => "example" } } it "should fail to register the plugin with ConfigurationError" do plugin = LogStash::Inputs::Beats.new(config) - expect {plugin.register}.to raise_error(LogStash::ConfigurationError) + expect { plugin.register }.to raise_error(LogStash::ConfigurationError) + end + end + + context "with invalid key configuration" do + let(:p12_key) { certificate.p12_key } + let(:config) { { "port" => 0, "ssl" => true, "ssl_certificate" => certificate.ssl_cert, "ssl_key" => p12_key } } + it "should fail to register the plugin" do + plugin = LogStash::Inputs::Beats.new(config) + expect( plugin.logger ).to receive(:error) do |msg, opts| + expect( msg ).to match /.*?configuration invalid/ + expect( opts[:message] ).to match /does not contain valid private key/ + end + expect { plugin.register }.to raise_error(LogStash::ConfigurationError) end end context "with invalid ciphers" do - let(:config) { { "port" => 0, "ssl" => true, "ssl_certificate" => certificate.ssl_cert, "type" => "example", "tags" => "Beats", "cipher_suites" => "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA38"} } + let(:config) { super.merge("ssl" => true, "cipher_suites" => "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA38") } it "should raise a configuration error" do plugin = LogStash::Inputs::Beats.new(config) + expect( plugin.logger ).to receive(:error) do |msg, opts| + expect( msg ).to match /.*?configuration invalid/ + expect( opts[:message] ).to match /TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA38.*? not available/ + end expect { plugin.register }.to raise_error(LogStash::ConfigurationError) end end context "verify_mode" do context "verify_mode configured to PEER" do - let(:config) { { "port" => 0, "ssl" => true, "ssl_verify_mode" => "peer", "ssl_certificate" => certificate.ssl_cert, "ssl_key" => certificate.ssl_key, "type" => "example", "tags" => "Beats"} } + let(:config) { super.merge("ssl" => true, "ssl_verify_mode" => "peer") } it "raise a ConfigurationError when certificate_authorities is not set" do plugin = LogStash::Inputs::Beats.new(config) - expect {plugin.register}.to raise_error(LogStash::ConfigurationError, "Using `verify_mode` set to PEER or FORCE_PEER, requires the configuration of `certificate_authorities`") + expect {plugin.register}.to raise_error(LogStash::ConfigurationError, "ssl_certificate_authorities => is a required setting when ssl_verify_mode => 'peer' is configured") end it "doesn't raise a configuration error when certificate_authorities is set" do @@ -80,11 +105,11 @@ end context "verify_mode configured to FORCE_PEER" do - let(:config) { { "port" => 0, "ssl" => true, "ssl_verify_mode" => "force_peer", "ssl_certificate" => certificate.ssl_cert, "ssl_key" => certificate.ssl_key, "type" => "example", "tags" => "Beats"} } + let(:config) { super.merge("ssl" => true, "ssl_verify_mode" => "force_peer") } it "raise a ConfigurationError when certificate_authorities is not set" do plugin = LogStash::Inputs::Beats.new(config) - expect {plugin.register}.to raise_error(LogStash::ConfigurationError, "Using `verify_mode` set to PEER or FORCE_PEER, requires the configuration of `certificate_authorities`") + expect {plugin.register}.to raise_error(LogStash::ConfigurationError, "ssl_certificate_authorities => is a required setting when ssl_verify_mode => 'force_peer' is configured") end it "doesn't raise a configuration error when certificate_authorities is set" do @@ -98,7 +123,7 @@ context "with ssl disabled" do context "and certificate configuration" do - let(:config) { { "port" => 0, "ssl" => false, "ssl_certificate" => certificate.ssl_cert, "type" => "example", "tags" => "Beats" } } + let(:config) { { "port" => 0, "ssl" => false, "ssl_certificate" => certificate.ssl_cert, "type" => "example", "tags" => "Beats" } } it "should not fail" do plugin = LogStash::Inputs::Beats.new(config) diff --git a/spec/support/logstash_test.rb b/spec/support/logstash_test.rb index 4851f666..9fec9667 100644 --- a/spec/support/logstash_test.rb +++ b/spec/support/logstash_test.rb @@ -13,6 +13,13 @@ def initialize system("openssl req -x509 -batch -nodes -newkey rsa:2048 -keyout #{ssl_key} -out #{ssl_cert} -subj /CN=localhost > /dev/null 2>&1") end + + def p12_key + p12_key = Stud::Temporary.pathname("p12_key") + system "openssl pkcs12 -export -passout pass:123 -inkey #{ssl_key} -in #{ssl_cert} -out #{p12_key}" + p12_key + end + end class << self diff --git a/src/main/java/org/logstash/beats/BeatsHandler.java b/src/main/java/org/logstash/beats/BeatsHandler.java index eeaf7b71..16564222 100644 --- a/src/main/java/org/logstash/beats/BeatsHandler.java +++ b/src/main/java/org/logstash/beats/BeatsHandler.java @@ -81,12 +81,12 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E if (!(cause instanceof SSLHandshakeException)) { messageListener.onException(ctx, cause); } - String causeMessage = cause.getMessage() == null ? cause.getClass().toString() : cause.getMessage(); - + final Throwable realCause = extractCause(cause, 0); if (logger.isDebugEnabled()){ - logger.debug(format("Handling exception: " + causeMessage), cause); + logger.debug(format("Handling exception: " + cause + " (caused by: " + realCause + ")"), cause); + } else { + logger.info(format("Handling exception: " + cause + " (caused by: " + realCause + ")")); } - logger.info(format("Handling exception: " + causeMessage)); } finally{ super.exceptionCaught(ctx, cause); ctx.flush(); @@ -133,4 +133,13 @@ private String format(String message) { return "[local: " + localhost + ", remote: " + remotehost + "] " + message; } + + private static final int MAX_CAUSE_NESTING = 10; + + private static Throwable extractCause(final Throwable ex, final int nesting) { + final Throwable cause = ex.getCause(); + if (cause == null || cause == ex) return ex; + if (nesting >= MAX_CAUSE_NESTING) return cause; // do not recurse infinitely + return extractCause(cause, nesting + 1); + } } From cb842ab856e4a8b51decc1df05fec6128b4e38a9 Mon Sep 17 00:00:00 2001 From: Joao Duarte Date: Mon, 4 Jan 2021 12:49:20 +0000 Subject: [PATCH 27/39] [skip ci] update travis ci badge from .org to .com --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c81f768f..00c56164 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Logstash Plugin -[![Travis Build Status](https://travis-ci.org/logstash-plugins/logstash-input-beats.svg)](https://travis-ci.org/logstash-plugins/logstash-input-beats) +[![Travis Build Status](https://travis-ci.com/logstash-plugins/logstash-input-beats.svg)](https://travis-ci.com/logstash-plugins/logstash-input-beats) This is a plugin for [Logstash](https://github.com/elastic/logstash). From 872dfd9d698343867a71f6de32406fed79d41b7b Mon Sep 17 00:00:00 2001 From: Karen Metts <35154725+karenzone@users.noreply.github.com> Date: Tue, 12 Jan 2021 13:11:01 -0500 Subject: [PATCH 28/39] Doc: Replace cross doc links with attributes (#403) Bump to v.6.0.13 --- CHANGELOG.md | 3 +++ VERSION | 2 +- docs/index.asciidoc | 11 ++++++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f01cece1..81e21f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.0.13 + - [DOC] Update links to use shared attributes + ## 6.0.12 - Fix: log error when SSL context building fails [#402](https://github.com/logstash-plugins/logstash-input-beats/pull/402). We've also made sure to log messages on configuration errors as LS 7.8/7.9 only prints details when level set to debug. diff --git a/VERSION b/VERSION index 9eaadd7b..dd6bb261 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.12 +6.0.13 diff --git a/docs/index.asciidoc b/docs/index.asciidoc index bc9c1d35..8f25e2f0 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -56,11 +56,12 @@ NOTE: If ILM is not being used, set `index` to Logstash creates an index per day, based on the `@timestamp` value of the events coming from Beats. -IMPORTANT: If you are shipping events that span multiple lines, you need to -use the https://www.elastic.co/guide/en/beats/filebeat/current/multiline-examples.html[configuration options available in Filebeat] to handle multiline events -before sending the event data to Logstash. You cannot use the -{logstash-ref}/plugins-codecs-multiline.html[Multiline codec plugin] to handle multiline events. Doing so will -result in the failure to start Logstash. +IMPORTANT: If you are shipping events that span multiple lines, you need to use +the {filebeat-ref}/multiline-examples.html[configuration options available in +Filebeat] to handle multiline events before sending the event data to Logstash. +You cannot use the {logstash-ref}/plugins-codecs-multiline.html[Multiline codec +plugin] to handle multiline events. Doing so will result in the failure to start +Logstash. [id="plugins-{type}s-{plugin}-versioned-indexes"] ==== Versioned Beats Indices From af619302fe9f67be76b98be608e559526955ab67 Mon Sep 17 00:00:00 2001 From: Karol Bucek Date: Tue, 26 Jan 2021 13:30:37 +0100 Subject: [PATCH 29/39] Feat: log + unwrap generic SSL context exceptions (#405) The unwrapping at the Java level is for exceptions wrapped by Netty. These exceptions as they are are rather useless (unless they're properly logged with a cause's printStackTrace), and since we're pretty much type-less Ruby on the layer above we should rather propagate the cause ... Full exception details will be logged at debug level from the Java side - since we seem to prefer (manual) exception logging at the plugin level. We also make sure to log cause, if any, on the Ruby side which now catches all (expected) Java exceptions. --- CHANGELOG.md | 3 +++ VERSION | 2 +- lib/logstash/inputs/beats.rb | 7 ++++++- .../org/logstash/netty/SslContextBuilder.java | 20 +++++++++++++++++-- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81e21f0a..f011f669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.0.14 + - Feat: log + unwrap generic SSL context exceptions [#405](https://github.com/logstash-plugins/logstash-input-beats/pull/405) + ## 6.0.13 - [DOC] Update links to use shared attributes diff --git a/VERSION b/VERSION index dd6bb261..eabfd97e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.13 +6.0.14 diff --git a/lib/logstash/inputs/beats.rb b/lib/logstash/inputs/beats.rb index 9f491e44..36817798 100644 --- a/lib/logstash/inputs/beats.rb +++ b/lib/logstash/inputs/beats.rb @@ -220,7 +220,7 @@ def new_ssl_handshake_provider(ssl_context_builder) rescue java.lang.IllegalArgumentException => e @logger.error("SSL configuration invalid", error_details(e)) raise LogStash::ConfigurationError, e - rescue java.security.GeneralSecurityException => e + rescue java.lang.Exception => e # java.security.GeneralSecurityException @logger.error("SSL configuration failed", error_details(e, true)) raise e end @@ -254,6 +254,11 @@ def configuration_error(message) def error_details(e, trace = false) error_details = { :exception => e.class, :message => e.message } error_details[:backtrace] = e.backtrace if trace || @logger.debug? + cause = e.cause + if cause && e != cause + error_details[:cause] = { :exception => cause.class, :message => cause.message } + error_details[:cause][:backtrace] = cause.backtrace if trace || @logger.debug? + end error_details end diff --git a/src/main/java/org/logstash/netty/SslContextBuilder.java b/src/main/java/org/logstash/netty/SslContextBuilder.java index 92e64bc1..a65d1952 100644 --- a/src/main/java/org/logstash/netty/SslContextBuilder.java +++ b/src/main/java/org/logstash/netty/SslContextBuilder.java @@ -6,6 +6,7 @@ import org.apache.logging.log4j.Logger; import javax.crypto.Cipher; +import javax.net.ssl.SSLException; import javax.net.ssl.SSLServerSocketFactory; import java.io.File; import java.io.FileInputStream; @@ -141,7 +142,7 @@ public File getSslCertificateFile() { return sslCertificateFile; } - public SslContext buildContext() throws IOException, CertificateException { + public SslContext buildContext() throws Exception { io.netty.handler.ssl.SslContextBuilder builder = io.netty.handler.ssl.SslContextBuilder.forServer(sslCertificateFile, sslKeyFile, passPhrase); if (logger.isDebugEnabled()) { @@ -167,7 +168,22 @@ public SslContext buildContext() throws IOException, CertificateException { builder.clientAuth(ClientAuth.NONE); } builder.protocols(protocols); - return builder.build(); + + try { + return builder.build(); + } catch (SSLException e) { + logger.debug("Failed to initialize SSL", e); + // unwrap generic wrapped exception from Netty's JdkSsl{Client|Server}Context + if ("failed to initialize the server-side SSL context".equals(e.getMessage()) || + "failed to initialize the client-side SSL context".equals(e.getMessage())) { + // Netty catches Exception and simply wraps: throw new SSLException("...", e); + if (e.getCause() instanceof Exception) throw (Exception) e.getCause(); + } + throw e; + } catch (Exception e) { + logger.debug("Failed to initialize SSL", e); + throw e; + } } private X509Certificate[] loadCertificateCollection(String[] certificates) throws IOException, CertificateException { From 1a746f95bb6bb2a8a2b3639546289454936366ed Mon Sep 17 00:00:00 2001 From: andsel Date: Tue, 19 Jan 2021 16:49:49 +0100 Subject: [PATCH 30/39] Introduced ECS compatibility (PR #404) ECS remapping: - 'host' => '[@metadata][input][beats][host][name]' if 'add_hostname' is true - '@metadata.ip_address' => '[@metadata][input][beats][host][ip]' - '@metadata.tls_peer.protocol' => '[@metadata][input][beats][tls][version_protocol]' - '@metadata.tls_peer.subject' => '[@metadata][input][beats][tls][client][subject]' - '@metadata.tls_peer.cipher_suite' => '[@metadata][input][beats][tls][cipher]' Warn: '@metadata.tls_peer.status' is not remapped becuase there isn't a 1:1 mapping in ECS concepts Co-authored-by: Karen Metts <35154725+karenzone@users.noreply.github.com> Fix #387 --- CHANGELOG.md | 7 +- VERSION | 2 +- docs/index.asciidoc | 36 +++++ lib/logstash/inputs/beats.rb | 13 ++ .../inputs/beats/decoded_event_transform.rb | 11 +- .../inputs/beats/event_transform_common.rb | 4 +- lib/logstash/inputs/beats/message_listener.rb | 35 ++++- logstash-input-beats.gemspec | 1 + .../beats/decoded_event_transform_spec.rb | 3 +- .../beats/event_transform_common_spec.rb | 3 +- spec/inputs/beats/message_listener_spec.rb | 136 +++++++++++------- spec/inputs/beats/raw_event_transform_spec.rb | 3 +- spec/support/shared_examples.rb | 60 ++++---- 13 files changed, 220 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f011f669..d37eb002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ +## 6.1.0 + - ECS compatibility enablement, now an `ecs_compatibility` setting is used to declare the level of ECS compatibility + (`disabled` or `v1`) at plugin level. `disabled` let the plugin behave like before while `v1` does a rename of + `host` and `@metadata.ip_address` event fields. [404](https://github.com/logstash-plugins/logstash-input-beats/pull/404) + ## 6.0.14 - - Feat: log + unwrap generic SSL context exceptions [#405](https://github.com/logstash-plugins/logstash-input-beats/pull/405) +- Feat: log + unwrap generic SSL context exceptions [#405](https://github.com/logstash-plugins/logstash-input-beats/pull/405) ## 6.0.13 - [DOC] Update links to use shared attributes diff --git a/VERSION b/VERSION index eabfd97e..dfda3e0b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.14 +6.1.0 diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 8f25e2f0..919deba6 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -86,6 +86,14 @@ Logstash `@timestamp` field. This configuration results in daily index names like +filebeat-{logstash_version}-{localdate}+. + +[id="plugins-{type}s-{plugin}-ecs_metadata"] +==== Event Metadata and the Elastic Common Schema (ECS) +When decoding `beats` events, this plugin adds two fields related to the event: the deprecated `host` +which contains the `hostname` provided by beats and the `ip_address` containing the remote address +of the client's connection. When <> is +enabled these are now moved in ECS compatible namespace. + [id="plugins-{type}s-{plugin}-options"] ==== Beats Input Configuration Options @@ -97,6 +105,7 @@ This plugin supports the following configuration options plus the <> |<>|No | <> |<>|No | <> |<>|No +| <> | <>|No | <> |<>|No | <> |<>|No | <> |<>|Yes @@ -144,6 +153,33 @@ The list of ciphers suite to use, listed by priorities. Close Idle clients after X seconds of inactivity. +[id="plugins-{type}s-{plugin}-ecs_compatibility"] +===== `ecs_compatibility` + +* Value type is <> +* Supported values are: +** `disabled`: unstructured connection metadata added at root level +** `v1`: structured connection metadata added under ECS compliant namespaces +* Default value depends on which version of Logstash is running: +** When Logstash provides a `pipeline.ecs_compatibility` setting, its value is used as the default +** Otherwise, the default value is `disabled`. + +Controls this plugin's compatibility with the {ecs-ref}[Elastic Common Schema (ECS)]. +The value of this setting affects the keys for the Beats connection's metadata on the event: + +.Metadata Location by `ecs_compatibility` value +[cols=" :number, :default => LogStash::Config::CpuCoreStrategy.maximum + attr_reader :field_hostname, :field_hostip + def register # For Logstash 2.4 we need to make sure that the logger is correctly set for the # java classes before actually loading them. @@ -156,6 +162,13 @@ def register configuration_error "Multiline codec with beats input is not supported. Please refer to the beats documentation for how to best manage multiline data. See https://www.elastic.co/guide/en/beats/filebeat/current/multiline-examples.html" end + # define ecs name mapping + @field_hostname = ecs_select[disabled: "host", v1: "[@metadata][input][beats][host][name]"] + @field_hostip = ecs_select[disabled: "[@metadata][ip_address]", v1: "[@metadata][input][beats][host][ip]"] + @field_tls_protocol_version = ecs_select[disabled: "[@metadata][tls_peer][protocol]", v1: "[@metadata][input][beats][tls][version_protocol]"] + @field_tls_peer_subject = ecs_select[disabled: "[@metadata][tls_peer][subject]", v1: "[@metadata][input][beats][tls][client][subject]"] + @field_tls_cipher = ecs_select[disabled: "[@metadata][tls_peer][cipher_suite]", v1: "[@metadata][input][beats][tls][cipher]"] + @logger.info("Starting input listener", :address => "#{@host}:#{@port}") @server = create_server diff --git a/lib/logstash/inputs/beats/decoded_event_transform.rb b/lib/logstash/inputs/beats/decoded_event_transform.rb index d040db31..deaf4c6f 100644 --- a/lib/logstash/inputs/beats/decoded_event_transform.rb +++ b/lib/logstash/inputs/beats/decoded_event_transform.rb @@ -9,7 +9,16 @@ def transform(event, hash) ts = coerce_ts(hash.delete("@timestamp")) event.set("@timestamp", ts) unless ts.nil? - hash.each { |k, v| event.set(k, v) } + hash.each do |k, v| + #could be a nested map, so we need to merge and not overwrite + existing_value = event.get(k) + if existing_value.is_a?(Hash) + existing_value = existing_value.merge(v) + else + existing_value = v + end + event.set(k, existing_value) + end super(event) event.tag("beats_input_codec_#{codec_name}_applied") if include_codec_tag? event diff --git a/lib/logstash/inputs/beats/event_transform_common.rb b/lib/logstash/inputs/beats/event_transform_common.rb index f8bc6d39..37d6c016 100644 --- a/lib/logstash/inputs/beats/event_transform_common.rb +++ b/lib/logstash/inputs/beats/event_transform_common.rb @@ -15,8 +15,8 @@ def copy_beat_hostname(event) return unless @input.add_hostname host = event.get("[beat][hostname]") - if host && event.get("host").nil? - event.set("host", host) + if host && event.get(@input.field_hostname).nil? + event.set(@input.field_hostname, host) end end diff --git a/lib/logstash/inputs/beats/message_listener.rb b/lib/logstash/inputs/beats/message_listener.rb index 177856e2..23a44ff5 100644 --- a/lib/logstash/inputs/beats/message_listener.rb +++ b/lib/logstash/inputs/beats/message_listener.rb @@ -31,7 +31,9 @@ def onNewMessage(ctx, message) hash = message.getData ip_address = ip_address(ctx) - hash['@metadata']['ip_address'] = ip_address unless ip_address.nil? || hash['@metadata'].nil? + unless ip_address.nil? || hash['@metadata'].nil? + set_nested(hash, @input.field_hostip, ip_address) + end target_field = extract_target_field(hash) extract_tls_peer(hash, ctx) @@ -140,11 +142,12 @@ def extract_tls_peer(hash, ctx) end if tls_verified + set_nested(hash, @field_tls_protocol_version, tls_session.getProtocol()) + set_nested(hash, @field_tls_peer_subject, tls_session.getPeerPrincipal().getName()) + set_nested(hash, @field_tls_cipher, tls_session.getCipherSuite()) + hash['@metadata']['tls_peer'] = { - :status => "verified", - :protocol => tls_session.getProtocol(), - :subject => tls_session.getPeerPrincipal().getName(), - :cipher_suite => tls_session.getCipherSuite() + :status => "verified" } else hash['@metadata']['tls_peer'] = { @@ -154,6 +157,28 @@ def extract_tls_peer(hash, ctx) end end + # set the value for field_name into the hash, nesting into sub-hashes and creating hashes where necessary + public #only to make it testable + def set_nested(hash, field_name, value) + field_ref = Java::OrgLogstash::FieldReference.from(field_name) + # create @metadata sub-hash if needed + if field_ref.type == Java::OrgLogstash::FieldReference::META_CHILD + unless hash.key?("@metadata") + hash["@metadata"] = {} + end + nesting_hash = hash["@metadata"] + else + nesting_hash = hash + end + + field_ref.path.each do |token| + nesting_hash[token] = {} unless nesting_hash.key?(token) + nesting_hash = nesting_hash[token] + end + nesting_hash[field_ref.key] = value + end + + private def extract_target_field(hash) if from_filebeat?(hash) hash.delete(FILEBEAT_LOG_LINE_FIELD).to_s diff --git a/logstash-input-beats.gemspec b/logstash-input-beats.gemspec index 695056c1..e690b91d 100644 --- a/logstash-input-beats.gemspec +++ b/logstash-input-beats.gemspec @@ -27,6 +27,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency "thread_safe", "~> 0.3.5" s.add_runtime_dependency "logstash-codec-multiline", ">= 2.0.5" s.add_runtime_dependency 'jar-dependencies', '~> 0.3', '>= 0.3.4' + s.add_runtime_dependency 'logstash-mixin-ecs_compatibility_support', '~>1.1' s.add_development_dependency "flores", "~>0.0.6" s.add_development_dependency "rspec" diff --git a/spec/inputs/beats/decoded_event_transform_spec.rb b/spec/inputs/beats/decoded_event_transform_spec.rb index 23208f40..086fa666 100644 --- a/spec/inputs/beats/decoded_event_transform_spec.rb +++ b/spec/inputs/beats/decoded_event_transform_spec.rb @@ -29,7 +29,8 @@ subject { described_class.new(input).transform(event, map) } - include_examples "Common Event Transformation" + include_examples "Common Event Transformation", :disabled, "host" + include_examples "Common Event Transformation", :v1, "[@metadata][input][beats][host][name]" it "tags the event" do expect(subject.get("tags")).to include("beats_input_codec_plain_applied") diff --git a/spec/inputs/beats/event_transform_common_spec.rb b/spec/inputs/beats/event_transform_common_spec.rb index 53104161..7e9f75d3 100644 --- a/spec/inputs/beats/event_transform_common_spec.rb +++ b/spec/inputs/beats/event_transform_common_spec.rb @@ -7,5 +7,6 @@ describe LogStash::Inputs::Beats::EventTransformCommon do subject { described_class.new(input).transform(event) } - include_examples "Common Event Transformation" + include_examples "Common Event Transformation", :disabled, "host" + include_examples "Common Event Transformation", :v1, "[@metadata][input][beats][host][name]" end diff --git a/spec/inputs/beats/message_listener_spec.rb b/spec/inputs/beats/message_listener_spec.rb index 5cd1dcd6..a68be7f6 100644 --- a/spec/inputs/beats/message_listener_spec.rb +++ b/spec/inputs/beats/message_listener_spec.rb @@ -52,10 +52,73 @@ def flush(&block) end end +shared_examples "when the message is from any libbeat" do |ecs_compatibility, host_field_name| + let(:input) do + input = LogStash::Inputs::Beats.new({ "port" => 5555, "codec" => codec, "ecs_compatibility" => "#{ecs_compatibility}" }) + input.register + input + end + + #Requires data modeled as Java, not Ruby since the actual code pulls from Java backed (Netty) object + let(:data) do + d = HashMap.new + d.put('@metadata', HashMap.new) + d.put('metric', 1) + d.put('name', "super-stats") + d + end + + let(:message) { MockMessage.new("abc", data)} + + it "extract the event" do + subject.onNewMessage(ctx, message) + event = queue.pop + expect(event.get("message")).to be_nil + expect(event.get("metric")).to eq(1) + expect(event.get("name")).to eq("super-stats") + expect(event.get(host_field_name)).to eq(ip_address) + end + + context 'when the remote address is nil' do + let(:ctx) { OngoingMethodMock.new("remoteAddress", nil)} + + it 'extracts the event' do + subject.onNewMessage(ctx, message) + event = queue.pop + expect(event.get("message")).to be_nil + expect(event.get("metric")).to eq(1) + expect(event.get("name")).to eq("super-stats") + expect(event.get(host_field_name)).to eq(nil) + end + end + + context 'when getting the remote address raises' do + let(:raising_ctx) { double("context")} + + before do + allow(raising_ctx).to receive(:channel).and_raise("nope") + subject.onNewConnection(raising_ctx) + end + + it 'extracts the event' do + subject.onNewMessage(raising_ctx, message) + event = queue.pop + expect(event.get("message")).to be_nil + expect(event.get("metric")).to eq(1) + expect(event.get("name")).to eq("super-stats") + expect(event.get(host_field_name)).to eq(nil) + end + end +end + describe LogStash::Inputs::Beats::MessageListener do let(:queue) { Queue.new } let(:codec) { DummyCodec.new } - let(:input) { LogStash::Inputs::Beats.new({ "port" => 5555, "codec" => codec }) } + let(:input) do + input = LogStash::Inputs::Beats.new({ "port" => 5555, "codec" => codec }) + input.register + input + end let(:ip_address) { "10.0.0.1" } let(:remote_address) { OngoingMethodMock.new("getHostAddress", ip_address) } @@ -146,59 +209,8 @@ def flush(&block) end end - context "when the message is from any libbeat" do - #Requires data modeled as Java, not Ruby since the actual code pulls from Java backed (Netty) object - let(:data) do - d = HashMap.new - d.put('@metadata', HashMap.new) - d.put('metric', 1) - d.put('name', "super-stats") - d - end - - let(:message) { MockMessage.new("abc", data)} - - it "extract the event" do - subject.onNewMessage(ctx, message) - event = queue.pop - expect(event.get("message")).to be_nil - expect(event.get("metric")).to eq(1) - expect(event.get("name")).to eq("super-stats") - expect(event.get("[@metadata][ip_address]")).to eq(ip_address) - end - - context 'when the remote address is nil' do - let(:ctx) { OngoingMethodMock.new("remoteAddress", nil)} - - it 'extracts the event' do - subject.onNewMessage(ctx, message) - event = queue.pop - expect(event.get("message")).to be_nil - expect(event.get("metric")).to eq(1) - expect(event.get("name")).to eq("super-stats") - expect(event.get("[@metadata][ip_address]")).to eq(nil) - end - end - - context 'when getting the remote address raises' do - let(:raising_ctx) { double("context")} - - before do - allow(raising_ctx).to receive(:channel).and_raise("nope") - subject.onNewConnection(raising_ctx) - end - - it 'extracts the event' do - subject.onNewMessage(raising_ctx, message) - event = queue.pop - expect(event.get("message")).to be_nil - expect(event.get("metric")).to eq(1) - expect(event.get("name")).to eq("super-stats") - expect(event.get("[@metadata][ip_address]")).to eq(nil) - end - end - - end + it_behaves_like "when the message is from any libbeat", :disabled, "[@metadata][ip_address]" + it_behaves_like "when the message is from any libbeat", :v1, "[@metadata][input][beats][host][ip]" end context "onException" do @@ -222,4 +234,20 @@ def flush(&block) expect(queue).not_to be_empty end end + + context "set_nested" do + let(:hash) {{}} + + it "creates correctly the nested maps" do + subject.set_nested(hash, "[root][inner][leaf]", 5) + expect(hash["root"]["inner"]["leaf"]).to eq(5) + end + + it "doesn't overwrite existing the nested maps" do + hash = {"root" => {"foo" => {"bar" => "Hello"}}} + subject.set_nested(hash, "[root][inner][leaf]", 5) + expect(hash["root"]["inner"]["leaf"]).to eq(5) + expect(hash["root"]["foo"]["bar"]).to eq("Hello") + end + end end diff --git a/spec/inputs/beats/raw_event_transform_spec.rb b/spec/inputs/beats/raw_event_transform_spec.rb index 4c1dc6eb..28c4557e 100644 --- a/spec/inputs/beats/raw_event_transform_spec.rb +++ b/spec/inputs/beats/raw_event_transform_spec.rb @@ -18,7 +18,8 @@ subject { described_class.new(input).transform(event) } - include_examples "Common Event Transformation" + include_examples "Common Event Transformation", :disabled, "host" + include_examples "Common Event Transformation", :v1, "[@metadata][input][beats][host][name]" it "tags the event" do expect(subject.get("tags")).to include("beats_input_raw_event") diff --git a/spec/support/shared_examples.rb b/spec/support/shared_examples.rb index da1c37ff..8a80bd7d 100644 --- a/spec/support/shared_examples.rb +++ b/spec/support/shared_examples.rb @@ -1,5 +1,5 @@ # encoding: utf-8 -shared_examples "Common Event Transformation" do +shared_examples "Common Event Transformation" do |ecs_compatibility, host_field_name| let(:tag) { "140-rpm-beats" } let(:config) do { @@ -21,12 +21,18 @@ } end + def key_as_nested_maps(key, value) + evt = LogStash::Event.new + evt.set(key, value) + evt.to_hash_with_metadata + end + it "adds configured tags to the event" do expect(subject.get("tags")).to include(tag) end context 'when add_hostname is true' do - let(:config) { super.merge({'add_hostname' => true})} + let(:config) { super.merge({'add_hostname' => true, 'ecs_compatibility' => ecs_compatibility})} context 'when a host is provided in beat.host.name' do let(:already_exist) { "already_exist" } @@ -53,35 +59,35 @@ let(:producer_host) { "newhost01" } let(:event_map) { super.merge({ "beat" => { "hostname" => producer_host }}) } - context "when no `host` key already exists on the event" do - it "copies the value in `beat.hostname` to `host`" do - expect(subject.get("host")).to eq(producer_host) + context "when no `#{host_field_name}` key already exists on the event" do + it "copies the value in `beat.hostname` to `#{host_field_name}`" do + expect(subject.get(host_field_name)).to eq(producer_host) end end - context "when `host` key exists on the event" do + context "when `#{host_field_name}` key exists on the event" do let(:already_exist) { "already_exist" } - let(:event_map) { super.merge({ "host" => already_exist }) } + let(:event_map) { super.merge(key_as_nested_maps(host_field_name, already_exist)) } it "doesn't override it" do - expect(subject.get("host")).to eq(already_exist) + expect(subject.get(host_field_name)).to eq(already_exist) end end end context "when no host is provided in beat" do - context "when no `host` key already exists on the event" do + context "when no `#{host_field_name}` key already exists on the event" do it "does not set the host" do - expect(subject.get("host")).to be_nil + expect(subject.get(host_field_name)).to be_nil end end - context "when `host` key already exists on the event" do + context "when `#{host_field_name}` key already exists on the event" do let(:already_exist) { "already_exist" } - let(:event_map) { super.merge({ "host" => already_exist }) } + let(:event_map) { super.merge(key_as_nested_maps(host_field_name, already_exist)) } it "doesn't override it" do - expect(subject.get("host")).to eq(already_exist) + expect(subject.get(host_field_name)).to eq(already_exist) end end end @@ -95,18 +101,18 @@ let(:producer_host) { "newhost01" } let(:event_map) { super.merge({ "beat" => { "host" => {"name" => producer_host }}}) } - context "when no `host` key already exists on the event" do + context "when no `#{host_field_name}` key already exists on the event" do it "does not set the host" do - expect(subject.get("host")).to be_nil + expect(subject.get(host_field_name)).to be_nil end end - context "when `host` key already exists on the event" do + context "when `#{host_field_name}` key already exists on the event" do let(:already_exist) { "already_exist" } - let(:event_map) { super.merge({ "host" => already_exist }) } + let(:event_map) { super.merge(key_as_nested_maps(host_field_name, already_exist)) } it "doesn't override it" do - expect(subject.get("host")).to eq(already_exist) + expect(subject.get(host_field_name)).to eq(already_exist) end end end @@ -115,35 +121,35 @@ let(:producer_host) { "newhost01" } let(:event_map) { super.merge({ "beat" => { "hostname" => producer_host }}) } - context "when no `host` key already exists on the event" do + context "when no `#{host_field_name}` key already exists on the event" do it "does not set the host" do - expect(subject.get("host")).to be_nil + expect(subject.get(host_field_name)).to be_nil end end context "when `host` key already exists on the event" do let(:already_exist) { "already_exist" } - let(:event_map) { super.merge({ "host" => already_exist }) } + let(:event_map) { super.merge(key_as_nested_maps(host_field_name, already_exist)) } it "doesn't override it" do - expect(subject.get("host")).to eq(already_exist) + expect(subject.get(host_field_name)).to eq(already_exist) end end end context "when no host is provided in beat" do - context "when no `host` key already exists on the event" do + context "when no `#{host_field_name}` key already exists on the event" do it "does not set the host" do - expect(subject.get("host")).to be_nil + expect(subject.get(host_field_name)).to be_nil end end - context "when `host` key already exists on the event" do + context "when `#{host_field_name}` key already exists on the event" do let(:already_exist) { "already_exist" } - let(:event_map) { super.merge({ "host" => already_exist }) } + let(:event_map) { super.merge(key_as_nested_maps(host_field_name, already_exist)) } it "doesn't override it" do - expect(subject.get("host")).to eq(already_exist) + expect(subject.get(host_field_name)).to eq(already_exist) end end end From 3f03ea37b7e101cfd36de880bcba2eb6d40f28cf Mon Sep 17 00:00:00 2001 From: Joao Duarte Date: Wed, 3 Mar 2021 10:52:58 +0000 Subject: [PATCH 31/39] [tests] change super to super() - jruby/jruby#6571 --- .../inputs/beats/decoded_event_transform_spec.rb | 4 ++-- spec/inputs/beats_spec.rb | 10 +++++----- spec/integration/filebeat_spec.rb | 16 ++++++++-------- spec/integration/logstash_forwarder_spec.rb | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/spec/inputs/beats/decoded_event_transform_spec.rb b/spec/inputs/beats/decoded_event_transform_spec.rb index 086fa666..52494d5a 100644 --- a/spec/inputs/beats/decoded_event_transform_spec.rb +++ b/spec/inputs/beats/decoded_event_transform_spec.rb @@ -44,7 +44,7 @@ context "map contains a timestamp" do context "when its valid" do let(:timestamp) { Time.now } - let(:map) { super.merge({"@timestamp" => timestamp }) } + let(:map) { super().merge({"@timestamp" => timestamp }) } it "uses as the event timestamp" do expect(subject.get("@timestamp")).to eq(LogStash::Timestamp.coerce(timestamp)) @@ -52,7 +52,7 @@ end context "when its not valid" do - let(:map) { super.merge({"@timestamp" => "invalid" }) } + let(:map) { super().merge({"@timestamp" => "invalid" }) } it "fallback the current time" do expect(subject.get("@timestamp")).to be_kind_of(LogStash::Timestamp) diff --git a/spec/inputs/beats_spec.rb b/spec/inputs/beats_spec.rb index 51aec7ea..2fd740a0 100644 --- a/spec/inputs/beats_spec.rb +++ b/spec/inputs/beats_spec.rb @@ -25,7 +25,7 @@ context "#register" do context "host related configuration" do - let(:config) { super.merge("host" => host, "port" => port, "client_inactivity_timeout" => client_inactivity_timeout, "executor_threads" => threads) } + let(:config) { super().merge("host" => host, "port" => port, "client_inactivity_timeout" => client_inactivity_timeout, "executor_threads" => threads) } let(:host) { "192.168.1.20" } let(:port) { 9000 } let(:client_inactivity_timeout) { 400 } @@ -76,7 +76,7 @@ end context "with invalid ciphers" do - let(:config) { super.merge("ssl" => true, "cipher_suites" => "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA38") } + let(:config) { super().merge("ssl" => true, "cipher_suites" => "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA38") } it "should raise a configuration error" do plugin = LogStash::Inputs::Beats.new(config) @@ -90,7 +90,7 @@ context "verify_mode" do context "verify_mode configured to PEER" do - let(:config) { super.merge("ssl" => true, "ssl_verify_mode" => "peer") } + let(:config) { super().merge("ssl" => true, "ssl_verify_mode" => "peer") } it "raise a ConfigurationError when certificate_authorities is not set" do plugin = LogStash::Inputs::Beats.new(config) @@ -105,7 +105,7 @@ end context "verify_mode configured to FORCE_PEER" do - let(:config) { super.merge("ssl" => true, "ssl_verify_mode" => "force_peer") } + let(:config) { super().merge("ssl" => true, "ssl_verify_mode" => "force_peer") } it "raise a ConfigurationError when certificate_authorities is not set" do plugin = LogStash::Inputs::Beats.new(config) @@ -154,7 +154,7 @@ let(:codec) { LogStash::Codecs::Multiline.new("pattern" => '^2015', "what" => "previous", "negate" => true) } - let(:config) { super.merge({ "codec" => codec }) } + let(:config) { super().merge({ "codec" => codec }) } it "raise a ConfigurationError when multiline codec is set" do plugin = LogStash::Inputs::Beats.new(config) diff --git a/spec/integration/filebeat_spec.rb b/spec/integration/filebeat_spec.rb index 64244985..5f11e166 100644 --- a/spec/integration/filebeat_spec.rb +++ b/spec/integration/filebeat_spec.rb @@ -99,7 +99,7 @@ context "TLS" do context "Server verification" do let(:filebeat_config) do - super.merge({ + super().merge({ "output" => { "logstash" => { "hosts" => ["#{host}:#{port}"], @@ -111,7 +111,7 @@ end let(:input_config) do - super.merge({ + super().merge({ "ssl" => true, "ssl_certificate" => certificate_file, "ssl_key" => certificate_key_file @@ -129,7 +129,7 @@ context "when specifying a cipher" do let(:filebeat_config) do - super.merge({ + super().merge({ "output" => { "logstash" => { "hosts" => ["#{host}:#{port}"], @@ -145,7 +145,7 @@ end let(:input_config) { - super.merge({ + super().merge({ "cipher_suites" => [logstash_cipher], "tls_min_version" => "1.2" }) @@ -194,7 +194,7 @@ LogStash::Inputs::Beats.new(input_config) } let(:input_config) { - super.merge({ + super().merge({ "ssl_key_passphrase" => passphrase, "ssl_key" => certificate_key_file_pkcs8 })} @@ -229,7 +229,7 @@ context "Client verification / Mutual validation" do let(:filebeat_config) do - super.merge({ + super().merge({ "output" => { "logstash" => { "hosts" => ["#{host}:#{port}"], @@ -245,7 +245,7 @@ end let(:input_config) do - super.merge({ + super().merge({ "ssl" => true, "ssl_certificate_authorities" => certificate_authorities, "ssl_certificate" => server_certificate_file, @@ -327,7 +327,7 @@ context "client from secondary CA" do let(:filebeat_config) do - super.merge({ + super().merge({ "output" => { "logstash" => { "hosts" => ["#{host}:#{port}"], diff --git a/spec/integration/logstash_forwarder_spec.rb b/spec/integration/logstash_forwarder_spec.rb index ad064ff5..da48d324 100644 --- a/spec/integration/logstash_forwarder_spec.rb +++ b/spec/integration/logstash_forwarder_spec.rb @@ -74,7 +74,7 @@ context "TLS" do context "Server Verification" do let(:input_config) do - super.merge({ + super().merge({ "ssl" => true, "ssl_certificate" => certificate_file, "ssl_key" => certificate_key_file, From de2f1b864b372e1ba0c92326357a4d06a2119dda Mon Sep 17 00:00:00 2001 From: Andrea Selva Date: Mon, 29 Mar 2021 14:41:24 +0200 Subject: [PATCH 32/39] Used super() instead of super in RSpec shared examples (#412) * Used super() instead of super in RSpec shared examples --- spec/integration/filebeat_spec.rb | 2 +- spec/support/shared_examples.rb | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/spec/integration/filebeat_spec.rb b/spec/integration/filebeat_spec.rb index 5f11e166..885ce45c 100644 --- a/spec/integration/filebeat_spec.rb +++ b/spec/integration/filebeat_spec.rb @@ -86,7 +86,7 @@ end context "without pipelining" do - let(:filebeat_config) { config = super; config["output"]["logstash"]["pipelining"] = 0; config } + let(:filebeat_config) { config = super(); config["output"]["logstash"]["pipelining"] = 0; config } include_examples "send events" context "with large batches" do diff --git a/spec/support/shared_examples.rb b/spec/support/shared_examples.rb index 8a80bd7d..3f89723c 100644 --- a/spec/support/shared_examples.rb +++ b/spec/support/shared_examples.rb @@ -32,12 +32,12 @@ def key_as_nested_maps(key, value) end context 'when add_hostname is true' do - let(:config) { super.merge({'add_hostname' => true, 'ecs_compatibility' => ecs_compatibility})} + let(:config) { super().merge({'add_hostname' => true, 'ecs_compatibility' => ecs_compatibility})} context 'when a host is provided in beat.host.name' do let(:already_exist) { "already_exist" } let(:producer_host) { "newhost01" } - let(:event_map) { super.merge({ "beat" => { "host" => {"name" => producer_host }}}) } + let(:event_map) { super().merge({ "beat" => { "host" => {"name" => producer_host }}}) } context "when no `host` key already exists on the event" do it "does not set the host value" do @@ -47,7 +47,7 @@ def key_as_nested_maps(key, value) context "when `host` key exists on the event" do let(:already_exist) { "already_exist" } - let(:event_map) { super.merge({ "host" => already_exist }) } + let(:event_map) { super().merge({ "host" => already_exist }) } it "doesn't override it" do expect(subject.get("host")).to eq(already_exist) @@ -57,7 +57,7 @@ def key_as_nested_maps(key, value) context "when a host is set in `beat.hostname`" do let(:producer_host) { "newhost01" } - let(:event_map) { super.merge({ "beat" => { "hostname" => producer_host }}) } + let(:event_map) { super().merge({ "beat" => { "hostname" => producer_host }}) } context "when no `#{host_field_name}` key already exists on the event" do it "copies the value in `beat.hostname` to `#{host_field_name}`" do @@ -67,7 +67,7 @@ def key_as_nested_maps(key, value) context "when `#{host_field_name}` key exists on the event" do let(:already_exist) { "already_exist" } - let(:event_map) { super.merge(key_as_nested_maps(host_field_name, already_exist)) } + let(:event_map) { super().merge(key_as_nested_maps(host_field_name, already_exist)) } it "doesn't override it" do expect(subject.get(host_field_name)).to eq(already_exist) @@ -84,7 +84,7 @@ def key_as_nested_maps(key, value) context "when `#{host_field_name}` key already exists on the event" do let(:already_exist) { "already_exist" } - let(:event_map) { super.merge(key_as_nested_maps(host_field_name, already_exist)) } + let(:event_map) { super().merge(key_as_nested_maps(host_field_name, already_exist)) } it "doesn't override it" do expect(subject.get(host_field_name)).to eq(already_exist) @@ -94,12 +94,12 @@ def key_as_nested_maps(key, value) end context 'when add hostname is false' do - let(:config) { super.merge({'add_hostname' => false})} + let(:config) { super().merge({'add_hostname' => false})} context 'when a host is provided in beat.host.name' do let(:already_exist) { "already_exist" } let(:producer_host) { "newhost01" } - let(:event_map) { super.merge({ "beat" => { "host" => {"name" => producer_host }}}) } + let(:event_map) { super().merge({ "beat" => { "host" => {"name" => producer_host }}}) } context "when no `#{host_field_name}` key already exists on the event" do it "does not set the host" do @@ -109,7 +109,7 @@ def key_as_nested_maps(key, value) context "when `#{host_field_name}` key already exists on the event" do let(:already_exist) { "already_exist" } - let(:event_map) { super.merge(key_as_nested_maps(host_field_name, already_exist)) } + let(:event_map) { super().merge(key_as_nested_maps(host_field_name, already_exist)) } it "doesn't override it" do expect(subject.get(host_field_name)).to eq(already_exist) @@ -119,7 +119,7 @@ def key_as_nested_maps(key, value) context "when a host is provided in `beat.hostname`" do let(:producer_host) { "newhost01" } - let(:event_map) { super.merge({ "beat" => { "hostname" => producer_host }}) } + let(:event_map) { super().merge({ "beat" => { "hostname" => producer_host }}) } context "when no `#{host_field_name}` key already exists on the event" do it "does not set the host" do @@ -129,7 +129,7 @@ def key_as_nested_maps(key, value) context "when `host` key already exists on the event" do let(:already_exist) { "already_exist" } - let(:event_map) { super.merge(key_as_nested_maps(host_field_name, already_exist)) } + let(:event_map) { super().merge(key_as_nested_maps(host_field_name, already_exist)) } it "doesn't override it" do expect(subject.get(host_field_name)).to eq(already_exist) @@ -146,7 +146,7 @@ def key_as_nested_maps(key, value) context "when `#{host_field_name}` key already exists on the event" do let(:already_exist) { "already_exist" } - let(:event_map) { super.merge(key_as_nested_maps(host_field_name, already_exist)) } + let(:event_map) { super().merge(key_as_nested_maps(host_field_name, already_exist)) } it "doesn't override it" do expect(subject.get(host_field_name)).to eq(already_exist) From 3b62690dfb8cf0b87b9cf0696bbf7f6bac0400d7 Mon Sep 17 00:00:00 2001 From: Karen Metts <35154725+karenzone@users.noreply.github.com> Date: Mon, 29 Mar 2021 15:32:58 -0400 Subject: [PATCH 33/39] Doc: Add input-agent doc file (#411) --- docs/agent.asciidoc | 320 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 docs/agent.asciidoc diff --git a/docs/agent.asciidoc b/docs/agent.asciidoc new file mode 100644 index 00000000..41a61dc6 --- /dev/null +++ b/docs/agent.asciidoc @@ -0,0 +1,320 @@ +:plugin: agent +:type: input +:default_codec: plain + +/////////////////////////////////////////// +START - GENERATED VARIABLES, DO NOT EDIT! +/////////////////////////////////////////// + +// Copied from Beats generated plugin output. +// Not actively generated at this time! + +//// +:version: %VERSION% +:release_date: %RELEASE_DATE% +:changelog_url: %CHANGELOG_URL% +:include_path: ../../../../logstash/docs/include +//// + +/////////////////////////////////////////// +END - GENERATED VARIABLES, DO NOT EDIT! +/////////////////////////////////////////// + +[id="plugins-{type}s-{plugin}"] + +=== Agent input plugin + +include::{include_path}/plugin_header.asciidoc[] + +==== Description + +This input plugin enables Logstash to receive events from the +https://www.elastic.co/products/beats[Elastic Beats] framework. + +The following example shows how to configure Logstash to listen on port +5044 for incoming Beats connections and to index into Elasticsearch. + +[source,logstash] +----- + +input { + beats { + port => 5044 + } +} + +output { + elasticsearch { + hosts => ["http://localhost:9200"] + index => "%{[@metadata][beat]}-%{[@metadata][version]}" <1> + } +} +----- +<1> `%{[@metadata][beat]}` sets the first part of the index name to the value +of the `beat` metadata field and `%{[@metadata][version]}` sets the second part to +the Beat's version. For example: +metricbeat-7.4.0. + +Events indexed into Elasticsearch with the Logstash configuration shown here +will be similar to events directly indexed by Beats into Elasticsearch. + +NOTE: If ILM is not being used, set `index` to +`%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}` instead so +Logstash creates an index per day, based on the `@timestamp` value of the events +coming from Beats. + +IMPORTANT: If you are shipping events that span multiple lines, you need to use +the {filebeat-ref}/multiline-examples.html[configuration options available in +Filebeat] to handle multiline events before sending the event data to Logstash. +You cannot use the {logstash-ref}/plugins-codecs-multiline.html[Multiline codec +plugin] to handle multiline events. Doing so will result in the failure to start +Logstash. + +[id="plugins-{type}s-{plugin}-versioned-indexes"] +==== Versioned Beats Indices + +To minimize the impact of future schema changes on your existing indices and +mappings in Elasticsearch, configure the Elasticsearch output to write to +versioned indices. The pattern that you specify for the `index` setting +controls the index name: + +[source,yaml] +---- +index => "%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}" +---- + +`%{[@metadata][beat]}`:: Sets the first part of the index name to the value of +the `beat` metadata field, for example, `filebeat`. +`%{[@metadata][version]}`:: Sets the second part of the name to the Beat +version, for example, +{logstash_version}+. +`%{+YYYY.MM.dd}`:: Sets the third part of the name to a date based on the +Logstash `@timestamp` field. + +This configuration results in daily index names like ++filebeat-{logstash_version}-{localdate}+. + + +[id="plugins-{type}s-{plugin}-ecs_metadata"] +==== Event Metadata and the Elastic Common Schema (ECS) +When decoding `beats` events, this plugin adds two fields related to the event: the deprecated `host` +which contains the `hostname` provided by beats and the `ip_address` containing the remote address +of the client's connection. When <> is +enabled these are now moved in ECS compatible namespace. + +[id="plugins-{type}s-{plugin}-options"] +==== Agent Input Configuration Options + +This plugin supports the following configuration options plus the <> described later. + +[cols="<,<,<",options="header",] +|======================================================================= +|Setting |Input type|Required +| <> |<>|No +| <> |<>|No +| <> |<>|No +| <> | <>|No +| <> |<>|No +| <> |<>|No +| <> |<>|Yes +| <> |<>|No +| <> |a valid filesystem path|No +| <> |<>|No +| <> |<>|No +| <> |a valid filesystem path|No +| <> |<>|No +| <> |<>, one of `["none", "peer", "force_peer"]`|No +| <> |<>|No +| <> |<>|No +| <> |<>|No +|======================================================================= + +Also see <> for a list of options supported by all +input plugins. + +  + +[id="plugins-{type}s-{plugin}-add_hostname"] +===== `add_hostname` + + deprecated[6.0.0, The default value has been changed to `false`. In 7.0.0 this setting will be removed] + + * Value type is <> + * Default value is `false` + +Flag to determine whether to add `host` field to event using the value supplied by the beat in the `hostname` field. + + +[id="plugins-{type}s-{plugin}-cipher_suites"] +===== `cipher_suites` + + * Value type is <> + * Default value is `java.lang.String[TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256]@459cfcca` + +The list of ciphers suite to use, listed by priorities. + +[id="plugins-{type}s-{plugin}-client_inactivity_timeout"] +===== `client_inactivity_timeout` + + * Value type is <> + * Default value is `60` + +Close Idle clients after X seconds of inactivity. + +[id="plugins-{type}s-{plugin}-ecs_compatibility"] +===== `ecs_compatibility` + +* Value type is <> +* Supported values are: +** `disabled`: unstructured connection metadata added at root level +** `v1`: structured connection metadata added under ECS compliant namespaces +* Default value depends on which version of Logstash is running: +** When Logstash provides a `pipeline.ecs_compatibility` setting, its value is used as the default +** Otherwise, the default value is `disabled`. + +Controls this plugin's compatibility with the {ecs-ref}[Elastic Common Schema (ECS)]. +The value of this setting affects the keys for the Beats connection's metadata on the event: + +.Metadata Location by `ecs_compatibility` value +[cols="> + * Default value is `"0.0.0.0"` + +The IP address to listen on. + +[id="plugins-{type}s-{plugin}-include_codec_tag"] +===== `include_codec_tag` + + * Value type is <> + * Default value is `true` + + + +[id="plugins-{type}s-{plugin}-port"] +===== `port` + + * This is a required setting. + * Value type is <> + * There is no default value for this setting. + +The port to listen on. + +[id="plugins-{type}s-{plugin}-ssl"] +===== `ssl` + + * Value type is <> + * Default value is `false` + +Events are by default sent in plain text. You can +enable encryption by setting `ssl` to true and configuring +the `ssl_certificate` and `ssl_key` options. + +[id="plugins-{type}s-{plugin}-ssl_certificate"] +===== `ssl_certificate` + + * Value type is <> + * There is no default value for this setting. + +SSL certificate to use. + +[id="plugins-{type}s-{plugin}-ssl_certificate_authorities"] +===== `ssl_certificate_authorities` + + * Value type is <> + * Default value is `[]` + +Validate client certificates against these authorities. +You can define multiple files or paths. All the certificates will +be read and added to the trust store. You need to configure the `ssl_verify_mode` +to `peer` or `force_peer` to enable the verification. + + +[id="plugins-{type}s-{plugin}-ssl_handshake_timeout"] +===== `ssl_handshake_timeout` + + * Value type is <> + * Default value is `10000` + +Time in milliseconds for an incomplete ssl handshake to timeout + +[id="plugins-{type}s-{plugin}-ssl_key"] +===== `ssl_key` + + * Value type is <> + * There is no default value for this setting. + +SSL key to use. +NOTE: This key need to be in the PKCS8 format, you can convert it with https://www.openssl.org/docs/man1.1.0/apps/pkcs8.html[OpenSSL] +for more information. + +[id="plugins-{type}s-{plugin}-ssl_key_passphrase"] +===== `ssl_key_passphrase` + + * Value type is <> + * There is no default value for this setting. + +SSL key passphrase to use. + +[id="plugins-{type}s-{plugin}-ssl_verify_mode"] +===== `ssl_verify_mode` + + * Value can be any of: `none`, `peer`, `force_peer` + * Default value is `"none"` + +By default the server doesn't do any client verification. + +`peer` will make the server ask the client to provide a certificate. +If the client provides a certificate, it will be validated. + +`force_peer` will make the server ask the client to provide a certificate. +If the client doesn't provide a certificate, the connection will be closed. + +This option needs to be used with `ssl_certificate_authorities` and a defined list of CAs. + +[id="plugins-{type}s-{plugin}-ssl_peer_metadata"] +===== `ssl_peer_metadata` + + * Value type is <> + * Default value is `false` + +Enables storing client certificate information in event's metadata. + +This option is only valid when `ssl_verify_mode` is set to `peer` or `force_peer`. + +[id="plugins-{type}s-{plugin}-tls_max_version"] +===== `tls_max_version` + + * Value type is <> + * Default value is `1.2` + +The maximum TLS version allowed for the encrypted connections. The value must be the one of the following: +1.0 for TLS 1.0, 1.1 for TLS 1.1, 1.2 for TLS 1.2 + +[id="plugins-{type}s-{plugin}-tls_min_version"] +===== `tls_min_version` + + * Value type is <> + * Default value is `1` + +The minimum TLS version allowed for the encrypted connections. The value must be one of the following: +1.0 for TLS 1.0, 1.1 for TLS 1.1, 1.2 for TLS 1.2 + + + +[id="plugins-{type}s-{plugin}-common-options"] +include::{include_path}/{type}.asciidoc[] + +:default_codec!: From 42cdfbae18d1ea3f480da56e32ca1fd960c03948 Mon Sep 17 00:00:00 2001 From: Andres Rodriguez Date: Tue, 30 Mar 2021 16:58:26 -0400 Subject: [PATCH 34/39] Doc: Improve ECS documentation Improve the ECS related documentation. Moves the table with the ECS mapped fields to a different location, better visible to users. --- docs/index.asciidoc | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 919deba6..699ea447 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -92,7 +92,19 @@ This configuration results in daily index names like When decoding `beats` events, this plugin adds two fields related to the event: the deprecated `host` which contains the `hostname` provided by beats and the `ip_address` containing the remote address of the client's connection. When <> is -enabled these are now moved in ECS compatible namespace. +enabled these are now moved in ECS compatible namespace. Here's how <> affects output. + +[cols="> for detailed information. [id="plugins-{type}s-{plugin}-host"] ===== `host` From 3d0e1752371b61b84cc33f47c14624cc42432d8e Mon Sep 17 00:00:00 2001 From: Karen Metts <35154725+karenzone@users.noreply.github.com> Date: Tue, 30 Mar 2021 19:31:35 -0400 Subject: [PATCH 35/39] Bump to v.6.1.1 (#414) Bump version to publish doc changes in #413 --- CHANGELOG.md | 9 ++++++--- VERSION | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d37eb002..0ea9db96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ +## 6.1.1 + - [DOC] Enhanced ECS compatibility information for ease of use and readability + [#413](https://github.com/logstash-plugins/logstash-input-beats/pull/413) + ## 6.1.0 - - ECS compatibility enablement, now an `ecs_compatibility` setting is used to declare the level of ECS compatibility - (`disabled` or `v1`) at plugin level. `disabled` let the plugin behave like before while `v1` does a rename of - `host` and `@metadata.ip_address` event fields. [404](https://github.com/logstash-plugins/logstash-input-beats/pull/404) + - ECS compatibility enablement. Adds `ecs_compatibility` setting to declare the level of ECS compatibility (`disabled` or `v1`) at plugin level. When `disabled`, the plugin behaves like before, while `v1` does a rename of + `host` and `@metadata.ip_address` event fields. [#404](https://github.com/logstash-plugins/logstash-input-beats/pull/404) ## 6.0.14 - Feat: log + unwrap generic SSL context exceptions [#405](https://github.com/logstash-plugins/logstash-input-beats/pull/405) diff --git a/VERSION b/VERSION index dfda3e0b..f3b5af39 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.1.0 +6.1.1 From 16d68a5b49807d08024ecb52fb4e362629339b21 Mon Sep 17 00:00:00 2001 From: Karen Metts <35154725+karenzone@users.noreply.github.com> Date: Mon, 12 Apr 2021 13:47:31 -0400 Subject: [PATCH 36/39] Doc: Add plugin name attribute (#415) Allow for attributes in code samples Bump to v.6.1.2 --- CHANGELOG.md | 3 +++ VERSION | 2 +- docs/index.asciidoc | 29 +++++++++++++++++------------ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ea9db96..9a87e70a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.1.2 + - [DOC] Added naming attribute to control plugin name that appears in docs, and set up framework to make attributes viable in code sample + ## 6.1.1 - [DOC] Enhanced ECS compatibility information for ease of use and readability [#413](https://github.com/logstash-plugins/logstash-input-beats/pull/413) diff --git a/VERSION b/VERSION index f3b5af39..5e325424 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.1.1 +6.1.2 diff --git a/docs/index.asciidoc b/docs/index.asciidoc index 699ea447..fa3e8663 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -1,6 +1,7 @@ :plugin: beats :type: input :default_codec: plain +:plugin-uc: Beats /////////////////////////////////////////// START - GENERATED VARIABLES, DO NOT EDIT! @@ -15,7 +16,7 @@ END - GENERATED VARIABLES, DO NOT EDIT! [id="plugins-{type}s-{plugin}"] -=== Beats input plugin +=== {plugin-uc} input plugin include::{include_path}/plugin_header.asciidoc[] @@ -25,9 +26,9 @@ This input plugin enables Logstash to receive events from the https://www.elastic.co/products/beats[Elastic Beats] framework. The following example shows how to configure Logstash to listen on port -5044 for incoming Beats connections and to index into Elasticsearch. +5044 for incoming {plugin-uc} connections and to index into Elasticsearch. -[source,logstash] +["source","sh",subs="attributes"] ----- input { @@ -45,16 +46,16 @@ output { ----- <1> `%{[@metadata][beat]}` sets the first part of the index name to the value of the `beat` metadata field and `%{[@metadata][version]}` sets the second part to -the Beat's version. For example: +the {plugin-uc}'s version. For example: metricbeat-7.4.0. Events indexed into Elasticsearch with the Logstash configuration shown here -will be similar to events directly indexed by Beats into Elasticsearch. +will be similar to events directly indexed by {plugin-uc} into Elasticsearch. NOTE: If ILM is not being used, set `index` to `%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}` instead so Logstash creates an index per day, based on the `@timestamp` value of the events -coming from Beats. +coming from {plugin-uc}. IMPORTANT: If you are shipping events that span multiple lines, you need to use the {filebeat-ref}/multiline-examples.html[configuration options available in @@ -64,7 +65,7 @@ plugin] to handle multiline events. Doing so will result in the failure to start Logstash. [id="plugins-{type}s-{plugin}-versioned-indexes"] -==== Versioned Beats Indices +==== Versioned indices To minimize the impact of future schema changes on your existing indices and mappings in Elasticsearch, configure the Elasticsearch output to write to @@ -89,10 +90,14 @@ This configuration results in daily index names like [id="plugins-{type}s-{plugin}-ecs_metadata"] ==== Event Metadata and the Elastic Common Schema (ECS) -When decoding `beats` events, this plugin adds two fields related to the event: the deprecated `host` -which contains the `hostname` provided by beats and the `ip_address` containing the remote address -of the client's connection. When <> is -enabled these are now moved in ECS compatible namespace. Here's how <> affects output. + +When decoding {plugin-uc} events, this plugin adds two fields related to the event: +the deprecated `host` which contains the `hostname` provided by {plugin-uc} and the +`ip_address` containing the remote address of the client's connection. When +<> is enabled +these are now moved in ECS compatible namespace. Here's how +<> affects +output. [cols="> described later. From 9ac7be642598a20aa09ed6c41a8e5ea9f1a19ba8 Mon Sep 17 00:00:00 2001 From: Karen Metts <35154725+karenzone@users.noreply.github.com> Date: Thu, 15 Apr 2021 12:33:23 -0400 Subject: [PATCH 37/39] Doc: Remove unnecessary file (#416) Functionality replaced by adding variables to a shared source file --- docs/agent.asciidoc | 320 -------------------------------------------- 1 file changed, 320 deletions(-) delete mode 100644 docs/agent.asciidoc diff --git a/docs/agent.asciidoc b/docs/agent.asciidoc deleted file mode 100644 index 41a61dc6..00000000 --- a/docs/agent.asciidoc +++ /dev/null @@ -1,320 +0,0 @@ -:plugin: agent -:type: input -:default_codec: plain - -/////////////////////////////////////////// -START - GENERATED VARIABLES, DO NOT EDIT! -/////////////////////////////////////////// - -// Copied from Beats generated plugin output. -// Not actively generated at this time! - -//// -:version: %VERSION% -:release_date: %RELEASE_DATE% -:changelog_url: %CHANGELOG_URL% -:include_path: ../../../../logstash/docs/include -//// - -/////////////////////////////////////////// -END - GENERATED VARIABLES, DO NOT EDIT! -/////////////////////////////////////////// - -[id="plugins-{type}s-{plugin}"] - -=== Agent input plugin - -include::{include_path}/plugin_header.asciidoc[] - -==== Description - -This input plugin enables Logstash to receive events from the -https://www.elastic.co/products/beats[Elastic Beats] framework. - -The following example shows how to configure Logstash to listen on port -5044 for incoming Beats connections and to index into Elasticsearch. - -[source,logstash] ------ - -input { - beats { - port => 5044 - } -} - -output { - elasticsearch { - hosts => ["http://localhost:9200"] - index => "%{[@metadata][beat]}-%{[@metadata][version]}" <1> - } -} ------ -<1> `%{[@metadata][beat]}` sets the first part of the index name to the value -of the `beat` metadata field and `%{[@metadata][version]}` sets the second part to -the Beat's version. For example: -metricbeat-7.4.0. - -Events indexed into Elasticsearch with the Logstash configuration shown here -will be similar to events directly indexed by Beats into Elasticsearch. - -NOTE: If ILM is not being used, set `index` to -`%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}` instead so -Logstash creates an index per day, based on the `@timestamp` value of the events -coming from Beats. - -IMPORTANT: If you are shipping events that span multiple lines, you need to use -the {filebeat-ref}/multiline-examples.html[configuration options available in -Filebeat] to handle multiline events before sending the event data to Logstash. -You cannot use the {logstash-ref}/plugins-codecs-multiline.html[Multiline codec -plugin] to handle multiline events. Doing so will result in the failure to start -Logstash. - -[id="plugins-{type}s-{plugin}-versioned-indexes"] -==== Versioned Beats Indices - -To minimize the impact of future schema changes on your existing indices and -mappings in Elasticsearch, configure the Elasticsearch output to write to -versioned indices. The pattern that you specify for the `index` setting -controls the index name: - -[source,yaml] ----- -index => "%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}" ----- - -`%{[@metadata][beat]}`:: Sets the first part of the index name to the value of -the `beat` metadata field, for example, `filebeat`. -`%{[@metadata][version]}`:: Sets the second part of the name to the Beat -version, for example, +{logstash_version}+. -`%{+YYYY.MM.dd}`:: Sets the third part of the name to a date based on the -Logstash `@timestamp` field. - -This configuration results in daily index names like -+filebeat-{logstash_version}-{localdate}+. - - -[id="plugins-{type}s-{plugin}-ecs_metadata"] -==== Event Metadata and the Elastic Common Schema (ECS) -When decoding `beats` events, this plugin adds two fields related to the event: the deprecated `host` -which contains the `hostname` provided by beats and the `ip_address` containing the remote address -of the client's connection. When <> is -enabled these are now moved in ECS compatible namespace. - -[id="plugins-{type}s-{plugin}-options"] -==== Agent Input Configuration Options - -This plugin supports the following configuration options plus the <> described later. - -[cols="<,<,<",options="header",] -|======================================================================= -|Setting |Input type|Required -| <> |<>|No -| <> |<>|No -| <> |<>|No -| <> | <>|No -| <> |<>|No -| <> |<>|No -| <> |<>|Yes -| <> |<>|No -| <> |a valid filesystem path|No -| <> |<>|No -| <> |<>|No -| <> |a valid filesystem path|No -| <> |<>|No -| <> |<>, one of `["none", "peer", "force_peer"]`|No -| <> |<>|No -| <> |<>|No -| <> |<>|No -|======================================================================= - -Also see <> for a list of options supported by all -input plugins. - -  - -[id="plugins-{type}s-{plugin}-add_hostname"] -===== `add_hostname` - - deprecated[6.0.0, The default value has been changed to `false`. In 7.0.0 this setting will be removed] - - * Value type is <> - * Default value is `false` - -Flag to determine whether to add `host` field to event using the value supplied by the beat in the `hostname` field. - - -[id="plugins-{type}s-{plugin}-cipher_suites"] -===== `cipher_suites` - - * Value type is <> - * Default value is `java.lang.String[TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256]@459cfcca` - -The list of ciphers suite to use, listed by priorities. - -[id="plugins-{type}s-{plugin}-client_inactivity_timeout"] -===== `client_inactivity_timeout` - - * Value type is <> - * Default value is `60` - -Close Idle clients after X seconds of inactivity. - -[id="plugins-{type}s-{plugin}-ecs_compatibility"] -===== `ecs_compatibility` - -* Value type is <> -* Supported values are: -** `disabled`: unstructured connection metadata added at root level -** `v1`: structured connection metadata added under ECS compliant namespaces -* Default value depends on which version of Logstash is running: -** When Logstash provides a `pipeline.ecs_compatibility` setting, its value is used as the default -** Otherwise, the default value is `disabled`. - -Controls this plugin's compatibility with the {ecs-ref}[Elastic Common Schema (ECS)]. -The value of this setting affects the keys for the Beats connection's metadata on the event: - -.Metadata Location by `ecs_compatibility` value -[cols="> - * Default value is `"0.0.0.0"` - -The IP address to listen on. - -[id="plugins-{type}s-{plugin}-include_codec_tag"] -===== `include_codec_tag` - - * Value type is <> - * Default value is `true` - - - -[id="plugins-{type}s-{plugin}-port"] -===== `port` - - * This is a required setting. - * Value type is <> - * There is no default value for this setting. - -The port to listen on. - -[id="plugins-{type}s-{plugin}-ssl"] -===== `ssl` - - * Value type is <> - * Default value is `false` - -Events are by default sent in plain text. You can -enable encryption by setting `ssl` to true and configuring -the `ssl_certificate` and `ssl_key` options. - -[id="plugins-{type}s-{plugin}-ssl_certificate"] -===== `ssl_certificate` - - * Value type is <> - * There is no default value for this setting. - -SSL certificate to use. - -[id="plugins-{type}s-{plugin}-ssl_certificate_authorities"] -===== `ssl_certificate_authorities` - - * Value type is <> - * Default value is `[]` - -Validate client certificates against these authorities. -You can define multiple files or paths. All the certificates will -be read and added to the trust store. You need to configure the `ssl_verify_mode` -to `peer` or `force_peer` to enable the verification. - - -[id="plugins-{type}s-{plugin}-ssl_handshake_timeout"] -===== `ssl_handshake_timeout` - - * Value type is <> - * Default value is `10000` - -Time in milliseconds for an incomplete ssl handshake to timeout - -[id="plugins-{type}s-{plugin}-ssl_key"] -===== `ssl_key` - - * Value type is <> - * There is no default value for this setting. - -SSL key to use. -NOTE: This key need to be in the PKCS8 format, you can convert it with https://www.openssl.org/docs/man1.1.0/apps/pkcs8.html[OpenSSL] -for more information. - -[id="plugins-{type}s-{plugin}-ssl_key_passphrase"] -===== `ssl_key_passphrase` - - * Value type is <> - * There is no default value for this setting. - -SSL key passphrase to use. - -[id="plugins-{type}s-{plugin}-ssl_verify_mode"] -===== `ssl_verify_mode` - - * Value can be any of: `none`, `peer`, `force_peer` - * Default value is `"none"` - -By default the server doesn't do any client verification. - -`peer` will make the server ask the client to provide a certificate. -If the client provides a certificate, it will be validated. - -`force_peer` will make the server ask the client to provide a certificate. -If the client doesn't provide a certificate, the connection will be closed. - -This option needs to be used with `ssl_certificate_authorities` and a defined list of CAs. - -[id="plugins-{type}s-{plugin}-ssl_peer_metadata"] -===== `ssl_peer_metadata` - - * Value type is <> - * Default value is `false` - -Enables storing client certificate information in event's metadata. - -This option is only valid when `ssl_verify_mode` is set to `peer` or `force_peer`. - -[id="plugins-{type}s-{plugin}-tls_max_version"] -===== `tls_max_version` - - * Value type is <> - * Default value is `1.2` - -The maximum TLS version allowed for the encrypted connections. The value must be the one of the following: -1.0 for TLS 1.0, 1.1 for TLS 1.1, 1.2 for TLS 1.2 - -[id="plugins-{type}s-{plugin}-tls_min_version"] -===== `tls_min_version` - - * Value type is <> - * Default value is `1` - -The minimum TLS version allowed for the encrypted connections. The value must be one of the following: -1.0 for TLS 1.0, 1.1 for TLS 1.1, 1.2 for TLS 1.2 - - - -[id="plugins-{type}s-{plugin}-common-options"] -include::{include_path}/{type}.asciidoc[] - -:default_codec!: From ab0667a9b5cdfa4d21c22750151f092abcef234e Mon Sep 17 00:00:00 2001 From: Tero Saarni Date: Tue, 20 Apr 2021 19:50:48 +0300 Subject: [PATCH 38/39] Updated Jackson dependencies (#418) --- CHANGELOG.md | 3 +++ build.gradle | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a87e70a..4b9f2971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## Unreleased + - Updated Jackson dependencies + ## 6.1.2 - [DOC] Added naming attribute to control plugin name that appears in docs, and set up framework to make attributes viable in code sample diff --git a/build.gradle b/build.gradle index 84cb9211..ad8cab54 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ sourceCompatibility = 1.8 targetCompatibility = 1.8 String jacksonVersion = '2.9.10' -String jacksonDatabindVersion = '2.9.10.4' +String jacksonDatabindVersion = '2.9.10.8' repositories { mavenCentral() From 27bad62a26a81fc000a9d21495b8dc7174ab63e9 Mon Sep 17 00:00:00 2001 From: Karol Bucek Date: Wed, 21 Apr 2021 07:00:05 +0200 Subject: [PATCH 39/39] Fix: safe-guard byte buf allocation (#420) since `inflateCompressedFrame` might have leaked, see #408 --- CHANGELOG.md | 5 +- VERSION | 2 +- .../java/org/logstash/beats/BeatsParser.java | 52 ++++++++++++------- .../beats/InvalidFrameProtocolException.java | 2 +- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9f2971..91df1e32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ -## Unreleased +## 6.1.3 + - Fix: safe-guard byte buf allocation [#420](https://github.com/logstash-plugins/logstash-input-beats/pull/420) - Updated Jackson dependencies ## 6.1.2 @@ -13,7 +14,7 @@ `host` and `@metadata.ip_address` event fields. [#404](https://github.com/logstash-plugins/logstash-input-beats/pull/404) ## 6.0.14 -- Feat: log + unwrap generic SSL context exceptions [#405](https://github.com/logstash-plugins/logstash-input-beats/pull/405) + - Feat: log + unwrap generic SSL context exceptions [#405](https://github.com/logstash-plugins/logstash-input-beats/pull/405) ## 6.0.13 - [DOC] Update links to use shared attributes diff --git a/VERSION b/VERSION index 5e325424..88d06f10 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.1.2 +6.1.3 diff --git a/src/main/java/org/logstash/beats/BeatsParser.java b/src/main/java/org/logstash/beats/BeatsParser.java index 9e852e49..61337d3b 100644 --- a/src/main/java/org/logstash/beats/BeatsParser.java +++ b/src/main/java/org/logstash/beats/BeatsParser.java @@ -47,7 +47,7 @@ private enum States { private boolean decodingCompressedBuffer = false; @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws InvalidFrameProtocolException, IOException { if(!hasEnoughBytes(in)) { if (decodingCompressedBuffer){ throw new InvalidFrameProtocolException("Insufficient bytes in compressed content to decode: " + currentState); @@ -124,7 +124,7 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t int fieldsCount = (int) in.readUnsignedInt(); int count = 0; - if(fieldsCount <= 0) { + if (fieldsCount <= 0) { throw new InvalidFrameProtocolException("Invalid number of fields, received: " + fieldsCount); } @@ -178,20 +178,19 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t case READ_COMPRESSED_FRAME: { logger.trace("Running: READ_COMPRESSED_FRAME"); - // Use the compressed size as the safe start for the buffer. - ByteBuf buffer = inflateCompressedFrame(ctx, in); - transition(States.READ_HEADER); + inflateCompressedFrame(ctx, in, (buffer) -> { + transition(States.READ_HEADER); - decodingCompressedBuffer = true; - try { - while (buffer.readableBytes() > 0) { - decode(ctx, buffer, out); + decodingCompressedBuffer = true; + try { + while (buffer.readableBytes() > 0) { + decode(ctx, buffer, out); + } + } finally { + decodingCompressedBuffer = false; + transition(States.READ_HEADER); } - } finally { - decodingCompressedBuffer = false; - buffer.release(); - transition(States.READ_HEADER); - } + }); break; } case READ_JSON: { @@ -211,18 +210,28 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) t } } - private ByteBuf inflateCompressedFrame(final ChannelHandlerContext ctx, final ByteBuf in) throws IOException { + private void inflateCompressedFrame(final ChannelHandlerContext ctx, final ByteBuf in, final CheckedConsumer fn) + throws IOException { + // Use the compressed size as the safe start for the buffer. ByteBuf buffer = ctx.alloc().buffer(requiredBytes); + try { + decompressImpl(in, buffer); + fn.accept(buffer); + } finally { + buffer.release(); + } + } + + private void decompressImpl(final ByteBuf in, final ByteBuf out) throws IOException { Inflater inflater = new Inflater(); try ( - ByteBufOutputStream buffOutput = new ByteBufOutputStream(buffer); + ByteBufOutputStream buffOutput = new ByteBufOutputStream(out); InflaterOutputStream inflaterStream = new InflaterOutputStream(buffOutput, inflater) ) { in.readBytes(inflaterStream, requiredBytes); - }finally{ + } finally { inflater.end(); } - return buffer; } private boolean hasEnoughBytes(ByteBuf in) { @@ -234,7 +243,7 @@ private void transition(States next) { } private void transition(States nextState, int requiredBytes) { - if(logger.isTraceEnabled()) { + if (logger.isTraceEnabled()) { logger.trace("Transition, from: " + currentState + ", to: " + nextState + ", requiring " + requiredBytes + " bytes"); } this.currentState = nextState; @@ -247,4 +256,9 @@ private void batchComplete() { batch = null; } + @FunctionalInterface + private interface CheckedConsumer { + void accept(T t) throws IOException; + } + } diff --git a/src/main/java/org/logstash/beats/InvalidFrameProtocolException.java b/src/main/java/org/logstash/beats/InvalidFrameProtocolException.java index d25187c9..118d2b0b 100644 --- a/src/main/java/org/logstash/beats/InvalidFrameProtocolException.java +++ b/src/main/java/org/logstash/beats/InvalidFrameProtocolException.java @@ -1,6 +1,6 @@ package org.logstash.beats; -public class InvalidFrameProtocolException extends Exception { +public class InvalidFrameProtocolException extends RuntimeException { InvalidFrameProtocolException(String message) { super(message); }