Module: Metasploit::Credential::EntityRelationshipDiagram

Defined in:
lib/metasploit/credential/entity_relationship_diagram.rb

Overview

TODO:

Extract (along with MetasploitDataModel::EntityRelationshipDiagram), common ERD code and move to metasploit-documentation or metasploit-entity_relationship_diagram

Constant Summary collapse

ATTRIBUTES =

Enable all attributes

[
    :content,
    :foreign_keys,
    :primary_keys,
    :timestamps
]
INDIRECT =

Only show direct relationships since the ERD is for use with SQL and there is no need to show has_many :through for those purposes.

false
INHERITANCE =

Show inheritance for Single-Table Inheritance

true
NOTATION =

Use crowsfoot notation since its what we use for manually drawn diagrams.

:crowsfoot
DEFAULT_OPTIONS =

Default options for Diagram.

{
    attributes: ATTRIBUTES,
    indirect: INDIRECT,
    inheritance: INHERITANCE,
    notation: NOTATION
}

Class Method Summary collapse

Class Method Details

.cluster(*classes) ⇒ Set<Class<ApplicationRecord>>

Cluster of classes that are reachable through belongs_to from ‘classes`.

Parameters:

  • classes (Array<Class<ApplicationRecord>>)

    classes that must be in cluster. All other classes in the returned cluster will be classes to which ‘classes` belong directly or indirectly.

Returns:

  • (Set<Class<ApplicationRecord>>)


61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/metasploit/credential/entity_relationship_diagram.rb', line 61

def self.cluster(*classes)
  class_queue = classes.dup
  visited_class_set = Set.new

  until class_queue.empty?
    klass = class_queue.pop
    # add immediately to visited set in case there are recursive associations
    visited_class_set.add klass

    # only iterate belongs_to as they need to be included so that foreign keys aren't let dangling in the ERD.
    reflections = klass.reflect_on_all_associations(:belongs_to)

    reflections.each do |reflection|
      if reflection.options[:polymorphic]
        target_klasses = polymorphic_classes(reflection)
      else
        target_klasses = [reflection.klass]
      end

      target_klasses.each do |target_klass|
        unless visited_class_set.include? target_klass
          class_queue << target_klass
        end
      end
    end
  end

  visited_class_set
end

.cluster_by_classHash{Class<ApplicationRecord> => Set<Class<ApplicationRecord>>}

All clusters of classes that are reachable through belongs_to from each ApplicationRecord descendant

Returns:

  • (Hash{Class<ApplicationRecord> => Set<Class<ApplicationRecord>>})

    Maps entry point to cluster to its cluster.



43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/metasploit/credential/entity_relationship_diagram.rb', line 43

def self.cluster_by_class
  cluster_by_class = {}

  Metasploit::Credential::Engine.instance.eager_load!

  ApplicationRecord.descendants.each do |klass|
    klass_cluster = cluster(klass)
    cluster_by_class[klass] = klass_cluster
  end

  cluster_by_class
end

.create(options = {}) ⇒ String

Creates Graphviz diagram.

Parameters:

  • options (Hash{Symbol => Object}) (defaults to: {})

Options Hash (options):

  • :domain (RailsERD::Domain) — default: {domain}

    The domain to diagram.

  • :filename (String)

    name of file (without extension) to which to write diagram.

  • :title (String)

    Title of the diagram to include on the diagram.

Returns:

  • (String)

    path where diagram was written.



98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/metasploit/credential/entity_relationship_diagram.rb', line 98

def self.create(options={})
  domain = options[:domain]
  domain ||= self.domain

  diagram_options = options.except(:domain)
  merged_diagram_options = DEFAULT_OPTIONS.merge(diagram_options)

  require 'rails_erd/domain'
  diagram = RailsERD::Diagram::Graphviz.new(domain, merged_diagram_options)
  path = diagram.create

  path
end

.domainRailsERD::Domain

Domain containing all models in this gem.

Returns:

  • (RailsERD::Domain)


116
117
118
119
120
121
# File 'lib/metasploit/credential/entity_relationship_diagram.rb', line 116

def self.domain
  require_models

  require 'rails_erd/domain'
  RailsERD::Domain.generate
end

.maximal_clustersArray<Set<Class<ApplicationRecord>>>

Set of largest clusters from cluster_by_class.

Returns:

  • (Array<Set<Class<ApplicationRecord>>>)


126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/metasploit/credential/entity_relationship_diagram.rb', line 126

def self.maximal_clusters
  clusters = cluster_by_class.values
  unique_clusters = clusters.uniq

  maximal_clusters = unique_clusters.dup
  cluster_queue = unique_clusters.dup

  until cluster_queue.empty?
    cluster = cluster_queue.pop

    proper_subset = false

    maximal_clusters.each do |maximal_cluster|
      if cluster.proper_subset? maximal_cluster
        proper_subset = true
        break
      end
    end

    if proper_subset
      maximal_clusters.delete(cluster)
    end
  end

  maximal_clusters
end

.polymorphic_classes(belongs_to_reflection) ⇒ Array<ApplicationRecord>

Calculates the target classes for a polymorphic ‘belongs_to`.

Returns:

  • (Array<ApplicationRecord>)


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/metasploit/credential/entity_relationship_diagram.rb', line 156

def self.polymorphic_classes(belongs_to_reflection)
  name = belongs_to_reflection.name

  ApplicationRecord.descendants.each_with_object([]) { |descendant, target_classes|
    has_many_reflections = descendant.reflect_on_all_associations(:has_many)

    has_many_reflections.each do |has_many_reflection|
      as = has_many_reflection.options[:as]

      if as == name
        target_classes << descendant
      end
    end
  }
end