Chef Foodcritic rule not catching attribute strings - ruby

I have written a foodcritic rule to catch any attempt to write to a blacklist of directories/files under the /etc directory.
When blacklisted paths are passed to resource declarations as strings in a recipe, the rule triggers, however when they are passed as attributes, the rule does not trigger:
#resources = [
'file',
'template',
'remote_file',
'remote_directory',
'directory'
]
#blacklist = [
'/etc/ssh/',
'/etc/init',
...
]
rule 'RULE001', 'do not manipulate /etc other than init/,init.d/ & default/' do
tags %w(security)
recipe do |ast|
violations = []
#resources.each do |resource_type|
violations << find_resources(ast, type: resource_type).select do |resource|
res_str = (resource_attribute(resource, 'path' || resource_name(resource)).to_s
#blacklist.any? { |cmd| res_str.include? cmd }
end
end
violations.flatten
end
end
Testing this using the below, the literal strings are caught, however when passed as attributes they are passed. Can anyone see what I'm missing?
attributes/default.rb:
default['testbook']['etc-test'] = '/etc/ssh/test.conf'
default['testbook']['etc-dir-test'] = 'etc/ssh/somedir/'
recipes/default.rb:
#template '/etc/ssh/test.conf' do <-- caught
template node['testbook']['etc-test'] do #<-- not caught
source 'test.conf'
owner 'nobody'
group 'nobody'
mode '0644'
action :create
end
#directory '/etc/ssh/somedir' do <-- caught
directory node['testbook']['etc-dir-test'] do <-- not caught
action :create
end

Yes, this isn't something you can fully handle via static analysis. Foodcritic and tools like it can only handle things that are static in the code, anything that could vary at runtime won't be known.

Related

List properties of a resource

I'm implementing a custom resource which is basically a facade to an existing resource (in the example below its the vault_certificate resource).
Using the existing resource this code is valid:
certificate = vault_certificate 'a common name' do
combine_certificate_and_chain true
output_certificates false # Just to decrease the chef-client run output
vault_path "pki/issue/#{node['deployment']}"
end
template "a path" do
source 'nginx/dummy.conf.erb'
variables(
certificate: certificate.certificate_filename
key: certificate.key_filename
)
end
Notice I can invoke certificate.certificate_filename or certificate.key_filename. Or more generally I can read any property defined by the vault_certificate resource.
Now with the new resource (sort of a facade to vault_certificate)
provides :vault_certificate_handle_exceptions
unified_mode true
property :common_name, String, name_property: true
property :max_retries, Integer, default: 5
action :create do
require 'retries'
# new_resource.max_retries is being used inside the retry_options. I omitted that part as its not relevant for the question
with_retries(retry_options) do
begin
vault_certificate new_resource.common_name do
combine_certificate_and_chain true
output_certificates false # Just to decrease the chef-client run output
vault_path "pki/issue/#{node['deployment']}"
ignore_failure :quiet
end
rescue Vault::HTTPClientError => e
data = JSON.parse(e.errors)['data']
if data['error'] == 'Certificate not found locally'
# This error is one we can recover from (actually we are expecting it). This raise with VaultCertificateError will trigger the with_retries.
raise VaultCertificateError.new("Waiting for the certificate to appear in the store (because I'm not the leader)", data)
else
# Any other error means something really went wrong.
raise e
end
end
end
end
If I now use this resource and try to invoke .certificate_filename or .key_filename:
certificate = vault_certificate_handle_exceptions 'a common name' do
action :create
end
template "a path" do
source 'nginx/dummy.conf.erb'
variables(
certificate: certificate.certificate_filename
key: certificate.key_filename
)
end
I get an error saying the method certificate_filename (or key_filename) is not defined for vault_certificate_handle_exceptions. To solve it I resorted to this hack:
provides :vault_certificate_handle_exceptions
unified_mode true
property :common_name, String, name_property: true
property :max_retries, Integer, default: 5
action :create do
require 'retries'
# new_resource.max_retries is being used inside the retry_options. I omitted that part as its not relevant for the question
with_retries(retry_options) do
begin
cert = vault_certificate new_resource.common_name do
combine_certificate_and_chain true
output_certificates false # Just to decrease the chef-client run output
vault_path "pki/issue/#{node['deployment']}"
ignore_failure :quiet
end
# These lines ensure we can read the vault_certificate properties as if they were properties of this resource (vault_certificate_handle_exceptions)
Chef::ResourceResolver.resolve(cert.resource_name).properties.keys.each do |name|
new_resource.send(:define_singleton_method, name.to_sym) do
cert.send(name.to_sym)
end
end
rescue Vault::HTTPClientError => e
data = JSON.parse(e.errors)['data']
if data['error'] == 'Certificate not found locally'
# This error is one we can recover from (actually we are expecting it). This raise with VaultCertificateError will trigger the with_retries.
raise VaultCertificateError.new("Waiting for the certificate to appear in the store (because I'm not the leader)", data)
else
# Any other error means something really went wrong.
raise e
end
end
end
end
Is there a cleaner way to achieve this? If not, is there a more direct way to list all the properties of a resource? I thought cert.properties would work, but no luck there.

Only_if with Loop in Ruby_block to pass Foodcritic FC022

I've been trying to figure out the better approach to use condition inside a ruby_block to avoid FC022 rulewhen the code gets evaluated by Foodcritic.
FC022: Resource condition within loop may not behave as expected
The code of mine is as follows
ruby_block 'file configuration' do
block do
files = [
'/etc/file01.conf',
'/etc/file02.conf',
]
files.each do |f|
file = Chef::Util:FileEdit.new(f)
file.insert_line_if_no_match('something', 'something')
file.write_file
only_if { ::File.exist?(f) }
end
end
Removing only_if will pass FC022 rule in Footcritic.

Chef Custom Resources : mutable property or an alternative

I read that as of 2016 OpsCode recommends against LWRP and the alternative HWRP. They rather recommend to use custom resources.
While this makes sense it leaves a lot to be desired in terms of leveraging ruby (unless I've misunderstood?)
I would like to be able to modify an array based on some boolean properties. I can then use that array to pass to the template, as in:
property :get_iptables_info, [TrueClass, FalseClass], default: false
property :get_pkglist, [TrueClass, FalseClass], default: false
property :cw_logs, Array, default: [], required: false
action :create do
ruby_block 'cw_iptables' do
block do
new_resource.cw_logs.push({ "#{new_resource.custom_dir}/iptables/iptables.txt" => { "log_group_name" => new_resource.log_group_name+"/iptables"}})
end
action :run
only_if {new_resource.get_iptables_info}
end
template "my_template" do
variables ({:logstreams => cw_logs})
end
end
Then in my template:
<% #logstreams.each_pair do |path, _object| %>
["#{path}"]
log_group_name = _object["log_group_name"]
<% end %>
The problem is that properties are immutable. So I get the error:
RuntimeError
------------
ruby_block[cw_iptables] (/tmp/kitchen/cache/cookbooks/custom_cw/resources/logs.rb line 43) had an error: RuntimeError: can't modify frozen Array
What's the correct way to do this? What's the correct way to write ruby code within the resource so that it's more modular and uses methods/functions?
Mutating properties inside a custom resource is generally a bad idea (there are exceptions but this isn't one of them). Better would be to use a local variable. Related, you don't need to use a ruby_block like that when you're already in a custom resource's action:
action :create do
cw_logs = new_resource.cw_logs
if new_resource.get_iptables_info
cw_logs += [whatever extra stuff you want]
end
template "my_template" do
variables logstreams: cw_logs
end
end
Notably that's using += instead of push which doesn't mutate the original. If you used push you would want cw_logs = new_resource.cw_logs.dup or similar.

Chef update certain user's .bashrc

I'm trying to update certain users .bashrc JAVA_HOME environment variable after installing JDK. I get this strange error that I don't understand. Here's the block of code in question.
node['etc']['passwd'].each do |user, data|
only_if {data['uid'] > 9000}
jdk_dir_array = Dir.entries('/usr/java/').select {|entry| File.directory? File.join('/usr/java/',entry) and !(entry =='.' || entry == '..') }
jdk_dir_name = jdk_dir_array.shift
file = Chef::Util::FileEdit.new("#{data['dir']}/.bashrc")
file.search_file_replace( /JAVA_HOME=.*/,"JAVA_HOME=/usr/java/#{jdk_dir_name}")
file.write_file
end
The error I'm getting is this:
NoMethodError
-------------
No resource or method named `only_if' for `Chef::Recipe "install_jdk"'
I don't understand why it thinks "only_if" is a method of the recipe when I declare it inside of the node.each block.
i should point out that if I put this in a ruby_block and hardcode the path to a single user's home directory the code works as expected. I'm trying to update multiple users and that's where I'm stumped.
only_if is a method you use on a resource, not in either a recipe or inside the block of a ruby_block. What you want is something more like like:
node['etc']['passwd'].each do |user, data|
ruby_block "edit #{user} bashrc" do
only_if { data['uid'] > 9000 }
block do
jdk_dir_array = Dir.entries('/usr/java/').select {|entry| File.directory? File.join('/usr/java/',entry) and !(entry =='.' || entry == '..') }
jdk_dir_name = jdk_dir_array.shift
file = Chef::Util::FileEdit.new("#{data['dir']}/.bashrc")
file.search_file_replace( /JAVA_HOME=.*/,"JAVA_HOME=/usr/java/#{jdk_dir_name}")
file.write_file
end
end
I really recommend not doing this though. Check out the line cookbook for a more refined way to approach this, or consider having Chef manage the whole file via a template resource.

Chef Recipes - Setting node attributes in ruby_block

I have a Chef recipe for a multi-node web service, each node of which needs to get the hostname and IP of the other nodes, to put it into its own local configuration.
The code is shown below. The problem is that when the node.set[][] assignments are made in the ruby_block as shown, the values are empty when the template that relies upon them is created. If I want to create that template, I have to move all of the ruby_block code outside, and have it "loose" in the recipe. Which makes it harder to do unit-testing with Chefspec and the like.
Can any Chef guru set me straight? Is it just impossible to do node.set[] like this inside of a ruby_block? And if so, why doesn't it say so in the docs?
$cm = { :name => "web", :hostname => "" , :ip_addr => "" }
$ca = { :name => "data", :hostname => "" , :ip_addr => "" }
$cg = { :name => "gateway", :hostname => "" , :ip_addr => "" }
$component_list = [$cm, $ca, $cg]
ruby_block "get host addresses" do
block do
for cmpnt in $component_list
# do REST calls to external service to get cmpnt.hostname, ip_addr
# .......
node.set[cmpnt.name]['name'] = cmpnt.name
node.set[cmpnt.name]['host'] = cmpnt.hostname
node.set[cmpnt.name]['ip'] = cmpnt.ip_addr
end
end
end
template "/etc/app/configuration/config.xml" do
source "config.xml.erb"
variables( :dataHost => node['data']['host'],
:webHost => node['web']['host'],
:gatewayHost => node['gateway']['host'] )
action :create
end
I also added
subscribes :create, "ruby_block[get host addresses]", :immediately
to the template definition to ensure that the ruby_block ran before the template was created. This didn't make a difference.
I realize this is an old post, however for future reference, I just ran across this gist which gives a nice example of node variable assignments in the Compile vs. Converge phases. To adapt the gist to your example, you'll need to add code like the following to your ruby_block:
template_r = run_context.resource_collection.find(:template => "/etc/app/configuration/config.xml")
template_r.content node['data']['host']
template_r.content node['web']['host']
template_r.content node['gateway']['host']
For Chef 11, also see Lazy Attribute Evaluation.
The problem seems to be that attribute values inside your template resource definition get evaluated before actually invoking any resources.
I.e. the file is first executed as simple Ruby, compiling the resources, and only the the resource actions gets invoked. By that time, it is too late already.
I ran into the same problem when trying to encapsulate certain attribute manipulations into a resource. It simply does not work. Should anyone know a solution to this problem, I would appreciate it very much.
EDIT:
b = ruby_block...
...
end
b.run_action(:create)
Could possibly do the trick. It invokes the resource immediately.
The simplest answer to this is to not use chef attributes and not use ruby_block to do the work of talking to the REST API. The code can also be moved to a custom resource for better reuse:
unified_mode true
provides :my_resource
action :run do
cm = { :name => "web", :hostname => "" , :ip_addr => "" }
ca = { :name => "data", :hostname => "" , :ip_addr => "" }
cg = { :name => "gateway", :hostname => "" , :ip_addr => "" }
component_list = [cm, ca, cg]
hash = {}
for cmpnt in component_list
# do REST calls to external service to get cmpnt.hostname, ip_addr
# .......
hash[cmpnt.name] = {}
hash[cmpnt.name]['name'] = cmpnt.name
hash[cmpnt.name]['host'] = cmpnt.hostname
hash[cmpnt.name]['ip'] = cmpnt.ip_addr
end
template "/etc/app/configuration/config.xml" do
source "config.xml.erb"
variables( :dataHost => hash['data']['host'],
:webHost => hash['web']['host'],
:gatewayHost => hash['gateway']['host'] )
action :create
end
end
By using unified_mode and moving into a custom resource, it also makes it easier to use a node attribute without requiring the use of lazy {} or ruby_blocks. It also still allows chef configuration (like setting up resolv.conf or other network requirements before doing the REST calls) prior to calling this code while not having to think about compile/converge two pass issues in recipe context.
There is also no reason to use a resource like ruby_block to do pure ruby processing which does not change the system under management. In this case the ruby_block is hitting a REST service purely to collect data. That does not need to be placed into a Chef resource. It isn't clear from the question if that was being done because the questioner though it was a "best practice" (in this case it is not), or if it was being done to move execution to compile time in order to allow other chef resources that aren't part of the question to fire first (in which case using a custom resource is a much better solution than using a ruby_block).
It's been a while since this question, but in case someone is still looking for it, lazy evaluate is your friend:
template '/tmp/sql_file.sql' do
source "sql_file.sql.erb"
mode 0700
variables lazy {
# Create a new instance of MySQL library
mysql_lib = Acx::MySQL.new(
'127.0.0.1', 'root', node['mysql']['service']['pass']
)
password = node['mysql']['service']['support_admin']['ct_password']
# It returns the encrypted password after evaluate it, to
# be used in template variables
{ admin_password: mysql_lib.encrypted_password(password) }
}
end
https://docs.chef.io/resource_common.html#lazy-evaluation

Resources