Class: Yara::ScanResult

Inherits:
Object
  • Object
show all
Defined in:
lib/yara/scan_result.rb

Overview

Public: Represents a single rule match result from YARA scanning.

A ScanResult contains information about a YARA rule that matched during scanning, including the rule name, metadata, string patterns, and detailed pattern match information. This class provides access to rule information extracted from both the YARA-X API and parsed rule source code.

The enhanced version provides detailed pattern match information including exact offsets and lengths of each pattern match, allowing for precise forensic analysis and data extraction.

Examples

# Typically created by Scanner during scanning
scanner.scan(data) do |result|
  puts "Matched rule: #{result.rule_name}"
  puts "Author: #{result.rule_meta[:author]}"

  # New: Access detailed pattern matches
  result.pattern_matches.each do |pattern_name, matches|
    puts "Pattern #{pattern_name}: #{matches.size} matches"
    matches.each do |match|
      matched_text = data[match.offset, match.length]
      puts "  At offset #{match.offset}: '#{matched_text}'"
    end
  end
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(rule_name, rule_ptr, is_match = true, rule_source = nil, scanned_data = nil) ⇒ ScanResult

Public: Initialize a new ScanResult.

This constructor is typically called internally by Scanner when a rule matches during scanning. It extracts available information from both the YARA-X API and the original rule source code, including detailed pattern match information.

rule_name - A String containing the rule identifier/name rule_ptr - An FFI Pointer to the YRX_RULE structure is_match - A Boolean indicating if this represents a match (default true) rule_source - An optional String containing the original rule source for parsing scanned_data - An optional String containing the data that was scanned (needed for pattern matches)

Examples

# Typically created internally by Scanner
result = ScanResult.new("MyRule", rule_ptr, true, rule_source, scanned_data)


79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/yara/scan_result.rb', line 79

def initialize(rule_name, rule_ptr, is_match = true, rule_source = nil, scanned_data = nil)
  @rule_name = rule_name
  @rule_ptr = rule_ptr
  @is_match = is_match
  @rule_source = rule_source
  @scanned_data = scanned_data
  @rule_meta = {}
  @rule_strings = {}
  @pattern_matches = {}
  @tags = []
  @namespace = nil

  # Extract information using YARA-X API when rule pointer is available
  if @rule_ptr && !@rule_ptr.null?
    # TODO: Re-enable structured metadata after fixing union handling
    # extract_structured_metadata
    extract_tags
    extract_namespace
    extract_pattern_matches
  end

  # Parse metadata and strings from source (primary method for now)
  if @rule_source
    
    parse_strings_from_source
  end
end

Instance Attribute Details

#namespaceObject (readonly)

Public: Namespace of the rule, if defined.

YARA rules can be organized into namespaces to avoid naming conflicts and provide logical grouping. This contains the namespace name or nil if the rule is in the default namespace.



60
61
62
# File 'lib/yara/scan_result.rb', line 60

def namespace
  @namespace
end

#pattern_matchesObject (readonly)

Public: Hash of pattern names to arrays of PatternMatch objects.

This provides detailed information about exactly where each pattern matched in the scanned data, including offset and length information.



46
47
48
# File 'lib/yara/scan_result.rb', line 46

def pattern_matches
  @pattern_matches
end

#rule_metaObject (readonly)

Public: Hash of metadata key-value pairs extracted from the rule.



37
38
39
# File 'lib/yara/scan_result.rb', line 37

def rule_meta
  @rule_meta
end

#rule_nameObject (readonly)

Public: The name identifier of the matched rule.



31
32
33
# File 'lib/yara/scan_result.rb', line 31

def rule_name
  @rule_name
end

#rule_ptrObject (readonly)

Public: FFI pointer to the underlying YRX_RULE structure.



34
35
36
# File 'lib/yara/scan_result.rb', line 34

def rule_ptr
  @rule_ptr
end

#rule_stringsObject (readonly)

Public: Hash of string pattern names and their values from the rule.



40
41
42
# File 'lib/yara/scan_result.rb', line 40

def rule_strings
  @rule_strings
end

#tagsObject (readonly)

