Skip to content

Commit fdefdfa

Browse files
Add Resource and Layered Services
- add resource field to Mdm::Vuln and Mdm::Service - add Mdm::ServiceLink join table between child and parent services - add migration files to update the database - specs
1 parent ac92362 commit fdefdfa

14 files changed

+357
-10
lines changed

app/models/mdm/service.rb

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,28 @@ class Mdm::Service < ApplicationRecord
9696
dependent: :destroy,
9797
inverse_of: :service
9898

99+
# @!attribute [rw] parent_links
100+
# Links to parent services of this service.
101+
#
102+
# @return [Array<Mdm::ServiceLink>]
103+
has_many :parent_links,
104+
class_name: 'Mdm::ServiceLink',
105+
foreign_key: 'child_id',
106+
dependent: :destroy,
107+
inverse_of: :child
108+
109+
# @!attribute [rw] parent_links
110+
# Link to child services of this service.
111+
#
112+
# @return [Array<Mdm::ServiceLink>]
113+
has_many :child_links,
114+
class_name: 'Mdm::ServiceLink',
115+
foreign_key: 'parent_id',
116+
dependent: :destroy,
117+
inverse_of: :parent
118+
119+
120+
99121
#
100122
# through: :task_services
101123
#
@@ -128,6 +150,27 @@ class Mdm::Service < ApplicationRecord
128150
# @return [Array<Mdm::WebVuln>]
129151
has_many :web_vulns, :through => :web_sites, :class_name => 'Mdm::WebVuln'
130152

153+
#
154+
# through: :parent_links
155+
#
156+
157+
# @!attribute [rw] parents
158+
# Parent services of this service.
159+
#
160+
# @return [Array<Mdm::Service>]
161+
has_many :parents, through: :parent_links, source: :parent
162+
163+
#
164+
# through: :child_links
165+
#
166+
167+
# @!attribute [rw] children
168+
# Child services of this service.
169+
#
170+
# @return [Array<Mdm::Service>]
171+
has_many :children, through: :child_links, source: :child
172+
173+
131174
#
132175
# Attributes
133176
#
@@ -157,6 +200,11 @@ class Mdm::Service < ApplicationRecord
157200
#
158201
# @return [String] element of {STATES}.
159202

203+
# @!attribute [rw] resource
204+
# Additional resource information about the service, such as a URL or path.
205+
#
206+
# @return [JSONB]
207+
160208
#
161209
# Callbacks
162210
#
@@ -227,7 +275,9 @@ class Mdm::Service < ApplicationRecord
227275
message: 'already exists on this host and protocol',
228276
scope: [
229277
:host_id,
230-
:proto
278+
:proto,
279+
:name,
280+
:resource
231281
]
232282
}
233283
validates :proto,
@@ -263,5 +313,14 @@ def normalize_host_os
263313
end
264314
end
265315

316+
# Destroy this service if it does not have parents {#service_links}
317+
#
318+
# @return [void]
319+
def destroy_if_orphaned
320+
self.class.transaction do
321+
destroy if parents.count == 0
322+
end
323+
end
324+
266325
Metasploit::Concern.run(self)
267326
end

app/models/mdm/service_link.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Join model between {Mdm::Service} and {Mdm::Service} for many-to-many self-referencing relationship
2+
class Mdm::ServiceLink < ApplicationRecord
3+
self.table_name = 'service_links'
4+
5+
#
6+
# Associations
7+
#
8+
9+
# Parent service
10+
belongs_to :parent,
11+
class_name: 'Mdm::Service',
12+
inverse_of: :child_links
13+
14+
# Child service
15+
belongs_to :child,
16+
class_name: 'Mdm::Service',
17+
inverse_of: :parent_links
18+
19+
# Destroy orphaned child when destroying a service link
20+
after_destroy :destroy_orphan_child
21+
22+
#
23+
# Attributes
24+
#
25+
26+
# @!attribute created_at
27+
# When this task service was created.
28+
#
29+
# @return [DateTime]
30+
31+
# @!attribute updated_at
32+
# The last time this task service was updated.
33+
#
34+
# @return [DateTime]
35+
36+
#
37+
# Validations
38+
#
39+
40+
validates :parent_id,
41+
:uniqueness => {
42+
:scope => :child_id
43+
}
44+
45+
def destroy_orphan_child
46+
Mdm::Service.where(id: child.id).first&.destroy_if_orphaned
47+
end
48+
private :destroy_orphan_child
49+
50+
Metasploit::Concern.run(self)
51+
end
52+

