lib/mihari/analyzers/rule.rb in mihari-5.2.1 vs lib/mihari/analyzers/rule.rb in mihari-5.2.2
- old
+ new
@@ -32,115 +32,164 @@
"slack" => Emitters::Slack,
"the_hive" => Emitters::TheHive,
"webhook" => Emitters::Webhook
}.freeze
- # @return [Mihari::Structs::Rule]
- attr_reader :rule
-
- class Rule < Base
+ class Rule
include Mixins::FalsePositive
- def initialize(**kwargs)
- super(**kwargs)
+ # @return [Mihari::Structs::Rule]
+ attr_reader :rule
+ # @return [Time]
+ attr_reader :base_time
+
+ #
+ # @param [Mihari::Structs::Rule] rule
+ #
+ def initialize(rule:)
+ @rule = rule
+ @base_time = Time.now.utc
+
validate_analyzer_configurations
end
#
# Returns a list of artifacts matched with queries
#
# @return [Array<Mihari::Artifact>]
#
def artifacts
- artifacts = []
-
- rule.queries.each do |original_params|
- parmas = original_params.deep_dup
-
- analyzer_name = parmas[:analyzer]
- klass = get_analyzer_class(analyzer_name)
-
- query = parmas[:query]
-
- # set interval in the top level
- options = parmas[:options] || {}
- interval = options[:interval]
-
- parmas[:interval] = interval if interval
-
- # set rule
- parmas[:rule] = rule
-
- analyzer = klass.new(query, **parmas)
-
- # Use #normalized_artifacts method to get atrifacts as Array<Mihari::Artifact>
- # So Mihari::Artifact object has "source" attribute (e.g. "Shodan")
- artifacts << analyzer.normalized_artifacts
- end
-
- artifacts.flatten
+ rule.queries.map { |params| run_query(params.deep_dup) }.flatten
end
#
# Normalize artifacts
- # - Uniquefy artifacts by #uniq(&:data)
- # - Reject an invalid artifact (for just in case)
+ # - Reject invalid artifacts (for just in case)
# - Select artifacts with allowed data types
- # - Reject artifacts with disallowed data values
+ # - Reject artifacts with false positive values
+ # - Set rule ID
#
# @return [Array<Mihari::Artifact>]
#
def normalized_artifacts
@normalized_artifacts ||= artifacts.uniq(&:data).select(&:valid?).select do |artifact|
rule.data_types.include? artifact.data_type
end.reject do |artifact|
falsepositive? artifact.data
+ end.map do |artifact|
+ artifact.rule_id = rule.id
+ artifact
end
end
#
+ # Uniquify artifacts (assure rule level uniqueness)
+ #
+ # @return [Array<Mihari::Artifact>]
+ #
+ def unique_artifacts
+ @unique_artifacts ||= normalized_artifacts.select do |artifact|
+ artifact.unique?(base_time: base_time, artifact_lifetime: rule.artifact_lifetime)
+ end
+ end
+
+ #
# Enriched artifacts
#
# @return [Array<Mihari::Artifact>]
#
def enriched_artifacts
@enriched_artifacts ||= Parallel.map(unique_artifacts) do |artifact|
- rule.enrichers.each do |enricher|
- artifact.enrich_by_enricher(enricher[:enricher])
- end
-
+ rule.enrichers.each { |enricher| artifact.enrich_by_enricher enricher[:enricher] }
artifact
end
end
#
- # Normalized disallowed data values
+ # Bulk emit
#
- # @return [Array<Regexp, String>]
+ # @return [Array<Mihari::Alert>]
#
- def normalized_falsepositives
- @normalized_falsepositives ||= rule.falsepositives.map { |v| normalize_falsepositive v }
+ def bulk_emit
+ Parallel.map(valid_emitters) { |emitter| emit emitter }.compact
end
#
+ # Emit an alert
+ #
+ # @param [Mihari::Emitters::Base] emitter
+ #
+ # @return [Mihari::Alert, nil]
+ #
+ def emit(emitter)
+ return if enriched_artifacts.empty?
+
+ alert_or_something = emitter.run(artifacts: enriched_artifacts, rule: rule)
+
+ Mihari.logger.info "Emission by #{emitter.class} is succeeded"
+
+ alert_or_something
+ rescue StandardError => e
+ Mihari.logger.info "Emission by #{emitter.class} is failed: #{e}"
+ end
+
+ #
+ # Set artifacts & run emitters in parallel
+ #
+ # @return [Mihari::Alert, nil]
+ #
+ def run
+ # memoize enriched artifacts
+ enriched_artifacts
+
+ alert_or_something = bulk_emit
+ # returns Mihari::Alert created by the database emitter
+ alert_or_something.find { |res| res.is_a?(Mihari::Alert) }
+ end
+
+ private
+
+ #
# Check whether a value is a falsepositive value or not
#
# @return [Boolean]
#
def falsepositive?(value)
- return true if normalized_falsepositives.include?(value)
+ return true if rule.falsepositives.include?(value)
- normalized_falsepositives.select do |falsepositive|
+ rule.falsepositives.select do |falsepositive|
falsepositive.is_a?(Regexp)
end.any? do |falseposistive|
falseposistive.match?(value)
end
end
- private
+ #
+ # @param [Hash] params
+ #
+ # @return [Array<Mihari::Artifact>]
+ #
+ def run_query(params)
+ analyzer_name = params[:analyzer]
+ klass = get_analyzer_class(analyzer_name)
+ # set interval in the top level
+ options = params[:options] || {}
+ interval = options[:interval]
+ params[:interval] = interval if interval
+
+ # set rule
+ params[:rule] = rule
+ query = params[:query]
+ analyzer = klass.new(query, **params)
+
+ # Use #normalized_artifacts method to get artifacts as Array<Mihari::Artifact>
+ # So Mihari::Artifact object has "source" attribute (e.g. "Shodan")
+ analyzer.normalized_artifacts
+ end
+
#
# Get emitter class
#
# @param [String] emitter_name
#
@@ -151,22 +200,30 @@
return emitter if emitter
raise ArgumentError, "#{emitter_name} is not supported"
end
- def valid_emitters
- @valid_emitters ||= rule.emitters.filter_map do |original_params|
- params = original_params.deep_dup
+ #
+ # @param [Hash] params
+ #
+ # @return [Mihari::Emitter:Base]
+ #
+ def validate_emitter(params)
+ name = params[:emitter]
+ params.delete(:emitter)
- name = params[:emitter]
- params.delete(:emitter)
+ klass = get_emitter_class(name)
+ emitter = klass.new(**params)
- klass = get_emitter_class(name)
- emitter = klass.new(**params)
+ emitter.valid? ? emitter : nil
+ end
- emitter.valid? ? emitter : nil
- end
+ #
+ # @return [Array<Mihari::Emitter::Base>]
+ #
+ def valid_emitters
+ @valid_emitters ||= rule.emitters.filter_map { |params| validate_emitter(params.deep_dup) }
end
#
# Get analyzer class
#
@@ -185,16 +242,15 @@
# Validate configuration of analyzers
#
def validate_analyzer_configurations
rule.queries.each do |params|
analyzer_name = params[:analyzer]
+
klass = get_analyzer_class(analyzer_name)
+ klass_name = klass.to_s.split("::").last
instance = klass.new("dummy")
- unless instance.configured?
- klass_name = klass.to_s.split("::").last
- raise ConfigurationError, "#{klass_name} is not configured correctly"
- end
+ raise ConfigurationError, "#{klass_name} is not configured correctly" unless instance.configured?
end
end
end
end
end