Public: Array of rule tags for categorization and organization.

Tags are labels attached to rules that help categorize and organize rule sets. Common tags include malware family names, platforms, or behavior categories.



53
54
55
# File 'lib/yara/scan_result.rb', line 53

def tags
  @tags
end

Instance Method Details

#all_matchesObject

Public: Get all match locations as a flattened array.

This method returns all pattern matches across all patterns as a single array, sorted by offset. Useful for getting an overview of all match locations in the data.

Examples

# Get all matches sorted by location
all_matches = result.all_matches.sort_by(&:offset)
all_matches.each { |m| puts "Match at #{m.offset}" }

Returns an Array of PatternMatch objects sorted by offset.



167
168
169
# File 'lib/yara/scan_result.rb', line 167

def all_matches
  @pattern_matches.values.flatten.sort_by(&:offset)
end

#extract_namespaceObject

Internal: Extract rule namespace using YARA-X API.

This method uses the YARA-X C API to access the namespace that contains this rule. Namespaces provide logical grouping and avoid naming conflicts.

Returns nothing (modifies @namespace attribute).



470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/yara/scan_result.rb', line 470

def extract_namespace
  return unless @rule_ptr && !@rule_ptr.null?

  # Get namespace information
  ns_ptr = ::FFI::MemoryPointer.new(:pointer)
  len_ptr = ::FFI::MemoryPointer.new(:size_t)

  result = Yara::FFI.yrx_rule_namespace(@rule_ptr, ns_ptr, len_ptr)
  return unless result == Yara::FFI::YRX_SUCCESS

  namespace_ptr = ns_ptr.get_pointer(0)
  return if namespace_ptr.nil? || namespace_ptr.null?

  namespace_len = len_ptr.get_ulong(0)
  @namespace = namespace_len > 0 ? namespace_ptr.read_string(namespace_len) : nil
rescue
  # Set to nil if extraction fails
  @namespace = nil
end

#extract_pattern_matchesObject

Internal: Extract detailed pattern match information using YARA-X API.

This method uses the YARA-X C API to iterate through all patterns defined in the matched rule and collect detailed match information including exact offsets and lengths for each match.

This replaces the need to parse pattern information from rule source code and provides precise forensic data about what matched and where.

Returns nothing (modifies @pattern_matches hash).



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/yara/scan_result.rb', line 330

def extract_pattern_matches
  return unless @rule_ptr && !@rule_ptr.null?

  # Collect pattern match data by iterating through patterns
  pattern_callback = proc do |pattern_ptr, user_data|
    next if pattern_ptr.nil? || pattern_ptr.null?

    # Get pattern identifier
    ident_ptr = ::FFI::MemoryPointer.new(:pointer)
    len_ptr = ::FFI::MemoryPointer.new(:size_t)

    result = Yara::FFI.yrx_pattern_identifier(pattern_ptr, ident_ptr, len_ptr)
    next unless result == Yara::FFI::YRX_SUCCESS

    identifier_ptr = ident_ptr.get_pointer(0)
    next if identifier_ptr.nil? || identifier_ptr.null?

    identifier_len = len_ptr.get_ulong(0)
    pattern_name = identifier_ptr.read_string(identifier_len).to_sym

    # Initialize match array for this pattern
    @pattern_matches[pattern_name] ||= []

    # Iterate through matches for this pattern
    match_callback = proc do |match_ptr, match_user_data|
      next if match_ptr.nil? || match_ptr.null?

      # Extract match details using FFI struct
      match = Yara::FFI::YRX_MATCH.new(match_ptr)
      pattern_match = PatternMatch.new(match[:offset], match[:length])
      @pattern_matches[pattern_name] << pattern_match
    end

    # Iterate through all matches for this pattern
    Yara::FFI.yrx_pattern_iter_matches(pattern_ptr, match_callback, nil)
  end

  # Iterate through all patterns in the rule
  Yara::FFI.yrx_rule_iter_patterns(@rule_ptr, pattern_callback, nil)
end

#extract_structured_metadataObject

Internal: Extract structured metadata using YARA-X API.