app/models/mdm/vuln.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,12 @@ class Mdm::Vuln < ApplicationRecord
160160
#
161161
# @return [Integer]
162162

163+
# @!attribute [rw] resource
164+
# The resource that this vulnerability is associated with.
165+
# It is stored as a JSONB object.
166+
#
167+
# @return [JSONB]
168+
163169
#
164170
# Callbacks
165171
#
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddResourceToMdmVuln < ActiveRecord::Migration[7.0]
2+
def change
3+
add_column :vulns, :resource, :jsonb, null: false, default: {}
4+
end
5+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddResourceToServices < ActiveRecord::Migration[7.0]
2+
def change
3+
add_column :services, :resource, :jsonb, null: false, default: {}
4+
end
5+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class CreateServiceLinks < ActiveRecord::Migration[7.0]
2+
def change
3+
create_table :service_links do |t|
4+
t.references :parent, null: false, foreign_key: { to_table: :services }
5+
t.references :child, null: false, foreign_key: { to_table: :services }
6+
t.timestamps
7+
end
8+
add_index :service_links, [:parent_id, :child_id], unique: true
9+
end
10+
end
11+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class DropServiceUniquenessIndex2 < ActiveRecord::Migration[4.2]
2+
def change
3+
remove_index(:services, :host_id_and_port_and_proto)
4+
end
5+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class RemoveDuplicateServices3 < ActiveRecord::Migration[4.2]
2+
def change
3+
select_mgr = Mdm::Service.arel_table.project(
4+
Mdm::Service[:host_id],
5+
Mdm::Service[:proto],
6+
Mdm::Service[:port].count
7+
).group(
8+
'host_id',
9+
'port',
10+
'proto'
11+
).having(Mdm::Service[:port].count.gt(1))
12+
13+
Mdm::Service.find_by_sql(select_mgr).each(&:destroy)
14+
15+
add_index :services, [:host_id, :port, :proto, :name, :resource], unique: true, name: 'index_services_on_5_columns'
16+
end
17+
end

lib/mdm.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ module Mdm
4444
autoload :WmapRequest
4545
autoload :WmapTarget
4646
autoload :Workspace
47+
autoload :ServiceLink
4748

