unclesp1d3r/CipherSwarm

View on GitHub
app/models/hash_list.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
92%
# frozen_string_literal: true

# SPDX-FileCopyrightText:  2024 UncleSp1d3r
# SPDX-License-Identifier: MPL-2.0

# The HashList class represents a list of hash items associated with a project.
# It includes various validations, associations, and methods for processing and
# retrieving information about the hash items.
#
# Associations:
# - has_one_attached :file
# - belongs_to :project, touch: true
# - has_many :campaigns, dependent: :destroy
# - has_many :hash_items, dependent: :destroy
# - belongs_to :hash_type
#
# Validations:
# - Validates presence of :name
# - Validates uniqueness of :name (case insensitive)
# - Validates presence of :file on create
# - Validates length of :name (maximum 255 characters)
# - Validates length of :separator (exactly 1 character, allow blank)
# - Validates attachment of :file based on processed status
#
# Scopes:
# - sensitive: Returns hash lists marked as sensitive
# - accessible_to(user): Returns hash lists accessible to the given user
#
# Callbacks:
# - after_save :process_hash_list, if: :file_attached?
#
# Methods:
# - completion: Returns the completion status of the hash list
# - cracked_count: Returns the count of cracked hash items
# - cracked_list: Returns a string representation of the cracked hash list
# - hash_item_count: Returns the count of hash items in the hash list
# - hash_mode: Returns the hashcat mode of the hash type
# - uncracked_count: Returns the count of uncracked hash items
# - uncracked_items: Returns an ActiveRecord relation of uncracked hash items
# - uncracked_list: Returns a string representation of the uncracked hash list
# - uncracked_list_checksum: Calculates the MD5 checksum of the uncracked_list
#
# Private Methods:
# - file_attached?: Checks if a file is attached and not yet processed
# - process_hash_list: Processes the hash list (synchronously in test environment, asynchronously otherwise)
#
# == Schema Information
#
# Table name: hash_lists
#
#  id                                                                                                                        :bigint           not null, primary key
#  description(Description of the hash list)                                                                                 :text
#  hash_items_count                                                                                                          :integer          default(0)
#  name(Name of the hash list)                                                                                               :string           not null, indexed
#  processed(Is the hash list processed into hash items?)                                                                    :boolean          default(FALSE), not null
#  sensitive(Is the hash list sensitive?)                                                                                    :boolean          default(FALSE), not null
#  separator(Separator used in the hash list file to separate the hash from the password or other metadata. Default is ":".) :string(1)        default(":"), not null
#  created_at                                                                                                                :datetime         not null
#  updated_at                                                                                                                :datetime         not null
#  hash_type_id                                                                                                              :bigint           not null, indexed
#  project_id(Project that the hash list belongs to)                                                                         :bigint           not null, indexed
#
# Indexes
#
#  index_hash_lists_on_hash_type_id  (hash_type_id)
#  index_hash_lists_on_name          (name) UNIQUE
#  index_hash_lists_on_project_id    (project_id)
#
# Foreign Keys
#
#  fk_rails_...  (hash_type_id => hash_types.id)
#  fk_rails_...  (project_id => projects.id)
#
class HashList < ApplicationRecord
  has_one_attached :file
  belongs_to :project, touch: true
  has_many :campaigns, dependent: :destroy
  has_many :hash_items, dependent: :destroy
  belongs_to :hash_type

  validates :name, presence: true, uniqueness: { case_sensitive: false }
  validates :file, presence: { on: :create }
  validates :name, length: { maximum: 255 }
  validates :separator, length: { is: 1, allow_blank: true }
  validate :file_must_be_attached

  broadcasts_refreshes unless Rails.env.test?

  scope :sensitive, -> { where(sensitive: true) }
  # create a scope for hash lists that are either not sensitive or are in a project that the user has access to
  scope :accessible_to, ->(user) { where(project_id: user.projects) }

  default_scope { order(:created_at) }

  delegate :hash_mode, to: :hash_type

  after_save :process_hash_list, if: :file_attached?

  # Returns a string representing the completion status of the hash list.
  #
  # The completion status is calculated by dividing the number of cracked items
  # by the total number of hash items in the list.
  #
  # @return [String] The completion status in the format "cracked_count / total_count".
  def completion
    return "importing..." unless processed?

    "#{cracked_count} / #{hash_item_count}"
  end

  # Returns the count of hash items that have been cracked (i.e., their plain_text is not nil).
  # @return [Integer]
  def cracked_count
    Rails.cache.fetch("#{cache_key_with_version}/cracked_count", expires_in: 20.minutes) do
      hash_items.where.not(plain_text: nil).size
    end
  end

  # Returns a string representation of the cracked hash list.
  #
  # This method retrieves the hash items from the database that have been cracked,
  # and constructs a string representation of each hash item in the format: "hash_value:plain_text".
  #
  # Example:
  #   hash_list.cracked_list
  #   # => "hash1:plain_text1\nhash2:plain_text2\n..."
  #
  # Returns:
  #   A string representation of the cracked hash list.
  # @return [String]
  def cracked_list
    # This should output as "hash:plain_text" for each item if the separator is set to ":"
    hash = hash_items.where.not(plain_text: nil).pluck(:hash_value, :plain_text)
    hash.map { |h, p| "#{h}#{separator}#{p}" }.join("\n")
  end

  # Returns the count of items in the hash.
  #
  # @return [Integer] the number of items in the hash
  def hash_item_count
    hash_items.size
  end

  # Returns the count of uncracked hash items.
  #
  # @return [Integer] the number of uncracked hash items
  def uncracked_count
    hash_items.uncracked.size
  end

  # Returns a collection of hash items that are uncracked.
  #
  # @return [ActiveRecord::Relation] a collection of uncracked hash items
  def uncracked_items
    hash_items.uncracked
  end

  # Returns a string representation of the uncracked hash list.
  #
  # This method retrieves the hash items from the database that have not been cracked,
  # and constructs a string representation of each hash item in the format: "hash_value".
  #
  # Example:
  #   hash_list.uncracked_list
  #   # => "hash1\nhash2\n..."
  #
  # Returns:
  #   A string representation of the uncracked hash list.
  # @return [String]
  def uncracked_list
    # This should output as "hash" for each item
    hash_lines = []
    hash = uncracked_items.pluck(:hash_value)
    hash.each do |h|
      hash_lines << "#{h}"
    end
    hash_lines.join("\n")
  end

  # Calculates the MD5 checksum of the uncracked_list.
  #
  # This method calculates the MD5 checksum of the uncracked_list string and returns it as a base64-encoded string.
  #
  # @return [String] The MD5 checksum of the uncracked_list as a base64-encoded string.
  def uncracked_list_checksum
    md5 = OpenSSL::Digest.new("MD5")
    md5.update(uncracked_list)
    md5.base64digest
  end

  private

  # Checks if a file is attached and not processed.
  #
  # @return [Boolean] true if a file is attached and not processed, false otherwise.
  def file_attached?
    file.attached? && !processed?
  end

  def file_must_be_attached
    errors.add(:file, "must be attached") unless processed? || file.attached?
  end

  # Processes the hash list.
  #
  # If the current environment is test, the `ProcessHashListJob` is performed immediately.
  # Otherwise, the `ProcessHashListJob` is performed asynchronously.
  #
  # @return [void]
  def process_hash_list
    if Rails.env.test?
      ProcessHashListJob.perform_now(id)
      return
    end
    ProcessHashListJob.perform_later(id)
  end
end