This method uses the YARA-X C API to access rule metadata with proper type information, replacing the regex-based parsing approach with reliable structured access.

Returns nothing (modifies @rule_meta hash).



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/yara/scan_result.rb', line 378

def 
  return unless @rule_ptr && !@rule_ptr.null?

  # Callback to process each metadata entry
   = proc do |, user_data|
    next if .nil? || .null?

    begin
      # Extract metadata using FFI struct
       = Yara::FFI::YRX_METADATA.new()
      identifier_ptr = [:identifier]
      next if identifier_ptr.nil? || identifier_ptr.null?

      identifier = identifier_ptr.read_string.to_sym
      value_type = [:value_type]

      # Extract value based on type using union access
      # Note: We need to read from the value union at the correct offset
      value_ptr = .pointer + .offset_of(:value)

      case value_type
      when Yara::FFI::YRX_I64
        value = value_ptr.read_long_long
      when Yara::FFI::YRX_F64
        value = value_ptr.read_double
      when Yara::FFI::YRX_BOOLEAN
        value = value_ptr.read_char != 0
      when Yara::FFI::YRX_STRING
        string_ptr_ptr = value_ptr.read_pointer
        value = string_ptr_ptr.nil? || string_ptr_ptr.null? ? "" : string_ptr_ptr.read_string
      when Yara::FFI::YRX_BYTES
        bytes_ptr = value_ptr.read_pointer
        if bytes_ptr.nil? || bytes_ptr.null?
          value = ""
        else
          # Read the YRX_METADATA_BYTES struct
          length = bytes_ptr.read_size_t
          data_ptr = bytes_ptr.read_pointer(8) # offset past the length field
          value = length > 0 && !data_ptr.null? ? data_ptr.read_string(length) : ""
        end
      else
        value = nil  # Unknown type
      end

      @rule_meta[identifier] = value unless value.nil?
    rescue
      # Skip problematic metadata entries rather than failing entirely
      # This ensures partial extraction works even if some entries have issues
    end
  end

  # Iterate through all metadata entries
  Yara::FFI.(@rule_ptr, , nil)
rescue
  # If structured metadata extraction fails, fall back to source parsing
  # This ensures backwards compatibility
end

#extract_tagsObject

Internal: Extract rule tags using YARA-X API.

This method uses the YARA-X C API to access all tags defined for the rule. Tags provide categorization and organization capabilities for rule sets.

Returns nothing (modifies @tags array).



442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/yara/scan_result.rb', line 442

def extract_tags
  return unless @rule_ptr && !@rule_ptr.null?

  # Callback to process each tag
  tag_callback = proc do |tag_ptr, user_data|
    next if tag_ptr.nil? || tag_ptr.null?

    begin
      tag = tag_ptr.read_string
      @tags << tag unless tag.empty?
    rescue
      # Skip problematic tags rather than failing entirely
    end
  end

  # Iterate through all tags
  Yara::FFI.yrx_rule_iter_tags(@rule_ptr, tag_callback, nil)

  # If iteration fails, ensure @tags is at least an empty array
  @tags ||= []
end

#has_tag?(tag) ⇒ Boolean

Public: Check if the rule has a specific tag.

This method checks whether the rule includes the specified tag. Tag comparison is case-sensitive.

tag - A String representing the tag to check for

Examples

if result.has_tag?("malware")
  puts "This rule is tagged as malware"
end

Returns a Boolean indicating whether the rule has the tag.

Returns:

  • (Boolean)


203
204
205
206
# File 'lib/yara/scan_result.rb', line 203

def has_tag?(tag)
  return false if tag.nil?
  @tags.include?(tag.to_s)
end

#match?Boolean

Public: Check if this result represents a rule match.

Examples

if result.match?
  puts "Rule #{result.rule_name} matched!"
end

Returns a Boolean indicating whether the rule matched.

Returns:

  • (Boolean)


116
117
118
# File 'lib/yara/scan_result.rb', line 116

def match?
  @is_match
end

#matches_for_pattern(pattern_name) ⇒ Object

Public: Get all matches for a specific pattern by name.

This method returns an array of PatternMatch objects for the specified pattern identifier, or an empty array if the pattern didn’t match or doesn’t exist.

