Skip to content

Commit

Permalink
Added DNS zone check
Browse files Browse the repository at this point in the history
  • Loading branch information
Yuri Zubov authored and majormoses committed Mar 29, 2018
1 parent 1cd5a00 commit 3fda4d1
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 12 deletions.
3 changes: 0 additions & 3 deletions .kitchen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ platforms:
- name: debian-8

suites:
- name: ruby-21
driver:
image: ruby:2.1-slim
- name: ruby-22
driver:
image: ruby:2.2-slim
Expand Down
1 change: 0 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

MethodLength:
Max: 200

Expand Down
2 changes: 0 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ before_install:
install:
- bundle install
rvm:
- 2.1
- 2.2
- 2.3.0
- 2.4.1
Expand All @@ -32,7 +31,6 @@ deploy:
on:
tags: true
all_branches: true
rvm: 2.1
rvm: 2.2
rvm: 2.3.0
rvm: 2.4.1
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ This project adheres to [Semantic Versioning](http://semver.org/).
This CHANGELOG follows the format listed [here](https://github.com/sensu-plugins/community/blob/master/HOW_WE_CHANGELOG.md)

## [Unreleased]
### Added
- Added many checks for DNS zone (@yuri-zubov sponsored by Actility, https://www.actility.com)

### Security
- updated yard dependency to `~> 0.9.11` per: https://nvd.nist.gov/vuln/detail/CVE-2017-17042 (@yuri-zubov sponsored by Actility, https://www.actility.com)
- updated rubocop dependency to `~> 0.51.0` per: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-8418. (@yuri-zubov sponsored by Actility, https://www.actility.com)


### Breaking Changes
- Dropping ruby `< 2.2` support (@yuri-zubov)

## [1.4.0] - 2018-03-21
### Added
Expand Down
193 changes: 193 additions & 0 deletions bin/check-dns-zone.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#! /usr/bin/env ruby
#
# check-dns
#
# DESCRIPTION:
# This plugin checks DNS Zone using dnsruby and whois.
#
# OUTPUT:
# plain text
#
# PLATFORMS:
# Linux, BSD
#
# DEPENDENCIES:
# gem: sensu-plugin
# gem: dnsruby
# gem: whois
#
# USAGE:
# example commands
#
# NOTES:
# Does it behave differently on specific platforms, specific use cases, etc
#
# LICENSE:
# Zubov Yuri <[email protected]> sponsored by Actility, https://www.actility.com
# Released under the same terms as Sensu (the MIT license); see LICENSE
# for details.
#

require 'sensu-plugin/check/cli'
require 'dnsruby'
require 'whois'
require 'whois/parser'
require 'ipaddr'
require 'resolv'

#
# DNS
#
class DNSZone < Sensu::Plugin::Check::CLI
option :domain,
description: 'Domain to resolve (or ip if type PTR)',
short: '-d DOMAIN',
long: '--domain DOMAIN'

option :threshold,
description: 'Percentage of DNS queries that must succeed',
short: '-l PERCENT',
long: '--threshold PERCENT',
proc: proc(&:to_i),
default: 100

option :timeout,
description: 'Set timeout for query',
short: '-T TIMEOUT',
long: '--timeout TIMEOUT',
proc: proc(&:to_i),
default: 5

option :warn_only,
description: 'Warn instead of critical on failure',
short: '-w',
long: '--warn-only',
boolean: true

def resolve_ns
Resolv::DNS.new.getresources(config[:domain], Resolv::DNS::Resource::IN::NS).map { |e| e.name.to_s }
end

def check_whois(entries)
errors = []
success = []
record = Whois.whois(config[:domain])
parser = record.parser
additional_text = "(whois #{parser.nameservers.map(&:name)}) (dig #{entries})"
if Set.new(parser.nameservers.map(&:name)) == Set.new(entries)
success << "Resolved #{config[:domain]} #{config[:type]} equal with whois #{additional_text}"
else
errors << "Resolved #{config[:domain]} #{config[:type]} did not include #{config[:result]} #{additional_text}"
end
[errors, success]
end

def check_results(entries)
errors = []
success = []

%w[check_whois check_axfr soa_query].each do |check|
result = send(check, entries)
errors.push(*result[0])
success.push(*result[1])
end

result = check_dns_connection(entries, true)
errors.push(*result[0])
success.push(*result[1])

result = check_dns_connection(entries, false)
errors.push(*result[0])
success.push(*result[1])

[errors, success]
end

def check_dns_connection(entries, use_tcp = false)
errors = []
success = []

entries.each do |ns|
Dnsruby::Resolv.getaddresses(ns).each do |ip|
begin
resolv = Dnsruby::Resolver.new(nameserver: ip.to_s, do_caching: false, use_tcp: use_tcp)
resolv.query_timeout = config[:timeout]
resolv.query(config[:domain], Dnsruby::Types.ANY)
type_of_connection = use_tcp ? 'tcp' : 'udp'
success << "Resolved DNS #{ns}(#{ip}) uses #{type_of_connection}"
rescue StandardError
errors << "Resolved DNS #{ns}(#{ip}) doesn't use #{type_of_connection}"
end
end
end
[errors, success]
end

def check_axfr(entries)
errors = []
success = []

entries.each do |ns|
Dnsruby::Resolv.new.getaddresses(ns).each do |ip|
begin
resolv = Dnsruby::Resolver.new(nameserver: ip.to_s, do_caching: false)
resolv.query_timeout = config[:timeout]
resolv.query(config[:domain], 'AXFR', 'IN')

errors << "Resolved DNS #{ns}(#{ip}) has AXFR"
rescue StandardError
success << "Resolved DNS #{ns}(#{ip}) doesn't have AXFR"
end
end
end
[errors, success]
end

def soa_query(entries)
errors = []
success = []
resp = ::Resolv::DNS.new.getresources(config[:domain], Resolv::DNS::Resource::IN::SOA)
primary_serial_number = resp.map(&:serial).first
primary_server = resp.map { |r| r.mname.to_s }
primary_server_name = resp.map { |r| r.rname.to_s }

entries.each_with_index do |ns, _index|
::Resolv::DNS.new.getaddresses(ns).each do |ip|
serial_number = nil
server_name = nil
server = nil

::Resolv::DNS.open nameserver: ip.to_s, ndots: 1 do |dns|
resp = dns.getresources(config[:domain], Resolv::DNS::Resource::IN::SOA)
serial_number = resp.map(&:serial).first
server_name = resp.map { |r| r.rname.to_s }
server = resp.map { |r| r.mname.to_s }
end

if serial_number == primary_serial_number
success << "SOA Query correct for server #{ns}(#{ip})} SOA #{server_name} (#{serial_number}) #{server} - \
SOA primary server #{primary_server_name} (#{primary_serial_number}) #{primary_server}"
else
errors << "SOA Query wrong for server #{ns}(#{ip})} SOA #{server_name} (#{serial_number}) #{server} - \
SOA primary server #{primary_server_name} (#{primary_serial_number}) #{primary_server}"
end
end
end

[errors, success]
end

def run
unknown 'No domain specified' if config[:domain].nil?

entries = resolve_ns
errors, success = check_results(entries)
percent = success.count.to_f / (success.count.to_f + errors.count.to_f) * 100
if percent < config[:threshold]
output = "#{percent.to_i}% of tests succeeded: #{errors.uniq.join(", \n")}"
config[:warn_only] ? warning(output) : critical(output)
else
ok(success.uniq.join(", \n"))
end
end
end
4 changes: 2 additions & 2 deletions bin/check-dns.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def resolve_domain(server)
begin
entry = resolv.query(config[:domain], config[:type], config[:class])
resolv.query_timeout = config[:timeout]
rescue => e
rescue StandardError => e
entry = e
end
entries << entry
Expand All @@ -155,7 +155,7 @@ def check_against_regex(entries, regex)
if answer.match(regex)
ok "Resolved #{config[:domain]} #{config[:type]} matched #{regex}"
end
end # b.each()
end
critical "Resolved #{config[:domain]} #{config[:type]} did not match #{regex}"
end

Expand Down
2 changes: 1 addition & 1 deletion bin/metrics-dns.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def run
output "#{config[:scheme]}.#{config[:type]}.#{key_name}.response_time", result.to_f.round(8)
rescue Dnsruby::NXDomain
critical "Could not resolve #{config[:domain]} #{config[:type]} record"
rescue => e
rescue StandardError => e
unknown e
end

Expand Down
8 changes: 5 additions & 3 deletions sensu-plugins-dns.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
s.platform = Gem::Platform::RUBY
s.post_install_message = 'You can use the embedded Ruby by setting EMBEDDED_RUBY=true in /etc/default/sensu'
s.require_paths = ['lib']
s.required_ruby_version = '>= 2.0.0'
s.required_ruby_version = '>= 2.2.0'
# s.signing_key = File.expand_path(pvt_key) if $PROGRAM_NAME =~ /gem\z/
s.summary = 'Sensu plugins for dns'
s.test_files = s.files.grep(%r{^(test|spec|features)/})
Expand All @@ -38,6 +38,8 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
s.add_runtime_dependency 'dnsruby', '~> 1.59', '>= 1.59.2'
s.add_development_dependency 'codeclimate-test-reporter', '~> 0.4'
s.add_development_dependency 'github-markup', '~> 1.3'
s.add_runtime_dependency 'whois', '~> 4.0'
s.add_runtime_dependency 'whois-parser', '~> 1.0'
s.add_development_dependency 'kitchen-docker', '~> 2.6'
s.add_development_dependency 'kitchen-localhost', '~> 0.3'
# locked to keep ruby 2.1 support, this is pulled in by test-kitchen
Expand All @@ -46,8 +48,8 @@ Gem::Specification.new do |s| # rubocop:disable Metrics/BlockLength
s.add_development_dependency 'rake', '~> 10.0'
s.add_development_dependency 'redcarpet', '~> 3.2'
s.add_development_dependency 'rspec', '~> 3.1'
s.add_development_dependency 'rubocop', '~> 0.49.0'
s.add_development_dependency 'rubocop', '~> 0.51.0'
# intentionally locked as 1.17 requires ruby 2.3+
s.add_development_dependency 'test-kitchen', '~> 1.16.0'
s.add_development_dependency 'yard', '~> 0.8'
s.add_development_dependency 'yard', '~> 0.9.11'
end

0 comments on commit 3fda4d1

Please sign in to comment.