yara-ffi 3.1.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,101 +1,603 @@
1
1
  module Yara
2
+ # Public: Represents a single rule match result from YARA scanning.
3
+ #
4
+ # A ScanResult contains information about a YARA rule that matched during
5
+ # scanning, including the rule name, metadata, string patterns, and detailed
6
+ # pattern match information. This class provides access to rule information
7
+ # extracted from both the YARA-X API and parsed rule source code.
8
+ #
9
+ # The enhanced version provides detailed pattern match information including
10
+ # exact offsets and lengths of each pattern match, allowing for precise
11
+ # forensic analysis and data extraction.
12
+ #
13
+ # Examples
14
+ #
15
+ # # Typically created by Scanner during scanning
16
+ # scanner.scan(data) do |result|
17
+ # puts "Matched rule: #{result.rule_name}"
18
+ # puts "Author: #{result.rule_meta[:author]}"
19
+ #
20
+ # # New: Access detailed pattern matches
21
+ # result.pattern_matches.each do |pattern_name, matches|
22
+ # puts "Pattern #{pattern_name}: #{matches.size} matches"
23
+ # matches.each do |match|
24
+ # matched_text = data[match.offset, match.length]
25
+ # puts " At offset #{match.offset}: '#{matched_text}'"
26
+ # end
27
+ # end
28
+ # end
2
29
  class ScanResult
3
- RULE_MATCHING = 1
4
- RULE_NOT_MATCHING = 2
30
+ # Public: The name identifier of the matched rule.
31
+ attr_reader :rule_name
5
32
 
6
- META_FLAGS_LAST_IN_RULE = 1
33
+ # Public: FFI pointer to the underlying YRX_RULE structure.
34
+ attr_reader :rule_ptr
7
35
 
8
- META_TYPE_INTEGER = 1
9
- # META_TYPE_STRING = 2
10
- META_TYPE_BOOLEAN = 3
36
+ # Public: Hash of metadata key-value pairs extracted from the rule.
37
+ attr_reader :rule_meta
11
38
 
12
- STRING_FLAGS_LAST_IN_RULE = 0
39
+ # Public: Hash of string pattern names and their values from the rule.
40
+ attr_reader :rule_strings
13
41
 
14
- attr_reader :callback_type, :rule
42
+ # Public: Hash of pattern names to arrays of PatternMatch objects.
43
+ #
44
+ # This provides detailed information about exactly where each pattern
45
+ # matched in the scanned data, including offset and length information.
46
+ attr_reader :pattern_matches
15
47
 
16
- def initialize(callback_type, rule, user_data)
17
- @callback_type = callback_type
18
- @rule = rule
19
- @rule_meta = extract_rule_meta
20
- @rule_strings = extract_rule_strings
21
- @user_data_number = user_data[:number]
48
+ # Public: Array of rule tags for categorization and organization.
49
+ #
50
+ # Tags are labels attached to rules that help categorize and organize
51
+ # rule sets. Common tags include malware family names, platforms,
52
+ # or behavior categories.
53
+ attr_reader :tags
54
+
55
+ # Public: Namespace of the rule, if defined.
56
+ #
57
+ # YARA rules can be organized into namespaces to avoid naming conflicts
58
+ # and provide logical grouping. This contains the namespace name or
59
+ # nil if the rule is in the default namespace.
60
+ attr_reader :namespace
61
+
62
+ # Public: Initialize a new ScanResult.
63
+ #
64
+ # This constructor is typically called internally by Scanner when a rule
65
+ # matches during scanning. It extracts available information from both
66
+ # the YARA-X API and the original rule source code, including detailed
67
+ # pattern match information.
68
+ #
69
+ # rule_name - A String containing the rule identifier/name
70
+ # rule_ptr - An FFI Pointer to the YRX_RULE structure
71
+ # is_match - A Boolean indicating if this represents a match (default true)
72
+ # rule_source - An optional String containing the original rule source for parsing
73
+ # scanned_data - An optional String containing the data that was scanned (needed for pattern matches)
74
+ #
75
+ # Examples
76
+ #
77
+ # # Typically created internally by Scanner
78
+ # result = ScanResult.new("MyRule", rule_ptr, true, rule_source, scanned_data)
79
+ def initialize(rule_name, rule_ptr, is_match = true, rule_source = nil, scanned_data = nil)
80
+ @rule_name = rule_name
81
+ @rule_ptr = rule_ptr
82
+ @is_match = is_match
83
+ @rule_source = rule_source
84
+ @scanned_data = scanned_data
85
+ @rule_meta = {}
86
+ @rule_strings = {}
87
+ @pattern_matches = {}
88
+ @tags = []
89
+ @namespace = nil
90
+
91
+ # Extract information using YARA-X API when rule pointer is available
92
+ if @rule_ptr && !@rule_ptr.null?
93
+ # TODO: Re-enable structured metadata after fixing union handling
94
+ # extract_structured_metadata
95
+ extract_tags
96
+ extract_namespace
97
+ extract_pattern_matches
98
+ end
99
+
100
+ # Parse metadata and strings from source (primary method for now)
101
+ if @rule_source
102
+ parse_metadata_from_source
103
+ parse_strings_from_source
104
+ end
22
105
  end
