Parsing Command Output in Ansible with JC

Ansible is a popular automation framework that allows you to configure any number of remote hosts in a declarative and idempotent way. A common use-case is to run a shell command on the remote host, return the STDOUT output, loop through it and parse it.

Starting in Ansible 2.9 with the community.general collection, it is possible to use jc as a filter to automatically parse the command output for you so you can easily use the output as an object. The official filter documentation can be found here. Even more detailed documentation can be found here.

For more information on the motivations for creating jc, see my blog post.

Installation

To use the jc filter plugin, you just need to install jc and the community.general collection on the Ansible controller. Ansible version 2.9 or higher is required to install the community.general collection.

Installing jc:

$ pip3 install jc

Installing the community.general Ansible collection:

$ ansible-galaxy collection install community.general

Now we are ready to use the jc filter plugin!

Syntax

To use the jc filter plugin you just need to pipe the command output to the plugin and specify the parser as an argument. For example, this is how you would parse the output of ps on the remote host:

  tasks:
  - shell: ps aux
    register: result
  - set_fact:
      myvar: "{{ result.stdout | community.general.jc('ps') }}"

Note: Use underscores instead of dashes (if any) in the parser name. e.g. git-log becomes git_log

This will generate a myvar object that includes the exact same information you would have received by running jc ps aux on the remote host. Now you can use object notation to pull out the information you are interested in.

A Simple Example

Let’s put it all together with a very simple example. In this example we will run the date command on the remote host and print the timezone as a debug message:

- name: Get Timezone
  hosts: ubuntu
  tasks:
  - shell: date
    register: result
  - set_fact:
      myvar: "{{ result.stdout | community.general.jc('date') }}"
  - debug:
      msg: "The timezone is: {{ myvar.timezone }}"

Instead of parsing the STDOUT text manually, we used the timezone attribute of the myvar object that jc gave us. Let’s see this in action:

$ ansible-playbook get-timezone.yml 

PLAY [Get Timezone] *****************************************************************************

TASK [Gathering Facts] **************************************************************************
ok: [192.168.1.239]

TASK [shell] ************************************************************************************
changed: [192.168.1.239]

TASK [set_fact] *********************************************************************************
ok: [192.168.1.239]

TASK [debug] ************************************************************************************
ok: [192.168.1.239] => {
    "msg": "The timezone is: UTC"
}

PLAY RECAP **************************************************************************************
192.168.1.239              : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Simple – no more need to grep/awk/sed your way through the output to get what you are looking for!

For a complete list of jc parsers available and their associated schemas, see the parser documentation.

Happy parsing!

JC Version 1.13.1 Released

Try the jc web demo!

I’m happy to announce the release of jc version 1.13.1 available on github and pypi.

jc now supports over 55 commands and file-types, including the new ping, sysctl, traceroute, and tracepath command parsers. The INI file parser has been enhanced to support simple key/value text files and the route command parser now supports IPv6 tables.

Custom local parser plugins are now supported. This allows overriding existing parsers and rapid development of new parsers.

Other updates include verbose debugging, more consistent handling of empty data, and many parser fixes for FreeBSD.

jc can be installed via pip or through several new official OS package repositories, including Fedora, openSUSE, Arch Linux, NixOS Linux, Guix System Linux, FreeBSD, and macOS. For more information on how to get jc, click here.

To upgrade with pip:

$ pip3 install --upgrade jc

New Features

  • jc is now available on the official Fedora repository (dnf install jc)
  • jc is now available on the official Arch Linux repository (pacman -S jc)
  • jc is now available on the official NixOS repository (nix-env -iA nixpkgs.jc)
  • jc is now available on the official Guix System Linux repository (guix install jc)
  • jc is now available on the official FreeBSD ports repository (portsnap fetch update && cd /usr/ports/textproc/py-jc && make install clean)
  • jc is in process (Intent To Package) for Debian packaging.
  • Local custom parser plugins allow you to override packaged parsers or rapidly create your own.
  • Verbose debugging is now supported with the -dd command argument.
  • All parsers now correctly return empty objects when sent empty data.
  • Older versions of the pygments library (>=2.3.0) are now supported (for Debian packaging)

New Parsers

jc now supports 55 parsers. New parsers include ping, sysctl, tracepath, and traceroute.

Documentation and schemas for all parsers can be found here.

ping command parser

Linux, macOS, and FreeBSD support for the ping command:

$ ping 8.8.8.8 -c 3 | jc --ping -p          # or:  jc -p ping 8.8.8.8 -c 3
{
  "destination_ip": "8.8.8.8",
  "data_bytes": 56,
  "pattern": null,
  "destination": "8.8.8.8",
  "packets_transmitted": 3,
  "packets_received": 3,
  "packet_loss_percent": 0.0,
  "duplicates": 0,
  "time_ms": 2005.0,
  "round_trip_ms_min": 23.835,
  "round_trip_ms_avg": 30.46,
  "round_trip_ms_max": 34.838,
  "round_trip_ms_stddev": 4.766,
  "responses": [
    {
      "type": "reply",
      "timestamp": null,
      "bytes": 64,
      "response_ip": "8.8.8.8",
      "icmp_seq": 1,
      "ttl": 118,
      "time_ms": 23.8,
      "duplicate": false
    },
    {
      "type": "reply",
      "timestamp": null,
      "bytes": 64,
      "response_ip": "8.8.8.8",
      "icmp_seq": 2,
      "ttl": 118,
      "time_ms": 34.8,
      "duplicate": false
    },
    {
      "type": "reply",
      "timestamp": null,
      "bytes": 64,
      "response_ip": "8.8.8.8",
      "icmp_seq": 3,
      "ttl": 118,
      "time_ms": 32.7,
      "duplicate": false
    }
  ]
}

sysctl command parser

Linux, macOS, and FreeBSD support for the sysctl -a command:

$ sysctl -a | jc --sysctl -p          # or:  jc -p sysctl -a
{
  "user.cs_path": "/usr/bin:/bin:/usr/sbin:/sbin",
  "user.bc_base_max": 99,
  "user.bc_dim_max": 2048,
  "user.bc_scale_max": 99,
  "user.bc_string_max": 1000,
  "user.coll_weights_max": 2,
  "user.expr_nest_max": 32
  ...
}

tracepath command parser

Linux support for the tracepath command:

$ tracepath6 3ffe:2400:0:109::2 | jc --tracepath -p
{
  "pmtu": 1480,
  "forward_hops": 2,
  "return_hops": 2,
  "hops": [
    {
      "ttl": 1,
      "guess": true,
      "host": "[LOCALHOST]",
      "reply_ms": null,
      "pmtu": 1500,
      "asymmetric_difference": null,
      "reached": false
    },
    {
      "ttl": 1,
      "guess": false,
      "host": "dust.inr.ac.ru",
      "reply_ms": 0.411,
      "pmtu": null,
      "asymmetric_difference": null,
      "reached": false
    },
    {
      "ttl": 2,
      "guess": false,
      "host": "dust.inr.ac.ru",
      "reply_ms": 0.39,
      "pmtu": 1480,
      "asymmetric_difference": 1,
      "reached": false
    },
    {
      "ttl": 2,
      "guess": false,
      "host": "3ffe:2400:0:109::2",
      "reply_ms": 463.514,
      "pmtu": null,
      "asymmetric_difference": null,
      "reached": true
    }
  ]
}

traceroute command parser

Linux, macOS, and FreeBSD support for the traceroute command:

$ traceroute -m 3 8.8.8.8 | jc --traceroute -p          # or:  jc -p traceroute -m 3 8.8.8.8
{
  "destination_ip": "8.8.8.8",
  "destination_name": "8.8.8.8",
  "hops": [
    {
      "hop": 1,
      "probes": [
        {
          "annotation": null,
          "asn": null,
          "ip": "192.168.1.254",
          "name": "dsldevice.local.net",
          "rtt": 6.616
        },
        {
          "annotation": null,
          "asn": null,
          "ip": "192.168.1.254",
          "name": "dsldevice.local.net",
          "rtt": 6.413
        },
        {
          "annotation": null,
          "asn": null,
          "ip": "192.168.1.254",
          "name": "dsldevice.local.net",
          "rtt": 6.308
        }
      ]
    },
    {
      "hop": 2,
      "probes": [
        {
          "annotation": null,
          "asn": null,
          "ip": "76.220.24.1",
          "name": "76-220-24-1.lightspeed.sntcca.sbcglobal.net",
          "rtt": 29.367
        },
        {
          "annotation": null,
          "asn": null,
          "ip": "76.220.24.1",
          "name": "76-220-24-1.lightspeed.sntcca.sbcglobal.net",
          "rtt": 40.197
        },
        {
          "annotation": null,
          "asn": null,
          "ip": "76.220.24.1",
          "name": "76-220-24-1.lightspeed.sntcca.sbcglobal.net",
          "rtt": 29.162
        }
      ]
    },
    {
      "hop": 3,
      "probes": [
        {
          "annotation": null,
          "asn": null,
          "ip": null,
          "name": null,
          "rtt": null
        }
      ]
    }
  ]
}

