Skip to content

Commit 2dee50d

Browse files
kiawinmartijnvg
authored andcommitted
Added ASN support for Ingest GeoIP plugin.
Closes #27849
1 parent 3db84a3 commit 2dee50d

File tree

6 files changed

+183
-17
lines changed

6 files changed

+183
-17
lines changed

docs/plugins/ingest-geoip.asciidoc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ The GeoIP processor adds information about the geographical location of IP addre
55
This processor adds this information by default under the `geoip` field. The `geoip` processor can resolve both IPv4 and
66
IPv6 addresses.
77

8-
The ingest-geoip plugin ships by default with the GeoLite2 City and GeoLite2 Country geoip2 databases from Maxmind made available
8+
The ingest-geoip plugin ships by default with the GeoLite2 City, GeoLite2 Country and GeoLite2 ASN geoip2 databases from Maxmind made available
99
under the CCA-ShareAlike 4.0 license. For more details see, http://dev.maxmind.com/geoip/geoip2/geolite2/
1010

1111
The GeoIP processor can run with other geoip2 databases from Maxmind. The files must be copied into the geoip config directory,
1212
and the `database_file` option should be used to specify the filename of the custom database. Custom database files must be compressed
13-
with gzip. The geoip config directory is located at `$ES_HOME/config/ingest-geoip` and holds the shipped databases too.
13+
with gzip. The geoip config directory is located at `$ES_HOME/ingest-geoip` and holds the shipped databases too.
1414

1515
:plugin_name: ingest-geoip
1616
include::install_remove.asciidoc[]
@@ -36,7 +36,11 @@ include::install_remove.asciidoc[]
3636
`country_iso_code`, `country_name`, `continent_name`, `region_name`, `city_name`, `timezone`, `latitude`, `longitude`
3737
and `location`. The fields actually added depend on what has been found and which properties were configured in `properties`.
3838
* If the GeoLite2 Country database is used, then the following fields may be added under the `target_field`: `ip`,
39-
`country_iso_code`, `country_name` and `continent_name`. The fields actually added depend on what has been found and which properties were configured in `properties`.
39+
`country_iso_code`, `country_name` and `continent_name`. The fields actually added depend on what has been found and which properties
40+
were configured in `properties`.
41+
* If the GeoLite2 ASN database is used, then the following fields may be added under the `target_field`: `ip`,
42+
`asn`, and `organization_name`. The fields actually added depend on what has been found and which properties were configured
43+
in `properties`.
4044

4145
Here is an example that uses the default city database and adds the geographical information to the `geoip` field based on the `ip` field:
4246

plugins/ingest-geoip/build.gradle

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ esplugin {
2323
}
2424

