inspec-core 4.38.9 → 4.49.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -1
  3. data/etc/deprecations.json +1 -1
  4. data/lib/inspec/base_cli.rb +11 -1
  5. data/lib/inspec/cached_fetcher.rb +2 -2
  6. data/lib/inspec/cli.rb +14 -4
  7. data/lib/inspec/control_eval_context.rb +64 -17
  8. data/lib/inspec/dsl.rb +18 -3
  9. data/lib/inspec/fetcher/url.rb +45 -3
  10. data/lib/inspec/fetcher.rb +3 -3
  11. data/lib/inspec/plugin/v1/registry.rb +6 -2
  12. data/lib/inspec/profile.rb +146 -6
  13. data/lib/inspec/resources/apache_conf.rb +8 -6
  14. data/lib/inspec/resources/cassandra.rb +64 -0
  15. data/lib/inspec/resources/cassandradb_conf.rb +47 -0
  16. data/lib/inspec/resources/cassandradb_session.rb +68 -0
  17. data/lib/inspec/resources/chrony_conf.rb +55 -0
  18. data/lib/inspec/resources/csv.rb +26 -3
  19. data/lib/inspec/resources/groups.rb +22 -3
  20. data/lib/inspec/resources/ibmdb2_conf.rb +57 -0
  21. data/lib/inspec/resources/ibmdb2_session.rb +69 -0
  22. data/lib/inspec/resources/mongodb_session.rb +88 -0
  23. data/lib/inspec/resources/mssql_sys_conf.rb +48 -0
  24. data/lib/inspec/resources/opa.rb +26 -0
  25. data/lib/inspec/resources/opa_api.rb +39 -0
  26. data/lib/inspec/resources/opa_cli.rb +43 -0
  27. data/lib/inspec/resources/oracle.rb +66 -0
  28. data/lib/inspec/resources/oracledb_conf.rb +40 -0
  29. data/lib/inspec/resources/oracledb_listener_conf.rb +123 -0
  30. data/lib/inspec/resources/oracledb_session.rb +23 -6
  31. data/lib/inspec/resources/postgres_session.rb +15 -10
  32. data/lib/inspec/resources/registry_key.rb +1 -1
  33. data/lib/inspec/resources/security_identifier.rb +8 -14
  34. data/lib/inspec/resources/security_policy.rb +4 -3
  35. data/lib/inspec/resources/service.rb +7 -1
  36. data/lib/inspec/resources/sybase_conf.rb +37 -0
  37. data/lib/inspec/resources/sybase_session.rb +111 -0
  38. data/lib/inspec/resources/users.rb +16 -2
  39. data/lib/inspec/resources/windows_firewall.rb +1 -1
  40. data/lib/inspec/resources/wmi.rb +1 -1
  41. data/lib/inspec/resources.rb +12 -0
  42. data/lib/inspec/run_data/profile.rb +0 -2
  43. data/lib/inspec/runner.rb +2 -0
  44. data/lib/inspec/utils/filter.rb +1 -1
  45. data/lib/inspec/version.rb +1 -1
  46. data/lib/plugins/inspec-init/templates/profiles/aws/inspec.yml +1 -1
  47. data/lib/plugins/inspec-init/templates/profiles/azure/inspec.yml +1 -1
  48. data/lib/plugins/inspec-init/templates/profiles/gcp/inspec.yml +1 -1
  49. data/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb +16 -15
  50. metadata +18 -2