23
106
 
24
- attr_reader :rule_meta, :rule_strings, :user_data_number
107
+ # Public: Check if this result represents a rule match.
108
+ #
109
+ # Examples
110
+ #
111
+ # if result.match?
112
+ # puts "Rule #{result.rule_name} matched!"
113
+ # end
114
+ #
115
+ # Returns a Boolean indicating whether the rule matched.
116
+ def match?
117
+ @is_match
118
+ end
25
119
 
26
- def rule_name
27
- @rule[:identifier]
120
+ # Public: Get all matches for a specific pattern by name.
121
+ #
122
+ # This method returns an array of PatternMatch objects for the specified
123
+ # pattern identifier, or an empty array if the pattern didn't match or
124
+ # doesn't exist.
125
+ #
126
+ # pattern_name - A String or Symbol identifying the pattern (e.g., "$text1")
127
+ #
128
+ # Examples
129
+ #
130
+ # # Get matches for a specific pattern
131
+ # matches = result.matches_for_pattern("$suspicious_string")
132
+ # matches.each { |m| puts "Found at offset #{m.offset}" }
133
+ #
134
+ # Returns an Array of PatternMatch objects.
135
+ def matches_for_pattern(pattern_name)
136
+ key = pattern_name.is_a?(Symbol) ? pattern_name : pattern_name.to_sym
137
+ @pattern_matches[key] || []
28
138
  end
29
139
 
30
- def scan_complete?
31
- callback_type == SCAN_FINISHED
140
+ # Public: Get the total number of pattern matches across all patterns.
141
+ #
142
+ # This convenience method counts the total matches across all patterns
143
+ # that triggered for this rule.
144
+ #
145
+ # Examples
146
+ #
147
+ # puts "Rule matched with #{result.total_matches} pattern matches"
148
+ #
149
+ # Returns an Integer count of total matches.
150
+ def total_matches
151
+ @pattern_matches.values.map(&:size).sum
32
152
  end
33
153
 
34
- def rule_outcome?
35
- [RULE_MATCHING, RULE_NOT_MATCHING].include?(callback_type)
154
+ # Public: Get all match locations as a flattened array.
155
+ #
156
+ # This method returns all pattern matches across all patterns as a single
157
+ # array, sorted by offset. Useful for getting an overview of all match
158
+ # locations in the data.
159
+ #
160
+ # Examples
161
+ #
162
+ # # Get all matches sorted by location
163
+ # all_matches = result.all_matches.sort_by(&:offset)
164
+ # all_matches.each { |m| puts "Match at #{m.offset}" }
165
+ #
166
+ # Returns an Array of PatternMatch objects sorted by offset.
167
+ def all_matches
168
+ @pattern_matches.values.flatten.sort_by(&:offset)
36
169
  end
