a system for parsing JSON data structures and identifying anomalies
While the name and the structure of this tool is very generic, it was built for a highly specific purpose: Modern Cisco Switches allow export of their configuration in JSON format. The purpose of the jsonHound is to identify misconfiguration of the switches. It does that by identifying interesting data structures, like interfaces, and then choosing a set of checks to verify that they are properly configured. The checks are chosen by looking at features of the interface or the configuration as a whole. For example, if an interface is part of VLAN 643 we know that this is part of your IP Telefony VLAN and will thus require a particular set of configuration options to be active.
The configuration of the jsonHound works in 3 stages:
- Stage 1 identifies the "interesting structures"
- Stage 2 applies a set of checks to these structures
jsonHound is implemented in in Raku programming language, and Raku is also used to write the jsonHound rule files.
A jsonHound rule file is just a Raku module that does use JsonHound
at the top, and contains
identification and validation setup. It's fine to use Raku language features to help factor out
re-use within the ruleset, and even to spread the rules over multiple modules, and use
those.
The identification state is set up by declaring Raku subset
types, which pick out "subsets"
of the JSON document to check. The simplest way to identify part of the document that should be
considered is by using JSONPath:
subset ArpInspection
is json-path(q[$['Cisco-IOS-XE-native:native'].ip.arp.inspection.vlan]);
A where
clause can be applied in order to further constrain what is matched, by looking
into the deserialized JSON that was matched by the JSONPath query.
subset GigabitEthernet
is json-path(q[$['Cisco-IOS-XE-native:native'].interface.GigabitEthernet[*]])
where {
[&&] .keys > 1, # Not just a name
.<name> ne '0/0',
(.<description> // '') ne '__SKIP__'
}
It's also possible to add further constraints (but not further JSONPath) using a
derived subset
type:
sub get-vlan($ge) {
$ge<switchport><Cisco-IOS-XE-switch:access><vlan><vlan>
}
subset VLan31
of GigabitEthernet
where { get-vlan($_) == 31 };
Validations are set up by calling the validate
sub, passing it a name for the validation
(to be displayed upon failure) and one or more identified document sections. For example:
validate 'dot1x-not-set', -> VLan31 $ge {
$ge<Cisco-IOS-XE-dot1x:dot1x>:exists
}
The block should evaluate to a true value if the validation is successful. If it evalutes to a false value, then validation fails and this will be reported. The JSON that was matched in the identification phase is passed using the variable declared in the signature.
The validate
block will be called once for each matching item. In the event that multiple
parameters are specified, then it will be called with the product of them (e.g. all of the
combinations). For example, given:
validate 'Missing global DHCP snooping', -> ArpInspection $inspection, GigabitEthernet $ge {
$ge<switchport><Cisco-IOS-XE-switch:access><vlan><vlan> ~~ any(ranges($inspection))
}
sub ranges($range-list) {
$range-list.split(',').map({ /(\d+)['-'(\d+)]?/; $1 ?? (+$0 .. +$1) !! +$0 })
}
If ArpInspection
matches 1 time and GigabitEthernet
matches 4 times, then it will be
called 1 * 4 = 4
times (the most typical use here is to pick out sections to match up
with some global value).
It is possible on report extra information by placing it into the validation rule's name. This is done by:
- Passing a block that takes named parameters and uses them to build up the name of the
rule, which will then be reported. It's neatest to do this using the
$:name
named parameter placeholder syntax. - In case of validation failure, providing arguments for use in that reporting by passing
them to the
report
function.
For example:
validate {"Wrong reauthentication value (was $:value)"}, -> Authentication $auth {
my $value = $auth<timer><reauthenticate><value>;
if $value == 1800 {
True
}
else {
report :$value;
False
}
}
However, since report
returns False
, simple rules like this may instead be written
as simply:
validate {"Wrong reauthentication value (was $:value)"}, -> Authentication $auth {
my $value = $auth<timer><reauthenticate><value>;
$value == 1800 or report :$value
}
It is allowed to have multiple such parameters, which may be provided with a single
call to report
or multiple calls to report
over time. Do as is most comfortable
for the rule being implemented.
It's possible to use the Either[...]
construct to produce a disjunction of two
or more identifiers. For example, given:
subset MegabitEthernet
is json-path(q[$['Cisco-IOS-XE-native:native'].interface.MegabitEthernet[*]]);
subset GigabitEthernet
is json-path(q[$['Cisco-IOS-XE-native:native'].interface.GigabitEthernet[*]]);
One could write a validation rule that applies to either by doing:
validate 'Ethernet correctly configured', -> Either[MegabitEthernet, GigabitEthernet] $eth {
...
}
If the same Either
expression would be repeated multiple times, it may be factored
out by declaring a constant
:
my constant Ethernet = Either[MegabitEthernet, GigabitEthernet];
validate 'Ethernet correctly configured', -> Ethernet $eth {
...
}
A validation rule using multiple Either
types will be invoked for all
permutations of matches, as is usually the case with multi-parameter rules.
Once installed, run with:
jsonhound RuleFile.pm6 file1.json file2.json
It's possible to read the json data from STDIN
directly using -
cat t/03-wrong-reauth.json | jsonhound examples/GigabitEthernetChecks.pm6 -
To run it within the repository (e.g. for development), do:
raku -Ilib bin/jsonhound RuleFile.pm file1.json file2.json
If more than one JSON input file is specified, then they will be parsed and validated in parallel.
The default is to send a report to STDERR and to exit with 0 if all validation rule
passed, or 1 if there is a validation rule failure. However, this can be controlled
by passing the --reporter=...
command line option. Valid options are:
cli
- the default human-friendly output to STDERR, with exit code 0 or 1 as appropriatenagios
- Nagios plugin output and exit codesyslog
- Report any validation rule failures to syslog as warnings; has no impact on exit code
It is allowed to combine these by listing them comma-separated. However, note that the first reporter that provides an exit code will be the one that gets to decide the exit code. Thus this:
raku -Ilib bin/jsonhound --reporter=nagios,cli,syslog RuleFile.pm file1.json
Is probably correct (the nagios
reporter controls the exit code), while:
raku -Ilib bin/jsonhound --reporter=cli,nagios,syslog RuleFile.pm file1.json
Is probably not what's wanted (however, in this case one would probably also get
away with it, in so far as 0
and 1
are quite sensible exit codes for a Nagios
plugin anyway).
To produce a debug message in a validation rule, call debug
and pass the
message (whatever is passed will be coerced to a string, if it is not one
already).
validate 'Missing global DHCP snooping', -> ArpInspection $inspection, GigabitEthernet $ge {
my $vlan = $ge<switchport><Cisco-IOS-XE-switch:access><vlan><vlan>;
debug "VLan is $vlan";
$vlan ~~ any(ranges($inspection))
}
By default, these are not reported. However, they can be reported by the
cli
reporter by passing --debug=failed
(only report debug output from
failed validation rules) or --debug=all
(report all debug output).
You can also run the tool with docker directly from the git checkout without installing raku locally.
docker-compose build
docker-compose run jsonhound examples/GigabitEthernetChecks.pm6 t/00-Switch1-OK.json