mirror of
https://github.com/eronnen/procmon-parser
synced 2026-06-08 14:05:59 +00:00
352 lines
15 KiB
Python
352 lines
15 KiB
Python
"""
|
|
Python types that procmon logs use
|
|
"""
|
|
|
|
import binascii
|
|
import datetime
|
|
import enum
|
|
|
|
from six import string_types
|
|
|
|
from procmon_parser.consts import Column, EventClass, get_error_message, ProcessOperation, ColumnToOriginalName
|
|
|
|
__all__ = ['PMLError', 'Module', 'Process', 'Event', 'PMLStructReader']
|
|
|
|
|
|
EPOCH_AS_FILETIME = 116444736000000000 # January 1, 1970 as MS file time
|
|
HUNDREDS_OF_NANOSECONDS = 10000000
|
|
|
|
|
|
class PMLError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class Module(object):
|
|
"""Information about a loaded module in a process or in the kernel
|
|
"""
|
|
|
|
def __init__(self, base_address=0, size=0, path="", version="", company="", description="", timestamp=0):
|
|
self.base_address = base_address
|
|
self.size = size
|
|
self.path = path
|
|
self.version = version
|
|
self.company = company
|
|
self.description = description
|
|
self.timestamp = timestamp
|
|
|
|
def __eq__(self, other):
|
|
if type(other) is type(self):
|
|
return self.__dict__ == other.__dict__
|
|
return False
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __str__(self):
|
|
return "\"{}\", address={}, size={}".format(
|
|
self.path, "0x{:x}".format(self.base_address), "0x{:x}".format(self.size))
|
|
|
|
def __repr__(self):
|
|
return "Module({}, {}, \"{}\", \"{}\", \"{}\", \"{}\", {})" \
|
|
.format(self.base_address, self.size, self.path, self.version, self.company,
|
|
self.description, self.timestamp)
|
|
|
|
def __hash__(self):
|
|
return hash((self.base_address, self.size, self.path, self.timestamp))
|
|
|
|
|
|
class Process(object):
|
|
"""Information about a process in the system
|
|
"""
|
|
|
|
def __init__(self, pid=0, parent_pid=0, authentication_id=0, session=0, virtualized=0, is_process_64bit=False,
|
|
integrity="", user="", process_name="", image_path="", command_line="", company="", version="",
|
|
description="", start_time=None, end_time=None, modules=None):
|
|
self.pid = pid
|
|
self.parent_pid = parent_pid
|
|
self.authentication_id = authentication_id
|
|
self.session = session
|
|
self.virtualized = virtualized
|
|
self.is_process_64bit = bool(is_process_64bit)
|
|
self.integrity = integrity
|
|
self.user = user
|
|
self.process_name = process_name
|
|
self.image_path = image_path
|
|
self.command_line = command_line
|
|
self.company = company
|
|
self.version = version
|
|
self.description = description
|
|
self.start_time = start_time
|
|
self.end_time = end_time
|
|
self.modules = modules or []
|
|
|
|
def __eq__(self, other):
|
|
if type(other) is type(self):
|
|
return self.__dict__ == other.__dict__
|
|
return False
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __str__(self):
|
|
return "\"{}\", {}".format(self.image_path, self.pid)
|
|
|
|
def __repr__(self):
|
|
return "Process({}, {}, {}, {}, {}, \"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\", \"{}\")" \
|
|
.format(self.pid, self.parent_pid, self.authentication_id, self.session, self.virtualized,
|
|
self.is_process_64bit, self.integrity, self.user, self.process_name, self.image_path,
|
|
self.command_line, self.company, self.version, self.description)
|
|
|
|
def __hash__(self):
|
|
return hash((self.pid, self.parent_pid, self.image_path, self.command_line, self.start_time, self.end_time))
|
|
|
|
|
|
class Event(object):
|
|
def __init__(self, process=None, tid=0, event_class=None, operation=None, duration=0,
|
|
date_filetime=None, result=0, stacktrace=None, category=None, path=None, details=None):
|
|
self.process = process
|
|
self.tid = tid
|
|
self.event_class = EventClass[event_class] if isinstance(event_class, string_types) else EventClass(event_class)
|
|
self.operation = operation.name if isinstance(operation, enum.IntEnum) else operation
|
|
self.date_filetime = date_filetime
|
|
self.result = result
|
|
self.duration = duration
|
|
self.stacktrace = stacktrace
|
|
self.category = category
|
|
self.path = path
|
|
self.details = details
|
|
|
|
def __eq__(self, other):
|
|
if type(other) is type(self):
|
|
return self.__dict__ == other.__dict__
|
|
return False
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __str__(self):
|
|
return "Process Name={}, Pid={}, Operation={}, Path=\"{}\", Time={}".format(
|
|
self.process.process_name, self.process.pid, self.operation, self.path,
|
|
self._strftime_date(self.date_filetime, True, True))
|
|
|
|
def __repr__(self):
|
|
return "Event({}, {}, \"{}\", \"{}\", {}, {}, {}, \"{}\", \"{}\", {})" \
|
|
.format(self.process, self.tid, self.event_class.name, self.operation, self.duration,
|
|
self.date_filetime, self.result, self.category, self.path, self.details)
|
|
|
|
def __hash__(self):
|
|
return hash((self.process.pid, self.tid, self.operation, self.date_filetime))
|
|
|
|
def date(self, is_utc=True):
|
|
if self.date_filetime is not None:
|
|
from_timestamp = datetime.datetime.utcfromtimestamp if is_utc else datetime.datetime.fromtimestamp
|
|
return from_timestamp(
|
|
(self.date_filetime - EPOCH_AS_FILETIME) // HUNDREDS_OF_NANOSECONDS) + datetime.timedelta(
|
|
microseconds=((self.date_filetime % HUNDREDS_OF_NANOSECONDS) // 10))
|
|
else:
|
|
return None
|
|
|
|
@staticmethod
|
|
def _strftime_date(date_filetime, show_day=True, show_nanoseconds=False):
|
|
# Actually Procmon prints it in local time instead of UTC
|
|
hundred_nanoseconds = (date_filetime % HUNDREDS_OF_NANOSECONDS)
|
|
d = datetime.datetime.utcfromtimestamp((date_filetime - EPOCH_AS_FILETIME) // HUNDREDS_OF_NANOSECONDS)
|
|
|
|
if show_nanoseconds:
|
|
time_of_day = d.strftime("%I:%M:%S.{:07d} %p").lstrip('0').format(hundred_nanoseconds)
|
|
else:
|
|
time_of_day = d.strftime("%I:%M:%S %p").lstrip('0')
|
|
|
|
if not show_day:
|
|
return time_of_day
|
|
day = d.strftime("%m/%d/%Y ").lstrip('0').replace('/0', '/')
|
|
return day + time_of_day
|
|
|
|
@staticmethod
|
|
def _strftime_relative_time(delta_hundred_nanosecs):
|
|
secs = delta_hundred_nanosecs // HUNDREDS_OF_NANOSECONDS
|
|
hundred_nanosecs = delta_hundred_nanosecs % HUNDREDS_OF_NANOSECONDS
|
|
return "{:02d}:{:02d}:{:02d}.{:07d}".format(secs // 3600, (secs // 60) % 60, secs % 60, hundred_nanosecs)
|
|
|
|
@staticmethod
|
|
def _strftime_duration(duration_hundred_nanosecs):
|
|
secs = duration_hundred_nanosecs // HUNDREDS_OF_NANOSECONDS
|
|
hundred_nanosecs = duration_hundred_nanosecs % HUNDREDS_OF_NANOSECONDS
|
|
return "{}.{:07d}".format(secs, hundred_nanosecs)
|
|
|
|
@staticmethod
|
|
def _get_bool_str(b):
|
|
if isinstance(b, bool):
|
|
return str(b)
|
|
if b == 0:
|
|
return str(False)
|
|
elif b == 1:
|
|
return str(True)
|
|
return "n/a"
|
|
|
|
def _get_compatible_csv_operation_name(self):
|
|
if "<Unknown>" in self.operation:
|
|
return "<Unknown>"
|
|
if EventClass.Process == self.event_class:
|
|
return self.operation.replace('_', ' ')
|
|
return self.operation
|
|
|
|
def _get_compatible_csv_detail_column(self):
|
|
"""Returns the detail column as a string which is compatible to Procmon's detail format in the exported csv.
|
|
"""
|
|
if not self.details:
|
|
return ""
|
|
details = self.details.copy()
|
|
if self.operation == ProcessOperation.Load_Image.name:
|
|
details["Image Base"] = "0x{:x}".format(details["Image Base"])
|
|
details["Image Size"] = "0x{:x}".format(details["Image Size"])
|
|
elif self.operation == ProcessOperation.Thread_Exit.name:
|
|
details["User Time"] = Event._strftime_duration(details["User Time"])
|
|
details["Kernel Time"] = Event._strftime_duration(details["Kernel Time"])
|
|
elif self.operation == ProcessOperation.Process_Start.name:
|
|
details["Environment"] = "\n;\t" + "\n;\t".join(details["Environment"])
|
|
elif EventClass.Registry == self.event_class:
|
|
commas_formatted_keys = ["Length", "SubKeys", "Values", "Index"]
|
|
for key in commas_formatted_keys:
|
|
if key in details:
|
|
details[key] = '{:,}'.format(details[key])
|
|
|
|
hexa_formatted_keys = ["HandleTags", "UserFlags", "Wow64Flags"]
|
|
for key in hexa_formatted_keys:
|
|
if key in details:
|
|
details[key] = "0x{:x}".format(details[key])
|
|
|
|
removed_keys = ["TitleIndex", "MaxNameLen", "MaxValueNameLen", "MaxValueDataLen",
|
|
"ClassOffset", "ClassLength", "MaxClassLen"]
|
|
for key in removed_keys:
|
|
if key in details:
|
|
del details[key]
|
|
if "LastWriteTime" in details:
|
|
if self.operation == "RegSetInfoKey":
|
|
details["LastWriteTime"] = self._strftime_date(details["LastWriteTime"])
|
|
else:
|
|
del details["LastWriteTime"]
|
|
|
|
if details.get("Type", '') == "REG_BINARY" and "Data" in details:
|
|
binary_ascii = binascii.b2a_hex(details["Data"]).decode('ascii').upper()
|
|
binary_ascii_formatted = ' '.join(binary_ascii[i:i+2] for i in range(0, len(binary_ascii), 2))
|
|
details["Data"] = binary_ascii_formatted
|
|
elif details.get("Type", '') == "REG_QWORD" and "Data" in details:
|
|
details["Data"] = '' # Procmon doesnt print qword in csv, I don't know why
|
|
elif details.get("Type", '') == "REG_MULTI_SZ" and "Data" in details:
|
|
details["Data"] = ', '.join(details["Data"])
|
|
elif "Data" in details and isinstance(details["Data"], string_types):
|
|
details["Data"] = "\n;".join(details["Data"].split('\r\n')) # They add ; before a new line
|
|
|
|
if self.operation == "RegQueryValue" and "Name" in details:
|
|
del details["Name"]
|
|
elif self.operation == "RegQueryKey" and details["Query"] == "Name" and "Name" in details:
|
|
del details["Name"]
|
|
elif EventClass.File_System == self.event_class:
|
|
commas_formatted_keys = ["AllocationSize", "Offset", "Length"]
|
|
for key in commas_formatted_keys:
|
|
if key in details and int == type(details[key]):
|
|
details[key] = '{:,}'.format(details[key])
|
|
|
|
return ", ".join("{}: {}".format(k, v) for k, v in details.items())
|
|
|
|
def get_compatible_csv_info(self, first_event_date_filetime=None):
|
|
"""Returns data for every Procmon column in compatible format to the exported csv by procmon
|
|
"""
|
|
first_event_date_filetime = first_event_date_filetime if first_event_date_filetime else self.date_filetime
|
|
record = {
|
|
Column.DATE_AND_TIME: Event._strftime_date(self.date_filetime, True, False),
|
|
Column.PROCESS_NAME: self.process.process_name,
|
|
Column.PID: str(self.process.pid),
|
|
Column.OPERATION: self._get_compatible_csv_operation_name(),
|
|
Column.RESULT: get_error_message(self.result),
|
|
Column.DETAIL: self._get_compatible_csv_detail_column(),
|
|
Column.SEQUENCE: 'n/a', # They do it too
|
|
Column.COMPANY: self.process.company,
|
|
Column.DESCRIPTION: self.process.description,
|
|
Column.COMMAND_LINE: self.process.command_line,
|
|
Column.USER: self.process.user,
|
|
Column.IMAGE_PATH: self.process.image_path,
|
|
Column.SESSION: str(self.process.session),
|
|
Column.PATH: self.path,
|
|
Column.TID: str(self.tid),
|
|
Column.RELATIVE_TIME: Event._strftime_relative_time(self.date_filetime - first_event_date_filetime),
|
|
Column.DURATION:
|
|
Event._strftime_duration(self.duration) if get_error_message(self.result) != "" else "",
|
|
Column.TIME_OF_DAY: Event._strftime_date(self.date_filetime, False, True),
|
|
Column.VERSION: self.process.version,
|
|
Column.EVENT_CLASS: self.event_class.name.replace('_', ' '),
|
|
Column.AUTHENTICATION_ID:
|
|
"{:08x}:{:08x}".format(self.process.authentication_id >> 32,
|
|
self.process.authentication_id & 0xFFFFFFFF),
|
|
Column.VIRTUALIZED: Event._get_bool_str(self.process.virtualized),
|
|
Column.INTEGRITY: self.process.integrity,
|
|
Column.CATEGORY: self.category,
|
|
Column.PARENT_PID: str(self.process.parent_pid),
|
|
Column.ARCHITECTURE: "64-bit" if self.process.is_process_64bit else "32-bit",
|
|
Column.COMPLETION_TIME:
|
|
Event._strftime_date(self.date_filetime + self.duration, False, True)
|
|
if get_error_message(self.result) != "" else "",
|
|
}
|
|
|
|
compatible_record = {ColumnToOriginalName[k]: v for k, v in record.items()}
|
|
return compatible_record
|
|
|
|
|
|
class PMLStructReader(object):
|
|
@property
|
|
def header(self):
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def events_offsets(self):
|
|
raise NotImplementedError()
|
|
|
|
def get_event_at_offset(self, offset):
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def number_of_events(self):
|
|
return self.header.number_of_events
|
|
|
|
def processes(self):
|
|
"""Return a list of all the known processes in the log file
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def __getitem__(self, index):
|
|
if isinstance(index, slice):
|
|
return [self.get_event_at_offset(offset) for offset in self.events_offsets[index]]
|
|
elif isinstance(index, int):
|
|
return self.get_event_at_offset(self.events_offsets[index])
|
|
|
|
raise TypeError("Bad index")
|
|
|
|
def _get_os_name(self):
|
|
windows_names = {
|
|
(6, 0): "Windows Vista",
|
|
(6, 1): "Windows 7",
|
|
(6, 2): "Windows 8",
|
|
(6, 3): "Windows 8.1",
|
|
(10, 0): "Windows 10",
|
|
}
|
|
|
|
windows_name = windows_names[(self.header.windows_major_number, self.header.windows_minor_number)]
|
|
if self.header.service_pack_name:
|
|
windows_name += ", {}".format(self.header.service_pack_name)
|
|
|
|
return "{} (build {}.{})".format(windows_name, self.header.windows_build_number,
|
|
self.header.windows_build_number_after_decimal_point)
|
|
|
|
def system_details(self):
|
|
"""Return the system details of the computer which captured the logs (like Tools -> System Details in Procmon)
|
|
"""
|
|
return {
|
|
"Computer Name": self.header.computer_name,
|
|
"Operating System": self._get_os_name(),
|
|
"System Root": self.header.system_root,
|
|
"Logical Processors": self.header.number_of_logical_processors,
|
|
"Memory (RAM)": "{} GB".format((self.header.ram_memory_size / (1024.0 ** 3)) // 0.01 / 100),
|
|
"System Type": "64-bit" if self.header.is_64bit else "32-bit"
|
|
}
|