Skip to content

Commit dd081ab

Browse files
Merge pull request #21633 from sjanusz-r7/rubocop-redundant-module-info-keys
Rubocop redundant module info keys
2 parents 050265a + 6a951c5 commit dd081ab

3 files changed

Lines changed: 568 additions & 0 deletions

File tree

.rubocop.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require:
2828
- ./lib/rubocop/cop/lint/datastore_srvhost_usage.rb
2929
- ./lib/rubocop/cop/lint/bare_check_code_in_non_exploit.rb
3030
- ./lib/rubocop/cop/lint/check_code_missing_reason.rb
31+
- ./lib/rubocop/cop/lint/module_redundant_arch_platform.rb
3132

3233
Lint/CheckCodeMissingReason:
3334
Enabled: true
@@ -214,6 +215,11 @@ Lint/ModuleEnforceNotes:
214215
- 'modules/auxiliary/**/*'
215216
- 'modules/post/**/*'
216217

218+
Lint/ModuleRedundantArchPlatform:
219+
Enabled: true
220+
Include:
221+
- 'modules/**/*'
222+
217223
Lint/DeprecatedGemVersion:
218224
Enabled: true
219225
Exclude:
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# frozen_string_literal: true
2+
3+
module RuboCop
4+
module Cop
5+
module Lint
6+
# Checks that modules with `Targets` defined do not redundantly specify
7+
# `Arch` or `Platform` at the top level of update_info when that
8+
# information is already present in all targets.
9+
#
10+
# The framework uses target-level Arch/Platform when present, falling
11+
# back to the module-level value only when the target does not specify
12+
# them (see `Msf::Exploit#target_arch` and `#target_platform`).
13+
# Therefore, specifying `Arch` or `Platform` at the top level is
14+
# redundant when every target already carries that information.
15+
#
16+
# @example
17+
# # bad - Arch at top level when all targets define it
18+
# update_info(
19+
# info,
20+
# 'Arch' => ARCH_X86,
21+
# 'Targets' => [
22+
# ['Windows', { 'Arch' => ARCH_X86 }]
23+
# ]
24+
# )
25+
#
26+
# # bad - Platform at top level when all targets define it
27+
# update_info(
28+
# info,
29+
# 'Platform' => 'win',
30+
# 'Targets' => [
31+
# ['Windows', { 'Platform' => 'win' }]
32+
# ]
33+
# )
34+
#
35+
# # good - no top-level Arch/Platform when targets define them
36+
# update_info(
37+
# info,
38+
# 'Targets' => [
39+
# ['Windows x86', { 'Platform' => 'win', 'Arch' => ARCH_X86 }],
40+
# ['Windows x64', { 'Platform' => 'win', 'Arch' => ARCH_X64 }]
41+
# ]
42+
# )
43+
#
44+
# # good - top-level Arch/Platform when targets don't define them
45+
# update_info(
46+
# info,
47+
# 'Arch' => ARCH_X86,
48+
# 'Platform' => 'win',
49+
# 'Targets' => [
50+
# ['Automatic', {}]
51+
# ]
52+
# )
53+
class ModuleRedundantArchPlatform < Base
54+
extend AutoCorrector
55+
include RangeHelp
56+
57+
MSG = 'Remove top-level `%s` as it is already defined in all `Targets`'
58+
59+
CHECKED_KEYS = %w[Arch Platform].freeze
60+
61+
def_node_matcher :find_update_info_node, <<~PATTERN
62+
(def :initialize _args (begin (super $(send nil? {:update_info :merge_info} (lvar :info) (hash ...))) ...))
63+
PATTERN
64+
65+
def_node_matcher :find_nested_update_info_node, <<~PATTERN
66+
(def :initialize _args (super $(send nil? {:update_info :merge_info} (lvar :info) (hash ...)) ...))
67+
PATTERN
68+
69+
def on_def(node)
70+
update_info_node = find_update_info_node(node) || find_nested_update_info_node(node)
71+
return if update_info_node.nil?
72+
73+
hash = update_info_node.arguments.find { |argument| hash_arg?(argument) }
74+
return if hash.nil?
75+
76+
top_level_pairs = {}
77+
targets_node = nil
78+
79+
hash.each_pair do |key, value|
80+
next unless key.type == :str
81+
82+
if CHECKED_KEYS.include?(key.value)
83+
top_level_pairs[key.value] = hash.pairs.find { |pair| pair.key == key }
84+
elsif key.value == 'Targets'
85+
targets_node = value
86+
end
87+
end
88+
89+
return if targets_node.nil?
90+
return if top_level_pairs.empty?
91+
return unless targets_node.type == :array
92+
93+
targets = targets_node.children
94+
return if targets.empty?
95+
96+
CHECKED_KEYS.each do |key_name|
97+
pair = top_level_pairs[key_name]
98+
next unless pair
99+
next unless all_targets_define_key?(targets, key_name)
100+
101+
add_offense(pair.key, message: MSG % key_name) do |corrector|
102+
remove_pair_with_line(corrector, pair)
103+
end
104+
end
105+
end
106+
107+
private
108+
109+
# Checks whether every target in the Targets array defines the given key.
110+
# Each target is expected to be an array node like:
111+
# ['Name', { 'Arch' => ..., 'Platform' => ... }]
112+
def all_targets_define_key?(targets, key_name)
113+
targets.all? do |target|
114+
next false unless target.type == :array
115+
116+
target_hash = target.children.find { |child| hash_arg?(child) }
117+
next false if target_hash.nil?
118+
119+
target_hash.pairs.any? do |pair|
120+
pair.key.type == :str && pair.key.value == key_name
121+
end
122+
end
123+
end
124+
125+
def hash_arg?(node)
126+
node.type == :hash
127+
end
128+
129+
# Removes a key-value pair and its entire line (leading whitespace,
130+
# trailing comma, and newline).
131+
def remove_pair_with_line(corrector, pair)
132+
range = pair.source_range
133+
end_pos = range.end_pos
134+
source = range.source_buffer.source
135+
136+
# Consume trailing comma and whitespace/newline
137+
while end_pos < source.length && source[end_pos] =~ /[ \t]/
138+
end_pos += 1
139+
end
140+
if end_pos < source.length && source[end_pos] == ','
141+
end_pos += 1
142+
end
143+
while end_pos < source.length && source[end_pos] =~ /[ \t]/
144+
end_pos += 1
145+
end
146+
if end_pos < source.length && source[end_pos] == "\n"
147+
end_pos += 1
148+
end
149+
150+
# Consume the leading whitespace on this line
151+
start_pos = range.begin_pos
152+
while start_pos > 0 && source[start_pos - 1] =~ /[ \t]/
153+
start_pos -= 1
154+
end
155+
156+
removal_range = range.with(begin_pos: start_pos, end_pos: end_pos)
157+
corrector.remove(removal_range)
158+
end
159+
end
160+
end
161+
end
162+
end

0 commit comments

Comments
 (0)