-
Notifications
You must be signed in to change notification settings - Fork 58
Add ModuleExecution and related data models #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,219 @@ | ||||||
| # One invocation of one Metasploit module (or one direct-write | ||||||
| # pseudo-execution. Captures the full forensic record so every | ||||||
| # artifact a module produces or touches can be traced back to a single | ||||||
| # row in `module_executions`. | ||||||
| class Mdm::ModuleExecution < ApplicationRecord | ||||||
| self.table_name = 'module_executions' | ||||||
|
|
||||||
| # | ||||||
| # CONSTANTS | ||||||
| # | ||||||
|
|
||||||
| # Why the module was run. `direct_write` is used when an external | ||||||
| # workflow inserts artifacts (hosts, services, creds, loot, ...) | ||||||
| # without invoking a module via the framework. | ||||||
| KINDS = %w[run check import direct_write].freeze | ||||||
|
|
||||||
| # Module types as exposed by Framework. | ||||||
| MODULE_TYPES = %w[exploit auxiliary post payload encoder evasion nop external].freeze | ||||||
|
|
||||||
| # User interface or programmatic surface that initiated this execution. | ||||||
| ORIGINATING_UIS = %w[console rpc json_rpc mcp external import plugin autocheck].freeze | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I'd suggest adding 'module' as an originator to account for modules running other modules. Additionally since some of these aren't exactly user-interfaces, it may be better to use a slightly more generic name like ORIGINATORS vs ORIGINATING_UIS.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right it's not accurate. I would prefer using |
||||||
|
|
||||||
| # Terminal lifecycle states. `running` is the only non-terminal value | ||||||
| # and is permitted while {#ended_at} is `NULL`. | ||||||
| TERMINAL_STATUSES = %w[running success neutral expected_failure unhandled_exception].freeze | ||||||
|
Comment on lines
+23
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If unhandled_exception is the inverse of expected_failure, it might be more intuitive to track it as unexpected_failure.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me give more details about these status:
So, |
||||||
|
|
||||||
| # | ||||||
| # Associations | ||||||
| # | ||||||
|
|
||||||
| # Workspace that owns this execution. All artifacts produced by the | ||||||
| # execution share this workspace. | ||||||
| # | ||||||
| # @return [Mdm::Workspace] | ||||||
| belongs_to :workspace, | ||||||
| class_name: 'Mdm::Workspace', | ||||||
| inverse_of: :module_executions | ||||||
|
|
||||||
| # User who initiated this execution, when known. May be `nil` for | ||||||
| # executions originating from non-authenticated surfaces or from | ||||||
| # background workers without a session. | ||||||
| # | ||||||
| # @return [Mdm::User] | ||||||
| # @return [nil] when no user is associated with this execution. | ||||||
| belongs_to :originating_user, | ||||||
| class_name: 'Mdm::User', | ||||||
| optional: true, | ||||||
| inverse_of: :module_executions | ||||||
|
|
||||||
| # Parent execution that spawned this one (e.g. an exploit's auto-run | ||||||
| # post module). Forms a tree of related executions when present. | ||||||
| # | ||||||
| # @return [Mdm::ModuleExecution] | ||||||
| # @return [nil] for top-level executions. | ||||||
| belongs_to :parent_execution, | ||||||
| class_name: 'Mdm::ModuleExecution', | ||||||
| optional: true, | ||||||
| inverse_of: :children | ||||||
|
|
||||||
| # Child executions spawned by this execution. | ||||||
| # | ||||||
| # @return [ActiveRecord::Relation<Mdm::ModuleExecution>] | ||||||
| has_many :children, | ||||||
| class_name: 'Mdm::ModuleExecution', | ||||||
| foreign_key: :parent_execution_id, | ||||||
| inverse_of: :parent_execution, | ||||||
| dependent: :nullify | ||||||
|
|
||||||
| # Errors raised within this execution. Renamed from `errors` to avoid | ||||||
| # colliding with `ActiveModel::Validations#errors`. | ||||||
| # | ||||||
| # @return [ActiveRecord::Relation<Mdm::ModuleExecutionError>] | ||||||
| has_many :execution_errors, | ||||||
| class_name: 'Mdm::ModuleExecutionError', | ||||||
| inverse_of: :module_execution, | ||||||
| dependent: :destroy | ||||||
|
|
||||||
| # Optional generalized timeline events recorded for this execution. | ||||||
| # | ||||||
| # @return [ActiveRecord::Relation<Mdm::ModuleExecutionEvent>] | ||||||
| has_many :events, | ||||||
| class_name: 'Mdm::ModuleExecutionEvent', | ||||||
| inverse_of: :module_execution, | ||||||
| dependent: :destroy | ||||||
|
|
||||||
| # | ||||||
| # Attributes | ||||||
| # | ||||||
|
|
||||||
| # @!attribute [rw] module_reference_name | ||||||
| # Reference name of the module that was executed, e.g. | ||||||
| # `auxiliary/scanner/smb/smb_version`. | ||||||
| # | ||||||
| # @return [String] | ||||||
|
|
||||||
| # @!attribute [rw] module_type | ||||||
| # One of {MODULE_TYPES}. | ||||||
| # | ||||||
| # @return [String] | ||||||
|
|
||||||
| # @!attribute [rw] kind | ||||||
| # One of {KINDS}. Defaults to `'run'`. | ||||||
| # | ||||||
| # @return [String] | ||||||
|
|
||||||
| # @!attribute [rw] options_snapshot | ||||||
| # The datastore options the module was invoked with | ||||||
| # | ||||||
| # @return [Hash] | ||||||
| # @return [nil] when no options were captured. | ||||||
|
|
||||||
| # @!attribute [rw] originating_ui | ||||||
| # One of {ORIGINATING_UIS}. | ||||||
| # | ||||||
| # @return [String] | ||||||
|
|
||||||
| # @!attribute [rw] originating_token_ref | ||||||
| # Opaque reference to the auth token / API key used to authorize | ||||||
| # this execution, when applicable. Never the token itself. | ||||||
| # | ||||||
| # @return [String] | ||||||
| # @return [nil] when not applicable. | ||||||
|
|
||||||
| # @!attribute [rw] started_at | ||||||
| # Wall-clock time when the framework began running the module. | ||||||
| # | ||||||
| # @return [DateTime] | ||||||
|
|
||||||
| # @!attribute [rw] ended_at | ||||||
| # Wall-clock time when the module finished, regardless of outcome. | ||||||
| # Must be greater than or equal to {#started_at}. | ||||||
| # | ||||||
| # @return [DateTime] | ||||||
| # @return [nil] while the execution is still running. | ||||||
|
|
||||||
| # @!attribute [rw] terminal_status | ||||||
| # One of {TERMINAL_STATUSES}. Constrained to be `'running'` (or | ||||||
| # `nil`) while {#ended_at} is `nil`, and a non-`running` value | ||||||
| # once {#ended_at} is set. | ||||||
| # | ||||||
| # @return [String] | ||||||
| # @return [nil] while the execution is still running. | ||||||
|
|
||||||
| # @!attribute [rw] failure_reason | ||||||
| # Short coded reason describing why the module ended in a non- | ||||||
| # success terminal status (e.g. `'no-target'`, | ||||||
| # `'payload-not-supported'`). | ||||||
| # | ||||||
| # @return [String] | ||||||
| # @return [nil] when the execution did not fail. | ||||||
|
|
||||||
| # @!attribute [rw] failure_message | ||||||
| # Human-readable message describing the failure. | ||||||
| # | ||||||
| # @return [String] | ||||||
| # @return [nil] when the execution did not fail. | ||||||
|
|
||||||
| # @!attribute [rw] single_entity_failure_count | ||||||
| # How many individually-failed entities (e.g. hosts in a scanner) | ||||||
| # the module recorded during this execution. Used to surface | ||||||
| # per-entity failure summaries without listing every error row. | ||||||
| # | ||||||
| # @return [Integer] | ||||||
| # @return [0] when no per-entity failures were recorded. | ||||||
|
|
||||||
| # @!attribute [rw] last_single_entity_errors | ||||||
| # Capped sample of the most recent per-entity error | ||||||
| # summaries, suitable for quick inspection in the UI. | ||||||
| # | ||||||
| # @return [Array<Hash>] | ||||||
| # @return [nil] when no per-entity errors were recorded. | ||||||
|
|
||||||
| # @!attribute [rw] created_at | ||||||
| # When this execution row was inserted. | ||||||
| # | ||||||
| # @return [DateTime] | ||||||
|
|
||||||
| # @!attribute [rw] updated_at | ||||||
| # Last time this execution row was updated. | ||||||
| # | ||||||
| # @return [DateTime] | ||||||
|
|
||||||
| # | ||||||
| # Validations | ||||||
| # | ||||||
|
|
||||||
| validates :module_reference_name, presence: true | ||||||
| validates :module_type, presence: true, inclusion: { in: MODULE_TYPES } | ||||||
| validates :kind, presence: true, inclusion: { in: KINDS } | ||||||
| validates :originating_ui, presence: true, inclusion: { in: ORIGINATING_UIS } | ||||||
| validates :terminal_status, inclusion: { in: TERMINAL_STATUSES, allow_nil: true } | ||||||
| validates :started_at, presence: true | ||||||
| validates :single_entity_failure_count, | ||||||
| numericality: { only_integer: true, greater_than_or_equal_to: 0 } | ||||||
|
|
||||||
| validate :ended_at_not_before_started_at | ||||||
| validate :terminal_status_consistent_with_ended_at | ||||||
|
|
||||||
| Metasploit::Concern.run(self) | ||||||
|
|
||||||
| private | ||||||
|
|
||||||
| def ended_at_not_before_started_at | ||||||
| return if ended_at.blank? || started_at.blank? | ||||||
| return if ended_at >= started_at | ||||||
|
|
||||||
| errors.add(:ended_at, 'must be greater than or equal to started_at') | ||||||
| end | ||||||
|
|
||||||
| def terminal_status_consistent_with_ended_at | ||||||
| if ended_at.nil? | ||||||
| return if terminal_status.nil? || terminal_status == 'running' | ||||||
|
|
||||||
| errors.add(:terminal_status, "must be 'running' or blank while ended_at is NULL") | ||||||
| elsif terminal_status.nil? || terminal_status == 'running' | ||||||
| errors.add(:terminal_status, 'must be a terminal value once ended_at is set') | ||||||
| end | ||||||
| end | ||||||
| end | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,88 @@ | ||||||||||
| # One row per unhandled exception or explicit failure raised within a | ||||||||||
| # {Mdm::ModuleExecution}. A single execution may produce many error rows | ||||||||||
| # when it touches multiple entities (e.g. one row per failed host in | ||||||||||
| # a scanner sweep). | ||||||||||
| class Mdm::ModuleExecutionError < ApplicationRecord | ||||||||||
| self.table_name = 'module_execution_errors' | ||||||||||
|
|
||||||||||
| # | ||||||||||
| # CONSTANTS | ||||||||||
| # | ||||||||||
|
|
||||||||||
| # Lifecycle phases of a module run during which an error may be | ||||||||||
| # captured. `run` covers auxiliary modules, the others map to the | ||||||||||
| # exploit lifecycle (setup → check → exploit → cleanup) and the | ||||||||||
| # post-exploitation pass. | ||||||||||
| LIFECYCLE_PHASES = %w[setup check exploit cleanup post run].freeze | ||||||||||
|
|
||||||||||
| # | ||||||||||
| # Associations | ||||||||||
| # | ||||||||||
|
|
||||||||||
| # The module execution this error was raised within. | ||||||||||
| # | ||||||||||
| # @return [Mdm::ModuleExecution] | ||||||||||
| belongs_to :module_execution, | ||||||||||
| class_name: 'Mdm::ModuleExecution', | ||||||||||
| inverse_of: :execution_errors | ||||||||||
|
|
||||||||||
| # | ||||||||||
| # Attributes | ||||||||||
| # | ||||||||||
|
|
||||||||||
| # @!attribute [rw] exception_class | ||||||||||
| # Fully-qualified Ruby class name of the exception, when the | ||||||||||
| # failure was raised as one (e.g. `'Rex::ConnectionError'`). | ||||||||||
| # | ||||||||||
| # @return [String] | ||||||||||
| # @return [nil] when the failure was not raised via an exception. | ||||||||||
|
|
||||||||||
| # @!attribute [rw] message | ||||||||||
| # Human-readable failure message. | ||||||||||
| # | ||||||||||
| # @return [String] | ||||||||||
| # @return [nil] when no message was captured. | ||||||||||
|
|
||||||||||
| # @!attribute [rw] backtrace | ||||||||||
| # Captured backtrace (newline-joined) at the point the error was | ||||||||||
| # recorded. | ||||||||||
| # | ||||||||||
| # @return [String] | ||||||||||
| # @return [nil] when no backtrace was available. | ||||||||||
|
|
||||||||||
| # @!attribute [rw] lifecycle_phase | ||||||||||
| # One of {LIFECYCLE_PHASES}. | ||||||||||
| # | ||||||||||
| # @return [String] | ||||||||||
|
|
||||||||||
| # @!attribute [rw] failure_reason | ||||||||||
| # Short coded reason classifying the failure (e.g. | ||||||||||
| # `'connection-refused'`, `'auth-failed'`). | ||||||||||
| # | ||||||||||
| # @return [String] | ||||||||||
| # @return [nil] when the failure has no coded reason. | ||||||||||
|
|
||||||||||
| # @!attribute [rw] occurred_at | ||||||||||
| # Wall-clock time the error was raised. | ||||||||||
| # | ||||||||||
| # @return [DateTime] | ||||||||||
|
|
||||||||||
| # @!attribute [rw] created_at | ||||||||||
| # When this error row was inserted. | ||||||||||
| # | ||||||||||
| # @return [DateTime] | ||||||||||
|
|
||||||||||
| # @!attribute [rw] updated_at | ||||||||||
| # Last time this error row was updated. | ||||||||||
| # | ||||||||||
| # @return [DateTime] | ||||||||||
|
|
||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should also be immutable.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this makes no sense for Error and Events. I removed these fields. Thanks! |
||||||||||
| # | ||||||||||
| # Validations | ||||||||||
| # | ||||||||||
|
|
||||||||||
| validates :lifecycle_phase, presence: true, inclusion: { in: LIFECYCLE_PHASES } | ||||||||||
| validates :occurred_at, presence: true | ||||||||||
|
|
||||||||||
| Metasploit::Concern.run(self) | ||||||||||
| end | ||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,58 @@ | ||||||||||
| # Optional generalized timeline entry attached to a | ||||||||||
| # {Mdm::ModuleExecution}. Used to record milestones that aren't | ||||||||||
| # artifacts in their own right (e.g. `'session_opened'`, | ||||||||||
| # `'target_selected'`, `'check_returned'`). | ||||||||||
| class Mdm::ModuleExecutionEvent < ApplicationRecord | ||||||||||
| self.table_name = 'module_execution_events' | ||||||||||
|
|
||||||||||
| # | ||||||||||
| # Associations | ||||||||||
| # | ||||||||||
|
|
||||||||||
| # The module execution this event belongs to. | ||||||||||
| # | ||||||||||
| # @return [Mdm::ModuleExecution] | ||||||||||
| belongs_to :module_execution, | ||||||||||
| class_name: 'Mdm::ModuleExecution', | ||||||||||
| inverse_of: :events | ||||||||||
|
|
||||||||||
| # | ||||||||||
| # Attributes | ||||||||||
| # | ||||||||||
|
|
||||||||||
| # @!attribute [rw] name | ||||||||||
| # Short identifier for the event, e.g. `'session_opened'`. | ||||||||||
| # | ||||||||||
| # @return [String] | ||||||||||
|
|
||||||||||
| # @!attribute [rw] payload | ||||||||||
| # Free-form structured data describing the event. Schema is | ||||||||||
| # per-event-name; consumers should treat unknown keys as opaque. | ||||||||||
| # | ||||||||||
| # @return [Hash] | ||||||||||
| # @return [nil] when the event carries no structured payload. | ||||||||||
|
|
||||||||||
| # @!attribute [rw] occurred_at | ||||||||||
| # Wall-clock time the event was emitted. | ||||||||||
| # | ||||||||||
| # @return [DateTime] | ||||||||||
|
|
||||||||||
| # @!attribute [rw] created_at | ||||||||||
| # When this event row was inserted. | ||||||||||
| # | ||||||||||
| # @return [DateTime] | ||||||||||
|
|
||||||||||
| # @!attribute [rw] updated_at | ||||||||||
| # Last time this event row was updated. | ||||||||||
| # | ||||||||||
| # @return [DateTime] | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Events should be immutable, so let's remove the
Suggested change
|
||||||||||
|
|
||||||||||
| # | ||||||||||
| # Validations | ||||||||||
| # | ||||||||||
|
|
||||||||||
| validates :name, presence: true | ||||||||||
| validates :occurred_at, presence: true | ||||||||||
|
|
||||||||||
| Metasploit::Concern.run(self) | ||||||||||
| end | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think "external" is a module type in the same sense as the others. You can have for example an external exploit or auxiliary module that's implemented in Python. I would suggest removing it or tracking it as a flag so you can note something is an external-exploit, external-auxiliary, etc.