37
170
 
38
- def match?
39
- callback_type == RULE_MATCHING
40
- end
41
-
42
- private
43
-
44
- def extract_rule_meta
45
- metas = {}
46
- reading_metas = true
47
- meta_index = 0
48
- meta_pointer = @rule[:metas]
49
- while reading_metas do
50
- meta = YrMeta.new(meta_pointer + meta_index * YrMeta.size)
51
- metas.merge!(meta_as_hash(meta))
52
- flags = meta[:flags]
53
- if flags == META_FLAGS_LAST_IN_RULE
54
- reading_metas = false
55
- else
56
- meta_index += 1
171
+ # Public: Check if a specific pattern had any matches.
172
+ #
173
+ # This convenience method checks whether the specified pattern identifier
174
+ # had any matches during scanning.
175
+ #
176
+ # pattern_name - A String or Symbol identifying the pattern
177
+ #
178
+ # Examples
179
+ #
180
+ # if result.pattern_matched?("$malware_signature")
181
+ # puts "Malware signature detected!"
182
+ # end
183
+ #
184
+ # Returns a Boolean indicating whether the pattern matched.
185
+ def pattern_matched?(pattern_name)
186
+ matches_for_pattern(pattern_name).any?
187
+ end
188
+
189
+ # Public: Check if the rule has a specific tag.
190
+ #
191
+ # This method checks whether the rule includes the specified tag.
192
+ # Tag comparison is case-sensitive.
193
+ #
194
+ # tag - A String representing the tag to check for
195
+ #
196
+ # Examples
197
+ #
198
+ # if result.has_tag?("malware")
199
+ # puts "This rule is tagged as malware"
200
+ # end
201
+ #
202
+ # Returns a Boolean indicating whether the rule has the tag.
203
+ def has_tag?(tag)
204
+ return false if tag.nil?
205
+ @tags.include?(tag.to_s)
206
+ end
207
+
208
+ # Public: Get the qualified rule name including namespace.
209
+ #
210
+ # This method returns the fully qualified rule name, including the
211
+ # namespace if present. For rules in the default namespace, this
212
+ # is the same as rule_name.
213
+ #
214
+ # Examples
215
+ #
216
+ # result.qualified_name # => "malware.suspicious_behavior"
217
+ # # or just "rule_name" if no namespace
218
+ #
219
+ # Returns a String containing the qualified rule name.
220
+ def qualified_name
221
+ if @namespace && !@namespace.empty?
222
+ "#{@namespace}.#{@rule_name}"
223
+ else
224
+ @rule_name
225
+ end
226
+ end
227
+
228
+ # Public: Get a typed metadata value by key.
229
+ #
230
+ # This method provides type-safe access to metadata values, returning
231
+ # the actual Ruby type (String, Integer, Boolean, Float) instead of
232
+ # requiring manual type conversion.
233
+ #
234
+ # key - A String or Symbol identifying the metadata key
235
+ #
236
+ # Examples
237
+ #
238
+ # result.metadata_value(:severity) # => 8 (Integer)
239
+ # result.metadata_value("author") # => "Security Team" (String)
240
+ # result.metadata_value(:active) # => true (Boolean)
241
+ #
242
+ # Returns the metadata value in its native Ruby type, or nil if not found.
243
+ def metadata_value(key)
244
+ return nil if key.nil?
245
+ @rule_meta[key.to_sym]
246
+ end
247
+
248
+ # Public: Get an integer metadata value by key.
249
+ #
250
+ # This method provides a convenient way to access integer metadata
251
+ # with automatic type checking.
252
+ #
253
+ # key - A String or Symbol identifying the metadata key
254
+ #
255
+ # Examples
256
+ #
257
+ # result.metadata_int(:severity) # => 8
258
+ # result.metadata_int(:version) # => 2
259
+ #
260
+ # Returns an Integer value, or nil if key doesn't exist or isn't an integer.
261
+ def metadata_int(key)
262
+ value = metadata_value(key)
263
+ value.is_a?(Integer) ? value : nil
264
+ end
265
+
266
+ # Public: Get a string metadata value by key.
267
+ #
268
+ # This method provides a convenient way to access string metadata
269
+ # with automatic type checking.
270
+ #
271
+ # key - A String or Symbol identifying the metadata key
272
+ #
273
+ # Examples
274
+ #
275
+ # result.metadata_string(:author) # => "Security Team"
276
+ # result.metadata_string(:description) # => "Detects malware"
277
+ #
278
+ # Returns a String value, or nil if key doesn't exist or isn't a string.
279
+ def metadata_string(key)
280
+ value = metadata_value(key)
281
+ value.is_a?(String) ? value : nil
282
+ end
283
+
284
+ # Public: Get a boolean metadata value by key.
285
+ #
286
+ # This method provides a convenient way to access boolean metadata
287
+ # with automatic type checking.
288
+ #
289
+ # key - A String or Symbol identifying the metadata key
290
+ #
291
+ # Examples
292
+ #
293
+ # result.metadata_bool(:active) # => true
294
+ # result.metadata_bool(:enabled) # => false
295
+ #
296
+ # Returns a Boolean value, or nil if key doesn't exist or isn't a boolean.
297
+ def metadata_bool(key)
298
+ value = metadata_value(key)
299
+ [true, false].include?(value) ? value : nil
300
+ end
301
+
302
+ # Public: Get a float metadata value by key.
303
+ #
304
+ # This method provides a convenient way to access float metadata
305
+ # with automatic type checking.
306
+ #
307
+ # key - A String or Symbol identifying the metadata key
308
+ #
309
+ # Examples
310
+ #
311
+ # result.metadata_float(:confidence) # => 0.95
312
+ # result.metadata_float(:ratio) # => 3.14
313
+ #
314
+ # Returns a Float value, or nil if key doesn't exist or isn't a float.
315
+ def metadata_float(key)
316
+ value = metadata_value(key)
317
+ value.is_a?(Float) ? value : nil
318
+ end
319
+
320
+ # Internal: Extract detailed pattern match information using YARA-X API.
321
+ #
322
+ # This method uses the YARA-X C API to iterate through all patterns defined
323
+ # in the matched rule and collect detailed match information including exact
324
+ # offsets and lengths for each match.
325
+ #
326
+ # This replaces the need to parse pattern information from rule source code
327
+ # and provides precise forensic data about what matched and where.
328
+ #
329
+ # Returns nothing (modifies @pattern_matches hash).
330
+ def extract_pattern_matches
331
+ return unless @rule_ptr && !@rule_ptr.null?
332
+
333
+ # Collect pattern match data by iterating through patterns
334
+ pattern_callback = proc do |pattern_ptr, user_data|
335
+ next if pattern_ptr.nil? || pattern_ptr.null?
336
+
337
+ # Get pattern identifier
338
+ ident_ptr = ::FFI::MemoryPointer.new(:pointer)
339
+ len_ptr = ::FFI::MemoryPointer.new(:size_t)
340
+
341
+ result = Yara::FFI.yrx_pattern_identifier(pattern_ptr, ident_ptr, len_ptr)
342
+ next unless result == Yara::FFI::YRX_SUCCESS
343
+
344
+ identifier_ptr = ident_ptr.get_pointer(0)
345
+ next if identifier_ptr.nil? || identifier_ptr.null?
346
+
347
+ identifier_len = len_ptr.get_ulong(0)
348
+ pattern_name = identifier_ptr.read_string(identifier_len).to_sym
349
+
350
+ # Initialize match array for this pattern
351
+ @pattern_matches[pattern_name] ||= []
352
+
353
+ # Iterate through matches for this pattern
354
+ match_callback = proc do |match_ptr, match_user_data|
355
+ next if match_ptr.nil? || match_ptr.null?
356
+
357
+ # Extract match details using FFI struct
358
+ match = Yara::FFI::YRX_MATCH.new(match_ptr)
359
+ pattern_match = PatternMatch.new(match[:offset], match[:length])
360
+ @pattern_matches[pattern_name] << pattern_match
57
361
  end
