plaso-rubanetra/plaso/parsers/cups_ipp.py
2020-04-06 18:48:34 +02:00

346 lines
12 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2014 The Plaso Project Authors.
# Please see the AUTHORS file for details on individual authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""The CUPS IPP Control Files Parser.
CUPS IPP version 1.0:
* http://tools.ietf.org/html/rfc2565
* http://tools.ietf.org/html/rfc2566
* http://tools.ietf.org/html/rfc2567
* http://tools.ietf.org/html/rfc2568
* http://tools.ietf.org/html/rfc2569
* http://tools.ietf.org/html/rfc2639
CUPS IPP version 1.1:
* http://tools.ietf.org/html/rfc2910
* http://tools.ietf.org/html/rfc2911
* http://tools.ietf.org/html/rfc3196
* http://tools.ietf.org/html/rfc3510
CUPS IPP version 2.0:
* N/A
"""
import construct
import logging
import os
from plaso.lib import errors
from plaso.lib import event
from plaso.lib import eventdata
from plaso.lib import timelib
from plaso.parsers import interface
from plaso.parsers import manager
__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)'
# TODO: RFC Pendings types: resolution, dateTime, rangeOfInteger.
# "dateTime" is not used by Mac OS, instead it uses integer types.
# TODO: Only tested against CUPS IPP Mac OS X.
class CupsIppEvent(event.EventObject):
"""Convenience class for an cups ipp event."""
DATA_TYPE = 'cups:ipp:event'
def __init__(
self, timestamp, timestamp_desc, data_dict):
"""Initializes the event object.
Args:
timestamp: Timestamp of the entry.
timestamp_desc: Description of the timestamp.
data_dict: Dictionary with all the pairs coming from IPP file.
user: String with the system user name.
owner: String with the real name of the user.
computer_name: String with the name of the computer.
printer_id: String with the identification name of the print.
uri: String with the URL of the CUPS service.
job_id: String with the identification id of the job.
job_name: String with the job name.
copies: Integer with the number of copies.
application: String with the application that prints the document.
doc_usingtype: String with the type of document.
data_dict: Dictionary with all the parsed data comming from the file.
"""
super(CupsIppEvent, self).__init__()
self.timestamp = timelib.Timestamp.FromPosixTime(timestamp)
self.timestamp_desc = timestamp_desc
# TODO: Find a better solution than to have join for each attribute.
self.user = self._ListToString(data_dict.get('user', None))
self.owner = self._ListToString(data_dict.get('owner', None))
self.computer_name = self._ListToString(data_dict.get(
'computer_name', None))
self.printer_id = self._ListToString(data_dict.get('printer_id', None))
self.uri = self._ListToString(data_dict.get('uri', None))
self.job_id = self._ListToString(data_dict.get('job_id', None))
self.job_name = self._ListToString(data_dict.get('job_name', None))
self.copies = data_dict.get('copies', 0)[0]
self.application = self._ListToString(data_dict.get('application', None))
self.doc_type = self._ListToString(data_dict.get('doc_type', None))
self.data_dict = data_dict
def _ListToString(self, values):
"""Returns a string from a list value using comma as a delimiter.
If any value inside the list contains comma, which is the delimiter,
the entire field is surrounded with double quotes.
Args:
values: A list or tuple containing the values.
Returns:
A string containing all the values joined using comma as a delimiter
or None.
"""
if values is None:
return
if type(values) not in (list, tuple):
return
for index, value in enumerate(values):
if ',' in value:
values[index] = u'"{0:s}"'.format(value)
try:
return u', '.join(values)
except UnicodeDecodeError as exception:
logging.error(
u'Unable to parse log line, with error: {0:s}'.format(exception))
class CupsIppParser(interface.BaseParser):
"""Parser for CUPS IPP files. """
NAME = 'cups_ipp'
DESCRIPTION = u'Parser for CUPS IPP files.'
# INFO:
# For each file, we have only one document with three different timestamps:
# Created, process and finished.
# Format:
# [HEADER: MAGIC + KNOWN_TYPE][GROUP A]...[GROUP Z][GROUP_END: 0x03]
# GROUP: [GROUP ID][PAIR A]...[PAIR Z] where [PAIR: NAME + VALUE]
# GROUP ID: [1byte ID]
# PAIR: [TagID][\x00][Name][Value])
# TagID: 1 byte integer with the type of "Value".
# Name: [Length][Text][\00]
# Name can be empty when the name has more than one value.
# Example: family name "lopez mata" with more than one surname.
# Type_Text + [0x06, family, 0x00] + [0x05, lopez, 0x00] +
# Type_Text + [0x00, 0x00] + [0x04, mata, 0x00]
# Value: can be integer, boolean, or text provided by TagID.
# If boolean, Value: [\x01][0x00(False)] or [\x01(True)]
# If integer, Value: [\x04][Integer]
# If text, Value: [Length text][Text][\00]
# Magic number that identify the CUPS IPP supported version.
IPP_MAJOR_VERSION = 2
IPP_MINOR_VERSION = 0
# Supported Operation ID.
IPP_OP_ID = 5
# CUPS IPP File header.
CUPS_IPP_HEADER = construct.Struct(
'cups_ipp_header_struct',
construct.UBInt8('major_version'),
construct.UBInt8('minor_version'),
construct.UBInt16('operation_id'),
construct.UBInt32('request_id'))
# Group ID that indicates the end of the IPP Control file.
GROUP_END = 3
# Identification Groups.
GROUP_LIST = [1, 2, 4, 5, 6, 7]
# Type ID.
TYPE_GENERAL_INTEGER = 32
TYPE_INTEGER = 33
TYPE_ENUMERATION = 35
TYPE_BOOL = 34
# Type of values that can be extracted.
INTEGER_8 = construct.UBInt8('integer')
INTEGER_32 = construct.UBInt32('integer')
TEXT = construct.PascalString(
'text',
length_field=construct.UBInt8('length'))
BOOLEAN = construct.Struct(
'boolean_value',
construct.Padding(1),
INTEGER_8)
INTEGER = construct.Struct(
'integer_value',
construct.Padding(1),
INTEGER_32)
# Name of the pair.
PAIR_NAME = construct.Struct(
'pair_name',
TEXT,
construct.Padding(1))
# Specific CUPS IPP to generic name.
NAME_PAIR_TRANSLATION = {
'printer-uri': u'uri',
'job-uuid': u'job_id',
'DestinationPrinterID': u'printer_id',
'job-originating-user-name': u'user',
'job-name': u'job_name',
'document-format': u'doc_type',
'job-originating-host-name': u'computer_name',
'com.apple.print.JobInfo.PMApplicationName': u'application',
'com.apple.print.JobInfo.PMJobOwner': u'owner'}
def Parse(self, parser_context, file_entry, parser_chain=None):
"""Extract a entry from an CUPS IPP file.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
parser_chain: Optional string containing the parsing chain up to this
point. The default is None.
"""
parser_chain = self._BuildParserChain(parser_chain)
file_object = file_entry.GetFileObject()
file_object.seek(0, os.SEEK_SET)
try:
header = self.CUPS_IPP_HEADER.parse_stream(file_object)
except (IOError, construct.FieldError) as exception:
file_object.close()
raise errors.UnableToParseFile(
u'Unable to parse CUPS IPP Header with error: {0:s}'.format(
exception))
if (header.major_version != self.IPP_MAJOR_VERSION or
header.minor_version != self.IPP_MINOR_VERSION):
file_object.close()
raise errors.UnableToParseFile(
u'[{0:s}] Unsupported version number.'.format(self.NAME))
if header.operation_id != self.IPP_OP_ID:
# Warn if the operation ID differs from the standard one. We should be
# able to parse the file nonetheless.
logging.debug(
u'[{0:s}] Unsupported operation identifier in file: {1:s}.'.format(
self.NAME, parser_context.GetDisplayName(file_entry)))
# Read the pairs extracting the name and the value.
data_dict = {}
name, value = self.ReadPair(parser_context, file_entry, file_object)
while name or value:
# Translate the known "name" CUPS IPP to a generic name value.
pretty_name = self.NAME_PAIR_TRANSLATION.get(name, name)
data_dict.setdefault(pretty_name, []).append(value)
name, value = self.ReadPair(parser_context, file_entry, file_object)
if u'time-at-creation' in data_dict:
event_object = CupsIppEvent(
data_dict['time-at-creation'][0],
eventdata.EventTimestamp.CREATION_TIME, data_dict)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if u'time-at-processing' in data_dict:
event_object = CupsIppEvent(
data_dict['time-at-processing'][0],
eventdata.EventTimestamp.START_TIME, data_dict)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
if u'time-at-completed' in data_dict:
event_object = CupsIppEvent(
data_dict['time-at-completed'][0],
eventdata.EventTimestamp.END_TIME, data_dict)
parser_context.ProduceEvent(
event_object, parser_chain=parser_chain, file_entry=file_entry)
file_object.close()
def ReadPair(self, parser_context, file_entry, file_object):
"""Reads an attribute name and value pair from a CUPS IPP event.
Args:
parser_context: A parser context object (instance of ParserContext).
file_entry: A file entry object (instance of dfvfs.FileEntry).
file_object: a file-like object that points to a file.
Returns:
A list of name and value. If name and value cannot be read both are
set to None.
"""
# Pair = Type ID + Name + Value.
try:
# Can be:
# Group ID + IDtag = Group ID (1byte) + Tag ID (1byte) + '0x00'.
# IDtag = Tag ID (1byte) + '0x00'.
type_id = self.INTEGER_8.parse_stream(file_object)
if type_id == self.GROUP_END:
return None, None
elif type_id in self.GROUP_LIST:
# If it is a group ID we must read the next byte that contains
# the first TagID.
type_id = self.INTEGER_8.parse_stream(file_object)
# 0x00 separator character.
_ = self.INTEGER_8.parse_stream(file_object)
except (IOError, construct.FieldError):
logging.warning(
u'[{0:s}] Unsupported identifier in file: {1:s}.'.format(
self.NAME, parser_context.GetDisplayName(file_entry)))
return None, None
# Name = Length name + name + 0x00
try:
name = self.PAIR_NAME.parse_stream(file_object).text
except (IOError, construct.FieldError):
logging.warning(
u'[{0:s}] Unsupported name in file: {1:s}.'.format(
self.NAME, parser_context.GetDisplayName(file_entry)))
return None, None
# Value: can be integer, boolean or text select by Type ID.
try:
if type_id in [
self.TYPE_GENERAL_INTEGER, self.TYPE_INTEGER, self.TYPE_ENUMERATION]:
value = self.INTEGER.parse_stream(file_object).integer
elif type_id == self.TYPE_BOOL:
value = bool(self.BOOLEAN.parse_stream(file_object).integer)
else:
value = self.TEXT.parse_stream(file_object)
except (IOError, construct.FieldError):
logging.warning(
u'[{0:s}] Unsupported value in file: {1:s}.'.format(
self.NAME, parser_context.GetDisplayName(file_entry)))
return None, None
return name, value
manager.ParsersManager.RegisterParser(CupsIppParser)