Skip to content

system for parsing JSON data structures and identifying anomalies

License

Notifications You must be signed in to change notification settings

oposs/jsonhound

Repository files navigation

jsonHound Build Status

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.

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.

Identification

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

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:

  1. 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.
  2. 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.

Disjunctions of identifiers

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.

The command line interface

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 appropriate
  • nagios - Nagios plugin output and exit code
  • syslog - 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).

Debug messages

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

Running with Docker

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