cookstyle 8.7.6 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/config/chefstyle.yml +2 -0
  3. data/config/cookstyle.yml +145 -6
  4. data/lib/cookstyle/version.rb +2 -2
  5. data/lib/rubocop/chef/cookbook_helpers.rb +23 -0
  6. data/lib/rubocop/chef.rb +1 -1
  7. data/lib/rubocop/cop/chef/correctness/chef_application_fatal.rb +16 -4
  8. data/lib/rubocop/cop/chef/correctness/conditional_unified_mode_true.rb +91 -0
  9. data/lib/rubocop/cop/chef/correctness/empty_resource_guard.rb +18 -2
  10. data/lib/rubocop/cop/chef/correctness/execute_delete_file.rb +127 -0
  11. data/lib/rubocop/cop/chef/correctness/invalid_checksum.rb +79 -0
  12. data/lib/rubocop/cop/chef/correctness/invalid_platform_family_helper.rb +1 -1
  13. data/lib/rubocop/cop/chef/correctness/invalid_platform_family_values_in_case.rb +10 -0
  14. data/lib/rubocop/cop/chef/correctness/invalid_platform_values_in_case.rb +10 -0
  15. data/lib/rubocop/cop/chef/correctness/invalid_value_for_platform_helper.rb +1 -0
  16. data/lib/rubocop/cop/chef/correctness/openssl_password_helpers.rb +1 -1
  17. data/lib/rubocop/cop/chef/correctness/platform_version_string_comparison.rb +66 -0
  18. data/lib/rubocop/cop/chef/correctness/powershell_delete_file.rb +5 -5
  19. data/lib/rubocop/cop/chef/correctness/powershell_file_exists.rb +1 -1
  20. data/lib/rubocop/cop/chef/correctness/resource_with_none_action.rb +3 -3
  21. data/lib/rubocop/cop/chef/correctness/ruby_guard_without_block.rb +91 -0
  22. data/lib/rubocop/cop/chef/correctness/service_resource.rb +71 -20
  23. data/lib/rubocop/cop/chef/deprecation/chef_handler_recipe.rb +8 -1
  24. data/lib/rubocop/cop/chef/deprecation/chef_rest.rb +4 -1
  25. data/lib/rubocop/cop/chef/deprecation/chef_shellout.rb +2 -2
  26. data/lib/rubocop/cop/chef/deprecation/chef_sugar_helpers.rb +6 -0
  27. data/lib/rubocop/cop/chef/deprecation/chef_windows_platform_helper.rb +1 -1
  28. data/lib/rubocop/cop/chef/deprecation/chocolatey_package_uninstall_action.rb +3 -3
  29. data/lib/rubocop/cop/chef/deprecation/depends_partial_search.rb +4 -0
  30. data/lib/rubocop/cop/chef/deprecation/deprecated_mixins.rb +2 -2
  31. data/lib/rubocop/cop/chef/deprecation/hwrp_without_provides.rb +4 -4
  32. data/lib/rubocop/cop/chef/deprecation/hwrp_without_unified_mode_true.rb +2 -2
  33. data/lib/rubocop/cop/chef/deprecation/inherits_compat_resource.rb +3 -3
  34. data/lib/rubocop/cop/chef/deprecation/legacy_yum_cookbook_recipes.rb +9 -1
  35. data/lib/rubocop/cop/chef/deprecation/locale_lc_all_property.rb +5 -0
  36. data/lib/rubocop/cop/chef/deprecation/resource_uses_only_resource_name.rb +9 -1
  37. data/lib/rubocop/cop/chef/deprecation/resource_uses_provider_base_method.rb +4 -0
  38. data/lib/rubocop/cop/chef/deprecation/ruby_block_create_action.rb +42 -3
  39. data/lib/rubocop/cop/chef/deprecation/use_automatic_resource_name.rb +9 -0
  40. data/lib/rubocop/cop/chef/deprecation/use_yaml_dump.rb +5 -0
  41. data/lib/rubocop/cop/chef/deprecation/xml_ruby_recipe.rb +5 -1
  42. data/lib/rubocop/cop/chef/modernize/apt_default_recipe.rb +1 -1
  43. data/lib/rubocop/cop/chef/modernize/build_essential.rb +4 -1
  44. data/lib/rubocop/cop/chef/modernize/chef_14_resources.rb +8 -0
  45. data/lib/rubocop/cop/chef/modernize/chef_15_resources.rb +11 -0
  46. data/lib/rubocop/cop/chef/modernize/chef_gem_nokogiri.rb +4 -0
  47. data/lib/rubocop/cop/chef/modernize/conditional_using_test.rb +10 -3
  48. data/lib/rubocop/cop/chef/modernize/cron_d_file_or_template.rb +6 -4
  49. data/lib/rubocop/cop/chef/modernize/databag_helpers.rb +6 -3
  50. data/lib/rubocop/cop/chef/modernize/depends_chef_vault_cookbook.rb +4 -0
  51. data/lib/rubocop/cop/chef/modernize/depends_chocolatey_cookbooks.rb +7 -0
  52. data/lib/rubocop/cop/chef/modernize/depends_kernel_module_cookbook.rb +6 -0
  53. data/lib/rubocop/cop/chef/modernize/depends_locale_cookbook.rb +6 -0
  54. data/lib/rubocop/cop/chef/modernize/depends_openssl_cookbook.rb +9 -0
  55. data/lib/rubocop/cop/chef/modernize/depends_timezone_lwrp_cookbook.rb +4 -0
  56. data/lib/rubocop/cop/chef/modernize/depends_windows_firewall_cookbook.rb +8 -0
  57. data/lib/rubocop/cop/chef/modernize/depends_zypper_cookbook.rb +7 -0
  58. data/lib/rubocop/cop/chef/modernize/dsl_include_in_resource.rb +1 -1
  59. data/lib/rubocop/cop/chef/modernize/execute_apt_update.rb +3 -1
  60. data/lib/rubocop/cop/chef/modernize/execute_archive_extract.rb +121 -0
  61. data/lib/rubocop/cop/chef/modernize/execute_update_alternatives.rb +100 -0
  62. data/lib/rubocop/cop/chef/modernize/includes_mixin_shellout.rb +4 -0
  63. data/lib/rubocop/cop/chef/modernize/libarchive_file.rb +5 -3
  64. data/lib/rubocop/cop/chef/modernize/ohai_default_recipe.rb +6 -1
  65. data/lib/rubocop/cop/chef/modernize/powershell_download_file.rb +64 -0
  66. data/lib/rubocop/cop/chef/modernize/powershell_expand_archive.rb +8 -1
  67. data/lib/rubocop/cop/chef/modernize/powershell_guard_interpreter.rb +4 -4
  68. data/lib/rubocop/cop/chef/modernize/sc_windows_resource.rb +2 -2
  69. data/lib/rubocop/cop/chef/modernize/seven_zip_archive.rb +7 -1
  70. data/lib/rubocop/cop/chef/modernize/shellouts_to_chocolatey.rb +15 -4
  71. data/lib/rubocop/cop/chef/modernize/use_require_relative.rb +1 -1
  72. data/lib/rubocop/cop/chef/modernize/windows_default_recipe.rb +1 -1
  73. data/lib/rubocop/cop/chef/modernize/windows_zipfile.rb +8 -2
  74. data/lib/rubocop/cop/chef/modernize/zipfile_resource.rb +7 -1
  75. data/lib/rubocop/cop/chef/modernize/zypper_repo.rb +2 -2
  76. data/lib/rubocop/cop/chef/redundant/custom_resource_with_allowed_actions.rb +10 -0
  77. data/lib/rubocop/cop/chef/redundant/double_compile_time.rb +22 -8
  78. data/lib/rubocop/cop/chef/redundant/service_guard_on_stop_disable.rb +117 -0
  79. data/lib/rubocop/cop/chef/redundant/string_property_with_nil_default.rb +2 -2
  80. data/lib/rubocop/cop/chef/redundant/unnecessary_name_property.rb +1 -1
  81. data/lib/rubocop/cop/chef/security/insecure_remote_file_source.rb +82 -0
  82. data/lib/rubocop/cop/chef/security/ssh_private_key.rb +9 -0
  83. data/lib/rubocop/cop/chef/sharing/empty_property_description.rb +62 -0
  84. data/lib/rubocop/cop/chef/style/include_recipe_with_parentheses.rb +5 -2
  85. data/lib/rubocop/cop/chef/style/true_false_resource_properties.rb +1 -1
  86. data/lib/rubocop/monkey_patches/directive_comment.rb +21 -3
  87. metadata +15 -4
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright:: Copyright (c) 2016-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
+ # Author:: Tim Smith (<tsmith84@gmail.com>)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module RuboCop
19
+ module Cop
20
+ module Chef
21
+ module Correctness
22
+ # Use the `file` or `directory` resources built into Chef Infra Client with the `:delete` action
23
+ # to remove files and directories instead of shelling out to `rm`. The resources are idempotent,
24
+ # report correctly on what they changed, and work without a shell.
25
+ #
26
+ # The two resources are not interchangeable and the one you pick has to match what is on disk.
27
+ # `directory` asserts the path really is a directory before deleting, so it raises
28
+ # `Cannot delete directory[...]` on a file even with `recursive true`, and `file` calls
29
+ # `File.delete`, so it raises `Errno::EISDIR` on a directory. A `directory` delete also needs
30
+ # `recursive true` for anything that isn't empty, or it raises `Errno::ENOTEMPTY`.
31
+ #
32
+ # Only a single `rm` of one path is flagged. A command containing a glob, a shell operator, or
33
+ # anything beyond the one deletion is left alone, since the `file` and `directory` resources
34
+ # can't express those.
35
+ #
36
+ # @example
37
+ #
38
+ # # bad -- rm without -r, which the shell will not let you point at a directory
39
+ # execute 'rm /etc/foo.conf'
40
+ #
41
+ # # good
42
+ # file '/etc/foo.conf' do
43
+ # action :delete
44
+ # end
45
+ #
46
+ # # bad -- a recursive rm of a directory
47
+ # execute 'delete the thing' do
48
+ # command 'rm -rf /opt/thing'
49
+ # end
50
+ #
51
+ # # good
52
+ # directory '/opt/thing' do
53
+ # recursive true
54
+ # action :delete
55
+ # end
56
+ #
57
+ class ExecuteDeleteFile < Base
58
+ include RuboCop::Chef::CookbookHelpers
59
+
60
+ # The two resources are not interchangeable, so the message has to name the right one.
61
+ # `directory` asserts the path really is a directory before deleting, and `file` calls
62
+ # File.delete, so pointing at the wrong one fails the converge rather than doing nothing.
63
+ FILE_MSG = 'Use the `file` resource with the :delete action to remove a file instead of shelling out to rm'
64
+ DIRECTORY_MSG = 'Use the `directory` resource with `recursive true` and the :delete action to remove a directory instead of shelling out to rm. Use the `file` resource instead if the path is a file.'
65
+
66
+ RESTRICT_ON_SEND = [:execute].freeze
67
+
68
+ # a lone rm of a single path: optional flags, one target, nothing else on the line.
69
+ # the target excludes the shell metacharacters used for substitution and expansion, since
70
+ # a path the shell computes at runtime isn't something the file/directory resources can take
71
+ DELETE_COMMAND = %r{\A\s*(?:/bin/|/usr/bin/)?rm\s+(?<flags>(?:-[a-zA-Z]+\s+)*)(?<path>[^\s;&|<>$`(){}]+)\s*\z}.freeze
72
+
73
+ # the shell resources whose script lives in a code property
74
+ SCRIPT_RESOURCES = %i(bash sh csh ksh zsh).freeze
75
+
76
+ def_node_matcher :execute_with_command_name?, '(send nil? :execute (str $_))'
77
+
78
+ def on_send(node)
79
+ execute_with_command_name?(node) do |command|
80
+ message = message_for(command)
81
+ add_offense(node, message: message, severity: :refactor) if message
82
+ end
83
+ end
84
+
85
+ def on_block(node)
86
+ match_property_in_resource?(:execute, 'command', node) do |property|
87
+ report_property(node, property)
88
+ end
89
+
90
+ match_property_in_resource?(SCRIPT_RESOURCES, 'code', node) do |property|
91
+ report_property(node, property)
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def report_property(node, property)
98
+ command = method_arg_ast_to_string(property)
99
+ return unless command
100
+
101
+ message = message_for(command)
102
+ add_offense(node, message: message, severity: :refactor) if message
103
+ end
104
+
105
+ # Does the command do nothing but delete one named path, and if so which resource replaces
106
+ # it? Globs are excluded because the file and directory resources take a single path rather
107
+ # than a pattern.
108
+ #
109
+ # Without -r the shell refuses to remove a directory, so a bare `rm` provably targets a
110
+ # file and `file` is the right answer. With -r the author is removing a tree, so
111
+ # `directory` is, though the path could still be a file that -rf was used on out of habit.
112
+ #
113
+ # @param [String] command
114
+ #
115
+ # @return [String, nil] the message to report, or nil when this isn't a simple delete
116
+ def message_for(command)
117
+ match = DELETE_COMMAND.match(command)
118
+ return if match.nil?
119
+ return if match[:path].match?(/[*?\[]/)
120
+
121
+ match[:flags].match?(/-[a-zA-Z]*r/i) ? DIRECTORY_MSG : FILE_MSG
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright:: Copyright (c) 2016-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
+ # Author:: Tim Smith (<tsmith84@gmail.com>)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module RuboCop
19
+ module Cop
20
+ module Chef
21
+ module Correctness
22
+ # The `checksum` property of the file downloading resources is a SHA-256 digest. An MD5 or
23
+ # SHA-1 digest can never match, so the resource fails the run with a checksum mismatch rather
24
+ # than installing anything.
25
+ #
26
+ # Only digests that are unambiguously the wrong algorithm are flagged: 32 character (MD5) and
27
+ # 40 character (SHA-1) hex strings. Anything else is left alone.
28
+ #
29
+ # @example
30
+ #
31
+ # # bad
32
+ # remote_file '/tmp/foo.tar.gz' do
33
+ # source 'https://example.com/foo.tar.gz'
34
+ # checksum 'd41d8cd98f00b204e9800998ecf8427e'
35
+ # end
36
+ #
37
+ # # good
38
+ # remote_file '/tmp/foo.tar.gz' do
39
+ # source 'https://example.com/foo.tar.gz'
40
+ # checksum 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
41
+ # end
42
+ #
43
+ class InvalidChecksum < Base
44
+ include RuboCop::Chef::CookbookHelpers
45
+
46
+ MSG = 'The checksum property takes a SHA-256 digest. An MD5 or SHA-1 digest can never match, so the run fails with a checksum mismatch.'
47
+
48
+ # the file downloading resources that validate a SHA-256 checksum
49
+ RESOURCES = %i(remote_file windows_package msu_package dmg_package).freeze
50
+
51
+ # hex digest lengths of the algorithms people reach for by mistake, with the article that
52
+ # reads correctly in front of each name
53
+ WRONG_ALGORITHM_LENGTHS = { 32 => 'an MD5', 40 => 'a SHA-1' }.freeze
54
+
55
+ def on_block(node)
56
+ match_property_in_resource?(RESOURCES, 'checksum', node) do |checksum|
57
+ value = checksum.arguments.first
58
+ next unless value&.str_type?
59
+
60
+ algorithm = WRONG_ALGORITHM_LENGTHS[value.value.length]
61
+ next unless algorithm && hex?(value.value)
62
+
63
+ add_offense(checksum, message: "#{MSG} This looks like #{algorithm} digest.", severity: :refactor)
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # @param [String] string
70
+ #
71
+ # @return [Boolean]
72
+ def hex?(string)
73
+ string.match?(/\A\h+\z/)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -27,7 +27,7 @@ module RuboCop
27
27
  # platform_family?('redhat')
28
28
  # platform_family?('sles')
29
29
  #
30
- # # bad
30
+ # # good
31
31
  # platform_family?('rhel')
32
32
  # platform_family?('suse')
33
33
  #
@@ -27,6 +27,16 @@ module RuboCop
27
27
  # case node['platform_family']
28
28
  # when 'redhat'
29
29
  # puts "I'm on a RHEL-like system"
30
+ # when 'debian'
31
+ # puts "I'm on a Debian-like system"
32
+ # end
33
+ #
34
+ # # good
35
+ # case node['platform_family']
36
+ # when 'rhel'
37
+ # puts "I'm on a RHEL-like system"
38
+ # when 'debian'
39
+ # puts "I'm on a Debian-like system"
30
40
  # end
31
41
  #
32
42
  class InvalidPlatformFamilyInCase < Base
@@ -27,6 +27,16 @@ module RuboCop
27
27
  # case node['platform']
28
28
  # when 'rhel'
29
29
  # puts "I'm on a Red Hat system!"
30
+ # when 'ubuntu'
31
+ # puts "I'm on Ubuntu!"
32
+ # end
33
+ #
34
+ # # good
35
+ # case node['platform']
36
+ # when 'redhat'
37
+ # puts "I'm on a Red Hat system!"
38
+ # when 'ubuntu'
39
+ # puts "I'm on Ubuntu!"
30
40
  # end
31
41
  #
32
42
  class InvalidPlatformInCase < Base
@@ -28,6 +28,7 @@ module RuboCop
28
28
  # %w(rhel mac_os_x_server) => { 'default' => 'foo' },
29
29
  # %w(sles) => { 'default' => 'bar' }
30
30
  # )
31
+ #
31
32
  # # good
32
33
  # value_for_platform(
33
34
  # %w(redhat mac_os_x) => { 'default' => 'foo' },
@@ -31,7 +31,7 @@ module RuboCop
31
31
  def_node_matcher :openssl_helper?, <<~PATTERN
32
32
  (const
33
33
  (const
34
- (const nil? :Opscode) :OpenSSL) :Password)
34
+ (const {nil? cbase} :Opscode) :OpenSSL) :Password)
35
35
  PATTERN
36
36
 
37
37
  def on_const(node)
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright:: Copyright (c) 2016-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
+ # Author:: Tim Smith (<tsmith84@gmail.com>)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module RuboCop
19
+ module Cop
20
+ module Chef
21
+ module Correctness
22
+ # Ohai reports version attributes as strings, so comparing one with `<`, `>`, `<=`, or `>=`
23
+ # compares them character by character rather than as versions. That gives the wrong answer
24
+ # whenever the version numbers have different digit counts:
25
+ #
26
+ # '7.9' >= '10' # => true, because '7' sorts after '1'
27
+ # '9' > '10' # => true
28
+ #
29
+ # Compare the major version as an integer with `node['platform_version'].to_i`, or compare
30
+ # full versions with `Gem::Version`.
31
+ #
32
+ # @example
33
+ #
34
+ # # bad
35
+ # node['platform_version'] >= '10'
36
+ # node['platform_version'] < '7.4'
37
+ #
38
+ # # good
39
+ # node['platform_version'].to_i >= 10
40
+ # Gem::Version.new(node['platform_version']) >= Gem::Version.new('7.4')
41
+ #
42
+ # # good - equality is a plain string match, which is fine
43
+ # node['platform_version'] == '10'
44
+ #
45
+ class PlatformVersionStringComparison < Base
46
+ MSG = 'Ohai version attributes are strings, so comparing them with an ordering operator compares them character by character. Use .to_i for a major version comparison, or Gem::Version to compare full versions.'
47
+ RESTRICT_ON_SEND = %i(< > <= >=).freeze
48
+
49
+ # the flat Ohai attributes that hold a dotted version string
50
+ def_node_matcher :version_attribute_comparison?, <<-PATTERN
51
+ (send
52
+ (send (send nil? :node) :[] (str {"platform_version" "os_version"}))
53
+ {:< :> :<= :>=}
54
+ (str _))
55
+ PATTERN
56
+
57
+ def on_send(node)
58
+ version_attribute_comparison?(node) do
59
+ add_offense(node, severity: :refactor)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -26,13 +26,13 @@ module RuboCop
26
26
  # # bad
27
27
  # powershell_script 'Cleanup old files' do
28
28
  # code 'Remove-Item C:\Windows\foo\bar.txt'
29
- # only_if { ::File.exist?('C:\\Windows\\foo\\bar.txt') }
29
+ # only_if { ::File.exist?('C:\Windows\foo\bar.txt') }
30
30
  # end
31
31
  #
32
- # ### correct
33
- # file 'C:\Windows\foo\bar.txt' do
34
- # action :delete
35
- # end
32
+ # # good
33
+ # file 'C:\Windows\foo\bar.txt' do
34
+ # action :delete
35
+ # end
36
36
  #
37
37
  class PowershellScriptDeleteFile < Base
38
38
  include RuboCop::Chef::CookbookHelpers
@@ -24,7 +24,7 @@ module RuboCop
24
24
  # @example
25
25
  #
26
26
  # # bad
27
- # powershell_out('Test-Path "C:\\Program Files\\LAPS\\CSE\\AdmPwd.dll"').stdout.strip == 'True'
27
+ # powershell_out('Test-Path "C:\Program Files\LAPS\CSE\AdmPwd.dll"').stdout.strip == 'True'
28
28
  #
29
29
  # # good
30
30
  # ::File.exist?('C:\Program Files\LAPS\CSE\AdmPwd.dll')
@@ -41,10 +41,10 @@ module RuboCop
41
41
 
42
42
  def on_block(node)
43
43
  match_property_in_resource?(nil, 'action', node) do |action_node|
44
- action_node.arguments.each do |action|
45
- next unless action.source == ':none'
44
+ each_action_symbol(action_node) do |action, prefix|
45
+ next unless action.value == :none
46
46
  add_offense(action, severity: :refactor) do |corrector|
47
- corrector.replace(action, ':nothing')
47
+ corrector.replace(action, "#{prefix}nothing")
48
48
  end
49
49
  end
50
50
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright:: Copyright (c) 2016-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
+ # Author:: Tim Smith (<tsmith84@gmail.com>)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module RuboCop
19
+ module Cop
20
+ module Chef
21
+ module Correctness
22
+ # A `not_if`/`only_if` guard takes either a string, which is run as a shell command, or a block,
23
+ # which is run as Ruby. Passing a Ruby expression directly gives the guard the expression's
24
+ # *result* rather than the expression, because it is evaluated while the recipe is compiled.
25
+ #
26
+ # A guard that receives `true` or `false` raises at converge time:
27
+ #
28
+ # ArgumentError: Invalid only_if/not_if command, expected a string: true (TrueClass)
29
+ #
30
+ # Worse, an expression that happens to return a string is accepted and then run as a shell
31
+ # command, so the guard quietly tests something entirely different to what was intended.
32
+ #
33
+ # Only expressions that clearly produce a boolean are flagged, so a shell command built in Ruby
34
+ # and held in a variable is left alone.
35
+ #
36
+ # @example
37
+ #
38
+ # # bad
39
+ # not_if ::File.exist?('/etc/foo')
40
+ # only_if node['foo']['version'] == '1.0'
41
+ #
42
+ # # good
43
+ # not_if { ::File.exist?('/etc/foo') }
44
+ # only_if { node['foo']['version'] == '1.0' }
45
+ #
46
+ # # good - a string guard runs as a shell command
47
+ # not_if 'test -f /etc/foo'
48
+ #
49
+ class RubyGuardWithoutBlock < Base
50
+ extend AutoCorrector
51
+
52
+ MSG = 'A Ruby expression used as a resource guard has to be wrapped in a block. Passing it directly hands the guard the expression result, which raises at converge time or silently runs a shell command.'
53
+ RESTRICT_ON_SEND = [:not_if, :only_if].freeze
54
+
55
+ # comparison and negation operators always produce a boolean
56
+ BOOLEAN_OPERATORS = %i(== != < > <= >= =~ !~ ! equal? eql?).freeze
57
+
58
+ def_node_matcher :guard_with_argument, '(send nil? {:not_if :only_if} $_)'
59
+
60
+ def on_send(node)
61
+ guard_with_argument(node) do |argument|
62
+ next unless boolean_expression?(argument)
63
+
64
+ add_offense(node, severity: :refactor) do |corrector|
65
+ corrector.replace(node, "#{node.method_name} { #{argument.source} }")
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # Can we tell that this expression produces a boolean rather than a shell command? Anything
73
+ # we can't be sure about, such as a bare method call or a local variable, is left alone: it
74
+ # may well hold a perfectly good command string.
75
+ #
76
+ # @param [RuboCop::AST::Node] node
77
+ #
78
+ # @return [Boolean]
79
+ def boolean_expression?(node)
80
+ return true if node.boolean_type? || node.defined_type?
81
+ return true if node.operator_keyword? # && and ||
82
+ return node.children.one? && boolean_expression?(node.children.first) if node.begin_type?
83
+ return false unless node.send_type?
84
+
85
+ node.method_name.to_s.end_with?('?') || BOOLEAN_OPERATORS.include?(node.method_name)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  #
3
- # Copyright:: 2016, Chris Henry
3
+ # Copyright:: Copyright (c) 2016-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
+ # Author:: Tim Smith (<tsmith84@gmail.com>)
4
5
  #
5
6
  # Licensed under the Apache License, Version 2.0 (the "License");
6
7
  # you may not use this file except in compliance with the License.
@@ -18,36 +19,86 @@ module RuboCop
18
19
  module Cop
19
20
  module Chef
20
21
  module Correctness
21
- # Use a service resource to start and stop services
22
+ # Use the `service` resource to manage services instead of shelling out. The resource is
23
+ # idempotent, picks the right init system for the platform, and reports correctly on what it
24
+ # changed. A shelled out command runs on every converge whether or not anything needed to
25
+ # change, so the run reports the resource as updated every time.
22
26
  #
23
- # @example when command starts a service
27
+ # @example
24
28
  #
25
29
  # # bad
26
- # command "/etc/init.d/mysql start"
27
- # command "/sbin/service/memcached start"
30
+ # execute 'restart apache' do
31
+ # command 'systemctl restart httpd'
32
+ # end
33
+ #
34
+ # execute '/etc/init.d/httpd start'
35
+ #
36
+ # # good
37
+ # service 'httpd' do
38
+ # action :restart
39
+ # end
28
40
  #
29
41
  class ServiceResource < Base
30
- MSG = 'Use a service resource to start and stop services'
31
- RESTRICT_ON_SEND = [:command].freeze
42
+ MSG = 'Use the service resource to manage services instead of shelling out to an init script, service, systemctl, or a similar tool'
43
+ RESTRICT_ON_SEND = %i(command code execute).freeze
44
+
45
+ # the state changing verbs. `status` is left out: a status check is usually a guard rather
46
+ # than something the service resource replaces
47
+ SERVICE_ACTIONS = %w(
48
+ start stop restart reload force-reload try-restart reload-or-restart
49
+ enable disable mask unmask
50
+ ).freeze
51
+
52
+ # commands whose service action is captured and checked against SERVICE_ACTIONS
53
+ ACTION_COMMANDS = [
54
+ %r{\A/(?:etc/init\.d|etc/rc\.d|usr/local/etc/rc\.d)/\S+\s+(\S+)}, # /etc/init.d/httpd start
55
+ %r{\A(?:/usr/sbin/|/sbin/)?service\s+\S+\s+(\S+)}, # service httpd start
56
+ %r{\A(?:/usr/bin/|/bin/)?systemctl\s+(?:--\S+\s+)*(\S+)\s+\S+}, # systemctl start httpd
57
+ /\Ainvoke-rc\.d\s+\S+\s+(\S+)/, # invoke-rc.d httpd start
58
+ /\Ainitctl\s+(\S+)\s+\S+/, # initctl start httpd
59
+ /\Asvcadm\s+(\S+)\s+\S+/, # svcadm enable httpd
60
+ ].freeze
61
+
62
+ # commands that manage a service whatever the rest of the line says
63
+ DIRECT_COMMANDS = [
64
+ /\A(?:start|stop|restart)\s+\S+\z/, # upstart: start httpd
65
+ /\Alaunchctl\s+(?:load|unload|start|stop|enable|disable|bootstrap|bootout)\s/,
66
+ /\Achkconfig\s+(?:--\S+\s+)*\S+\s+(?:on|off)\z/, # chkconfig httpd on
67
+ /\Aupdate-rc\.d\s/, # update-rc.d httpd defaults
68
+ /\A(?:sc|net)\s+(?:start|stop|config)\s+\S+/i, # windows: net start httpd
69
+ ].freeze
70
+
71
+ # a command doing more than the one service action can't be swapped for the resource
72
+ SHELL_OPERATORS = ['&&', '||', ';', '|', '`', '$('].freeze
32
73
 
33
- def_node_matcher :execute_command?, <<-PATTERN
34
- (send nil? :command $str)
35
- PATTERN
74
+ def_node_matcher :command_property?, '(send nil? {:command :code} $str)'
75
+ def_node_matcher :execute_with_command_name?, '(send nil? :execute $str)'
36
76
 
37
77
  def on_send(node)
38
- execute_command?(node) do |command|
39
- if starts_service?(command)
40
- add_offense(command, severity: :refactor)
41
- end
78
+ command_property?(node) do |command|
79
+ add_offense(command, severity: :refactor) if manages_service?(command.value)
80
+ end
81
+
82
+ execute_with_command_name?(node) do |command|
83
+ add_offense(node, severity: :refactor) if manages_service?(command.value)
42
84
  end
43
85
  end
44
86
 
45
- def starts_service?(cmd)
46
- cmd_str = cmd.to_s
47
- (cmd_str.include?('/etc/init.d') || ['service ', '/sbin/service ',
48
- 'start ', 'stop ', 'invoke-rc.d '].any? do |service_cmd|
49
- cmd_str.start_with?(service_cmd)
50
- end) && %w(start stop restart reload).any? { |a| cmd_str.include?(a) }
87
+ private
88
+
89
+ # @param [String] command the literal command string
90
+ #
91
+ # @return [Boolean]
92
+ def manages_service?(command)
93
+ cmd = command.strip
94
+ return false if SHELL_OPERATORS.any? { |operator| cmd.include?(operator) }
95
+
96
+ ACTION_COMMANDS.each do |pattern|
97
+ match = pattern.match(cmd)
98
+ return true if match && SERVICE_ACTIONS.include?(match[1].downcase)
99
+ end
100
+
101
+ DIRECT_COMMANDS.any? { |pattern| cmd.match?(pattern) }
51
102
  end
52
103
  end
53
104
  end
@@ -27,6 +27,13 @@ module RuboCop
27
27
  # include_recipe 'chef_handler'
28
28
  # include_recipe 'chef_handler::default'
29
29
  #
30
+ # # good
31
+ # # the chef_handler resource ships in Chef Infra Client, so no recipe include is needed
32
+ # chef_handler 'MyHandler' do
33
+ # source '/etc/chef/handlers/my_handler.rb'
34
+ # action :enable
35
+ # end
36
+ #
30
37
  class ChefHandlerRecipe < Base
31
38
  include RangeHelp
32
39
  extend AutoCorrector
@@ -35,7 +42,7 @@ module RuboCop
35
42
  RESTRICT_ON_SEND = [:include_recipe].freeze
36
43
 
37
44
  def_node_matcher :chef_handler_recipe?, <<-PATTERN
38
- (send nil? :include_recipe (str {"chef_handler" "chef_handler::default"}))
45
+ (send {nil? (send nil? :run_context)} :include_recipe (str {"chef_handler" "chef_handler::default"}))
39
46
  PATTERN
40
47
 
41
48
  def on_send(node)
@@ -27,6 +27,9 @@ module RuboCop
27
27
  # require 'chef/rest'
28
28
  # Chef::REST::RESTRequest.new(:GET, FOO, nil).call
29
29
  #
30
+ # # good
31
+ # Chef::ServerAPI.new(Chef::Config[:chef_server_url]).get('nodes')
32
+ #
30
33
  class UsesChefRESTHelpers < Base
31
34
  MSG = "Don't use the helpers in Chef::REST which were removed in Chef Infra Client 13"
32
35
  RESTRICT_ON_SEND = [:require].freeze
@@ -36,7 +39,7 @@ module RuboCop
36
39
  PATTERN
37
40
 
38
41
  def_node_matcher :rest_const?, <<-PATTERN
39
- (const (const nil? :Chef) :REST)
42
+ (const (const {nil? cbase} :Chef) :REST)
40
43
  PATTERN
41
44
 
42
45
  def on_send(node)
@@ -43,7 +43,7 @@ module RuboCop
43
43
  def_node_matcher :include_shellout?, <<-PATTERN
44
44
  (send nil? :include
45
45
  (const
46
- (const nil? :Chef) :ShellOut))
46
+ (const {nil? cbase} :Chef) :ShellOut))
47
47
  PATTERN
48
48
 
49
49
  def_node_matcher :require_shellout?, <<-PATTERN
@@ -53,7 +53,7 @@ module RuboCop
53
53
  def_node_matcher :shellout_new?, <<-PATTERN
54
54
  (send
55
55
  (const
56
- (const nil? :Chef) :ShellOut) :new
56
+ (const {nil? cbase} :Chef) :ShellOut) :new
57
57
  ... )
58
58
  PATTERN
59
59
 
@@ -41,6 +41,12 @@ module RuboCop
41
41
  # nexentacore?
42
42
  # opensolaris?
43
43
  #
44
+ # # good
45
+ # # the equivalents now shipping in Chef Infra Client and in Ruby itself
46
+ # vagrant?
47
+ # node.recipe?('foo::bar')
48
+ # shell_out!('/opt/app/bin/app --version', out: File::NULL)
49
+ #
44
50
  class ChefSugarHelpers < Base
45
51
  MSG = 'Do not use legacy chef-sugar helper methods, which will not be moved into Chef Infra Client itself. For a complete set of chef-sugar helpers now shipping in Chef Infra Client itself see https://github.com/chef/chef/tree/main/chef-utils#getting-started'
46
52
  RESTRICT_ON_SEND = [:vagrant_key?, :vagrant_domain?, :vagrant_user?, :require_chef_gem, :best_ip_for, :nexus?, :ios_xr?, :ruby_20?, :ruby_19?, :includes_recipe?, :wrlinux?, :dev_null, :nexentacore_platform?, :opensolaris_platform?, :nexentacore?, :opensolaris?].freeze