Updated Parsers

There have been many parser updates since v1.11.0. The INI file parser has been enhanced to support files and output that contains simple key/value pairs. The route command parser has been enhanced to add support for IPv6 routing tables. The uname parser provides more intuitive debug messages and an issue in the iptables command parser was fixed, allowing it to convert the last row of a table. Many other parser enhancements including the consistent handling of blank input, FreeBSD support, and minor field additions and fixes are included.

Key/Value Pair Files with the INI File Parser

The INI file parser has been enhanced to now support files containing simple key/value pairs. Files can include comments prepended with # or ; and keys and values can be delimited by = or : with or without spaces. Quotation marks are stripped from quoted values, though they can be kept with the -r (raw output) jc argument.

These types of files can be found in many places, including configuration files in /etc. (e.g. /etc/sysconfig/network-scripts).

$ cat keyvalue.txt
# this file contains key/value pairs
name = John Doe
address=555 California Drive
age: 34
; comments can include # or ;
# delimiter can be = or :
# quoted values have quotation marks stripped by default
# but can be preserved with the -r argument
occupation:"Engineer"

$ cat keyvalue.txt | jc --ini -p
{
  "name": "John Doe",
  "address": "555 California Drive",
  "age": "34",
  "occupation": "Engineer"
}

route Command Parser

The route command parser has been enhanced to support IPv6 tables.

$ route -6 | jc --route -p          # or: jc -p route -6
[
  {
    "destination": "[::]/96",
    "next_hop": "[::]",
    "flags": "!n",
    "metric": 1024,
    "ref": 0,
    "use": 0,
    "iface": "lo",
    "flags_pretty": [
      "REJECT"
    ]
  },
  {
    "destination": "0.0.0.0/96",
    "next_hop": "[::]",
    "flags": "!n",
    "metric": 1024,
    "ref": 0,
    "use": 0,
    "iface": "lo",
    "flags_pretty": [
      "REJECT"
    ]
  },
  {
    "destination": "2002:a00::/24",
    "next_hop": "[::]",
    "flags": "!n",
    "metric": 1024,
    "ref": 0,
    "use": 0,
    "iface": "lo",
    "flags_pretty": [
      "REJECT"
    ]
  },
  ...
]

Schema Changes

There are no schema changes in this release.

Full Parser List

  • airport -I
  • airport -s
  • arp
  • blkid
  • crontab
  • crontab-u
  • CSV
  • df
  • dig
  • dmidecode
  • du
  • env
  • file
  • free
  • fstab
  • /etc/group
  • /etc/gshadow
  • history
  • /etc/hosts
  • id
  • ifconfig
  • INI
  • iptables
  • jobs
  • last and lastb
  • ls
  • lsblk
  • lsmod
  • lsof
  • mount
  • netstat
  • ntpq
  • /etc/passwd
  • ping
  • pip list
  • pip show
  • ps
  • route
  • /etc/shadow
  • ss
  • stat
  • sysctl
  • systemctl
  • systemctl list-jobs
  • systemctl list-sockets
  • systemctl list-unit-files
  • timedatectl
  • tracepath
  • traceroute
  • uname -a
  • uptime
  • w
  • who
  • XML
  • YAML

For more information on the motivations for creating jc, see my blog post.

Happy parsing!

More Comprehensive Tracebacks in Python

Python has great exception-handling with nice traceback messages that can help debug issues with your code. Here’s an example of a typical traceback message:

Traceback (most recent call last):
  File "/Users/kbrazil/Library/Python/3.7/bin/jc", line 11, in <module>
    load_entry_point('jc', 'console_scripts', 'jc')()
  File "/Users/kbrazil/git/jc/jc/cli.py", line 396, in main
    result = parser.parse(data, raw=raw, quiet=quiet)
  File "/Users/kbrazil/git/jc/jc/parsers/uname.py", line 108, in parse
    raw_output['kernel_release'] = parsed_line.pop(0)
IndexError: pop from empty list

I usually read these from the bottom-up to zero-in on the issue. Here I can see that my program is trying to pop the last item off a list called parsed_line, but the list is empty and Python doesn’t know what to do, so it quits with an IndexError exception.

The traceback conveniently includes the line number and snippet of the offending code. This is usually correct, but the line numbering can be off depending on the type of error or exception. This might be enough information for me to dig into the code and figure out why parsed_line is empty. But what about a more complex example?

Traceback (most recent call last):
  File "/Users/kbrazil/Library/Python/3.7/bin/jc", line 11, in <module>
    load_entry_point('jc', 'console_scripts', 'jc')()
  File "/Users/kbrazil/git/jc/jc/cli.py", line 396, in main
    result = parser.parse(data, raw=raw, quiet=quiet)
  File "/Users/kbrazil/git/jc/jc/parsers/arp.py", line 226, in parse
    'hwtype': line[4].lstrip('[').rstrip(']'),
IndexError: list index out of range

In this traceback I can see that the program is trying to pull the fifth item from the line list but Python can’t grab it – probably because the list doesn’t have that many items. This traceback doesn’t show me the state of the variables, so I can’t tell what input the function took or what the line variable looks like when causing this issue.

What I’d really like is to see more context (the code lines before and after the error) along with the variable state when the error occurred. Many times this is done with a debugger or with print() statements. But there is another way!

cgitb (deprecated)

Back in 1995 when CGI scripts were all the rage, Python added the cgi library along with its helper module, cgitb. This module would print more verbose traceback messages to the browser to help with troubleshooting. Conveniently, its traceback messages would include surrounding code context and variable state! cgitb is poorly named since it can drop-in replace standard tracebacks on any type of program. The name might be why it never really gained traction. Unfortunately, cgitb is set to be deprecated in Python 3.10, but let’s see how it works and then check out how to replace it:

IndexError
Python 3.7.6: /usr/local/opt/python/bin/python3.7
Mon Jul  6 12:09:08 2020

A problem occurred in a Python script.  Here is the sequence of
function calls leading up to the error, in the order they occurred.

 /Users/kbrazil/Library/Python/3.7/bin/jc in <module>()
    2 # EASY-INSTALL-ENTRY-SCRIPT: 'jc','console_scripts','jc'
    3 __requires__ = 'jc'
    4 import re
    5 import sys
    6 from pkg_resources import load_entry_point
    7 
    8 if __name__ == '__main__':
    9     sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
   10     sys.exit(
   11         load_entry_point('jc', 'console_scripts', 'jc')()
   12     )
load_entry_point = <function load_entry_point>

 /Users/kbrazil/git/jc/jc/cli.py in main()
  391 
  392         if parser_name in parsers:
  393             # load parser module just in time so we don't need to load all modules
  394             parser = parser_module(arg)
  395             try:
  396                 result = parser.parse(data, raw=raw, quiet=quiet)
  397                 found = True
  398                 break
  399 
  400             except Exception:
  401                 if debug:
result undefined
parser = <module 'jc.parsers.arp' from '/Users/kbrazil/git/jc/jc/parsers/arp.py'>
parser.parse = <function parse>
data = "#!/usr/bin/env python3\n\nimport jc.parsers.ls\nimp...print(tabulate.tabulate(parsed, headers='keys'))\n"
raw = False
quiet = False

 /Users/kbrazil/git/jc/jc/parsers/arp.py in parse(data="#!/usr/bin/env python3\n\nimport jc.parsers.ls\nimp...print(tabulate.tabulate(parsed, headers='keys'))\n", raw=False, quiet=False)
  221             for line in cleandata:
  222                 line = line.split()
  223                 output_line = {
  224                     'name': line[0],
  225                     'address': line[1].lstrip('(').rstrip(')'),
  226                     'hwtype': line[4].lstrip('[').rstrip(']'),
  227                     'hwaddress': line[3],
  228                     'iface': line[6],
  229                 }
  230                 raw_output.append(output_line)
  231 
