Linux.Network.NM.Connections

NetworkManager is a popular high-level interface for configuring networks in Linux systems, in particular Ubuntu and other Debian-based flavours. This artifact lists the NetworkManager state, all configured connections and their settings, as well as when the connections were last activated. A list of BSSIDs per connection is also retrieved.

All the information is retrieved from NetworkManager configuration files and other state files. Connection information is stored in the /etc/NetworkManager/system-connections as long as the “keyfile” plugin is selected in /etc/NetworkManager/NetworkManager.conf (this is the default). Note that by default, NetworkManager doesn’t manage connections defined in /etc/network/interfaces.

Whether the connections are currently active is not stored in file and must be queried using using nmcli or through dbus. This artifact runs nmcli as an external program to retrieve this information. Information such as IP addresses, routes, DNS servers, available Wi-Fi networks and other settings will also be collected through nmcli.

This artifact also exports two functions, parse_ini(filename) and parse_ini_as_dict(filename), which may be useful to parse INI files in other artifacts.


name: Linux.Network.NM.Connections
author: 'Andreas Misje - @misje'
description: |
  NetworkManager is a popular high-level interface for configuring
  networks in Linux systems, in particular Ubuntu and other Debian-based
  flavours. This artifact lists the NetworkManager state, all configured
  connections and their settings, as well as when the connections were
  last activated. A list of BSSIDs per connection is also retrieved.

  All the information is retrieved from NetworkManager configuration
  files and other state files. Connection information is stored in
  the /etc/NetworkManager/system-connections as long as the "keyfile"
  plugin is selected in /etc/NetworkManager/NetworkManager.conf (this
  is the default). Note that by default, NetworkManager doesn't manage
  connections defined in /etc/network/interfaces.

  Whether the connections are currently active is not stored in file
  and must be queried using using nmcli or through dbus. This artifact
  runs nmcli as an external program to retrieve this information.
  Information such as IP addresses, routes, DNS servers, available Wi-Fi
  networks and other settings will also be collected through nmcli.

  This artifact also exports two functions, parse_ini(filename) and
  parse_ini_as_dict(filename), which may be useful to parse INI files
  in other artifacts.

reference:
  - https://developer-old.gnome.org/NetworkManager/stable/nm-settings-keyfile.html
  - https://developer-old.gnome.org/NetworkManager/stable/settings-connection.html
  - https://developer-old.gnome.org/NetworkManager/stable/settings-802-11-wireless.html

type: CLIENT

required_permissions:
    - EXECVE

parameters:
  - name: RedactSecrets
    default: true
    type: bool
    description: |
        Replace Wi-FI PSKs (wifi-security/psk) with "\<REDACTED\>".

export: |
    /* Parse an INI config file and return Section (the part enclosed in '[]' on
       lines of their own), Key and Value. */
    LET parse_ini(filename) = SELECT * FROM foreach(row={
        SELECT * FROM parse_records_with_regex(file=filename,
            regex='''(?m)\[\s*(?P<Section>[^\]]+)\s*\](?P<Contents>[^\[]*)''')
        }, query={
            SELECT Section, Key, Value
                FROM parse_records_with_regex(file=Contents,
                    accessor='data',
                    regex='^[\s\n]*(?P<Key>[^=]+)=(?P<Value>.*)')
        })

    /* Parse an INI config file and return a single column, Contents, with
       the contents. Section names are prepended to keys, separated by '/'. */
    LET parse_ini_as_dict(filename) = SELECT to_dict(item={
            SELECT lowcase(string=Section + '/' + Key) AS _key, Value AS _value
            FROM parse_ini(filename=filename)
        }) AS Contents
        FROM scope()

column_types:
  - name: LastActivated
    type: timestamp
    description: |
        When the connection was last fully successfully activated. This
        timestamp may be updated periodically while the connection is active.

precondition: |
    SELECT OS FROM info() WHERE OS = 'linux'