4849
# Causes the model_name for all Mdm modules to not include the Mdm:: prefix in their name.
4950
#
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
RSpec.describe Mdm::ServiceLink, type: :model do
2+
it_should_behave_like 'Metasploit::Concern.run'
3+
4+
context 'factory' do
5+
it 'should be valid' do
6+
service_link = FactoryBot.build(:mdm_service_link)
7+
expect(service_link).to be_valid
8+
end
9+
end
10+
11+
context 'database' do
12+
13+
context 'timestamps'do
14+
it { is_expected.to have_db_column(:created_at).of_type(:datetime).with_options(:null => false) }
15+
it { is_expected.to have_db_column(:updated_at).of_type(:datetime).with_options(:null => false) }
16+
end
17+
18+
context 'columns' do
19+
it { is_expected.to have_db_column(:parent_id).of_type(:integer).with_options(:null => false) }
20+
it { is_expected.to have_db_column(:child_id).of_type(:integer).with_options(:null => false) }
21+
end
22+
end
23+
24+
context '#destroy' do
25+
it 'should successfully destroy one Mdm::ServiceLink' do
26+
service_link = FactoryBot.create(:mdm_service_link)
27+
expect { service_link.destroy }.to_not raise_error
28+
expect { service_link.reload }.to raise_error(ActiveRecord::RecordNotFound)
29+
end
30+
31+
context 'with one parent and one child' do
32+
let(:parent_service1) { FactoryBot.create(:mdm_service, name: 'parent_service1') }
33+
let(:child_service1) { FactoryBot.create(:mdm_service, name: 'child_service1') }
34+
let!(:service_link1) { FactoryBot.create(:mdm_service_link, parent: parent_service1, child: child_service1) }
35+
36+
it 'should only destroy the child service' do
37+
expect { service_link1.destroy }.to_not raise_error
38+
expect { service_link1.reload }.to raise_error(ActiveRecord::RecordNotFound)
39+
expect { child_service1.reload }.to raise_error(ActiveRecord::RecordNotFound)
40+
expect { parent_service1.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
41+
end
42+
43+
context 'with multiple children' do
44+
let(:child_service2) { FactoryBot.create(:mdm_service, name: 'child_service2') }
45+
let!(:service_link2) { FactoryBot.create(:mdm_service_link, parent: parent_service1, child: child_service2) }
46+
47+
it 'should only destroy the child service related to this service link' do
48+
expect { service_link1.destroy }.to_not raise_error
49+
expect { service_link1.reload }.to raise_error(ActiveRecord::RecordNotFound)
50+
expect { service_link2.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
51+
expect { child_service1.reload }.to raise_error(ActiveRecord::RecordNotFound)
52+
expect { child_service2.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
53+
expect { parent_service1.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
54+
end
55+
end
56+
57+
context 'with multiple nested children' do
58+
let(:child_service2) { FactoryBot.create(:mdm_service, name: 'child_service2') }
59+
let!(:service_link2) { FactoryBot.create(:mdm_service_link, parent: child_service1, child: child_service2) }
60+
61+
it 'should only destroy the nested child services and service links' do
62+
expect { service_link1.destroy }.to_not raise_error
63+
expect { service_link1.reload }.to raise_error(ActiveRecord::RecordNotFound)
64+
expect { service_link2.reload }.to raise_error(ActiveRecord::RecordNotFound)
65+
expect { child_service1.reload }.to raise_error(ActiveRecord::RecordNotFound)
66+
expect { child_service2.reload }.to raise_error(ActiveRecord::RecordNotFound)
67+
expect { parent_service1.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
68+
end
69+
end
70+
71+
context 'with a child that has another parent' do
72+
let(:parent_service2) { FactoryBot.create(:mdm_service, name: 'parent_service2') }
73+
let!(:service_link2) { FactoryBot.create(:mdm_service_link, parent: parent_service2, child: child_service1) }
74+
75+
it 'should not destroy the child' do
76+
expect { service_link1.destroy }.to_not raise_error
77+
expect { service_link1.reload }.to raise_error(ActiveRecord::RecordNotFound)
78+
expect { service_link2.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
79+
expect { child_service1.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
80+
expect { parent_service1.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
81+
expect { parent_service2.reload }.to_not raise_error(ActiveRecord::RecordNotFound)
82+
end
83+
end
84+
end
85+
end
86+
87+
context "Associations" do
88+
it { is_expected.to belong_to(:parent).class_name('Mdm::Service') }
89+
it { is_expected.to belong_to(:child).class_name('Mdm::Service') }
90+
end
91+
92+
context "validations" do
93+
it "should not allow duplicate associations" do
94+
parent_service = FactoryBot.build(:mdm_service)
95+
child_service = FactoryBot.build(:mdm_service)
96+
FactoryBot.create(:mdm_service_link, :parent => parent_service, :child => child_service)
97+
service_link2 = FactoryBot.build(:mdm_service_link, :parent => parent_service, :child => child_service)
98+
expect(service_link2).not_to be_valid
99+
end
100+
end
101+
102+
context 'callbacks' do
103+
context 'before_destroy' do
104+
it 'should call #destroy_orphan_child' do
105+
service_link = FactoryBot.create(:mdm_service_link)
106+
expect(service_link).to receive(:destroy_orphan_child)
107+
service_link.destroy
108+
end
109+
end
110+
end
111+
112+
end

0 commit comments

Comments
 (0)