pattern_name - A String or Symbol identifying the pattern (e.g., “$text1”)

Examples

# Get matches for a specific pattern
matches = result.matches_for_pattern("$suspicious_string")
matches.each { |m| puts "Found at offset #{m.offset}" }

Returns an Array of PatternMatch objects.



135
136
137
138
# File 'lib/yara/scan_result.rb', line 135

def matches_for_pattern(pattern_name)
  key = pattern_name.is_a?(Symbol) ? pattern_name : pattern_name.to_sym
  @pattern_matches[key] || []
end

#metadata_bool(key) ⇒ Object

Public: Get a boolean metadata value by key.

This method provides a convenient way to access boolean metadata with automatic type checking.

key - A String or Symbol identifying the metadata key

Examples

result.(:active)  # => true
result.(:enabled) # => false

Returns a Boolean value, or nil if key doesn’t exist or isn’t a boolean.



297
298
299
300
# File 'lib/yara/scan_result.rb', line 297

def (key)
  value = (key)
  [true, false].include?(value) ? value : nil
end

#metadata_float(key) ⇒ Object

Public: Get a float metadata value by key.

This method provides a convenient way to access float metadata with automatic type checking.

key - A String or Symbol identifying the metadata key

Examples

result.(:confidence) # => 0.95
result.(:ratio)      # => 3.14

Returns a Float value, or nil if key doesn’t exist or isn’t a float.



315
316
317
318
# File 'lib/yara/scan_result.rb', line 315

def (key)
  value = (key)
  value.is_a?(Float) ? value : nil
end

#metadata_int(key) ⇒ Object

Public: Get an integer metadata value by key.

This method provides a convenient way to access integer metadata with automatic type checking.

key - A String or Symbol identifying the metadata key

Examples

result.(:severity)   # => 8
result.(:version)    # => 2

Returns an Integer value, or nil if key doesn’t exist or isn’t an integer.



261
262
263
264
# File 'lib/yara/scan_result.rb', line 261

def (key)
  value = (key)
  value.is_a?(Integer) ? value : nil
end

#metadata_string(key) ⇒ Object

Public: Get a string metadata value by key.

This method provides a convenient way to access string metadata with automatic type checking.

key - A String or Symbol identifying the metadata key

Examples

result.(:author)      # => "Security Team"
result.(:description) # => "Detects malware"

Returns a String value, or nil if key doesn’t exist or isn’t a string.



279
280
281
282
# File 'lib/yara/scan_result.rb', line 279

def (key)
  value = (key)
  value.is_a?(String) ? value : nil
end

#metadata_value(key) ⇒ Object

Public: Get a typed metadata value by key.

This method provides type-safe access to metadata values, returning the actual Ruby type (String, Integer, Boolean, Float) instead of requiring manual type conversion.

key - A String or Symbol identifying the metadata key

Examples

result.(:severity)    # => 8 (Integer)
result.("author")     # => "Security Team" (String)
result.(:active)      # => true (Boolean)

Returns the metadata value in its native Ruby type, or nil if not found.



243
244
245
246
# File 'lib/yara/scan_result.rb', line 243

def (key)
  return nil if key.nil?
  @rule_meta[key.to_sym]
end

#parse_meta_value(value) ⇒ Object

Internal: Parse and convert metadata values to appropriate Ruby types.

This method handles basic type conversion for metadata values extracted from rule source code. It recognizes quoted strings, boolean literals, and numeric values, converting them to appropriate Ruby types.

value - A String containing the raw metadata value from rule source

Examples

parse_meta_value('"hello"')  # => "hello"
parse_meta_value('true')     # => true
parse_meta_value('42')       # => 42
parse_meta_value('other')    # => "other"

Returns the parsed value in the appropriate Ruby type.



589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'lib/yara/scan_result.rb', line 589

def parse_meta_value(value)
  case value
  when /^".*"$/
    value[1...-1] # Remove quotes
  when /^true$/i
    true
  when /^false$/i
    false
  when /^\d+$/
    value.to_i
  else
    value
  end
end

#parse_metadata_from_sourceObject