sources:
  - name: State
    description: |
        NetworkManager have three states that may be toggled:
        NetworkingEnabled, which disables all networking (managed by
        NetworkManager); WirelessEnabled, which disables wireless networking;
        and WWANEnabled, which disables mobile data connections.
    query: |
        LET State_ = SELECT parse_string_with_regex(string=Data,
        regex=('''NetworkingEnabled=(?P<NetworkingEnabled>\S+)''',
            '''WirelessEnabled=(?P<WirelessEnabled>\S+)''',
            '''WWANEnabled=(?P<WWANEnabled>\S+)''')) AS Fields
        FROM read_file(filenames='/var/lib/NetworkManager/NetworkManager.state')

        LET State = SELECT Fields.NetworkingEnabled AS NetworkingEnabled,
            Fields.WirelessEnabled AS WirelessEnabled,
            Fields.WWANEnabled AS WWANEnabled
            FROM State_

        SELECT * FROM State

  - name: ConnectionConfigs
    description: |
        All connections configured in NetworkManager. Columns returned are
        OSPath and a dict with the connection configuration.
    query: |
        LET ConfiguredConnections <= SELECT * FROM foreach(row={
            SELECT OSPath FROM glob(globs='/etc/NetworkManager/system-connections/*.nmconnection')
            }, query=if(condition=RedactSecrets, then={
                SELECT OSPath,
                    Contents + dict(`wifi-security/psk`='<REDACTED>')
                        AS Contents
                    FROM parse_ini_as_dict(filename=OSPath)
            }, else={
                SELECT *, OSPath FROM parse_ini_as_dict(filename=OSPath)
            })
        )

        SELECT OSPath, Contents FROM ConfiguredConnections

  - name: Connections
    description: |
        Return a handful of useful properties from ConnectionConfigs in a
        more readable table with individual column names: Name, UUID, Type,
        Device and LastActivated. LastActivated are fetched from another
        state file and combined with the results from ConnectionConfigs.
    query: |
        LET Timestamps = SELECT UUID, if(condition=parse_float(string=Timestamp),
            then=timestamp(epoch=Timestamp), else=null) AS Timestamp
            FROM parse_records_with_regex(file='/var/lib/NetworkManager/timestamps',
                regex='''(?P<UUID>[-A-Fa-f0-9]+)+=(?P<Timestamp>\S+)''')

        LET Connections <= SELECT Name, _UUID AS UUID, Type, Device, LastActivated
            FROM foreach(row={
                SELECT * FROM ConfiguredConnections
                }, query={
                    SELECT Contents.`connection/id` AS Name,
                        Contents.`connection/uuid` AS _UUID,
                        Contents.`connection/type` AS Type,
                        Contents.`connection/interface-name` AS Device,
                        Timestamp AS LastActivated
                    FROM Timestamps
                    WHERE _UUID=UUID
            })

        SELECT * FROM Connections

  - name: ActiveConnections
    description: |
        Return connections from Connections that are currently active,
        by asking the NetworkManager daemon through the utility "nmcli".
    query: |
        LET nmcli = SELECT Stdout
            FROM execve(argv=['nmcli', '-t', '-f', 'uuid', 'connection', 'show', '--active'])

        LET ActiveConnections = SELECT * FROM foreach(row={
            SELECT * FROM parse_lines(accessor='data',
                filename=nmcli.Stdout)
            }, query={
                SELECT * FROM Connections WHERE UUID=Line
            })

        SELECT * FROM ActiveConnections

  - name: DeviceStatus
    description: |
        Ask NetworkManager through "nmcli" about the status of all network
        interfaces, managed as well unmanaged, with detailed information such
        as IP addresses, routes, MTU and DNS settings.
    query: |
        LET nmcli = SELECT Stdout
            FROM execve(argv=['nmcli', '-t', 'device', 'show'])

        LET DeviceStatus = SELECT * FROM foreach(row=split(sep='\n\n',
            string=nmcli.Stdout), query={
                SELECT parse_string_with_regex(string=_value,
                    regex='''GENERAL.DEVICE:(?P<Device>.+)''').Device AS Device,
                    to_dict(item={
                        SELECT Key AS _key, Value AS _value
                            FROM parse_records_with_regex(file=_value, accessor='data',
                                regex='^\n?(?P<Key>[^:]+):(?P<Value>.*)')
                    }) AS Status
                    FROM scope()
            })

        /* We're pretty much done now, but the output could be a lot nicer to
           work with. */

        LET S = scope()
        LET Status <= SELECT * FROM foreach(row={SELECT * FROM DeviceStatus},
            column='Status')

        LET to_array(col, dev) = filter(list=array(a={
            SELECT * FROM column_filter(include=col, query={
                SELECT * FROM Status WHERE `GENERAL.DEVICE` = dev
            })}), condition='x=>x')

        LET prettify_route(col, dev) = SELECT *
            FROM foreach(row=to_array(dev=dev, col=col), query={
                SELECT S.Dest AS Dest, S.NextHop AS NextHop, int(int=S.Metric) AS Metric
                FROM foreach(row={
                    SELECT parse_string_with_regex(string=S._value, regex=(
                        '''dst\s*=\s*(?P<Dest>[^,]+)''',
                        '''nh\s*=\s*(?P<NextHop>[^,]+)''',
                        '''mt\s*=\s*(?P<Metric>\d+)''')) AS R
                        FROM scope()
                    }, column='R')
                })
                WHERE Dest

        SELECT `GENERAL.DEVICE` AS Device,
            S.`GENERAL.TYPE` AS Type,
            S.`GENERAL.CONNECTION` AS Connection,
            S.`GENERAL.STATE` AS State,
            S.`GENERAL.HWADDR` AS Mac,
            S.`GENERAL.MTU` AS MTU,
            to_array(dev=`GENERAL.DEVICE`, col='IP4.ADDRESS') AS Addresses,
            prettify_route(dev=`GENERAL.DEVICE`, col='IP4.ROUTE') AS Routes,
            S.`IP4.GATEWAY` AS Gateway,
            to_array(dev=`GENERAL.DEVICE`, col='IP4.DNS') AS DNSServers,
            to_array(dev=`GENERAL.DEVICE`, col='IP4.DOMAIN') AS DNSDomains,
            to_array(dev=`GENERAL.DEVICE`, col='IP4.SEARCHES') AS DNSSearches,
            to_array(dev=`GENERAL.DEVICE`, col='IP6.ADDRESS') AS _IPv6Addresses,
            prettify_route(dev=`GENERAL.DEVICE`, col='IP6.ROUTE') AS _IPv6Routes,
            S.`IP6.GATEWAY` AS _IPv6Gateway,
            to_array(dev=`GENERAL.DEVICE`, col='IP6.DNS') AS _IPv6DNSServers,
            to_array(dev=`GENERAL.DEVICE`, col='IP6.DOMAIN') AS _IPv6DNSDomains,
            to_array(dev=`GENERAL.DEVICE`, col='IP6.SEARCHES') AS _IPv6DNSSearches
            FROM Status

  - name: AvailableAccessPoints
    description: |
        Ask NetworkManager through "nmcli" about details about all available
        Wi-Fi access points
    query: |
        LET nmcli = SELECT Stdout
            FROM execve(argv=['nmcli', '-t', '-m', 'multiline', '-f',
                'ssid,bssid,mode,chan,freq,rate,signal,security,wpa-flags,rsn-flags,device,active,in-use',
                'device', 'wifi', 'list'])

        LET AccessPoints = SELECT * FROM foreach(row=filter(list=split(sep='\x01\x02',
                /* Separate sections of key–values by injecting a blob and then
                   split on that string. nmcli also has a "tabular" output mode,
                   but ":", the separator, is also part of the output and is
                   "escaped" by "/", making the parsing difficult. */
                string=regex_replace(source=nmcli.Stdout, re='(?m)^IN-USE:.*$',
                    replace='$0\x01\x02')), regex='.'), query={

                SELECT to_dict(item={
                    SELECT Key AS _key, Value as _value FROM parse_records_with_regex(
                        file=_value, accessor='data', regex='^\n?(?P<Key>[^:]+):(?P<Value>.*)')
                }) AS Contents FROM scope() WHERE _value
            })
            WHERE Contents

        SELECT DEVICE AS Device, SSID, BSSID, MODE AS Mode, CHAN AS Chan,
            FREQ AS Freq, RATE AS Rate, SIGNAL AS Signal, SECURITY AS Security,
            ACTIVE='yes' AS Active, `WPA-FLAGS` AS WPAFlags, `RSN-FLAGS` AS RSNFlags,
            `IN-USE`='*' AS InUse
            FROM foreach(row=AccessPoints, column='Contents')

  - name: SeenBSSIDs
    description: |
        A list of BSSIDs (each BSSID formatted as a MAC address like
        "00:11:22:33:44:55") that have been detected as part of the Wi-Fi
        network. NetworkManager internally tracks previously seen BSSIDs.
    query: |
        LET SeenBSSIDs <= SELECT UUID AS _UUID, filter(list=split(sep_string=',', string=BSSIDs),
            regex='.+') AS BSSIDs
            FROM parse_records_with_regex(file='/var/lib/NetworkManager/seen-bssids',
                regex='''(?P<UUID>[-A-Fa-f0-9]+)+=(?P<BSSIDs>\S+)''')

        SELECT * FROM foreach(row=SeenBSSIDs, query={
            SELECT Name, UUID, Device, BSSIDs FROM Connections
            WHERE _UUID=UUID
        })