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.
@@ -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