plunk 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 355fabb9e6a73101c70a9990992abe5974626a76
4
+ data.tar.gz: d61e5388b448da8412899038e8b0e3a72f3a81ac
5
+ SHA512:
6
+ metadata.gz: 94c4b45f0018ac30b45b9922625d7ca25f7c28e8ff7506f831ad70e246179812e55b66b794611dc2f86a22e41ba0f6f7f53087f0bc8e0ddfc7d12ce81f108593
7
+ data.tar.gz: e957db51d157857e99a2b69138d678d0375ad27e98c594c30fff19e355a628d5a16d36f09d5b88b646c3c9af8d4cba687c04ab7f652ff87e827a6743f70d6ade
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ plunk (0.0.0)
5
+ json
6
+ parslet
7
+ rest-client
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ blankslate (2.1.2.4)
13
+ diff-lcs (1.2.5)
14
+ json (1.8.1)
15
+ mime-types (2.0)
16
+ parslet (1.5.0)
17
+ blankslate (~> 2.0)
18
+ rest-client (1.6.7)
19
+ mime-types (>= 1.16)
20
+ rspec (2.14.1)
21
+ rspec-core (~> 2.14.0)
22
+ rspec-expectations (~> 2.14.0)
23
+ rspec-mocks (~> 2.14.0)
24
+ rspec-core (2.14.7)
25
+ rspec-expectations (2.14.4)
26
+ diff-lcs (>= 1.1.3, < 2.0)
27
+ rspec-mocks (2.14.4)
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ plunk!
34
+ rspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Elbii
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ plunk
2
+ =====
3
+
4
+ Human-friendly query language for Elasticsearch
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
data/lib/plunk.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Plunk
2
+
3
+ end
@@ -0,0 +1,121 @@
1
+ require 'json'
2
+ require 'rest-client'
3
+
4
+ # Wrapper for Elasticsearch API
5
+ class Plunk::Elasticsearch
6
+ attr_accessor :host, :port, :fields
7
+
8
+ def initialize(opts={})
9
+ @scheme = opts[:scheme] || "http"
10
+ @host = opts[:host] || "localhost"
11
+ @port = opts[:port] || "9200"
12
+ @size = opts[:size] || "10000"
13
+ @endpoint = "#{@scheme}://#{@host}:#{@port}"
14
+ end
15
+
16
+ # Get list of all field mappings stored in ES
17
+ # TODO: cache response from ES
18
+ def available_fields
19
+ uri = URI.escape "#{@endpoint}/_mapping"
20
+ result = JSON.parse(RestClient.get(uri))
21
+
22
+ @fields = {}
23
+ Plunk::Elasticsearch.nested_values(result, 'properties')
24
+
25
+ @fields
26
+ end
27
+
28
+ def active_record_errors_for(query)
29
+ return " can't be blank." if query.blank?
30
+
31
+ uri = URI.escape "#{@endpoint}/_validate/query?explain=true"
32
+ response = RestClient.post(uri, build_ES_validator(query))
33
+
34
+ json = JSON.parse(response)
35
+
36
+ json['valid'] ? nil : json['explanations'].collect { |exp| exp['error'] }
37
+ end
38
+
39
+ #
40
+ # UTILITY METHODS
41
+ #
42
+ def self.search(query)
43
+ uri = URI.escape "#{@endpoint}/_search?size=#{@size}"
44
+
45
+ RestClient.post uri, build_ES_query(query)
46
+ end
47
+
48
+ # returns all values for all occurences of the given nested key
49
+ def self.nested_values(hash, key)
50
+ hash.each do |k, v|
51
+ if k == key
52
+ @fields.merge! v
53
+ else
54
+ nested_values(v, key) if v.is_a? Hash
55
+ end
56
+ end
57
+ end
58
+
59
+ # nested field matcher
60
+ def self.extract_values(hash, keys)
61
+ @vals ||= []
62
+
63
+ hash.each_pair do |k, v|
64
+ if v.is_a? Hash
65
+ extract_values(v, keys)
66
+ elsif v.is_a? Array
67
+ v.flatten!
68
+ if v.first.is_a? Hash
69
+ v.each { |el| extract_values(el, keys) }
70
+ elsif keys.include? k
71
+ @vals += v
72
+ end
73
+ elsif keys.include? k
74
+ @vals << v
75
+ end
76
+ end
77
+
78
+ return @vals
79
+ end
80
+
81
+ def self.build_ES_validator(query)
82
+ if valid_json? query
83
+ # Strip the top-level "query" paramter since ES doesn't expect it
84
+ JSON.parse(query)['query'].to_json
85
+ else
86
+ <<-END
87
+ {
88
+ "query_string": {
89
+ "query": "#{query}"
90
+ }
91
+ }
92
+ END
93
+ end
94
+ end
95
+
96
+ def self.build_ES_query(query)
97
+ if valid_json? query
98
+ query
99
+ else
100
+ <<-END
101
+ {
102
+ "query": {
103
+ "query_string": {
104
+ "query": "#{query}"
105
+ }
106
+ }
107
+ }
108
+ END
109
+ end
110
+ end
111
+
112
+ def self.valid_json?(json)
113
+ begin
114
+ JSON.parse json
115
+ true
116
+ rescue JSON::ParserError
117
+ false
118
+ end
119
+ end
120
+ end
121
+
@@ -0,0 +1,84 @@
1
+ require 'parslet'
2
+
3
+ class Plunk::Parser < Parslet::Parser
4
+ # Single character rules
5
+ rule(:lparen) { str('(') >> space? }
6
+ rule(:rparen) { str(')') >> space? }
7
+ rule(:comma) { str(',') >> space? }
8
+ rule(:digit) { match('[0-9]') }
9
+ rule(:space) { match('\s').repeat(1) }
10
+ rule(:space?) { space.maybe }
11
+
12
+ # Numbers
13
+ rule(:integer) { str('-').maybe >> digit.repeat(1) >> space? }
14
+ rule(:float) {
15
+ str('-').maybe >> digit.repeat(1) >> str('.') >> digit.repeat(1) >> space?
16
+ }
17
+ rule(:number) { integer | float }
18
+
19
+ # Field / value
20
+ rule(:identifier) { match['_@a-zA-Z.'].repeat(1) }
21
+ rule(:wildcard) { match('[a-zA-Z0-9.*]').repeat(1) }
22
+ rule(:searchop) { match('[=]').as(:op) >> space? }
23
+
24
+ # boolean operators search
25
+ rule(:concatop) { (str('OR') | str('AND')) >> space? }
26
+ rule(:operator) { match('[|]').as(:op) >> space? }
27
+ rule(:timerange) {
28
+ integer.as(:quantity) >> match('s|m|h|d|w').as(:quantifier)
29
+ }
30
+
31
+ # Grammar parts
32
+ rule(:rhs) {
33
+ (regex | wildcard | integer | subsearch |
34
+ (lparen >> (space? >> (wildcard | integer) >>
35
+ (space >> concatop).maybe).repeat(1) >> rparen))
36
+ }
37
+
38
+ rule(:regex) {
39
+ str('/') >> match('[^/]').repeat >> str('/')
40
+ }
41
+
42
+ rule(:search) {
43
+ identifier.as(:field) >> space? >> searchop >> space? >>
44
+ rhs.as(:value) | rhs.as(:match)
45
+ }
46
+
47
+ rule(:last) {
48
+ str("last") >> space >> timerange.as(:timerange) >> space >>
49
+ search.as(:search)
50
+ }
51
+
52
+ rule(:binaryop) {
53
+ (paren | search).as(:left) >> space? >> operator >> job.as(:right)
54
+ }
55
+
56
+ rule(:subsearch) {
57
+ str('`') >> space? >> nested_search >> str('`')
58
+ }
59
+
60
+ rule(:nested_search) {
61
+ match('[^|]').repeat.as(:term) >> str('|') >> space? >>
62
+ match('[^`]').repeat.as(:extractors)
63
+ }
64
+
65
+ rule(:joined_search) {
66
+
67
+ }
68
+
69
+ rule(:paren) {
70
+ lparen >> space? >> job >> space? >> rparen
71
+ }
72
+
73
+ rule(:job) {
74
+ binaryop | paren | last | search
75
+ }
76
+
77
+ # root :job
78
+ rule(:plunk_query) {
79
+ job >> (space >> job).repeat
80
+ }
81
+
82
+ root :plunk_query
83
+ end
84
+
@@ -0,0 +1,38 @@
1
+ require_relative 'elasticsearch'
2
+
3
+ class Plunk::ResultSet
4
+ attr_accessor :query
5
+
6
+ def initialize(opts=nil)
7
+ if opts
8
+ @query = { query: { }}
9
+
10
+ if opts[:query_string]
11
+ @query[:query][:query_string] = { query: opts[:query_string] }
12
+ end
13
+
14
+ if opts[:start_time] and opts[:end_time]
15
+ @query[:query][:range] = {
16
+ '@timestamp' => {
17
+ gte: opts[:start_time],
18
+ lte: opts[:end_time]
19
+ }
20
+ }
21
+ end
22
+ end
23
+ end
24
+
25
+ def *(rs)
26
+ self.join(rs)
27
+ end
28
+
29
+ def join(rs)
30
+ ResultSet.new("[ #{@query} * #{rs.query} ]")
31
+ end
32
+
33
+ def eval
34
+ return unless @query
35
+ Elasticsearch.search(@query.to_json)
36
+ end
37
+ end
38
+
@@ -0,0 +1,64 @@
1
+ require 'parslet'
2
+
3
+ class Plunk::Transformer < Parslet::Transform
4
+
5
+ rule(match: simple(:value)) do
6
+ ResultSet.new(query_string: "#{value}")
7
+ end
8
+
9
+ rule(
10
+ field: simple(:field),
11
+ value: {
12
+ term: simple(:term),
13
+ extractors: simple(:extractors)
14
+ },
15
+ op: '=') do
16
+
17
+ rs = ResultSet.new(query_string: "#{field}:#{term}")
18
+
19
+ json = JSON.parse rs.eval
20
+ values = Elasticsearch.extract_values json, extractors.to_s.split(',')
21
+
22
+ if values.empty?
23
+ ResultSet.new
24
+ else
25
+ ResultSet.new(query_string: "(#{values.uniq.join(' OR ')})")
26
+ end
27
+ end
28
+
29
+ rule(field: simple(:field), value: simple(:value), op: '=') do
30
+ ResultSet.new(query_string: "#{field}:#{value}")
31
+ end
32
+
33
+ rule(
34
+ search: simple(:query),
35
+ timerange: {
36
+ quantity: simple(:quantity),
37
+ quantifier: simple(:quantifier)
38
+ }) do
39
+
40
+ int_quantity = quantity.to_s.to_i
41
+
42
+ start_time =
43
+ case quantifier
44
+ when 's'
45
+ int_quantity.seconds.ago
46
+ when 'm'
47
+ int_quantity.minutes.ago
48
+ when 'h'
49
+ int_quantity.hours.ago
50
+ when 'd'
51
+ int_quantity.days.ago
52
+ when 'w'
53
+ int_quantity.weeks.ago
54
+ end
55
+
56
+ end_time = Time.now.utc.to_datetime
57
+
58
+ ResultSet.new(
59
+ query_string: query,
60
+ start_time: start_time,
61
+ end_time: end_time)
62
+ end
63
+ end
64
+
data/plunk.gemspec ADDED
@@ -0,0 +1,16 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "plunk"
3
+ s.version = "0.0.0"
4
+ s.date = "2013-11-26"
5
+ s.add_runtime_dependency "json"
6
+ s.add_runtime_dependency "parslet"
7
+ s.add_runtime_dependency "rest-client"
8
+ s.add_development_dependency "rspec"
9
+ s.summary = "Elasticsearch query language"
10
+ s.description = "Human-friendly query language for Elasticsearch"
11
+ s.authors = ["Ram Mehta", "Jamil Bou Kheir"]
12
+ s.email = ["jamil@elbii.com", "ram.mehta@gmail.com"]
13
+ s.files = `git ls-files`.split("\n")
14
+ s.homepage = "https://github.com/elbii/plunk"
15
+ s.license = "MIT"
16
+ end
@@ -0,0 +1,15 @@
1
+ require 'plunk'
2
+ require 'plunk/elasticsearch'
3
+
4
+ describe Plunk::Elasticsearch do
5
+ before :all do
6
+ @elasticsearch = Plunk::Elasticsearch.new
7
+ end
8
+
9
+ context 'test field mapping' do
10
+ it 'should successfully list all fields' do
11
+ fields = @elasticsearch.available_fields
12
+ expect(fields).to be_a Hash
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,101 @@
1
+ require 'plunk'
2
+ require 'plunk/parser'
3
+
4
+ # Print ascii_tree when exception occurs
5
+ class Plunk::ParserWrapper < Plunk::Parser
6
+ def parse(query)
7
+ begin
8
+ super(query)
9
+ rescue Parslet::ParseFailed => failure
10
+ puts failure.cause.ascii_tree
11
+ end
12
+ end
13
+ end
14
+
15
+ describe Plunk::Parser do
16
+ before :all do
17
+ @parser = Plunk::ParserWrapper.new
18
+ end
19
+
20
+ context 'single-query searches' do
21
+ it 'should parse a single keyword' do
22
+ @parsed = @parser.parse 'bar'
23
+ expect(@parsed[:match].to_s).to eq 'bar'
24
+ end
25
+
26
+ it 'should parse a single field/value combo' do
27
+ @parsed = @parser.parse 'tshark.http.@src_ip=bar'
28
+ expect(@parsed[:field].to_s).to eq 'tshark.http.@src_ip'
29
+ expect(@parsed[:value].to_s).to eq 'bar'
30
+ expect(@parsed[:op].to_s).to eq '='
31
+ end
32
+
33
+ it 'should parse a single boolean expression' do
34
+ @parsed = @parser.parse '(bar OR car)'
35
+ expect(@parsed[:match].to_s).to eq '(bar OR car)'
36
+ end
37
+
38
+ it 'should parse a single field / value complex boolean expression' do
39
+ @parsed = @parser.parse 'ids.attackers=(bar OR car)'
40
+ expect(@parsed[:field].to_s).to eq 'ids.attackers'
41
+ expect(@parsed[:value].to_s).to eq '(bar OR car)'
42
+ expect(@parsed[:op].to_s).to eq '='
43
+ end
44
+
45
+ it 'should parse a single field / parenthesized value' do
46
+ @parsed = @parser.parse 'ids.attacker=(10.150.44.195)'
47
+ expect(@parsed[:field].to_s).to eq 'ids.attacker'
48
+ expect(@parsed[:value].to_s).to eq '(10.150.44.195)'
49
+ expect(@parsed[:op].to_s).to eq '='
50
+ end
51
+
52
+ it 'should parse a basic regex search' do
53
+ @parsed = @parser.parse 'foo=/blah foo/'
54
+ expect(@parsed[:field].to_s).to eq 'foo'
55
+ expect(@parsed[:value].to_s).to eq '/blah foo/'
56
+ end
57
+
58
+ context 'the last command' do
59
+ it 'should parse last command with a search' do
60
+ @parsed = @parser.parse 'last 24w tshark.@src_ip = bar'
61
+ expect(@parsed[:timerange][:quantity].to_s).to eq '24'
62
+ expect(@parsed[:timerange][:quantifier].to_s).to eq 'w'
63
+ expect(@parsed[:search][:field].to_s).to eq 'tshark.@src_ip'
64
+ expect(@parsed[:search][:value].to_s).to eq 'bar'
65
+ end
66
+ end
67
+
68
+ context 'chained searches' do
69
+ it 'should parse last command with a regex' do
70
+ @parsed = @parser.parse 'last 24w foo=/blah/'
71
+ expect(@parsed[:timerange][:quantity].to_s).to eq '24'
72
+ expect(@parsed[:timerange][:quantifier].to_s).to eq 'w'
73
+ expect(@parsed[:search][:field].to_s).to eq 'foo'
74
+ expect(@parsed[:search][:value].to_s).to eq '/blah/'
75
+ end
76
+ end
77
+
78
+ it 'should parse last command with boolean' do
79
+ @parsed = @parser.parse 'last 1h (foo OR bar)'
80
+ expect(@parsed[:search][:match].to_s).to eq '(foo OR bar)'
81
+ end
82
+
83
+ it 'should parse key/value with regex' do
84
+ @parsed = @parser.parse 'foo=bar fe.ip=/whodunnit/'
85
+ expect(@parsed[0][:field].to_s).to eq 'foo'
86
+ expect(@parsed[0][:value].to_s).to eq 'bar'
87
+ expect(@parsed[1][:field].to_s).to eq 'fe.ip'
88
+ expect(@parsed[1][:value].to_s).to eq '/whodunnit/'
89
+ end
90
+ end
91
+
92
+ context 'nested search' do
93
+ it 'should parse the nested search' do
94
+ @parsed = @parser.parse 'tshark.len = ` 226 | tshark.frame.time_epoch,tshark.ip.src`'
95
+ expect(@parsed[:field].to_s).to eq 'tshark.len'
96
+ expect(@parsed[:op].to_s).to eq '='
97
+ expect(@parsed[:value][:term].to_s).to eq '226 '
98
+ expect(@parsed[:value][:extractors].to_s).to eq 'tshark.frame.time_epoch,tshark.ip.src'
99
+ end
100
+ end
101
+ end
File without changes
@@ -0,0 +1,10 @@
1
+
2
+ # after :each do
3
+ # transformed = @transformer.apply(@parsed)
4
+ # if transformed.is_a? Array
5
+ # transformed.map(&:class).uniq.should =~ Array(ResultSet)
6
+ # else
7
+ # transformed.should be_a ResultSet
8
+ # end
9
+ # end
10
+
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: plunk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ram Mehta
8
+ - Jamil Bou Kheir
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-11-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - '>='
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: parslet
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rest-client
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ description: Human-friendly query language for Elasticsearch
71
+ email:
72
+ - jamil@elbii.com
73
+ - ram.mehta@gmail.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - .gitignore
79
+ - Gemfile
80
+ - Gemfile.lock
81
+ - LICENSE
82
+ - README.md
83
+ - Rakefile
84
+ - lib/plunk.rb
85
+ - lib/plunk/elasticsearch.rb
86
+ - lib/plunk/parser.rb
87
+ - lib/plunk/result_set.rb
88
+ - lib/plunk/transformer.rb
89
+ - plunk.gemspec
90
+ - spec/elasticsearch_spec.rb
91
+ - spec/parser_spec.rb
92
+ - spec/result_set_spec.rb
93
+ - spec/transformer_spec.rb
94
+ homepage: https://github.com/elbii/plunk
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 2.1.11
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: Elasticsearch query language
118
+ test_files: []