Skip to content

Commit 0b3812c

Browse files
committed
(CAT-1301) Add check unsafe interpolations check
This commit adds the check unsafe interpolations check to core puppet-lint insted of having it as a standalone gem
1 parent 4a76c16 commit 0b3812c

File tree

2 files changed

+316
-0
lines changed

2 files changed

+316
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
PuppetLint.new_check(:check_unsafe_interpolations) do
2+
COMMANDS = Array['command', 'onlyif', 'unless']
3+
INTERPOLATED_STRINGS = Array[:DQPRE, :DQMID]
4+
USELESS_CHARS = Array[:WHITESPACE, :COMMA]
5+
def check
6+
# Gather any exec commands' resources into an array
7+
exec_resources = resource_indexes.map { |resource|
8+
resource_parameters = resource[:param_tokens].map(&:value)
9+
resource if resource[:type].value == 'exec' && !(COMMANDS & resource_parameters).empty?
10+
}.compact
11+
12+
# Iterate over title tokens and raise a warning if any are variables
13+
unless get_exec_titles.empty?
14+
get_exec_titles.each do |title|
15+
check_unsafe_title(title)
16+
end
17+
end
18+
19+
# Iterate over each command found in any exec
20+
exec_resources.each do |command_resources|
21+
check_unsafe_interpolations(command_resources)
22+
end
23+
end
24+
25+
# Iterate over the tokens in a title and raise a warning if an interpolated variable is found
26+
def check_unsafe_title(title)
27+
title.each do |token|
28+
notify_warning(token.next_code_token) if interpolated?(token)
29+
end
30+
end
31+
32+
# Iterates over an exec resource and if a command, onlyif or unless paramter is found, it is checked for unsafe interpolations
33+
def check_unsafe_interpolations(command_resources)
34+
command_resources[:tokens].each do |token|
35+
# Skip iteration if token isn't a command of type :NAME
36+
next unless COMMANDS.include?(token.value) && token.type == :NAME
37+
# Don't check the command if it is parameterised
38+
next if parameterised?(token)
39+
40+
check_command(token).each do |t|
41+
notify_warning(t)
42+
end
43+
end
44+
end
45+
46+
# Raises a warning given a token and message
47+
def notify_warning(token)
48+
notify :warning,
49+
message: "unsafe interpolation of variable '#{token.value}' in exec command",
50+
line: token.line,
51+
column: token.column
52+
end
53+
54+
# Iterates over the tokens in a command and adds it to an array of violations if it is an input variable
55+
def check_command(token)
56+
# Initialise variables needed in while loop
57+
rule_violations = []
58+
current_token = token
59+
60+
# Iterate through tokens in command
61+
while current_token.type != :NEWLINE
62+
# Check if token is a varibale and if it is parameterised
63+
rule_violations.append(current_token.next_code_token) if interpolated?(current_token)
64+
current_token = current_token.next_token
65+
end
66+
67+
rule_violations
68+
end
69+
70+
# A command is parameterised if its args are placed in an array
71+
# This function checks if the current token is a :FARROW and if so, if it is followed by an LBRACK
72+
def parameterised?(token)
73+
current_token = token
74+
while current_token.type != :NEWLINE
75+
return true if current_token.type == :FARROW && current_token.next_token.next_token.type == :LBRACK
76+
current_token = current_token.next_token
77+
end
78+
end
79+
80+
# This function is a replacement for puppet_lint's title_tokens function which assumes titles have single quotes
81+
# This function adds a check for titles in double quotes where there could be interpolated variables
82+
def get_exec_titles
83+
result = []
84+
tokens.each_index do |token_idx|
85+
next unless ['exec'].include?(tokens[token_idx].value)
86+
# We have a resource declaration. Now find the title
87+
tokens_array = []
88+
# Check if title is an array
89+
if tokens[token_idx].next_code_token.next_code_token.type == :LBRACK
90+
# Get the start and end indices of the array of titles
91+
array_start_idx = tokens.rindex { |r| r.type == :LBRACK }
92+
array_end_idx = tokens.rindex { |r| r.type == :RBRACK }
93+
94+
# Grab everything within the array
95+
title_array_tokens = tokens[(array_start_idx + 1)..(array_end_idx - 1)]
96+
tokens_array.concat(title_array_tokens.reject do |token|
97+
USELESS_CHARS.include?(token.type)
98+
end)
99+
result << tokens_array
100+
# Check if title is double quotes string
101+
elsif tokens[token_idx].next_code_token.next_code_token.type == :DQPRE
102+
# Find the start and end of the title
103+
title_start_idx = tokens.find_index(tokens[token_idx].next_code_token.next_code_token)
104+
title_end_idx = title_start_idx + index_offset_for(':', tokens[title_start_idx..tokens.length])
105+
106+
result << tokens[title_start_idx..title_end_idx]
107+
# Title is in single quotes
108+
else
109+
tokens_array.concat([tokens[token_idx].next_code_token.next_code_token])
110+
111+
result << tokens_array
112+
end
113+
end
114+
result
115+
end
116+
117+
def interpolated?(token)
118+
INTERPOLATED_STRINGS.include?(token.type)
119+
end
120+
121+
# Finds the index offset of the next instance of `value` in `tokens_slice` from the original index
122+
def index_offset_for(value, tokens_slice)
123+
i = 0
124+
while i < tokens_slice.length
125+
return i if value.include?(tokens_slice[i].value)
126+
i += 1
127+
end
128+
end
129+
end
130+
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
require 'spec_helper'
2+
3+
describe 'check_unsafe_interpolations' do
4+
let(:msg) { "unsafe interpolation of variable 'foo' in exec command" }
5+
6+
context 'with fix disabled' do
7+
context 'exec with unsafe interpolation in command' do
8+
let(:code) do
9+
<<-PUPPET
10+
class foo {
11+
12+
exec { 'bar':
13+
command => "echo ${foo}",
14+
}
15+
16+
}
17+
PUPPET
18+
end
19+
20+
it 'detects an unsafe exec command argument' do
21+
expect(problems).to have(1).problems
22+
end
23+
24+
it 'creates one warning' do
25+
expect(problems).to contain_warning(msg)
26+
end
27+
end
28+
29+
context 'exec with multiple unsafe interpolations in command' do
30+
let(:code) do
31+
<<-PUPPET
32+
class foo {
33+
34+
exec { 'bar':
35+
command => "echo ${foo} ${bar}",
36+
}
37+
38+
}
39+
PUPPET
40+
end
41+
42+
it 'detects multiple unsafe exec command arguments' do
43+
expect(problems).to have(2).problems
44+
end
45+
46+
it 'creates two warnings' do
47+
expect(problems).to contain_warning(msg)
48+
expect(problems).to contain_warning(msg)
49+
end
50+
end
51+
52+
context 'code that uses title with unsafe string as command' do
53+
let(:code) do
54+
<<-PUPPET
55+
class foo {
56+
57+
exec { "echo ${foo}": }
58+
59+
}
60+
PUPPET
61+
end
62+
63+
it 'detects one problem' do
64+
expect(problems).to have(1).problems
65+
end
66+
67+
it 'creates one warning' do
68+
expect(problems).to contain_warning(msg)
69+
end
70+
end
71+
72+
context 'exec with a safe string in command' do
73+
let(:code) do
74+
<<-PUPPET
75+
class foo {
76+
77+
exec { 'bar':
78+
command => "echo foo",
79+
}
80+
81+
}
82+
PUPPET
83+
end
84+
85+
it 'detects zero problems' do
86+
expect(problems).to have(0).problems
87+
end
88+
end
89+
90+
context 'exec that has an array of args in command' do
91+
let(:code) do
92+
<<-PUPPET
93+
class foo {
94+
95+
exec { 'bar':
96+
command => ['echo', $foo],
97+
}
98+
}
99+
PUPPET
100+
end
101+
102+
it 'detects zero problems' do
103+
expect(problems).to have(0).problems
104+
end
105+
end
106+
107+
context 'exec that has an array of args in command' do
108+
let(:code) do
109+
<<-PUPPET
110+
class foo {
111+
112+
exec { ["foo", "bar", "baz"]:
113+
command => echo qux,
114+
}
115+
}
116+
PUPPET
117+
end
118+
119+
it 'detects zero problems' do
120+
expect(problems).to have(0).problems
121+
end
122+
end
123+
124+
context 'file resource' do
125+
let(:code) do
126+
<<-PUPPET
127+
class foo {
128+
file { '/etc/bar':
129+
ensure => file,
130+
backup => false,
131+
content => $baz,
132+
}
133+
}
134+
PUPPET
135+
end
136+
137+
it 'detects zero problems' do
138+
expect(problems).to have(0).problems
139+
end
140+
end
141+
142+
context 'file resource and an exec with unsafe interpolation in command' do
143+
let(:code) do
144+
<<-PUPPET
145+
class foo {
146+
file { '/etc/bar':
147+
ensure => file,
148+
backup => false,
149+
content => $baz,
150+
}
151+
152+
exec { 'qux':
153+
command => "echo ${foo}",
154+
}
155+
}
156+
PUPPET
157+
end
158+
159+
it 'detects one problem' do
160+
expect(problems).to have(1).problems
161+
end
162+
end
163+
164+
context 'case statement and an exec' do
165+
let(:code) do
166+
<<-PUPPET
167+
class foo {
168+
case bar {
169+
baz : {
170+
echo qux
171+
}
172+
}
173+
174+
exec { 'foo':
175+
command => "echo bar",
176+
}
177+
}
178+
PUPPET
179+
end
180+
181+
it 'detects zero problems' do
182+
expect(problems).to have(0).problems
183+
end
184+
end
185+
end
186+
end

0 commit comments

Comments
 (0)