diff --git a/docs-chef-io/content/inspec/resources/cassandradb_conf.md b/docs-chef-io/content/inspec/resources/cassandradb_conf.md new file mode 100644 index 000000000..2580939c0 --- /dev/null +++ b/docs-chef-io/content/inspec/resources/cassandradb_conf.md @@ -0,0 +1,45 @@ ++++ +title = "cassandradb_conf resource" +draft = false +gh_repo = "inspec" +platform = "os" + +[menu] + [menu.inspec] + title = "cassandradb_conf" + identifier = "inspec/resources/os/cassandradb_conf.md cassandradb_conf resource" + parent = "inspec/resources/os" ++++ + +Use the `cassandradb_conf` Chef InSpec audit resource to test the configuration of a Cassandra database, which is typically located at `$CASSANDRA_HOME/cassandra.yaml` or `$CASSANDRA_HOME\conf\cassandra.yaml` depending upon the platform. + +## Installation + +This resource is distributed along with Chef InSpec itself. You can use it automatically. + +## Requirements + +- The value of the `CASSANDRA_HOME` environment variable must be set in the system. + +## Syntax + +A `cassandradb_conf` resource block fetches configurations in the `cassandra.yaml` file, and then compares them with the value stated in the test: + + describe cassandradb_conf do + its('config item') { should eq 'value' } + end + +## Examples + +The following examples show how to use this Chef InSpec audit resource. + +### Test parameters set within the configuration file + + describe cassandradb_conf do + its('listen_address') { should eq 'localhost' } + its('num_tokens') { should eq 16 } + end + +## Matchers + +For a full list of available matchers, please visit our [matchers page](/inspec/matchers/). diff --git a/docs-chef-io/content/inspec/resources/cassandradb_session.md b/docs-chef-io/content/inspec/resources/cassandradb_session.md new file mode 100644 index 000000000..187b9c945 --- /dev/null +++ b/docs-chef-io/content/inspec/resources/cassandradb_session.md @@ -0,0 +1,76 @@ ++++ +title = "cassandradb_session resource" +draft = false +gh_repo = "inspec" +platform = "os" + +[menu] + [menu.inspec] + title = "cassandradb_session" + identifier = "inspec/resources/os/cassandradb_session.md cassandradb_session resource" + parent = "inspec/resources/os" ++++ + +Use the `cassandradb_session` Chef InSpec audit resource to test Cassandra Query Language (CQL) commands run against a Cassandra database. + +## Availability + +### Installation + +This resource is distributed along with Chef InSpec itself. You can use it automatically. + +## Syntax + +A `cassandradb_session` resource block declares the username, password, host, and port to use for the session, and then the command to be run: + + describe cassandradb_session(user: 'USERNAME', password: 'PASSWORD', host: 'localhost', port: 9042).query('QUERY') do + its('value') { should eq('EXPECTED') } + end + +where + +- `cassandradb_session` declares a username, password, host and port to run the query. +- `query('QUERY')` contains the query to be run. +- `its('value') { should eq('expected') }` compares the results of the query against the expected result in the test. + +### Optional Parameters + +The `cassandradb_session` InSpec resource accepts `user`, `password`, `host`, and `port` parameters. + +In Particular: + +#### `user` + +Default value: `cassandra`. + +#### `password` + +Default value: `cassandra`. + +## Examples + +The following examples show how to use this Chef InSpec audit resource. + +### Test for matching values using a Cassandra query + +```ruby +cql = cassandradb_session(user: 'MY_USER', password: 'PASSWORD', host: 'localhost', port: 9042) + +describe cql.query("SELECT cluster_name FROM system.local") do + its('output') { should match /Test Cluster/ } +end +``` + +### Test for matching values using a Cassandra query from a sample database + +```ruby +cql = cassandradb_session(user: 'MY_USER', password: 'PASSWORD', host: 'localhost', port: 9042) + +describe cql.query("use SAMPLEDB; SELECT name FROM SAMPLETABLE") do + its('output') { should match /Test Name/ } +end +``` + +## Matchers + +For a full list of available matchers, please visit our [matchers page](/inspec/matchers/). diff --git a/lib/inspec/resources.rb b/lib/inspec/resources.rb index 7b780221e..486a19947 100644 --- a/lib/inspec/resources.rb +++ b/lib/inspec/resources.rb @@ -37,6 +37,9 @@ require "inspec/resources/chocolatey_package" require "inspec/resources/command" require "inspec/resources/cran" require "inspec/resources/cpan" +require "inspec/resources/cassandradb_session" +require "inspec/resources/cassandradb_conf" +require "inspec/resources/cassandra" require "inspec/resources/crontab" require "inspec/resources/dh_params" require "inspec/resources/directory" diff --git a/lib/inspec/resources/cassandra.rb b/lib/inspec/resources/cassandra.rb new file mode 100644 index 000000000..fe9bd13f3 --- /dev/null +++ b/lib/inspec/resources/cassandra.rb @@ -0,0 +1,64 @@ +module Inspec::Resources + class Cassandra < Inspec.resource(1) + name "cassandra" + supports platform: "unix" + supports platform: "windows" + + desc "The 'cassandra' resource is a helper for the 'cql_conf'" + + attr_reader :conf_path + + def initialize + case inspec.os[:family] + when "debian", "redhat", "linux", "suse" + determine_conf_dir_and_path_in_linux + when "windows" + determine_conf_dir_and_path_in_windows + end + end + + def to_s + "CassandraDB" + end + + private + + def determine_conf_dir_and_path_in_linux + cassandra_home = inspec.os_env("CASSANDRA_HOME").content + + if cassandra_home.nil? || cassandra_home.empty? + warn "$CASSANDRA_HOME environment variable not set in the system" + nil + else + conf_path = "#{cassandra_home}/cassandra.yaml" + if !inspec.file(conf_path).exist? + warn "Cassandra conf file not found in #{cassandra_home} directory." + nil + else + @conf_path = conf_path + end + end + rescue => e + fail_resource "Errors reading cassandra conf file: #{e}" + end + + def determine_conf_dir_and_path_in_windows + cassandra_home = inspec.os_env("CASSANDRA_HOME").content + + if cassandra_home.nil? || cassandra_home.empty? + warn "CASSANDRA_HOME environment variable not set in the system" + nil + else + conf_path = "#{cassandra_home}\\conf\\cassandra.yaml" + if !inspec.file(conf_path).exist? + warn "Cassandra conf file not found in #{cassandra_home}\\conf directory." + nil + else + @conf_path = conf_path + end + end + rescue => e + fail_resource "Errors reading cassandra conf file: #{e}" + end + end +end diff --git a/lib/inspec/resources/cassandradb_conf.rb b/lib/inspec/resources/cassandradb_conf.rb new file mode 100644 index 000000000..da3f84cd8 --- /dev/null +++ b/lib/inspec/resources/cassandradb_conf.rb @@ -0,0 +1,47 @@ +require "inspec/resources/json" +require "inspec/resources/cassandra" + +module Inspec::Resources + class CassandradbConf < JsonConfig + name "cassandradb_conf" + supports platform: "unix" + supports platform: "windows" + desc "Use the cql_conf InSpec audit resource to test the contents of the configuration file for Cassandra DB" + example <<~EXAMPLE + describe cassandradb_conf do + its('listen_address') { should eq '0.0.0.0' } + end + EXAMPLE + + def initialize(conf_path = nil) + cassandra = nil + if conf_path.nil? + cassandra = inspec.cassandra + @conf_path = cassandra.conf_path + else + @conf_path = conf_path + end + + if cassandra && cassandra.resource_failed? + raise cassandra.resource_exception_message + elsif @conf_path.nil? + return skip_resource "Cassandra db conf path is not set" + end + + super(@conf_path) + end + + private + + def parse(content) + YAML.load(content) + rescue => e + raise Inspec::Exceptions::ResourceFailed, "Unable to parse `cassandra.yaml` file: #{e.message}" + end + + def resource_base_name + "Cassandra Configuration" + end + + end +end diff --git a/lib/inspec/resources/cassandradb_session.rb b/lib/inspec/resources/cassandradb_session.rb new file mode 100644 index 000000000..c32c4b84f --- /dev/null +++ b/lib/inspec/resources/cassandradb_session.rb @@ -0,0 +1,68 @@ +module Inspec::Resources + class Lines + attr_reader :output + + def initialize(raw, desc) + @output = raw + @desc = desc + end + + def to_s + @desc + end + end + + class CassandradbSession < Inspec.resource(1) + name "cassandradb_session" + supports platform: "unix" + supports platform: "windows" + desc "Use the cassandradb_session InSpec resource to test commands against an Cassandra database" + example <<~EXAMPLE + cql = cassandradb_session(user: 'my_user', password: 'password', host: 'host', port: 'port') + describe cql.query("SELECT cluster_name FROM system.local") do + its('output') { should match /Test Cluster/ } + end + EXAMPLE + + attr_reader :user, :password, :host, :port + + def initialize(opts = {}) + @user = opts[:user] || "cassandra" + @password = opts[:password] || "cassandra" + @host = opts[:host] + @port = opts[:port] + end + + def query(q) + cassandra_cmd = create_cassandra_cmd(q) + cmd = inspec.command(cassandra_cmd) + out = cmd.stdout + "\n" + cmd.stderr + if cmd.exit_status != 0 || out =~ /Unable to connect to any servers/ || out.downcase =~ /^error:.*/ + raise Inspec::Exceptions::ResourceFailed, "Cassandra query with errors: #{out}" + else + Lines.new(cmd.stdout.strip, "Cassandra query: #{q}") + end + end + + def to_s + "Cassandra DB Session" + end + + private + + def create_cassandra_cmd(q) + # TODO: simple escape, must be handled by a library + # that does this securely + escaped_query = q.gsub(/\\/, "\\\\").gsub(/"/, '\\"').gsub(/\$/, '\\$') + + # construct the query + command = "cqlsh" + command += " #{@host}" unless @host.nil? + command += " #{@port}" unless @port.nil? + command += " -u #{@user}" + command += " -p #{@password}" + command += " --execute '#{escaped_query}'" + command + end + end +end diff --git a/test/fixtures/cmd/cassandra-connection-error b/test/fixtures/cmd/cassandra-connection-error new file mode 100644 index 000000000..f3fa5c8a5 --- /dev/null +++ b/test/fixtures/cmd/cassandra-connection-error @@ -0,0 +1 @@ +Unable to connect to any servers \ No newline at end of file diff --git a/test/fixtures/cmd/cassandra-connection-success b/test/fixtures/cmd/cassandra-connection-success new file mode 100644 index 000000000..687392fcd --- /dev/null +++ b/test/fixtures/cmd/cassandra-connection-success @@ -0,0 +1 @@ +\r\n cluster_name\r\n--------------\r\n Test Cluster\r\n\r\n(1 rows)\r\n\n \ No newline at end of file diff --git a/test/fixtures/cmd/env b/test/fixtures/cmd/env index bf82033b2..e1f840fde 100644 --- a/test/fixtures/cmd/env +++ b/test/fixtures/cmd/env @@ -1,2 +1,3 @@ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ORACLE_HOME=/opt/oracle/product/18c/dbhomeXE +CASSANDRA_HOME=/etc/cassandra diff --git a/test/fixtures/cmd/fetch-cassandra-conf-in-windows b/test/fixtures/cmd/fetch-cassandra-conf-in-windows new file mode 100644 index 000000000..6a7078206 --- /dev/null +++ b/test/fixtures/cmd/fetch-cassandra-conf-in-windows @@ -0,0 +1 @@ +C:\Program Files\apache-cassandra-3.11.4-bin\apache-cassandra-3.11.4 \ No newline at end of file diff --git a/test/fixtures/files/cassandra.yaml b/test/fixtures/files/cassandra.yaml new file mode 100644 index 000000000..6fce6baaf --- /dev/null +++ b/test/fixtures/files/cassandra.yaml @@ -0,0 +1,6 @@ +cluster_name: 'Test Cluster' +num_tokens: 16 +listen_address: localhost +native_transport_port: 9042 +audit_logging_options: + enabled: false \ No newline at end of file diff --git a/test/helpers/mock_loader.rb b/test/helpers/mock_loader.rb index 61e882ddf..94ed37edf 100644 --- a/test/helpers/mock_loader.rb +++ b/test/helpers/mock_loader.rb @@ -115,6 +115,8 @@ class MockLoader "/etc/mongod.conf" => mockfile.call("mongod.conf"), "/opt/oracle/product/18c/dbhomeXE/network/admin/listener.ora" => mockfile.call("listener.ora"), "C:\\app\\Administrator\\product\\18.0.0\\dbhomeXE\\network\\admin\\listener.ora" => mockfile.call("listener.ora"), + "/etc/cassandra/cassandra.yaml" => mockfile.call("cassandra.yaml"), + "C:\\Program Files\\apache-cassandra-3.11.4-bin\\apache-cassandra-3.11.4\\conf\\cassandra.yaml" => mockfile.call("cassandra.yaml"), "/etc/rabbitmq/rabbitmq.config" => mockfile.call("rabbitmq.config"), "kitchen.yml" => mockfile.call("kitchen.yml"), "example.csv" => mockfile.call("example.csv"), @@ -502,6 +504,7 @@ class MockLoader "sh -c 'type \"sqlplus\"'" => cmd.call("oracle-cmd"), "1998da5bc0f09bd5258fad51f45447556572b747f631661831d6fcb49269a448" => cmd.call("oracle-result"), "${Env:ORACLE_HOME}" => cmd.call("fetch-oracle-listener-in-windows"), + "${Env:CASSANDRA_HOME}" => cmd.call("fetch-cassandra-conf-in-windows"), # nginx mock cmd %{nginx -V 2>&1} => cmd.call("nginx-v"), %{/usr/sbin/nginx -V 2>&1} => cmd.call("nginx-v"), diff --git a/test/unit/resources/cassandradb_conf_test.rb b/test/unit/resources/cassandradb_conf_test.rb new file mode 100644 index 000000000..c542b7a9c --- /dev/null +++ b/test/unit/resources/cassandradb_conf_test.rb @@ -0,0 +1,33 @@ +require "helper" +require "inspec/resource" +require "inspec/resources/cassandradb_conf" + +describe "Inspec::Resources::CassandradbConf" do + it "verify configurations of cassandra DB in linux when conf path is passed" do + resource = MockLoader.new(:centos7).load_resource("cassandradb_conf", "/etc/cassandra/cassandra.yaml") + _(resource.params["listen_address"]).must_equal "localhost" + _(resource.params["native_transport_port"]).must_equal 9042 + _(resource.params["audit_logging_options"]["enabled"]).must_equal false + end + + it "verify configurations of cassandra DB in windows when conf path is passed" do + resource = MockLoader.new(:windows).load_resource("cassandradb_conf", "C:\\Program Files\\apache-cassandra-3.11.4-bin\\apache-cassandra-3.11.4\\conf\\cassandra.yaml") + _(resource.params["listen_address"]).must_equal "localhost" + _(resource.params["native_transport_port"]).must_equal 9042 + _(resource.params["audit_logging_options"]["enabled"]).must_equal false + end + + it "verify configurations of cassandra DB in linux when conf path is not passed" do + resource = MockLoader.new(:centos7).load_resource("cassandradb_conf", nil) + _(resource.params["listen_address"]).must_equal "localhost" + _(resource.params["native_transport_port"]).must_equal 9042 + _(resource.params["audit_logging_options"]["enabled"]).must_equal false + end + + it "verify configurations of cassandra DB in windows when conf path is not passed" do + resource = MockLoader.new(:windows).load_resource("cassandradb_conf", nil) + _(resource.params["listen_address"]).must_equal "localhost" + _(resource.params["native_transport_port"]).must_equal 9042 + _(resource.params["audit_logging_options"]["enabled"]).must_equal false + end +end diff --git a/test/unit/resources/cassandradb_session_test.rb b/test/unit/resources/cassandradb_session_test.rb new file mode 100644 index 000000000..36cf53a50 --- /dev/null +++ b/test/unit/resources/cassandradb_session_test.rb @@ -0,0 +1,50 @@ +require "helper" +require "inspec/resource" +require "inspec/resources/cassandradb_session" + +describe "Inspec::Resources::CassandradbSession" do + it "verify cassandradb_session configuration" do + resource = load_resource("cassandradb_session", host: "localhost", port: 9042) + _(resource.resource_failed?).must_equal false + _(resource.user).must_equal "cassandra" + _(resource.password).must_equal "cassandra" + _(resource.host).must_equal "localhost" + _(resource.port).must_equal 9042 + end + + it "success when connection is estalished" do + resource = quick_resource(:cassandradb_session, :linux, user: "USER", password: "rightpassword", host: "localhost", port: 9042) do |cmd| + cmd.strip! + case cmd + when "cqlsh localhost 9042 -u USER -p rightpassword --execute 'SELECT cluster_name FROM system.local'" then + stdout_file "test/fixtures/cmd/cassandra-connection-success" + else + raise cmd.inspect + end + end + + _(resource.resource_failed?).must_equal false + query = resource.query("SELECT cluster_name FROM system.local") + _(query.output).must_match(/Test Cluster/) + end + + it "fails when no connection established" do + resource = quick_resource(:cassandradb_session, :linux, user: "USER", password: "wrongpassword", host: "localhost", port: 1234) do |cmd| + cmd.strip! + case cmd + when "cqlsh localhost 1234 -u USER -p wrongpassword --execute 'SELECT cluster_name FROM system.local'" then + stdout_file "test/fixtures/cmd/cassandra-connection-error" + else + raise cmd.inspect + end + ex = assert_raises(Inspec::Exceptions::ResourceFailed) { resource.query("SELECT cluster_name FROM system.local") } + _(ex.message).must_include("Cassandra query with errors") + end + end + + it "does not fails auth when no user or no password is provided" do + resource = quick_resource(:cassandradb_session, :linux) + _(resource.resource_failed?).must_equal false + end + +end