line = ['#!/usr/bin/env', 'python3']
].lstrip undefined
IndexError: list index out of range
    __cause__ = None
    __class__ = <class 'IndexError'>
    __context__ = None
    __delattr__ = <method-wrapper '__delattr__' of IndexError object>
    __dict__ = {}
    __dir__ = <built-in method __dir__ of IndexError object>
    __doc__ = 'Sequence index out of range.'
    __eq__ = <method-wrapper '__eq__' of IndexError object>
    __format__ = <built-in method __format__ of IndexError object>
    __ge__ = <method-wrapper '__ge__' of IndexError object>
    __getattribute__ = <method-wrapper '__getattribute__' of IndexError object>
    __gt__ = <method-wrapper '__gt__' of IndexError object>
    __hash__ = <method-wrapper '__hash__' of IndexError object>
    __init__ = <method-wrapper '__init__' of IndexError object>
    __init_subclass__ = <built-in method __init_subclass__ of type object>
    __le__ = <method-wrapper '__le__' of IndexError object>
    __lt__ = <method-wrapper '__lt__' of IndexError object>
    __ne__ = <method-wrapper '__ne__' of IndexError object>
    __new__ = <built-in method __new__ of type object>
    __reduce__ = <built-in method __reduce__ of IndexError object>
    __reduce_ex__ = <built-in method __reduce_ex__ of IndexError object>
    __repr__ = <method-wrapper '__repr__' of IndexError object>
    __setattr__ = <method-wrapper '__setattr__' of IndexError object>
    __setstate__ = <built-in method __setstate__ of IndexError object>
    __sizeof__ = <built-in method __sizeof__ of IndexError object>
    __str__ = <method-wrapper '__str__' of IndexError object>
    __subclasshook__ = <built-in method __subclasshook__ of type object>
    __suppress_context__ = False
    __traceback__ = <traceback object>
    args = ('list index out of range',)
    with_traceback = <built-in method with_traceback of IndexError object>

The above is a description of an error in a Python program.  Here is
the original traceback:

Traceback (most recent call last):
  File "/Users/kbrazil/Library/Python/3.7/bin/jc", line 11, in <module>
    load_entry_point('jc', 'console_scripts', 'jc')()
  File "/Users/kbrazil/git/jc/jc/cli.py", line 396, in main
    result = parser.parse(data, raw=raw, quiet=quiet)
  File "/Users/kbrazil/git/jc/jc/parsers/arp.py", line 226, in parse
    'hwtype': line[4].lstrip('[').rstrip(']'),
IndexError: list index out of range

This verbose traceback gives me just what I’m looking for! Though the default is 5, I told cgitb to print out 11 lines of context. Now I can see the two variables I’m particularly interested in to troubleshoot this issue: data and line.

data = "#!/usr/bin/env python3\n\nimport jc.parsers.ls\nimp...print(tabulate.tabulate(parsed, headers='keys'))\n"

(Notice how it snips the value if it’s too long. Pretty cool!)

line = ['#!/usr/bin/env', 'python3']

Now I can easily see that the data that was input into the function does not look like the type of data expected at all. (it is expecting text output from the arp command and instead it was fed in another Python script file) I can also see that the line list only has two items.

I included cgitb in jc to provide a verbose debug command option (-dd) to help speed up troubleshooting of parsing issues – typically during development of a new parser or to quickly identify an issue a user is having over email. It seemed perfect for my needs and aside from the weird name it worked well.

Then I noticed that cgitb was to be deprecated along with the cgi module with no replacement.

tracebackplus

I decided to vendorize the builtin cgitb library so it wouldn’t be orphaned in later versions of Python. After looking at the code I found it would be pretty easy to simplify the module by taking out all of the HTML rendering cruft. And why not rename it to something more descriptive while we’re at it? After not too much thought, I settled on tracebackplus.

Like cgitb, tracebackplus doesn’t require any external libraries and can easily replace standard tracebacks with the following code:

import tracebackplus
tracebackplus.enable(context=11)

Here is the code for tracebackplus along with the permissive MIT license. Feel free to use this code in your projects.

Here’s an example of how it is being used in jc to provide different levels of debugging using the -d (standard traceback) or -dd (tracebackplus) command line arguments:

try:
    result = parser.parse(data, raw=raw, quiet=quiet)
    found = True
    break

except Exception:
    if debug:
        if verbose_debug:
            import jc.tracebackplus
            jc.tracebackplus.enable(context=11)

        raise

    else:
        import jc.utils
        jc.utils.error_message(
            f'{parser_name} parser could not parse the input data. Did you use the correct parser?\n'
            '                 For details use the -d or -dd option.')
        sys.exit(1)

Happy debugging!

JC Version 1.11.1 Released

Try the jc web demo!

I’m happy to announce the release of jc version 1.11.1 available on github and pypi.

jc now supports over 50 commands and file-types and now can be installed via Homebrew (macOS) and zypper (OpenSUSE). In addition, jc can now be installed via DEB and RPM packages or run as a single binary on linux or macOS. You can set your own custom colors for jc to display and more command parsers are supported on macOS. See below for more information on the new features.

To upgrade, run:

$ pip3 install --upgrade jc

RPM/DEB packages and Binaries can also be found here.

OS package repositories (e.g. brew, zypper, etc.) will be updated with the latest version of jc on their own future release schedules.

New Features

  • jc now supports custom colors. You can customize the colors by setting the JC_COLORS environment variable.
  • jc is now available on macOS via Homebrew (brew install jc)
  • jc is now available on OpenSUSE via zypper
  • DEB, RPM, and Binary packages are now available for linux and macOS
  • Several back-end updates to support packaging on standard linux distribution package repositories in the future (e.g. Fedora)

New Parsers

jc now supports 51 parsers. The dmidecode command is now supported for linux platforms.

Documentation and schemas for all parsers can be found here.

dmidecode command parser

Linux support for the dmidecode command:

# jc -p dmidecode
[
  {
    "handle": "0x0000",
    "type": 0,
    "bytes": 24,
    "description": "BIOS Information",
    "values": {
      "vendor": "Phoenix Technologies LTD",
      "version": "6.00",
      "release_date": "04/13/2018",
      "address": "0xEA490",
      "runtime_size": "88944 bytes",
      "rom_size": "64 kB",
      "characteristics": [
        "ISA is supported",
        "PCI is supported",
        "PC Card (PCMCIA) is supported",
        "PNP is supported",
        "APM is supported",
        "BIOS is upgradeable",
        "BIOS shadowing is allowed",
        "ESCD support is available",
        "Boot from CD is supported",
        "Selectable boot is supported",
        "EDD is supported",
        "Print screen service is supported (int 5h)",
        "8042 keyboard services are supported (int 9h)",
        "Serial services are supported (int 14h)",
        "Printer services are supported (int 17h)",
        "CGA/mono video services are supported (int 10h)",
        "ACPI is supported",
        "Smart battery is supported",
        "BIOS boot specification is supported",
        "Function key-initiated network boot is supported",
        "Targeted content distribution is supported"
      ],
      "bios_revision": "4.6",
      "firmware_revision": "0.0"
    }
  },
  ...
]

Updated Parsers

The netstat command is now supported on macOS:

$ jc -p netstat
[
  {
    "proto": "tcp4",
    "recv_q": 0,
    "send_q": 0,
    "local_address": "mylaptop.local",
    "foreign_address": "173.199.15.254",
    "state": "SYN_SENT   ",
    "kind": "network",
    "local_port": "57561",
    "foreign_port": "https",
    "transport_protocol": "tcp",
    "network_protocol": "ipv4",
    "local_port_num": 57561
  },
  {
    "proto": "tcp4",
    "recv_q": 0,
    "send_q": 0,
    "local_address": "mylaptop.local",
    "foreign_address": "192.0.71.3",
    "state": "ESTABLISHED",
    "kind": "network",
    "local_port": "57525",
    "foreign_port": "https",
    "transport_protocol": "tcp",
    "network_protocol": "ipv4",
    "local_port_num": 57525
  },
  ...
]

The netstat parser has been enhanced to support the -r (routes) and -i (interfaces) options on both linux and macOS.

