"""Text parser plugin for SELinux audit log (audit.log) files.
audit.log log line example:
type=AVC msg=audit(1105758604.519:420): avc: denied { getattr } for pid=5962
comm="httpd" path="/home/auser/public_html" dev=sdb2 ino=921135
Where msg=audit(1105758604.519:420) contains the number of seconds since January 1, 1970
00:00:00 UTC and the number of milliseconds after the dot for example: "seconds:
1105758604, milliseconds: 519".
The number after the timestamp (420 in the example) is a 'serial number' that can be
used to correlate multiple logs generated from the same event.
"""
import pyparsing
from dfdatetime import posix_time as dfdatetime_posix_time
from plaso.containers import events
from plaso.lib import definitions
from plaso.lib import errors
from plaso.parsers import text_parser
from plaso.parsers.text_plugins import interface
[docs]
class SELinuxLogEventData(events.EventData):
"""SELinux log event data.
Attributes:
account (str): name of the account (acct) that the event acts on, such as
"root" for an attempt to authenticate as root. Note that this is an
account name, where user_identifier is the numeric user identifier of
the process that caused the event, which is typically a different
user, such as the user running "su".
architecture (str): CPU architecture (arch); the resolved name (e.g.
"x86_64") when the record is ENRICHED, otherwise the raw value.
audit_login_identifier (str): audit login identifier (auid), the login
user identifier that is retained across su and sudo, or None if unset.
audit_rule_keys (list[str]): keys (key) of the audit rule that triggered
the record, where a rule can have multiple keys.
audit_serial (int): audit serial number, used to correlate the records
that belong to a single audited event.
audit_session_identifier (str): audit session identifier (ses), or None
if unset.
audit_type (str): audit type.
executable (str): path of the executable (exe).
exit_code (int): exit status of the system call (exit), where a negative
value represents an errno value.
file_mode (int): file mode (mode) of the file, which includes the file type
and the permissions, such as 0o100640 for a regular file that is
readable and writable by its owner and readable by its group.
file_path (str): file path (name) referenced by a PATH record.
group_identifier (str): group identifier (gid) of the process.
last_written_time (dfdatetime.DateTimeValues): entry last written date and time.
message_body (str): message body.
name_type (str): type of the path reference (nametype), such as NORMAL,
PARENT, CREATE or DELETE.
operation (str): operation (op) that is audited, such as
"PAM:authentication", "add_rule" or "LOAD".
operation_result (bool): True if the audited operation was successful. The
log format represents this value as "success" or "failed", or as "1" or
"0" on record types such as CONFIG_CHANGE and LOGIN.
owner_group_identifier (str): group identifier that owns the file (ogid).
owner_user_identifier (str): user identifier that owns the file (ouid).
parent_process_identifier (str): parent process identifier (ppid).
pid (str): process identifier (PID) that created the SELinux log line.
process_arguments (str): arguments of an executed program (the EXECVE argc
and a0 .. aN fields), separated by spaces.
process_name (str): name of the process (comm).
process_title (str): process title (proctitle) of the process, which
contains the command line with its arguments separated by spaces.
remote_address (str): source address (addr) of a remote event.
remote_hostname (str): source hostname (hostname) of a remote event.
security_context (str): security context (subj) of the process, such as a
SELinux or AppArmor label.
success (bool): True if the system call was successful. The log format
represents this value as "yes" or "no".
system_call (str): system call (syscall).
terminal (str): controlling terminal (terminal) of the event.
user_identifier (str): user identifier (uid) of the process.
working_directory (str): working directory (cwd) of the process at
execution time.
"""
DATA_TYPE = "selinux:line"
[docs]
def __init__(self):
"""Initializes event data."""
super().__init__(data_type=self.DATA_TYPE)
self.account = None
self.architecture = None
self.audit_login_identifier = None
self.audit_rule_keys = None
self.audit_serial = None
self.audit_session_identifier = None
self.audit_type = None
self.executable = None
self.exit_code = None
self.file_mode = None
self.file_path = None
self.group_identifier = None
self.last_written_time = None
self.message_body = None
self.name_type = None
self.operation = None
self.operation_result = None
self.owner_group_identifier = None
self.owner_user_identifier = None
self.parent_process_identifier = None
self.pid = None
self.process_arguments = None
self.process_name = None
self.process_title = None
self.remote_address = None
self.remote_hostname = None
self.security_context = None
self.success = None
self.system_call = None
self.terminal = None
self.user_identifier = None
self.working_directory = None
[docs]
class SELinuxTextPlugin(interface.TextPlugin):
"""Text parser plugin for SELinux audit log (audit.log) files."""
NAME = "selinux"
DATA_FORMAT = "SELinux audit log (audit.log) file"
_INTEGER = pyparsing.Word(pyparsing.nums).set_parse_action(
lambda tokens: int(tokens[0], 10)
)
# Values are deliberately not unquoted when parsed, so that a quoted value,
# which is a literal, can be distinguished from an unquoted value, which is
# hex-encoded.
_KEY_VALUE_GROUP = pyparsing.Group(
pyparsing.Word(pyparsing.alphanums + "-_")
+ pyparsing.Suppress("=")
+ (
pyparsing.QuotedString('"', unquote_results=False)
^ pyparsing.QuotedString("'", unquote_results=False)
^ pyparsing.Word(pyparsing.printables)
)
)
_KEY_VALUE_DICT = pyparsing.Dict(pyparsing.ZeroOrMore(_KEY_VALUE_GROUP))
_HEX_DIGITS = frozenset("0123456789ABCDEFabcdef")
_TIMESTAMP = pyparsing.Group(_INTEGER + pyparsing.Suppress(".") + _INTEGER)
_END_OF_LINE = pyparsing.Suppress(pyparsing.LineEnd())
# A log line is formatted as: type=TYPE msg=audit([0-9]+\.[0-9]+:[0-9]+): .*
_LOG_LINE = (
pyparsing.Suppress("type=")
+ (
pyparsing.Word(pyparsing.srange("[A-Z_]"))
^ pyparsing.Regex(r"UNKNOWN\[[0-9]+\]")
).set_results_name("type")
+ pyparsing.Suppress("msg=audit(")
+ _TIMESTAMP.set_results_name("timestamp")
+ pyparsing.Suppress(":")
+ _INTEGER.set_results_name("serial")
+ pyparsing.Suppress("):")
+ pyparsing.restOfLine().set_results_name("message_body")
+ _END_OF_LINE
)
_LINE_STRUCTURES = [("log_line", _LOG_LINE)]
VERIFICATION_GRAMMAR = _LOG_LINE
# auditd sentinels that stand in for an absent value.
_SENTINEL_VALUES = frozenset(["?", "(null)", "(none)"])
# Value that auditd uses for an unset numeric value, which is (uint32_t) -1.
_UNSET_NUMERIC_VALUE = "4294967295"
# Values of a result field, as interpreted by libauparse, which represents a
# value that is not defined here as unset.
_RESULT_VALUES = {
"0": False,
"1": True,
"failed": False,
"no": False,
"success": True,
"yes": True,
}
# Separator of the audit rule keys of a rule with multiple keys, which is
# AUDIT_KEY_SEPARATOR.
_RULE_KEY_SEPARATOR = "\x01"
def _DecodeHexValue(self, parser_mediator, hex_value):
"""Decodes a hex-encoded value, preserving the original bytes.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
hex_value (str): hex-encoded value.
Returns:
tuple[str, bool]: decoded value, where bytes that are not valid UTF-8 are
kept as escaped byte values, and value to indicate the value was
corrupted. A value that is not validly hex-encoded is returned
unchanged.
"""
try:
decoded_bytes = bytes.fromhex(hex_value)
except ValueError:
parser_mediator.ProduceWarning(
f"unable to decode hex-encoded value: {hex_value:s}"
)
return hex_value, True
try:
return decoded_bytes.decode("utf-8"), False
except UnicodeDecodeError:
parser_mediator.ProduceWarning(
f"unable to decode UTF-8 in hex-encoded value: {hex_value:s}"
)
return decoded_bytes.decode("utf-8", errors="backslashreplace"), True
def _GetValues(self, body):
"""Retrieves the values of the fields in a message body.
Audit records store fields such as "acct", "exe" and "res" either at the
top level of the message body or inside a nested "msg" field, depending on
the record type. The values of a nested "msg" field are therefore merged
into the result, where they take precedence.
Args:
body (str): message body.
Returns:
dict[str, str]: value per field name, where a value of a quoted field is
kept quoted.
"""
values = self._KEY_VALUE_DICT.parse_string(body).as_dict()
nested_body = values.get("msg", None)
if nested_body and nested_body[0] == "'":
values.update(
self._KEY_VALUE_DICT.parse_string(nested_body[1:-1]).as_dict()
)
return values
def _GetValue(self, values, name):
"""Retrieves the value of a field.
Args:
values (dict[str, str]): value per field name.
name (str): field name.
Returns:
tuple[str, bool]: value, or None if the field is not present, its value
is empty or its value is an auditd sentinel such as "?" or "(null)",
and value to indicate the value was quoted.
"""
value = values.get(name, None)
if value is None:
return None, False
is_quoted = value[0] == '"'
if is_quoted:
value = value[1:-1]
if not value or value in self._SENTINEL_VALUES:
return None, False
return value, is_quoted
def _GetStringValue(self, values, name):
"""Retrieves the value of a field as a string.
Args:
values (dict[str, str]): value per field name.
name (str): field name.
Returns:
str: value, or None if the field has no usable value.
"""
value, _ = self._GetValue(values, name)
return value
def _GetEncodedStringValue(self, parser_mediator, values, name):
"""Retrieves the value of a field that auditd can store hex-encoded.
auditd stores the value of these fields hex-encoded if it contains
characters that would otherwise need to be escaped, such as a space, and
quoted if not.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
values (dict[str, str]): value per field name.
name (str): field name.
Returns:
tuple[str, bool]: value, or None if the field has no usable value, and
value to indicate the value was corrupted.
"""
value, is_quoted = self._GetValue(values, name)
if value is None:
return None, False
# A quoted value is a literal, where an unquoted value is hex-encoded.
if is_quoted or not all(character in self._HEX_DIGITS for character in value):
return value, False
return self._DecodeHexValue(parser_mediator, value)
def _GetResultValue(self, parser_mediator, values, name):
"""Retrieves the value of a result field as a boolean.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
values (dict[str, str]): value per field name.
name (str): field name.
Returns:
tuple[bool, bool]: result, or None if the field has no usable value or
its value is unset, and value to indicate the value was corrupted.
"""
value = self._GetStringValue(values, name)
if value is None or value == self._UNSET_NUMERIC_VALUE:
return None, False
result = self._RESULT_VALUES.get(value.lower(), None)
if result is None:
parser_mediator.ProduceWarning(f"unsupported result value: {value:s}")
return None, True
return result, False
def _GetRuleKeys(self, parser_mediator, values):
"""Retrieves the audit rule keys of a record.
An audit rule can have multiple keys, which auditd stores in a single
field separated by AUDIT_KEY_SEPARATOR.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
values (dict[str, str]): value per field name.
Returns:
tuple[list[str], bool]: audit rule keys, or None if the record has no
audit rule key, and value to indicate a value was corrupted.
"""
value, corrupted = self._GetEncodedStringValue(parser_mediator, values, "key")
if value is None:
return None, corrupted
return value.split(self._RULE_KEY_SEPARATOR), corrupted
def _GetArguments(self, parser_mediator, values):
"""Retrieves the command line of an executed program.
The arguments of an EXECVE record are stored as a number of arguments
(argc) and the individual arguments (a0 .. aN), which are joined with a
space, as ausearch does.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
values (dict[str, str]): value per field name.
Returns:
tuple[str, bool]: command line, or None if the record has no arguments,
and value to indicate a value was corrupted.
"""
number_of_arguments = self._GetStringValue(values, "argc")
if number_of_arguments is None:
return None, False
try:
number_of_arguments = int(number_of_arguments, 10)
except ValueError:
parser_mediator.ProduceWarning(
f"invalid number of arguments: {number_of_arguments:s}"
)
return None, True
corrupted = False
arguments = []
for index in range(number_of_arguments):
argument, value_corrupted = self._GetEncodedStringValue(
parser_mediator, values, f"a{index:d}"
)
corrupted = corrupted or value_corrupted
if argument is not None:
arguments.append(argument)
return " ".join(arguments) or None, corrupted
def _GetIdentifierValue(self, values, name):
"""Retrieves the value of an identifier field.
Args:
values (dict[str, str]): value per field name.
name (str): field name.
Returns:
str: identifier, or None if the field has no usable value or the
identifier is unset.
"""
value = self._GetStringValue(values, name)
if value == self._UNSET_NUMERIC_VALUE:
return None
return value
def _GetIntegerValue(self, parser_mediator, values, name, base):
"""Retrieves the value of a field as an integer.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
values (dict[str, str]): value per field name.
name (str): field name.
base (int): base of the numeric value.
Returns:
tuple[int, bool]: value, or None if the field has no usable value, and
value to indicate the value was corrupted.
"""
value = self._GetStringValue(values, name)
if value is None:
return None, False
try:
return int(value, base), False
except ValueError:
parser_mediator.ProduceWarning(f"unsupported {name:s} value: {value:s}")
return None, True
def _ParseRecord(self, parser_mediator, key, structure):
"""Parses a pyparsing structure.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
key (str): name of the parsed structure.
structure (pyparsing.ParseResults): tokens from a parsed log line.
Raises:
ParseError: if the structure cannot be parsed.
"""
if key == "log_line":
time_elements_structure = self._GetValueFromStructure(
structure, "timestamp"
)
# Try to parse the message body as key value pairs. Note that not all log
# lines will be properly formatted key value pairs.
message_body = self._GetValueFromStructure(
structure, "message_body", default_value=""
).strip()
# ENRICHED audit logs (the modern default on both Fedora/RHEL and Ubuntu)
# append an interpreted suffix after a 0x1d (group separator) byte, for
# example "... key=(null)\x1dARCH=x86_64 SYSCALL=execve AUID=...". Split it
# off: the raw key=value body provides the fields and the raw numeric
# identifiers are kept for offline soundness; the resolved suffix is used
# only for the system call name, which is architecture and kernel specific
# and hard to resolve from an offline image. RAW logs have no suffix.
raw_body, _, enriched_body = message_body.partition("\x1d")
raw_body = raw_body.strip()
# Fields such as "acct", "op" and "res" are stored either at the top
# level of the message body or inside a nested "msg" field, hence the
# values of a nested "msg" field are merged into the values.
top_level_values = self._KEY_VALUE_DICT.parse_string(raw_body).as_dict()
values = self._GetValues(raw_body)
enriched_values = self._GetValues(enriched_body)
corrupted = False
event_data = SELinuxLogEventData()
event_data.audit_serial = self._GetValueFromStructure(structure, "serial")
event_data.audit_type = self._GetValueFromStructure(structure, "type")
event_data.last_written_time = self._ParseTimeElements(
time_elements_structure
)
event_data.message_body = raw_body or None
if values:
# Fields that are only stored at the top level of the message body.
event_data.group_identifier = self._GetStringValue(
top_level_values, "gid"
)
event_data.parent_process_identifier = self._GetStringValue(
top_level_values, "ppid"
)
event_data.pid = self._GetStringValue(top_level_values, "pid")
event_data.security_context = self._GetStringValue(
top_level_values, "subj"
)
event_data.system_call = self._GetStringValue(
top_level_values, "syscall"
)
event_data.user_identifier = self._GetStringValue(
top_level_values, "uid"
)
event_data.architecture = self._GetStringValue(values, "arch")
event_data.audit_login_identifier = self._GetIdentifierValue(
values, "auid"
)
event_data.audit_session_identifier = self._GetIdentifierValue(
values, "ses"
)
event_data.name_type = self._GetStringValue(values, "nametype")
event_data.operation = self._GetStringValue(values, "op")
event_data.owner_group_identifier = self._GetStringValue(values, "ogid")
event_data.owner_user_identifier = self._GetStringValue(values, "ouid")
event_data.remote_address = self._GetStringValue(values, "addr")
event_data.remote_hostname = self._GetStringValue(values, "hostname")
event_data.terminal = self._GetStringValue(values, "terminal")
event_data.audit_rule_keys, value_corrupted = self._GetRuleKeys(
parser_mediator, values
)
corrupted = corrupted or value_corrupted
event_data.operation_result, value_corrupted = self._GetResultValue(
parser_mediator, values, "res"
)
corrupted = corrupted or value_corrupted
event_data.success, value_corrupted = self._GetResultValue(
parser_mediator, values, "success"
)
corrupted = corrupted or value_corrupted
event_data.exit_code, value_corrupted = self._GetIntegerValue(
parser_mediator, values, "exit", 10
)
corrupted = corrupted or value_corrupted
event_data.file_mode, value_corrupted = self._GetIntegerValue(
parser_mediator, values, "mode", 8
)
corrupted = corrupted or value_corrupted
event_data.process_arguments, value_corrupted = self._GetArguments(
parser_mediator, values
)
corrupted = corrupted or value_corrupted
for attribute_name, field_name in (
("account", "acct"),
("executable", "exe"),
("file_path", "name"),
("process_name", "comm"),
("working_directory", "cwd"),
):
value, value_corrupted = self._GetEncodedStringValue(
parser_mediator, values, field_name
)
setattr(event_data, attribute_name, value)
corrupted = corrupted or value_corrupted
process_title, value_corrupted = self._GetEncodedStringValue(
parser_mediator, values, "proctitle"
)
corrupted = corrupted or value_corrupted
if process_title:
# The arguments in a process title are separated by a NUL
# character.
process_title = process_title.replace("\x00", " ")
event_data.process_title = process_title
if enriched_values:
enriched_system_call = self._GetStringValue(enriched_values, "SYSCALL")
if enriched_system_call:
event_data.system_call = enriched_system_call
enriched_architecture = self._GetStringValue(enriched_values, "ARCH")
if enriched_architecture:
event_data.architecture = enriched_architecture
parser_mediator.ProduceEventData(event_data, corrupted=corrupted)
def _ParseTimeElements(self, time_elements_structure):
"""Parses date and time elements of a log line.
Args:
time_elements_structure (pyparsing.ParseResults): date and time elements
of a log line.
Returns:
dfdatetime.PosixTimeInMilliseconds: date and time value.
Raises:
ParseError: if a valid date and time value cannot be derived from
the time elements.
"""
try:
seconds, milliseconds = time_elements_structure
timestamp = (seconds * definitions.MILLISECONDS_PER_SECOND) + milliseconds
return dfdatetime_posix_time.PosixTimeInMilliseconds(timestamp=timestamp)
except (TypeError, ValueError) as exception:
raise errors.ParseError(
f"Unable to parse time elements with error: {exception!s}"
)
[docs]
def CheckRequiredFormat(self, parser_mediator, text_reader):
"""Check if the log record has the minimal structure required by the plugin.
Args:
parser_mediator (ParserMediator): mediates interactions between parsers
and other components, such as storage and dfVFS.
text_reader (EncodedTextReader): text reader.
Returns:
bool: True if this is the correct plugin, False otherwise.
"""
try:
structure = self._VerifyString(text_reader.lines)
except errors.ParseError:
return False
time_elements_structure = self._GetValueFromStructure(structure, "timestamp")
try:
self._ParseTimeElements(time_elements_structure)
except errors.ParseError:
return False
return True
text_parser.TextLogParser.RegisterPlugin(SELinuxTextPlugin)