txt: ## dist_github-oposs-JSONHound.md
  # jsonHound [Build Status](https://travis-ci.org/oposs/jsonhound)
  
  *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
  
  ```
  