Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search operation model for JSONB #71

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ jobs:
bundle exec rake yard

- name: Upload coverage report
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.ruby }}
name: coverage-${{ matrix.os }}-${{ matrix.ruby }}
path: |
coverage/
retention-days: 1
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ end

# used by dummy application
group :development, :test do
# Temporary, remove once the Rails 7.1 update is complete
# see: https://stackoverflow.com/questions/79360526/uninitialized-constant-activesupportloggerthreadsafelevellogger-nameerror
gem 'concurrent-ruby', '1.3.4'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# supplies factories for producing model instance for specs
# Version 4.1.0 or newer is needed to support generate calls without the 'FactoryGirl.' in factory definitions syntax.
gem 'factory_bot'
Expand Down
52 changes: 52 additions & 0 deletions app/models/metasploit/model/search/operation/jsonb.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Search operation with {Metasploit::Model::Search::Operation::Base#operator} with `#type` ':jsonb'.
class Metasploit::Model::Search::Operation::Jsonb < Metasploit::Model::Search::Operation::Base
#
# Validations
#

validates :value,
:presence => true

#
# Methods
#

# Sets {Metasploit::Model::Search::Operation::Base#value} by parsing the `formatted_value`
# String and attempting to generate a valid JSON if it contains a colon.
# Otherwise, it keeps the same value as a String.
#
# @param formatted_value [#to_s]
# @return [String] representing a JSON if `formatted_value` contains a colon.
# Otherwise it is a the same as `formatted_value`
def value=(formatted_value)
@value = transform_value(formatted_value.to_s)
end


private

# Transform an input String to a JSON if it contains a colon. It returns the String if not.
# Also, the first colon is used as a delimiter between the key and the value.
# Any subsequent colon will be part of the value.
# Finally, it handles double/single quotes to escape any colon that are not
# suppose to be a delimiter between the key and the value.
#
# @param input [#to_s]
# @return [String] representing a JSON if `input` contains a colon. Otherwise it is a the same as `input`
def transform_value(input)
# Regex to find the first colon that is NOT inside quotes
match = input.match(/((?:[^'":]++|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')*?):(.*)/)

# If no valid colon is found, return the original string
return input unless match

key = match[1].strip # Extract key (before first valid colon)
value = match[2].strip # Extract value (after first valid colon)

# Remove starting and ending quotes and ensure they are valid JSON strings
key = key.gsub(/^["'](.*)["']$/, '\1').to_json
value = value.gsub(/^["'](.*)["']$/, '\1').to_json
"{#{key}: #{value}}"
end

end
3 changes: 2 additions & 1 deletion app/models/metasploit/model/search/operator/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ class Metasploit::Model::Search::Operator::Attribute < Metasploit::Model::Search
{
set: :string
},
:string
:string,
:jsonb
]

#
Expand Down
1 change: 1 addition & 0 deletions lib/metasploit/model/search/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module Metasploit::Model::Search::Operation
autoload :Set
autoload :String
autoload :Value
autoload :Jsonb

# @param options [Hash{Symbol => Object}]
# @option options [Metasploit::Module::Search::Query] :query The query that the parsed operation is a part.
Expand Down
106 changes: 106 additions & 0 deletions spec/app/models/metasploit/model/search/operation/jsonb_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
RSpec.describe Metasploit::Model::Search::Operation::Jsonb, type: :model do
context 'validation' do
context 'value' do
before(:example) do
operation.valid?
end

let(:errors) do
operation.errors[:value]
end

let(:operation) do
described_class.new(:value => value)
end

context 'with String' do
let(:value) do
'search_string'
end

it 'should not record error' do
expect(errors).to be_empty
end
end

context 'with Integer' do
let(:value) do
3
end

it 'should not record error' do
expect(errors).to be_empty
end
end

context 'with a Symbol' do
let(:value) do
:mysym
end

it 'should not record error' do
expect(errors).to be_empty
end
end
end
end

context '#value' do
subject(:value) do
operation.value
end

let(:operation) do
described_class.new(:value => formatted_value)
end

context 'with String' do
let(:formatted_value) do
'test value'
end

it 'should be passed as a String' do
expect(value).to eq(formatted_value.to_s)
end
end

context 'with Integer' do
let(:formatted_value) do
3
end

it 'should be passed as a String' do
expect(value).to eq(formatted_value.to_s)
end
end

context 'with String containing colon characters' do
{
'key:value' => '{"key": "value"}',
'key:value:extra' => '{"key": "value:extra"}',
'"quoted:part":value' => '{"quoted:part": "value"}',
'"quoted:part":value:extra' => '{"quoted:part": "value:extra"}',
'a:b:c:d' => '{"a": "b:c:d"}',
'"x:y:z":a:b' => '{"x:y:z": "a:b"}',
"'single:quote':value" => '{"single:quote": "value"}',
"'single:quote':value:extra" => '{"single:quote": "value:extra"}',
"'a:b':c:d" => '{"a:b": "c:d"}',
'"x:y"and"z:w":final' => '{"x:y\"and\"z:w": "final"}',
"'x:y'and'z:w':final" => '{"x:y\'and\'z:w": "final"}',
'"a:b":c:"d:e"' => '{"a:b": "c:\"d:e\""}',
"'a:b':c:'d:e'" => '{"a:b": "c:\'d:e\'"}',
}.each do |input, expected|

context "with the string #{input}" do
let(:formatted_value) { input }

it 'should be passed as a valid JSON string' do
expect(value).to eq(expected)
end
end

end

end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
}
it { is_expected.to include(:integer) }
it { is_expected.to include(:string) }
it { is_expected.to include(:jsonb) }
end
end

Expand Down
Loading