362
+
363
+ # Iterate through all matches for this pattern
364
+ Yara::FFI.yrx_pattern_iter_matches(pattern_ptr, match_callback, nil)
58
365
  end
59
- metas
60
- end
61
-
62
- def extract_rule_strings
63
- strings = {}
64
- reading_strings = true
65
- string_index = 0
66
- string_pointer = @rule[:strings]
67
- while reading_strings do
68
- string = YrString.new(string_pointer + string_index * YrString.size)
69
- string_length = string[:length]
70
- flags = string[:flags]
71
- if flags == STRING_FLAGS_LAST_IN_RULE
72
- reading_strings = false
73
- else
74
- strings.merge!(string_as_hash(string)) unless string_length == 0
75
- string_index += 1
366
+
367
+ # Iterate through all patterns in the rule
368
+ Yara::FFI.yrx_rule_iter_patterns(@rule_ptr, pattern_callback, nil)
369
+ end
370
+
371
+ # Internal: Extract structured metadata using YARA-X API.
372
+ #
373
+ # This method uses the YARA-X C API to access rule metadata with proper
374
+ # type information, replacing the regex-based parsing approach with
375
+ # reliable structured access.
376
+ #
377
+ # Returns nothing (modifies @rule_meta hash).
378
+ def extract_structured_metadata
379
+ return unless @rule_ptr && !@rule_ptr.null?
380
+
381
+ # Callback to process each metadata entry
382
+ metadata_callback = proc do |metadata_ptr, user_data|
383
+ next if metadata_ptr.nil? || metadata_ptr.null?
384
+
385
+ begin
386
+ # Extract metadata using FFI struct
387
+ metadata = Yara::FFI::YRX_METADATA.new(metadata_ptr)
388
+ identifier_ptr = metadata[:identifier]
389
+ next if identifier_ptr.nil? || identifier_ptr.null?
390
+
391
+ identifier = identifier_ptr.read_string.to_sym
392
+ value_type = metadata[:value_type]
393
+
394
+ # Extract value based on type using union access
395
+ # Note: We need to read from the value union at the correct offset
396
+ value_ptr = metadata.pointer + metadata.offset_of(:value)
397
+
398
+ case value_type
399
+ when Yara::FFI::YRX_I64
400
+ value = value_ptr.read_long_long
401
+ when Yara::FFI::YRX_F64
402
+ value = value_ptr.read_double
403
+ when Yara::FFI::YRX_BOOLEAN
404
+ value = value_ptr.read_char != 0
405
+ when Yara::FFI::YRX_STRING
406
+ string_ptr_ptr = value_ptr.read_pointer
407
+ value = string_ptr_ptr.nil? || string_ptr_ptr.null? ? "" : string_ptr_ptr.read_string
408
+ when Yara::FFI::YRX_BYTES
409
+ bytes_ptr = value_ptr.read_pointer
410
+ if bytes_ptr.nil? || bytes_ptr.null?
411
+ value = ""
412
+ else
413
+ # Read the YRX_METADATA_BYTES struct
414
+ length = bytes_ptr.read_size_t
415
+ data_ptr = bytes_ptr.read_pointer(8) # offset past the length field
416
+ value = length > 0 && !data_ptr.null? ? data_ptr.read_string(length) : ""
417
+ end
418
+ else
419
+ value = nil # Unknown type
420
+ end
421
+
422
+ @rule_meta[identifier] = value unless value.nil?
423
+ rescue
424
+ # Skip problematic metadata entries rather than failing entirely
425
+ # This ensures partial extraction works even if some entries have issues
76
426
  end
