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