Class: Recog::Fingerprint

Inherits:
Object
  • Object
show all
Defined in:
lib/recog/fingerprint.rb,
lib/recog/fingerprint/regexp_factory.rb

Overview

A fingerprint that can be matched against a particular kind of fingerprintable data, e.g. an HTTP Server header

Defined Under Namespace

Modules: RegexpFactory Classes: Test

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(xml, match_key = nil, protocol = nil) ⇒ Fingerprint

Returns a new instance of Fingerprint.

Parameters:

  • xml (Nokogiri::XML::Element)
  • match_key (String) (defaults to: nil)

    See Recog::DB

  • protocol (String) (defaults to: nil)

    Protocol such as ftp, mssql, http, etc.



32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/recog/fingerprint.rb', line 32

def initialize(xml, match_key=nil, protocol=nil)
  @match_key = match_key
  @protocol = protocol
  @name   = parse_description(xml)
  @regex  = create_regexp(xml)
  @params = {}
  @tests = []

  @protocol.downcase! if @protocol
  parse_examples(xml)
  parse_params(xml)
end

Instance Attribute Details

#nameString (readonly)

A human readable name describing this fingerprint

Returns:

  • (String)

    Contents of the source XML's description tag



11
12
13
# File 'lib/recog/fingerprint.rb', line 11

def name
  @name
end

#paramsHash<String,Array> (readonly)

Collection of indexes for capture groups created by #match

Returns:

  • (Hash<String,Array>)

    Keys are things like "os.name", values are a two element Array. The first element is an index for the capture group that returns that thing. If the index is 0, the second element is a static value for that thing; otherwise it is undefined.



22
23
24
# File 'lib/recog/fingerprint.rb', line 22

def params
  @params
end

#regexRegexp (readonly)

Regular expression pulled from the DB xml file.

Returns:

  • (Regexp)

    the Regexp to try when calling #match

See Also:

  • #create_regexp


17
18
19
# File 'lib/recog/fingerprint.rb', line 17

def regex
  @regex
end

#testsvoid (readonly)

This method returns an undefined value.

Collection of example strings that should #match our #regex



27
28
29
# File 'lib/recog/fingerprint.rb', line 27

def tests
  @tests
end

Instance Method Details

#match(match_string) ⇒ Hash?

Attempt to match the given string.

Parameters:

  • match_string (String)

Returns:

  • (Hash, nil)

    Keys will be host, service, and os attributes



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/recog/fingerprint.rb', line 58

def match(match_string)
  # match_string.force_encoding('BINARY') if match_string
  begin
    match_data = @regex.match(match_string)
  rescue Encoding::CompatibilityError => e
    begin
      # Replace invalid UTF-8 characters with spaces, just as DAP does.
      encoded_str = match_string.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => '')
      match_data = @regex.match(encoded_str)
    rescue Exception => e
      output_diag_data('Exception while re-encoding match_string to UTF-8', match_string, e)
    end
  rescue Exception => e
    output_diag_data('Exception while running regex against match_string', match_string, e)
  end
  return if match_data.nil?

  result = { 'matched' => @name }
  @params.each_pair do |k,v|
    pos = v[0]
    if pos == 0
      # A match offset of 0 means this param has a hardcoded value
      result[k] = v[1]
    else
      # A match offset other than 0 means the value should come from
      # the corresponding match result index
      result[k] = match_data[ pos ]
    end
  end

  # Use the protocol specified in the XML database if there isn't one
  # provided as part of this fingerprint.
  if @protocol
    unless result['service.protocol']
      result['service.protocol'] = @protocol
    end
  end

  result['fingerprint_db'] = @match_key if @match_key

  return result
end

#output_diag_data(message, data, exception) ⇒ Object



45
46
47
48
49
50
51
52
# File 'lib/recog/fingerprint.rb', line 45

def output_diag_data(message, data, exception)
  STDERR.puts message
  STDERR.puts exception.inspect
  STDERR.puts "Length:   #{data.length}"
  STDERR.puts "Encoding: #{data.encoding}"
  STDERR.puts "Problematic data:\n#{data}"
  STDERR.puts "Raw bytes:\n#{data.pretty_inspect}\n"
end

#verify_params {|status, message| ... } ⇒ Object

Ensure all the #params are valid

Yield Parameters:

  • status (Symbol)

    One of :warn, :fail, or :success to indicate whether a param is valid

  • message (String)

    A human-readable string explaining the status



107
108
109
110
111
112
113
114
115
116
117
# File 'lib/recog/fingerprint.rb', line 107

def verify_params(&block)
  return if params.empty?
  params.each do |param_name, pos_value|
    pos, value = pos_value
    if pos > 0 && !value.to_s.empty?
      yield :fail, "'#{@name}'s #{param_name} is a non-zero pos but specifies a value of '#{value}'"
    elsif pos == 0 && value.to_s.empty?
      yield :fail, "'#{@name}'s #{param_name} is not a capture (pos=0) but doesn't specify a value"
    end
  end
end

#verify_tests {|status, message| ... } ⇒ Object

Ensure all the #tests actually match the fingerprint and return the expected capture groups.

Yield Parameters:

  • status (Symbol)

    One of :warn, :fail, or :success to indicate whether a test worked

  • message (String)

    A human-readable string explaining the status



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/recog/fingerprint.rb', line 126

def verify_tests(&block)
  if tests.size == 0
    yield :warn, "'#{@name}' has no test cases"
  end

  tests.each do |test|
    result = match(test.content)
    if result.nil?
      yield :fail, "'#{@name}' failed to match #{test.content.inspect} with #{@regex}'"
      next
    end

    message = test
    status = :success
    # Ensure that all the attributes as provided by the example were parsed
    # out correctly and match the capture group values we expect.
    test.attributes.each do |k, v|
      next if k == '_encoding'
      if !result.has_key?(k) || result[k] != v
        message = "'#{@name}' failed to find expected capture group #{k} '#{v}'. Result was #{result[k]}"
        status = :fail
        break
      end
    end
    yield status, message
  end
end