77
427
  end
78
- strings
428
+
429
+ # Iterate through all metadata entries
430
+ Yara::FFI.yrx_rule_iter_metadata(@rule_ptr, metadata_callback, nil)
431
+ rescue
432
+ # If structured metadata extraction fails, fall back to source parsing
433
+ # This ensures backwards compatibility
79
434
  end
80
435
 
81
- def meta_as_hash(meta)
82
- value = meta_value(meta[:string], meta[:integer], meta[:type])
83
- { meta[:identifier].to_sym => value }
436
+ # Internal: Extract rule tags using YARA-X API.
437
+ #
438
+ # This method uses the YARA-X C API to access all tags defined for the rule.
439
+ # Tags provide categorization and organization capabilities for rule sets.
440
+ #
441
+ # Returns nothing (modifies @tags array).
442
+ def extract_tags
443
+ return unless @rule_ptr && !@rule_ptr.null?
444
+
445
+ # Callback to process each tag
446
+ tag_callback = proc do |tag_ptr, user_data|
447
+ next if tag_ptr.nil? || tag_ptr.null?
448
+
449
+ begin
450
+ tag = tag_ptr.read_string
451
+ @tags << tag unless tag.empty?
452
+ rescue
453
+ # Skip problematic tags rather than failing entirely
454
+ end
455
+ end
456
+
457
+ # Iterate through all tags
458
+ Yara::FFI.yrx_rule_iter_tags(@rule_ptr, tag_callback, nil)
459
+
460
+ # If iteration fails, ensure @tags is at least an empty array
461
+ @tags ||= []
84
462
  end
