|
| 1 | +COMMANDS = Array['command', 'onlyif', 'unless'] |
| 2 | +INTERPOLATED_STRINGS = Array[:DQPRE, :DQMID] |
| 3 | +USELESS_CHARS = Array[:WHITESPACE, :COMMA] |
| 4 | + |
| 5 | +PuppetLint.new_check(:check_unsafe_interpolations) do |
| 6 | + def check |
| 7 | + # Gather any exec commands' resources into an array |
| 8 | + exec_resources = resource_indexes.filter_map do |resource| |
| 9 | + resource_parameters = resource[:param_tokens].map(&:value) |
| 10 | + resource if resource[:type].value == 'exec' && !(COMMANDS & resource_parameters).empty? |
| 11 | + end |
| 12 | + |
| 13 | + # Iterate over title tokens and raise a warning if any are variables |
| 14 | + unless get_exec_titles.empty? |
| 15 | + get_exec_titles.each do |title| |
| 16 | + check_unsafe_title(title) |
| 17 | + end |
| 18 | + end |
| 19 | + |
| 20 | + # Iterate over each command found in any exec |
| 21 | + exec_resources.each do |command_resources| |
| 22 | + check_unsafe_interpolations(command_resources) |
| 23 | + end |
| 24 | + end |
| 25 | + |
| 26 | + # Iterate over the tokens in a title and raise a warning if an interpolated variable is found |
| 27 | + def check_unsafe_title(title) |
| 28 | + title.each do |token| |
| 29 | + notify_warning(token.next_code_token) if interpolated?(token) |
| 30 | + end |
| 31 | + end |
| 32 | + |
| 33 | + # Iterates over an exec resource and if a command, onlyif or unless paramter is found, it is checked for unsafe interpolations |
| 34 | + def check_unsafe_interpolations(command_resources) |
| 35 | + command_resources[:tokens].each do |token| |
| 36 | + # Skip iteration if token isn't a command of type :NAME |
| 37 | + next unless COMMANDS.include?(token.value) && token.type == :NAME |
| 38 | + # Don't check the command if it is parameterised |
| 39 | + next if parameterised?(token) |
| 40 | + |
| 41 | + check_command(token).each do |t| |
| 42 | + notify_warning(t) |
| 43 | + end |
| 44 | + end |
| 45 | + end |
| 46 | + |
| 47 | + # Raises a warning given a token and message |
| 48 | + def notify_warning(token) |
| 49 | + notify :warning, |
| 50 | + message: "unsafe interpolation of variable '#{token.value}' in exec command", |
| 51 | + line: token.line, |
| 52 | + column: token.column |
| 53 | + end |
| 54 | + |
| 55 | + # Iterates over the tokens in a command and adds it to an array of violations if it is an input variable |
| 56 | + def check_command(token) |
| 57 | + # Initialise variables needed in while loop |
| 58 | + rule_violations = [] |
| 59 | + current_token = token |
| 60 | + |
| 61 | + # Iterate through tokens in command |
| 62 | + while current_token.type != :NEWLINE |
| 63 | + # Check if token is a varibale and if it is parameterised |
| 64 | + rule_violations.append(current_token.next_code_token) if interpolated?(current_token) |
| 65 | + current_token = current_token.next_token |
| 66 | + end |
| 67 | + |
| 68 | + rule_violations |
| 69 | + end |
| 70 | + |
| 71 | + # A command is parameterised if its args are placed in an array |
| 72 | + # This function checks if the current token is a :FARROW and if so, if it is followed by an LBRACK |
| 73 | + def parameterised?(token) |
| 74 | + current_token = token |
| 75 | + while current_token.type != :NEWLINE |
| 76 | + return true if current_token.type == :FARROW && current_token.next_token.next_token.type == :LBRACK |
| 77 | + |
| 78 | + current_token = current_token.next_token |
| 79 | + end |
| 80 | + end |
| 81 | + |
| 82 | + # This function is a replacement for puppet_lint's title_tokens function which assumes titles have single quotes |
| 83 | + # This function adds a check for titles in double quotes where there could be interpolated variables |
| 84 | + def get_exec_titles |
| 85 | + result = [] |
| 86 | + tokens.each_with_index do |_token, token_idx| |
| 87 | + next if tokens[token_idx].value != 'exec' |
| 88 | + |
| 89 | + # We have a resource declaration. Now find the title |
| 90 | + tokens_array = [] |
| 91 | + # Check if title is an array |
| 92 | + if tokens[token_idx]&.next_code_token&.next_code_token&.type == :LBRACK |
| 93 | + # Get the start and end indices of the array of titles |
| 94 | + array_start_idx = tokens.rindex { |r| r.type == :LBRACK } |
| 95 | + array_end_idx = tokens.rindex { |r| r.type == :RBRACK } |
| 96 | + |
| 97 | + # Grab everything within the array |
| 98 | + title_array_tokens = tokens[(array_start_idx + 1)..(array_end_idx - 1)] |
| 99 | + tokens_array.concat(title_array_tokens.reject do |token| |
| 100 | + USELESS_CHARS.include?(token.type) |
| 101 | + end) |
| 102 | + result << tokens_array |
| 103 | + # Check if title is double quotes string |
| 104 | + elsif tokens[token_idx].next_code_token.next_code_token.type == :DQPRE |
| 105 | + # Find the start and end of the title |
| 106 | + title_start_idx = tokens.find_index(tokens[token_idx].next_code_token.next_code_token) |
| 107 | + title_end_idx = title_start_idx + index_offset_for(':', tokens[title_start_idx..tokens.length]) |
| 108 | + |
| 109 | + result << tokens[title_start_idx..title_end_idx] |
| 110 | + # Title is in single quotes |
| 111 | + else |
| 112 | + tokens_array.concat([tokens[token_idx].next_code_token.next_code_token]) |
| 113 | + |
| 114 | + result << tokens_array |
| 115 | + end |
| 116 | + end |
| 117 | + result |
| 118 | + end |
| 119 | + |
| 120 | + def interpolated?(token) |
| 121 | + INTERPOLATED_STRINGS.include?(token.type) |
| 122 | + end |
| 123 | + |
| 124 | + # Finds the index offset of the next instance of `value` in `tokens_slice` from the original index |
| 125 | + def index_offset_for(value, tokens_slice) |
| 126 | + tokens_slice.each_with_index do |token, i| |
| 127 | + return i if value.include?(token.value) |
| 128 | + end |
| 129 | + end |
| 130 | +end |
0 commit comments