metasploit-erd 0.0.1

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