Internal: Parse metadata from the original rule source code.

This method uses regular expressions to extract key-value pairs from the rule’s meta section. It handles string, boolean, and numeric values with basic type conversion. This is a temporary implementation until YARA-X provides direct API access to rule metadata.

Examples

# Given rule source with:
# meta:
#   author = "security_team"
#   version = 1
#   active = true

result.rule_meta[:author]  # => "security_team"
result.rule_meta[:version] # => 1
result.rule_meta[:active]  # => true

Returns nothing (modifies @rule_meta hash).



510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
# File 'lib/yara/scan_result.rb', line 510

def 
  return unless @rule_source

  # Extract metadata section more carefully
  if @rule_source =~ /meta:\s*(.*?)(?:strings:|condition:)/m
    meta_section = $1.strip

    # Parse each line in the meta section
    meta_section.split("\n").each do |line|
      line = line.strip
      next if line.empty?

      if line =~ /^(\w+)\s*=\s*(.+)$/
        key, value = $1, $2
        parsed_value = parse_meta_value(value.strip)
        @rule_meta[key.to_sym] = parsed_value
      end
    end
  end
end

#parse_strings_from_sourceObject

Internal: Parse string patterns from the original rule source code.

This method uses regular expressions to extract pattern definitions from the rule’s strings section. It captures both the pattern variable names (like $string1) and their values, cleaning up quotes and regex delimiters. This is a temporary implementation until YARA-X provides direct API access.

Examples

# Given rule source with:
# strings:
#   $text = "hello world"
#   $regex = /pattern[0-9]+/
#   $hex = { 41 42 43 }

result.rule_strings[:$text]  # => "hello world"
result.rule_strings[:$regex] # => "pattern[0-9]+"
result.rule_strings[:$hex]   # => "{ 41 42 43 }"

Returns nothing (modifies @rule_strings hash).



551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
# File 'lib/yara/scan_result.rb', line 551

def parse_strings_from_source
  return unless @rule_source

  # Extract strings section more carefully
  if @rule_source =~ /strings:\s*(.*?)(?:condition:)/m
    strings_section = $1.strip

    # Parse each line in the strings section
    strings_section.split("\n").each do |line|
      line = line.strip
      next if line.empty?

      if line =~ /^(\$\w+)\s*=\s*(.+)$/
        name, pattern = $1, $2
        # Clean up the pattern (remove quotes, regex delimiters)
        cleaned_pattern = pattern.strip.gsub(/^["\/]|["\/]$/, '')
        @rule_strings[name.to_sym] = cleaned_pattern
      end
    end
  end
end

#pattern_matched?(pattern_name) ⇒ Boolean

Public: Check if a specific pattern had any matches.

This convenience method checks whether the specified pattern identifier had any matches during scanning.

pattern_name - A String or Symbol identifying the pattern

Examples

if result.pattern_matched?("$malware_signature")
  puts "Malware signature detected!"
end

Returns a Boolean indicating whether the pattern matched.

Returns:

  • (Boolean)


185
186
187
# File 'lib/yara/scan_result.rb', line 185

def pattern_matched?(pattern_name)
  matches_for_pattern(pattern_name).any?
end

#qualified_nameObject

Public: Get the qualified rule name including namespace.

This method returns the fully qualified rule name, including the namespace if present. For rules in the default namespace, this is the same as rule_name.

Examples

result.qualified_name  # => "malware.suspicious_behavior"
# or just "rule_name" if no namespace

Returns a String containing the qualified rule name.



220
221
222
223
224
225
226
# File 'lib/yara/scan_result.rb', line 220

def qualified_name
  if @namespace && !@namespace.empty?
    "#{@namespace}.#{@rule_name}"
  else
    @rule_name
  end
end

#total_matchesObject

Public: Get the total number of pattern matches across all patterns.

This convenience method counts the total matches across all patterns that triggered for this rule.

Examples

puts "Rule matched with #{result.total_matches} pattern matches"

Returns an Integer count of total matches.



150
151
152
# File 'lib/yara/scan_result.rb', line 150

def total_matches
  @pattern_matches.values.map(&:size).sum
end