-
Notifications
You must be signed in to change notification settings - Fork 0
/
check_metadata
executable file
·152 lines (138 loc) · 8.24 KB
/
check_metadata
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/usr/bin/env perl
# @author Guy Halse http://orcid.org/0000-0002-9388-8592
# @copyright Copyright (c) 2017, Tertiary Education and Research Network of South Africa
# @license https://github.com/tenet-ac-za/monitoring-plugins/blob/master/LICENSE MIT License
#
use strict;
use warnings;
use Monitoring::Plugin;
use LWP::UserAgent;
use HTTP::Status;
use URI;
use Date::Parse;
use POSIX;
use File::Temp qw/tempfile/;
use XML::LibXML;
use XML::LibXML::XPathContext;
my $np = Monitoring::Plugin->new(
'usage' => 'Usage: %s [ -v|--verbose ] [-H <vhost>] [-I <address>] [-t <timeout>] [-u <url>]',
'license' => 'MIT License <https://github.com/tenet-ac-za/monitoring-plugins/blob/master/LICENSE>',
'version' => '0.1',
'blurb' => "A simple plugin to check remote SAML metadata feeds",
'extra' => "
There are a *lot* of features we'd like to add to this, most notably
it should do better xmldsig checking. We may get there eventually...
",
);
$np->add_arg('spec' => 'url|u=s', 'help' => 'URL to fetch (default: /)', 'default' => '/',);
$np->add_arg('spec' => 'host|H=s', 'help' => 'Hostname to use for vhost', 'required' => 1);
$np->add_arg('spec' => 'address|I=s', 'help' => 'IP address or name (use numeric address if possible to bypass DNS lookup).');
$np->add_arg('spec' => 'port|p=i', 'help' => 'Port to use (defaults to 80 or 443)',);
$np->add_arg('spec' => 'ssl|S', 'help' => 'Use SSL');
$np->add_arg('spec' => 'xmlsig|x=s', 'help' => 'PEM encoded certificate to use for XMLdsig verification');
$np->add_arg('spec' => 'vw=i', 'help' => 'Warn if validty < vw days (default=2)', 'default' => 2);
$np->add_arg('spec' => 'vc=i', 'help' => 'Critical if validty < vc days (default=1)', 'default' => 1);
$np->add_arg('spec' => 'ew=i', 'help' => 'Warn if md:EntityDescriptor count < ew (default=10)', 'default' => 10);
$np->add_arg('spec' => 'ec=i', 'help' => 'Critical if md:EntityDescriptor count < ec (default=5)', 'default' => 5);
$np->add_arg('spec' => 'iw=i', 'help' => 'Warn if md:IDPSSODescriptor count < iw (default=1)', 'default' => 1);
$np->add_arg('spec' => 'ic=i', 'help' => 'Critical if md:IDPSSODescriptor count < ic (default=0)', 'default' => 0);
$np->add_arg('spec' => 'sw=i', 'help' => 'Warn if md:SPSSODescriptor count < sw (default=1)', 'default' => 1);
$np->add_arg('spec' => 'sc=i', 'help' => 'Critical if md:SPSSODescriptor count < sc (default=0)', 'default' => 0);
$np->add_arg('spec' => 'fw=i', 'help' => 'Warn if HTTP document expires < fw days (default=4)', 'default' => 4);
$np->add_arg('spec' => 'fc=i', 'help' => 'Critical if HTTP document expires < fc days (default=4)', 'default' => 2);
$np->getopts;
# sensible defaults for port
my $port = $np->opts->port ? $np->opts->port : ( $np->opts->ssl ? 443 : 80);
# construct a URI, congniscent of the fact we may be dealing with vhosting
my $uri = new URI(($np->opts->ssl ? 'https' : 'http') . '://' . ($np->opts->address ? $np->opts->address : $np->opts->host) . ':' . $port . $np->opts->url, $np->opts->ssl ? 'https' : 'http');
print STDERR "Set URI to $uri\n" if $np->opts->verbose;
# Fetch the metadata XML file
my $ua = LWP::UserAgent->new;
$ua->timeout($np->opts->timeout);
$ua->ssl_opts('SSL_ca_path' => '/etc/ssl/certs');
my $res = $ua->get($uri, 'Host' => $np->opts->host);
$np->plugin_die("Timeout!") if $res->code == HTTP::Status::HTTP_REQUEST_TIMEOUT;
if (!$res->is_success()) {
$np->plugin_exit(CRITICAL, 'HTTP error ' . $res->code . ' ' . $res->message);
}
print STDERR "Got LWP response " . $res->code . "\n" if $np->opts->verbose;
# Check the freshness (Expires, Cache-Control max-age, etc)
print STDERR "fresh_until=" . $res->fresh_until() . " (" . strftime('%FT%TZ',gmtime($res->fresh_until())) . ") fc=" . $np->opts->fc . " fw=" . $np->opts->fw . "\n" if $np->opts->verbose;
if ($res->fresh_until() > time() + ($np->opts->fc * 86400)) {
$np->add_message(CRITICAL, "fresh_until=" . $res->fresh_until() . " > " . $np->opts->fc . " days");
} elsif ($res->fresh_until() > time() + ($np->opts->fw * 86400)) {
$np->add_message(WARNING, "fresh_until=" . $res->fresh_until() . " > " . $np->opts->fw . " days");
}
# Parse the XML
my $xml = new XML::LibXML; my $dom;
eval { $dom = $xml->load_xml('string' => $res->decoded_content); };
if ( $@ ) { $np->plugin_exit(CRITICAL, 'Response is not well-formed XML'); }
print STDERR "Response is well-formed XML\n" if $np->opts->verbose;
# Attempt to verify xmldsig using xmlsec1 if it is available (hacky)
if ($np->opts->xmlsig) {
$np->plugin_die('Cannot find /usr/bin/xmlsec1') if (! -f '/usr/bin/xmlsec1');
my($fh, $filename) = tempfile();
print $fh $res->decoded_content;
my $cert = $np->opts->xmlsig;
my $out = qx{/usr/bin/xmlsec1 --verify --disable-error-msgs --trusted-pem '$cert' --id-attr:ID 'urn:oasis:names:tc:SAML:2.0:metadata:EntitiesDescriptor' '$filename' 2>&1};
print STDERR "XML Sig: $out\n" if $np->opts->verbose;
if ($? != 0 or $out !~ m/^OK/) {
$np->add_message(CRITICAL, "XML signature validation failed");
}
close($fh);
unlink($filename) if -f $filename and not $np->opts->verbose;
}
# Create an XPath object with the right namespaces
my $xp = XML::LibXML::XPathContext->new($dom->getDocumentElement());
$xp->registerNs('md', 'urn:oasis:names:tc:SAML:2.0:metadata');
$xp->registerNs('shibmd', 'urn:mace:shibboleth:metadata:1.0');
$xp->registerNs('mdui', 'urn:oasis:names:tc:SAML:metadata:ui');
$xp->registerNs('ds', 'http://www.w3.org/2000/09/xmldsig#');
$xp->registerNs('mdrpi', 'urn:oasis:names:tc:SAML:metadata:rpi');
$xp->registerNs('mdattr', 'urn:oasis:names:tc:SAML:metadata:attribute');
$xp->registerNs('saml', 'urn:oasis:names:tc:SAML:2.0:assertion');
# Check the validity
my ($validUntil) = $xp->findvalue('/md:EntitiesDescriptor/@validUntil');
($validUntil) = $xp->findvalue('/md:EntityDescriptor/@validUntil') unless $validUntil;
print STDERR "validUntil=$validUntil vc=" . $np->opts->vc . " vw=" . $np->opts->vw . "\n" if $np->opts->verbose;
if (!$validUntil or str2time($validUntil) < time + ($np->opts->vc * 86400)) {
$np->add_message(CRITICAL, 'Metadata validUntil=' . $validUntil . ' < ' . $np->opts->vc . ' days');
} elsif (str2time($validUntil) < time + ($np->opts->vw * 86400)) {
$np->add_message(WARNING, 'Metadata validUntil=' . $validUntil . ' < ' . $np->opts->vw . ' days');
}
# Count the number of entities
my ($entities) = $xp->findvalue('count(//md:EntityDescriptor)');
$entities+=0;
print STDERR "count(md:EntityDescriptor)=$entities ec=" . $np->opts->ec . " ew=" . $np->opts->ew . "\n" if $np->opts->verbose;
$np->add_perfdata('label' => 'entities', 'value' => $entities, 'min' => 0, 'warning' => $np->opts->ew, 'critical' => $np->opts->ec);
if ($entities < $np->opts->ec) {
$np->add_message(CRITICAL, 'count(md:EntityDescriptor)=' . $entities . ' < ' . $np->opts->ec);
} elsif ($entities < $np->opts->ew) {
$np->add_message(WARNING, 'count(md:EntityDescriptor)=' . $entities . ' < ' . $np->opts->ew);
}
# Count the number of IdPs
my ($idps) = $xp->findvalue('count(//md:EntityDescriptor/md:IDPSSODescriptor)', $dom);
$idps+=0;
print STDERR "count(md:IDPSSODescriptor)=$idps ic=" . $np->opts->ic . " iw=" . $np->opts->iw . "\n" if $np->opts->verbose;
$np->add_perfdata('label' => 'idps', 'value' => $idps, 'min' => 0, 'warning' => $np->opts->iw, 'critical' => $np->opts->ic);
if ($idps < $np->opts->ic) {
$np->add_message(CRITICAL, 'count(md:IDPSSODescriptor)=' . $idps . ' < ' . $np->opts->ic);
} elsif ($idps < $np->opts->iw) {
$np->add_message(WARNING, 'count(md:IDPSSODescriptor)=' . $idps . ' < ' . $np->opts->iw);
}
# Count the number of SPs
my ($sps) = $xp->findvalue('count(//md:EntityDescriptor/md:SPSSODescriptor)', $dom);
print STDERR "count(md:SPSSODescriptor)=$sps sc=" . $np->opts->sc . " sw=" . $np->opts->sw . "\n" if $np->opts->verbose;
$np->add_perfdata('label' => 'sps', 'value' => $sps, 'min' => 0, 'warning' => $np->opts->sw, 'critical' => $np->opts->sc);
if ($sps < $np->opts->sc) {
$np->add_message(CRITICAL, 'count(md:SPSSODescriptor)=' . $sps . ' < ' . $np->opts->sc);
} elsif ($sps < $np->opts->sw) {
$np->add_message(WARNING, 'count(md:SPSSODescriptor)=' . $sps . ' < ' . $np->opts->sw);
}
# All checks complete, return something
my ($code, $message) = $np->check_messages();
if ($code == OK) {
$np->plugin_exit(OK, "$entities entities {idp:$idps, sp:$sps}");
} else {
$np->plugin_exit($code, $message);
}