Skip to content

Commit 39310ef

Browse files
authored
Migrate GreyNoise API from v2 to v3 (#26296)
* Migrate GreyNoise API from v2 to v3 * changelog * fix test
1 parent 407628b commit 39310ef

3 files changed

Lines changed: 69 additions & 29 deletions

File tree

changelog/unreleased/pr-26296.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type = "f"
2+
message = "Fixed broken GreyNoise Quick IP Data Adapter by migrating to v3 API endpoint."
3+
pulls = ["26296"]

graylog2-server/src/main/java/org/graylog/integrations/dataadapters/GreyNoiseQuickIPDataAdapter.java

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,20 @@
1919
import com.codahale.metrics.MetricRegistry;
2020
import com.fasterxml.jackson.annotation.JsonAutoDetect;
2121
import com.fasterxml.jackson.annotation.JsonCreator;
22+
import com.fasterxml.jackson.annotation.JsonIgnore;
2223
import com.fasterxml.jackson.annotation.JsonInclude;
2324
import com.fasterxml.jackson.annotation.JsonProperty;
24-
import com.fasterxml.jackson.annotation.JsonIgnore;
2525
import com.fasterxml.jackson.annotation.JsonTypeName;
26+
import com.fasterxml.jackson.databind.JsonNode;
27+
import com.fasterxml.jackson.databind.ObjectMapper;
2628
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2729
import com.google.auto.value.AutoValue;
2830
import com.google.common.annotations.VisibleForTesting;
2931
import com.google.common.collect.Maps;
3032
import com.google.inject.assistedinject.Assisted;
31-
import com.unboundid.util.json.JSONException;
32-
import com.unboundid.util.json.JSONObject;
33+
import jakarta.annotation.Nonnull;
34+
import jakarta.inject.Inject;
35+
import jakarta.validation.constraints.NotEmpty;
3336
import okhttp3.OkHttpClient;
3437
import okhttp3.Request;
3538
import okhttp3.Response;
@@ -47,10 +50,6 @@
4750
import org.slf4j.Logger;
4851
import org.slf4j.LoggerFactory;
4952

50-
import jakarta.annotation.Nonnull;
51-
import jakarta.inject.Inject;
52-
import jakarta.validation.constraints.NotEmpty;
53-
5453
import java.io.IOException;
5554
import java.util.Map;
5655
import java.util.Objects;
@@ -59,12 +58,14 @@
5958
public class GreyNoiseQuickIPDataAdapter extends LookupDataAdapter {
6059
private static final Logger LOG = LoggerFactory.getLogger(GreyNoiseQuickIPDataAdapter.class);
6160
public static final String NAME = "GreyNoise";
62-
static final String GREYNOISE_IPQC_ENDPOINT = "https://api.greynoise.io/v2/noise/quick/";
61+
// v3 unified IP Lookup endpoint; the quick variant is selected with the "quick=true" query parameter.
62+
static final String GREYNOISE_IPQC_ENDPOINT = "https://api.greynoise.io/v3/ip/";
6363

6464
private final EncryptedValueService encryptedValueService;
6565
private final Config config;
6666
private final OkHttpClient okHttpClient;
6767
private final CustomizationConfig customizationConfig;
68+
private final ObjectMapper objectMapper;
6869

6970
private static final AtomicBoolean VALID_GREYNOISE_LICENSE = new AtomicBoolean(false);
7071

@@ -75,12 +76,14 @@ public GreyNoiseQuickIPDataAdapter(@Assisted("id") String id,
7576
MetricRegistry metricRegistry,
7677
EncryptedValueService encryptedValueService,
7778
OkHttpClient okHttpClient,
78-
CustomizationConfig customizationConfig) {
79+
CustomizationConfig customizationConfig,
80+
ObjectMapper objectMapper) {
7981
super(id, name, config, metricRegistry);
8082
this.config = (Config) config;
8183
this.encryptedValueService = encryptedValueService;
8284
this.okHttpClient = okHttpClient;
8385
this.customizationConfig = customizationConfig;
86+
this.objectMapper = objectMapper;
8487
}
8588

8689
@Override
@@ -135,10 +138,10 @@ protected LookupResult doGet(Object keyObject) {
135138
}
136139
}
137140
Request request = new Request.Builder()
138-
.url(GREYNOISE_IPQC_ENDPOINT + ip)
141+
.url(GREYNOISE_IPQC_ENDPOINT + ip + "?quick=true")
139142
.method("GET", null)
140143
.addHeader("Accept", "application/json")
141-
.addHeader("key", encryptedValueService.decrypt(config.apiToken()))
144+
.addHeader("key", Objects.requireNonNull(encryptedValueService.decrypt(config.apiToken())))
142145
.addHeader("User-Agent", customizationConfig.productName())
143146
.build();
144147
try (Response response = okHttpClient.newCall(request).execute()) {
@@ -150,18 +153,31 @@ protected LookupResult doGet(Object keyObject) {
150153
}
151154

152155
@VisibleForTesting
153-
static LookupResult parseResponse(Response response) {
154-
156+
LookupResult parseResponse(Response response) {
155157
if (response.isSuccessful()) {
156158
Map<Object, Object> map = Maps.newHashMap();
157159

158160
try {
159-
JSONObject obj = new JSONObject(response.body().string());
160-
map.put("ip", Objects.requireNonNull(obj).getFieldAsString("ip"));
161-
map.put("noise", Objects.requireNonNull(obj).getFieldAsBoolean("noise"));
162-
map.put("code", Objects.requireNonNull(obj).getFieldAsString("code"));
163-
map.put("riot", Objects.requireNonNull(obj).getFieldAsBoolean("riot"));
164-
} catch (JSONException | IOException e) {
161+
JsonNode root = objectMapper.readTree(response.body().string());
162+
if (root.hasNonNull("ip")) {
163+
map.put("ip", root.get("ip").asText());
164+
}
165+
166+
// v3 nests the scanner data; "found" is the v3 equivalent of the v2 top-level "noise" flag.
167+
JsonNode isi = root.path("internet_scanner_intelligence");
168+
map.put("noise", isi.path("found").asBoolean(false));
169+
if (isi.hasNonNull("classification")) {
170+
map.put("classification", isi.get("classification").asText());
171+
}
172+
173+
// v3 nests the RIOT data under business_service_intelligence; "found" replaces the v2 "riot" flag.
174+
JsonNode bsi = root.path("business_service_intelligence");
175+
map.put("riot", bsi.path("found").asBoolean(false));
176+
final String trustLevel = bsi.path("trust_level").asText("");
177+
if (!trustLevel.isEmpty()) {
178+
map.put("trust_level", trustLevel);
179+
}
180+
} catch (IOException e) {
165181
LOG.error("An error occurred while parsing Lookup result [{}]", e.toString());
166182
}
167183
return LookupResult.withoutTTL().multiValue(map).build();
@@ -174,7 +190,9 @@ static LookupResult parseResponse(Response response) {
174190
public void set(Object key, Object value) {
175191
}
176192

177-
// Check if provided API token has a valid non-community GreyNoise subscription.
193+
// Check if the provided API token is accepted by GreyNoise. The free/community tier has been retired, so any
194+
// authenticated request (HTTP 200) corresponds to a valid subscription, while an invalid or missing key returns
195+
// HTTP 401.
178196
private boolean isValidSubscription(String apiKey) {
179197
Request request = new Request.Builder()
180198
.url("https://api.greynoise.io/ping")
@@ -185,9 +203,7 @@ private boolean isValidSubscription(String apiKey) {
185203
.build();
186204

187205
try (Response response = okHttpClient.newCall(request).execute()) {
188-
JSONObject json = new JSONObject(response.body().string());
189-
return response.isSuccessful()
190-
&& json.hasField("offering") && !json.getFieldAsString("offering").equals("community");
206+
return response.isSuccessful();
191207
} catch (Exception e) {
192208
LOG.warn("An error occurred while retrieving subscription type.", e);
193209
return false;

graylog2-server/src/test/java/org/graylog/integrations/dataadapters/GreyNoiseDataAdapterTest.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,52 @@
1616
*/
1717
package org.graylog.integrations.dataadapters;
1818

19+
import com.codahale.metrics.MetricRegistry;
20+
import com.fasterxml.jackson.databind.ObjectMapper;
1921
import okhttp3.MediaType;
22+
import okhttp3.OkHttpClient;
2023
import okhttp3.Protocol;
2124
import okhttp3.Request;
2225
import okhttp3.Response;
2326
import okhttp3.ResponseBody;
2427
import org.assertj.core.api.Assertions;
2528
import org.graylog2.plugin.lookup.LookupResult;
29+
import org.graylog2.security.encryption.EncryptedValue;
30+
import org.graylog2.security.encryption.EncryptedValueService;
31+
import org.graylog2.web.customization.CustomizationConfig;
2632
import org.junit.jupiter.api.BeforeEach;
2733
import org.junit.jupiter.api.Test;
2834

2935
import static org.hamcrest.CoreMatchers.notNullValue;
3036
import static org.hamcrest.MatcherAssert.assertThat;
37+
import static org.mockito.Mockito.mock;
3138

3239
public class GreyNoiseDataAdapterTest {
3340

3441
Request mockRequest;
3542
Response mockResponse;
3643
String stringResponse;
3744

45+
private GreyNoiseQuickIPDataAdapter adapter;
46+
3847
@BeforeEach
3948
public void setUp() throws Exception {
4049

41-
stringResponse = "{\"ip\":\"192.168.1.1\",\"noise\":true,\"code\":\"0x01\"}";
50+
stringResponse = "{\"ip\":\"192.168.1.1\","
51+
+ "\"business_service_intelligence\":{\"found\":true,\"trust_level\":\"1\"},"
52+
+ "\"internet_scanner_intelligence\":{\"found\":true,\"classification\":\"malicious\"}}";
4253

4354
mockRequest = new Request.Builder()
44-
.url("https://api.greynoise.io/v2/noise/quick/")
55+
.url("https://api.greynoise.io/v3/ip/")
56+
.build();
57+
58+
final GreyNoiseQuickIPDataAdapter.Config config = GreyNoiseQuickIPDataAdapter.Config.builder()
59+
.type(GreyNoiseQuickIPDataAdapter.NAME)
60+
.apiToken(EncryptedValue.createUnset())
4561
.build();
62+
adapter = new GreyNoiseQuickIPDataAdapter("id", "name", config, new MetricRegistry(),
63+
mock(EncryptedValueService.class), mock(OkHttpClient.class), mock(CustomizationConfig.class),
64+
new ObjectMapper());
4665
}
4766

4867
private void getvalidResponse() {
@@ -59,15 +78,17 @@ private void getvalidResponse() {
5978
public void parseBodyWithMultiValue(){
6079
getvalidResponse();
6180

62-
final LookupResult result = GreyNoiseQuickIPDataAdapter.parseResponse(mockResponse);
81+
final LookupResult result = adapter.parseResponse(mockResponse);
6382
assertThat(result, notNullValue());
6483
Assertions.assertThat(result.isEmpty()).isFalse();
6584
Assertions.assertThat(result.hasError()).isFalse();
6685
Assertions.assertThat(result.singleValue()).isEqualTo(null);
6786
Assertions.assertThat(result.multiValue()).isNotNull();
68-
Assertions.assertThat(result.multiValue().containsValue("192.168.1.1")).isTrue();
69-
Assertions.assertThat(result.multiValue().containsValue("0x01")).isTrue();
70-
Assertions.assertThat(result.multiValue().containsValue(true)).isTrue();
87+
Assertions.assertThat(result.multiValue().get("ip")).isEqualTo("192.168.1.1");
88+
Assertions.assertThat(result.multiValue().get("noise")).isEqualTo(true);
89+
Assertions.assertThat(result.multiValue().get("riot")).isEqualTo(true);
90+
Assertions.assertThat(result.multiValue().get("classification")).isEqualTo("malicious");
91+
Assertions.assertThat(result.multiValue().get("trust_level")).isEqualTo("1");
7192
}
7293

7394
}

0 commit comments

Comments
 (0)