Skip to content
Merged
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
61 changes: 60 additions & 1 deletion app/models/mdm/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,28 @@ class Mdm::Service < ApplicationRecord
dependent: :destroy,
inverse_of: :service

# @!attribute [rw] parent_links
# Links to parent services of this service.
#
# @return [Array<Mdm::ServiceLink>]
has_many :parent_links,
class_name: 'Mdm::ServiceLink',
foreign_key: 'child_id',
dependent: :destroy,
inverse_of: :child

# @!attribute [rw] parent_links
# Link to child services of this service.
#
# @return [Array<Mdm::ServiceLink>]
has_many :child_links,
class_name: 'Mdm::ServiceLink',
foreign_key: 'parent_id',
dependent: :destroy,
inverse_of: :parent



#
# through: :task_services
#
Expand Down Expand Up @@ -128,6 +150,27 @@ class Mdm::Service < ApplicationRecord
# @return [Array<Mdm::WebVuln>]
has_many :web_vulns, :through => :web_sites, :class_name => 'Mdm::WebVuln'

#
# through: :parent_links
#

# @!attribute [rw] parents
# Parent services of this service.
#
# @return [Array<Mdm::Service>]
has_many :parents, through: :parent_links, source: :parent

#
# through: :child_links
#

# @!attribute [rw] children
# Child services of this service.
#
# @return [Array<Mdm::Service>]
has_many :children, through: :child_links, source: :child


#
# Attributes
#
Expand Down Expand Up @@ -157,6 +200,11 @@ class Mdm::Service < ApplicationRecord
#
# @return [String] element of {STATES}.

# @!attribute [rw] resource
# Additional resource information about the service, such as a URL or path.
#
# @return [JSONB]

#
# Callbacks
#
Expand Down Expand Up @@ -227,7 +275,9 @@ class Mdm::Service < ApplicationRecord
message: 'already exists on this host and protocol',
scope: [
:host_id,
:proto
:proto,
:name,
:resource
]
}
validates :proto,
Expand Down Expand Up @@ -263,5 +313,14 @@ def normalize_host_os
end
end

# Destroy this service if it does not have parents {#service_links}
#
# @return [void]
def destroy_if_orphaned
self.class.transaction do
destroy if parents.count == 0
end
end

Metasploit::Concern.run(self)
end
52 changes: 52 additions & 0 deletions app/models/mdm/service_link.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Join model between {Mdm::Service} and {Mdm::Service} for many-to-many self-referencing relationship
class Mdm::ServiceLink < ApplicationRecord
self.table_name = 'service_links'

#
# Associations
#

# Parent service
belongs_to :parent,
class_name: 'Mdm::Service',
inverse_of: :child_links

# Child service
belongs_to :child,
class_name: 'Mdm::Service',
inverse_of: :parent_links

# Destroy orphaned child when destroying a service link
after_destroy :destroy_orphan_child

#
# Attributes
#

# @!attribute created_at
# When this task service was created.
#
# @return [DateTime]

# @!attribute updated_at
# The last time this task service was updated.
#
# @return [DateTime]

#
# Validations
#

validates :parent_id,
:uniqueness => {
:scope => :child_id
}

def destroy_orphan_child
Mdm::Service.where(id: child.id).first&.destroy_if_orphaned
end
private :destroy_orphan_child

Metasploit::Concern.run(self)
end

6 changes: 6 additions & 0 deletions app/models/mdm/vuln.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ class Mdm::Vuln < ApplicationRecord
#
# @return [Integer]

# @!attribute [rw] resource
# The resource that this vulnerability is associated with.
# It is stored as a JSONB object.
#
# @return [JSONB]

#
# Callbacks
#
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20250716155919_add_resource_to_mdm_vuln.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddResourceToMdmVuln < ActiveRecord::Migration[7.0]
def change
add_column :vulns, :resource, :jsonb, null: false, default: {}
end
end
5 changes: 5 additions & 0 deletions db/migrate/20250717170556_add_resource_to_services.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddResourceToServices < ActiveRecord::Migration[7.0]
def change
add_column :services, :resource, :jsonb, null: false, default: {}
end
end
11 changes: 11 additions & 0 deletions db/migrate/20250718122714_create_service_links.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateServiceLinks < ActiveRecord::Migration[7.0]
def change
create_table :service_links do |t|
t.references :parent, null: false, foreign_key: { to_table: :services }
t.references :child, null: false, foreign_key: { to_table: :services }
t.timestamps
end
add_index :service_links, [:parent_id, :child_id], unique: true
end
end

