recog 2.3.23 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +25 -16
- data/Rakefile +2 -9
- data/lib/recog/db_manager.rb +1 -1
- data/lib/recog/version.rb +1 -1
- data/{bin → recog/bin}/recog_match +0 -1
- data/{xml → recog/xml}/apache_modules.xml +0 -0
- data/{xml → recog/xml}/apache_os.xml +0 -0
- data/{xml → recog/xml}/architecture.xml +0 -0
- data/{xml → recog/xml}/dhcp_vendor_class.xml +9 -9
- data/{xml → recog/xml}/dns_versionbind.xml +0 -0
- data/{xml → recog/xml}/favicons.xml +63 -1
- data/{xml → recog/xml}/fingerprints.xsd +0 -0
- data/{xml → recog/xml}/ftp_banners.xml +0 -0
- data/{xml → recog/xml}/h323_callresp.xml +0 -0
- data/{xml → recog/xml}/hp_pjl_id.xml +0 -0
- data/{xml → recog/xml}/html_title.xml +47 -0
- data/{xml → recog/xml}/http_cookies.xml +19 -0
- data/{xml → recog/xml}/http_servers.xml +74 -1
- data/{xml → recog/xml}/http_wwwauth.xml +13 -0
- data/{xml → recog/xml}/imap_banners.xml +0 -0
- data/{xml → recog/xml}/ldap_searchresult.xml +0 -0
- data/{xml → recog/xml}/mdns_device-info_txt.xml +0 -0
- data/{xml → recog/xml}/mdns_workstation_txt.xml +0 -0
- data/{xml → recog/xml}/mysql_banners.xml +0 -0
- data/{xml → recog/xml}/mysql_error.xml +0 -0
- data/{xml → recog/xml}/nntp_banners.xml +0 -0
- data/{xml → recog/xml}/ntp_banners.xml +0 -0
- data/{xml → recog/xml}/operating_system.xml +0 -0
- data/{xml → recog/xml}/pop_banners.xml +0 -0
- data/{xml → recog/xml}/rsh_resp.xml +0 -0
- data/{xml → recog/xml}/rtsp_servers.xml +0 -0
- data/{xml → recog/xml}/sip_banners.xml +0 -0
- data/{xml → recog/xml}/sip_user_agents.xml +0 -0
- data/{xml → recog/xml}/smb_native_lm.xml +0 -0
- data/{xml → recog/xml}/smb_native_os.xml +0 -0
- data/{xml → recog/xml}/smtp_banners.xml +0 -0
- data/{xml → recog/xml}/smtp_debug.xml +0 -0
- data/{xml → recog/xml}/smtp_ehlo.xml +0 -0
- data/{xml → recog/xml}/smtp_expn.xml +0 -0
- data/{xml → recog/xml}/smtp_help.xml +0 -0
- data/{xml → recog/xml}/smtp_mailfrom.xml +0 -0
- data/{xml → recog/xml}/smtp_noop.xml +0 -0
- data/{xml → recog/xml}/smtp_quit.xml +0 -0
- data/{xml → recog/xml}/smtp_rcptto.xml +0 -0
- data/{xml → recog/xml}/smtp_rset.xml +0 -0
- data/{xml → recog/xml}/smtp_turn.xml +0 -0
- data/{xml → recog/xml}/smtp_vrfy.xml +0 -0
- data/{xml → recog/xml}/snmp_sysdescr.xml +21 -6
- data/{xml → recog/xml}/snmp_sysobjid.xml +11 -0
- data/{xml → recog/xml}/ssh_banners.xml +0 -0
- data/{xml → recog/xml}/telnet_banners.xml +34 -1
- data/{xml → recog/xml}/tls_jarm.xml +8 -0
- data/{xml → recog/xml}/x11_banners.xml +0 -0
- data/{xml → recog/xml}/x509_issuers.xml +13 -2
- data/{xml → recog/xml}/x509_subjects.xml +0 -0
- data/recog.gemspec +9 -5
- data/spec/spec_helper.rb +4 -0
- metadata +56 -145
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -37
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
- data/.github/ISSUE_TEMPLATE/fingerprint_request.md +0 -27
- data/.github/PULL_REQUEST_TEMPLATE +0 -24
- data/.github/SECURITY.md +0 -35
- data/.github/dependabot.yml +0 -8
- data/.github/workflows/ci.yml +0 -26
- data/.github/workflows/verify.yml +0 -89
- data/.gitignore +0 -23
- data/.rspec +0 -3
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/.snyk +0 -10
- data/.travis.yml +0 -25
- data/.vscode/bin/monitor-recog-fingerprints.sh +0 -54
- data/.vscode/extensions.json +0 -5
- data/.vscode/settings.json +0 -8
- data/.vscode/tasks.json +0 -77
- data/CONTRIBUTING.md +0 -278
- data/bin/recog_cleanup +0 -16
- data/bin/recog_export +0 -81
- data/bin/recog_standardize +0 -163
- data/bin/recog_verify +0 -98
- data/cpe-remap.yaml +0 -374
- data/features/data/failing_banners_fingerprints.xml +0 -20
- data/features/data/matching_banners_fingerprints.xml +0 -23
- data/features/data/multiple_banners_fingerprints.xml +0 -32
- data/features/data/no_tests.xml +0 -3
- data/features/data/sample_banner.txt +0 -2
- data/features/data/schema_failure.xml +0 -4
- data/features/data/successful_tests.xml +0 -18
- data/features/data/tests_with_failures.xml +0 -26
- data/features/data/tests_with_warnings.xml +0 -17
- data/features/match.feature +0 -36
- data/features/support/aruba.rb +0 -3
- data/features/support/env.rb +0 -6
- data/features/support/hooks.rb +0 -9
- data/features/verify.feature +0 -112
- data/identifiers/README.md +0 -70
- data/identifiers/fields.txt +0 -105
- data/identifiers/hw_device.txt +0 -86
- data/identifiers/hw_family.txt +0 -121
- data/identifiers/hw_product.txt +0 -463
- data/identifiers/os_architecture.txt +0 -10
- data/identifiers/os_device.txt +0 -77
- data/identifiers/os_family.txt +0 -235
- data/identifiers/os_product.txt +0 -357
- data/identifiers/service_family.txt +0 -249
- data/identifiers/service_product.txt +0 -778
- data/identifiers/vendor.txt +0 -859
- data/misc/convert_mysql_err +0 -61
- data/misc/order.xsl +0 -17
- data/requirements.txt +0 -2
- data/spec/lib/fingerprint_self_test_spec.rb +0 -175
- data/tools/dev/hooks/pre-commit +0 -21
- data/update_cpes.py +0 -343
data/misc/convert_mysql_err
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
# Takes the MySQL error messages from sql/share/errmsg-utf8.txt, locates the
|
4
|
-
# provided error message type (for example, ER_HOST_NOT_PRIVILEGED), then
|
5
|
-
# creates XML snippets for each locale to be used in Recog. Note that this
|
6
|
-
# cannot be used as-is to generate mysql_errors.xml, or oftentimes even parts
|
7
|
-
# -- it merely spits out XML snippets that you can start with; many will still
|
8
|
-
# need to be modified by hand.
|
9
|
-
|
10
|
-
require 'builder'
|
11
|
-
require 'open-uri'
|
12
|
-
require 'securerandom'
|
13
|
-
|
14
|
-
def generate_recog(error_name, locale, error_message)
|
15
|
-
xml = Builder::XmlMarkup.new(target: STDOUT, indent: 2)
|
16
|
-
xml.fingerprint(pattern: error_message) do
|
17
|
-
xml.description "Oracle MySQL error #{error_name} (#{locale})"
|
18
|
-
xml.example(error_message)
|
19
|
-
xml.param(pos: 0, name: 'service.vendor', value: 'Oracle')
|
20
|
-
xml.param(pos: 0, name: 'service.family', value: 'MySQL')
|
21
|
-
xml.param(pos: 0, name: 'service.product', value: 'MySQL')
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
unless ARGV.size == 2
|
26
|
-
fail "Usage: #{$PROGRAM_NAME} <path/URI for errmsg-utf8.txt> <error name>"
|
27
|
-
end
|
28
|
-
|
29
|
-
path = ARGV.first
|
30
|
-
error_name = ARGV.last
|
31
|
-
|
32
|
-
lines = IO.readlines(open(path))
|
33
|
-
|
34
|
-
fail "Nothing read from #{path}" if lines.empty?
|
35
|
-
|
36
|
-
unless (error_start = lines.find_index { |line| line.strip =~ /^#{error_name}(?:\s+\S+)?$/ })
|
37
|
-
fail "Unable to find #{error_name} in #{path}"
|
38
|
-
end
|
39
|
-
|
40
|
-
locale_map = {}
|
41
|
-
lines.slice(error_start + 1, lines.size).each do |line|
|
42
|
-
if /^\s+(?<locale>\S+)\s+"(?<error_message>.*)",?$/ =~ line
|
43
|
-
locale_map[locale] = error_message
|
44
|
-
else
|
45
|
-
break
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# Many of the error messages contain format strings. This can be problematic
|
50
|
-
# in that they need to be removed or otherwise handled as part of the 'pattern'
|
51
|
-
# attribute and appropriately filled in in any example elements. So simply try
|
52
|
-
# a rough count of the possible format strings and warn the user so that they
|
53
|
-
# can deal with it.
|
54
|
-
format_count = locale_map.values.map { |error_message| error_message.scan(/%/).size }.inject(&:+)
|
55
|
-
unless format_count == 0
|
56
|
-
warn("#{format_count} possible format strings found -- you'll need to deal with this")
|
57
|
-
end
|
58
|
-
|
59
|
-
Hash[locale_map.sort].map do |locale, error_message|
|
60
|
-
generate_recog(error_name, locale, error_message)
|
61
|
-
end
|
data/misc/order.xsl
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
<?xml version="1.0"?>
|
2
|
-
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
|
3
|
-
<xsl:output encoding="UTF-8" indent="yes" method="xml"/>
|
4
|
-
<xsl:template match="@*|node()">
|
5
|
-
<xsl:copy>
|
6
|
-
<xsl:apply-templates select="@*|node()"/>
|
7
|
-
</xsl:copy>
|
8
|
-
</xsl:template>
|
9
|
-
<xsl:template match="fingerprints/fingerprint">
|
10
|
-
<xsl:copy>
|
11
|
-
<xsl:copy-of select="@*"/>
|
12
|
-
<xsl:apply-templates select="description"/>
|
13
|
-
<xsl:apply-templates select="example"/>
|
14
|
-
<xsl:apply-templates select="param"/>
|
15
|
-
</xsl:copy>
|
16
|
-
</xsl:template>
|
17
|
-
</xsl:stylesheet>
|
data/requirements.txt
DELETED
@@ -1,175 +0,0 @@
|
|
1
|
-
require 'recog/db'
|
2
|
-
require 'regexp_parser'
|
3
|
-
require 'nokogiri'
|
4
|
-
|
5
|
-
describe Recog::DB do
|
6
|
-
let(:schema) { Nokogiri::XML::Schema(open(File.expand_path(File.join(%w(xml fingerprints.xsd))))) }
|
7
|
-
Dir[File.expand_path File.join('xml', '*.xml')].each do |xml_file_name|
|
8
|
-
|
9
|
-
describe "##{File.basename(xml_file_name)}" do
|
10
|
-
|
11
|
-
it "is valid XML" do
|
12
|
-
doc = Nokogiri::XML(open(xml_file_name))
|
13
|
-
errors = schema.validate(doc)
|
14
|
-
expect(errors).to be_empty, "#{xml_file_name} is invalid recog XML -- #{errors.inspect}"
|
15
|
-
end
|
16
|
-
|
17
|
-
db = Recog::DB.new(xml_file_name)
|
18
|
-
|
19
|
-
it "has a match key" do
|
20
|
-
expect(db.match_key).not_to be_nil
|
21
|
-
expect(db.match_key).not_to be_empty
|
22
|
-
end
|
23
|
-
|
24
|
-
it "has valid 'preference' value" do
|
25
|
-
# Reserve values below 0.10 and above 0.90 for users
|
26
|
-
# See xml/fingerprints.xsd
|
27
|
-
expect(db.preference.class).to be ::Float
|
28
|
-
expect(db.preference).to be_between(0.10, 0.90)
|
29
|
-
end
|
30
|
-
|
31
|
-
fp_descriptions = []
|
32
|
-
db.fingerprints.each_index do |i|
|
33
|
-
fp = db.fingerprints[i]
|
34
|
-
|
35
|
-
it "doesn't have a duplicate description" do
|
36
|
-
if fp_descriptions.include?(fp.name)
|
37
|
-
fail "'#{fp.name}'s description is not unique"
|
38
|
-
else
|
39
|
-
fp_descriptions << fp.name
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
context "#{fp.name}" do
|
44
|
-
param_names = []
|
45
|
-
it "has consistent os.device and hw.device" do
|
46
|
-
if fp.params['os.device'] && fp.params['hw.device'] && (fp.params['os.device'] != fp.params['hw.device'])
|
47
|
-
fail "#{fp.name} has both hw.device and os.device but with differing values"
|
48
|
-
end
|
49
|
-
end
|
50
|
-
fp.params.each do |param_name, pos_value|
|
51
|
-
pos, value = pos_value
|
52
|
-
it "has valid looking fingerprint parameter names" do
|
53
|
-
unless param_name =~ /^(?:cookie|[^\.]+\..*)$/
|
54
|
-
fail "'#{param_name}' is invalid"
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
it "doesn't have param values for capture params" do
|
59
|
-
if pos > 0 && !value.to_s.empty?
|
60
|
-
fail "'#{fp.name}'s #{param_name} is a non-zero pos but specifies a value of '#{value}'"
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
it "has parameter values other than General, Server or Unknown, which are not helpful" do
|
65
|
-
if pos == 0 && value =~ /^(?i:general|server|unknown)$/
|
66
|
-
fail "'#{param_name}' has general/server/unknown value '#{value}'"
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
it "doesn't omit values for non-capture params" do
|
71
|
-
if pos == 0 && value.to_s.empty?
|
72
|
-
fail "'#{fp.name}'s #{param_name} is not a capture (pos=0) but doesn't specify a value"
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
it "doesn't have duplicate params" do
|
77
|
-
if param_names.include?(param_name)
|
78
|
-
fail "'#{fp.name}'s has duplicate #{param_name}"
|
79
|
-
else
|
80
|
-
param_names << param_name
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
it "uses interpolation correctly" do
|
85
|
-
if pos == 0 && /\{(?<interpolated>[^\s{}]+)\}/ =~ value
|
86
|
-
unless fp.params.key?(interpolated)
|
87
|
-
fail "'#{fp.name}' uses interpolated value '#{interpolated}' that does not exist"
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
context "#{fp.regex}" do
|
95
|
-
|
96
|
-
it "has a valid looking name" do
|
97
|
-
expect(fp.name).not_to be_nil
|
98
|
-
expect(fp.name).not_to be_empty
|
99
|
-
end
|
100
|
-
|
101
|
-
it "has a regex" do
|
102
|
-
expect(fp.regex).not_to be_nil
|
103
|
-
expect(fp.regex.class).to be ::Regexp
|
104
|
-
end
|
105
|
-
|
106
|
-
it 'uses capturing regular expressions properly' do
|
107
|
-
# the list of index-based captures that the fingerprint is expecting
|
108
|
-
expected_capture_positions = fp.params.values.map(&:first).map(&:to_i).select { |position| position > 0 }
|
109
|
-
if fp.params.empty? && expected_capture_positions.size > 0
|
110
|
-
fail "Non-asserting fingerprint with regex #{fp.regex} captures #{expected_capture_positions.size} time(s); 0 are needed"
|
111
|
-
else
|
112
|
-
# parse the regex and count the number of captures
|
113
|
-
actual_capture_positions = []
|
114
|
-
capture_number = 1
|
115
|
-
Regexp::Scanner.scan(fp.regex).each do |token_parts|
|
116
|
-
if token_parts.first == :group && ![:close, :passive, :options, :options_switch].include?(token_parts[1])
|
117
|
-
actual_capture_positions << capture_number
|
118
|
-
capture_number += 1
|
119
|
-
end
|
120
|
-
end
|
121
|
-
# compare the captures actually performed to those being used and ensure that they contain
|
122
|
-
# the same elements regardless of order, preventing, over-, under- and other forms of mis-capturing.
|
123
|
-
actual_capture_positions = actual_capture_positions.sort.uniq
|
124
|
-
expected_capture_positions = expected_capture_positions.sort.uniq
|
125
|
-
expect(actual_capture_positions).to eq(expected_capture_positions),
|
126
|
-
"Regex has #{actual_capture_positions.size} capture groups, but the fingerprint expected #{expected_capture_positions.size} extractions."
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
# Not yet enforced
|
131
|
-
# it "has test cases" do
|
132
|
-
# expect(fp.tests.length).not_to equal(0)
|
133
|
-
# end
|
134
|
-
|
135
|
-
it "Has a reasonable number (<= 20) of test cases" do
|
136
|
-
expect(fp.tests.length).to be <= 20
|
137
|
-
end
|
138
|
-
|
139
|
-
fp_examples = []
|
140
|
-
fp.tests.each do |example|
|
141
|
-
it "doesn't have a duplicate examples" do
|
142
|
-
if fp_examples.include?(example.content)
|
143
|
-
fail "'#{fp.name}' has duplicate example '#{example.content}'"
|
144
|
-
else
|
145
|
-
fp_examples << example.content
|
146
|
-
end
|
147
|
-
end
|
148
|
-
it "Example '#{example.content}' matches this regex" do
|
149
|
-
match = fp.match(example.content)
|
150
|
-
expect(match).to_not be_nil, 'Regex did not match'
|
151
|
-
# test any extractions specified in the example
|
152
|
-
example.attributes.each_pair do |k,v|
|
153
|
-
next if k == '_encoding'
|
154
|
-
next if k == '_filename'
|
155
|
-
expect(match[k]).to eq(v), "Regex didn't extract expected value for fingerprint attribute #{k} -- got #{match[k]} instead of #{v}"
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
it "Example '#{example.content}' matches this regex first" do
|
160
|
-
db.fingerprints.slice(0, i).each_index do |previous_i|
|
161
|
-
prev_fp = db.fingerprints[previous_i]
|
162
|
-
prev_fp.tests.each do |prev_example|
|
163
|
-
match = prev_fp.match(example.content)
|
164
|
-
expect(match).to be_nil, "Matched regex ##{previous_i} (#{db.fingerprints[previous_i].regex}) rather than ##{i} (#{db.fingerprints[i].regex})"
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
data/tools/dev/hooks/pre-commit
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
#!/bin/sh
|
2
|
-
#
|
3
|
-
# Hook script to verify changes about to be committed.
|
4
|
-
# The hook should exit with non-zero status after issuing an appropriate
|
5
|
-
# message if it wants to stop the commit.
|
6
|
-
|
7
|
-
# Verify that each fingerprint asserts known identifiers.
|
8
|
-
git diff --cached --name-only --diff-filter=ACM -z xml/*.xml | xargs -0 ./bin/recog_standardize --write
|
9
|
-
|
10
|
-
# get status
|
11
|
-
status=$?
|
12
|
-
|
13
|
-
if [ $status -ne 0 ]; then
|
14
|
-
echo "Please review any new additions to the text files under 'identifiers/'."
|
15
|
-
echo "If any of these names are close to an existing name, update the offending"
|
16
|
-
echo "fingerprint to use the existing name instead. Once the fingerprints are fixed,"
|
17
|
-
echo "remove the 'extra' names from the identifiers files, and run the tool again."
|
18
|
-
exit 1
|
19
|
-
fi
|
20
|
-
|
21
|
-
exit 0
|
data/update_cpes.py
DELETED
@@ -1,343 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python
|
2
|
-
|
3
|
-
import logging
|
4
|
-
import re
|
5
|
-
import sys
|
6
|
-
|
7
|
-
import yaml
|
8
|
-
from lxml import etree
|
9
|
-
|
10
|
-
BASE_LOG_FORMAT = '%(levelname)s: %(message)s'
|
11
|
-
|
12
|
-
# CPE w/o 2.3 component: cpe:/a:nginx:nginx:0.1.0"
|
13
|
-
REGEX_CPE = re.compile('^cpe:/([aho]):([^:]+):([^:]+)')
|
14
|
-
# CPE w/ 2.3 component: cpe:2.3:a:f5:nginx:0.1.0:*:*:*:*:*:*:*
|
15
|
-
REGEX_CPE_23 = re.compile('^cpe:2.3:([aho]):([^:]+):([^:]+)')
|
16
|
-
|
17
|
-
XML_PATH_DEPRECATED_BY = "./{http://scap.nist.gov/schema/cpe-extension/2.3}cpe23-item/{http://scap.nist.gov/schema/cpe-extension/2.3}deprecation/{http://scap.nist.gov/schema/cpe-extension/2.3}deprecated-by"
|
18
|
-
|
19
|
-
|
20
|
-
def parse_r7_remapping(file):
|
21
|
-
with open(file) as remap_file:
|
22
|
-
return yaml.safe_load(remap_file)["mappings"]
|
23
|
-
|
24
|
-
|
25
|
-
def update_vp_map(target_map, cpe_type, vendor, product):
|
26
|
-
"""Add an entry to the dict tracking valid combinations
|
27
|
-
"""
|
28
|
-
|
29
|
-
if cpe_type not in target_map:
|
30
|
-
target_map[cpe_type] = {}
|
31
|
-
|
32
|
-
if vendor not in target_map[cpe_type]:
|
33
|
-
target_map[cpe_type][vendor] = set()
|
34
|
-
|
35
|
-
product = product.replace('%2f', '/')
|
36
|
-
target_map[cpe_type][vendor].add(product)
|
37
|
-
|
38
|
-
|
39
|
-
def update_deprecated_map(target_map, dep_string, entry):
|
40
|
-
"""Add an entry to the dict tracking deprecations
|
41
|
-
|
42
|
-
target_map example:
|
43
|
-
|
44
|
-
{
|
45
|
-
"a:100plus:101eip":
|
46
|
-
{
|
47
|
-
"deprecated_date": "2021-06-10T15:28:05.490Z",
|
48
|
-
"deprecated_by": "a:hundredplus:101eip"
|
49
|
-
}
|
50
|
-
}
|
51
|
-
|
52
|
-
Args:
|
53
|
-
target_map (dict): dict containing deprecations
|
54
|
-
dep_string (str): key to add in the format of 'type:vendor:product'
|
55
|
-
entry (lxml.etree._Element): XML element to pull additional data from
|
56
|
-
|
57
|
-
Returns:
|
58
|
-
None, target_map modified in place
|
59
|
-
"""
|
60
|
-
|
61
|
-
deprecated_date = entry.get("deprecation_date", "")
|
62
|
-
|
63
|
-
# Find the CPE that deprecated this entry
|
64
|
-
raw_dep_by = entry.find(XML_PATH_DEPRECATED_BY).get('name')
|
65
|
-
|
66
|
-
# Extract the type, vendor, product
|
67
|
-
dep_by_match = REGEX_CPE_23.match(raw_dep_by)
|
68
|
-
if not dep_by_match:
|
69
|
-
logging.error("CPE %s is deprecated but we can't build the deprecation mapping entry for some reason.", dep_string)
|
70
|
-
return
|
71
|
-
|
72
|
-
dep_type, dep_vendor, dep_product = dep_by_match.group(1, 2, 3)
|
73
|
-
deprecated_by = "{}:{}:{}".format(dep_type, dep_vendor, dep_product)
|
74
|
-
|
75
|
-
if dep_string not in target_map:
|
76
|
-
target_map[dep_string] = {}
|
77
|
-
|
78
|
-
if not target_map[dep_string].get('deprecated_date'):
|
79
|
-
target_map[dep_string]['deprecated_date'] = deprecated_date
|
80
|
-
|
81
|
-
if not target_map[dep_string].get('deprecated_by'):
|
82
|
-
target_map[dep_string]['deprecated_by'] = deprecated_by
|
83
|
-
|
84
|
-
|
85
|
-
def parse_cpe_vp_map(file):
|
86
|
-
deprecated_map = {}
|
87
|
-
vp_map = {} # cpe_type -> vendor -> products
|
88
|
-
|
89
|
-
parser = etree.XMLParser(remove_comments=False)
|
90
|
-
doc = etree.parse(file, parser)
|
91
|
-
namespaces = {
|
92
|
-
'ns': 'http://cpe.mitre.org/dictionary/2.0',
|
93
|
-
'meta': 'http://scap.nist.gov/schema/cpe-dictionary-metadata/0.2'
|
94
|
-
}
|
95
|
-
for entry in doc.xpath("//ns:cpe-list/ns:cpe-item", namespaces=namespaces):
|
96
|
-
cpe_name = entry.get("name")
|
97
|
-
if not cpe_name:
|
98
|
-
continue
|
99
|
-
|
100
|
-
cpe_match = REGEX_CPE.match(cpe_name)
|
101
|
-
if cpe_match:
|
102
|
-
cpe_type, vendor, product = cpe_match.group(1, 2, 3)
|
103
|
-
# If the entry is deprecated then don't add it to our list of valid
|
104
|
-
# CPEs, but instead add it to a list for reference later.
|
105
|
-
if entry.get("deprecated"):
|
106
|
-
# This will be the key under which we store the deprecation data
|
107
|
-
deprecated_string = "{}:{}:{}".format(cpe_type, vendor, product)
|
108
|
-
|
109
|
-
update_deprecated_map(deprecated_map, deprecated_string, entry)
|
110
|
-
continue
|
111
|
-
|
112
|
-
update_vp_map(vp_map, cpe_type, vendor, product)
|
113
|
-
|
114
|
-
else:
|
115
|
-
logging.error("Unexpected CPE %s", cpe_name)
|
116
|
-
|
117
|
-
return vp_map, deprecated_map
|
118
|
-
|
119
|
-
|
120
|
-
def lookup_cpe(vendor, product, cpe_type, cpe_table, remap, deprecated_map):
|
121
|
-
"""Identify the correct vendor and product values for a CPE
|
122
|
-
|
123
|
-
This function attempts to determine the correct CPE using vendor and product
|
124
|
-
values supplied by the caller as well as a remapping dictionary for mapping
|
125
|
-
these values to more correct values used by NIST.
|
126
|
-
|
127
|
-
For example, the remapping might tell us that a value of 'alpine' for the
|
128
|
-
vendor string should be 'alpinelinux' instead, or for product 'solaris'
|
129
|
-
should be 'sunos'.
|
130
|
-
|
131
|
-
This function should only emit values seen in the official NIST CPE list
|
132
|
-
which is provided to it in cpe_table.
|
133
|
-
|
134
|
-
Lookup priority:
|
135
|
-
1. Original vendor / product
|
136
|
-
2. Original vendor / remap product
|
137
|
-
3. Remap vendor / original product
|
138
|
-
4. Remap vendor / remap product
|
139
|
-
|
140
|
-
Args:
|
141
|
-
vendor (str): vendor name
|
142
|
-
product (str): product name
|
143
|
-
cpe_type (str): CPE type - o, a, h, etc.
|
144
|
-
cpe_table (dict): dict containing the official NIST CPE data
|
145
|
-
remap (dict): dict containing the remapping values
|
146
|
-
deprecated_cves (set): set of all deprecated CPEs in the format
|
147
|
-
'type:vendor:product'
|
148
|
-
Returns:
|
149
|
-
success, vendor, product
|
150
|
-
"""
|
151
|
-
|
152
|
-
if (
|
153
|
-
vendor in cpe_table[cpe_type]
|
154
|
-
and product in cpe_table[cpe_type][vendor]
|
155
|
-
):
|
156
|
-
# Hot path, success with original values
|
157
|
-
return True, vendor, product
|
158
|
-
|
159
|
-
# Everything else depends on a remap of some sort.
|
160
|
-
# get the remappings for this one vendor string.
|
161
|
-
vendor_remap = None
|
162
|
-
|
163
|
-
remap_type = remap.get(cpe_type, None)
|
164
|
-
if remap_type:
|
165
|
-
vendor_remap = remap_type.get(vendor, None)
|
166
|
-
|
167
|
-
if vendor_remap:
|
168
|
-
# If we have product remappings, work that angle next
|
169
|
-
possible_product = None
|
170
|
-
if (
|
171
|
-
vendor_remap.get('products', None)
|
172
|
-
and product in vendor_remap['products']
|
173
|
-
):
|
174
|
-
possible_product = vendor_remap['products'][product]
|
175
|
-
|
176
|
-
if (vendor in cpe_table[cpe_type]
|
177
|
-
and possible_product
|
178
|
-
and possible_product in cpe_table[cpe_type][vendor]):
|
179
|
-
# Found original vendor, remap product
|
180
|
-
return True, vendor, possible_product
|
181
|
-
|
182
|
-
# Start working the process to find a match with a remapped vendor name
|
183
|
-
if vendor_remap.get('vendor', None):
|
184
|
-
new_vendor = vendor_remap['vendor']
|
185
|
-
|
186
|
-
if new_vendor in cpe_table[cpe_type]:
|
187
|
-
|
188
|
-
if product in cpe_table[cpe_type][new_vendor]:
|
189
|
-
# Found remap vendor, original product
|
190
|
-
return True, new_vendor, product
|
191
|
-
|
192
|
-
if possible_product and possible_product in cpe_table[cpe_type][new_vendor]:
|
193
|
-
# Found remap vendor, remap product
|
194
|
-
return True, new_vendor, possible_product
|
195
|
-
|
196
|
-
deprecated_string = "{}:{}:{}".format(cpe_type, vendor, product)
|
197
|
-
if deprecated_map.get(deprecated_string, False):
|
198
|
-
dep_by = deprecated_map[deprecated_string].get("deprecated_by", "")
|
199
|
-
dep_date = deprecated_map[deprecated_string].get("deprecated_date", "")
|
200
|
-
logging.error("Product %s from vendor %s invalid for CPE %s and no mapping. This combination is DEPRECATED by %s at %s",
|
201
|
-
product, vendor, cpe_type, dep_by, dep_date)
|
202
|
-
else:
|
203
|
-
logging.error("Product %s from vendor %s invalid for CPE %s and no mapping.",
|
204
|
-
product, vendor, cpe_type)
|
205
|
-
|
206
|
-
return False, None, None
|
207
|
-
|
208
|
-
|
209
|
-
def update_cpes(xml_file, cpe_vp_map, r7_vp_map, deprecated_cves):
|
210
|
-
parser = etree.XMLParser(remove_comments=False, remove_blank_text=True)
|
211
|
-
doc = etree.parse(xml_file, parser)
|
212
|
-
|
213
|
-
for fingerprint in doc.xpath('//fingerprint'):
|
214
|
-
|
215
|
-
# collect all the params, grouping by os and service params that could be used to compute a CPE
|
216
|
-
params = {}
|
217
|
-
for param in fingerprint.xpath('./param'):
|
218
|
-
name = param.attrib['name']
|
219
|
-
# remove any existing CPE params
|
220
|
-
if re.match(r'^.*\.cpe\d{0,2}$', name):
|
221
|
-
param.getparent().remove(param)
|
222
|
-
continue
|
223
|
-
|
224
|
-
match = re.search(r'^(?P<fp_type>hw|os|service(?:\.component)?)\.', name)
|
225
|
-
if match:
|
226
|
-
fp_type = match.group('fp_type')
|
227
|
-
if not fp_type in params:
|
228
|
-
params[fp_type] = {}
|
229
|
-
if name in params[fp_type]:
|
230
|
-
raise ValueError('Duplicated fingerprint named {} in fingerprint {} in file {}'.format(name, fingerprint.attrib['pattern'], xml_file))
|
231
|
-
params[fp_type][name] = param
|
232
|
-
|
233
|
-
# for each of the applicable os/service param groups, build a CPE
|
234
|
-
for fp_type in params:
|
235
|
-
if fp_type == 'os':
|
236
|
-
cpe_type = 'o'
|
237
|
-
elif fp_type.startswith('service'):
|
238
|
-
cpe_type = 'a'
|
239
|
-
elif fp_type == 'hw':
|
240
|
-
cpe_type = 'h'
|
241
|
-
else:
|
242
|
-
raise ValueError('Unhandled param type {}'.format(fp_type))
|
243
|
-
|
244
|
-
# extract the vendor/product/version values from each os/service group,
|
245
|
-
# using the static value ('Apache', for example) when pos is 0, and
|
246
|
-
# otherwise use a value that contains interpolation markers such that
|
247
|
-
# products/projects that use recog content can insert the value
|
248
|
-
# extracted from the banner/other data via regex capturing groups
|
249
|
-
fp_data = {
|
250
|
-
'vendor': None,
|
251
|
-
'product': None,
|
252
|
-
'version': '-',
|
253
|
-
}
|
254
|
-
for fp_datum in fp_data:
|
255
|
-
fp_datum_param_name = "{}.{}".format(fp_type, fp_datum)
|
256
|
-
if fp_datum_param_name in params[fp_type]:
|
257
|
-
fp_datum_e = params[fp_type][fp_datum_param_name]
|
258
|
-
if fp_datum_e.attrib['pos'] == '0':
|
259
|
-
fp_data[fp_datum] = fp_datum_e.attrib['value']
|
260
|
-
else:
|
261
|
-
fp_data[fp_datum] = "{{{}}}".format(fp_datum_e.attrib['name'])
|
262
|
-
|
263
|
-
vendor = fp_data['vendor']
|
264
|
-
product = fp_data['product']
|
265
|
-
version = fp_data['version']
|
266
|
-
|
267
|
-
# build a reasonable looking CPE value from the vendor/product/version,
|
268
|
-
# lowercasing, replacing whitespace with _, and more
|
269
|
-
if vendor and product:
|
270
|
-
if not cpe_type in cpe_vp_map:
|
271
|
-
logging.error("Didn't find CPE type '%s' for '%s' '%s'", cpe_type, vendor, product)
|
272
|
-
continue
|
273
|
-
|
274
|
-
vendor = vendor.lower().replace(' ', '_').replace(',', '')
|
275
|
-
product = product.lower().replace(' ', '_').replace(',', '').replace('!', '%21')
|
276
|
-
if 'unknown' in [vendor, product]:
|
277
|
-
continue
|
278
|
-
|
279
|
-
if (vendor.startswith('{') and vendor.endswith('}')) or (product.startswith('{') and product.endswith('}')):
|
280
|
-
continue
|
281
|
-
|
282
|
-
success, vendor, product = lookup_cpe(vendor, product, cpe_type, cpe_vp_map, r7_vp_map, deprecated_cves)
|
283
|
-
if not success:
|
284
|
-
continue
|
285
|
-
|
286
|
-
# Sanity check the value to ensure that no invalid values will
|
287
|
-
# slip in due to logic or mapping bugs.
|
288
|
-
# If it's not in the official NIST list then log it and kick it out
|
289
|
-
if product not in cpe_vp_map[cpe_type][vendor]:
|
290
|
-
logging.error("Invalid CPE type %s created for vendor %s and product %s. This may be due to an invalid mapping.", cpe_type, vendor, product)
|
291
|
-
continue
|
292
|
-
|
293
|
-
# building the CPE string
|
294
|
-
# Last minute escaping of '/' and `!`
|
295
|
-
product = product.replace('/', '\/').replace('%21', '\!')
|
296
|
-
cpe_value = 'cpe:/{}:{}:{}'.format(cpe_type, vendor, product)
|
297
|
-
|
298
|
-
if version:
|
299
|
-
cpe_value += ":{}".format(version)
|
300
|
-
|
301
|
-
cpe_param = etree.Element('param')
|
302
|
-
cpe_param.attrib['pos'] = '0'
|
303
|
-
cpe_param.attrib['name'] = '{}.cpe23'.format(fp_type)
|
304
|
-
cpe_param.attrib['value'] = cpe_value
|
305
|
-
|
306
|
-
for param_name in params[fp_type]:
|
307
|
-
param = params[fp_type][param_name]
|
308
|
-
parent = param.getparent()
|
309
|
-
index = parent.index(param) + 1
|
310
|
-
parent.insert(index, cpe_param)
|
311
|
-
|
312
|
-
root = doc.getroot()
|
313
|
-
|
314
|
-
with open(xml_file, 'wb') as xml_out:
|
315
|
-
xml_out.write(etree.tostring(root, pretty_print=True, xml_declaration=True, encoding=doc.docinfo.encoding))
|
316
|
-
|
317
|
-
|
318
|
-
def main():
|
319
|
-
if len(sys.argv) != 4:
|
320
|
-
logging.critical("Expecting exactly 3 arguments; recog XML file, CPE 2.3 XML dictionary, JSON remapping, got %s", (len(sys.argv) - 1))
|
321
|
-
sys.exit(1)
|
322
|
-
|
323
|
-
cpe_vp_map, deprecated_map = parse_cpe_vp_map(sys.argv[2])
|
324
|
-
if not cpe_vp_map:
|
325
|
-
logging.critical("No CPE vendor => product mappings read from CPE 2.3 XML dictionary %s", sys.argv[2])
|
326
|
-
sys.exit(1)
|
327
|
-
|
328
|
-
r7_vp_map = parse_r7_remapping(sys.argv[3])
|
329
|
-
if not r7_vp_map:
|
330
|
-
logging.warning("No Rapid7 vendor/product => CPE mapping read from %s", sys.argv[3])
|
331
|
-
|
332
|
-
# update format string for the logging handler to include the recog XML filename
|
333
|
-
logging.basicConfig(force=True, format=f"{sys.argv[1]}: {BASE_LOG_FORMAT}")
|
334
|
-
|
335
|
-
update_cpes(sys.argv[1], cpe_vp_map, r7_vp_map, deprecated_map)
|
336
|
-
|
337
|
-
|
338
|
-
if __name__ == '__main__':
|
339
|
-
logging.basicConfig(format=BASE_LOG_FORMAT)
|
340
|
-
try:
|
341
|
-
sys.exit(main())
|
342
|
-
except KeyboardInterrupt:
|
343
|
-
pass
|