Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rubocop/cop/lint/check_code_missing_reason.rb
74553 views
1
# frozen_string_literal: true
2
3
module RuboCop
4
module Cop
5
module Lint
6
# Detects CheckCode usages inside `check` methods that are missing a
7
# human-readable reason string.
8
#
9
# Every CheckCode *returned* from a `check` method should include a reason
10
# so that users understand why the target was assessed that way. The cop
11
# only fires inside `def check` bodies, which avoids false positives from
12
# the many legitimate non-return uses of CheckCode constants elsewhere
13
# (comparisons, case/when branches, array membership checks, etc.).
14
#
15
# Flagged patterns (inside `def check` only):
16
# - Bare constants with no call: `CheckCode::Safe`
17
# - Empty calls: `CheckCode::Safe()`
18
# - Kwargs-only calls: `CheckCode::Safe(details: {...})`
19
#
20
# @example
21
# # bad - bare constant, no reason
22
# def check
23
# CheckCode::Safe
24
# Exploit::CheckCode::Vulnerable
25
# end
26
#
27
# # bad - called with no reason string
28
# def check
29
# CheckCode::Safe()
30
# Exploit::CheckCode::Unknown()
31
# end
32
#
33
# # bad - only keyword args, no reason string
34
# def check
35
# CheckCode::Vulnerable(details: { version: '1.0' })
36
# end
37
#
38
# # good - reason string provided
39
# def check
40
# CheckCode::Safe('The target is not running the vulnerable service')
41
# Exploit::CheckCode::Appears("Version #{version} appears vulnerable")
42
# CheckCode::Vulnerable('Confirmed RCE', details: { version: version })
43
# end
44
#
45
# # fine - comparisons and case/when outside check are not flagged
46
# def exploit
47
# fail_with(...) unless check == CheckCode::Vulnerable
48
# case checkcode
49
# when Exploit::CheckCode::Vulnerable, Exploit::CheckCode::Appears
50
# print_good(checkcode.message)
51
# end
52
# end
53
#
54
class CheckCodeMissingReason < Base
55
MSG = 'Provide a human-readable reason string when returning a CheckCode, ' \
56
"e.g. `%<check_code>s('The target is not vulnerable because ...')`"
57
58
CHECK_CODE_METHODS = %i[
59
Unknown
60
Safe
61
Detected
62
Appears
63
Vulnerable
64
Unsupported
65
].to_set.freeze
66
67
# Matches the receiver of a CheckCode call or constant — the `CheckCode`
68
# portion of `CheckCode::Safe`, `Exploit::CheckCode::Safe`, or
69
# `Msf::Exploit::CheckCode::Safe`.
70
def_node_matcher :check_code_receiver?, <<~PATTERN
71
{
72
(const nil? :CheckCode)
73
(const (const nil? :Exploit) :CheckCode)
74
(const (const (const nil? :Msf) :Exploit) :CheckCode)
75
}
76
PATTERN
77
78
# Matches a bare CheckCode constant with no call, e.g. `CheckCode::Safe`
79
# or `Exploit::CheckCode::Appears`.
80
def_node_matcher :bare_check_code_const?, <<~PATTERN
81
(const #check_code_receiver? CHECK_CODE_METHODS)
82
PATTERN
83
84
# Matches a CheckCode method call, e.g. `CheckCode::Safe(...)` or
85
# `Exploit::CheckCode::Appears('msg')`.
86
def_node_matcher :check_code_call?, <<~PATTERN
87
(send #check_code_receiver? CHECK_CODE_METHODS ...)
88
PATTERN
89
90
def on_const(node)
91
return unless bare_check_code_const?(node)
92
return unless inside_check_method?(node)
93
# Skip if this const is the receiver of a send node — the on_send
94
# handler will cover that case and we don't want a double offense.
95
return if node.parent&.send_type? && node.parent.receiver == node
96
# Skip when used as a comparator: `checkcode == CheckCode::Safe`,
97
# `check.eql? CheckCode::Vulnerable`, case/when branches, arrays, etc.
98
# These are consumers of a CheckCode value, not return values.
99
return if used_as_comparator?(node)
100
101
add_offense(node, message: format(MSG, check_code: node.source))
102
end
103
104
def on_send(node)
105
return unless check_code_call?(node)
106
return unless inside_check_method?(node)
107
return if reason?(node)
108
109
add_offense(node, message: format(MSG, check_code: "#{node.receiver.source}::#{node.method_name}"))
110
end
111
112
private
113
114
# Returns true if the node is inside a `def check` method body.
115
def inside_check_method?(node)
116
node.each_ancestor(:def).any? { |def_node| def_node.method_name == :check }
117
end
118
119
COMPARISON_METHODS = %i[== != === =~ !~ eql? equal?].to_set.freeze
120
121
# Returns true when the CheckCode constant is being used as a comparator
122
# rather than as a return value — e.g. the RHS of `==`/`eql?`, a
123
# case/when branch, or an array element.
124
def used_as_comparator?(node)
125
parent = node.parent
126
return false unless parent
127
128
# `when CheckCode::Safe` or `when CheckCode::Safe, CheckCode::Appears`
129
return true if parent.when_type?
130
131
# `[CheckCode::Vulnerable, CheckCode::Appears]`
132
return true if parent.array_type?
133
134
# `result == CheckCode::Safe`, `check.eql? CheckCode::Vulnerable`, etc.
135
# The node must be an argument (not the receiver) of the comparison send.
136
return true if parent.send_type? &&
137
parent.receiver != node &&
138
COMPARISON_METHODS.include?(parent.method_name)
139
140
false
141
end
142
143
# Returns true if the call has a non-hash first positional argument —
144
# i.e. any value used as the reason (string, interpolated string,
145
# variable, exception object, method call result, etc.).
146
# A hash as the sole first arg means only keyword args were passed.
147
def reason?(node)
148
first_arg = node.arguments.first
149
return false if first_arg.nil?
150
return false if first_arg.hash_type?
151
152
true
153
end
154
end
155
end
156
end
157
end
158
159