5 changes: 5 additions & 0 deletions db/migrate/20250720082201_drop_service_uniqueness_index2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class DropServiceUniquenessIndex2 < ActiveRecord::Migration[7.0]
def change
remove_index(:services, :host_id_and_port_and_proto)
end
end
17 changes: 17 additions & 0 deletions db/migrate/20250721114306_remove_duplicate_services3.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class RemoveDuplicateServices3 < ActiveRecord::Migration[7.0]
def change
select_mgr = Mdm::Service.arel_table.project(
Mdm::Service[:host_id],
Mdm::Service[:proto],
Mdm::Service[:port].count
).group(
'host_id',
'port',
'proto'
).having(Mdm::Service[:port].count.gt(1))

Mdm::Service.find_by_sql(select_mgr).each(&:destroy)

add_index :services, [:host_id, :port, :proto, :name, :resource], unique: true, name: 'index_services_on_5_columns'
end
end
1 change: 1 addition & 0 deletions lib/mdm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ module Mdm
autoload :WmapRequest
autoload :WmapTarget
autoload :Workspace
autoload :ServiceLink

# Causes the model_name for all Mdm modules to not include the Mdm:: prefix in their name.
#
Expand Down
112 changes: 112 additions & 0 deletions spec/app/models/mdm/service_link_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
RSpec.describe Mdm::ServiceLink, type: :model do
it_should_behave_like 'Metasploit::Concern.run'

context 'factory' do
it 'should be valid' do
service_link = FactoryBot.build(:mdm_service_link)
expect(service_link).to be_valid
end
end

context 'database' do

context 'timestamps'do
it { is_expected.to have_db_column(:created_at).of_type(:datetime).with_options(:null => false) }
it { is_expected.to have_db_column(:updated_at).of_type(:datetime).with_options(:null => false) }
end

context 'columns' do
it { is_expected.to have_db_column(:parent_id).of_type(:integer).with_options(:null => false) }
it { is_expected.to have_db_column(:child_id).of_type(:integer).with_options(:null => false) }
end
end

context '#destroy' do
it 'should successfully destroy one Mdm::ServiceLink' do
service_link = FactoryBot.create(:mdm_service_link)
expect { service_link.destroy }.to_not raise_error
expect { service_link.reload }.to raise_error(ActiveRecord::RecordNotFound)
end

context 'with one parent and one child' do
let(:parent_service1) { FactoryBot.create(:mdm_service, name: 'parent_service1') }
let(:child_service1) { FactoryBot.create(:mdm_service, name: 'child_service1') }
let!(:service_link1) { FactoryBot.create(:mdm_service_link, parent: parent_service1, child: child_service1) }

it 'should only destroy the child service' do
expect { service_link1.destroy }.to_not raise_error
expect { service_link1.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { child_service1.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { parent_service1.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
end

context 'with multiple children' do
let(:child_service2) { FactoryBot.create(:mdm_service, name: 'child_service2') }
let!(:service_link2) { FactoryBot.create(:mdm_service_link, parent: parent_service1, child: child_service2) }

it 'should only destroy the child service related to this service link' do
expect { service_link1.destroy }.to_not raise_error
expect { service_link1.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { service_link2.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
expect { child_service1.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { child_service2.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
expect { parent_service1.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
end
end

context 'with multiple nested children' do
let(:child_service2) { FactoryBot.create(:mdm_service, name: 'child_service2') }
let!(:service_link2) { FactoryBot.create(:mdm_service_link, parent: child_service1, child: child_service2) }

it 'should only destroy the nested child services and service links' do
expect { service_link1.destroy }.to_not raise_error
expect { service_link1.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { service_link2.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { child_service1.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { child_service2.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { parent_service1.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
end
end

context 'with a child that has another parent' do
let(:parent_service2) { FactoryBot.create(:mdm_service, name: 'parent_service2') }
let!(:service_link2) { FactoryBot.create(:mdm_service_link, parent: parent_service2, child: child_service1) }

it 'should not destroy the child' do
expect { service_link1.destroy }.to_not raise_error
expect { service_link1.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect { service_link2.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
expect { child_service1.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
expect { parent_service1.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
expect { parent_service2.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
end
end
end
end

context "Associations" do
it { is_expected.to belong_to(:parent).class_name('Mdm::Service') }
it { is_expected.to belong_to(:child).class_name('Mdm::Service') }
end

context "validations" do
it "should not allow duplicate associations" do
parent_service = FactoryBot.build(:mdm_service)
child_service = FactoryBot.build(:mdm_service)
FactoryBot.create(:mdm_service_link, :parent => parent_service, :child => child_service)
service_link2 = FactoryBot.build(:mdm_service_link, :parent => parent_service, :child => child_service)
expect(service_link2).not_to be_valid
end
end

context 'callbacks' do
context 'before_destroy' do
it 'should call #destroy_orphan_child' do
service_link = FactoryBot.create(:mdm_service_link)
expect(service_link).to receive(:destroy_orphan_child)
service_link.destroy
end
end
end

end
Loading