85
463
 
86
- def string_as_hash(yr_string)
87
- string_pointer = yr_string[:string]
88
- string_identifier = yr_string[:identifier]
89
- { string_identifier.to_sym => string_pointer.read_string }
464
+ # Internal: Extract rule namespace using YARA-X API.
465
+ #
466
+ # This method uses the YARA-X C API to access the namespace that contains
467
+ # this rule. Namespaces provide logical grouping and avoid naming conflicts.
468
+ #
469
+ # Returns nothing (modifies @namespace attribute).
470
+ def extract_namespace
471
+ return unless @rule_ptr && !@rule_ptr.null?
472
+
473
+ # Get namespace information
474
+ ns_ptr = ::FFI::MemoryPointer.new(:pointer)
475
+ len_ptr = ::FFI::MemoryPointer.new(:size_t)
476
+
477
+ result = Yara::FFI.yrx_rule_namespace(@rule_ptr, ns_ptr, len_ptr)
478
+ return unless result == Yara::FFI::YRX_SUCCESS
479
+
480
+ namespace_ptr = ns_ptr.get_pointer(0)
481
+ return if namespace_ptr.nil? || namespace_ptr.null?
482
+
483
+ namespace_len = len_ptr.get_ulong(0)
484
+ @namespace = namespace_len > 0 ? namespace_ptr.read_string(namespace_len) : nil
485
+ rescue
486
+ # Set to nil if extraction fails
487
+ @namespace = nil
488
+ end
489
+
490
+ # Internal: Parse metadata from the original rule source code.
491
+ #
492
+ # This method uses regular expressions to extract key-value pairs from
493
+ # the rule's meta section. It handles string, boolean, and numeric values
494
+ # with basic type conversion. This is a temporary implementation until
495
+ # YARA-X provides direct API access to rule metadata.
496
+ #
497
+ # Examples
498
+ #
499
+ # # Given rule source with:
500
+ # # meta:
501
+ # # author = "security_team"
502
+ # # version = 1
503
+ # # active = true
504
+ #
505
+ # result.rule_meta[:author] # => "security_team"
506
+ # result.rule_meta[:version] # => 1
507
+ # result.rule_meta[:active] # => true
508
+ #
509
+ # Returns nothing (modifies @rule_meta hash).
510
+ def parse_metadata_from_source
511
+ return unless @rule_source
512
+
513
+ # Extract metadata section more carefully
514
+ if @rule_source =~ /meta:\s*(.*?)(?:strings:|condition:)/m
515
+ meta_section = $1.strip
516
+
517
+ # Parse each line in the meta section
518
+ meta_section.split("\n").each do |line|
519
+ line = line.strip
520
+ next if line.empty?
521
+
522
+ if line =~ /^(\w+)\s*=\s*(.+)$/
523
+ key, value = $1, $2
524
+ parsed_value = parse_meta_value(value.strip)
525
+ @rule_meta[key.to_sym] = parsed_value
526
+ end
527
+ end
528
+ end
529
+ end
530
+
531
+ # Internal: Parse string patterns from the original rule source code.
532
+ #
533
+ # This method uses regular expressions to extract pattern definitions from
534
+ # the rule's strings section. It captures both the pattern variable names
535
+ # (like $string1) and their values, cleaning up quotes and regex delimiters.
536
+ # This is a temporary implementation until YARA-X provides direct API access.
537
+ #
538
+ # Examples
539
+ #
540
+ # # Given rule source with:
541
+ # # strings:
542
+ # # $text = "hello world"
543
+ # # $regex = /pattern[0-9]+/
544
+ # # $hex = { 41 42 43 }
545
+ #
546
+ # result.rule_strings[:$text] # => "hello world"
547
+ # result.rule_strings[:$regex] # => "pattern[0-9]+"
548
+ # result.rule_strings[:$hex] # => "{ 41 42 43 }"
549
+ #
550
+ # Returns nothing (modifies @rule_strings hash).
551
+ def parse_strings_from_source
552
+ return unless @rule_source
553
+
554
+ # Extract strings section more carefully
555
+ if @rule_source =~ /strings:\s*(.*?)(?:condition:)/m
556
+ strings_section = $1.strip
557
+
558
+ # Parse each line in the strings section
559
+ strings_section.split("\n").each do |line|
560
+ line = line.strip
561
+ next if line.empty?
562
+
563
+ if line =~ /^(\$\w+)\s*=\s*(.+)$/
564
+ name, pattern = $1, $2
565
+ # Clean up the pattern (remove quotes, regex delimiters)
566
+ cleaned_pattern = pattern.strip.gsub(/^["\/]|["\/]$/, '')
567
+ @rule_strings[name.to_sym] = cleaned_pattern
568
+ end
569
+ end
570
+ end
90
571
  end