2525
dependencies {
26-
// Upgrade to 2.10.0 or higher when jackson-core gets upgraded to 2.9.x
26+
// Upgrade to 2.10.0 or higher when jackson-core gets upgraded to 2.9.x. Blocked by #27032
2727
compile ('com.maxmind.geoip2:geoip2:2.9.0')
2828
// geoip2 dependencies:
2929
compile("com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}")
@@ -36,10 +36,7 @@ dependencies {
3636
task copyDefaultGeoIp2DatabaseFiles(type: Copy) {
3737
from { zipTree(configurations.testCompile.files.find { it.name.contains('geolite2-databases')}) }
3838
into "${project.buildDir}/ingest-geoip"
39-
40-
// For now, do not include GeoLite2-ASN.mmdb.gz file, because it isn't used yet:
41-
include "GeoLite2-City.mmdb.gz"
42-
include "GeoLite2-Country.mmdb.gz"
39+
include "*.mmdb.gz"
4340
}
4441

4542
project.bundlePlugin.dependsOn(copyDefaultGeoIp2DatabaseFiles)

plugins/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpProcessor.java

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import com.maxmind.geoip2.DatabaseReader;
2323
import com.maxmind.geoip2.exception.AddressNotFoundException;
24+
import com.maxmind.geoip2.model.AsnResponse;
2425
import com.maxmind.geoip2.model.CityResponse;
2526
import com.maxmind.geoip2.model.CountryResponse;
2627
import com.maxmind.geoip2.record.City;
@@ -59,6 +60,7 @@ public final class GeoIpProcessor extends AbstractProcessor {
5960
public static final String TYPE = "geoip";
6061
private static final String CITY_DB_SUFFIX = "-City";
6162
private static final String COUNTRY_DB_SUFFIX = "-Country";
63+
private static final String ASN_DB_SUFFIX = "-ASN";
6264

6365
private final String field;
6466
private final String targetField;
@@ -107,6 +109,12 @@ public void execute(IngestDocument ingestDocument) {
107109
} catch (AddressNotFoundRuntimeException e) {
108110
geoData = Collections.emptyMap();
109111
}
112+
} else if (databaseType.endsWith(ASN_DB_SUFFIX)) {
113+
try {
114+
geoData = retrieveAsnGeoData(ipAddress);
115+
} catch (AddressNotFoundRuntimeException e) {
116+
geoData = Collections.emptyMap();
117+
}
110118
} else {
111119
throw new ElasticsearchParseException("Unsupported database type [" + dbReader.getMetadata().getDatabaseType()
112120
+ "]", new IllegalStateException());
@@ -256,12 +264,53 @@ private Map<String, Object> retrieveCountryGeoData(InetAddress ipAddress) {
256264
return geoData;
257265
}
258266

267+
private Map<String, Object> retrieveAsnGeoData(InetAddress ipAddress) {
268+
SpecialPermission.check();
269+
AsnResponse response = AccessController.doPrivileged((PrivilegedAction<AsnResponse>) () -> {
270+
try {
271+
return dbReader.asn(ipAddress);
272+
} catch (AddressNotFoundException e) {
273+
throw new AddressNotFoundRuntimeException(e);
274+
} catch (Exception e) {
275+
throw new RuntimeException(e);
276+
}
277+
});
278+
279+
Integer asn = response.getAutonomousSystemNumber();
280+
String organization_name = response.getAutonomousSystemOrganization();
281+
282+
Map<String, Object> geoData = new HashMap<>();
283+
for (Property property : this.properties) {
284+
switch (property) {
285+
case IP:
286+
geoData.put("ip", NetworkAddress.format(ipAddress));
287+
break;
288+
case ASN:
289+
if (asn != null) {
290+
geoData.put("asn", asn);
291+
}
292+
break;
293+
case ORGANIZATION_NAME:
294+
if (organization_name != null) {
295+
geoData.put("organization_name", organization_name);
296+
}
297+
break;
298+
}
299+
}
300+
return geoData;
301+
}
302+
259303
public static final class Factory implements Processor.Factory {
260304
static final Set<Property> DEFAULT_CITY_PROPERTIES = EnumSet.of(
261305
Property.CONTINENT_NAME, Property.COUNTRY_ISO_CODE, Property.REGION_NAME,
262306
Property.CITY_NAME, Property.LOCATION
263307
);
264-
static final Set<Property> DEFAULT_COUNTRY_PROPERTIES = EnumSet.of(Property.CONTINENT_NAME, Property.COUNTRY_ISO_CODE);
308+
static final Set<Property> DEFAULT_COUNTRY_PROPERTIES = EnumSet.of(
309+
Property.CONTINENT_NAME, Property.COUNTRY_ISO_CODE
310+
);
311+
static final Set<Property> DEFAULT_ASN_PROPERTIES = EnumSet.of(
312+
Property.IP, Property.ASN, Property.ORGANIZATION_NAME
313+
);
265314

266315
private final Map<String, DatabaseReaderLazyLoader> databaseReaders;
267316

@@ -302,6 +351,8 @@ public GeoIpProcessor create(Map<String, Processor.Factory> registry, String pro
302351
properties = DEFAULT_CITY_PROPERTIES;
303352
} else if (databaseType.endsWith(COUNTRY_DB_SUFFIX)) {
304353
properties = DEFAULT_COUNTRY_PROPERTIES;
354+
} else if (databaseType.endsWith(ASN_DB_SUFFIX)) {
355+
properties = DEFAULT_ASN_PROPERTIES;
305356
} else {
306357
throw newConfigurationException(TYPE, processorTag, "database_file", "Unsupported database type ["
307358
+ databaseType + "]");
@@ -331,18 +382,29 @@ enum Property {
331382
REGION_NAME,
332383
CITY_NAME,
333384
TIMEZONE,
334-
LOCATION;
385+
LOCATION,
386+
ASN,
387+
ORGANIZATION_NAME;
335388

336-
static final EnumSet<Property> ALL_CITY_PROPERTIES = EnumSet.allOf(Property.class);
337-
static final EnumSet<Property> ALL_COUNTRY_PROPERTIES = EnumSet.of(Property.IP, Property.CONTINENT_NAME,
338-
Property.COUNTRY_NAME, Property.COUNTRY_ISO_CODE);
389+
static final EnumSet<Property> ALL_CITY_PROPERTIES = EnumSet.of(
390+
Property.IP, Property.COUNTRY_ISO_CODE, Property.COUNTRY_NAME, Property.CONTINENT_NAME,
391+
Property.REGION_NAME, Property.CITY_NAME, Property.TIMEZONE, Property.LOCATION
392+
);
393+
static final EnumSet<Property> ALL_COUNTRY_PROPERTIES = EnumSet.of(
394+
Property.IP, Property.CONTINENT_NAME, Property.COUNTRY_NAME, Property.COUNTRY_ISO_CODE
395+
);
396+
static final EnumSet<Property> ALL_ASN_PROPERTIES = EnumSet.of(
397+
Property.IP, Property.ASN, Property.ORGANIZATION_NAME
398+
);
339399

340400
public static Property parseProperty(String databaseType, String value) {
341401
Set<Property> validProperties = EnumSet.noneOf(Property.class);
342402
if (databaseType.endsWith(CITY_DB_SUFFIX)) {
343403
validProperties = ALL_CITY_PROPERTIES;
344404
} else if (databaseType.endsWith(COUNTRY_DB_SUFFIX)) {
345405
validProperties = ALL_COUNTRY_PROPERTIES;
406+
} else if (databaseType.endsWith(ASN_DB_SUFFIX)) {
407+
validProperties = ALL_ASN_PROPERTIES;
346408
}
347409

348410
try {

plugins/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpProcessorFactoryTests.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public static void loadDatabaseReaders() throws IOException {
5858
geoIpConfigDir.resolve("GeoLite2-City.mmdb.gz"));
5959
Files.copy(new ByteArrayInputStream(StreamsUtils.copyToBytesFromClasspath("/GeoLite2-Country.mmdb.gz")),
6060
geoIpConfigDir.resolve("GeoLite2-Country.mmdb.gz"));
61+
Files.copy(new ByteArrayInputStream(StreamsUtils.copyToBytesFromClasspath("/GeoLite2-ASN.mmdb.gz")),
62+
geoIpConfigDir.resolve("GeoLite2-ASN.mmdb.gz"));
6163

6264
NodeCache cache = randomFrom(NoCache.getInstance(), new GeoIpCache(randomNonNegativeLong()));
6365
databaseReaders = IngestGeoIpPlugin.loadDatabaseReaders(geoIpConfigDir, cache);
@@ -122,6 +124,24 @@ public void testCountryBuildDefaults() throws Exception {
122124
assertFalse(processor.isIgnoreMissing());
123125
}
124126

127+
public void testAsnBuildDefaults() throws Exception {
128+
GeoIpProcessor.Factory factory = new GeoIpProcessor.Factory(databaseReaders);
129+
130+
Map<String, Object> config = new HashMap<>();
131+
config.put("field", "_field");
132+
config.put("database_file", "GeoLite2-ASN.mmdb.gz");
133+
String processorTag = randomAlphaOfLength(10);
134+
135+
GeoIpProcessor processor = factory.create(null, processorTag, config);
136+
137+
assertThat(processor.getTag(), equalTo(processorTag));
138+
assertThat(processor.getField(), equalTo("_field"));
139+
assertThat(processor.getTargetField(), equalTo("geoip"));
140+
assertThat(processor.getDbReader().getMetadata().getDatabaseType(), equalTo("GeoLite2-ASN"));
141+
assertThat(processor.getProperties(), sameInstance(GeoIpProcessor.Factory.DEFAULT_ASN_PROPERTIES));
142+
assertFalse(processor.isIgnoreMissing());
143+
}
144+
125145
public void testBuildTargetField() throws Exception {
126146
GeoIpProcessor.Factory factory = new GeoIpProcessor.Factory(databaseReaders);
127147
Map<String, Object> config = new HashMap<>();
@@ -146,20 +166,39 @@ public void testBuildDbFile() throws Exception {
146166
assertFalse(processor.isIgnoreMissing());
147167
}
148168

149-
public void testBuildWithCountryDbAndCityFields() throws Exception {
169+
public void testBuildWithCountryDbAndAsnFields() throws Exception {
150170
GeoIpProcessor.Factory factory = new GeoIpProcessor.Factory(databaseReaders);
151171
Map<String, Object> config = new HashMap<>();
152172
config.put("field", "_field");
153173
config.put("database_file", "GeoLite2-Country.mmdb.gz");
154-
EnumSet<GeoIpProcessor.Property> cityOnlyProperties = EnumSet.complementOf(GeoIpProcessor.Property.ALL_COUNTRY_PROPERTIES);
174+
EnumSet<GeoIpProcessor.Property> asnOnlyProperties = EnumSet.copyOf(GeoIpProcessor.Property.ALL_ASN_PROPERTIES);
175+
asnOnlyProperties.remove(GeoIpProcessor.Property.IP);
176+
String asnProperty = RandomPicks.randomFrom(Randomness.get(), asnOnlyProperties).toString();
177+
config.put("properties", Collections.singletonList(asnProperty));
178+
try {
179+
factory.create(null, null, config);
180+
fail("Exception expected");
181+
} catch (ElasticsearchParseException e) {
182+
assertThat(e.getMessage(), equalTo("[properties] illegal property value [" + asnProperty +
183+
"]. valid values are [IP, COUNTRY_ISO_CODE, COUNTRY_NAME, CONTINENT_NAME]"));
184+
}
185+
}
186+
187+
public void testBuildWithAsnDbAndCityFields() throws Exception {
188+
GeoIpProcessor.Factory factory = new GeoIpProcessor.Factory(databaseReaders);
189+
Map<String, Object> config = new HashMap<>();
190+
config.put("field", "_field");
191+
config.put("database_file", "GeoLite2-ASN.mmdb.gz");
192+
EnumSet<GeoIpProcessor.Property> cityOnlyProperties = EnumSet.copyOf(GeoIpProcessor.Property.ALL_CITY_PROPERTIES);
193+
cityOnlyProperties.remove(GeoIpProcessor.Property.IP);
155194
String cityProperty = RandomPicks.randomFrom(Randomness.get(), cityOnlyProperties).toString();
156195
config.put("properties", Collections.singletonList(cityProperty));
157196
try {
158197
factory.create(null, null, config);
159198
fail("Exception expected");
160199
} catch (ElasticsearchParseException e) {
161200
assertThat(e.getMessage(), equalTo("[properties] illegal property value [" + cityProperty +
162-
"]. valid values are [IP, COUNTRY_ISO_CODE, COUNTRY_NAME, CONTINENT_NAME]"));
201+
"]. valid values are [IP, ASN, ORGANIZATION_NAME]"));
163202
}
164203
}
165204

@@ -230,6 +269,8 @@ public void testLazyLoading() throws Exception {
230269
geoIpConfigDir.resolve("GeoLite2-City.mmdb.gz"));
231270
Files.copy(new ByteArrayInputStream(StreamsUtils.copyToBytesFromClasspath("/GeoLite2-Country.mmdb.gz")),
232271
geoIpConfigDir.resolve("GeoLite2-Country.mmdb.gz"));
272+
Files.copy(new ByteArrayInputStream(StreamsUtils.copyToBytesFromClasspath("/GeoLite2-ASN.mmdb.gz")),
273+
geoIpConfigDir.resolve("GeoLite2-ASN.mmdb.gz"));
233274

234275
// Loading another database reader instances, because otherwise we can't test lazy loading as the
235276
// database readers used at class level are reused between tests. (we want to keep that otherwise running this
@@ -249,6 +290,10 @@ public void testLazyLoading() throws Exception {
249290
config.put("field", "_field");
250291
config.put("database_file", "GeoLite2-Country.mmdb.gz");
251292
factory.create(null, "_tag", config);
293+
config = new HashMap<>();
294+
config.put("field", "_field");
295+
config.put("database_file", "GeoLite2-ASN.mmdb.gz");
296+
factory.create(null, "_tag", config);
252297

253298
for (DatabaseReaderLazyLoader lazyLoader : databaseReaders.values()) {
254299
assertNotNull(lazyLoader.databaseReader.get());

plugins/ingest-geoip/src/test/java/org/elasticsearch/ingest/geoip/GeoIpProcessorTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,26 @@ public void testCountryWithMissingLocation() throws Exception {
187187
assertThat(geoData.get("ip"), equalTo("80.231.5.0"));
188188
}
189189

190+
public void testAsn() throws Exception {
191+
String ip = "82.170.213.79";
192+
InputStream database = getDatabaseFileInputStream("/GeoLite2-ASN.mmdb.gz");
193+
GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field",
194+
new DatabaseReader.Builder(database).build(), "target_field", EnumSet.allOf(GeoIpProcessor.Property.class), false);
195+
196+
Map<String, Object> document = new HashMap<>();
197+
document.put("source_field", ip);
198+
IngestDocument ingestDocument = RandomDocumentPicks.randomIngestDocument(random(), document);
199+
processor.execute(ingestDocument);
200+
201+
assertThat(ingestDocument.getSourceAndMetadata().get("source_field"), equalTo(ip));
202+
@SuppressWarnings("unchecked")
203+
Map<String, Object> geoData = (Map<String, Object>) ingestDocument.getSourceAndMetadata().get("target_field");
204+
assertThat(geoData.size(), equalTo(3));
205+
assertThat(geoData.get("ip"), equalTo(ip));
206+
assertThat(geoData.get("asn"), equalTo(5615));
207+
assertThat(geoData.get("organization_name"), equalTo("KPN B.V."));
208+
}
209+
190210
public void testAddressIsNotInTheDatabase() throws Exception {
191211
InputStream database = getDatabaseFileInputStream("/GeoLite2-City.mmdb.gz");
192212
GeoIpProcessor processor = new GeoIpProcessor(randomAlphaOfLength(10), "source_field",

plugins/ingest-geoip/src/test/resources/rest-api-spec/test/ingest_geoip/20_geoip_processor.yml

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
- match: { _source.geoip.continent_name: "North America" }
8888

8989
---
90-
"Test geoip processor with different database file":
90+
"Test geoip processor with different database file - GeoLite2-Country":
9191
- do:
9292
ingest.put_pipeline:
9393
id: "my_pipeline"
@@ -195,3 +195,41 @@
195195
- match: { _source.geoip.location.lat: 44.9759 }
196196
- match: { _source.geoip.region_name: "Minnesota" }
197197
- match: { _source.geoip.continent_name: "North America" }
198+
199+
---
200+
"Test geoip processor with different database file - GeoLite2-ASN":
201+
- do:
202+
ingest.put_pipeline:
203+
id: "my_pipeline"
204+
body: >
205+
{
206+
"description": "_description",
207+
"processors": [
208+
{
209+
"geoip" : {
210+
"field" : "field1",
211+
"database_file" : "GeoLite2-ASN.mmdb.gz"
212+
}
213+
}
214+
]
215+
}
216+
- match: { acknowledged: true }
217+
218+
- do:
219+
index:
220+
index: test
221+
type: test
222+
id: 1
223+
pipeline: "my_pipeline"
224+
body: {field1: "82.170.213.79"}
225+
226+
- do:
227+
get:
228+
index: test
229+
type: test
230+
id: 1
231+
- match: { _source.field1: "82.170.213.79" }
232+
- length: { _source.geoip: 3 }
233+
- match: { _source.geoip.ip: "82.170.213.79" }
234+
- match: { _source.geoip.asn: 5615 }
235+
- match: { _source.geoip.organization_name: "KPN B.V." }

0 commit comments

Comments
 (0)