-
Notifications
You must be signed in to change notification settings - Fork 57
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
Open
cdelafuente-r7
wants to merge
4
commits into
rapid7:master
Choose a base branch
from
cdelafuente-r7:module-execution-data-models
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
a091880
Add ModuleExecution, ModuleExecutionError and ModuleExecutionEvent da…
cdelafuente-r7 7bafa64
Temporary fix for the YARD dependency issue.
cdelafuente-r7 f9fc9fb
Code review changes
cdelafuente-r7 f0acdb1
Add `check_code` and `check_message` to ModuleExecution data model
cdelafuente-r7 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| # 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].freeze | ||
|
|
||
| # User interface or programmatic surface that initiated this execution. | ||
| ORIGINATING_INTERFACES = %w[console rpc json_rpc mcp external import plugin autocheck].freeze | ||
|
|
||
| # 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 | ||
|
|
||
| # Six-value enum published by `Msf::Exploit::CheckCode` for | ||
| # {#check_code}. Only populated when {#kind} is `'check'`. | ||
| CHECK_CODES = %w[vulnerable appears detected safe unknown unsupported].freeze | ||
|
|
||
| # | ||
| # 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_interface | ||
| # One of {ORIGINATING_INTERFACES}. | ||
| # | ||
| # @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] check_code | ||
| # The {CHECK_CODES} value returned by a `kind = 'check'` execution | ||
| # (i.e. `Msf::Exploit::CheckCode#code`). NULL for non-check | ||
| # executions, and NULL for check executions that raised before | ||
| # returning a `CheckCode`. | ||
| # | ||
| # @return [String] | ||
| # @return [nil] when not populated. | ||
|
|
||
| # @!attribute [rw] check_message | ||
| # Human-readable message that accompanies {#check_code} (i.e. | ||
| # `Msf::Exploit::CheckCode#message`). NULL when {#check_code} is | ||
| # NULL, and may be NULL even when {#check_code} is set if the | ||
| # check returned no message. | ||
| # | ||
| # @return [String] | ||
| # @return [nil] when not populated. | ||
|
|
||
| # @!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_interface, presence: true, inclusion: { in: ORIGINATING_INTERFACES } | ||
| validates :terminal_status, inclusion: { in: TERMINAL_STATUSES, allow_nil: true } | ||
| validates :check_code, inclusion: { in: CHECK_CODES, 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 | ||
| validate :check_code_only_on_check_kind | ||
|
|
||
| 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 | ||
|
|
||
| def check_code_only_on_check_kind | ||
| return if kind == 'check' | ||
| return if check_code.nil? && check_message.nil? | ||
|
|
||
| errors.add(:check_code, 'may only be populated when kind is check') unless check_code.nil? | ||
| errors.add(:check_message, 'may only be populated when kind is check') unless check_message.nil? | ||
| end | ||
| end | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # 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] | ||
|
|
||
| # | ||
| # Validations | ||
| # | ||
|
|
||
| validates :lifecycle_phase, presence: true, inclusion: { in: LIFECYCLE_PHASES } | ||
| validates :occurred_at, presence: true | ||
|
|
||
| Metasploit::Concern.run(self) | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # 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] | ||
|
|
||
| # | ||
| # Validations | ||
| # | ||
|
|
||
| validates :name, presence: true | ||
| validates :occurred_at, presence: true | ||
|
|
||
| Metasploit::Concern.run(self) | ||
| end |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
If unhandled_exception is the inverse of expected_failure, it might be more intuitive to track it as unexpected_failure.
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.
Let me give more details about these status:
expected_failure: the module explicitly declared failure viafail_with. It is an error the module author has anticipated with a properfailure_reason.unhandled_exception: a Ruby exception that escaped the module's rescue chain (NoMethodError,Rex::ConnectionErroroutside a rescue, etc.). The author did not anticipate this; there's anexception_classand backtrace.So,
unhandled_exceptionandexpected_failurearen't strict inverses. For example,unhandled_exceptionwill help answer question like "which modules are buggy?"