Merge pull request #5683 from inspec/nm/cassandra-resource

Add support for Cassandra DB
This commit is contained in:
Clinton Wolfe 2021-10-05 21:55:24 -04:00 committed by GitHub
commit a79be0ca96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 399 additions and 0 deletions

View file

@ -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/).

View file

@ -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/).

View file

@ -37,6 +37,9 @@ require "inspec/resources/chocolatey_package"
require "inspec/resources/command" require "inspec/resources/command"
require "inspec/resources/cran" require "inspec/resources/cran"
require "inspec/resources/cpan" 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/crontab"
require "inspec/resources/dh_params" require "inspec/resources/dh_params"
require "inspec/resources/directory" require "inspec/resources/directory"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
Unable to connect to any servers

View file

@ -0,0 +1 @@
\r\n cluster_name\r\n--------------\r\n Test Cluster\r\n\r\n(1 rows)\r\n\n

View file

@ -1,2 +1,3 @@
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ORACLE_HOME=/opt/oracle/product/18c/dbhomeXE ORACLE_HOME=/opt/oracle/product/18c/dbhomeXE
CASSANDRA_HOME=/etc/cassandra

View file

@ -0,0 +1 @@
C:\Program Files\apache-cassandra-3.11.4-bin\apache-cassandra-3.11.4

6
test/fixtures/files/cassandra.yaml vendored Normal file
View file

@ -0,0 +1,6 @@
cluster_name: 'Test Cluster'
num_tokens: 16
listen_address: localhost
native_transport_port: 9042
audit_logging_options:
enabled: false

View file

@ -115,6 +115,8 @@ class MockLoader
"/etc/mongod.conf" => mockfile.call("mongod.conf"), "/etc/mongod.conf" => mockfile.call("mongod.conf"),
"/opt/oracle/product/18c/dbhomeXE/network/admin/listener.ora" => mockfile.call("listener.ora"), "/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"), "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"), "/etc/rabbitmq/rabbitmq.config" => mockfile.call("rabbitmq.config"),
"kitchen.yml" => mockfile.call("kitchen.yml"), "kitchen.yml" => mockfile.call("kitchen.yml"),
"example.csv" => mockfile.call("example.csv"), "example.csv" => mockfile.call("example.csv"),
@ -502,6 +504,7 @@ class MockLoader
"sh -c 'type \"sqlplus\"'" => cmd.call("oracle-cmd"), "sh -c 'type \"sqlplus\"'" => cmd.call("oracle-cmd"),
"1998da5bc0f09bd5258fad51f45447556572b747f631661831d6fcb49269a448" => cmd.call("oracle-result"), "1998da5bc0f09bd5258fad51f45447556572b747f631661831d6fcb49269a448" => cmd.call("oracle-result"),
"${Env:ORACLE_HOME}" => cmd.call("fetch-oracle-listener-in-windows"), "${Env:ORACLE_HOME}" => cmd.call("fetch-oracle-listener-in-windows"),
"${Env:CASSANDRA_HOME}" => cmd.call("fetch-cassandra-conf-in-windows"),
# nginx mock cmd # nginx mock cmd
%{nginx -V 2>&1} => cmd.call("nginx-v"), %{nginx -V 2>&1} => cmd.call("nginx-v"),
%{/usr/sbin/nginx -V 2>&1} => cmd.call("nginx-v"), %{/usr/sbin/nginx -V 2>&1} => cmd.call("nginx-v"),

View file

@ -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

View file

@ -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