$ jc -p netstat -r
[
  {
    "destination": "default",
    "gateway": "router.local",
    "route_flags": "UGSc",
    "route_refs": 102,
    "use": 24,
    "iface": "en0",
    "kind": "route"
  },
  {
    "destination": "127",
    "gateway": "localhost",
    "route_flags": "UCS",
    "route_refs": 0,
    "use": 0,
    "iface": "lo0",
    "kind": "route"
  },
  ...
]
$ jc -p netstat -i
[
  {
    "iface": "lo0",
    "mtu": 16384,
    "network": "<Link#1>",
    "address": null,
    "ipkts": 1777797,
    "ierrs": 0,
    "opkts": 1777797,
    "oerrs": 0,
    "coll": 0,
    "kind": "interface"
  },
  {
    "iface": "lo0",
    "mtu": 16384,
    "network": "127",
    "address": "localhost",
    "ipkts": 1777797,
    "ierrs": null,
    "opkts": 1777797,
    "oerrs": null,
    "coll": null,
    "kind": "interface"
  },
  {
    "iface": "lo0",
    "mtu": 16384,
    "network": "localhost",
    "address": "::1",
    "ipkts": 1777797,
    "ierrs": null,
    "opkts": 1777797,
    "oerrs": null,
    "coll": null,
    "kind": "interface"
  },
  ...
]

The stat command is now supported on macOS.

$ jc -p stat jc*
[
  {
    "file": "jc-1.11.1-linux.sha256",
    "device": "16778221",
    "inode": 82163627,
    "flags": "-rw-r--r--",
    "links": 1,
    "user": "joeuser",
    "group": "staff",
    "rdev": 0,
    "size": 69,
    "access_time": "May 26 08:27:44 2020",
    "modify_time": "May 24 18:47:25 2020",
    "change_time": "May 24 18:51:21 2020",
    "birth_time": "May 24 18:47:25 2020",
    "block_size": 4096,
    "blocks": 8,
    "osx_flags": "0"
  },
  {
    "file": "jc-1.11.1-linux.tar.gz",
    "device": "16778221",
    "inode": 82163628,
    "flags": "-rw-r--r--",
    "links": 1,
    "user": "joeuser",
    "group": "staff",
    "rdev": 0,
    "size": 20226936,
    "access_time": "May 26 08:27:44 2020",
    "modify_time": "May 24 18:47:25 2020",
    "change_time": "May 24 18:47:25 2020",
    "birth_time": "May 24 18:47:25 2020",
    "block_size": 4096,
    "blocks": 39512,
    "osx_flags": "0"
  },
  ...
]

Schema Changes

There are no schema changes in this release.

Full Parser List

  • airport -I
  • airport -s
  • arp
  • blkid
  • crontab
  • crontab-u
  • CSV
  • df
  • dig
  • dmidecode
  • du
  • env
  • file
  • free
  • fstab
  • /etc/group
  • /etc/gshadow
  • history
  • /etc/hosts
  • id
  • ifconfig
  • INI
  • iptables
  • jobs
  • last and lastb
  • ls
  • lsblk
  • lsmod
  • lsof
  • mount
  • netstat
  • ntpq
  • /etc/passwd
  • pip list
  • pip show
  • ps
  • route
  • /etc/shadow
  • ss
  • stat
  • systemctl
  • systemctl list-jobs
  • systemctl list-sockets
  • systemctl list-unit-files
  • timedatectl
  • uname -a
  • uptime
  • w
  • who
  • XML
  • YAML

For more information on the motivations for creating jc, see my blog post.

Happy parsing!

JC Version 1.10.2 Released

Try the jc web demo!

I’m happy to announce the release of jc version 1.10.2 available on github and pypi. See below for more information on the new features.

To upgrade, run:

$ pip3 install --upgrade jc

New Features

jc now supports color output by default when printing to the terminal. Color is automatically disabled when piping to another program. The -m (monochrome) option can be used to disable color output to the terminal.

New Parsers

No new parsers in this release.

Updated Parsers

  • file command parser: minor fix for some edge cases
  • arp command parser: fix macOS detection for some edge cases
  • dig command parser: add axfr support

Schema Changes

The dig command parser now supports the axfr option. The schema has been updated to add this section:

$ jc -p dig @81.4.108.41 axfr zonetransfer.me
[
  {
    "axfr": [
      {
        "name": "zonetransfer.me.",
        "ttl": 7200,
        "class": "IN",
        "type": "SOA",
        "data": "nsztm1.digi.ninja. robin.digi.ninja. 2019100801 172800 900 1209600 3600"
      },
      {
        "name": "zonetransfer.me.",
        "ttl": 300,
        "class": "IN",
        "type": "HINFO",
        "data": "\"Casio fx-700G\" \"Windows XP\""
      },
      {
        "name": "zonetransfer.me.",
        "ttl": 301,
        "class": "IN",
        "type": "TXT",
        "data": "\"google-site-verification=tyP28J7JAUHA9fw2sHXMgcCC0I6XBmmoVi04VlMewxA\""
      },
      ...
    ],
    "query_time": 805,
    "server": "81.4.108.41#53(81.4.108.41)",
    "when": "Thu Apr 09 08:05:31 PDT 2020",
    "size": "50 records (messages 1, bytes 1994)"
  }
]

Full Parser List

  • airport -I
  • airport -s
  • arp
  • blkid
  • crontab
  • crontab-u
  • CSV
  • df
  • dig
  • du
  • env
  • file
  • free
  • fstab
  • /etc/group
  • /etc/gshadow
  • history
  • /etc/hosts
  • id
  • ifconfig
  • INI
  • iptables
  • jobs
  • last and lastb
  • ls
  • lsblk
  • lsmod
  • lsof
  • mount
  • netstat
  • ntpq
  • /etc/passwd
  • pip list
  • pip show
  • ps
  • route
  • /etc/shadow
  • ss
  • stat
  • systemctl
  • systemctl list-jobs
  • systemctl list-sockets
  • systemctl list-unit-files
  • timedatectl
  • uname -a
  • uptime
  • w
  • who
  • XML
  • YAML

For more information on the motivations for creating jc, see my blog post.

Happy parsing!

Jello: The JQ Alternative for Pythonistas

Built on jello:

Jello Explorer (jellex): TUI interactive JSON filter using Python syntax

jello web demo

I’m a big fan of using structured data at the command line. So much so that I’ve written a couple of utilities to promote JSON in the CLI:

Typically I use jq to filter and process the JSON output into submission until I get what I want. But if you’re anything like me, you spend a lot of time googling how to do what you want in jq because the syntax can get a little out of hand. In fact, I keep notes with example jq queries I’ve used before in case I need those techniques again.

jq is great for simple things, but sometimes when I want to iterate through a deeply nested structure with arrays of objects I find python’s list and dictionary syntax easier to comprehend.

Hello jello

That’s why I created jello. jello works similarly to jq but uses the python interpreter, so you can iterate with loops, comprehensions, variables, expressions, etc. just like you would in a full-fledged python script.

The nice thing about jello is that it removes a lot of the boilerplate code you would need to ingest and output the JSON or JSON Lines data so you can focus on the logic.

Let’s take the following output from jc -ap:

