metasploit-erd 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +26 -0
- data/.rspec +3 -0
- data/.simplecov +42 -0
- data/.travis.yml +7 -0
- data/.yardopts +1 -0
- data/Gemfile +26 -0
- data/LICENSE +28 -0
- data/README.md +44 -0
- data/Rakefile +9 -0
- data/lib/metasploit/erd.rb +35 -0
- data/lib/metasploit/erd/cluster.rb +49 -0
- data/lib/metasploit/erd/clusterable.rb +39 -0
- data/lib/metasploit/erd/diagram.rb +89 -0
- data/lib/metasploit/erd/entity.rb +11 -0
- data/lib/metasploit/erd/entity/class.rb +69 -0
- data/lib/metasploit/erd/entity/namespace.rb +66 -0
- data/lib/metasploit/erd/relationship.rb +57 -0
- data/lib/metasploit/erd/version.rb +31 -0
- data/lib/tasks/yard.rake +32 -0
- data/metasploit-erd.gemspec +33 -0
- data/spec/metasploit/erd/cluster_spec.rb +158 -0
- data/spec/metasploit/erd/diagram_spec.rb +238 -0
- data/spec/metasploit/erd/entity/class_spec.rb +236 -0
- data/spec/metasploit/erd/entity/namespace_spec.rb +130 -0
- data/spec/metasploit/erd/relationship_spec.rb +246 -0
- data/spec/metasploit/erd/version_spec.rb +125 -0
- data/spec/metasploit/erd_spec.rb +15 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/shared/contexts/active_record_base_connection.rb +12 -0
- data/spec/support/shared/contexts/active_record_base_descendants_cleaner.rb +11 -0
- data/spec/support/shared/examples/metasploit/erd/clusterable.rb +284 -0
- metadata +187 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
# Entity for Entity-Relationship Diagram that wraps a `Class<ActiveRecord::Base>` to assist with finding its
|
2
|
+
# {#class_set directly related classes}
|
3
|
+
class Metasploit::ERD::Entity::Class
|
4
|
+
include Metasploit::ERD::Clusterable
|
5
|
+
|
6
|
+
#
|
7
|
+
# Attributes
|
8
|
+
#
|
9
|
+
|
10
|
+
attr_reader :klass
|
11
|
+
|
12
|
+
# @!attribute [r] klass
|
13
|
+
# The class whose `belongs_to` associations should be followed to generate
|
14
|
+
# {#class_set set of Classes on which it depends}.
|
15
|
+
#
|
16
|
+
# @return [Class<ActiveRecord::Base>]
|
17
|
+
|
18
|
+
#
|
19
|
+
# Instance Methods
|
20
|
+
#
|
21
|
+
|
22
|
+
# @param klass [Class<ActiveRecord::Base>]
|
23
|
+
def initialize(klass)
|
24
|
+
@klass = klass
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns all classes to which the {#klass} has a `belongs_to` association. Only `belongs_to` associations are traced
|
28
|
+
# because they have foreign keys and without the belongs_to associations the foreign keys would have no primary keys
|
29
|
+
# to which to point.
|
30
|
+
#
|
31
|
+
# @param source [Class<ActiveRecord::Base>] an `ActiveRecord::Base` subclass.
|
32
|
+
# @return [Set<Class<ActiveRecord::Base>>]
|
33
|
+
def class_set
|
34
|
+
reflections = klass.reflect_on_all_associations(:belongs_to)
|
35
|
+
|
36
|
+
reflections.each_with_object(Set.new) { |reflection, set|
|
37
|
+
relationship = Metasploit::ERD::Relationship.new(reflection)
|
38
|
+
set.merge(relationship.class_set)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
# Cluster seeded with {#klass}.
|
43
|
+
#
|
44
|
+
# @return [Metasploit::ERD::Cluster]
|
45
|
+
def cluster
|
46
|
+
Metasploit::ERD::Cluster.new(klass)
|
47
|
+
end
|
48
|
+
|
49
|
+
# (see Metasploit::ERD::Clusterable#diagram)
|
50
|
+
#
|
51
|
+
# @example Generate ERD for a Class in directory
|
52
|
+
# klass = Klass
|
53
|
+
# entity = Metasploit::ERD::Entity::Class.new(klass)
|
54
|
+
# # will add default .png extension
|
55
|
+
# diagram = entity.diagram(directory: directory)
|
56
|
+
# diagram.create
|
57
|
+
#
|
58
|
+
# @option options [String] :basename ("<klass.name.underscore>.erd") The basename to use for the `:filename`
|
59
|
+
# option.
|
60
|
+
# @option options [String] :title ("<klass.name> Namespace Entity-Relationship Diagram") Title for diagram.
|
61
|
+
def diagram(options={})
|
62
|
+
super_options = {
|
63
|
+
basename: "#{klass.name.underscore}.erd",
|
64
|
+
title: "#{klass} Entity-Relationship Diagram"
|
65
|
+
}.merge(options)
|
66
|
+
|
67
|
+
super(super_options)
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# Entity for a namespace with a given {#namespace_name name}.
|
2
|
+
class Metasploit::ERD::Entity::Namespace
|
3
|
+
include Metasploit::ERD::Clusterable
|
4
|
+
|
5
|
+
#
|
6
|
+
# Attributes
|
7
|
+
#
|
8
|
+
|
9
|
+
attr_reader :namespace_name
|
10
|
+
|
11
|
+
# @!attribute [r] namespace_name
|
12
|
+
# The `Module#name` of a namespace module for a collection of `Class<ActiveRecord::Base>`
|
13
|
+
#
|
14
|
+
# @return [String]
|
15
|
+
|
16
|
+
#
|
17
|
+
# Instance Methods
|
18
|
+
#
|
19
|
+
|
20
|
+
# @param namespace_name [String]
|
21
|
+
def initialize(namespace_name)
|
22
|
+
@namespace_name = namespace_name
|
23
|
+
end
|
24
|
+
|
25
|
+
# @note Caller must load all `ActiveRecord::Base` descendants that should be in the search domain.
|
26
|
+
#
|
27
|
+
# The entities in the namespace with `namespace_name`.
|
28
|
+
#
|
29
|
+
# @param namespace_name [String] The `Module#name` of the `Class` or `Module` that is the namespace for a collection
|
30
|
+
# of `ActiveRecord::Base` descendants.
|
31
|
+
# @return [Array<Class<ActiveRecord::Base>
|
32
|
+
def classes
|
33
|
+
ActiveRecord::Base.descendants.select { |klass|
|
34
|
+
klass.parents.any? { |parent|
|
35
|
+
parent.name == namespace_name
|
36
|
+
}
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Cluster seeded with all {#classes} in this namespace.
|
41
|
+
#
|
42
|
+
# @return [Metasploit::ERD::Cluster]
|
43
|
+
def cluster
|
44
|
+
Metasploit::ERD::Cluster.new(*classes)
|
45
|
+
end
|
46
|
+
|
47
|
+
# (see Metasploit::ERD::Clusterable#diagram)
|
48
|
+
#
|
49
|
+
# @example Generate ERD for namespace in directory
|
50
|
+
# entity = Metasploit::ERD::Entity::Namespace.new('Nested::Namespace')
|
51
|
+
# # will add default .png extension
|
52
|
+
# diagram = entity.diagram(directory: directory)
|
53
|
+
# diagram.create
|
54
|
+
#
|
55
|
+
# @option options [String] :basename ("<namespace_name.underscore>.erd") The basename to use for the `:filename`
|
56
|
+
# option.
|
57
|
+
# @option options [String] :title ("<namespace_name> Namespace Entity-Relationship Diagram") Title for diagram.
|
58
|
+
def diagram(options={})
|
59
|
+
super_options = {
|
60
|
+
basename: "#{namespace_name.underscore}.erd",
|
61
|
+
title: "#{namespace_name} Namespace Entity-Relationship Diagram"
|
62
|
+
}.merge(options)
|
63
|
+
|
64
|
+
super(super_options)
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# The relationship in an Entity-Relationship Diagram. Modelled using an {#association} extracted from a
|
2
|
+
# `Class<ActiveRecord::Base>` using reflection.
|
3
|
+
class Metasploit::ERD::Relationship
|
4
|
+
#
|
5
|
+
# Attributes
|
6
|
+
#
|
7
|
+
|
8
|
+
attr_reader :association
|
9
|
+
|
10
|
+
# @!attribute [r] association
|
11
|
+
# A `belongs_to` association.
|
12
|
+
#
|
13
|
+
# @return [ActiveRecord::Associations::BelongsToAssociation]
|
14
|
+
|
15
|
+
#
|
16
|
+
# Instance Methods
|
17
|
+
#
|
18
|
+
|
19
|
+
# @param association [ActiveRecord::Associations::BelongsToAssociation]
|
20
|
+
def initialize(association)
|
21
|
+
@association = association
|
22
|
+
end
|
23
|
+
|
24
|
+
# Set of classes pointed to by this association. Differs from `association.klass` as {#class_set} properly handles
|
25
|
+
# polymorphic associations by finding all `Class<ActiveRecord::Base>` that `has_many <inverse>, as: <name>` and so
|
26
|
+
# can fulfill `belongs_to <name>, polymorpic: true`.
|
27
|
+
#
|
28
|
+
# @return [Set<Class<ActiveRecord::Base>>]
|
29
|
+
def class_set
|
30
|
+
if association.options[:polymorphic]
|
31
|
+
polymorphic_class_set
|
32
|
+
else
|
33
|
+
Set.new([association.klass])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Finds the target classes for `belongs_to <name>, polymorphic: true`.
|
40
|
+
#
|
41
|
+
# @return [Set<Class<ActiveRecord::Base>>]
|
42
|
+
def polymorphic_class_set
|
43
|
+
name = association.name
|
44
|
+
|
45
|
+
ActiveRecord::Base.descendants.each_with_object(Set.new) { |descendant, class_set|
|
46
|
+
has_many_reflections = descendant.reflect_on_all_associations(:has_many)
|
47
|
+
|
48
|
+
has_many_reflections.each do |has_many_reflection|
|
49
|
+
as = has_many_reflection.options[:as]
|
50
|
+
|
51
|
+
if as == name
|
52
|
+
class_set.add descendant
|
53
|
+
end
|
54
|
+
end
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Metasploit
|
2
|
+
module ERD
|
3
|
+
# Holds components of {VERSION} as defined by {http://semver.org/spec/v2.0.0.html semantic versioning v2.0.0}.
|
4
|
+
module Version
|
5
|
+
# The major version number.
|
6
|
+
MAJOR = 0
|
7
|
+
# The minor version number, scoped to the {MAJOR} version number.
|
8
|
+
MINOR = 0
|
9
|
+
# The patch number, scoped to the {MINOR} version number.
|
10
|
+
PATCH = 1
|
11
|
+
|
12
|
+
# The full version string, including the {MAJOR}, {MINOR}, {PATCH}, and optionally, the `PRERELEASE` in the
|
13
|
+
# {http://semver.org/spec/v2.0.0.html semantic versioning v2.0.0} format.
|
14
|
+
#
|
15
|
+
# @return [String] '{MAJOR}.{MINOR}.{PATCH}' on master. '{MAJOR}.{MINOR}.{PATCH}-`<PRERELEASE>`' on any branch
|
16
|
+
# other than master.
|
17
|
+
def self.full
|
18
|
+
version = "#{MAJOR}.#{MINOR}.#{PATCH}"
|
19
|
+
|
20
|
+
if defined? PRERELEASE
|
21
|
+
version = "#{version}-#{PRERELEASE}"
|
22
|
+
end
|
23
|
+
|
24
|
+
version
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# @see Version.full
|
29
|
+
VERSION = Version.full
|
30
|
+
end
|
31
|
+
end
|
data/lib/tasks/yard.rake
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# @note All options not specific to any given rake task should go in the .yardopts file so they are available to both
|
2
|
+
# the below rake tasks and when invoking `yard` from the command line
|
3
|
+
|
4
|
+
if defined? YARD
|
5
|
+
namespace :yard do
|
6
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
7
|
+
# --no-stats here as 'stats' task called after will print fuller stats
|
8
|
+
t.options = ['--no-stats']
|
9
|
+
|
10
|
+
t.after = Proc.new {
|
11
|
+
Rake::Task['yard:stats'].execute
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
task :doc
|
16
|
+
|
17
|
+
desc "Shows stats for YARD Documentation including listing undocumented modules, classes, constants, and methods"
|
18
|
+
task :stats do
|
19
|
+
stats = YARD::CLI::Stats.new
|
20
|
+
stats.run('--compact', '--list-undoc')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @todo Figure out how to just clone description from yard:doc
|
25
|
+
desc "Generate YARD documentation"
|
26
|
+
# allow calling namespace to as a task that goes to default task for namespace
|
27
|
+
task :yard => ['yard:doc']
|
28
|
+
|
29
|
+
task :default => :yard
|
30
|
+
else
|
31
|
+
puts 'YARD not defined, so yard tasks cannot be setup.'
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'metasploit/erd/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'metasploit-erd'
|
8
|
+
spec.version = Metasploit::ERD::VERSION
|
9
|
+
spec.authors = ['Luke Imhoff']
|
10
|
+
spec.email = ['luke_imhoff@rapid7.com']
|
11
|
+
spec.summary = 'Extensions to rails-erd to find clusters of models to generate subdomains specific to each model'
|
12
|
+
spec.description = 'Traces the belongs_to associations on ActiveRecord::Base descendants to find the minimum ' \
|
13
|
+
'cluster in which all foreign keys are fulfilled in the Entity-Relationship Diagram.'
|
14
|
+
spec.homepage = 'https://github.com/rapid7/metasploit-erd'
|
15
|
+
spec.license = 'BSD-3-clause'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_development_dependency 'bundler', '~> 1.5'
|
23
|
+
spec.add_development_dependency 'rake', '~> 10.3'
|
24
|
+
spec.add_development_dependency 'rspec', '~> 2.14'
|
25
|
+
|
26
|
+
# restrict from rails 4.0 to be compatible with rest of metasploit ecosystem.
|
27
|
+
# @todo Update to work with rails 4 (MSP-9836)
|
28
|
+
rails_version_constraints = ['>= 3.2', '< 4.0.0']
|
29
|
+
|
30
|
+
spec.add_runtime_dependency 'activerecord', *rails_version_constraints
|
31
|
+
spec.add_runtime_dependency 'activesupport', *rails_version_constraints
|
32
|
+
spec.add_runtime_dependency 'rails-erd', '~> 1.1'
|
33
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Metasploit::ERD::Cluster do
|
4
|
+
include_context 'ActiveRecord::Base connection'
|
5
|
+
include_context 'ActiveRecord::Base.descendants cleaner'
|
6
|
+
|
7
|
+
subject(:cluster) {
|
8
|
+
described_class.new(*roots)
|
9
|
+
}
|
10
|
+
|
11
|
+
context '#class_set' do
|
12
|
+
subject(:class_set) {
|
13
|
+
cluster.class_set
|
14
|
+
}
|
15
|
+
|
16
|
+
context 'with roots' do
|
17
|
+
context 'with cycle' do
|
18
|
+
let(:roots) do
|
19
|
+
A
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Callbacks
|
24
|
+
#
|
25
|
+
|
26
|
+
before(:each) do
|
27
|
+
a_class = Class.new(ActiveRecord::Base) {
|
28
|
+
belongs_to :b,
|
29
|
+
class_name: 'B',
|
30
|
+
inverse_of: :as
|
31
|
+
|
32
|
+
has_many :cs,
|
33
|
+
class_name: 'C',
|
34
|
+
inverse_of: :a
|
35
|
+
}
|
36
|
+
|
37
|
+
stub_const('A', a_class)
|
38
|
+
|
39
|
+
ActiveRecord::Migration.verbose = false
|
40
|
+
|
41
|
+
ActiveRecord::Migration.create_table :as do |t|
|
42
|
+
t.references :b
|
43
|
+
end
|
44
|
+
|
45
|
+
b_class = Class.new(ActiveRecord::Base) {
|
46
|
+
has_many :as,
|
47
|
+
class_name: 'A',
|
48
|
+
inverse_of: :b
|
49
|
+
|
50
|
+
belongs_to :c,
|
51
|
+
class_name: 'C',
|
52
|
+
inverse_of: :bs
|
53
|
+
|
54
|
+
}
|
55
|
+
|
56
|
+
stub_const('B', b_class)
|
57
|
+
|
58
|
+
ActiveRecord::Migration.create_table :bs do |t|
|
59
|
+
t.references :c
|
60
|
+
end
|
61
|
+
|
62
|
+
c_class = Class.new(ActiveRecord::Base) {
|
63
|
+
belongs_to :a,
|
64
|
+
class_name: 'A',
|
65
|
+
inverse_of: :cs
|
66
|
+
|
67
|
+
has_many :bs,
|
68
|
+
class_name: 'B',
|
69
|
+
inverse_of: :c
|
70
|
+
}
|
71
|
+
|
72
|
+
stub_const('C', c_class)
|
73
|
+
|
74
|
+
ActiveRecord::Migration.create_table :cs do |t|
|
75
|
+
t.references :a
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'includes all classes in cycle' do
|
80
|
+
expect(class_set).to include A
|
81
|
+
expect(class_set).to include B
|
82
|
+
expect(class_set).to include C
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'with superclasses' do
|
87
|
+
#
|
88
|
+
# lets
|
89
|
+
#
|
90
|
+
|
91
|
+
let(:subclass) {
|
92
|
+
Class.new(superclass)
|
93
|
+
}
|
94
|
+
|
95
|
+
let(:superclass) {
|
96
|
+
Class.new(ActiveRecord::Base)
|
97
|
+
}
|
98
|
+
|
99
|
+
#
|
100
|
+
# Callbacks
|
101
|
+
#
|
102
|
+
|
103
|
+
before(:each) do
|
104
|
+
stub_const('Superclass', superclass)
|
105
|
+
stub_const('Subclass', subclass)
|
106
|
+
|
107
|
+
ActiveRecord::Migration.verbose = false
|
108
|
+
|
109
|
+
ActiveRecord::Migration.create_table superclass.table_name do |t|
|
110
|
+
# type column for hold Class#name for Single Table Inheritance (STI)
|
111
|
+
t.string :type, null: false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context 'with subclass as root' do
|
116
|
+
let(:roots) {
|
117
|
+
[
|
118
|
+
subclass
|
119
|
+
]
|
120
|
+
}
|
121
|
+
|
122
|
+
it 'includes subclass' do
|
123
|
+
expect(class_set).to include(subclass)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'includes superclass' do
|
127
|
+
expect(class_set).to include(superclass)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context 'with superclass as root' do
|
132
|
+
let(:roots) {
|
133
|
+
[
|
134
|
+
superclass
|
135
|
+
]
|
136
|
+
}
|
137
|
+
|
138
|
+
it 'includes superclass' do
|
139
|
+
expect(class_set).to include(superclass)
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'does not include subclasses because subclasses should not have additional foreign keys' do
|
143
|
+
expect(class_set).not_to include(subclass)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'without roots' do
|
150
|
+
let(:roots) do
|
151
|
+
[]
|
152
|
+
end
|
153
|
+
|
154
|
+
it { should be_a Set }
|
155
|
+
it { should be_empty }
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|