91
572
 
92
- def meta_value(string_value, int_value, type)
93
- if type == META_TYPE_INTEGER
94
- int_value
95
- elsif type == META_TYPE_BOOLEAN
96
- int_value == 1
573
+ # Internal: Parse and convert metadata values to appropriate Ruby types.
574
+ #
575
+ # This method handles basic type conversion for metadata values extracted
576
+ # from rule source code. It recognizes quoted strings, boolean literals,
577
+ # and numeric values, converting them to appropriate Ruby types.
578
+ #
579
+ # value - A String containing the raw metadata value from rule source
580
+ #
581
+ # Examples
582
+ #
583
+ # parse_meta_value('"hello"') # => "hello"
584
+ # parse_meta_value('true') # => true
585
+ # parse_meta_value('42') # => 42
586
+ # parse_meta_value('other') # => "other"
587
+ #
588
+ # Returns the parsed value in the appropriate Ruby type.
589
+ def parse_meta_value(value)
590
+ case value
591
+ when /^".*"$/
592
+ value[1...-1] # Remove quotes
593
+ when /^true$/i
594
+ true
595
+ when /^false$/i
596
+ false
597
+ when /^\d+$/
598
+ value.to_i
97
599
  else
98
- string_value
600
+ value
99
601
  end
100
602
  end
101
603
  end