$ jc -ap
{
  "name": "jc",
  "version": "1.9.2",
  "description": "jc cli output JSON conversion tool",
  "author": "Kelly Brazil",
  "author_email": "kellyjonbrazil@gmail.com",
  "parser_count": 50,
  "parsers": [
    {
      "name": "airport",
      "argument": "--airport",
      "version": "1.0",
      "description": "airport -I command parser",
      "author": "Kelly Brazil",
      "author_email": "kellyjonbrazil@gmail.com",
      "compatible": [
        "darwin"
      ],
      "magic_commands": [
        "airport -I"
      ]
    },
    {
      "name": "airport_s",
      "argument": "--airport-s",
      "version": "1.0",
      "description": "airport -s command parser",
      "author": "Kelly Brazil",
      "author_email": "kellyjonbrazil@gmail.com",
      "compatible": [
        "darwin"
      ],
      "magic_commands": [
        "airport -s"
      ]
    },
    ...
]

Let’s say I want a list of the parser names that are compatible with macOS. Here is a jq query that will get down to that level:

$ jc -a | jq '[.parsers[] | select(.compatible[] | contains("darwin")) | .name]' 
[
  "airport",
  "airport_s",
  "arp",
  "crontab",
  "crontab_u",
  "csv",
  ...
]

This is not too terribly bad, but you need to be careful about bracket and parenthesis placements. Here’s the same query in jello:

$ jc -a | jello '[parser.name for parser in _.parsers if "darwin" in parser.compatible]'
[
  "airport",
  "airport_s",
  "arp",
  "crontab",
  "crontab_u",
  "csv",
  ...
]

As you can see, jello gives you the JSON or JSON Lines input as a dictionary or list of dictionaries assigned to ‘_‘. Then you process it as you’d like using standard python syntax, with the convenience of dot notation. jello automatically takes care of slurping input and printing valid JSON or JSON Lines depending on the value of the last expression.

The example above is not quite as terse as using jq, but it’s more readable to someone who is familiar with python list comprehension. As with any programming language, there are multiple ways to skin a cat. We can also do a similar query with a for loop:

$ jc -a | jello '\
result = []
for parser in _.parsers:
  if "darwin" in parser.compatible:
    result.append(parser.name)
result'
[
  "airport",
  "airport_s",
  "arp",
  "crontab",
  "crontab_u",
  "csv",
  ...
]

Advanced JSON Processing

These are very simple examples and jq syntax might be ok here (though I prefer python syntax). But what if we try to do something more complex? Let’s take one of the advanced examples from the excellent jq tutorial by Matthew Lincoln.

Under Grouping and Counting, Matthew describes an advanced jq filter against a sample Twitter dataset that includes JSON Lines data. There he describes the following query:

“We can now create a table of users. Let’s create a table with columns for the user id, user name, followers count, and a column of their tweet ids separated by a semicolon.”

https://programminghistorian.org/en/lessons/json-and-jq

Here is the final jq query:

$ cat twitterdata.jlines | jq -s 'group_by(.user) | 
                                 .[] | 
                                 {
                                   user_id: .[0].user.id, 
                                   user_name: .[0].user.screen_name, 
                                   user_followers: .[0].user.followers_count, 
                                   tweet_ids: [.[].id | tostring] | join(";")
                                 }'
...
{
  "user_id": 47073035,
  "user_name": "msoltanm",
  "user_followers": 63,
  "tweet_ids": "619172275741298700"
}
{
  "user_id": 2569107372,
  "user_name": "SlavinOleg",
  "user_followers": 35,
  "tweet_ids": "501064198973960200;501064202794971140;501064214467731460;501064215759568900;501064220121632800"
}
{
  "user_id": 2369225023,
  "user_name": "SkogCarla",
  "user_followers": 10816,
  "tweet_ids": "501064217667960800"
}
{
  "user_id": 2477475030,
  "user_name": "bennharr",
  "user_followers": 151,
  "tweet_ids": "501064201503113200"
}
{
  "user_id": 42226593,
  "user_name": "shirleycolleen",
  "user_followers": 2114,
  "tweet_ids": "619172281294655500;619172179960328200"
}
...

This is a fantastic query! It’s actually deceptively simple looking – it takes quite a few paragraphs for Matthew to describe how it works and there are some tricky brackets, braces, and parentheses in there that need to be set just right. Let’s see how we could tackle this task with jello using standard python syntax:

$ cat twitterdata.jlines | jello -l '\
user_ids = set()
for tweet in _:
    user_ids.add(tweet.user.id)
result = []
for user in user_ids:
    user_profile = {}
    tweet_ids = []
    for tweet in _:
        if tweet.user.id == user:
            user_profile.update({
                "user_id": user,
                "user_name": tweet.user.screen_name,
                "user_followers": tweet.user.followers_count})
            tweet_ids.append(str(tweet.id))
    user_profile["tweet_ids"] = ";".join(tweet_ids)
    result.append(user_profile)
result'
...
{"user_id": 2696111005, "user_name": "EGEVER142", "user_followers": 1433, "tweet_ids": "619172303654518784"}
{"user_id": 42226593, "user_name": "shirleycolleen", "user_followers": 2114, "tweet_ids": "619172281294655488;619172179960328192"}
{"user_id": 106948003, "user_name": "MrKneeGrow", "user_followers": 172, "tweet_ids": "501064228627705857"}
{"user_id": 18270633, "user_name": "ahhthatswhy", "user_followers": 559, "tweet_ids": "501064204661850113"}
{"user_id": 14331818, "user_name": "edsu", "user_followers": 4220, "tweet_ids": "615973042443956225;618602288781860864"}
{"user_id": 2569107372, "user_name": "SlavinOleg", "user_followers": 35, "tweet_ids": "501064198973960192;501064202794971136;501064214467731457;501064215759568897;501064220121632768"}
{"user_id": 22668719, "user_name": "nodehyena", "user_followers": 294, "tweet_ids": "501064222772445187"}
...

So there’s 17 lines of python… again not as terse as jq, but for pythonistas this is probably a lot easier to understand what is going on. This is a pretty simple and naive implementation – there are probably much better approaches that are shorter, simpler, faster, etc. but the point is I can come back six months from now and understand what is going on if I need to debug or tweak it.

Just for fun, let’s pipe this result through jtbl to see what it looks like:

   user_id  user_name          user_followers  tweet_ids
----------  ---------------  ----------------  ----------------------------------------------------------------------------------------------
...
2481812382  SadieODoyle                    42  501064200035516416
2696111005  EGEVER142                    1433  619172303654518784
  42226593  shirleycolleen               2114  619172281294655488;619172179960328192
 106948003  MrKneeGrow                    172  501064228627705857
  18270633  ahhthatswhy                   559  501064204661850113
  14331818  edsu                         4220  615973042443956225;618602288781860864
2569107372  SlavinOleg                     35  501064198973960192;501064202794971136;501064214467731457;501064215759568897;501064220121632768
  22668719  nodehyena                     294  501064222772445187
  23598003  victoriasview                1163  501064228288364546
 851336634  20mUsa                      15643  50106414
...

Very cool! Find more examples at https://github.com/kellyjonbrazil/jello. I hope you find jello useful in your command line pipelines.

Try Jello Explorer and the jello web demo!

JC Version 1.9.0 Released

Try the jc web demo!

I’m happy to announce the release of jc version 1.9.0 available on github and pypi. See below for more information on the new features and parsers.

To upgrade, run:

$ pip3 install --upgrade jc

jc In The News!

The Linux Unplugged podcast gave a shoutout to jc on their February 18, 2020 episode for their App Pick segment. The discussion starts at 45:47. Go check out the podcast!

New Parsers

jc now includes 50 parsers! New parsers (tested on linux and OSX) include airport -I, airport -s, file, ntpq -p, and timedatectl commands.

Documentation and schemas for all parsers can be found here.

airport -I command parser

OSX support for the airport -I command:

$ airport -I | jc --airport -p          # or:  jc -p airport -I
{
  "agrctlrssi": -66,
  "agrextrssi": 0,
  "agrctlnoise": -90,
  "agrextnoise": 0,
  "state": "running",
  "op_mode": "station",
  "lasttxrate": 195,
  "maxrate": 867,
  "lastassocstatus": 0,
  "802_11_auth": "open",
  "link_auth": "wpa2-psk",
  "bssid": "3c:37:86:15:ad:f9",
  "ssid": "SnazzleDazzle",
  "mcs": 0,
  "channel": "48,80"
}

airport -s command parser

OSX support for the airport -s command.

$ airport -s | jc --airport-s -p          or: jc -p airport -s
[
  {
    "ssid": "DIRECT-4A-HP OfficeJet 3830",
    "bssid": "00:67:eb:2a:a7:3b",
    "rssi": -90,
    "channel": "6",
    "ht": true,
    "cc": "--",
    "security": [
      "WPA2(PSK/AES/AES)"
    ]
  },
  {
    "ssid": "Latitude38",
    "bssid": "c0:ff:d5:d2:7a:f3",
    "rssi": -85,
    "channel": "11",
    "ht": true,
    "cc": "US",
    "security": [
      "WPA2(PSK/AES/AES)"
    ]
  },
  {
    "ssid": "xfinitywifi",
    "bssid": "6e:e3:0e:b8:45:99",
    "rssi": -83,
    "channel": "11",
    "ht": true,
    "cc": "US",
    "security": [
      "NONE"
    ]
  },
  ...
]

file command parser

Linux and OSX support for the file command:

$ file * | jc --file -p          or:  jc -p file *
[
  {
    "filename": "Applications",
    "type": "directory"
  },
  {
    "filename": "another file with spaces",
    "type": "empty"
  },
  {
    "filename": "argstest.py",
    "type": "Python script text executable, ASCII text"
  },
  {
    "filename": "blkid-p.out",
    "type": "ASCII text"
  },
  {
    "filename": "blkid-pi.out",
    "type": "ASCII text, with very long lines"
  },
  {
    "filename": "cd_catalog.xml",
    "type": "XML 1.0 document text, ASCII text, with CRLF line terminators"
  },
  {
    "filename": "centosserial.sh",
    "type": "Bourne-Again shell script text executable, UTF-8 Unicode text"
  },
  ...
]

ntpq command parser

Linux support for the ntpq -p command.

$ ntpq -p | jc --ntpq -p          # or:  jc -p ntpq -p
[
  {
    "remote": "44.190.6.254",
    "refid": "127.67.113.92",
    "st": 2,
    "t": "u",
    "when": 1,
    "poll": 64,
    "reach": 1,
    "delay": 23.399,
    "offset": -2.805,
    "jitter": 2.131,
    "state": null
  },
  {
    "remote": "mirror1.sjc02.s",
    "refid": "216.218.254.202",
    "st": 2,
    "t": "u",
    "when": 2,
    "poll": 64,
    "reach": 1,
    "delay": 29.325,
    "offset": 1.044,
    "jitter": 4.069,
    "state": null
  }
]

timedatectl command parser

Linux support for the timedatectl command:

$ timedatectl | jc --timedatectl -p          # or:  jc -p timedatectl
{
  "local_time": "Tue 2020-03-10 17:53:21 PDT",
  "universal_time": "Wed 2020-03-11 00:53:21 UTC",
  "rtc_time": "Wed 2020-03-11 00:53:21",
  "time_zone": "America/Los_Angeles (PDT, -0700)",
  "ntp_enabled": true,
  "ntp_synchronized": true,
  "rtc_in_local_tz": false,
  "dst_active": true
}

Updated Parsers

No updated parsers in this release.

Schema Changes

There are no schema changes in this release.

Full Parser List

  • airport -I
  • airport -s
  • arp
  • blkid
  • crontab
  • crontab-u
  • CSV
  • df
  • dig
  • du
  • env
  • file
  • free
  • fstab
  • /etc/group
  • /etc/gshadow
  • history
  • /etc/hosts
  • id
  • ifconfig
  • INI
  • iptables
  • jobs
  • last and lastb
  • ls
  • lsblk
  • lsmod
  • lsof
  • mount
  • netstat
  • ntpq
  • /etc/passwd
  • pip list
  • pip show
  • ps
  • route
  • /etc/shadow
  • ss
  • stat
  • systemctl
  • systemctl list-jobs
  • systemctl list-sockets
  • systemctl list-unit-files
  • timedatectl
  • uname -a
  • uptime
  • w
  • who
  • XML
  • YAML

For more information on the motivations for creating jc, see my blog post.

Happy parsing!

JSON Tables in the Terminal

The other day I was looking around for a simple command-line tool to print JSON and JSON Lines data to a table in the terminal. I found a few programs that can do it with some massaging of the data, like visidata, jt, and json-table, but these really didn’t meet my requirements.

I wanted to pipe JSON or JSON Lines data into a program and get a nicely formatted table with correct headers without any additional configuration or arguments. I also wanted it to automatically fit the terminal width and wrap or truncate the columns to fit the data with no complicated configuration. Basically, I just wanted it to “do the right thing” so I can view JSON data in a tabular format without any fuss.

I ended up creating a little command-line utility called jtbl that does exactly that:

$ cat cities.json | jtbl 
  LatD    LatM    LatS  NS      LonD    LonM    LonS  EW    City               State
------  ------  ------  ----  ------  ------  ------  ----  -----------------  -------
    41       5      59  N         80      39       0  W     Youngstown         OH
    42      52      48  N         97      23      23  W     Yankton            SD
    46      35      59  N        120      30      36  W     Yakima             WA
    42      16      12  N         71      48       0  W     Worcester          MA
    43      37      48  N         89      46      11  W     Wisconsin Dells    WI
    36       5      59  N         80      15       0  W     Winston-Salem      NC
    49      52      48  N         97       9       0  W     Winnipeg           MB

jtbl is simple and elegant. It just takes in piped JSON or JSON Lines data and prints a table. There’s only one option to turn on column truncation vs. wrapping columns if the terminal width is too narrow to display the complete table.

$ jtbl -h
jtbl:   Converts JSON and JSON Lines to a table

Usage:  <JSON Data> | jtbl [OPTIONS]

        -t  truncate data instead of wrapping if too long for the terminal width
        -v  version info
        -h  help

Here’s an example using a relatively slim terminal width of 75:

$ jc dig www.cnn.com | jq '.[].answer' | jtbl 
╒═════════════════╤═════════╤════════╤═══════╤═════════════════╕
│ name            │ class   │ type   │   ttl │ data            │
╞═════════════════╪═════════╪════════╪═══════╪═════════════════╡
│ www.cnn.com.    │ IN      │ CNAME  │   201 │ turner-tls.map. │
│                 │         │        │       │ fastly.net.     │
├─────────────────┼─────────┼────────┼───────┼─────────────────┤
│ turner-tls.map. │ IN      │ A      │    22 │ 151.101.189.67  │
│ fastly.net.     │         │        │       │                 │
╘═════════════════╧═════════╧════════╧═══════╧═════════════════╛

or with truncation enabled:

$ jc dig www.cnn.com | jq '.[].answer' | jtbl -t 
name                  class    type      ttl  data
--------------------  -------  ------  -----  --------------------
www.cnn.com.          IN       CNAME     219  turner-tls.map.fastl
turner-tls.map.fastl  IN       A          10  151.101.189.67

Here’s an example using it to print the result of an XML API query response, converted to JSON with jc, and filtered with jq:

$ curl -X GET --basic -u "testuser:testpassword" https://reststop.randomhouse.com/resources/works/19306 | jc --xml | jq '.work' | jtbl
╒═════════════╤══════════╤══════════╤════════════╤══════════════╤════════════╤═════════════╤══════════════╤══════════════╤════════════╕
│ authorweb   │ titles   │   workid │ @uri       │ onsaledate   │ series     │ titleAuth   │ titleSubti   │ titleshort   │ titleweb   │
│             │          │          │            │              │            │             │ tleAuth      │              │            │
╞═════════════╪══════════╪══════════╪════════════╪══════════════╪════════════╪═════════════╪══════════════╪══════════════╪════════════╡
│ BROWN, DAN  │          │    19306 │ https://re │ 2003-09-02   │ Robert Lan │ Angels & D  │ Angels & D   │ ANGELS & D   │ Angels & D │
│             │          │          │ ststop.ran │ T00:00:00-   │ gdon       │ emons : Da  │ emons :  :   │ EMON(LPTP)   │ emons      │
│             │          │          │ domhouse.c │ 04:00        │            │ n Brown     │  Dan Brown   │ (REI)(MTI)   │            │
│             │          │          │ om/resourc │              │            │             │              │              │            │
│             │          │          │ es/works/1 │              │            │             │              │              │            │
│             │          │          │ 9306       │              │            │             │              │              │            │
╘═════════════╧══════════╧══════════╧════════════╧══════════════╧════════════╧═════════════╧══════════════╧══════════════╧════════════╛

Again, with truncation enabled:

$ curl -X GET --basic -u "testuser:testpassword" https://reststop.randomhouse.com/resources/works/19306 | jc --xml | jq '.work' | jtbl -t
authorweb    titles      workid  @uri        onsaledate    series      titleAuth    titleSubti    titleshort    titleweb
-----------  --------  --------  ----------  ------------  ----------  -----------  ------------  ------------  ----------
BROWN, DAN                19306  https://re  2003-09-02    ROBERT LAN  Angels & D   Angels & D    ANGELS & D    Angels & D

I found that having the ability to quickly see the JSON data in a tabular, horizontal format can sometimes help me visualize ‘where I am’ in the data more easily than looking at long vertical lists of JSON.

I hope you enjoy it!

JC Version 1.8.0 Released

Try the jc web demo!

I’m excited to announce the release of jc version 1.8.0 available on github and pypi. See below for more information on the new features and parsers.

To upgrade, run:

$ pip3 install --upgrade jc

New Parsers

jc now includes 45 parsers! New parsers (tested on linux and OSX) include blkid, last, lastb, who, /etc/passwd files, /etc/shadow files, /etc/group files, /etc/gshadow files, and CSV files.

Documentation and schemas for all parsers can be found here.

blkid command parser

Linux support for the blkid command:

$ blkid | jc --blkid -p          # or:  jc -p blkid
[
  {
    "device": "/dev/sda1",
    "uuid": "05d927ab-5875-49e4-ada1-7f46cb32c932",
    "type": "xfs"
  },
  {
    "device": "/dev/sda2",
    "uuid": "3klkIj-w1kk-DkJi-0XBJ-y3i7-i2Ac-vHqWBM",
    "type": "LVM2_member"
  },
  {
    "device": "/dev/mapper/centos-root",
    "uuid": "07d718ff-950c-4e5b-98f0-42a1147c77d9",
    "type": "xfs"
  },
  {
    "device": "/dev/mapper/centos-swap",
    "uuid": "615eb89a-bcbf-46fd-80e3-c483ff5c931f",
    "type": "swap"
  }
]

$ sudo blkid -o udev -ip /dev/sda2 | jc --blkid -p          # or:  sudo jc -p blkid -o udev -ip /dev/sda2
[
  {
    "id_fs_uuid": "3klkIj-w1kk-DkJi-0XBJ-y3i7-i2Ac-vHqWBM",
    "id_fs_uuid_enc": "3klkIj-w1kk-DkJi-0XBJ-y3i7-i2Ac-vHqWBM",
    "id_fs_version": "LVM2\x20001",
    "id_fs_type": "LVM2_member",
    "id_fs_usage": "raid",
    "id_iolimit_minimum_io_size": 512,
    "id_iolimit_physical_sector_size": 512,
    "id_iolimit_logical_sector_size": 512,
    "id_part_entry_scheme": "dos",
    "id_part_entry_type": "0x8e",
    "id_part_entry_number": 2,
    "id_part_entry_offset": 2099200,
    "id_part_entry_size": 39843840,
    "id_part_entry_disk": "8:0"
  }
]

last and lastb command parsers

Linux and OSX support for the last command. Linux support for the lastb command.

$ last | jc --last -p          # or:  jc -p last
[
  {
    "user": "joeuser",
    "tty": "ttys002",
    "hostname": null,
    "login": "Thu Feb 27 14:31",
    "logout": "still logged in"
  },
  {
    "user": "joeuser",
    "tty": "ttys003",
    "hostname": null,
    "login": "Thu Feb 27 10:38",
    "logout": "10:38",
    "duration": "00:00"
  },
  {
    "user": "joeuser",
    "tty": "ttys003",
    "hostname": null,
    "login": "Thu Feb 27 10:18",
    "logout": "10:18",
    "duration": "00:00"
  },
  ...
]

$ sudo lastb | jc --last -p          # or:  sudo jc -p lastb
[
  {
    "user": "joeuser",
    "tty": "ssh:notty",
    "hostname": "127.0.0.1",
    "login": "Tue Mar 3 00:48",
    "logout": "00:48",
    "duration": "00:00"
  },
  {
    "user": "joeuser",
    "tty": "ssh:notty",
    "hostname": "127.0.0.1",
    "login": "Tue Mar 3 00:48",
    "logout": "00:48",
    "duration": "00:00"
  },
  {
    "user": "jouser",
    "tty": "ssh:notty",
    "hostname": "127.0.0.1",
    "login": "Tue Mar 3 00:48",
    "logout": "00:48",
    "duration": "00:00"
  }
]

who command parser

Linux and OSX support for the who command:

$ who | jc --who -p          # or:  jc -p who
[
  {
    "user": "joeuser",
    "tty": "ttyS0",
    "time": "2020-03-02 02:52"
  },
  {
    "user": "joeuser",
    "tty": "pts/0",
    "time": "2020-03-02 05:15",
    "from": "192.168.71.1"
  }
]

$ who -a | jc --who -p          # or:  jc -p who -a
[
  {
    "event": "reboot",
    "time": "Feb 7 23:31",
    "pid": 1
  },
  {
    "user": "joeuser",
    "writeable_tty": "-",
    "tty": "console",
    "time": "Feb 7 23:32",
    "idle": "old",
    "pid": 105
  },
  {
    "user": "joeuser",
    "writeable_tty": "+",
    "tty": "ttys000",
    "time": "Feb 13 16:44",
    "idle": ".",
    "pid": 51217,
    "comment": "term=0 exit=0"
  },
  {
    "user": "joeuser",
    "writeable_tty": "?",
    "tty": "ttys003",
    "time": "Feb 28 08:59",
    "idle": "01:36",
    "pid": 41402
  },
  {
    "user": "joeuser",
    "writeable_tty": "+",
    "tty": "ttys004",
    "time": "Mar 1 16:35",
    "idle": ".",
    "pid": 15679,
    "from": "192.168.1.5"
  }
]

CSV File Parser

Convert generic CSV files to JSON. The parser will attempt to automatically detect the delimiter character. If it cannot detect the delimiter it will use the comma (‘,‘) as the delimiter. The file must contain a header row as the first line:

$ cat homes.csv 
"Sell", "List", "Living", "Rooms", "Beds", "Baths", "Age", "Acres", "Taxes"
142, 160, 28, 10, 5, 3,  60, 0.28,  3167
175, 180, 18,  8, 4, 1,  12, 0.43,  4033
129, 132, 13,  6, 3, 1,  41, 0.33,  1471
...

$ cat homes.csv | jc --csv -p
[
  {
    "Sell": "142",
    "List": "160",
    "Living": "28",
    "Rooms": "10",
    "Beds": "5",
    "Baths": "3",
    "Age": "60",
    "Acres": "0.28",
    "Taxes": "3167"
  },
  {
    "Sell": "175",
    "List": "180",
    "Living": "18",
    "Rooms": "8",
    "Beds": "4",
    "Baths": "1",
    "Age": "12",
    "Acres": "0.43",
    "Taxes": "4033"
  },
  {
    "Sell": "129",
    "List": "132",
    "Living": "13",
    "Rooms": "6",
    "Beds": "3",
    "Baths": "1",
    "Age": "41",
    "Acres": "0.33",
    "Taxes": "1471"
  },
  ...
]

/etc/passwd, /etc/shadow, /etc/group, and /etc/gshadow file parsers

Convert /etc/passwd, /etc/shadow, /etc/group, and /etc/gshadow files to JSON format:

$ cat /etc/passwd | jc --passwd -p
[
  {
    "username": "nobody",
    "password": "*",
    "uid": -2,
    "gid": -2,
    "comment": "Unprivileged User",
    "home": "/var/empty",
    "shell": "/usr/bin/false"
  },
  {
    "username": "root",
    "password": "*",
    "uid": 0,
    "gid": 0,
    "comment": "System Administrator",
    "home": "/var/root",
    "shell": "/bin/sh"
  },
  {
    "username": "daemon",
    "password": "*",
    "uid": 1,
    "gid": 1,
    "comment": "System Services",
    "home": "/var/root",
    "shell": "/usr/bin/false"
  },
  ...
]

$ sudo cat /etc/shadow | jc --shadow -p
[
  {
    "username": "root",
    "password": "*",
    "last_changed": 18113,
    "minimum": 0,
    "maximum": 99999,
    "warn": 7,
    "inactive": null,
    "expire": null
  },
  {
    "username": "daemon",
    "password": "*",
    "last_changed": 18113,
    "minimum": 0,
    "maximum": 99999,
    "warn": 7,
    "inactive": null,
    "expire": null
  },
  {
    "username": "bin",
    "password": "*",
    "last_changed": 18113,
    "minimum": 0,
    "maximum": 99999,
    "warn": 7,
    "inactive": null,
    "expire": null
  },
  ...
]

$ cat /etc/group | jc --group -p
[
  {
    "group_name": "nobody",
    "password": "*",
    "gid": -2,
    "members": []
  },
  {
    "group_name": "nogroup",
    "password": "*",
    "gid": -1,
    "members": []
  },
  {
    "group_name": "wheel",
    "password": "*",
    "gid": 0,
    "members": [
      "root"
    ]
  },
  {
    "group_name": "certusers",
    "password": "*",
    "gid": 29,
    "members": [
      "root",
      "_jabber",
      "_postfix",
      "_cyrus",
      "_calendar",
      "_dovecot"
    ]
  },
  ...
]

$ cat /etc/gshadow | jc --gshadow -p
[
  {
    "group_name": "root",
    "password": "*",
    "administrators": [],
    "members": []
  },
  {
    "group_name": "adm",
    "password": "*",
    "administrators": [],
    "members": [
      "syslog",
      "joeuser"
    ]
  },
  ...
]

Updated Parsers

  • The ls parser now supports filenames that contain newline characters when using ls -l or ls -b. A warning message will be sent to stderr if newlines are detected and ls -l or ls -b are not used:
$ ls | jc --ls

jc:  Warning - Newline characters detected. Filenames probably corrupted. Use ls -l or -b instead.

[{"filename": "this file has"}, {"filename": "a newline inside"}, {"filename": "this file has"}, {"filename": "four contiguous newlines inside"}, ...]
  • The ls parser now supports multiple directory listings, globbing, and recursive listings.
$ ls -R | jc --ls
[{"filename": "centos-7.7"}, {"filename": "create_fixtures.sh"}, {"filename": "generic"}, {"filename": "osx-10.11.6"}, {"filename": "osx-10.14.6"}, ...]

Alternative “Magic” Syntax

jc now accepts a simplified syntax for most command parsers. Instead of piping the data into jc you can now also prepend “jc” to the command you would like to convert. Note that command aliases are not supported:

$ jc dig www.example.com
[{"id": 31113, "opcode": "QUERY", "status": "NOERROR", "flags": ["qr", "rd", "ra"], "query_num": 1, "answer_num": 1, "authority_num": 0, "additional_num": 1, "question": {"name": "www.example.com.", "class": "IN", "type": "A"}, "answer": [{"name": "www.example.com.", "class": "IN", "type": "A", "ttl": 35366, "data": "93.184.216.34"}], "query_time": 37, "server": "2600", "when": "Mon Mar 02 16:13:31 PST 2020", "rcvd": 60}]

You can also insert jc options before the command:

$ jc -pqd dig www.example.com
[
  {
    "id": 7495,
    "opcode": "QUERY",
    "status": "NOERROR",
    "flags": [
      "qr",
      "rd",
      "ra"
    ],
    "query_num": 1,
    "answer_num": 1,
    "authority_num": 0,
    "additional_num": 1,
    "question": {
      "name": "www.example.com.",
      "class": "IN",
      "type": "A"
    },
    "answer": [
      {
        "name": "www.example.com.",
        "class": "IN",
        "type": "A",
        "ttl": 36160,
        "data": "93.184.216.34"
      }
    ],
    "query_time": 40,
    "server": "2600",
    "when": "Mon Mar 02 16:15:21 PST 2020",
    "rcvd": 60
  }
]

Schema Changes

There are no schema changes in this release.

Full Parser List

  • arp
  • blkid
  • crontab
  • crontab-u
  • CSV
  • df
  • dig
  • du
  • env
  • free
  • fstab
  • /etc/group
  • /etc/gshadow
  • history
  • /etc/hosts
  • id
  • ifconfig
  • INI
  • iptables
  • jobs
  • last and lastb
  • ls
  • lsblk
  • lsmod
  • lsof
  • mount
  • netstat
  • /etc/passwd
  • pip list
  • pip show
  • ps
  • route
  • /etc/shadow
  • ss
  • stat
  • systemctl
  • systemctl list-jobs
  • systemctl list-sockets
  • systemctl list-unit-files
  • uname -a
  • uptime
  • w
  • who
  • XML
  • YAML

For more information on the motivations for creating jc, see my blog post.

Happy parsing!

Applying Orchestration and Choreography to Cybersecurity Automation

Imagine a world where most of your security stack seamlessly integrates with each other, has access to the latest threat intelligence from internal and external sources, and automatically mitigates the most severe incidents. Suspicious files found in emails get sent to the closest sandbox for detonation, where the hash and other IOCs are sent to endpoints, NGFWs, proxies, etc. to inoculate the organization, and then send all of the relevant information to the SOC as an incident ticket.

Many organizations can at least do the above with a Security Orchestration Automation and Response (SOAR) platform implementation. Several vendors offer this type of Orchestration platform, including Splunk (Phantom), Palo Alto Networks (Demisto), Fortinet (Cybersponse), and IBM (Resilient). These platforms have become mainstream within the past few years and with more and more cybersecurity professionals learning the python programming language it has become easier to implement and customize them. In fact, no programming experience is needed at all for many use cases since playbooks can be created and maintained with a graphical builder.

Cybersponse Graphical Playbook Editor

I’m a big fan of using Orchestration to automate workflows with playbooks – in fact I’ve written integrations for Phantom and Demisto, and FortiSOAR. But there is another automation paradigm that doesn’t get talked about as much in the cybersecurity realm: Choreography.

Orchestration

So we already have an idea of what Orchestration is: it’s a central repository of vendor integrations and associated actions that can be connected together in clever and novel ways to create playbooks. Playbooks are like scripts that run based on incoming events, schedules, or can even be run manually to automate repetitive tasks. This automation removes the human-error factor and can reduce the workload of the Security team.

Centralized Automation with Orchestration

The key piece about Orchestration is that it is centralized. There is typically a central server that has all of the vendor integration information and playbooks. Alarms, logs, alerts, etc. get sent to this server so it can act as the conductor and tell each security device in the stack what to do and when to do it.

This approach has pros and cons:

Pros:

  • Very flexible – you can make a playbook do almost anything you can think of
  • Can version control the playbooks in a central repository like git
  • Large libraries of vendor apps
  • Typically have a good user communities

Cons:

  • Can be brittle if APIs change, unsupported vendors are introduced, or if there are connectivity issues to the central Orchestrator
  • Vendor lock-in to a SOAR platform / not open source
  • Can require python programming experience to onboard an unsupported security service or to create a complex playbook

Let’s compare this to Choreography – the other, lesser-known automation paradigm available to us.

Choreography

Choreography? Where did that come from? Well, the concepts of Orchestration and Choreography come from the world of Service Oriented Architecture (SOA). SOA had some good ideas, but it didn’t really take off until it recently morphed and rebranded as Microservice Architecture. (Yes, this is an over-simplification for the scope of this post)

We almost take microservice architectures for granted now. Cloud application delivery and containerization of services are not as bleeding-edge as they were just a couple of years ago. We intuitively understand that microservices act independently yet are connected to other microservices to make up an application. The way these microservices are connected can be described as Orchestration or Choreography.

Now we are just extending the metaphor and considering each piece of our security stack as a ‘microservice’. For example, your NGFW, sandbox, email security gateway, NAC, Intel feed, etc. are all cybersecurity microservices that need to be configured to talk to one another to enable your cybersecurity ‘application’.

Distributed Automation with Choreography

In the case of Choreography, each of these security ‘microservices’ (or security appliances) knows what they are supposed to do by subscribing to one or more channels on a message bus. This bus allows the service to receive alerts and IOC information in near-real-time and then publish their results on one or more channels on that same bus. It’s almost like layer 7 multicast for you router geeks out there.

In this paradigm, there is no need for a central repository of rules or playbooks for many standard use-cases because the ‘fabric’ gets smarter as more and more different types of security services join. Unlike an orchestra, which follows the lead of the conductor, each service works independently based on its own configuration. Each service knows its own dance moves and works harmoniously in relation to the other services.

The Message Bus

How does this work in the real world?

There are a couple examples of the Choreography approach being used in the Cybersecurity realm. A proprietary implementation by Fortinet (disclaimer: I am a Fortinet employee) is called the “Security Fabric”.

Fortinet Security Fabric

The Fortinet Security Fabric

Fortinet’s Security Fabric is a proprietary implementation that behaves like a message bus to learn about new Fortinet and Fabric Ready ecosystem partner appliances and services as soon as they connect to the fabric.  These services are configured to connect to the Security Fabric and take appropriate action when a security incident is identified.

For example, after installing a FortiSandbox appliance and adding it to the Security Fabric, other Fortinet or “Fabric-Ready” partner appliances, such as the NGFW and Secure Email Gateway can send suspicious files they detect to the Security Fabric where the sandbox service is listening. The FortiSandbox, in turn, can publish the IOC results of the scans it performs to the Security Fabric so other Fortinet or Fabric-Ready partner appliances (e.g. NGFW, FortiGuard, FortiEDR) can ingest them and take appropriate action.

This is very powerful. As more services are connected to the Security Fabric, it gets smarter, more capable, and scales – automatically.

OpenDXL

Another open-source, multi-vendor example of a message bus being used for cybersecurity choreography is OpenDXL. OpenDXL was originally developed by McAfee, as a security-specific message bus, but it was open-sourced under the Organization for the Advancement of Structured Information Standards (OASIS) Open Cybersecurity Alliance (OCA) project. (Disclaimer: Fortinet is a sponsor of OCA) This project brings together the message bus concept to integrate multiple cybersecurity services using well-known formats like STIX2 to influence its ontology.

OpenDXL Architecture

Some of the pros and cons of the Choreography approach:

Pros:

  • The ‘fabric’ automatically gets smarter and more capable as more security services are connected
  • No need for dozens of boilerplate playbooks
  • Open-source and proprietary options available
  • No reliance on a central conductor – less brittle to Orchestrator outages or misconfigurations.
  • Integrations “just work” together if they are part of the ecosystem

Cons:

  • Less granular control over automation workflows
  • Open-source options are still maturing
  • Typically, no central repository for service configurations

Which Way is the Best?

We know that automation will improve our security operations, but which approach is best? Since Orchestration and Choreography both have their own pros and cons that don’t overlap too much it probably makes sense to use both.

Choreography can reduce the amount of boilerplate playbooks you need to bootstrap your automation initiative, while Orchestration can be used to automate higher-level business or incident response workflows.

By applying the application architecture concepts of SOA and microservices to cybersecurity we can take security automation to the next level.