Skip to content

Commit 64ab72b

Browse files
authored
Merge pull request #71 from cdelafuente-r7/feat/model/search/operation/jsonb
Add search operation model for JSONB
2 parents 9789935 + 925a11f commit 64ab72b

File tree

7 files changed

+167
-3
lines changed

7 files changed

+167
-3
lines changed

.github/workflows/verify.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ jobs:
8080
bundle exec rake yard
8181
8282
- name: Upload coverage report
83-
uses: actions/upload-artifact@v2
83+
uses: actions/upload-artifact@v4
8484
with:
85-
name: coverage-${{ matrix.ruby }}
85+
name: coverage-${{ matrix.os }}-${{ matrix.ruby }}
8686
path: |
8787
coverage/
8888
retention-days: 1

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ end
99

1010
# used by dummy application
1111
group :development, :test do
12+
# Temporary, remove once the Rails 7.1 update is complete
13+
# see: https://stackoverflow.com/questions/79360526/uninitialized-constant-activesupportloggerthreadsafelevellogger-nameerror
14+
gem 'concurrent-ruby', '1.3.4'
1215
# supplies factories for producing model instance for specs
1316
# Version 4.1.0 or newer is needed to support generate calls without the 'FactoryGirl.' in factory definitions syntax.
1417
gem 'factory_bot'
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Search operation with {Metasploit::Model::Search::Operation::Base#operator} with `#type` ':jsonb'.
2+
class Metasploit::Model::Search::Operation::Jsonb < Metasploit::Model::Search::Operation::Base
3+
#
4+
# Validations
5+
#
6+
7+
validates :value,
8+
:presence => true
9+
10+
#
11+
# Methods
12+
#
13+
14+
# Sets {Metasploit::Model::Search::Operation::Base#value} by parsing the `formatted_value`
15+
# String and attempting to generate a valid JSON if it contains a colon.
16+
# Otherwise, it keeps the same value as a String.
17+
#
18+
# @param formatted_value [#to_s]
19+
# @return [String] representing a JSON if `formatted_value` contains a colon.
20+
# Otherwise it is a the same as `formatted_value`
21+
def value=(formatted_value)
22+
@value = transform_value(formatted_value.to_s)
23+
end
24+
25+
26+
private
27+
28+
# Transform an input String to a JSON if it contains a colon. It returns the String if not.
29+
# Also, the first colon is used as a delimiter between the key and the value.
30+
# Any subsequent colon will be part of the value.
31+
# Finally, it handles double/single quotes to escape any colon that are not
32+
# suppose to be a delimiter between the key and the value.
33+
#
34+
# @param input [#to_s]
35+
# @return [String] representing a JSON if `input` contains a colon. Otherwise it is a the same as `input`
36+
def transform_value(input)
37+
# Regex to find the first colon that is NOT inside quotes
38+
match = input.match(/((?:[^'":]++|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')*?):(.*)/)
39+
40+
# If no valid colon is found, return the original string
41+
return input unless match
42+
43+
key = match[1].strip # Extract key (before first valid colon)
44+
value = match[2].strip # Extract value (after first valid colon)
45+
46+
# Remove starting and ending quotes and ensure they are valid JSON strings
47+
key = key.gsub(/^["'](.*)["']$/, '\1').to_json
48+
value = value.gsub(/^["'](.*)["']$/, '\1').to_json
49+
"{#{key}: #{value}}"
50+
end
51+
52+
end

app/models/metasploit/model/search/operator/attribute.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class Metasploit::Model::Search::Operator::Attribute < Metasploit::Model::Search
1616
{
1717
set: :string
1818
},
19-
:string
19+
:string,
20+
:jsonb
2021
]
2122

2223
#

lib/metasploit/model/search/operation.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module Metasploit::Model::Search::Operation
1313
autoload :Set
1414
autoload :String
1515
autoload :Value
16+
autoload :Jsonb
1617

1718
# @param options [Hash{Symbol => Object}]
1819
# @option options [Metasploit::Module::Search::Query] :query The query that the parsed operation is a part.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
RSpec.describe Metasploit::Model::Search::Operation::Jsonb, type: :model do
2+
context 'validation' do
3+
context 'value' do
4+
before(:example) do
5+
operation.valid?
6+
end
7+
8+
let(:errors) do
9+
operation.errors[:value]
10+
end
11+
12+
let(:operation) do
13+
described_class.new(:value => value)
14+
end
15+
16+
context 'with String' do
17+
let(:value) do
18+
'search_string'
19+
end
20+
21+
it 'should not record error' do
22+
expect(errors).to be_empty
23+
end
24+
end
25+
26+
context 'with Integer' do
27+
let(:value) do
28+
3
29+
end
30+
31+
it 'should not record error' do
32+
expect(errors).to be_empty
33+
end
34+
end
35+
36+
context 'with a Symbol' do
37+
let(:value) do
38+
:mysym
39+
end
40+
41+
it 'should not record error' do
42+
expect(errors).to be_empty
43+
end
44+
end
45+
end
46+
end
47+
48+
context '#value' do
49+
subject(:value) do
50+
operation.value
51+
end
52+
53+
let(:operation) do
54+
described_class.new(:value => formatted_value)
55+
end
56+
57+
context 'with String' do
58+
let(:formatted_value) do
59+
'test value'
60+
end
61+
62+
it 'should be passed as a String' do
63+
expect(value).to eq(formatted_value.to_s)
64+
end
65+
end
66+
67+
context 'with Integer' do
68+
let(:formatted_value) do
69+
3
70+
end
71+
72+
it 'should be passed as a String' do
73+
expect(value).to eq(formatted_value.to_s)
74+
end
75+
end
76+
77+
context 'with String containing colon characters' do
78+
{
79+
'key:value' => '{"key": "value"}',
80+
'key:value:extra' => '{"key": "value:extra"}',
81+
'"quoted:part":value' => '{"quoted:part": "value"}',
82+
'"quoted:part":value:extra' => '{"quoted:part": "value:extra"}',
83+
'a:b:c:d' => '{"a": "b:c:d"}',
84+
'"x:y:z":a:b' => '{"x:y:z": "a:b"}',
85+
"'single:quote':value" => '{"single:quote": "value"}',
86+
"'single:quote':value:extra" => '{"single:quote": "value:extra"}',
87+
"'a:b':c:d" => '{"a:b": "c:d"}',
88+
'"x:y"and"z:w":final' => '{"x:y\"and\"z:w": "final"}',
89+
"'x:y'and'z:w':final" => '{"x:y\'and\'z:w": "final"}',
90+
'"a:b":c:"d:e"' => '{"a:b": "c:\"d:e\""}',
91+
"'a:b':c:'d:e'" => '{"a:b": "c:\'d:e\'"}',
92+
}.each do |input, expected|
93+
94+
context "with the string #{input}" do
95+
let(:formatted_value) { input }
96+
97+
it 'should be passed as a valid JSON string' do
98+
expect(value).to eq(expected)
99+
end
100+
end
101+
102+
end
103+
104+
end
105+
end
106+
end

spec/app/models/metasploit/model/search/operator/attribute_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
}
2626
it { is_expected.to include(:integer) }
2727
it { is_expected.to include(:string) }
28+
it { is_expected.to include(:jsonb) }
2829
end
2930
end
3031

0 commit comments

Comments
 (0)