@@ -0,0 +1,43 @@
1
+ require "inspec/resources/opa"
2
+
3
+ module Inspec::Resources
4
+ class OpaCli < Opa
5
+ name "opa_cli"
6
+ supports platform: "unix"
7
+ supports platform: "windows"
8
+
9
+ def initialize(opts = {})
10
+ @opa_executable_path = opts[:opa_executable_path] || "opa" # if this path is not provided then we will assume that it's been set in the ENV PATH
11
+ @policy = opts[:policy] || nil
12
+ @data = opts[:data] || nil
13
+ @query = opts[:query] || nil
14
+ if (@policy.nil? || @policy.empty?) || (@data.nil? || @data.empty?) || (@query.nil? || @query.empty?)
15
+ fail_resource "OPA policy, data and query are mandatory."
16
+ end
17
+ @content = load_result
18
+ super(@content)
19
+ end
20
+
21
+ def allow
22
+ @content["result"][0]["expressions"][0]["value"] if @content["result"][0]["expressions"][0]["text"].include?("allow")
23
+ end
24
+
25
+ def to_s
26
+ "OPA cli"
27
+ end
28
+
29
+ private
30
+
31
+ def load_result
32
+ raise Inspec::Exceptions::ResourceFailed, "#{resource_exception_message}" if resource_failed?
33
+
34
+ result = inspec.command("#{@opa_executable_path} eval -i '#{@data}' -d '#{@policy}' '#{@query}'")
35
+ if result.exit_status == 0
36
+ result.stdout.gsub("\n", "")
37
+ else
38
+ error = result.stdout + "\n" + result.stderr
39
+ raise Inspec::Exceptions::ResourceFailed, "Error while executing OPA query: #{error}"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,66 @@
1
+ require "inspec/resources/powershell"
2
+
3
+ module Inspec::Resources
4
+ class Oracle < Inspec.resource(1)
5
+ name "oracle"
6
+ supports platform: "unix"
7
+ supports platform: "windows"
8
+
9
+ desc "The 'oracle' resource is a helper for the 'oracledb_listener_conf'"
10
+
11
+ attr_reader :conf_path
12
+
13
+ def initialize
14
+ case inspec.os[:family]
15
+ when "debian", "redhat", "linux", "suse"
16
+ determine_conf_dir_and_path_in_linux
17
+ when "windows"
18
+ determine_conf_dir_and_path_in_windows
19
+ end
20
+ end
21
+
22
+ def to_s
23
+ "OracleDB"
24
+ end
25
+
26
+ private
27
+
28
+ def determine_conf_dir_and_path_in_linux
29
+ oracle_home = inspec.os_env("ORACLE_HOME").content
30
+
31
+ if oracle_home.nil? || oracle_home.empty?
32
+ warn "$ORACLE_HOME env value not set in the system"
33
+ nil
34
+ else
35
+ conf_path = "#{oracle_home}/network/admin/listener.ora"
36
+ if !inspec.file(conf_path).exist?
37
+ warn "No oracle listener settings found in $ORACLE_HOME/network/admin directory"
38
+ nil
39
+ else
40
+ @conf_path = conf_path
41
+ end
42
+ end
43
+ rescue => e
44
+ fail_resource "Errors reading listener settings: #{e}"
45
+ end
46
+
47
+ def determine_conf_dir_and_path_in_windows
48
+ oracle_home = inspec.os_env("ORACLE_HOME").content
49
+
50
+ if oracle_home.nil? || oracle_home.empty?
51
+ warn "ORACLE_HOME env value not set in the system"
52
+ nil
53
+ else
54
+ conf_path = "#{oracle_home}\\network\\admin\\listener.ora"
55
+ if !inspec.file(conf_path).exist?
56
+ warn "No oracle listener settings found in ORACLE_HOME\\network\\admin directory"
57
+ nil
58
+ else
59
+ @conf_path = conf_path
60
+ end
61
+ end
62
+ rescue => e
63
+ fail_resource "Errors reading listener settings: #{e}"
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,40 @@
1
+ require "inspec/resources/oracledb_session"
2
+
3
+ module Inspec::Resources
4
+ class OracledbConf < Inspec.resource(1)
5
+ name "oracledb_conf"
6
+ supports platform: "unix"
7
+ supports platform: "windows"
8
+ desc "Use the oracledb_conf InSpec audit resource to test the database settings for Oracle DB"
9
+ example <<~EXAMPLE
10
+ describe oracledb_conf(user: 'USER', password: 'PASSWORD') do
11
+ its("audit_sys_operations") { should cmp "true" }
12
+ its("sql92_security") { should cmp "true" }
13
+ end
14
+ EXAMPLE
15
+
16
+ attr_reader :oracledb_session
17
+
18
+ def initialize(opts = {})
19
+ @oracledb_session = inspec.oracledb_session(opts)
20
+ end
21
+
22
+ def method_missing(name)
23
+ setting = name.to_s.upcase
24
+ determine_database_setting(setting)
25
+ end
26
+
27
+ def to_s
28
+ "Oracle DB Configuration"
29
+ end
30
+
31
+ private
32
+
33
+ def determine_database_setting(setting)
34
+ sql_query = oracledb_session.query("SELECT UPPER(VALUE) AS UPPER_VALUE FROM V$SYSTEM_PARAMETER WHERE UPPER(NAME) = '#{setting}'")
35
+ sql_query.row(0).column("UPPER_VALUE").value
36
+ rescue => e
37
+ raise Inspec::Exceptions::ResourceFailed, "Errors fetching database settings for Oracle database: #{e}"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,123 @@
1
+ require "inspec/utils/object_traversal"
2
+ require "inspec/utils/simpleconfig"
3
+ require "inspec/utils/find_files"
4
+ require "inspec/utils/file_reader"
5
+ require "inspec/resources/oracle"
6
+
7
+ module Inspec::Resources
8
+ class OracledbListenerConf < Inspec.resource(1)
9
+ name "oracledb_listener_conf"
10
+ supports platform: "unix"
11
+ supports platform: "windows"
12
+ desc "Use the oracledb_listener_conf InSpec audit resource to test the listener settings for Oracle DB"
13
+ example <<~EXAMPLE
14
+ describe oracledb_listener_conf do
15
+ its('DEFAULT_SERVICE_LISTENER') { should eq 'XE' }
16
+ end
17
+ EXAMPLE
18
+
19
+ include FindFiles
20
+ include FileReader
21
+ include ObjectTraverser
22
+
23
+ def initialize(conf_path = nil)
24
+ oracle = nil
25
+ if conf_path.nil?
26
+ oracle = inspec.oracle
27
+ @conf_path = oracle.conf_path
28
+ else
29
+ @conf_path = conf_path
30
+ end
31
+
32
+ if oracle && oracle.resource_failed?
33
+ raise oracle.resource_exception_message
34
+ elsif @conf_path.nil?
35
+ return skip_resource "Oracle Listener conf path is not set"
36
+ end
37
+
38
+ @conf_dir = File.expand_path(File.dirname(@conf_path))
39
+ @files_contents = {}
40
+ @content = nil
41
+ @params = nil
42
+ read_content
43
+ end
44
+
45
+ def content
46
+ @content ||= read_content
47
+ end
48
+
49
+ def params(*opts)
50
+ @params || read_content
51
+ res = @params
52
+ opts.each do |opt|
53
+ res = res[opt] unless res.nil?
54
+ end
55
+ res
56
+ end
57
+
58
+ def value(key)
59
+ extract_value(key, @params)
60
+ end
61
+
62
+ def method_missing(*keys)
63
+ keys.shift if keys.is_a?(Array) && keys[0] == :[]
64
+ param = value(keys)
65
+ return nil if param.nil?
66
+ # extract first value if we have only one value in array
67
+ return param[0] if param.length == 1
68
+
69
+ param
70
+ end
71
+
72
+ def to_s
73
+ "Oracle Listener Configuration"
74
+ end
75
+
76
+ private
77
+
78
+ def read_content
79
+ @content = ""
80
+ @params = {}
81
+
82
+ to_read = [@conf_path]
83
+ until to_read.empty?
84
+ base_dir = File.dirname(to_read[0])
85
+ raw_conf = read_file(to_read[0])
86
+ @content += raw_conf
87
+
88
+ opts = {
89
+ assignment_regex: /^\s*([^=]*?)\s*=\s*[']?\s*(.*?)\s*[']?\s*$/,
90
+ }
91
+ params = SimpleConfig.new(raw_conf, opts).params
92
+ @params.merge!(params)
93
+
94
+ to_read = to_read.drop(1)
95
+ # see if there is more config files to include
96
+
97
+ to_read += include_files(params, base_dir).find_all do |fp|
98
+ not @files_contents.key? fp
99
+ end
100
+ end
101
+ @content
102
+ end
103
+
104
+ def include_files(params, base_dir)
105
+ include_files = Array(params["include"]) || []
106
+ include_files += Array(params["include_if_exists"]) || []
107
+ include_files.map! do |f|
108
+ Pathname.new(f).absolute? ? f : File.join(base_dir, f)
109
+ end
110
+
111
+ dirs = Array(params["include_dir"]) || []
112
+ dirs.each do |dir|
113
+ dir = File.join(base_dir, dir) if dir[0] != "/"
114
+ include_files += find_files(dir, depth: 1, type: "file")
115
+ end
116
+ include_files
117
+ end
118
+
119
+ def read_file(path)
120
+ @files_contents[path] ||= read_file_content(path)
121
+ end
122
+ end
123
+ end
@@ -42,6 +42,7 @@ module Inspec::Resources
42
42
  end
