metasploit-erd 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|