Skip to content

Commit

Permalink
Feat: Add spec and parser for cryptsetup luksDump (#3504)
Browse files Browse the repository at this point in the history
* Feat: Add spec and parser for cryptsetup luksDump

Signed-off-by: daniel.zatovic <[email protected]>

* Fix flake errors in cryptsetup parser test

Signed-off-by: daniel.zatovic <[email protected]>

* Rework luksDump parser and test.

Signed-off-by: daniel.zatovic <[email protected]>

* Fix documentation test and fix examples

Signed-off-by: daniel.zatovic <[email protected]>

* Add docs for cryptsetup parser and datasource

Signed-off-by: daniel.zatovic <[email protected]>

* Replace bash filtering by a custom datasource

Signed-off-by: daniel.zatovic <[email protected]>

* Add tests for the new cryptstup datasources

Signed-off-by: daniel.zatovic <[email protected]>

* Fix comment style

Signed-off-by: daniel.zatovic <[email protected]>

* Fix doc test inconsistency between Python 2 and 3

Signed-off-by: daniel.zatovic <[email protected]>

* Fix title length

Signed-off-by: daniel.zatovic <[email protected]>

* Fix LUKS capitalisation

Signed-off-by: daniel.zatovic <[email protected]>

* Remove stray print command

Signed-off-by: daniel.zatovic <[email protected]>

* Fix names formatting according to PEP8.

Signed-off-by: daniel.zatovic <[email protected]>

* Change variable names in docs

Signed-off-by: daniel.zatovic <[email protected]>

* Filter tokens manually instead of using an option

Signed-off-by: daniel.zatovic <[email protected]>

* Add component to detect cryptsetup token support

If the tokens are supported (cryptsetup package version at least 2.4.0
is installed), add --disable-external-tokens option to luksDump spec.

Signed-off-by: daniel.zatovic <[email protected]>

* Add tests for the cryptsetup component

Signed-off-by: daniel.zatovic <[email protected]>

* Enable dependent components in the config

Signed-off-by: daniel.zatovic <[email protected]>

* Extend test coverage

Signed-off-by: daniel.zatovic <[email protected]>

Signed-off-by: daniel.zatovic <[email protected]>
(cherry picked from commit 484befc)
  • Loading branch information
danzatt authored and Sachin Patil committed Sep 15, 2022
1 parent b792b52 commit fd614e7
Show file tree
Hide file tree
Showing 12 changed files with 804 additions and 1 deletion.
9 changes: 9 additions & 0 deletions docs/custom_datasources_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ insights.specs.datasources.lpstat
:show-inheritance:
:undoc-members:


insights.specs.datasources.luks_devices
---------------------------------------

.. automodule:: insights.specs.datasources.luks_devices
:members: luks_block_devices, luks_data_sources, LocalSpecs
:show-inheritance:
:undoc-members:

insights.specs.datasources.malware_detection
--------------------------------------------

Expand Down
3 changes: 3 additions & 0 deletions docs/shared_components_catalog/cryptsetup.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.. automodule:: insights.components.cryptsetup
:members:
:show-inheritance:
3 changes: 3 additions & 0 deletions docs/shared_parsers_catalog/cryptsetup_luksDump.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.. automodule:: insights.parsers.cryptsetup_luksDump
:members:
:show-inheritance:
7 changes: 7 additions & 0 deletions insights/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@
# needed because some specs aren't given names before they're used in DefaultSpecs
- name: insights.core.spec_factory
enabled: true
# needed by the 'luks_data_sources' spec
- name: insights.parsers.blkid.BlockIDInfo
enabled: true
- name: insights.components.cryptsetup
enabled: true
""".strip()


Expand Down
55 changes: 55 additions & 0 deletions insights/components/cryptsetup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
HasCryptsetupWithTokens, HasCryptsetupWithoutTokens
===================================================
The ``HasCryptsetupWithTokens``/``HasCryptsetupWithoutTokens`` component uses
``InstalledRpms`` parser to determine if cryptsetup package is installed and if
it has tokens support (since version 2.4.0), if not it raises ``SkipComponent``
so that the dependent component will not fire. Can be added as a dependency of
a parser so that the parser only fires if the ``cryptsetup`` dependency and
token support is met.
"""
from insights.core.plugins import component
from insights.parsers.installed_rpms import InstalledRpms, InstalledRpm
from insights.core.dr import SkipComponent


@component(InstalledRpms)
class HasCryptsetupWithTokens(object):
"""The ``HasCryptsetupWithTokens`` component uses ``InstalledRpms`` parser
to determine if cryptsetup package is installed and if it has tokens
support (since version 2.4.0), if not it raises ``SkipComponent``
Raises:
SkipComponent: When ``cryptsetup`` package is strictly less than 2.4.0,
or when cryptsetup package is not installed
"""
def __init__(self, rpms):
rpm = rpms.get_max("cryptsetup")

if rpm is None:
raise SkipComponent("cryptsetup package is not installed")

if rpm < InstalledRpm("cryptsetup-2.4.0-0"):
raise SkipComponent("cryptsetup package with token support is not installed")


@component(InstalledRpms)
class HasCryptsetupWithoutTokens(object):
"""The ``HasCryptsetupWithoutTokens`` component uses ``InstalledRpms``
parser to determine if cryptsetup package is installed and if it does not
have tokens support (below version 2.4.0), if not it raises
``SkipComponent``
Raises:
SkipComponent: When ``cryptsetup`` package is at least 2.4.0, or when
cryptsetup package is not installed
"""
def __init__(self, rpms):
rpm = rpms.get_max("cryptsetup")

if rpm is None:
raise SkipComponent("cryptsetup package is not installed")

if rpm >= InstalledRpm("cryptsetup-2.4.0-0"):
raise SkipComponent("cryptsetup package with token support is installed")
171 changes: 171 additions & 0 deletions insights/parsers/cryptsetup_luksDump.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"""
LuksDump - command ``cryptsetup luksDump``
==========================================
This class provides parsing for the output of cryptsetup luksDump
<device_name>. Outputs from LUKS1 and LUKS2 are supported.
"""

from insights import parser, Parser, SkipComponent
from insights.parsers import ParseException
from insights.parsr import (Literal, AnyChar, Char, Opt, String, WS,
HangingString, WithIndent, Many, EOF, Lift)
from insights.specs import Specs

import string


class DocParser(object):
def __init__(self):
value_chars = set(string.printable) - set("\n\r")

FirstLine = Literal("LUKS header information", value="header") << AnyChar.until(Char("\n")) + Opt(Many(Char("\n")))
FirstIndent = Literal(" ")
# we need to replace the \t by 8 spaces in the input,
# otherwise WithIndent does not work properly
# SecondIndent = Literal("\t")
SecondIndent = Literal(" " * 8)

Key = String(value_chars - set(":")) << Char(":") % "Key"
Value = WS >> HangingString(value_chars) % "Value"

MultilineContinuation = Many((Char("\n") + Literal(" " * 15) + SecondIndent) >> String(value_chars))
Value1 = WS >> String(value_chars) + Opt(MultilineContinuation).map(lambda x: "".join(x)) << Char("\n")
Value1 = Value1.map(lambda x: ("".join(x)).strip())

ZeroLevelKVPair = Key + Value1
FirstLevelKVPair = FirstIndent >> Key + Value1
SecondLevelKVPair = SecondIndent >> WithIndent(Key + Value)

Luks2SectionName = Key << Char("\n")
Luks2SectionEntry = (FirstLevelKVPair + Many(SecondLevelKVPair).map(dict)).map(self.convert_type)
Luks2Section = Luks2SectionName + Many(Luks2SectionEntry).map(dict) << Opt(Many(Char("\n")))
Luks2Body = Many(Luks2Section, lower=1)

Luks1Section = ZeroLevelKVPair + Many(SecondLevelKVPair).map(dict) << Opt(Many(Char("\n")))
Luks1Body = Many(Luks1Section.map(self.convert_status), lower=1)

KVBlock = Many(Key + Value1).map(dict)
LuksHeader = (FirstLine + KVBlock) << Opt(Many(Char("\n")))

# Luks2Body has to go first, because Luks1Body consumes also valid Luks2 bodies
self.Top = Lift(self.join_header_and_body) * LuksHeader * (Luks2Body | Luks1Body) << EOF

def join_header_and_body(self, header, body):
return dict([header] + body)

def convert_type(self, section):
section[1]["type"] = section[0][1]
return [section[0][0], section[1]]

def convert_status(self, section):
section[2]["status"] = section[1]
return [section[0], section[2]]

def __call__(self, content):
try:
return self.Top(content)
except Exception:
raise ParseException("There was an exception when parsing one of the outputs of cryptsetup luksDump commands.")


@parser(Specs.cryptsetup_luksDump)
class LuksDump(Parser):
"""
Sample input data is in the format::
LUKS header information
Version: 2
Epoch: 6
Metadata area: 16384 [bytes]
Keyslots area: 16744448 [bytes]
UUID: cfbcc942-e06b-4c4a-952f-e9c9b2011c27
Label: (no label)
Subsystem: (no subsystem)
Flags: (no flags)
Data segments:
0: crypt
offset: 16777216 [bytes]
length: (whole device)
cipher: aes-xts-plain64
sector: 4096 [bytes]
Keyslots:
0: luks2
Key: 512 bits
Priority: normal
Cipher: aes-xts-plain64
Cipher key: 512 bits
PBKDF: argon2id
Time cost: 7
Memory: 1048576
Threads: 4
Salt: 3d c4 1b 52 fe 1c 90 d8 2a 35 b2 62 34 e9 0a 59
e9 0e 48 57 b2 dd 45
AF stripes: 4000
AF hash: sha256
Area offset:32768 [bytes]
Area length:258048 [bytes]
Digest ID: 0
Tokens:
0: systemd-tpm2
Keyslot: 2
Digests:
0: pbkdf2
Hash: sha256
Iterations: 129774
Salt: e6 31 d5 74 e0 65 83 82 35 03 29 56 0e 80 36 5c
4d cd 4d f9 de 69 39 97
Digest: 21 aa b3 dc 9d 46 9b 0f 3a 0f 57 13 80 c6 0b bf
67 66 9e 73 ed 7d 09 2c
Examples:
>>> type(parsed_result)
<class 'insights.parsers.cryptsetup_luksDump.LuksDump'>
>>> from pprint import pprint
>>> pprint(parsed_result.dump["header"])
{'Epoch': '6',
'Flags': '(no flags)',
'Keyslots area': '16744448 [bytes]',
'Label': '(no label)',
'Metadata area': '16384 [bytes]',
'Subsystem': '(no subsystem)',
'UUID': 'cfbcc942-e06b-4c4a-952f-e9c9b2011c27',
'Version': '2'}
>>> pprint(parsed_result.dump["Keyslots"]["0"])
{'AF hash': 'sha256',
'AF stripes': '4000',
'Area length': '258048 [bytes]',
'Area offset': '32768 [bytes]',
'Cipher': 'aes-xts-plain64',
'Cipher key': '512 bits',
'Digest ID': '0',
'Key': '512 bits',
'Memory': '1048576',
'PBKDF': 'argon2id',
'Priority': 'normal',
'Salt': '3d c4 1b 52 fe 1c 90 d8 2a 35 b2 62 34 e9 0a 59 e9 0e 48 57 b2 dd 45',
'Threads': '4',
'Time cost': '7',
'type': 'luks2'}
>>> parsed_result.dump["Tokens"]["0"]["type"]
'systemd-tpm2'
Attributes:
dump(dict of dicts): A top level dict containing the dictionaries
representing the header, data segments, keyslots, digests
and tokens.
""" # noqa

def __init__(self, context):
self.parse_dump = DocParser()
super(LuksDump, self).__init__(context)

def parse_content(self, content):
if len(content) == 0 or (len(content) == 1 and "not a valid LUKS" in content[0]):
raise SkipComponent
self.dump = self.parse_dump("\n".join(content).replace("\t", " " * 8) + "\n")
1 change: 1 addition & 0 deletions insights/specs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class Specs(SpecSet):
crypto_policies_state_current = RegistryPoint()
crypto_policies_opensshserver = RegistryPoint()
crypto_policies_bind = RegistryPoint()
cryptsetup_luksDump = RegistryPoint(multi_output=True)
crt = RegistryPoint()
cups_ppd = RegistryPoint(multi_output=True)
current_clocksource = RegistryPoint()
Expand Down
125 changes: 125 additions & 0 deletions insights/specs/datasources/luks_devices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""
Custom datasource for gathering a list of encrypted LUKS block devices and their properties.
"""
from insights.components.cryptsetup import HasCryptsetupWithTokens, HasCryptsetupWithoutTokens
from insights.core.context import HostContext
from insights.core.dr import SkipComponent
from insights.core.plugins import datasource
from insights.core.spec_factory import DatasourceProvider, foreach_execute
from insights.parsers.blkid import BlockIDInfo
from insights.specs import Specs
import re


@datasource(BlockIDInfo, HostContext)
def luks_block_devices(broker):
"""
This datasource provides a list of LUKS encrypted device.
Sample data returned::
['/dev/sda', '/dev/nvme0n1p3']
Returns:
list: List of the LUKS encrypted block devices.
Raises:
SkipComponent: When there is not any LUKS encrypted block device on the
system.
"""

block_id = broker[BlockIDInfo]
if block_id:
devices = block_id.filter_by_type("crypto_LUKS")
if devices:
return sorted(map(lambda x: x["NAME"], devices))

raise SkipComponent


@datasource(luks_block_devices)
class LocalSpecs(Specs):
""" Local specs used only by LUKS_data_sources datasource. """
cryptsetup_luks_dump_token_commands = foreach_execute(luks_block_devices, "cryptsetup luksDump --disable-external-tokens %s", deps=[HasCryptsetupWithTokens])
cryptsetup_luks_dump_commands = foreach_execute(luks_block_devices, "cryptsetup luksDump %s", deps=[HasCryptsetupWithoutTokens])


def line_indentation(line):
"""
Compute line indentation level
Arguments:
line(str): The whole line
Returns:
int: the number of spaces the line is indentated by
"""
line = line.replace("\t", " " * 8)
return len(line) - len(line.lstrip())


def filter_token_lines(lines):
"""
Filter out token descriptions to keep just the Keyslot filed
Arguments:
lines(list): List of lines of the luksDump output
Returns:
list: The original lines, except the tokens section only contains only token name and associated keyslot
"""
in_tokens = False
remove_indices = []

for i, line in enumerate(lines):
if line == "Tokens:":
in_tokens = True
continue

if in_tokens and line_indentation(line) == 0:
in_tokens = False

if not in_tokens or line_indentation(line) == 2 or line.startswith("\tKeyslot:"):
continue

remove_indices.append(i)

return [i for j, i in enumerate(lines) if j not in remove_indices]


@datasource(HostContext, [LocalSpecs.cryptsetup_luks_dump_token_commands, LocalSpecs.cryptsetup_luks_dump_commands])
def luks_data_sources(broker):
"""
This datasource provides the output of 'cryptsetup luksDump' command for
every LUKS encrypted device on the system. The digest and salt fields are
filtered out as they can be potentially sensitive.
Returns:
list: List of outputs of the cryptsetup luksDump command.
Raises:
SkipComponent: When there is not any LUKS encrypted block device on the
system.
"""
datasources = []

commands = []
if LocalSpecs.cryptsetup_luks_dump_token_commands in broker:
commands.extend(broker[LocalSpecs.cryptsetup_luks_dump_token_commands])
if LocalSpecs.cryptsetup_luks_dump_commands in broker:
commands.extend(broker[LocalSpecs.cryptsetup_luks_dump_commands])

for command in commands:
lines_without_tokens = filter_token_lines(command.content)

regex = re.compile(r'[\t ]*(MK digest:|MK salt:|Salt:|Digest:)(\s*([a-z0-9][a-z0-9] ){16}\n)*(\s*([a-z0-9][a-z0-9] )+\n)?', flags=re.IGNORECASE)
filtered_content = regex.sub("", "\n".join(lines_without_tokens) + "\n")

datasources.append(
DatasourceProvider(content=filtered_content, relative_path="insights_commands/" + command.cmd.replace("/", ".").replace(" ", "_"))
)

if datasources:
return datasources

raise SkipComponent
Loading

0 comments on commit fd614e7

Please sign in to comment.