43
43
 
44
44
  def query(sql)
45
+ raise Inspec::Exceptions::ResourceSkipped, "#{resource_exception_message}" if resource_skipped?
45
46
  raise Inspec::Exceptions::ResourceFailed, "#{resource_exception_message}" if resource_failed?
46
47
 
47
48
  if @sqlcl_bin && inspec.command(@sqlcl_bin).exist?
@@ -78,7 +79,14 @@ module Inspec::Resources
78
79
  # using a db_role
79
80
  # su, using a db_role
80
81
  def command_builder(format_options, query)
81
- verified_query = verify_query(query)
82
+ if @db_role.nil? || @su_user.nil?
83
+ verified_query = verify_query(query)
84
+ else
85
+ escaped_query = query.gsub(/\\\\/, "\\").gsub(/"/, '\\"')
86
+ escaped_query = escaped_query.gsub("$", '\\$') unless escaped_query.include? "\\$"
87
+ verified_query = verify_query(escaped_query)
88
+ end
89
+
82
90
  sql_prefix, sql_postfix = "", ""
83
91
  if inspec.os.windows?
84
92
  sql_prefix = %{@'\n#{format_options}\n#{verified_query}\nEXIT\n'@ | }
@@ -87,11 +95,14 @@ module Inspec::Resources
87
95
  end
88
96
 
89
97
  if @db_role.nil?
90
- "#{sql_prefix}#{bin} #{user}/#{password}@#{host}:#{port}/#{@service}#{sql_postfix}"
98
+ %{#{sql_prefix}#{bin} #{user}/#{password}@#{host}:#{port}/#{@service}#{sql_postfix}}
91
99
  elsif @su_user.nil?
92
- "#{sql_prefix}#{bin} #{user}/#{password}@#{host}:#{port}/#{@service} as #{@db_role}#{sql_postfix}"
100
+ %{#{sql_prefix}#{bin} #{user}/#{password}@#{host}:#{port}/#{@service} as #{@db_role}#{sql_postfix}}
93
101
  else
94
- "su - #{@su_user} -c env ORACLE_SID=#{@service} #{@bin} / as #{@db_role}#{sql_postfix}"
102
+ # oracle_query_string is echoed to be able to extract the query output clearly
103
+ # su - su_user in certain versions of oracle returns a message
104
+ # Example of msg with query output: The Oracle base remains unchanged with value /oracle\n\nVALUE\n3\n
105
+ %{su - #{@su_user} -c "echo 'oracle_query_string'; env ORACLE_SID=#{@service} #{@bin} / as #{@db_role}#{sql_postfix}"}
95
106
  end
96
107
  end
97
108
 
@@ -101,9 +112,15 @@ module Inspec::Resources
101
112
  end
102
113
 
103
114
  def parse_csv_result(stdout)
104
- output = stdout.sub(/\r/, "").strip
115
+ output = stdout.split("oracle_query_string")[-1]
116
+ # comma_query_sub replaces the csv delimiter "," in the output.
117
+ # Handles CSV parsing of data like this (DROP,3) etc
118
+ output = output.sub(/\r/, "").strip.gsub(",", "comma_query_sub")
105
119
  converter = ->(header) { header.downcase }
106
- CSV.parse(output, headers: true, header_converters: converter).map { |row| Hashie::Mash.new(row.to_h) }
120
+ CSV.parse(output, headers: true, header_converters: converter).map do |row|
121
+ revised_row = row.entries.flatten.map { |entry| entry.gsub("comma_query_sub", ",") }
122
+ Hashie::Mash.new([revised_row].to_h)
123
+ end
107
124
  end
108
125
  end
109
126
  end
@@ -40,14 +40,13 @@ module Inspec::Resources
40
40
  end
41
41
  EXAMPLE
42
42
 
43
- def initialize(user, pass, host = nil, port = nil)
43
+ def initialize(user, pass, host = nil, port = nil, socket_path = nil)
44
44
  @user = user || "postgres"
45
45
  @pass = pass
46
46
  @host = host || "localhost"
47
47
  @port = port || 5432
48
+ @socket_path = socket_path
48
49
  raise Inspec::Exceptions::ResourceFailed, "Can't run PostgreSQL SQL checks without authentication." if @user.nil? || @pass.nil?
49
-
50
- test_connection
51
50
  end
52
51
 
53
52
  def query(query, db = [])
@@ -65,20 +64,26 @@ module Inspec::Resources
65
64
 
66
65
  private
67
66
 
68
- def test_connection
69
- query("select now()\;")
70
- end
71
-
72
67
  def escaped_query(query)
73
68
  Shellwords.escape(query)
74
69
  end
75
70
 
76
71
  def create_psql_cmd(query, db = [])
77
72
  dbs = db.map { |x| "#{x}" }.join(" ")
78
- if inspec.os.windows?
79
- "psql -d postgresql://#{@user}:#{@pass}@#{@host}:#{@port}/#{dbs} -A -t -w -c \"#{query}\""
73
+
74
+ if @socket_path && !inspec.os.windows?
75
+ # Socket path and empty host in the connection string establishes socket connection
76
+ # Socket connection only enabled for non-windows platforms
77
+ # Windows does not support unix domain sockets
78
+ "psql -d postgresql://#{@user}:#{@pass}@/#{dbs}?host=#{@socket_path} -A -t -w -c #{escaped_query(query)}"
80
79
  else
81
- "psql -d postgresql://#{@user}:#{@pass}@#{@host}:#{@port}/#{dbs} -A -t -w -c #{escaped_query(query)}"
80
+ # Host in connection string establishes tcp/ip connection
81
+ if inspec.os.windows?
82
+ warn "Socket based connection not supported in windows, connecting using host" if @socket_path
83
+ "psql -d postgresql://#{@user}:#{@pass}@#{@host}:#{@port}/#{dbs} -A -t -w -c \"#{query}\""
84
+ else
85
+ "psql -d postgresql://#{@user}:#{@pass}@#{@host}:#{@port}/#{dbs} -A -t -w -c #{escaped_query(query)}"
86
+ end
82
87
  end
83
88
  end
84
89
  end
@@ -105,7 +105,7 @@ module Inspec::Resources
105
105
  children_keys(@options[:path], filter)
106
106
  end
107
107
 
108
- # returns nil, if not existant or value
108
+ # returns nil, if not existent or value
109
109
  def method_missing(*keys)
110
110
  # allow the use of array syntax in an `its` block so that users
111
111
  # can use it to query for keys with . characters in them
@@ -57,14 +57,14 @@ module Inspec::Resources
57
57
  @sids = {}
58
58
  case @type
59
59
  when :group
60
- sid_data = wmi_results(:group)
60
+ sid_data = cim_results(:group)
61
61
  when :user
62
- sid_data = wmi_results(:user)
62
+ sid_data = cim_results(:user)
63
63
  when :unspecified
64
64
  # try group first, then user
65
- sid_data = wmi_results(:group)
65
+ sid_data = cim_results(:group)
66
66
  if sid_data.empty?
67
- sid_data = wmi_results(:user)
67
+ sid_data = cim_results(:user)
68
68
  end
69
69
  else
70
70
  raise "Unhandled entity type '#{@type}'"
@@ -72,20 +72,14 @@ module Inspec::Resources
72
72
  sid_data.each { |sid| @sids[sid[1]] = sid[2] }
73
73
  end
74
74
 
75
- def wmi_results(type)
76
- query = "wmic "
75
+ def cim_results(type)
77
76
  case type
78
77
  when :group
79
- query += "group"
78
+ cmd = "Get-CimInstance -ClassName Win32_Account | Select-Object -Property Domain, Name, SID | Where-Object { $_.Name -eq '#{@name}' -and { $_.SIDType -eq 4 -or $_.SIDType -eq 5 } } | ConvertTo-Csv -NoTypeInformation"
80
79
  when :user
81
- query += "useraccount"
80
+ cmd = "Get-CimInstance -ClassName Win32_Account | Select-Object -Property Domain, Name, SID, SIDType | Where-Object { $_.Name -eq '#{@name}' -and $_.SIDType -eq 1 } | ConvertTo-Csv -NoTypeInformation"
82
81
  end
83
- query += " where 'Name=\"#{@name}\"' get Name\",\"SID /format:csv"
84
- # Example output:
85
- # inspec> command("wmic useraccount where 'Name=\"Administrator\"' get Name\",\"SID /format:csv").stdout
86
- # => "\r\n\r\nNode,Name,SID\r\n\r\nComputer1,Administrator,S-1-5-21-650485088-1194226989-968533923-500\r\n\r\n"
87
- # Remove the \r characters, split on \n\n, ignore the CSV header row
88
- inspec.command(query).stdout.strip.tr("\r", "").split("\n\n")[1..-1].map { |entry| entry.split(",") }
82
+ inspec.command(cmd).stdout.strip.gsub("\"", "").tr("\r", "").split("\n")[1..-1].map { |entry| entry.split(",") }
89
83
  end
90
84
  end
91
85
  end
@@ -147,7 +147,7 @@ module Inspec::Resources
147
147
 
148
148
  # extracts the values, this methods detects:
149
149
  # numbers and SIDs and optimizes them for further usage
150
- def extract_value(val)
150
+ def extract_value(key, val)
151
151
  if val =~ /^\d+$/
152
152
  val.to_i
153
153
  # special handling for SID array
@@ -166,14 +166,15 @@ module Inspec::Resources
166
166
  elsif !(m = /^\"(.*)\"$/.match(val)).nil?
167
167
  m[1]
168
168
  else
169
- val
169
+ # When there is Registry Values we are not spliting the value for backward compatibility
170
+ key.include?("\\") ? val : val.split(",")
170
171
  end
171
172
  end
172
173
 
173
174
  def convert_hash(hash)
174
175
  new_hash = {}
175
176
  hash.each do |k, v|
176
- v.is_a?(Hash) ? value = convert_hash(v) : value = extract_value(v)
177
+ v.is_a?(Hash) ? value = convert_hash(v) : value = extract_value(k, v)
177
178
  new_hash[k.strip] = value
178
179
  end
179
180
  new_hash
@@ -141,7 +141,7 @@ module Inspec::Resources
141
141
  elsif version > 0
142
142
  SysV.new(inspec, service_ctl || "/usr/sbin/service")
143
143
  end
144
- when "redhat", "fedora", "centos", "oracle", "cloudlinux", "scientific"
144
+ when "redhat", "fedora", "centos", "oracle", "cloudlinux", "scientific", "rocky", "almalinux"
145
145
  version = os[:release].to_i
146
146
 
147
147
  systemd = ((platform != "fedora" && version >= 7) ||
@@ -152,6 +152,12 @@ module Inspec::Resources
152
152
  else
153
153
  SysV.new(inspec, service_ctl || "/sbin/service")
154
154
  end
155
+ when "alibaba"
156
+ if os[:release].to_i >= 3
157
+ Systemd.new(inspec, service_ctl)
158
+ else
159
+ SysV.new(inspec, service_ctl || "/sbin/service")
160
+ end
155
161
  when "wrlinux"
156
162
  SysV.new(inspec, service_ctl)
157
163
  when "mac_os_x", "darwin"
@@ -0,0 +1,37 @@
1
+ require "inspec/resources/sybase_session"
2
+
3
+ module Inspec::Resources
4
+ class SybaseConf < Inspec.resource(1)
5
+ name "sybase_conf"
6
+ supports platform: "unix"
7
+ # supports platform: "windows" # TODO
8
+ desc "Use the sybase_conf InSpec resource to test Sybase config settings"
9
+ example <<~EXAMPLE
10
+ describe sybase_conf("max memory", password: 'password', server: 'SYBASE') do
11
+ its("run_value") { should cmp 180224 }
12
+ end
13
+ EXAMPLE
14
+
15
+ attr_reader :conf_param, :sql_query
16
+ def initialize(conf_param_name, opts = {})
17
+ @conf_param = conf_param_name
18
+ opts[:username] ||= "sa"
19
+ opts[:database] ||= "master"
20
+ sql_session = inspec.sybase_session(opts)
21
+ @sql_query = sql_session.query("sp_configure \"#{conf_param}\"")
22
+ end
23
+
24
+ def run_value
25
+ sql_query.row(0).column("Run Value").value
26
+ end
27
+
28
+ def config_value
29
+ sql_query.row(0).column("Config Value").value
30
+ end
31
+
32
+ def to_s
33
+ "Sybase Conf #{conf_param}"
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,111 @@
1
+ require "inspec/resources/command"
2
+ require "inspec/utils/database_helpers"
3
+ require "hashie/mash"
4
+ require "csv" unless defined?(CSV)
5
+ require "tempfile" unless defined?(Tempfile)
6
+
7
+ module Inspec::Resources
8
+ # STABILITY: Experimental
9
+ # This resource needs further testing and refinement
10
+ #
11
+ class SybaseSession < Inspec.resource(1)
12
+ name "sybase_session"
13
+ supports platform: "unix"
14
+ # supports platform: "windows" # TODO
15
+ desc "Use the sybase_session InSpec resource to test commands against an Sybase database"
16
+ example <<~EXAMPLE
17
+ sql = sybase_session(username: 'my_user', password: 'password', server: 'SYBASE', database: 'pubs2')
18
+ describe sql.query(\"SELECT * FROM authors\").row(0).column('au_lname') do
19
+ its('value') { should eq 'Smith' }
20
+ end
21
+ EXAMPLE
22
+
23
+ # TODO: allow to set -I interfaces file
24
+ # TODO: allow to customize -s column separator
25
+ attr_reader :bin, :col_sep, :database, :password, :server, :sybase_home, :username
26
+
27
+ def initialize(opts = {})
28
+ @username = opts[:username]
29
+ @password = opts[:password]
30
+ @database = opts[:database]
31
+ @server = opts[:server]
32
+ @sybase_home = opts[:sybase_home] || "/opt/sap"
33
+ @bin = opts[:bin] || "isql"
34
+ @col_sep = "|"
35
+
36
+ fail_resource "Can't run Sybase checks without authentication" unless username && password
37
+ fail_resource "You must provide a server name for the session" unless server
38
+ fail_resource "You must provide a database name for the session" unless database
39
+ fail_resource "Cannot find #{bin} CLI tool" unless inspec.command(bin).exist?
40
+ end
41
+
42
+ def query(sql)
43
+ # We must write the SQl to a temp file on the remote target
44
+ # try to get a temp path
45
+ sql_file_path = upload_sql_file(sql)
46
+
47
+ # isql reuires that we have a matching locale set, but does not support C.UTF-8. en_US.UTF-8 is the least evil.
48
+ command = "LANG=en_US.UTF-8 SYBASE=#{sybase_home} #{bin} -s\"#{col_sep}\" -w80000 -S #{server} -U #{username} -D #{database} -P \"#{password}\" < #{sql_file_path}"
49
+ isql_cmd = inspec.command(command)
50
+
51
+ # Check for isql errors
52
+ res = isql_cmd.exit_status
53
+ raise Inspec::Exceptions::ResourceFailed.new("isql exited with code #{res} and stderr '#{isql_cmd.stderr}', stdout '#{isql_cmd.stdout}'") unless res == 0
54
+ # isql is ill-behaved, and returns 0 on error
55
+ raise Inspec::Exceptions::ResourceFailed.new("isql exited with error '#{isql_cmd.stderr}', stdout '#{isql_cmd.stdout}'") unless isql_cmd.stderr == ""
56
+ # check stdout for error messages when stderr is empty "Msg 102, Level 15, State 181:\nServer 'SYBASE', Line 1:\nIncorrect syntax near '.'.\n"
57
+ raise Inspec::Exceptions::ResourceFailed.new("isql exited with error #{isql_cmd.stdout}") if isql_cmd.stdout.match?(/Msg\s\d+,\sLevel\s\d+,\sState\s\d+/)
58
+
59
+ # Clean up temporary file
60
+ rm_cmd = inspec.command("rm #{sql_file_path}")
61
+ res = rm_cmd.exit_status # TODO: handle
62
+ raise Inspec::Exceptions::ResourceFailed.new("Unable to delete temproary SQL input file at #{sql_file_path}: #{rm_cmd.stderr}") unless res == 0
63
+
64
+ DatabaseHelper::SQLQueryResult.new(isql_cmd, parse_csv_result(isql_cmd.stdout))
65
+ end
66
+
67
+ def to_s
68
+ "Sybase Session"
69
+ end
70
+
71
+ private
72
+
73
+ def parse_csv_result(stdout)
74
+ output = stdout.gsub(/\r/, "").strip
75
+ lines = output.lines
76
+ # Remove second row (all dashes) and last 2 rows (blank and summary lines)
77
+ trimmed_output = ([lines[0]] << lines.slice(2..-3)).join("")
78
+ header_converter = Proc.new do |header|
79
+ # This is here to suppress a warning from Hashie::Mash when it encounters a
80
+ # header column that ends up with the name "default", which happens when using the
81
+ # sybase_conf resource. It does mean that aly query whose output field includes the name
82
+ # Default (exactly) will get renamed to default_value, but that seems unlikely.
83
+ if header.match?(/^Default\s+$/)
84
+ "default_value"
85
+ else
86
+ header.downcase.strip
87
+ end
88
+ end
89
+ field_converter = ->(field) { field&.strip }
90
+ CSV.parse(trimmed_output, headers: true, header_converters: header_converter, converters: field_converter, col_sep: col_sep).map { |row| Hashie::Mash.new(row.to_h) }
91
+ end
92
+
93
+ def upload_sql_file(sql)
94
+ remote_temp_dir = "/tmp"
95
+ remote_file_path = nil
96
+ local_temp_file = Tempfile.new(["sybase", ".sql"])
97
+ begin
98
+ local_temp_file.write("#{sql}\n")
99
+ local_temp_file.write("go\n")
100
+ local_temp_file.flush
101
+ filename = File.basename(local_temp_file.path)
102
+ remote_file_path = "#{remote_temp_dir}/#{filename}"
103
+ inspec.backend.upload([local_temp_file.path], remote_temp_dir)
104
+ ensure
105
+ local_temp_file.close
106
+ local_temp_file.unlink
107
+ end
108
+ remote_file_path
109
+ end
110
+ end
111
+ end
@@ -204,7 +204,9 @@ module Inspec::Resources
204
204
  alias group groupname
205
205
 
206
206
  def groups
207
- identity[:groups] unless identity.nil?
207
+ unless identity.nil?
208
+ inspec.os.windows? ? UserGroups.new(identity[:groups]) : identity[:groups]
209
+ end
208
210
  end
209
211
 
210
212
  def home
@@ -314,6 +316,18 @@ module Inspec::Resources
314
316
  end
315
317
  end
316
318
 
319
+ # Class defined to compare for groups without case-sensitivity
320
+ class UserGroups < Array
321
+ def initialize(user_groups)
322
+ @user_groups = user_groups
323
+ super
324
+ end
325
+
326
+ def include?(group)
327
+ !(@user_groups.select { |user_group| user_group.casecmp?(group) }.empty?)
328
+ end
329
+ end
330
+
317
331
  # This is an abstract class that every user provoider has to implement.
318
332
  # A user provider implements a system abstracts and helps the InSpec resource
319
333
  # hand-over system specific behavior to those providers
@@ -622,7 +636,7 @@ module Inspec::Resources
622
636
  name, _domain = parse_windows_account(username)
623
637
  return if collect_user_details.nil?
624
638
 
625
- res = collect_user_details.select { |user| user[:username] == name }
639
+ res = collect_user_details.select { |user| user[:username].casecmp? name }
626
640
  res[0] unless res.empty?
627
641
  end
628
642