logstash-filter-ipinfo-db-zuin 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2b36618d5861aa97840f2735fe419fd4303f858f553d883ee8f798c480153666
4
+ data.tar.gz: a298c76f7316d14c68d9efca5923797c7f6f231758ace241fa7a4c723344181d
5
+ SHA512:
6
+ metadata.gz: 7e066fcca849ed60a80c95b745756185d92f5494492df4bd3a7792d011788c2243c3d91a91ee9dd5cb85ab46917b83f38d1b42125fed45f5f7e92d9c9c953040
7
+ data.tar.gz: 7e38e997a2a75444dfefc2630f1632ff8a1929c5d2e66942135601d6c991e26979eb16ff2f3f3a61d61d4a681fb753026978482da72e842af9085ebb7546a272
data/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ ## 0.2.0
2
+
3
+ - Forked as `logstash-filter-ipinfo-db-zuin` (filter name stays `ipinfo-db`)
4
+ - Add `token` setting; the ipinfo.io token is no longer hardcoded
5
+ - Add `database` setting. The database is now stored at
6
+ `<path.data>/plugins/filters/ipinfo-db/country_asn.mmdb` by default instead of the
7
+ Logstash working directory, which is `/` (not writable) under the deb/rpm systemd service
8
+ - Validate the downloaded database before atomically moving it into place, so an interrupted
9
+ or invalid download no longer leaves a broken file that blocks every later start
10
+ - Add `database_age_warning_days` (default 30): log a warning at startup and once a day
11
+ when the database is older than that
12
+ - Handle IPs not in the database and missing source fields without raising exceptions
13
+ (faster on JRuby); log unexpected lookup errors as warnings instead of swallowing them
14
+ - IPs that are valid but not in the database are no longer tagged with `tag_on_failure`;
15
+ the event is left unchanged
16
+ - Filters that use the same database file share one in-memory copy instead of loading one
17
+ each (several `ipinfo-db` blocks, pipelines, config reloads)
18
+ - Pin `down` to `~> 5.2` (compatible with `logstash-core`, which requires `down ~> 5.2.0`)
19
+ and `maxmind-db` to `~> 1.2`
20
+ - Docs: README now documents installation and settings; `source` docs no longer claim hostname
21
+ support; package `docs/` and no longer package `vendor/`
22
+
23
+ ## 0.1.0
24
+
25
+ - Plugin created with the logstash plugin generator
26
+ - Initial devlopment
27
+ - Test case for plugin
28
+ - Documentation
29
+ - Review Preparation
30
+
data/CONTRIBUTORS ADDED
@@ -0,0 +1,11 @@
1
+ The following is a list of people who have contributed ideas, code, bug
2
+ reports, or in general have helped logstash along its way.
3
+
4
+ Contributors:
5
+ * Muhammad Awais Murad - awais@ipinfo.io
6
+ * Vincent Kuo - vit@zuin.ai
7
+
8
+ Note: If you've sent us patches, bug reports, or otherwise contributed to
9
+ Logstash, and you aren't on the list above and want to be, please let us know
10
+ and we'll make sure you're here. Contributions from folks like you are what make
11
+ open source awesome.
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ logstash_path = ENV['LOGSTASH_PATH'] || '/usr/share/logstash'
5
+
6
+ if Dir.exist?(logstash_path)
7
+ gem 'logstash-core', :path => "#{logstash_path}/logstash-core"
8
+ gem 'logstash-core-plugin-api', :path => "#{logstash_path}/logstash-core-plugin-api"
9
+ end
10
+
data/LICENSE ADDED
@@ -0,0 +1,11 @@
1
+ Licensed under the Apache License, Version 2.0 (the "License");
2
+ you may not use this file except in compliance with the License.
3
+ You may obtain a copy of the License at
4
+
5
+ http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software
8
+ distributed under the License is distributed on an "AS IS" BASIS,
9
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ See the License for the specific language governing permissions and
11
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # logstash-filter-ipinfo-db-zuin
2
+
3
+ A [Logstash](https://github.com/elastic/logstash) filter plugin that looks up the country and ASN of an
4
+ IP address in the [ipinfo.io](https://ipinfo.io) free country and ASN database (`.mmdb`), locally,
5
+ without calling the ipinfo API per event.
6
+
7
+ This is a fork of [`logstash-filter-ipinfo-db`](https://rubygems.org/gems/logstash-filter-ipinfo-db).
8
+ The gem is renamed, but the filter is still called `ipinfo-db`, so existing pipeline configs keep working.
9
+ Do not install both gems in the same Logstash.
10
+
11
+ The license is Apache 2.0.
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ bin/logstash-plugin install logstash-filter-ipinfo-db-zuin
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```
22
+ filter {
23
+ ipinfo-db {
24
+ source => "ip"
25
+ token => "${IPINFO_TOKEN}"
26
+ }
27
+ }
28
+ ```
29
+
30
+ On first start, the plugin downloads the database with your ipinfo.io token to
31
+ `<path.data>/plugins/filters/ipinfo-db/country_asn.mmdb` (for example `/var/lib/logstash/...` for
32
+ deb/rpm installs). Store the token in an environment variable or the Logstash keystore:
33
+
34
+ ```sh
35
+ bin/logstash-keystore add IPINFO_TOKEN
36
+ ```
37
+
38
+ The lookup result is written to the `ipinfo-db` field. For `8.8.8.8` it looks like this (illustrative):
39
+
40
+ ```json
41
+ "ipinfo-db": {
42
+ "country": "US",
43
+ "country_name": "United States",
44
+ "continent": "NA",
45
+ "continent_name": "North America",
46
+ "asn": "AS15169",
47
+ "as_name": "Google LLC",
48
+ "as_domain": "google.com"
49
+ }
50
+ ```
51
+
52
+ | Setting | Default | Description |
53
+ |---|---|---|
54
+ | `source` | (required) | Field containing the IP address (IPv4 or IPv6). Hostnames are not resolved. |
55
+ | `target` | `ipinfo-db` | Field to write the result into. |
56
+ | `token` | none | ipinfo.io token, used only to download the database when the file does not exist. |
57
+ | `database` | `<path.data>/plugins/filters/ipinfo-db/country_asn.mmdb` | Path to the `.mmdb` file. Downloaded there if missing. |
58
+ | `database_age_warning_days` | `30` | Log a warning when the database is older than this. `0` disables it. |
59
+ | `tag_on_failure` | `["_ipinfo_db_lookup_failure"]` | Tags added when the field is missing, the value is not a valid IP, or the lookup fails. IPs that are simply not in the database are not tagged. |
60
+
61
+ The plugin does not update the database automatically. To refresh it, delete the file and restart
62
+ Logstash, or replace the file with a newer copy. See [docs/index.asciidoc](docs/index.asciidoc) for the
63
+ full reference.
64
+
65
+ ## Development
66
+
67
+ You need JRuby with Bundler, and a local Logstash for `logstash-core`. Set `LOGSTASH_PATH` if it is not
68
+ in `/usr/share/logstash` (see `Gemfile`).
69
+
70
+ ```sh
71
+ bundle install
72
+ bundle exec rspec
73
+ ```
74
+
75
+ The default suite runs offline against `spec/fixtures/ipinfo-test.mmdb`, a tiny synthetic database
76
+ (documentation IP ranges only) generated by `spec/fixtures/generate_ipinfo_test_mmdb.py`. The live test,
77
+ which downloads the real database from ipinfo.io, is tagged `integration` and excluded by default:
78
+
79
+ ```sh
80
+ IPINFO_TOKEN=... bundle exec rspec --tag integration
81
+ ```
82
+
83
+ ### Verify on a real Logstash
84
+
85
+ `verify/verify.sh` builds the gem, installs it into the official Logstash Docker image, runs real
86
+ pipelines (download as a non-root user with working directory `/`, lookups, restart without token,
87
+ missing-token error) and the RSpec suite on Logstash's JRuby. It needs Docker, Ruby and Python 3:
88
+
89
+ ```sh
90
+ IPINFO_TOKEN=... verify/verify.sh 9.5.5
91
+ ```
92
+
93
+ Without `IPINFO_TOKEN`, the checks that download the live database (first start, restart and the
94
+ integration spec) are reported as `SKIP`; the install check, the missing-token error and the offline
95
+ specs still run.
96
+
97
+ Logs are written to `verify/out/`. GitHub Actions (`.github/workflows/verify.yml`) runs the same
98
+ script against Logstash 8.19.24 and 9.5.5 on pushes and pull requests to `main`, reading the token
99
+ from the `IPINFO_TOKEN` repository secret. Pull requests from forks don't get the secret, so they run
100
+ the offline checks only.
101
+
102
+ ### Run the plugin in Logstash
103
+
104
+ Build and install the gem:
105
+
106
+ ```sh
107
+ gem build logstash-filter-ipinfo-db-zuin.gemspec
108
+ bin/logstash-plugin install /path/to/logstash-filter-ipinfo-db-zuin-<version>.gem
109
+ ```
110
+
111
+ Or add it to the Logstash `Gemfile` from your local checkout, then run `bin/logstash-plugin install --no-verify`:
112
+
113
+ ```ruby
114
+ gem "logstash-filter-ipinfo-db-zuin", :path => "/path/to/logstash-filter-ipinfo-db"
115
+ ```
116
+
117
+ ## Help
118
+
119
+ Open an issue at https://github.com/reflective-technology/logstash-filter-ipinfo-db/issues, or ask in the
120
+ [Logstash discussion forum](https://discuss.elastic.co/c/logstash).
@@ -0,0 +1,127 @@
1
+ :plugin: logstash-filter-ipinfo-db-zuin
2
+ :type: filter
3
+
4
+ ///////////////////////////////////////////
5
+ START - GENERATED VARIABLES, DO NOT EDIT!
6
+ ///////////////////////////////////////////
7
+ :version: %VERSION%
8
+ :release_date: %RELEASE_DATE%
9
+ :changelog_url: %CHANGELOG_URL%
10
+ :include_path: ../../../../logstash/docs/include
11
+ ///////////////////////////////////////////
12
+ END - GENERATED VARIABLES, DO NOT EDIT!
13
+ ///////////////////////////////////////////
14
+
15
+ [id="plugins-{type}s-{plugin}"]
16
+
17
+ === logstash-filter-ipinfo-db-zuin filter plugin
18
+
19
+ include::{include_path}/plugin_header.asciidoc[]
20
+
21
+ ==== Description
22
+
23
+ The ipinfo filter adds information about the geographical location and autonomous system number (ASN) of IP addresses,
24
+ based on data from the ipinfo.io free country and ASN database.
25
+
26
+ For example, if you have a field named `foo` which you want to lookup for location and ASN in ipinfo.io free database, and you want to store the
27
+ result of lookup in `bar`, do this:
28
+ [source,ruby]
29
+ filter {
30
+ ipinfo-db {
31
+ source => "foo"
32
+ target => "bar"
33
+ token => "${IPINFO_TOKEN}"
34
+ }
35
+ }
36
+
37
+ [id="plugins-{type}s-{plugin}-options"]
38
+ ==== ipinfo-db Filter Configuration Options
39
+
40
+ This plugin supports the following configuration options plus the <<plugins-{type}s-{plugin}-common-options>> described later.
41
+
42
+ [cols="<,<,<",options="header",]
43
+ |=======================================================================
44
+ |Setting |Input type|Required
45
+ | <<plugins-{type}s-{plugin}-database>> |<<string,string>>|No
46
+ | <<plugins-{type}s-{plugin}-database_age_warning_days>> |<<number,number>>|No
47
+ | <<plugins-{type}s-{plugin}-source>> |<<string,string>>|Yes
48
+ | <<plugins-{type}s-{plugin}-tag_on_failure>> |<<array,array>>|No
49
+ | <<plugins-{type}s-{plugin}-target>> |<<string,string>>|No
50
+ | <<plugins-{type}s-{plugin}-token>> |<<password,password>>|No
51
+ |=======================================================================
52
+
53
+ Also see <<plugins-{type}s-{plugin}-common-options>> for a list of options supported by all
54
+ filter plugins.
55
+
56
+ &nbsp;
57
+
58
+ [id="plugins-{type}s-{plugin}-database"]
59
+ ===== `database`
60
+
61
+ * Value type is <<string,string>>
62
+ * Default value is `<path.data>/plugins/filters/ipinfo-db/country_asn.mmdb`
63
+
64
+ Path to the ipinfo `.mmdb` database file. If the file exists, the plugin uses it as is. If it does not exist,
65
+ the plugin downloads it to this path on startup using <<plugins-{type}s-{plugin}-token>>, creating parent
66
+ directories as needed. `path.data` is the Logstash data directory (for example `/var/lib/logstash` for
67
+ deb/rpm installs and `/usr/share/logstash/data` in Docker). Use an absolute path; a relative path is resolved
68
+ against the Logstash working directory.
69
+
70
+ The database is loaded into memory once per file: all `ipinfo-db` filters in the Logstash process that use
71
+ the same path (including across pipelines and config reloads) share one copy. If the file is replaced, the
72
+ next filter that starts, for example after a config reload, loads the new file. When no running filter uses a
73
+ database any more (its pipelines stopped, or were reloaded with a different `database` path), it is released
74
+ from memory.
75
+
76
+ [id="plugins-{type}s-{plugin}-database_age_warning_days"]
77
+ ===== `database_age_warning_days`
78
+
79
+ * Value type is <<number,number>>
80
+ * Default value is `30`
81
+
82
+ The plugin does not update the database by itself. It logs a warning when the database was built more than this
83
+ many days ago, using the build time stored in the `.mmdb` file. The check runs at startup and then once a day.
84
+ To refresh the database, delete the file so it is downloaded again on the next start, or replace it with a newer
85
+ copy. Set to `0` to disable the warning.
86
+
87
+ [id="plugins-{type}s-{plugin}-source"]
88
+ ===== `source`
89
+
90
+ * This is a required setting.
91
+ * Value type is <<string,string>>
92
+ * There is no default value for this setting.
93
+
94
+ The field containing the IP address (IPv4 or IPv6) to look up in the ipinfo database. Hostnames are not resolved;
95
+ a value that is not a valid IP address tags the event with <<plugins-{type}s-{plugin}-tag_on_failure>>.
96
+
97
+ [id="plugins-{type}s-{plugin}-tag_on_failure"]
98
+ ===== `tag_on_failure`
99
+
100
+ * Value type is <<array,array>>
101
+ * Default value is `["_ipinfo_db_lookup_failure"]`
102
+
103
+ Tags the event when the lookup cannot be done: the source field is missing or empty, the value is not a valid
104
+ IP address, or the lookup raises an error. This can be used in later analysis. An IP address that is valid but
105
+ not in the database (for example a private address) is not a failure: the event is left unchanged and not tagged.
106
+
107
+ [id="plugins-{type}s-{plugin}-target"]
108
+ ===== `target`
109
+
110
+ * Value type is <<string,string>>
111
+ * Default value is `ipinfo-db`
112
+
113
+ The field to write the lookup results into.
114
+
115
+
116
+ [id="plugins-{type}s-{plugin}-token"]
117
+ ===== `token`
118
+
119
+ * Value type is <<password,password>>
120
+ * There is no default value for this setting.
121
+
122
+ Your ipinfo.io access token, used to download the database when the file at
123
+ <<plugins-{type}s-{plugin}-database>> does not exist. If the file is missing and no token is set, the plugin fails to start.
124
+ To keep the token out of the pipeline config, use `${IPINFO_TOKEN}` with an environment variable or the Logstash keystore.
125
+
126
+ [id="plugins-{type}s-{plugin}-common-options"]
127
+ include::{include_path}/{type}.asciidoc[]
@@ -0,0 +1,268 @@
1
+ # encoding: utf-8
2
+ require "logstash/filters/base"
3
+ require 'down'
4
+ require 'maxmind/db'
5
+ require 'fileutils'
6
+ require 'securerandom'
7
+
8
+ # Looks up the country and ASN of an IP address in the ipinfo.io free country
9
+ # and ASN database (.mmdb) and stores the result in the event.
10
+ #
11
+ # filter {
12
+ # ipinfo-db {
13
+ # source => "ip"
14
+ # token => "${IPINFO_TOKEN}"
15
+ # }
16
+ # }
17
+ class LogStash::Filters::IpinfoDb < LogStash::Filters::Base
18
+
19
+ DOWNLOAD_URL = "https://ipinfo.io/data/free/country_asn.mmdb"
20
+ DEFAULT_FILE_NAME = "country_asn.mmdb"
21
+ AGE_CHECK_INTERVAL = 24 * 60 * 60 # seconds
22
+
23
+ config_name "ipinfo-db"
24
+
25
+ # The field containing the IP address to look up. Hostnames are not resolved.
26
+ config :source, :validate => :string, :required => true
27
+
28
+ # The field to write the lookup results into. If not specified, the default
29
+ # value will be used.
30
+ config :target, :validate => :string, :default => "ipinfo-db"
31
+
32
+ # Tags the event on failure to look up ipinfo information. This can be used in later analysis.
33
+ # If not specified, the default value will be used.
34
+ config :tag_on_failure, :validate => :array, :default => ["_ipinfo_db_lookup_failure"]
35
+
36
+ # ipinfo.io access token used to download the database. Only needed when
37
+ # the file at `database` does not exist yet. Use ${IPINFO_TOKEN} to read it
38
+ # from an environment variable or the Logstash keystore.
39
+ config :token, :validate => :password
40
+
41
+ # Path to the ipinfo .mmdb database file. If the file does not exist, it is
42
+ # downloaded to this path using `token`. Defaults to
43
+ # <path.data>/plugins/filters/ipinfo-db/country_asn.mmdb.
44
+ config :database, :validate => :string
45
+
46
+ # Log a warning when the database was built more than this many days ago.
47
+ # Checked at startup and then once a day. Set to 0 to disable.
48
+ config :database_age_warning_days, :validate => :number, :default => 30
49
+
50
+ # One in-memory reader per database file, shared by every filter instance in
51
+ # the process (several ipinfo-db blocks, pipelines, config reloads), so the
52
+ # database is held in the heap once. Readers are safe to use from multiple
53
+ # threads.
54
+ #
55
+ # - Entries record the identity (inode, size, mtime) of the file they were
56
+ # loaded from, verified before and after loading, so a replaced file is
57
+ # loaded again by the next instance that registers.
58
+ # - Entries are reference counted: register acquires, close releases, and the
59
+ # last release evicts the entry, so readers for files no filter uses any
60
+ # more (stopped pipelines, reloads to another database path) can be
61
+ # garbage collected.
62
+ # - Setup for a given file (existence check, download, load, cache insert)
63
+ # runs under a per-path lock, so pipelines starting together download and
64
+ # load a missing database only once, while unrelated database files still
65
+ # set up in parallel. @registry_mutex only guards the hashes.
66
+ @shared_readers = {}
67
+ @path_locks = {}
68
+ @registry_mutex = Mutex.new
69
+
70
+ LOAD_ATTEMPTS = 3
71
+
72
+ class << self
73
+ def with_path_lock(path)
74
+ lock = @registry_mutex.synchronize { @path_locks[path] ||= Mutex.new }
75
+ lock.synchronize { yield }
76
+ end
77
+
78
+ # Acquire the cached reader for path if it was loaded from the file
79
+ # currently there; nil otherwise.
80
+ def acquire_reader(path)
81
+ identity = file_identity(path)
82
+ @registry_mutex.synchronize do
83
+ entry = @shared_readers[path]
84
+ next nil unless entry && entry[:identity] == identity
85
+ entry[:refs] += 1
86
+ entry[:reader]
87
+ end
88
+ end
89
+
90
+ # Load path and cache the reader under the identity the file had for the
91
+ # whole load. If the file is replaced while loading, load it again.
92
+ def load_reader(path)
93
+ LOAD_ATTEMPTS.times do
94
+ before = file_identity(path)
95
+ reader = MaxMind::DB.new(path, mode: MaxMind::DB::MODE_MEMORY)
96
+ return store_reader(path, before, reader) if file_identity(path) == before
97
+ end
98
+ raise LogStash::ConfigurationError, "ipinfo-db: database file #{path} kept changing while it was being loaded"
99
+ end
100
+
101
+ # Cache reader (loaded from the file with identity) with one reference.
102
+ def store_reader(path, identity, reader)
103
+ @registry_mutex.synchronize { @shared_readers[path] = { :identity => identity, :reader => reader, :refs => 1 } }
104
+ reader
105
+ end
106
+
107
+ # Drop one reference; evict the entry when no filter uses it any more. A
108
+ # reader that was already superseded by a newer file is not in the cache.
109
+ def release_reader(path, reader)
110
+ @registry_mutex.synchronize do
111
+ entry = @shared_readers[path]
112
+ next unless entry && entry[:reader].equal?(reader)
113
+ entry[:refs] -= 1
114
+ @shared_readers.delete(path) if entry[:refs] <= 0
115
+ end
116
+ end
117
+
118
+ # The cached reader for path, if any (no reference taken). For specs.
119
+ def cached_reader(path)
120
+ @registry_mutex.synchronize { @shared_readers[File.expand_path(path)] }&.fetch(:reader)
121
+ end
122
+
123
+ # For specs.
124
+ def clear_shared_readers
125
+ @registry_mutex.synchronize { @shared_readers.clear }
126
+ end
127
+
128
+ def file_identity(path)
129
+ stat = File.stat(path)
130
+ [stat.ino, stat.size, stat.mtime.to_r]
131
+ end
132
+ end
133
+
134
+ def register
135
+ setup_database
136
+ check_database_age
137
+ end
138
+
139
+ def close
140
+ self.class.release_reader(@database, @db) if @db
141
+ @db = nil
142
+ end
143
+
144
+ def filter(event)
145
+ next_age_check = @next_age_check
146
+ check_database_age if next_age_check && Time.now >= next_age_check
147
+ @logger.debug? && @logger.debug("Running ipinfo-db lookup", :event => event)
148
+ ip = event.get(@source)
149
+
150
+ # Misses are common (private IPs, missing fields), so handle them without
151
+ # raising: exceptions are expensive in JRuby.
152
+ if ip.nil? || ip == ""
153
+ tag_unsuccessful_lookup(event, "source field is empty")
154
+ return
155
+ end
156
+
157
+ begin
158
+ record = @db.get(ip)
159
+ rescue ArgumentError => e
160
+ # Not a valid IP address (IPAddr errors are ArgumentErrors), or an IPv6
161
+ # address looked up in an IPv4-only database.
162
+ tag_unsuccessful_lookup(event, e.message)
163
+ return
164
+ rescue => e
165
+ @logger.warn("Unexpected error during ipinfo lookup", :ip => ip, :exception => e.class.name, :message => e.message)
166
+ tag_unsuccessful_lookup(event, e.message)
167
+ return
168
+ end
169
+
170
+ # Not in the database (e.g. private ranges): not an error, leave the event as is.
171
+ if record.nil?
172
+ @logger.debug? && @logger.debug("IP not found in the ipinfo database", :ip => ip)
173
+ return
174
+ end
175
+
176
+ begin
177
+ event.set(@target, record)
178
+ filter_matched(event)
179
+ rescue => e
180
+ @logger.warn("Unexpected error during ipinfo enrichment", :ip => ip, :exception => e.class.name, :message => e.message)
181
+ tag_unsuccessful_lookup(event, e.message)
182
+ end
183
+ end
184
+
185
+ def tag_unsuccessful_lookup(event, reason)
186
+ @logger.debug? && @logger.debug("ipinfo lookup failed", :ip => event.get(@source), :reason => reason)
187
+ @tag_on_failure.each{|tag| event.tag(tag)}
188
+ end
189
+
190
+ # Download .mmdb file and load it into mmdb reader
191
+ def setup_database
192
+ # Normalize once (expands ~ and makes relative paths absolute) so the existence
193
+ # check, download, rename and reader cache all use the same path.
194
+ @database = File.expand_path(@database || default_database_path)
195
+ path = @database
196
+
197
+ @db = self.class.with_path_lock(path) do
198
+ if File.exist?(path)
199
+ self.class.acquire_reader(path) || self.class.load_reader(path)
200
+ else
201
+ if @token.nil? || @token.value.to_s.empty?
202
+ raise LogStash::ConfigurationError, "ipinfo-db: database file #{@database} not found. Set `token` to download it, or place the file there."
203
+ end
204
+ reader, identity = download_database
205
+ # A rename keeps inode, size and mtime, so a different identity here means
206
+ # another process replaced the file after our rename: load what is there.
207
+ if File.exist?(path) && self.class.file_identity(path) == identity
208
+ self.class.store_reader(path, identity, reader)
209
+ else
210
+ self.class.load_reader(path)
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ private
217
+
218
+ # Warn when the loaded database is older than database_age_warning_days,
219
+ # based on the build time stored in the database itself.
220
+ def check_database_age
221
+ @next_age_check = nil
222
+ return if @database_age_warning_days <= 0
223
+
224
+ build_epoch = @db.metadata.build_epoch
225
+ return if build_epoch.nil?
226
+
227
+ @next_age_check = Time.now + AGE_CHECK_INTERVAL
228
+ age_days = ((Time.now.to_i - build_epoch) / 86400.0).floor
229
+ if age_days > @database_age_warning_days
230
+ @logger.warn("ipinfo database is outdated; lookups may be inaccurate. " \
231
+ "Delete the file to download a fresh copy on the next start (requires `token`), or replace it",
232
+ :path => @database, :age_days => age_days, :built_at => Time.at(build_epoch).utc.to_s,
233
+ :database_age_warning_days => @database_age_warning_days)
234
+ end
235
+ end
236
+
237
+ # Download into a temp file next to @database, check that it is a readable
238
+ # database, then rename it into place. The rename is atomic, so @database is
239
+ # never a partial or invalid file, even if the download is interrupted or
240
+ # several pipelines download at the same time. Returns the reader and the
241
+ # identity of the file it was loaded from.
242
+ def download_database
243
+ @logger.info("Downloading ipinfo database", :path => @database)
244
+ dir = File.dirname(@database)
245
+ FileUtils.mkdir_p(dir)
246
+ tmp_path = File.join(dir, ".#{File.basename(@database)}.#{SecureRandom.hex(8)}.tmp")
247
+
248
+ tempfile = Down.download("#{DOWNLOAD_URL}?token=#{@token.value}")
249
+ FileUtils.mv(tempfile.path, tmp_path)
250
+ begin
251
+ db = MaxMind::DB.new(tmp_path, mode: MaxMind::DB::MODE_MEMORY)
252
+ rescue MaxMind::DB::InvalidDatabaseError => e
253
+ raise LogStash::ConfigurationError, "ipinfo-db: downloaded database is not a valid .mmdb file: #{e.message}"
254
+ end
255
+ identity = self.class.file_identity(tmp_path) # only this process writes tmp_path
256
+ File.rename(tmp_path, @database)
257
+ [db, identity]
258
+ ensure
259
+ FileUtils.rm_f(tmp_path) if tmp_path
260
+ end
261
+
262
+ # Keep the downloaded database under Logstash's data directory, which is
263
+ # writable by the logstash user and persists across restarts.
264
+ def default_database_path
265
+ File.join(LogStash::SETTINGS.get_value("path.data"), "plugins", "filters", "ipinfo-db", DEFAULT_FILE_NAME)
266
+ end
267
+
268
+ end # class LogStash::Filters::IpinfoDb
@@ -0,0 +1,28 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'logstash-filter-ipinfo-db-zuin'
3
+ s.version = '0.2.0'
4
+ s.licenses = ['Apache-2.0']
5
+ s.summary = 'Logstash Filter Plugin for IPinfoDb'
6
+ s.description = 'This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline ' \
7
+ 'using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program. ' \
8
+ 'It looks up the country and ASN of IP addresses in the ipinfo.io free country and ASN database.'
9
+ s.homepage = 'https://github.com/reflective-technology/logstash-filter-ipinfo-db'
10
+ s.authors = ['ZUIN RD']
11
+ s.email = 'rd@zuin.ai'
12
+ s.require_paths = ['lib']
13
+
14
+ # Files
15
+ s.files = Dir['lib/**/*','spec/**/*','docs/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE']
16
+ # Tests
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+
19
+ # Special flag to let us know this is actually a logstash plugin
20
+ s.metadata = { "logstash_plugin" => "true", "logstash_group" => "filter" }
21
+
22
+ # Gem dependencies
23
+ s.add_runtime_dependency "logstash-core-plugin-api", "~> 2.0"
24
+ # logstash-core itself depends on down ~> 5.2.0, so this must allow 5.2.x.
25
+ s.add_runtime_dependency "down", "~> 5.2"
26
+ s.add_runtime_dependency "maxmind-db", "~> 1.2"
27
+ s.add_development_dependency 'logstash-devutils', '~> 2.6'
28
+ end
@@ -0,0 +1,379 @@
1
+ # encoding: utf-8
2
+ require_relative '../spec_helper'
3
+ require "logstash/filters/ipinfo-db"
4
+ require "fileutils"
5
+ require "tmpdir"
6
+ require "tempfile"
7
+ require "ipaddr"
8
+
9
+ # Synthetic database with documentation networks only; see spec/fixtures/generate_ipinfo_test_mmdb.py.
10
+ FIXTURE_DATABASE = File.expand_path("../fixtures/ipinfo-test.mmdb", __dir__)
11
+
12
+ describe LogStash::Filters::IpinfoDb do
13
+ after { described_class.clear_shared_readers }
14
+
15
+ describe "shared database reader" do
16
+ let(:data_dir) { Dir.mktmpdir }
17
+ let(:database) { File.join(data_dir, "ipinfo.mmdb").tap { |path| FileUtils.cp(FIXTURE_DATABASE, path) } }
18
+ after { FileUtils.rm_rf(data_dir) }
19
+
20
+ def registered(path)
21
+ described_class.new("source" => "ip", "database" => path, "database_age_warning_days" => 0).tap(&:register)
22
+ end
23
+
24
+ def reader(plugin)
25
+ plugin.instance_variable_get(:@db)
26
+ end
27
+
28
+ it "loads a database file once for all filter instances" do
29
+ expect(MaxMind::DB).to receive(:new).once.and_call_original
30
+ first = registered(database)
31
+ second = registered(database)
32
+ expect(reader(second)).to equal(reader(first))
33
+
34
+ event = LogStash::Event.new("ip" => "192.0.2.10")
35
+ second.filter(event)
36
+ expect(event.get("[ipinfo-db][country]")).to eq("US")
37
+ end
38
+
39
+ it "loads the file again after it is replaced" do
40
+ first = registered(database)
41
+ replacement = File.join(data_dir, "replacement.mmdb")
42
+ FileUtils.cp(FIXTURE_DATABASE, replacement)
43
+ File.rename(replacement, database)
44
+ second = registered(database)
45
+ expect(reader(second)).not_to equal(reader(first))
46
+ end
47
+
48
+ it "downloads a missing database once when filters start at the same time" do
49
+ missing = File.join(data_dir, "downloaded.mmdb")
50
+ allow(Down).to receive(:download) do
51
+ sleep 0.5 # keep the first download in progress while the second filter registers
52
+ Tempfile.new("ipinfo-download").tap do |file|
53
+ file.binmode
54
+ file.write(File.binread(FIXTURE_DATABASE))
55
+ file.flush
56
+ end
57
+ end
58
+ plugins = Array.new(2) do
59
+ described_class.new("source" => "ip", "database" => missing, "token" => "t", "database_age_warning_days" => 0)
60
+ end
61
+
62
+ plugins.map { |plugin| Thread.new { plugin.register } }.each(&:join)
63
+
64
+ expect(Down).to have_received(:download).once
65
+ expect(reader(plugins[1])).to equal(reader(plugins[0]))
66
+ expect(Dir.children(data_dir)).to eq(["downloaded.mmdb"])
67
+ end
68
+
69
+ it "evicts the reader when the last filter using it closes" do
70
+ first = registered(database)
71
+ second = registered(database)
72
+ first.close
73
+ expect(described_class.cached_reader(database)).to equal(reader(second))
74
+ second.close
75
+ expect(described_class.cached_reader(database)).to be_nil
76
+ end
77
+
78
+ it "evicts the old reader when a reload switches to another database path" do
79
+ old = registered(database)
80
+ versioned = File.join(data_dir, "ipinfo-v2.mmdb")
81
+ FileUtils.cp(FIXTURE_DATABASE, versioned)
82
+ current = registered(versioned)
83
+ old.close
84
+ expect(described_class.cached_reader(database)).to be_nil
85
+ expect(described_class.cached_reader(versioned)).to equal(reader(current))
86
+ end
87
+
88
+ it "loads the file again if it is replaced while it is being loaded" do
89
+ path = database
90
+ loads = 0
91
+ allow(MaxMind::DB).to receive(:new).and_wrap_original do |original, *args, **kwargs|
92
+ loads += 1
93
+ loaded = original.call(*args, **kwargs)
94
+ if loads == 1 # an external updater replaces the file mid-load
95
+ replacement = File.join(data_dir, "replacement.mmdb")
96
+ FileUtils.cp(FIXTURE_DATABASE, replacement)
97
+ File.rename(replacement, path)
98
+ end
99
+ loaded
100
+ end
101
+
102
+ registered(path)
103
+ expect(loads).to eq(2)
104
+ registered(path) # the cached identity matches the file now there, so no further load
105
+ expect(loads).to eq(2)
106
+ end
107
+
108
+ it "loads the file again if it is replaced right after a download is moved into place" do
109
+ target = File.join(data_dir, "downloaded.mmdb")
110
+ allow(Down).to receive(:download) do
111
+ Tempfile.new("ipinfo-download").tap do |file|
112
+ file.binmode
113
+ file.write(File.binread(FIXTURE_DATABASE))
114
+ file.flush
115
+ end
116
+ end
117
+ allow(File).to receive(:rename).and_wrap_original do |original, from, to|
118
+ result = original.call(from, to)
119
+ if to == target # an external updater rewrites it immediately
120
+ File.binwrite(to, File.binread(FIXTURE_DATABASE))
121
+ File.utime(Time.now, Time.now + 60, to)
122
+ end
123
+ result
124
+ end
125
+ expect(MaxMind::DB).to receive(:new).twice.and_call_original
126
+
127
+ plugin = described_class.new("source" => "ip", "database" => target, "token" => "t", "database_age_warning_days" => 0)
128
+ plugin.register
129
+ expect(described_class.cached_reader(target)).to equal(reader(plugin))
130
+ end
131
+
132
+ it "expands ~ in the database path before downloading and loading" do
133
+ home = File.join(data_dir, "home")
134
+ cwd = File.join(data_dir, "cwd")
135
+ FileUtils.mkdir_p([home, cwd])
136
+ allow(Down).to receive(:download) do
137
+ Tempfile.new("ipinfo-download").tap do |file|
138
+ file.binmode
139
+ file.write(File.binread(FIXTURE_DATABASE))
140
+ file.flush
141
+ end
142
+ end
143
+ original_home = ENV["HOME"]
144
+ begin
145
+ ENV["HOME"] = home
146
+ plugin = described_class.new("source" => "ip", "database" => "~/ipinfo/country_asn.mmdb",
147
+ "token" => "t", "database_age_warning_days" => 0)
148
+ Dir.chdir(cwd) { plugin.register }
149
+ ensure
150
+ ENV["HOME"] = original_home
151
+ end
152
+
153
+ expanded = File.join(home, "ipinfo", "country_asn.mmdb")
154
+ expect(File.file?(expanded)).to be true
155
+ expect(Dir.children(cwd)).to be_empty # no literal "~" directory
156
+ expect(described_class.cached_reader(expanded)).to equal(reader(plugin))
157
+ end
158
+
159
+ it "keeps separate readers for different files" do
160
+ other = File.join(data_dir, "other.mmdb")
161
+ FileUtils.cp(FIXTURE_DATABASE, other)
162
+ expect(reader(registered(other))).not_to equal(reader(registered(database)))
163
+ end
164
+ end
165
+
166
+ describe "pipeline lookups against the fixture database" do
167
+ config <<-CONFIG
168
+ filter {
169
+ ipinfo-db {
170
+ source => "ip"
171
+ database => "#{FIXTURE_DATABASE}"
172
+ database_age_warning_days => 0
173
+ }
174
+ }
175
+ CONFIG
176
+
177
+ sample({ "ip" => "192.0.2.10" }) do
178
+ expect(subject.get("ipinfo-db")).to eq(
179
+ "country" => "US", "country_name" => "United States",
180
+ "continent" => "NA", "continent_name" => "North America",
181
+ "asn" => "AS64496", "as_name" => "Example Networks US", "as_domain" => "example.com"
182
+ )
183
+ expect(subject.get("tags")).to be_nil
184
+ end
185
+
186
+ sample({ "ip" => "198.51.100.7" }) do
187
+ expect(subject.get("[ipinfo-db][country]")).to eq("JP")
188
+ expect(subject.get("[ipinfo-db][asn]")).to eq("AS64497")
189
+ end
190
+
191
+ sample({ "ip" => "2001:db8::1" }) do
192
+ expect(subject.get("[ipinfo-db][country]")).to eq("TW")
193
+ expect(subject.get("[ipinfo-db][asn]")).to eq("AS64498")
194
+ end
195
+
196
+ # Valid but not in the database: left unchanged, not tagged.
197
+ sample({ "ip" => "10.0.0.1" }) do
198
+ expect(subject.get("ipinfo-db")).to be_nil
199
+ expect(subject.get("tags")).to be_nil
200
+ end
201
+
202
+ sample({ "ip" => "not-an-ip" }) do
203
+ expect(subject.get("ipinfo-db")).to be_nil
204
+ expect(subject.get("tags")).to eq(["_ipinfo_db_lookup_failure"])
205
+ end
206
+
207
+ sample({ "other" => "no ip field" }) do
208
+ expect(subject.get("ipinfo-db")).to be_nil
209
+ expect(subject.get("tags")).to eq(["_ipinfo_db_lookup_failure"])
210
+ end
211
+ end
212
+
213
+ describe "pipeline with custom target and tag_on_failure" do
214
+ config <<-CONFIG
215
+ filter {
216
+ ipinfo-db {
217
+ source => "[client][ip]"
218
+ target => "[client][ipinfo]"
219
+ tag_on_failure => ["no_ipinfo"]
220
+ database => "#{FIXTURE_DATABASE}"
221
+ database_age_warning_days => 0
222
+ }
223
+ }
224
+ CONFIG
225
+
226
+ sample({ "client" => { "ip" => "192.0.2.10" } }) do
227
+ expect(subject.get("[client][ipinfo][country]")).to eq("US")
228
+ expect(subject.get("ipinfo-db")).to be_nil
229
+ end
230
+
231
+ sample({ "client" => { "ip" => "bogus" } }) do
232
+ expect(subject.get("tags")).to eq(["no_ipinfo"])
233
+ end
234
+ end
235
+
236
+ describe "age warning with a real database" do
237
+ let(:build_epoch) { MaxMind::DB.new(FIXTURE_DATABASE, mode: MaxMind::DB::MODE_MEMORY).metadata.build_epoch }
238
+
239
+ it "uses the build time stored in the database" do
240
+ plugin = described_class.new("source" => "ip", "database" => FIXTURE_DATABASE)
241
+ allow(Time).to receive(:now).and_return(Time.at(build_epoch + 40 * 86400))
242
+ expect(plugin.logger).to receive(:warn).with(/outdated/, hash_including(:age_days => 40))
243
+ plugin.register
244
+ end
245
+ end
246
+
247
+ # Downloads the real database from ipinfo.io. Excluded by default; run with
248
+ # IPINFO_TOKEN=... bundle exec rspec --tag integration
249
+ describe "live ipinfo.io database", :integration => true do
250
+ let(:data_dir) { Dir.mktmpdir }
251
+ after { FileUtils.rm_rf(data_dir) }
252
+
253
+ it "downloads the database and looks up a well-known IP" do
254
+ skip("set IPINFO_TOKEN to run") if ENV["IPINFO_TOKEN"].to_s.empty?
255
+ database = File.join(data_dir, "country_asn.mmdb")
256
+ plugin = described_class.new("source" => "ip", "database" => database, "token" => ENV["IPINFO_TOKEN"])
257
+ plugin.register
258
+ expect(File.size(database)).to be > 0
259
+ expect(Dir.children(data_dir)).to eq(["country_asn.mmdb"])
260
+
261
+ event = LogStash::Event.new("ip" => "8.8.8.8")
262
+ plugin.filter(event)
263
+ expect(event.get("[ipinfo-db][country]")).to eq("US")
264
+ expect(event.get("[ipinfo-db][asn]")).to eq("AS15169")
265
+ end
266
+ end
267
+
268
+ describe "database location" do
269
+ let(:data_dir) { Dir.mktmpdir }
270
+ after { FileUtils.rm_rf(data_dir) }
271
+
272
+ it "defaults to a file under path.data" do
273
+ allow(LogStash::SETTINGS).to receive(:get_value).and_call_original
274
+ allow(LogStash::SETTINGS).to receive(:get_value).with("path.data").and_return(data_dir)
275
+ expected = File.join(data_dir, "plugins", "filters", "ipinfo-db", "country_asn.mmdb")
276
+ plugin = described_class.new("source" => "ip")
277
+ expect { plugin.register }.to raise_error(LogStash::ConfigurationError, /#{Regexp.escape(expected)}/)
278
+ end
279
+
280
+ it "uses the configured database path" do
281
+ missing = File.join(data_dir, "missing.mmdb")
282
+ plugin = described_class.new("source" => "ip", "database" => missing)
283
+ expect { plugin.register }.to raise_error(LogStash::ConfigurationError, /#{Regexp.escape(missing)}/)
284
+ end
285
+
286
+ it "does not keep an invalid download" do
287
+ bad = Tempfile.new("bad-mmdb")
288
+ bad.write("not a database")
289
+ bad.flush
290
+ allow(Down).to receive(:download).and_return(bad)
291
+ database = File.join(data_dir, "ipinfo.mmdb")
292
+ plugin = described_class.new("source" => "ip", "database" => database, "token" => "t")
293
+ expect { plugin.register }.to raise_error(LogStash::ConfigurationError, /not a valid \.mmdb/)
294
+ expect(Dir.children(data_dir)).to be_empty
295
+ end
296
+ end
297
+
298
+ describe "database age warning" do
299
+ let(:data_dir) { Dir.mktmpdir }
300
+ let(:database) { File.join(data_dir, "ipinfo.mmdb").tap { |path| File.write(path, "x") } }
301
+ after { FileUtils.rm_rf(data_dir) }
302
+
303
+ def stub_db_built_days_ago(days)
304
+ metadata = double("metadata", :build_epoch => Time.now.to_i - days * 86400)
305
+ allow(MaxMind::DB).to receive(:new).and_return(double("db", :metadata => metadata))
306
+ end
307
+
308
+ it "warns when the database is older than database_age_warning_days" do
309
+ stub_db_built_days_ago(45)
310
+ plugin = described_class.new("source" => "ip", "database" => database)
311
+ expect(plugin.logger).to receive(:warn).with(/outdated/, hash_including(:age_days => 45))
312
+ plugin.register
313
+ end
314
+
315
+ it "does not warn for a recent database" do
316
+ stub_db_built_days_ago(3)
317
+ plugin = described_class.new("source" => "ip", "database" => database)
318
+ expect(plugin.logger).not_to receive(:warn).with(/outdated/, anything)
319
+ plugin.register
320
+ end
321
+
322
+ it "does not warn when disabled" do
323
+ stub_db_built_days_ago(400)
324
+ plugin = described_class.new("source" => "ip", "database" => database, "database_age_warning_days" => 0)
325
+ expect(plugin.logger).not_to receive(:warn).with(/outdated/, anything)
326
+ plugin.register
327
+ end
328
+ end
329
+
330
+ describe "lookup results" do
331
+ let(:data_dir) { Dir.mktmpdir }
332
+ let(:database) { File.join(data_dir, "ipinfo.mmdb").tap { |path| File.write(path, "x") } }
333
+ let(:db) { double("db", :metadata => double("metadata", :build_epoch => Time.now.to_i)) }
334
+ let(:plugin) { described_class.new("source" => "ip", "database" => database) }
335
+ after { FileUtils.rm_rf(data_dir) }
336
+
337
+ before do
338
+ allow(MaxMind::DB).to receive(:new).and_return(db)
339
+ plugin.register
340
+ end
341
+
342
+ def run(fields)
343
+ LogStash::Event.new(fields).tap { |event| plugin.filter(event) }
344
+ end
345
+
346
+ it "sets the target field when the IP is found" do
347
+ allow(db).to receive(:get).with("8.8.8.8").and_return("country" => "US")
348
+ event = run("ip" => "8.8.8.8")
349
+ expect(event.get("[ipinfo-db][country]")).to eq("US")
350
+ expect(event.get("tags")).to be_nil
351
+ end
352
+
353
+ it "leaves the event untouched when the IP is not in the database" do
354
+ allow(db).to receive(:get).with("10.0.0.1").and_return(nil)
355
+ event = run("ip" => "10.0.0.1")
356
+ expect(event.get("ipinfo-db")).to be_nil
357
+ expect(event.get("tags")).to be_nil
358
+ end
359
+
360
+ it "tags the event without a lookup when the source field is missing" do
361
+ expect(db).not_to receive(:get)
362
+ event = run("other" => "x")
363
+ expect(event.get("tags")).to eq(["_ipinfo_db_lookup_failure"])
364
+ end
365
+
366
+ it "tags the event when the value is not a valid IP" do
367
+ allow(db).to receive(:get).and_raise(IPAddr::InvalidAddressError, "invalid address: nope")
368
+ event = run("ip" => "nope")
369
+ expect(event.get("tags")).to eq(["_ipinfo_db_lookup_failure"])
370
+ end
371
+
372
+ it "logs unexpected errors and tags the event" do
373
+ allow(db).to receive(:get).and_raise(RuntimeError, "boom")
374
+ expect(plugin.logger).to receive(:warn).with(/Unexpected error/, hash_including(:message => "boom"))
375
+ event = run("ip" => "1.2.3.4")
376
+ expect(event.get("tags")).to eq(["_ipinfo_db_lookup_failure"])
377
+ end
378
+ end
379
+ end
@@ -0,0 +1,56 @@
1
+ """Generate spec/fixtures/ipinfo-test.mmdb, a tiny database for offline specs.
2
+
3
+ It uses the field layout of the ipinfo.io free country_asn database, but only documentation
4
+ networks (RFC 5737, RFC 3849) and documentation ASNs (RFC 5398), so its data is synthetic.
5
+
6
+ Regenerate from the repository root with:
7
+
8
+ docker run --rm -v "$PWD/spec/fixtures":/fixtures python:3.12-slim sh -c \
9
+ 'pip install -q mmdb-writer==0.2.7 netaddr==1.3.0 && python /fixtures/generate_ipinfo_test_mmdb.py'
10
+
11
+ Keep this data in sync with spec/filters/ipinfo-db_spec.rb.
12
+ """
13
+ from netaddr import IPSet
14
+ from mmdb_writer import MMDBWriter
15
+
16
+ RECORDS = {
17
+ "192.0.2.0/24": {
18
+ "country": "US",
19
+ "country_name": "United States",
20
+ "continent": "NA",
21
+ "continent_name": "North America",
22
+ "asn": "AS64496",
23
+ "as_name": "Example Networks US",
24
+ "as_domain": "example.com",
25
+ },
26
+ "198.51.100.0/24": {
27
+ "country": "JP",
28
+ "country_name": "Japan",
29
+ "continent": "AS",
30
+ "continent_name": "Asia",
31
+ "asn": "AS64497",
32
+ "as_name": "Example Networks JP",
33
+ "as_domain": "example.jp",
34
+ },
35
+ "2001:db8::/32": {
36
+ "country": "TW",
37
+ "country_name": "Taiwan",
38
+ "continent": "AS",
39
+ "continent_name": "Asia",
40
+ "asn": "AS64498",
41
+ "as_name": "Example Networks TW",
42
+ "as_domain": "example.tw",
43
+ },
44
+ }
45
+
46
+ writer = MMDBWriter(
47
+ ip_version=6,
48
+ ipv4_compatible=True,
49
+ database_type="ipinfo country_asn.mmdb",
50
+ languages=["en"],
51
+ description="Synthetic test database for logstash-filter-ipinfo-db-zuin specs",
52
+ )
53
+ for network, record in RECORDS.items():
54
+ writer.insert_network(IPSet([network]), record)
55
+ writer.to_db_file("/fixtures/ipinfo-test.mmdb")
56
+ print("wrote /fixtures/ipinfo-test.mmdb")
Binary file
@@ -0,0 +1,18 @@
1
+ # Licensed to Elasticsearch B.V. under one or more contributor
2
+ # license agreements. See the NOTICE file distributed with
3
+ # this work for additional information regarding copyright
4
+ # ownership. Elasticsearch B.V. licenses this file to you under
5
+ # the Apache License, Version 2.0 (the "License"); you may
6
+ # not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+
18
+ require "logstash/devutils/rspec/spec_helper"
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-filter-ipinfo-db-zuin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - ZUIN RD
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logstash-core-plugin-api
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: down
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: maxmind-db
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: logstash-devutils
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.6'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.6'
69
+ description: This gem is a Logstash plugin required to be installed on top of the
70
+ Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This
71
+ gem is not a stand-alone program. It looks up the country and ASN of IP addresses
72
+ in the ipinfo.io free country and ASN database.
73
+ email: rd@zuin.ai
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - CHANGELOG.md
79
+ - CONTRIBUTORS
80
+ - Gemfile
81
+ - LICENSE
82
+ - README.md
83
+ - docs/index.asciidoc
84
+ - lib/logstash/filters/ipinfo-db.rb
85
+ - logstash-filter-ipinfo-db-zuin.gemspec
86
+ - spec/filters/ipinfo-db_spec.rb
87
+ - spec/fixtures/generate_ipinfo_test_mmdb.py
88
+ - spec/fixtures/ipinfo-test.mmdb
89
+ - spec/spec_helper.rb
90
+ homepage: https://github.com/reflective-technology/logstash-filter-ipinfo-db
91
+ licenses:
92
+ - Apache-2.0
93
+ metadata:
94
+ logstash_plugin: 'true'
95
+ logstash_group: filter
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.5.22
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Logstash Filter Plugin for IPinfoDb
115
+ test_files:
116
+ - spec/filters/ipinfo-db_spec.rb
117
+ - spec/fixtures/generate_ipinfo_test_mmdb.py
118
+ - spec/fixtures/ipinfo-test.mmdb
119
+ - spec/spec_helper.rb