Linux.Forensics.Journal

Parses the binary journal logs. Systemd uses a binary log format to store logs. You can read these logs using journalctl command:

journalctl --file /run/log/journal/*/*.journal

This artifact uses the Velociraptor Binary parser to parse the binary format. The format is documented https://www.freedesktop.org/wiki/Software/systemd/journal-files/


name: Linux.Forensics.Journal
description: |
  Parses the binary journal logs. Systemd uses a binary log format to
  store logs. You can read these logs using journalctl command:

  `journalctl --file /run/log/journal/*/*.journal`

  This artifact uses the Velociraptor Binary parser to parse the
  binary format. The format is documented
  https://www.freedesktop.org/wiki/Software/systemd/journal-files/

parameters:
- name: JournalGlob
  type: glob
  description: A Glob expression for finding journal files.
  default: /run/log/journal/*/*.journal

- name: OnlyShowMessage
  type: bool
  description: If set we only show the message entry (similar to syslog).

- name: AlsoUpload
  type: bool
  description: If set we also upload the raw files.

export: |
    LET JournalProfile = '''[
    ["Header", "x=>x.header_size", [
      ["Signature", 0, "String", {
          "length": 8,
      }],
      ["header_size", 88, "uint64"],
      ["arena_size", 96, "uint64"],
      ["n_objects", 144, uint64],
      ["n_entries", 152, uint64],
      ["Objects", "x=>x.header_size", "Array", {
          "type": "ObjectHeader",
          "count": "x=>x.n_objects",
          "max_count": 100000
      }]
    ]],

    ["ObjectHeader", "x=>x.size", [
     ["Offset", 0, "Value", {
        "value": "x=>x.StartOf",
     }],
     ["type", 0, "Enumeration",{
         "type": "uint8",
         "choices": {
          "0": OBJECT_UNUSED,
          "1": OBJECT_DATA,
          "2": OBJECT_FIELD,
          "3": OBJECT_ENTRY,
          "4": OBJECT_DATA_HASH_TABLE,
          "5": OBJECT_FIELD_HASH_TABLE,
          "6": OBJECT_ENTRY_ARRAY,
          "7": OBJECT_TAG,
         }
     }],
     ["flags", 1, "uint8"],
     ["__real_size", 8, "uint64"],
     ["__round_size", 8, "Value", {
         "value": "x=>int(int=x.__real_size / 8) * 8",
     }],
     ["size", 0, "Value", {
         "value": "x=>if(condition=x.__real_size = x.__round_size, then=x.__round_size, else=x.__round_size + 8)",
     }],
     ["payload", 16, Union, {
         "selector": "x=>x.type",
         "choices": {
             "OBJECT_DATA": DataObject,
             "OBJECT_ENTRY": EntryObject,
         }
     }]
    ]],
    ["DataObject", 0, [
      ["payload", 48, String]
    ]],

    # This is basically a single log line -
    # it is really a list of references to data Objects
    ["EntryObject", 0, [
      ["seqnum", 0, "uint64"],
      ["realtime", 8, "uint64"],
      ["monotonic", 16, "uint64"],
      ["items", 48, Array, {
          "type": EntryItem,
          "count": 50,
          "sentinel": "x=>x.object.payload = NULL",
      }]
    ]],
    ["EntryItem", 16, [
     ["object", 0, "Pointer", {
         "type": "ObjectHeader",
     }],
    ]]
    ]
    '''

    -- We make a quick pass over the file to get all the OBJECT_ENTRY
    -- objects which are all we care about. By extracting Just the
    -- offsets of the OBJECT_ENTRY Objects in the first pass we can
    -- free memory we wont need.
    LET Offsets(File) = SELECT Offset
      FROM foreach(row=parse_binary(filename=File, profile=JournalProfile,
         struct="Header").Objects)
      WHERE type = "OBJECT_ENTRY"

    -- Now parse the ObjectEntry in each offset
    LET _ParseFile(File) = SELECT Offset,
        parse_binary(
         filename=File, profile=JournalProfile,
         struct="ObjectHeader", offset=Offset) AS Parsed
    FROM Offsets(File=File)

    -- Extract the timestamps and all the attributes
    LET ParseFile(File) = SELECT File, Offset,
       timestamp(epoch=Parsed.payload.realtime) AS Timestamp,
       Parsed.payload.items.object.payload.payload AS Data
    FROM _ParseFile(File=File)

sources:
- query: |
    SELECT * FROM foreach(row={
      SELECT OSPath FROM glob(globs=JournalGlob)
    }, query={
      SELECT *, if(condition=OnlyShowMessage,
          then=filter(list=Data, regex="^MESSAGE=")[0], else=Data) AS Data,
          if(condition=AlsoUpload, then=upload(file=File)) AS Upload
      FROM ParseFile(File=OSPath)
    })