├── .gitignore
├── Chapter01
├── comments.py
├── hello.py
└── hellov2.py
├── Chapter02
├── argument_parser.py
├── unix_converter.py
├── usb_lookup.py
└── user_input.py
├── Chapter03
├── setupapi_parser.py
├── setupapi_parser_v1.py
├── setupapi_parser_v2.py
└── usb_lookup.py
├── Chapter04
├── bitcoin_address_lookup.py
├── bitcoin_address_lookup.v1.py
├── bitcoin_address_lookup.v2.py
├── book.json
├── book.xml
└── unix_converter.py
├── Chapter05
├── file_lister.py
└── file_lister_peewee.py
├── Chapter06
├── Neguhe Qrag.bin
├── Writers
│ ├── __init__.py
│ ├── csv_writer.py
│ └── xlsx_writer.py
├── rot13.py
├── simplexlsx.v1.py
├── simplexlsx.v2.py
├── simplexlsx.v3.py
└── userassist_parser.py
├── Chapter07
├── TEST_DATA_README.md
├── fuzzy_hasher.py
├── hashing_example.py
├── ssdeep_python.py
└── test_data
│ ├── file_1
│ ├── file_1a
│ ├── file_2
│ ├── file_2a
│ ├── file_3
│ └── file_3a
├── Chapter08
├── img_42.jpg
├── metadata_parser.py
├── plugins
│ ├── __init__.py
│ ├── exif_parser.py
│ ├── id3_parser.py
│ └── office_parser.py
├── processors
│ ├── __init__.py
│ └── utility.py
└── writers
│ ├── __init__.py
│ ├── csv_writer.py
│ └── kml_writer.py
├── Chapter09
└── date_decoder.py
├── Chapter10
└── pysysinfo.py
├── Chapter11
├── Dockerfile
├── docker_libs
│ ├── LIBPFF-LICENSE.txt
│ └── usr
│ │ ├── lib
│ │ └── python2.7
│ │ │ └── dist-packages
│ │ │ ├── pypff.a
│ │ │ ├── pypff.la
│ │ │ └── pypff.so
│ │ └── local
│ │ └── lib
│ │ ├── libpff.a
│ │ ├── libpff.la
│ │ └── libpff.so.1.0.0
├── pst_indexer.py
└── stats_template.html
├── Chapter12
├── places.sqlite-wal
└── wal_crawler.py
├── Chapter13
├── chapter_13
│ ├── framework.py
│ ├── plugins
│ │ ├── __init__.py
│ │ ├── exif.py
│ │ ├── helper
│ │ │ ├── __init__.py
│ │ │ ├── usb_lookup.py
│ │ │ └── utility.py
│ │ ├── id3.py
│ │ ├── office.py
│ │ ├── pst_indexer.py
│ │ ├── setupapi.py
│ │ ├── userassist.py
│ │ └── wal_crawler.py
│ ├── requirements.txt
│ └── writers
│ │ ├── __init__.py
│ │ ├── csv_writer.py
│ │ ├── kml_writer.py
│ │ └── xlsx_writer.py
├── framework.py
├── plugins
│ ├── __init__.py
│ ├── exif.py
│ ├── helper
│ │ ├── __init__.py
│ │ ├── usb_lookup.py
│ │ └── utility.py
│ ├── id3.py
│ ├── office.py
│ ├── pst_indexer.py
│ ├── setupapi.py
│ ├── userassist.py
│ └── wal_crawler.py
├── requirements.txt
└── writers
│ ├── __init__.py
│ ├── csv_writer.py
│ ├── kml_writer.py
│ └── xlsx_writer.py
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | **/__MACOSX/
2 | **/.DS_Store
3 | **/*.pyc
--------------------------------------------------------------------------------
/Chapter01/comments.py:
--------------------------------------------------------------------------------
1 | # This is a comment
2 | print(5 + 5) # This is an inline comment. Everything to the right of the # symbol does not get executed
3 | """We can use three quotes to create
4 | multi-line comments."""
5 |
--------------------------------------------------------------------------------
/Chapter01/hello.py:
--------------------------------------------------------------------------------
1 | print("Hello World!")
2 |
--------------------------------------------------------------------------------
/Chapter01/hellov2.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | print("Hello World!")
3 |
--------------------------------------------------------------------------------
/Chapter02/argument_parser.py:
--------------------------------------------------------------------------------
1 | """Sample argparse example."""
2 | from __future__ import print_function
3 | import argparse
4 |
5 | """
6 | MIT License
7 | Copyright (c) 2018 Chapin Bryce, Preston Miller
8 | Please share comments and questions at:
9 | https://github.com/PythonForensics/Learning-Python-for-Forensics
10 | or email pyforcookbook@gmail.com
11 |
12 | Permission is hereby granted, free of charge, to any person
13 | obtaining a copy of this software and associated documentation
14 | files (the "Software"), to deal in the Software without
15 | restriction, including without limitation the rights to use,
16 | copy, modify, merge, publish, distribute, sublicense, and/or
17 | sell copies of the Software, and to permit persons to whom the
18 | Software is furnished to do so, subject to the following
19 | conditions:
20 |
21 | The above copyright notice and this permission notice shall be
22 | included in all copies or substantial portions of the Software.
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
25 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
27 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
28 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
29 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 | OTHER DEALINGS IN THE SOFTWARE.
31 | """
32 |
33 | __authors__ = ["Chapin Bryce", "Preston Miller"]
34 | __date__ = 20181027
35 | __description__ = "Argparse command-line parser sample"
36 |
37 |
38 | def main(args):
39 | print(args)
40 |
41 |
42 | if __name__ == '__main__':
43 | parser = argparse.ArgumentParser(
44 | description=__description__,
45 | epilog='Built by {}. Version {}'.format(
46 | ", ".join(__authors__), __date__),
47 | formatter_class=argparse.ArgumentDefaultsHelpFormatter
48 | )
49 |
50 | # Add positional required arguments
51 | parser.add_argument('timezone', help='timezone to apply')
52 |
53 | # Add non-positional required argument
54 | parser.add_argument('--source',
55 | help='source information', required=True)
56 |
57 | # Add optional arguments, allowing shorthand argument
58 | parser.add_argument('-l', '--log', help='Path to log file')
59 |
60 | # Using actions
61 | parser.add_argument('--no-email',
62 | help='disable emails', action="store_false")
63 | parser.add_argument('--send-email',
64 | help='enable emails', action="store_true")
65 | # Append values for each argument instance.
66 | parser.add_argument('--emails',
67 | help='email addresses to notify', action="append")
68 | # Count the number of instances. i.e. -vvv
69 | parser.add_argument('-v', help='add verbosity', action='count')
70 |
71 | # Defaults
72 | parser.add_argument('--length', default=55, type=int)
73 | parser.add_argument('--name', default='Alfred', type=str)
74 |
75 | # Handling Files
76 | parser.add_argument('input_file', type=argparse.FileType('r'))
77 | parser.add_argument('output_file', type=argparse.FileType('w'))
78 |
79 | # Allow only specified choices
80 | parser.add_argument('--file-type',
81 | choices=['E01', 'RAW', 'Ex01'])
82 |
83 | # Parsing defined arguments
84 | arguments = parser.parse_args()
85 | main(arguments)
86 |
--------------------------------------------------------------------------------
/Chapter02/unix_converter.py:
--------------------------------------------------------------------------------
1 | """Script to convert UNIX timestamps."""
2 | from __future__ import print_function
3 | import datetime
4 | import sys
5 |
6 | if sys.version_info[0] == 3:
7 | get_input = input
8 | elif sys.version_info[0] == 2:
9 | get_input = raw_input
10 | else:
11 | raise NotImplementedError(
12 | "Unsupported version of Python used.")
13 |
14 | """
15 | MIT License
16 | Copyright (c) 2018 Chapin Bryce, Preston Miller
17 | Please share comments and questions at:
18 | https://github.com/PythonForensics/Learning-Python-for-Forensics
19 | or email pyforcookbook@gmail.com
20 |
21 | Permission is hereby granted, free of charge, to any person
22 | obtaining a copy of this software and associated documentation
23 | files (the "Software"), to deal in the Software without
24 | restriction, including without limitation the rights to use,
25 | copy, modify, merge, publish, distribute, sublicense, and/or
26 | sell copies of the Software, and to permit persons to whom the
27 | Software is furnished to do so, subject to the following
28 | conditions:
29 |
30 | The above copyright notice and this permission notice shall be
31 | included in all copies or substantial portions of the Software.
32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
33 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
34 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
35 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
36 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
37 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
38 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
39 | OTHER DEALINGS IN THE SOFTWARE.
40 | """
41 |
42 | __authors__ = ["Chapin Bryce", "Preston Miller"]
43 | __date__ = 20181027
44 | __description__ = """Convert unix formatted timestamps (seconds
45 | since Epoch [1970-01-01 00:00:00]) to human readable."""
46 |
47 |
48 | def main():
49 | unix_ts = int(get_input('Unix timestamp to convert:\n>> '))
50 | print(unix_converter(unix_ts))
51 |
52 |
53 | def unix_converter(timestamp):
54 | date_ts = datetime.datetime.utcfromtimestamp(timestamp)
55 | return date_ts.strftime('%m/%d/%Y %I:%M:%S %p')
56 |
57 | if __name__ == '__main__':
58 | main()
59 |
--------------------------------------------------------------------------------
/Chapter02/usb_lookup.py:
--------------------------------------------------------------------------------
1 | """Script to lookup USB vendor and product values."""
2 | from __future__ import print_function
3 | try:
4 | from urllib2 import urlopen
5 | except ImportError:
6 | from urllib.request import urlopen
7 | import argparse
8 |
9 | """
10 | MIT License
11 | Copyright (c) 2018 Chapin Bryce, Preston Miller
12 | Please share comments and questions at:
13 | https://github.com/PythonForensics/Learning-Python-for-Forensics
14 | or email pyforcookbook@gmail.com
15 |
16 | Permission is hereby granted, free of charge, to any person
17 | obtaining a copy of this software and associated documentation
18 | files (the "Software"), to deal in the Software without
19 | restriction, including without limitation the rights to use,
20 | copy, modify, merge, publish, distribute, sublicense, and/or
21 | sell copies of the Software, and to permit persons to whom the
22 | Software is furnished to do so, subject to the following
23 | conditions:
24 |
25 | The above copyright notice and this permission notice shall be
26 | included in all copies or substantial portions of the Software.
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
34 | OTHER DEALINGS IN THE SOFTWARE.
35 | """
36 |
37 | __authors__ = ["Chapin Bryce", "Preston Miller"]
38 | __date__ = 20181027
39 | __description__ = "USB vid/pid lookup utility"
40 |
41 |
42 | def main(vid, pid):
43 | url = 'http://www.linux-usb.org/usb.ids'
44 | usbs = {}
45 | usb_file = urlopen(url)
46 | curr_id = ''
47 |
48 | for line in usb_file:
49 | if isinstance(line, bytes):
50 | line = line.decode('latin-1')
51 | if line.startswith('#') or line in ('\n', '\t'):
52 | continue
53 | else:
54 | if not(line.startswith('\t')) and line[0].isalnum():
55 | uid, name = line.strip().split(' ', 1)
56 | curr_id = uid
57 | usbs[uid] = [name.strip(), {}]
58 | elif line.startswith('\t') and line.count('\t') == 1:
59 | uid, name = line.strip().split(' ', 1)
60 | usbs[curr_id][1][uid] = name.strip()
61 |
62 | search_key(vid, pid, usbs)
63 |
64 |
65 | def search_key(vendor_key, product_key, usb_dict):
66 | vendor = usb_dict.get(vendor_key, None)
67 | if vendor is None:
68 | print('Vendor ID not found')
69 | exit()
70 |
71 | product = vendor[1].get(product_key, None)
72 | if product is None:
73 | print('Vendor: {}\nProduct Id not found.'.format(
74 | vendor[0]))
75 | exit(0)
76 |
77 | print('Vendor: {}\nProduct: {}'.format(vendor[0], product))
78 |
79 |
80 | if __name__ == '__main__':
81 | parser = argparse.ArgumentParser(
82 | description=__description__,
83 | epilog='Built by {}. Version {}'.format(
84 | ", ".join(__authors__), __date__),
85 | formatter_class=argparse.ArgumentDefaultsHelpFormatter
86 | )
87 | parser.add_argument('vid', help="VID value")
88 | parser.add_argument('pid', help="pID value")
89 | args = parser.parse_args()
90 | main(args.vid, args.pid)
91 |
--------------------------------------------------------------------------------
/Chapter02/user_input.py:
--------------------------------------------------------------------------------
1 | """Replicate user input in the console."""
2 | from __future__ import print_function
3 | import sys
4 |
5 | """
6 | MIT License
7 | Copyright (c) 2018 Chapin Bryce, Preston Miller
8 | Please share comments and questions at:
9 | https://github.com/PythonForensics/Learning-Python-for-Forensics
10 | or email pyforcookbook@gmail.com
11 |
12 | Permission is hereby granted, free of charge, to any person
13 | obtaining a copy of this software and associated documentation
14 | files (the "Software"), to deal in the Software without
15 | restriction, including without limitation the rights to use,
16 | copy, modify, merge, publish, distribute, sublicense, and/or
17 | sell copies of the Software, and to permit persons to whom the
18 | Software is furnished to do so, subject to the following
19 | conditions:
20 |
21 | The above copyright notice and this permission notice shall be
22 | included in all copies or substantial portions of the Software.
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
25 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
27 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
28 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
29 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 | OTHER DEALINGS IN THE SOFTWARE.
31 | """
32 |
33 | __authors__ = ["Chapin Bryce", "Preston Miller"]
34 | __date__ = 20181027
35 | __description__ = "Replicate user input in the console"
36 |
37 |
38 | def main():
39 | args = sys.argv
40 | print('Script:', args[0])
41 | args.pop(0)
42 | for i, argument in enumerate(sys.argv):
43 | print('Argument {}: {}'.format(i, argument))
44 | print('Type: {}'.format(type(argument)))
45 |
46 | if __name__ == '__main__':
47 | main()
48 |
--------------------------------------------------------------------------------
/Chapter03/setupapi_parser.py:
--------------------------------------------------------------------------------
1 | """Third iteration of the setupapi.dev.log parser."""
2 | from __future__ import print_function
3 | import argparse
4 | from io import open
5 | import os
6 | import sys
7 | import usb_lookup
8 |
9 | """
10 | MIT License
11 | Copyright (c) 2018 Chapin Bryce, Preston Miller
12 | Please share comments and questions at:
13 | https://github.com/PythonForensics/Learning-Python-for-Forensics
14 | or email pyforcookbook@gmail.com
15 |
16 | Permission is hereby granted, free of charge, to any person
17 | obtaining a copy of this software and associated documentation
18 | files (the "Software"), to deal in the Software without
19 | restriction, including without limitation the rights to use,
20 | copy, modify, merge, publish, distribute, sublicense, and/or
21 | sell copies of the Software, and to permit persons to whom the
22 | Software is furnished to do so, subject to the following
23 | conditions:
24 |
25 | The above copyright notice and this permission notice shall be
26 | included in all copies or substantial portions of the Software.
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
34 | OTHER DEALINGS IN THE SOFTWARE.
35 | """
36 |
37 | __authors__ = ["Chapin Bryce", "Preston Miller"]
38 | __date__ = 20181027
39 | __description__ = """This scripts reads a Windows 7 Setup API
40 | log and prints USB Devices to the user"""
41 |
42 | def main(in_file, local_usb_ids=None):
43 | """
44 | Main function to handle operation
45 | :param in_file: Str - Path to setupapi log to analyze
46 | :return: None
47 | """
48 |
49 | if os.path.isfile(in_file):
50 | device_information = parse_setupapi(in_file)
51 | usb_ids = prep_usb_lookup(local_usb_ids)
52 | for device in device_information:
53 | parsed_info = parse_device_info(device)
54 | if isinstance(parsed_info, dict):
55 | parsed_info = get_device_names(usb_ids,
56 | parsed_info)
57 | if parsed_info is not None:
58 | print_output(parsed_info)
59 | print('\n\n{} parsed and printed successfully.'.format(
60 | in_file))
61 |
62 | else:
63 | print("Input: {} was not found. Please check your path "
64 | "and permissions.".format(in_file))
65 | sys.exit(1)
66 |
67 |
68 | def parse_setupapi(setup_log):
69 | """
70 | Read data from provided file for Device Install Events for
71 | USB Devices
72 | :param setup_log: str - Path to valid setup api log
73 | :return: tuple of str - Device name and date
74 | """
75 | device_list = list()
76 | unique_list = set()
77 | with open(setup_log) as in_file:
78 | for line in in_file:
79 | lower_line = line.lower()
80 | if 'device install (hardware initiated)' in \
81 | lower_line and ('vid' in lower_line or
82 | 'ven' in lower_line):
83 | device_name = line.split('-')[1].strip()
84 | date = next(in_file).split('start')[1].strip()
85 | if device_name not in unique_list:
86 | device_list.append((device_name, date))
87 | unique_list.add(device_name)
88 |
89 | return device_list
90 |
91 |
92 | def parse_device_info(device_info):
93 | """
94 | Parses Vendor, Product, Revision and UID from a Setup API
95 | entry
96 | :param device_info: string of device information to parse
97 | :return: dictionary of parsed information or original string
98 | if error
99 | """
100 | # Initialize variables
101 | vid = ''
102 | pid = ''
103 | rev = ''
104 | uid = ''
105 |
106 | # Split string into segments on \\
107 | segments = device_info[0].split('\\')
108 |
109 | if 'usb' not in segments[0].lower():
110 | return None
111 | # Eliminate non-USB devices from output
112 | # May hide other storage devices
113 |
114 | for item in segments[1].split('&'):
115 | lower_item = item.lower()
116 | if 'ven' in lower_item or 'vid' in lower_item:
117 | vid = item.split('_', 1)[-1]
118 | elif 'dev' in lower_item or 'pid' in lower_item or \
119 | 'prod' in lower_item:
120 | pid = item.split('_', 1)[-1]
121 | elif 'rev' in lower_item or 'mi' in lower_item:
122 | rev = item.split('_', 1)[-1]
123 |
124 | if len(segments) >= 3:
125 | uid = segments[2].strip(']')
126 |
127 | if vid != '' or pid != '':
128 | return {'Vendor ID': vid.lower(),
129 | 'Product ID': pid.lower(),
130 | 'Revision': rev,
131 | 'UID': uid,
132 | 'First Installation Date': device_info[1]}
133 | # Unable to parse data, returning whole string
134 | return device_info
135 |
136 |
137 | def prep_usb_lookup(local_usb_ids=None):
138 | """
139 | Prepare the lookup of USB devices through accessing the most
140 | recent copy of the database at http://linux-usb.org/usb.ids
141 | or using the provided file and parsing it into a queriable
142 | dictionary format.
143 | """
144 | if local_usb_ids:
145 | usb_file = open(local_usb_ids, encoding='latin1')
146 | else:
147 | usb_file = usb_lookup.get_usb_file()
148 | return usb_lookup.parse_file(usb_file)
149 |
150 |
151 | def get_device_names(usb_dict, device_info):
152 | """
153 | Query `usb_lookup.py` for device information based on VID/PID.
154 | :param usb_dict: Dictionary from usb_lookup.py of known
155 | devices.
156 | :param device_info: Dictionary containing 'Vendor ID' and
157 | 'Product ID' keys and values.
158 | :return: original dictionary with 'Vendor Name' and
159 | 'Product Name' keys and values
160 | """
161 | device_name = usb_lookup.search_key(
162 | usb_dict, [device_info['Vendor ID'],
163 | device_info['Product ID']])
164 |
165 | device_info['Vendor Name'] = device_name[0]
166 | device_info['Product Name'] = device_name[1]
167 |
168 | return device_info
169 |
170 |
171 | def print_output(usb_information):
172 | """
173 | Print formatted information about USB Device
174 | :param usb_information: dictionary containing key/value
175 | data about each device or tuple of device information
176 | :return: None
177 | """
178 | print('{:-^15}'.format(''))
179 |
180 | if isinstance(usb_information, dict):
181 | for key_name, value_name in usb_information.items():
182 | print('{}: {}'.format(key_name, value_name))
183 | elif isinstance(usb_information, tuple):
184 | print('Device: {}'.format(usb_information[0]))
185 | print('Date: {}'.format(usb_information[1]))
186 |
187 | if __name__ == '__main__':
188 | # Run this code if the script is run from the command line.
189 | parser = argparse.ArgumentParser(
190 | description=__description__,
191 | epilog='Built by {}. Version {}'.format(
192 | ", ".join(__authors__), __date__),
193 | formatter_class=argparse.ArgumentDefaultsHelpFormatter
194 | )
195 |
196 | parser.add_argument('IN_FILE',
197 | help='Windows 7 SetupAPI file')
198 | parser.add_argument('--local',
199 | help='Path to local usb.ids file')
200 |
201 | args = parser.parse_args()
202 |
203 | # Run main program
204 | main(args.IN_FILE, args.local)
205 |
--------------------------------------------------------------------------------
/Chapter03/setupapi_parser_v1.py:
--------------------------------------------------------------------------------
1 | """First iteration of the setupapi.dev.log parser."""
2 | from __future__ import print_function
3 | from io import open
4 |
5 | """
6 | MIT License
7 | Copyright (c) 2018 Chapin Bryce, Preston Miller
8 | Please share comments and questions at:
9 | https://github.com/PythonForensics/Learning-Python-for-Forensics
10 | or email pyforcookbook@gmail.com
11 |
12 | Permission is hereby granted, free of charge, to any person
13 | obtaining a copy of this software and associated documentation
14 | files (the "Software"), to deal in the Software without
15 | restriction, including without limitation the rights to use,
16 | copy, modify, merge, publish, distribute, sublicense, and/or
17 | sell copies of the Software, and to permit persons to whom the
18 | Software is furnished to do so, subject to the following
19 | conditions:
20 |
21 | The above copyright notice and this permission notice shall be
22 | included in all copies or substantial portions of the Software.
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
25 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
27 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
28 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
29 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 | OTHER DEALINGS IN THE SOFTWARE.
31 | """
32 |
33 | __authors__ = ["Chapin Bryce", "Preston Miller"]
34 | __date__ = 20181027
35 | __description__ = """This scripts reads a Windows 7 Setup API
36 | log and prints USB Devices to the user"""
37 |
38 |
39 | def main():
40 | """
41 | Primary controller for script.
42 | :return: None
43 | """
44 | # Insert your own path to your sample setupapi.dev.log here.
45 | file_path = 'setupapi.dev.log'
46 |
47 | # Print version information when the script is run
48 | print('='*22)
49 | print('SetupAPI Parser, v', __date__)
50 | print('='*22)
51 | parse_setupapi(file_path)
52 |
53 |
54 | def parse_setupapi(setup_file):
55 | """
56 | Interpret the file
57 | :param setup_file: path to the setupapi.dev.log
58 | :return: None
59 | """
60 | in_file = open(setup_file)
61 | data = in_file.readlines()
62 |
63 | for i, line in enumerate(data):
64 | if 'device install (hardware initiated)' in line.lower():
65 | device_name = data[i].split('-')[1].strip()
66 | date = data[i+1].split('start')[1].strip()
67 | print_output(device_name, date)
68 | in_file.close()
69 |
70 |
71 | def print_output(usb_name, usb_date):
72 | """
73 | Print the information discovered
74 | :param usb_name: String USB Name to print
75 | :param usb_date: String USB Date to print
76 | :return: None
77 | """
78 | print('Device: {}'.format(usb_name))
79 | print('First Install: {}'.format(usb_date))
80 |
81 |
82 | if __name__ == '__main__':
83 | # Run the program
84 | main()
85 |
--------------------------------------------------------------------------------
/Chapter03/setupapi_parser_v2.py:
--------------------------------------------------------------------------------
1 | """Second iteration of the setupapi.dev.log parser."""
2 | from __future__ import print_function
3 | import argparse
4 | from io import open
5 | import os
6 | import sys
7 |
8 | """
9 | MIT License
10 | Copyright (c) 2018 Chapin Bryce, Preston Miller
11 | Please share comments and questions at:
12 | https://github.com/PythonForensics/Learning-Python-for-Forensics
13 | or email pyforcookbook@gmail.com
14 |
15 | Permission is hereby granted, free of charge, to any person
16 | obtaining a copy of this software and associated documentation
17 | files (the "Software"), to deal in the Software without
18 | restriction, including without limitation the rights to use,
19 | copy, modify, merge, publish, distribute, sublicense, and/or
20 | sell copies of the Software, and to permit persons to whom the
21 | Software is furnished to do so, subject to the following
22 | conditions:
23 |
24 | The above copyright notice and this permission notice shall be
25 | included in all copies or substantial portions of the Software.
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
28 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
31 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33 | OTHER DEALINGS IN THE SOFTWARE.
34 | """
35 |
36 | __authors__ = ["Chapin Bryce", "Preston Miller"]
37 | __date__ = 20181027
38 | __description__ = """This scripts reads a Windows 7 Setup API
39 | log and prints USB Devices to the user"""
40 |
41 |
42 | def main(in_file):
43 | """
44 | Main function to handle operation
45 | :param in_file: string path to Windows 7 setupapi.dev.log
46 | :return: None
47 | """
48 | if os.path.isfile(in_file):
49 | print('{:=^22}'.format(''))
50 | print('{} {}'.format('SetupAPI Parser, v', __date__))
51 | print('{:=^22} \n'.format(''))
52 | device_information = parse_setupapi(in_file)
53 | for device in device_information:
54 | print_output(device[0], device[1])
55 | else:
56 | print('Input is not a file.')
57 | sys.exit(1)
58 |
59 |
60 | def parse_setupapi(setup_log):
61 | """
62 | Read data from provided file for Device Install Events for
63 | USB Devices
64 | :param setup_log: str - Path to valid setup api log
65 | :return: list of tuples - Tuples contain device name and date
66 | in that order
67 | """
68 | device_list = list()
69 | with open(setup_log) as in_file:
70 | for line in in_file:
71 | lower_line = line.lower()
72 | # if 'Device Install (Hardware initiated)' in line:
73 | if 'device install (hardware initiated)' in \
74 | lower_line and ('ven' in lower_line or
75 | 'vid' in lower_line):
76 | device_name = line.split('-')[1].strip()
77 |
78 | if 'usb' not in device_name.split(
79 | '\\')[0].lower():
80 | continue
81 | # Remove most non-USB devices
82 | # This can remove records that may be
83 | # relevant so please always validate that
84 | # the data reduction does not remove results
85 | # of interest to you.
86 |
87 | date = next(in_file).split('start')[1].strip()
88 | device_list.append((device_name, date))
89 |
90 | return device_list
91 |
92 |
93 | def print_output(usb_name, usb_date):
94 | """
95 | Print the information discovered
96 | :param usb_name: String USB Name to print
97 | :param usb_date: String USB Date to print
98 | :return: None
99 | """
100 | print('Device: {}'.format(usb_name))
101 | print('First Install: {}\n'.format(usb_date))
102 |
103 |
104 | if __name__ == '__main__':
105 | # Run this code if the script is run from the command line.
106 | parser = argparse.ArgumentParser(
107 | description=__description__,
108 | epilog='Built by {}. Version {}'.format(
109 | ", ".join(__authors__), __date__),
110 | formatter_class=argparse.ArgumentDefaultsHelpFormatter
111 | )
112 |
113 | parser.add_argument('IN_FILE',
114 | help='Windows 7 SetupAPI file')
115 | args = parser.parse_args()
116 |
117 | # Run main program
118 | main(args.IN_FILE)
119 |
--------------------------------------------------------------------------------
/Chapter03/usb_lookup.py:
--------------------------------------------------------------------------------
1 | """Updated USB Lookup script based on the version found in Chapter 2."""
2 | from __future__ import print_function
3 | import argparse
4 | try:
5 | from urllib2 import urlopen
6 | except ImportError:
7 | from urllib.request import urlopen
8 |
9 | """
10 | MIT License
11 | Copyright (c) 2018 Chapin Bryce, Preston Miller
12 | Please share comments and questions at:
13 | https://github.com/PythonForensics/Learning-Python-for-Forensics
14 | or email pyforcookbook@gmail.com
15 |
16 | Permission is hereby granted, free of charge, to any person obtaining a copy
17 | of this software and associated documentation files (the "Software"), to deal
18 | in the Software without restriction, including without limitation the rights
19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20 | copies of the Software, and to permit persons to whom the Software is
21 | furnished to do so, subject to the following conditions:
22 | The above copyright notice and this permission notice shall be included in
23 | all copies or substantial portions of the Software.
24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30 | SOFTWARE.
31 | """
32 |
33 | __authors__ = ["Chapin Bryce", "Preston Miller"]
34 | __date__ = 20181027
35 | __description__ = """Reads Linux-usb.org's USB.ids file and parses into
36 | usable data for parsing VID/PIDs"""
37 |
38 |
39 | def main(vid, pid, ids_file=None):
40 | """
41 | Main function to control operation. Requires arguments passed as VID PID
42 | on the command line. If discovered in data set, the common names will be
43 | printed to stdout
44 | :return: None
45 | """
46 | if ids_file:
47 | usb_file = open(ids_file, encoding='latin1')
48 | else:
49 | usb_file = get_usb_file()
50 | usbs = parse_file(usb_file)
51 | results = search_key(usbs, (vid, pid))
52 | print("Vendor: {}\nProduct: {}".format(results[0], results[1]))
53 |
54 | def get_usb_file():
55 | """
56 | Retrieves USB.ids database from the web.
57 | """
58 | url = 'http://www.linux-usb.org/usb.ids'
59 | return urlopen(url)
60 |
61 |
62 | def parse_file(usb_file):
63 | """
64 | Parses the USB.ids file. If this is run offline, please download the
65 | USB.ids and pass the open file to this function.
66 | ie: parse_file(open('path/to/USB.ids', 'r'))
67 | :return: dictionary of entires for querying
68 | """
69 | usbs = {}
70 | curr_id = ''
71 | for line in usb_file:
72 | if isinstance(line, bytes):
73 | line = line.decode('latin-1')
74 | if line.startswith('#') or line in ('\n', '\t'):
75 | continue
76 | else:
77 | if not line.startswith('\t') and (line[0].isdigit() or
78 | line[0].islower()):
79 | uid, name = get_record(line.strip())
80 | curr_id = uid
81 | usbs[uid] = [name.strip(), {}]
82 | elif line.startswith('\t') and line.count('\t') == 1:
83 | uid, name = get_record(line.strip())
84 | usbs[curr_id][1][uid] = name.strip()
85 | return usbs
86 |
87 |
88 | def get_record(record_line):
89 | """
90 | Split records out by dynamic position. By finding the space, we can
91 | determine the location to split the record for extraction. To learn more
92 | about this, uncomment the print statements and see what the code is
93 | doing behind the scenes!
94 | """
95 | # print("Line: {}".format(record_line))
96 | split = record_line.find(' ')
97 | # print("Split: {}".format(split))
98 | record_id = record_line[:split]
99 | # print("Record ID: ".format(record_id))
100 | record_name = record_line[split + 1:]
101 | # print("Record Name: ".format(record_name))
102 | return record_id, record_name
103 |
104 |
105 | def search_key(usb_dict, ids):
106 | """
107 | Compare provided IDs to the built USB dictionary. If found, it will
108 | return the common name, otherwise returns the string "unknown".
109 | """
110 | vendor_key = ids[0]
111 | product_key = ids[1]
112 |
113 | vendor, vendor_data = usb_dict.get(vendor_key, ['unknown', {}])
114 | product = 'unknown'
115 | if vendor != 'unknown':
116 | product = vendor_data.get(product_key, 'unknown')
117 |
118 | return vendor, product
119 |
120 |
121 | if __name__ == '__main__':
122 | parser = argparse.ArgumentParser(
123 | description=__description__,
124 | epilog="version {} built by {}. MIT License 2018.".format(
125 | __date__, " & ".join(__authors__)),
126 | formatter_class=argparse.ArgumentDefaultsHelpFormatter
127 | )
128 | parser.add_argument('vid', help='Number representing the VID. ie. 0123')
129 | parser.add_argument('pid', help='Number representing the PID. ie. 0123')
130 | parser.add_argument('--ids', '-i', help='Local path to the usb.ids file')
131 | args = parser.parse_args()
132 | main(args.vid, args.pid, args.ids)
133 |
--------------------------------------------------------------------------------
/Chapter04/bitcoin_address_lookup.v1.py:
--------------------------------------------------------------------------------
1 | """First iteration of the Bitcoin JSON transaction parser."""
2 | import argparse
3 | import json
4 | import urllib.request
5 | import unix_converter as unix
6 | import sys
7 |
8 | """
9 | MIT License
10 | Copyright (c) 2018 Chapin Bryce, Preston Miller
11 | Please share comments and questions at:
12 | https://github.com/PythonForensics/Learning-Python-for-Forensics
13 | or email pyforcookbook@gmail.com
14 |
15 | Permission is hereby granted, free of charge, to any person
16 | obtaining a copy of this software and associated documentation
17 | files (the "Software"), to deal in the Software without
18 | restriction, including without limitation the rights to use,
19 | copy, modify, merge, publish, distribute, sublicense, and/or
20 | sell copies of the Software, and to permit persons to whom the
21 | Software is furnished to do so, subject to the following
22 | conditions:
23 |
24 | The above copyright notice and this permission notice shall be
25 | included in all copies or substantial portions of the Software.
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
28 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
31 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33 | OTHER DEALINGS IN THE SOFTWARE.
34 | """
35 |
36 | __author__ = 'Preston Miller & Chapin Bryce'
37 | __date__ = '20180729'
38 | __description__ = """This scripts downloads address transactions
39 | using blockchain.info public APIs"""
40 |
41 |
42 | def main(address):
43 | """
44 | The main function handles coordinating logic
45 | :param address: The Bitcoin Address to lookup
46 | :return: Nothing
47 | """
48 | raw_account = get_address(address)
49 | account = json.loads(raw_account.read())
50 | print_transactions(account)
51 |
52 |
53 | def get_address(address):
54 | """
55 | The get_address function uses the blockchain.info Data API
56 | to pull pull down account information and transactions for
57 | address of interest
58 | :param address: The Bitcoin Address to lookup
59 | :return: The response of the url request
60 | """
61 | url = 'https://blockchain.info/address/{}?format=json'
62 | formatted_url = url.format(address)
63 | try:
64 | return urllib.request.urlopen(formatted_url)
65 | except urllib.error.URLError:
66 | print('Received URL Error for {}'.format(formatted_url))
67 | sys.exit(1)
68 |
69 |
70 | def print_transactions(account):
71 | """
72 | The print_transaction function is responsible for presenting
73 | transaction details to end user.
74 | :param account: The JSON decoded account and transaction data
75 | :return:
76 | """
77 | print_header(account)
78 | print('Transactions')
79 |
80 | for i, tx in enumerate(account['txs']):
81 | print('Transaction #{}'.format(i))
82 | print('Transaction Hash:', tx['hash'])
83 | print('Transaction Date: {}'.format(
84 | unix.unix_converter(tx['time'])))
85 | for output in tx['out']:
86 | inputs = get_inputs(tx)
87 | if len(inputs) > 1:
88 | print('{} --> {} ({:.8f} BTC)'.format(
89 | ' & '.join(inputs), output['addr'],
90 | output['value'] * 10**-8))
91 | else:
92 | print('{} --> {} ({:.8f} BTC)'.format(
93 | ''.join(inputs), output['addr'],
94 | output['value'] * 10**-8))
95 |
96 | print('{:=^22}\n'.format(''))
97 |
98 |
99 | def print_header(account):
100 | """
101 | The print_header function prints overall header information
102 | containing basic address information.
103 | :param account: The JSON decoded account and transaction data
104 | :return: Nothing
105 | """
106 | print('Address:', account['address'])
107 | print('Current Balance: {:.8f} BTC'.format(
108 | account['final_balance'] * 10**-8))
109 | print('Total Sent: {:.8f} BTC'.format(
110 | account['total_sent'] * 10**-8))
111 | print('Total Received: {:.8f} BTC'.format(
112 | account['total_received'] * 10**-8))
113 | print('Number of Transactions:', account['n_tx'])
114 | print('{:=^22}\n'.format(''))
115 |
116 |
117 | def get_inputs(tx):
118 | """
119 | The get_inputs function is a small helper function that returns
120 | input addresses for a given transaction
121 | :param tx: A single instance of a Bitcoin transaction
122 | :return: inputs, a list of inputs
123 | """
124 | inputs = []
125 | for input_addr in tx['inputs']:
126 | inputs.append(input_addr['prev_out']['addr'])
127 | return inputs
128 |
129 | if __name__ == '__main__':
130 | # Run this code if the script is run from the command line.
131 | parser = argparse.ArgumentParser(
132 | description='BTC Address Lookup',
133 | epilog='Developed by ' + __author__ + ' on ' + __date__)
134 |
135 | parser.add_argument('ADDR', help='Bitcoin Address')
136 |
137 | args = parser.parse_args()
138 |
139 | # Print Script Information
140 | print('{:=^22}'.format(''))
141 | print('{}'.format('Bitcoin Address Lookup'))
142 | print('{:=^22} \n'.format(''))
143 |
144 | # Run main program
145 | main(args.ADDR)
146 |
--------------------------------------------------------------------------------
/Chapter04/bitcoin_address_lookup.v2.py:
--------------------------------------------------------------------------------
1 | """Second iteration of the Bitcoin JSON transaction parser."""
2 | import argparse
3 | import json
4 | import logging
5 | import sys
6 | import os
7 | import urllib.request
8 | import unix_converter as unix
9 |
10 | """
11 | MIT License
12 | Copyright (c) 2018 Chapin Bryce, Preston Miller
13 | Please share comments and questions at:
14 | https://github.com/PythonForensics/Learning-Python-for-Forensics
15 | or email pyforcookbook@gmail.com
16 |
17 | Permission is hereby granted, free of charge, to any person
18 | obtaining a copy of this software and associated documentation
19 | files (the "Software"), to deal in the Software without
20 | restriction, including without limitation the rights to use,
21 | copy, modify, merge, publish, distribute, sublicense, and/or
22 | sell copies of the Software, and to permit persons to whom the
23 | Software is furnished to do so, subject to the following
24 | conditions:
25 |
26 | The above copyright notice and this permission notice shall be
27 | included in all copies or substantial portions of the Software.
28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
29 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
30 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
31 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
32 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
33 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
34 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
35 | OTHER DEALINGS IN THE SOFTWARE.
36 | """
37 |
38 | __author__ = 'Preston Miller & Chapin Bryce'
39 | __date__ = '20180729'
40 | __description__ = """This scripts downloads address transactions
41 | using blockchain.info public APIs"""
42 |
43 |
44 | def main(address):
45 | """
46 | The main function handles coordinating logic
47 | :param address: The Bitcoin Address to lookup
48 | :return: Nothing
49 | """
50 | logging.info('Initiated program for {} address'.format(
51 | address))
52 | logging.info(
53 | 'Obtaining JSON structured data from blockchain.info')
54 | raw_account = get_address(address)
55 | account = json.loads(raw_account.read())
56 | print_transactions(account)
57 |
58 |
59 | def get_address(address):
60 | """
61 | The get_address function uses the blockchain.info Data API
62 | to pull pull down account information and transactions for
63 | address of interest
64 | :param address: The Bitcoin Address to lookup
65 | :return: The response of the url request
66 | """
67 | url = 'https://blockchain.info/address/{}?format=json'
68 | formatted_url = url.format(address)
69 | try:
70 | return urllib.request.urlopen(formatted_url)
71 | except urllib.error.URLError as e:
72 | logging.error('URL Error for {}'.format(formatted_url))
73 | if hasattr(e, 'code') and hasattr(e, 'headers'):
74 | logging.debug('{}: {}'.format(e.code, e.reason))
75 | logging.debug('{}'.format(e.headers))
76 | print('Received URL Error for {}'.format(formatted_url))
77 | logging.info('Program exiting...')
78 | sys.exit(1)
79 |
80 |
81 | def print_transactions(account):
82 | """
83 | The print_transaction function is responsible for presenting
84 | transaction details to end user.
85 | :param account: The JSON decoded account and transaction data
86 | :return: Nothing
87 | """
88 | logging.info(
89 | 'Printing account and transaction data to console.')
90 | print_header(account)
91 | print('Transactions')
92 | for i, tx in enumerate(account['txs']):
93 | print('Transaction #{}'.format(i))
94 | print('Transaction Hash:', tx['hash'])
95 | print('Transaction Date: {}'.format(
96 | unix.unix_converter(tx['time'])))
97 | for output in tx['out']:
98 | inputs = get_inputs(tx)
99 | if len(inputs) > 1:
100 | print('{} --> {} ({:.8f} BTC)'.format(
101 | ' & '.join(inputs), output['addr'],
102 | output['value'] * 10**-8))
103 | elif len(inputs) == 1:
104 | print('{} --> {} ({:.8f} BTC)'.format(
105 | ''.join(inputs), output['addr'],
106 | output['value'] * 10**-8))
107 | else:
108 | logging.warn(
109 | 'Detected 0 inputs for transaction {}').format(
110 | tx['hash'])
111 | print('Detected 0 inputs for transaction.')
112 |
113 | print('{:=^22}\n'.format(''))
114 |
115 |
116 | def print_header(account):
117 | """
118 | The print_header function prints overall header information
119 | containing basic address information.
120 | :param account: The JSON decoded account and transaction data
121 | :return: Nothing
122 | """
123 | print('Address:', account['address'])
124 | print('Current Balance: {:.8f} BTC'.format(
125 | account['final_balance'] * 10**-8))
126 | print('Total Sent: {:.8f} BTC'.format(
127 | account['total_sent'] * 10**-8))
128 | print('Total Received: {:.8f} BTC'.format(
129 | account['total_received'] * 10**-8))
130 | print('Number of Transactions:', account['n_tx'])
131 | print('{:=^22}\n'.format(''))
132 |
133 |
134 | def get_inputs(tx):
135 | """
136 | The get_inputs function is a small helper function that returns
137 | input addresses for a given transaction
138 | :param tx: A single instance of a Bitcoin transaction
139 | :return: inputs, a list of inputs
140 | """
141 | inputs = []
142 | for input_addr in tx['inputs']:
143 | inputs.append(input_addr['prev_out']['addr'])
144 | return inputs
145 |
146 | if __name__ == '__main__':
147 | # Run this code if the script is run from the command line.
148 | parser = argparse.ArgumentParser(
149 | description='BTC Address Lookup',
150 | epilog='Developed by ' + __author__ + ' on ' + __date__)
151 |
152 | parser.add_argument('ADDR', help='Bitcoin Address')
153 | parser.add_argument('-l', help="""Specify log directory.
154 | Defaults to current working directory.""")
155 |
156 | args = parser.parse_args()
157 |
158 | # Set up Log
159 | if args.l:
160 | if not os.path.exists(args.l):
161 | os.makedirs(args.l)
162 | log_path = os.path.join(args.l, 'btc_addr_lookup.log')
163 | else:
164 | log_path = 'btc_addr_lookup.log'
165 | logging.basicConfig(
166 | filename=log_path, level=logging.DEBUG,
167 | format='%(asctime)s | %(levelname)s | %(message)s',
168 | filemode='w')
169 |
170 | logging.info('Starting Bitcoin Address Lookup')
171 | logging.debug('System ' + sys.platform)
172 | logging.debug('Version ' + sys.version)
173 |
174 | # Print Script Information
175 | print('{:=^22}'.format(''))
176 | print('{}'.format('Bitcoin Address Lookup'))
177 | print('{:=^22} \n'.format(''))
178 |
179 | # Run main program
180 | main(args.ADDR)
181 |
--------------------------------------------------------------------------------
/Chapter04/book.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Learning Python Forensics",
3 | "authors": "Preston Miller & Chapin Bryce",
4 | "publisher": "Packt Publishing",
5 | "pageCount": 500,
6 | "numberOfChapters": 13,
7 | "chapters":
8 | [
9 | {
10 | "chapterNumber": 1,
11 | "chapterTitle": "Now for Something Completely Different",
12 | "pageCount": 30
13 | },
14 | {
15 | "chapterNumber": 2,
16 | "chapterTitle": "Python Fundamentals",
17 | "pageCount": 25
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/Chapter04/book.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Preston Miller & Chapin Bryce
4 |
5 |
6 | 1
7 | Now for Something Completely Different
8 | 30
9 |
10 |
11 | 2
12 | Python Fundamentals
13 | 25
14 |
15 |
16 | 13
17 | 500
18 | Packt Publishing
19 | Learning Python for Forensics
20 |
--------------------------------------------------------------------------------
/Chapter04/unix_converter.py:
--------------------------------------------------------------------------------
1 | """UNIX timestamp converter script."""
2 | from __future__ import print_function
3 |
4 | """
5 | MIT License
6 | Copyright (c) 2018 Chapin Bryce, Preston Miller
7 | Please share comments and questions at:
8 | https://github.com/PythonForensics/Learning-Python-for-Forensics
9 | or email pyforcookbook@gmail.com
10 |
11 | Permission is hereby granted, free of charge, to any person
12 | obtaining a copy of this software and associated documentation
13 | files (the "Software"), to deal in the Software without
14 | restriction, including without limitation the rights to use,
15 | copy, modify, merge, publish, distribute, sublicense, and/or
16 | sell copies of the Software, and to permit persons to whom the
17 | Software is furnished to do so, subject to the following
18 | conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 | OTHER DEALINGS IN THE SOFTWARE.
30 | """
31 |
32 | import datetime
33 |
34 | __author__ = 'Preston Miller & Chapin Bryce'
35 | __date__ = '20180729'
36 | __description__ = """Convert unix formatted timestamps (seconds
37 | since Epoch [1970-01-01 00:00:00]) to human readable"""
38 |
39 | def main():
40 | unix_ts = int(raw_input('Unix timestamp to convert:\n>> '))
41 | print(unix_converter(unix_ts))
42 |
43 | def unix_converter(timestamp):
44 | date_ts = datetime.datetime.utcfromtimestamp(timestamp)
45 | return date_ts.strftime('%m/%d/%Y %I:%M:%S %p')
46 |
47 | if __name__ == '__main__':
48 | main()
49 |
--------------------------------------------------------------------------------
/Chapter06/Neguhe Qrag.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter06/Neguhe Qrag.bin
--------------------------------------------------------------------------------
/Chapter06/Writers/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.append(os.path.join(os.path.dirname(__file__)))
4 |
5 | import csv_writer
6 | import xlsx_writer
7 |
8 | """
9 | MIT License
10 | Copyright (c) 2018 Chapin Bryce, Preston Miller
11 | Please share comments and questions at:
12 | https://github.com/PythonForensics/Learning-Python-for-Forensics
13 | or email pyforcookbook@gmail.com
14 |
15 | Permission is hereby granted, free of charge, to any person
16 | obtaining a copy of this software and associated documentation
17 | files (the "Software"), to deal in the Software without
18 | restriction, including without limitation the rights to use,
19 | copy, modify, merge, publish, distribute, sublicense, and/or
20 | sell copies of the Software, and to permit persons to whom the
21 | Software is furnished to do so, subject to the following
22 | conditions:
23 |
24 | The above copyright notice and this permission notice shall be
25 | included in all copies or substantial portions of the Software.
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
28 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
31 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33 | OTHER DEALINGS IN THE SOFTWARE.
34 | """
--------------------------------------------------------------------------------
/Chapter06/Writers/csv_writer.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import sys
3 | if sys.version_info[0] == 2:
4 | import unicodecsv as csv
5 | elif sys.version_info[0] == 3:
6 | import csv
7 | from datetime import datetime, timedelta
8 | import logging
9 |
10 | """
11 | MIT License
12 | Copyright (c) 2018 Chapin Bryce, Preston Miller
13 | Please share comments and questions at:
14 | https://github.com/PythonForensics/Learning-Python-for-Forensics
15 | or email pyforcookbook@gmail.com
16 |
17 | Permission is hereby granted, free of charge, to any person
18 | obtaining a copy of this software and associated documentation
19 | files (the "Software"), to deal in the Software without
20 | restriction, including without limitation the rights to use,
21 | copy, modify, merge, publish, distribute, sublicense, and/or
22 | sell copies of the Software, and to permit persons to whom the
23 | Software is furnished to do so, subject to the following
24 | conditions:
25 |
26 | The above copyright notice and this permission notice shall be
27 | included in all copies or substantial portions of the Software.
28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
29 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
30 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
31 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
32 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
33 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
34 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
35 | OTHER DEALINGS IN THE SOFTWARE.
36 | """
37 |
38 | def csv_writer(data, out_file):
39 | """
40 | The csv_writer function writes the parsed UA data to a csv
41 | file
42 | :param data: the list of lists containing parsed UA data
43 | :param out_file: the desired output directory and filename
44 | for the csv file
45 | :return: Nothing
46 | """
47 | print('[+] Writing CSV output.')
48 | logging.info('Writing CSV to ' + out_file + '.')
49 | headers = ['ID', 'Name', 'Path', 'Session ID', 'Count',
50 | 'Last Used Date (UTC)', 'Focus Time (ms)', 'Focus Count']
51 |
52 | if sys.version_info[0] == 2:
53 | csvfile = open(out_file, "wb")
54 | elif sys.version_info[0] == 3:
55 | csvfile = open(out_file, "w", newline='',
56 | encoding='utf-8')
57 |
58 | with csvfile:
59 | writer = csv.DictWriter(csvfile, fieldnames=headers,
60 | extrasaction='ignore')
61 | # Writes the header from list supplied to fieldnames
62 | # keyword argument
63 | writer.writeheader()
64 |
65 | for i, dictionary in enumerate(data):
66 | # Insert the 'ID' value to each dictionary in the
67 | # list. Add 1 to start ID at 1 instead of 0.
68 | dictionary['ID'] = i + 1
69 | # Convert the FILETIME object in the fourth index to
70 | # human readable value
71 | dictionary['Last Used Date (UTC)'] = file_time(
72 | dictionary['Last Used Date (UTC)'])
73 | writer.writerow(dictionary)
74 |
75 | csvfile.flush()
76 | csvfile.close()
77 | msg = 'Completed writing CSV file. Program exiting successfully.'
78 | print('[*]', msg)
79 | logging.info(msg)
80 |
81 |
82 | def file_time(ft):
83 | """
84 | The fileTime function converts Windows FILETIME objects into
85 | human readable value
86 | :param ft: the FILETIME to convert
87 | :return: date_str, the human readable datetime value
88 | """
89 | if ft is not None and ft != 0:
90 | return datetime(1601, 1, 1) + timedelta(microseconds=ft / 10)
91 | else:
92 | return 0
93 |
--------------------------------------------------------------------------------
/Chapter06/rot13.py:
--------------------------------------------------------------------------------
1 | """Prototype ROT-13 encoder and decoder."""
2 | from __future__ import print_function
3 |
4 | """
5 | MIT License
6 | Copyright (c) 2018 Chapin Bryce, Preston Miller
7 | Please share comments and questions at:
8 | https://github.com/PythonForensics/Learning-Python-for-Forensics
9 | or email pyforcookbook@gmail.com
10 |
11 | Permission is hereby granted, free of charge, to any person
12 | obtaining a copy of this software and associated documentation
13 | files (the "Software"), to deal in the Software without
14 | restriction, including without limitation the rights to use,
15 | copy, modify, merge, publish, distribute, sublicense, and/or
16 | sell copies of the Software, and to permit persons to whom the
17 | Software is furnished to do so, subject to the following
18 | conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 | OTHER DEALINGS IN THE SOFTWARE.
30 | """
31 |
32 | def rot_code(data):
33 | """
34 | The rot_code function encodes/decodes data using string
35 | indexing
36 | :param data: A string
37 | :return: The rot-13 encoded/decoded string
38 | """
39 | rot_chars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
40 | 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
41 | 'u', 'v', 'w', 'x', 'y', 'z']
42 |
43 | substitutions = []
44 |
45 | # Walk through each individual character
46 | for c in data:
47 |
48 | # Walk through each individual character
49 | if c.isupper():
50 |
51 | try:
52 | # Find the position of the character in
53 | # rot_chars list
54 | index = rot_chars.index(c.lower())
55 | except ValueError:
56 | substitutions.append(c)
57 | continue
58 |
59 | # Calculate the relative index that is 13
60 | # characters away from the index
61 | substitutions.append(
62 | (rot_chars[(index-13)]).upper())
63 |
64 | else:
65 |
66 | try:
67 | # Find the position of the character in
68 | # rot_chars list
69 | index = rot_chars.index(c)
70 | except ValueError:
71 | substitutions.append(c)
72 | continue
73 |
74 | substitutions.append(rot_chars[((index-13))])
75 |
76 | return ''.join(substitutions)
77 |
78 | if __name__ == '__main__':
79 | print(rot_code('Jul, EBG-13?'))
80 |
--------------------------------------------------------------------------------
/Chapter06/simplexlsx.v1.py:
--------------------------------------------------------------------------------
1 | """First iteration of a simple XLSX writer."""
2 | import xlsxwriter
3 | from datetime import datetime
4 |
5 | """
6 | MIT License
7 | Copyright (c) 2018 Chapin Bryce, Preston Miller
8 | Please share comments and questions at:
9 | https://github.com/PythonForensics/Learning-Python-for-Forensics
10 | or email pyforcookbook@gmail.com
11 |
12 | Permission is hereby granted, free of charge, to any person
13 | obtaining a copy of this software and associated documentation
14 | files (the "Software"), to deal in the Software without
15 | restriction, including without limitation the rights to use,
16 | copy, modify, merge, publish, distribute, sublicense, and/or
17 | sell copies of the Software, and to permit persons to whom the
18 | Software is furnished to do so, subject to the following
19 | conditions:
20 |
21 | The above copyright notice and this permission notice shall be
22 | included in all copies or substantial portions of the Software.
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
25 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
27 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
28 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
29 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 | OTHER DEALINGS IN THE SOFTWARE.
31 | """
32 |
33 | school_data = [['Department', 'Students', 'Cumulative GPA',
34 | 'Final Date'],
35 | ['Computer Science', 235, 3.44,
36 | datetime(2015, 7, 23, 18, 0, 0)],
37 | ['Chemistry', 201, 3.26,
38 | datetime(2015, 7, 25, 9, 30, 0)],
39 | ['Forensics', 99, 3.8,
40 | datetime(2015, 7, 23, 9, 30, 0)],
41 | ['Astronomy', 115, 3.21,
42 | datetime(2015, 7, 19, 15, 30, 0)]]
43 |
44 |
45 | def write_xlsx(data):
46 | """
47 | The write_xlsx function creates an XLSX spreadsheet from a
48 | list of lists
49 | :param data: A list of lists to be written in the spreadsheet
50 | :return: Nothing
51 | """
52 | workbook = xlsxwriter.Workbook('MyWorkbook.xlsx')
53 | main_sheet = workbook.add_worksheet('MySheet')
54 |
55 | date_format = workbook.add_format(
56 | {'num_format': 'mm/dd/yy hh:mm:ss AM/PM'})
57 |
58 | for i, entry in enumerate(data):
59 | if i == 0:
60 | main_sheet.write(i, 0, entry[0])
61 | main_sheet.write(i, 1, entry[1])
62 | main_sheet.write(i, 2, entry[2])
63 | main_sheet.write(i, 3, entry[3])
64 | else:
65 | main_sheet.write(i, 0, entry[0])
66 | main_sheet.write_number(i, 1, entry[1])
67 | main_sheet.write_number(i, 2, entry[2])
68 | main_sheet.write_datetime(i, 3, entry[3], date_format)
69 |
70 | workbook.close()
71 |
72 |
73 | write_xlsx(school_data)
74 |
--------------------------------------------------------------------------------
/Chapter06/simplexlsx.v2.py:
--------------------------------------------------------------------------------
1 | """Second iteration of a simple XLSX writer."""
2 |
3 | import xlsxwriter
4 | from datetime import datetime
5 |
6 | """
7 | MIT License
8 | Copyright (c) 2018 Chapin Bryce, Preston Miller
9 | Please share comments and questions at:
10 | https://github.com/PythonForensics/Learning-Python-for-Forensics
11 | or email pyforcookbook@gmail.com
12 |
13 | Permission is hereby granted, free of charge, to any person
14 | obtaining a copy of this software and associated documentation
15 | files (the "Software"), to deal in the Software without
16 | restriction, including without limitation the rights to use,
17 | copy, modify, merge, publish, distribute, sublicense, and/or
18 | sell copies of the Software, and to permit persons to whom the
19 | Software is furnished to do so, subject to the following
20 | conditions:
21 |
22 | The above copyright notice and this permission notice shall be
23 | included in all copies or substantial portions of the Software.
24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
26 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
28 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
29 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
30 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
31 | OTHER DEALINGS IN THE SOFTWARE.
32 | """
33 |
34 | school_data = [['Computer Science', 235, 3.44,
35 | datetime(2015, 7, 23, 18, 0, 0)],
36 | ['Chemistry', 201, 3.26,
37 | datetime(2015, 7, 25, 9, 30, 0)],
38 | ['Forensics', 99, 3.8,
39 | datetime(2015, 7, 23, 9, 30, 0)],
40 | ['Astronomy', 115, 3.21,
41 | datetime(2015, 7, 19, 15, 30, 0)]]
42 |
43 |
44 | def write_xlsx(data):
45 | """
46 | The write_xlsx function creates an XLSX spreadsheet from a
47 | list of lists
48 | :param data: A list of lists to be written in the spreadsheet
49 | :return: Nothing
50 | """
51 | workbook = xlsxwriter.Workbook('MyWorkbook.xlsx')
52 | main_sheet = workbook.add_worksheet('MySheet')
53 |
54 | date_format = workbook.add_format(
55 | {'num_format': 'mm/dd/yy hh:mm:ss AM/PM'})
56 | length = str(len(data) + 1)
57 |
58 | main_sheet.add_table(('A1:D' + length),
59 | {'data': data,
60 | 'columns': [{'header': 'Department'}, {'header': 'Students'},
61 | {'header': 'Cumulative GPA'},
62 | {'header': 'Final Date', 'format': date_format}]})
63 |
64 | workbook.close()
65 |
66 |
67 | write_xlsx(school_data)
68 |
--------------------------------------------------------------------------------
/Chapter06/simplexlsx.v3.py:
--------------------------------------------------------------------------------
1 | """Third iteration of a simple XLSX writer."""
2 |
3 | import xlsxwriter
4 | from datetime import datetime
5 |
6 | """
7 | MIT License
8 | Copyright (c) 2018 Chapin Bryce, Preston Miller
9 | Please share comments and questions at:
10 | https://github.com/PythonForensics/Learning-Python-for-Forensics
11 | or email pyforcookbook@gmail.com
12 |
13 | Permission is hereby granted, free of charge, to any person
14 | obtaining a copy of this software and associated documentation
15 | files (the "Software"), to deal in the Software without
16 | restriction, including without limitation the rights to use,
17 | copy, modify, merge, publish, distribute, sublicense, and/or
18 | sell copies of the Software, and to permit persons to whom the
19 | Software is furnished to do so, subject to the following
20 | conditions:
21 |
22 | The above copyright notice and this permission notice shall be
23 | included in all copies or substantial portions of the Software.
24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
26 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
28 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
29 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
30 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
31 | OTHER DEALINGS IN THE SOFTWARE.
32 | """
33 |
34 | school_data = [['Computer Science', 235, 3.44,
35 | datetime(2015, 7, 23, 18, 0, 0)],
36 | ['Chemistry', 201, 3.26,
37 | datetime(2015, 7, 25, 9, 30, 0)],
38 | ['Forensics', 99, 3.8,
39 | datetime(2015, 7, 23, 9, 30, 0)],
40 | ['Astronomy', 115, 3.21,
41 | datetime(2015, 7, 19, 15, 30, 0)]]
42 |
43 |
44 | def write_xlsx(data):
45 | """
46 | The write_xlsx function creates an XLSX spreadsheet from a
47 | list of lists
48 | :param data: A list of lists to be written in the spreadsheet
49 | :return: Nothing
50 | """
51 | workbook = xlsxwriter.Workbook('MyWorkbook.xlsx')
52 | main_sheet = workbook.add_worksheet('MySheet')
53 |
54 | date_format = workbook.add_format(
55 | {'num_format': 'mm/dd/yy hh:mm:ss AM/PM'})
56 | length = str(len(data) + 1)
57 |
58 | main_sheet.add_table(('A1:D' + length),
59 | {'data': data,
60 | 'columns': [{'header': 'Department'}, {'header': 'Students'},
61 | {'header': 'Cumulative GPA'},
62 | {'header': 'Final Date',
63 | 'format': date_format}]})
64 |
65 | department_grades = workbook.add_chart({'type':'column'})
66 | department_grades.set_title(
67 | {'name':'Department and Grade distribution'})
68 | department_grades.add_series(
69 | {'categories':'=MySheet!$A$2:$A$5',
70 | 'values':'=MySheet!$C$2:$C$5'})
71 | main_sheet.insert_chart('A8', department_grades)
72 | workbook.close()
73 |
74 |
75 | write_xlsx(school_data)
76 |
--------------------------------------------------------------------------------
/Chapter07/TEST_DATA_README.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | This file describes the contents of the other files in this directory. Each of the
4 | files named `file_#` were created using the below bash for loop on a macOS
5 | system:
6 |
7 | ```bash
8 | for i in 1 2 3; do dd if=/dev/urandom of="file_$i" bs=1K count=200; done
9 | ```
10 |
11 | These files are treated as "originals" and variations of these files were
12 | generated to demonstrate how ssdeep can compare files through different types
13 | of modifications.
14 |
15 | The below descriptions identify what changes were made to each of the
16 | files. For simplicity, we append the letter "a" after each modified version
17 | so it is clear which original they relate to and that they are a modified
18 | version of the original.
19 |
20 | All offsets are decimal, not hexadecimal.
21 |
22 | ## `file_1a`
23 |
24 | from decimal offset 27000 of `file_1`, 24000 bytes were selected and moved
25 | to offset 74000 (50000)
26 |
27 | ## `file_2a`
28 |
29 | from decimal offset 50000 of `file_2`, the same 24000 bytes from `file_1` were
30 | inserted
31 |
32 | ## `file_3a`
33 |
34 | from decimal offset 0 of `file_3`, 3500 bytes were removed.
35 |
36 |
--------------------------------------------------------------------------------
/Chapter07/hashing_example.py:
--------------------------------------------------------------------------------
1 | """Sample script to hash large files effiently."""
2 | import argparse
3 | import hashlib
4 |
5 | """
6 | MIT License
7 | Copyright (c) 2018 Chapin Bryce, Preston Miller
8 | Please share comments and questions at:
9 | https://github.com/PythonForensics/Learning-Python-for-Forensics
10 | or email pyforcookbook@gmail.com
11 |
12 | Permission is hereby granted, free of charge, to any person
13 | obtaining a copy of this software and associated documentation
14 | files (the "Software"), to deal in the Software without
15 | restriction, including without limitation the rights to use,
16 | copy, modify, merge, publish, distribute, sublicense, and/or
17 | sell copies of the Software, and to permit persons to whom the
18 | Software is furnished to do so, subject to the following
19 | conditions:
20 |
21 | The above copyright notice and this permission notice shall be
22 | included in all copies or substantial portions of the Software.
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
25 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
27 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
28 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
29 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 | OTHER DEALINGS IN THE SOFTWARE.
31 | """
32 |
33 | HASH_LIBS = ['md5', 'sha1', 'sha256', 'sha512']
34 | BUFFER_SIZE = 1024**3
35 |
36 | parser = argparse.ArgumentParser()
37 | parser.add_argument("FILE", help="File to hash")
38 | parser.add_argument("-a", "--algorithm",
39 | help="Hash algorithm to use", choices=HASH_LIBS,
40 | default="sha512")
41 | args = parser.parse_args()
42 |
43 | alg = getattr(hashlib, args.algorithm)()
44 |
45 | with open(args.FILE, 'rb') as input_file:
46 |
47 | buffer_data = input_file.read(BUFFER_SIZE)
48 | while buffer_data:
49 | alg.update(buffer_data)
50 | buffer_data = input_file.read(BUFFER_SIZE)
51 |
52 | print(alg.hexdigest())
53 |
--------------------------------------------------------------------------------
/Chapter07/ssdeep_python.py:
--------------------------------------------------------------------------------
1 | """Example script that uses the ssdeep python bindings."""
2 | import argparse
3 | import logging
4 | import os
5 | import sys
6 |
7 | import ssdeep
8 |
9 | """
10 | MIT License
11 | Copyright (c) 2018 Chapin Bryce, Preston Miller
12 | Please share comments and questions at:
13 | https://github.com/PythonForensics/Learning-Python-for-Forensics
14 | or email pyforcookbook@gmail.com
15 |
16 | Permission is hereby granted, free of charge, to any person
17 | obtaining a copy of this software and associated documentation
18 | files (the "Software"), to deal in the Software without
19 | restriction, including without limitation the rights to use,
20 | copy, modify, merge, publish, distribute, sublicense, and/or
21 | sell copies of the Software, and to permit persons to whom the
22 | Software is furnished to do so, subject to the following
23 | conditions:
24 |
25 | The above copyright notice and this permission notice shall be
26 | included in all copies or substantial portions of the Software.
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
34 | OTHER DEALINGS IN THE SOFTWARE.
35 | """
36 |
37 | __authors__ = ["Chapin Bryce", "Preston Miller"]
38 | __date__ = 20181027
39 | __description__ = '''Compare known file to another file or files
40 | in a directory using ssdeep.'''
41 |
42 | # Argument handling constants
43 | OUTPUT_OPTS = ['txt', 'json', 'csv']
44 | logger = logging.getLogger(__file__)
45 |
46 |
47 | def main(known_file, comparison, output_type):
48 | """
49 | The main function handles the main operations of the script
50 | :param known_file: path to known file
51 | :param comparison: path to look for similar files
52 | :param output_type: type of output to provide
53 | :return: None
54 | """
55 |
56 | # Check output formats
57 | if output_type not in OUTPUT_OPTS:
58 | logger.error(
59 | "Unsupported output format '{}' selected. Please "
60 | "use one of {}".format(
61 | output_type, ", ".join(OUTPUT_OPTS)))
62 | sys.exit(2)
63 | elif output_type == 'csv':
64 | # Special handling for CSV headers
65 | print('"similarity","known_file","known_hash",'
66 | '"comp_file","comp_hash"')
67 |
68 | # Check provided file paths
69 | known_file = os.path.abspath(known_file)
70 | comparison = os.path.abspath(comparison)
71 |
72 | # Generate ssdeep signature for known file
73 | if not os.path.exists(known_file):
74 | logger.error("Error - path {} not found".format(
75 | comparison))
76 | sys.exit(1)
77 |
78 | known_hash = ssdeep.hash_from_file(known_file)
79 |
80 | # Generate and test ssdeep signature for comparison file(s)
81 | if os.path.isdir(comparison):
82 | # Process files in folders
83 | for root, _, files in os.walk(comparison):
84 | for f in files:
85 | file_entry = os.path.join(root, f)
86 | comp_hash = ssdeep.hash_from_file(file_entry)
87 | comp_val = ssdeep.compare(known_hash, comp_hash)
88 | output(known_file, known_hash,
89 | file_entry, comp_hash,
90 | comp_val, output_type)
91 |
92 | elif os.path.isfile(comparison):
93 | # Process a single file
94 | comp_hash = ssdeep.hash_from_file(comparison)
95 | comp_val = ssdeep.compare(known_hash, comp_hash)
96 | output(known_file, known_hash, file_entry, comp_hash,
97 | comp_val, output_type)
98 | else:
99 | logger.error("Error - path {} not found".format(
100 | comparison))
101 | sys.exit(1)
102 |
103 |
104 | def output(known_file, known_hash, comp_file, comp_hash, comp_val,
105 | output_type='txt'):
106 | """Write the output of the script in the specified format
107 | :param sigval (str): Calculated hash
108 | :param filename (str): name of the file processed
109 | :param output_type (str): Formatter to use for output
110 | """
111 | comp_val = str(comp_val)
112 | if output_type == 'txt':
113 | msg = "{similarity} - {known_file} {known_hash} | "
114 | msg += "{comp_file} {comp_hash}"
115 | elif output_type == 'json':
116 | msg = '{{"similarity": {similarity}, "known_file": '
117 | msg += '"{known_file}", "known_hash": "{known_hash}", '
118 | msg += '"comparison_file": "{comp_file}", '
119 | msg += '"comparison_hash": "{comp_hash}"}}'
120 | elif output_type == 'csv':
121 | msg = '"{similarity}","{known_file}","{known_hash}"'
122 | msg += '"{comp_file}","{comp_hash}"'
123 | else:
124 | raise NotImplementedError(
125 | "Unsupported output type: {}".format(output_type))
126 |
127 | print(msg.format(
128 | similarity=comp_val,
129 | known_file=known_file,
130 | known_hash=known_hash,
131 | comp_file=comp_file,
132 | comp_hash=comp_hash))
133 |
134 | if __name__ == '__main__':
135 | parser = argparse.ArgumentParser(
136 | description=__description__,
137 | epilog='Built by {}. Version {}'.format(
138 | ", ".join(__authors__), __date__),
139 | formatter_class=argparse.ArgumentDefaultsHelpFormatter
140 | )
141 | parser.add_argument('KNOWN',
142 | help='Path to known file to use to compare')
143 | parser.add_argument('COMPARISON',
144 | help='Path to file or directory to compare to known. '
145 | 'Will recurse through all sub directories')
146 | parser.add_argument('-o', '--output-type',
147 | help='Format of output.', choices=OUTPUT_OPTS,
148 | default="txt")
149 | parser.add_argument('-l', help='specify log file path',
150 | default="./")
151 |
152 | args = parser.parse_args()
153 |
154 | if args.l:
155 | if not os.path.exists(args.l):
156 | os.makedirs(args.l)
157 | log_path = os.path.join(args.l, 'ssdeep_python.log')
158 | else:
159 | log_path = 'ssdeep_python.log'
160 |
161 |
162 | logger.setLevel(logging.DEBUG)
163 | msg_fmt = logging.Formatter("%(asctime)-15s %(funcName)-20s"
164 | "%(levelname)-8s %(message)s")
165 | strhndl = logging.StreamHandler(sys.stderr) # Set to stderr
166 | strhndl.setFormatter(fmt=msg_fmt)
167 | fhndl = logging.FileHandler(log_path, mode='a')
168 | fhndl.setFormatter(fmt=msg_fmt)
169 | logger.addHandler(strhndl)
170 | logger.addHandler(fhndl)
171 |
172 | logger.info('Starting SSDeep Python v. {}'.format(__date__))
173 | logger.debug('System ' + sys.platform)
174 | logger.debug('Version ' + sys.version.replace("\n", " "))
175 |
176 | logger.info('Script Starting')
177 | main(args.KNOWN, args.COMPARISON, args.output_type)
178 | logger.info('Script Completed')
179 |
--------------------------------------------------------------------------------
/Chapter07/test_data/file_1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter07/test_data/file_1
--------------------------------------------------------------------------------
/Chapter07/test_data/file_1a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter07/test_data/file_1a
--------------------------------------------------------------------------------
/Chapter07/test_data/file_2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter07/test_data/file_2
--------------------------------------------------------------------------------
/Chapter07/test_data/file_2a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter07/test_data/file_2a
--------------------------------------------------------------------------------
/Chapter07/test_data/file_3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter07/test_data/file_3
--------------------------------------------------------------------------------
/Chapter07/test_data/file_3a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter07/test_data/file_3a
--------------------------------------------------------------------------------
/Chapter08/img_42.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter08/img_42.jpg
--------------------------------------------------------------------------------
/Chapter08/metadata_parser.py:
--------------------------------------------------------------------------------
1 | """EXIF, ID3, and Office Metadata parser."""
2 | from __future__ import print_function
3 | import argparse
4 | import os
5 | import sys
6 | import logging
7 |
8 | import plugins
9 | import writers
10 |
11 | """
12 | MIT License
13 | Copyright (c) 2018 Chapin Bryce, Preston Miller
14 | Please share comments and questions at:
15 | https://github.com/PythonForensics/Learning-Python-for-Forensics
16 | or email pyforcookbook@gmail.com
17 |
18 | Permission is hereby granted, free of charge, to any person
19 | obtaining a copy of this software and associated documentation
20 | files (the "Software"), to deal in the Software without
21 | restriction, including without limitation the rights to use,
22 | copy, modify, merge, publish, distribute, sublicense, and/or
23 | sell copies of the Software, and to permit persons to whom the
24 | Software is furnished to do so, subject to the following
25 | conditions:
26 |
27 | The above copyright notice and this permission notice shall be
28 | included in all copies or substantial portions of the Software.
29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
30 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
31 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
32 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
33 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
34 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
35 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
36 | OTHER DEALINGS IN THE SOFTWARE.
37 | """
38 |
39 | __author__ = 'Preston Miller & Chapin Bryce'
40 | __date__ = '20181125'
41 | __description__ = ('This scripts handles processing and output '
42 | 'of various embedded metadata files')
43 |
44 |
45 | def main(input_dir, output_dir):
46 | """
47 | The main function generates a file listing, sends files to be
48 | processed, and output written.
49 | :param input_dir: The input directory to scan for suported
50 | embedded metadata containing files
51 | :param output_dir: The output directory to write metadata
52 | reports to
53 | :return: Nothing.
54 | """
55 | # Create lists to store each supported embedded metadata
56 | # before writing to output
57 | exif_metadata = []
58 | office_metadata = []
59 | id3_metadata = []
60 |
61 | # Walk through list of files
62 | msg = 'Generating file listing and running plugins.'
63 | print('[+]', msg)
64 | logging.info(msg)
65 | for root, subdir, files in os.walk(input_dir, topdown=True):
66 | for file_name in files:
67 | current_file = os.path.join(root, file_name)
68 | ext = os.path.splitext(current_file)[1].lower()
69 |
70 | # PLUGINS
71 | if ext == '.jpeg' or ext == '.jpg':
72 | try:
73 | ex_metadata, exif_headers = plugins.exif_parser.exif_parser(
74 | current_file)
75 | exif_metadata.append(ex_metadata)
76 | except TypeError:
77 | print(('[-] File signature mismatch. '
78 | 'Continuing to next file.'))
79 | logging.error((('JPG & TIFF File Signature '
80 | 'check failed for ' + current_file)))
81 | continue
82 |
83 | elif ext == '.docx' or ext == '.pptx' or ext == '.xlsx':
84 | try:
85 | of_metadata, office_headers = plugins.office_parser.office_parser(
86 | current_file)
87 | office_metadata.append(of_metadata)
88 | except TypeError:
89 | print(('[-] File signature mismatch. '
90 | 'Continuing to next file.'))
91 | logging.error((('DOCX, XLSX, & PPTX File '
92 | 'Signature check failed for ' + current_file))
93 | )
94 | continue
95 |
96 | elif ext == '.mp3':
97 | try:
98 | id_metadata, id3_headers = plugins.id3_parser.id3_parser(
99 | current_file)
100 | id3_metadata.append(id_metadata)
101 | except TypeError:
102 | print(('[-] File signature mismatch. '
103 | 'Continuing to next file.'))
104 | logging.error((('MP3 File Signature check '
105 | 'failed for ' + current_file)))
106 | continue
107 |
108 | # WRITERS
109 | msg = 'Writing output to ' + output_dir
110 | print('[+]', msg)
111 | logging.info(msg)
112 |
113 | if len(exif_metadata) > 0:
114 | writers.kml_writer.kml_writer(exif_metadata,
115 | output_dir, 'exif_metadata.kml')
116 | writers.csv_writer.csv_writer(exif_metadata, exif_headers,
117 | output_dir, 'exif_metadata.csv')
118 |
119 | if len(office_metadata) > 0:
120 | writers.csv_writer.csv_writer(office_metadata,
121 | office_headers, output_dir, 'office_metadata.csv')
122 |
123 | if len(id3_metadata) > 0:
124 | writers.csv_writer.csv_writer(id3_metadata, id3_headers,
125 | output_dir, 'id3_metadata.csv')
126 |
127 | msg = 'Program completed successfully -- exiting..'
128 | print('[*]', msg)
129 | logging.info(msg)
130 |
131 | if __name__ == '__main__':
132 |
133 | parser = argparse.ArgumentParser(description=__description__,
134 | epilog='Developed by ' +
135 | __author__ + ' on ' +
136 | __date__)
137 | parser.add_argument('INPUT_DIR', help='Input Directory')
138 | parser.add_argument('OUTPUT_DIR', help='Output Directory')
139 | parser.add_argument('-l', help='File path of log file.')
140 | args = parser.parse_args()
141 |
142 | if args.l:
143 | if not os.path.exists(args.l):
144 | os.makedirs(args.l)
145 | log_path = os.path.join(args.l, 'metadata_parser.log')
146 | else:
147 | log_path = 'metadata_parser.log'
148 | logging.basicConfig(filename=log_path, level=logging.DEBUG,
149 | format=('%(asctime)s | %(levelname)s | '
150 | '%(message)s'), filemode='a')
151 |
152 | logging.info('Starting Metadata_Parser')
153 | logging.debug('System ' + sys.platform)
154 | logging.debug('Version ' + sys.version)
155 |
156 | if not os.path.exists(args.OUTPUT_DIR):
157 | os.makedirs(args.OUTPUT_DIR)
158 |
159 | if(os.path.exists(args.INPUT_DIR) and
160 | os.path.isdir(args.INPUT_DIR)):
161 | main(args.INPUT_DIR, args.OUTPUT_DIR)
162 | else:
163 | msg =('Supplied input directory does not exist or is'
164 | 'not a directory')
165 | print('[-]', msg)
166 | logging.error(msg)
167 | sys.exit(1)
--------------------------------------------------------------------------------
/Chapter08/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.append(os.path.join(os.path.dirname(__file__)))
4 |
5 | import exif_parser
6 | import id3_parser
7 | import office_parser
8 |
9 | """
10 | MIT License
11 | Copyright (c) 2018 Chapin Bryce, Preston Miller
12 | Please share comments and questions at:
13 | https://github.com/PythonForensics/Learning-Python-for-Forensics
14 | or email pyforcookbook@gmail.com
15 |
16 | Permission is hereby granted, free of charge, to any person
17 | obtaining a copy of this software and associated documentation
18 | files (the "Software"), to deal in the Software without
19 | restriction, including without limitation the rights to use,
20 | copy, modify, merge, publish, distribute, sublicense, and/or
21 | sell copies of the Software, and to permit persons to whom the
22 | Software is furnished to do so, subject to the following
23 | conditions:
24 |
25 | The above copyright notice and this permission notice shall be
26 | included in all copies or substantial portions of the Software.
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
34 | OTHER DEALINGS IN THE SOFTWARE.
35 | """
--------------------------------------------------------------------------------
/Chapter08/plugins/id3_parser.py:
--------------------------------------------------------------------------------
1 | import os
2 | from time import gmtime, strftime
3 |
4 | from mutagen import mp3, id3
5 |
6 | import processors
7 |
8 | """
9 | MIT License
10 | Copyright (c) 2018 Chapin Bryce, Preston Miller
11 | Please share comments and questions at:
12 | https://github.com/PythonForensics/Learning-Python-for-Forensics
13 | or email pyforcookbook@gmail.com
14 |
15 | Permission is hereby granted, free of charge, to any person
16 | obtaining a copy of this software and associated documentation
17 | files (the "Software"), to deal in the Software without
18 | restriction, including without limitation the rights to use,
19 | copy, modify, merge, publish, distribute, sublicense, and/or
20 | sell copies of the Software, and to permit persons to whom the
21 | Software is furnished to do so, subject to the following
22 | conditions:
23 |
24 | The above copyright notice and this permission notice shall be
25 | included in all copies or substantial portions of the Software.
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
28 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
31 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33 | OTHER DEALINGS IN THE SOFTWARE.
34 | """
35 |
36 |
37 | def id3_parser(filename):
38 | """
39 | The id3_parser function confirms the file type and sends it to
40 | be processed.
41 | :param filename: name of the file potentially containing exif
42 | metadata.
43 | :return: A dictionary from get_tags, containing the embedded
44 | EXIF metadata.
45 | """
46 |
47 | # MP3 signatures
48 | signatures = ['494433']
49 | if processors.utility.check_header(
50 | filename, signatures, 3) == True:
51 | return get_tags(filename)
52 | else:
53 | print(('File signature does not match known '
54 | 'MP3 signatures.'))
55 | raise TypeError(('File signature does not match '
56 | 'MP3 object.'))
57 |
58 |
59 | def get_tags(filename):
60 | """
61 | The get_tags function extracts the ID3 metadata from the data
62 | object.
63 | :param filename: the path and name to the data object.
64 | :return: tags and headers, tags is a dictionary containing ID3
65 | metadata and headers are the order of keys for the CSV output.
66 | """
67 |
68 | # Set up CSV headers
69 | header = ['Path', 'Name', 'Size', 'Filesystem CTime',
70 | 'Filesystem MTime', 'Title', 'Subtitle', 'Artist', 'Album',
71 | 'Album/Artist', 'Length (Sec)', 'Year', 'Category',
72 | 'Track Number', 'Comments', 'Publisher', 'Bitrate',
73 | 'Sample Rate', 'Encoding', 'Channels', 'Audio Layer']
74 | tags = {}
75 | tags['Path'] = filename
76 | tags['Name'] = os.path.basename(filename)
77 | tags['Size'] = processors.utility.convert_size(
78 | os.path.getsize(filename))
79 | tags['Filesystem CTime'] = strftime('%m/%d/%Y %H:%M:%S',
80 | gmtime(os.path.getctime(filename)))
81 | tags['Filesystem MTime'] = strftime('%m/%d/%Y %H:%M:%S',
82 | gmtime(os.path.getmtime(filename)))
83 |
84 | # MP3 Specific metadata
85 | audio = mp3.MP3(filename)
86 | if 'TENC' in audio.keys():
87 | tags['Encoding'] = audio['TENC'][0]
88 | tags['Bitrate'] = audio.info.bitrate
89 | tags['Channels'] = audio.info.channels
90 | tags['Audio Layer'] = audio.info.layer
91 | tags['Length (Sec)'] = audio.info.length
92 | tags['Sample Rate'] = audio.info.sample_rate
93 |
94 | # ID3 embedded metadata tags
95 | id = id3.ID3(filename)
96 | if 'TPE1' in id.keys():
97 | tags['Artist'] = id['TPE1'][0]
98 | if 'TRCK' in id.keys():
99 | tags['Track Number'] = id['TRCK'][0]
100 | if 'TIT3' in id.keys():
101 | tags['Subtitle'] = id['TIT3'][0]
102 | if 'COMM::eng' in id.keys():
103 | tags['Comments'] = id['COMM::eng'][0]
104 | if 'TDRC' in id.keys():
105 | tags['Year'] = id['TDRC'][0]
106 | if 'TALB' in id.keys():
107 | tags['Album'] = id['TALB'][0]
108 | if 'TIT2' in id.keys():
109 | tags['Title'] = id['TIT2'][0]
110 | if 'TCON' in id.keys():
111 | tags['Category'] = id['TCON'][0]
112 | if 'TPE2' in id.keys():
113 | tags['Album/Artist'] = id['TPE2'][0]
114 | if 'TPUB' in id.keys():
115 | tags['Publisher'] = id['TPUB'][0]
116 |
117 | return tags, header
118 |
--------------------------------------------------------------------------------
/Chapter08/plugins/office_parser.py:
--------------------------------------------------------------------------------
1 | import zipfile
2 | import os
3 | from time import gmtime, strftime
4 |
5 | from lxml import etree
6 | import processors
7 |
8 | """
9 | MIT License
10 | Copyright (c) 2018 Chapin Bryce, Preston Miller
11 | Please share comments and questions at:
12 | https://github.com/PythonForensics/Learning-Python-for-Forensics
13 | or email pyforcookbook@gmail.com
14 |
15 | Permission is hereby granted, free of charge, to any person
16 | obtaining a copy of this software and associated documentation
17 | files (the "Software"), to deal in the Software without
18 | restriction, including without limitation the rights to use,
19 | copy, modify, merge, publish, distribute, sublicense, and/or
20 | sell copies of the Software, and to permit persons to whom the
21 | Software is furnished to do so, subject to the following
22 | conditions:
23 |
24 | The above copyright notice and this permission notice shall be
25 | included in all copies or substantial portions of the Software.
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
28 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
31 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33 | OTHER DEALINGS IN THE SOFTWARE.
34 | """
35 |
36 |
37 | def office_parser(filename):
38 | """
39 | The office_parser function confirms the file type and sends it
40 | to be processed.
41 | :param filename: name of the file potentially containing
42 | embedded metadata.
43 | :return: A dictionary from get_tags, containing the embedded
44 | metadata.
45 | """
46 |
47 | # DOCX, XLSX, and PPTX signatures
48 | signatures = ['504b030414000600']
49 | if processors.utility.check_header(
50 | filename, signatures, 8) == True:
51 | return get_tags(filename)
52 | else:
53 | print(('File signature does not match known '
54 | 'signatures.'))
55 | raise TypeError(('File signature does not match '
56 | 'Office objects.'))
57 |
58 |
59 | def get_tags(filename):
60 | """
61 | The get_tags function extracts the office metadata from the
62 | data object.
63 | :param filename: the path and name to the data object.
64 | :return: tags and headers, tags is a dictionary containing
65 | office metadata and headers are the order of keys for the CSV
66 | output.
67 | """
68 |
69 | # Set up CSV headers
70 | headers = ['Path', 'Name', 'Size', 'Filesystem CTime',
71 | 'Filesystem MTime', 'Title', 'Author(s)','Create Date',
72 | 'Modify Date', 'Last Modified By Date', 'Subject', 'Keywords',
73 | 'Description', 'Category', 'Status', 'Revision',
74 | 'Edit Time (Min)', 'Page Count', 'Word Count',
75 | 'Character Count', 'Line Count',
76 | 'Paragraph Count', 'Slide Count', 'Note Count',
77 | 'Hidden Slide Count', 'Company', 'Hyperlink Base']
78 |
79 | # Create a ZipFile class from the input object
80 | # This allows us to read or write to the 'Zip archive'
81 | zf = zipfile.ZipFile(filename)
82 |
83 | # These two XML files contain the embedded metadata of
84 | # interest
85 | try:
86 | core = etree.fromstring(zf.read('docProps/core.xml'))
87 | app = etree.fromstring(zf.read('docProps/app.xml'))
88 | except KeyError as e:
89 | assert Warning(e)
90 | return {}, headers
91 |
92 | tags = {}
93 | tags['Path'] = filename
94 | tags['Name'] = os.path.basename(filename)
95 | tags['Size'] = processors.utility.convert_size(
96 | os.path.getsize(filename))
97 | tags['Filesystem CTime'] = strftime('%m/%d/%Y %H:%M:%S',
98 | gmtime(os.path.getctime(filename)))
99 | tags['Filesystem MTime'] = strftime('%m/%d/%Y %H:%M:%S',
100 | gmtime(os.path.getmtime(filename)))
101 |
102 | # Core Tags
103 |
104 | for child in core.iterchildren():
105 |
106 | if 'title' in child.tag:
107 | tags['Title'] = child.text
108 | if 'subject' in child.tag:
109 | tags['Subject'] = child.text
110 | if 'creator' in child.tag:
111 | tags['Author(s)'] = child.text
112 | if 'keywords' in child.tag:
113 | tags['Keywords'] = child.text
114 | if 'description' in child.tag:
115 | tags['Description'] = child.text
116 | if 'lastModifiedBy' in child.tag:
117 | tags['Last Modified By Date'] = child.text
118 | if 'created' in child.tag:
119 | tags['Create Date'] = child.text
120 | if 'modified' in child.tag:
121 | tags['Modify Date'] = child.text
122 | if 'category' in child.tag:
123 | tags['Category'] = child.text
124 | if 'contentStatus' in child.tag:
125 | tags['Status'] = child.text
126 |
127 | if (filename.endswith('.docx') or
128 | filename.endswith('.pptx')):
129 | if 'revision' in child.tag:
130 | tags['Revision'] = child.text
131 |
132 | # App Tags
133 | for child in app.iterchildren():
134 |
135 | if filename.endswith('.docx'):
136 | if 'TotalTime' in child.tag:
137 | tags['Edit Time (Min)'] = child.text
138 | if 'Pages' in child.tag:
139 | tags['Page Count'] = child.text
140 | if 'Words' in child.tag:
141 | tags['Word Count'] = child.text
142 | if 'Characters' in child.tag:
143 | tags['Character Count'] = child.text
144 | if 'Lines' in child.tag:
145 | tags['Line Count'] = child.text
146 | if 'Paragraphs' in child.tag:
147 | tags['Paragraph Count'] = child.text
148 | if 'Company' in child.tag:
149 | tags['Company'] = child.text
150 | if 'HyperlinkBase' in child.tag:
151 | tags['Hyperlink Base'] = child.text
152 |
153 | elif filename.endswith('.pptx'):
154 | if 'TotalTime' in child.tag:
155 | tags['Edit Time (Min)'] = child.text
156 | if 'Words' in child.tag:
157 | tags['Word Count'] = child.text
158 | if 'Paragraphs' in child.tag:
159 | tags['Paragraph Count'] = child.text
160 | if 'Slides' in child.tag:
161 | tags['Slide Count'] = child.text
162 | if 'Notes' in child.tag:
163 | tags['Note Count'] = child.text
164 | if 'HiddenSlides' in child.tag:
165 | tags['Hidden Slide Count'] = child.text
166 | if 'Company' in child.tag:
167 | tags['Company'] = child.text
168 | if 'HyperlinkBase' in child.tag:
169 | tags['Hyperlink Base'] = child.text
170 | else:
171 | if 'Company' in child.tag:
172 | tags['Company'] = child.text
173 | if 'HyperlinkBase' in child.tag:
174 | tags['Hyperlink Base'] = child.text
175 |
176 | return tags, headers
177 |
--------------------------------------------------------------------------------
/Chapter08/processors/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.append(os.path.join(os.path.dirname(__file__)))
4 |
5 | import utility
6 |
7 | """
8 | MIT License
9 | Copyright (c) 2018 Chapin Bryce, Preston Miller
10 | Please share comments and questions at:
11 | https://github.com/PythonForensics/Learning-Python-for-Forensics
12 | or email pyforcookbook@gmail.com
13 |
14 | Permission is hereby granted, free of charge, to any person
15 | obtaining a copy of this software and associated documentation
16 | files (the "Software"), to deal in the Software without
17 | restriction, including without limitation the rights to use,
18 | copy, modify, merge, publish, distribute, sublicense, and/or
19 | sell copies of the Software, and to permit persons to whom the
20 | Software is furnished to do so, subject to the following
21 | conditions:
22 |
23 | The above copyright notice and this permission notice shall be
24 | included in all copies or substantial portions of the Software.
25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
27 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
29 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
30 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
31 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
32 | OTHER DEALINGS IN THE SOFTWARE.
33 | """
--------------------------------------------------------------------------------
/Chapter08/processors/utility.py:
--------------------------------------------------------------------------------
1 | import binascii
2 | import logging
3 |
4 | """
5 | MIT License
6 | Copyright (c) 2018 Chapin Bryce, Preston Miller
7 | Please share comments and questions at:
8 | https://github.com/PythonForensics/Learning-Python-for-Forensics
9 | or email pyforcookbook@gmail.com
10 |
11 | Permission is hereby granted, free of charge, to any person
12 | obtaining a copy of this software and associated documentation
13 | files (the "Software"), to deal in the Software without
14 | restriction, including without limitation the rights to use,
15 | copy, modify, merge, publish, distribute, sublicense, and/or
16 | sell copies of the Software, and to permit persons to whom the
17 | Software is furnished to do so, subject to the following
18 | conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 | OTHER DEALINGS IN THE SOFTWARE.
30 | """
31 |
32 |
33 | def check_header(filename, headers, size):
34 | """
35 | The check_header function reads a supplied size of the file
36 | and checks against known signatures to determine the file
37 | type.
38 | :param filename: The name of the file.
39 | :param headers: A list of known file signatures for the
40 | file type(s).
41 | :param size: The amount of data to read from the file for
42 | signature verification.
43 | :return: Boolean, True if the signatures match;
44 | otherwise, False.
45 | """
46 | with open(filename, 'rb') as infile:
47 | header = infile.read(size)
48 | hex_header = binascii.hexlify(header).decode('utf-8')
49 | for signature in headers:
50 | if hex_header == signature:
51 | return True
52 | else:
53 | pass
54 | logging.warn(('The signature for {} ({}) does not match '
55 | 'known signatures: {}').format(
56 | filename, hex_header, headers))
57 | return False
58 |
59 | def convert_size(size):
60 | """
61 | The convert_size function converts an integer representing
62 | bytes into a human-readable format.
63 | :param size: The size in bytes of a file
64 | :return: The human-readable size.
65 | """
66 | sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
67 | index = 0
68 | while size > 1024:
69 | size /= 1024.
70 | index += 1
71 | return '{:.2f} {}'.format(size, sizes[index])
72 |
73 |
--------------------------------------------------------------------------------
/Chapter08/writers/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.append(os.path.join(os.path.dirname(__file__)))
4 |
5 | import kml_writer
6 | import csv_writer
7 |
8 | """
9 | MIT License
10 | Copyright (c) 2018 Chapin Bryce, Preston Miller
11 | Please share comments and questions at:
12 | https://github.com/PythonForensics/Learning-Python-for-Forensics
13 | or email pyforcookbook@gmail.com
14 |
15 | Permission is hereby granted, free of charge, to any person
16 | obtaining a copy of this software and associated documentation
17 | files (the "Software"), to deal in the Software without
18 | restriction, including without limitation the rights to use,
19 | copy, modify, merge, publish, distribute, sublicense, and/or
20 | sell copies of the Software, and to permit persons to whom the
21 | Software is furnished to do so, subject to the following
22 | conditions:
23 |
24 | The above copyright notice and this permission notice shall be
25 | included in all copies or substantial portions of the Software.
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
28 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
31 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33 | OTHER DEALINGS IN THE SOFTWARE.
34 | """
--------------------------------------------------------------------------------
/Chapter08/writers/csv_writer.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import sys
3 | import os
4 | if sys.version_info[0] == 2:
5 | import unicodecsv as csv
6 | elif sys.version_info[0] == 3:
7 | import csv
8 | import logging
9 |
10 |
11 | """
12 | MIT License
13 | Copyright (c) 2018 Chapin Bryce, Preston Miller
14 | Please share comments and questions at:
15 | https://github.com/PythonForensics/Learning-Python-for-Forensics
16 | or email pyforcookbook@gmail.com
17 |
18 | Permission is hereby granted, free of charge, to any person
19 | obtaining a copy of this software and associated documentation
20 | files (the "Software"), to deal in the Software without
21 | restriction, including without limitation the rights to use,
22 | copy, modify, merge, publish, distribute, sublicense, and/or
23 | sell copies of the Software, and to permit persons to whom the
24 | Software is furnished to do so, subject to the following
25 | conditions:
26 |
27 | The above copyright notice and this permission notice shall be
28 | included in all copies or substantial portions of the Software.
29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
30 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
31 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
32 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
33 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
34 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
35 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
36 | OTHER DEALINGS IN THE SOFTWARE.
37 | """
38 |
39 |
40 | def csv_writer(output_data, headers, output_dir, output_name):
41 | """
42 | The csv_writer function uses the csv DictWriter module to
43 | write the list of dictionaries. The DictWriter can take
44 | a fieldnames argument, as a list, which represents the
45 | desired order of columns.
46 | :param output_data: The list of dictionaries containing
47 | embedded metadata.
48 | :param headers: A list of keys in the dictionary that
49 | represent the desired order of columns in the output.
50 | :param output_dir: The folder to write the output CSV to.
51 | :param output_name: The name of the output CSV.
52 | :return:
53 | """
54 | msg = 'Writing ' + output_name + ' CSV output.'
55 | print('[+]', msg)
56 | logging.info(msg)
57 |
58 | out_file = os.path.join(output_dir, output_name)
59 |
60 | if sys.version_info[0] == 2:
61 | csvfile = open(out_file, "wb")
62 | elif sys.version_info[0] == 3:
63 | csvfile = open(out_file, "w", newline='',
64 | encoding='utf-8')
65 |
66 | with csvfile:
67 | # We use DictWriter instead of Writer to write
68 | # dictionaries to CSV.
69 | writer = csv.DictWriter(csvfile, fieldnames=headers)
70 |
71 | # Writerheader writes the header based on the supplied
72 | # headers object
73 | writer.writeheader()
74 | for dictionary in output_data:
75 | if dictionary:
76 | writer.writerow(dictionary)
77 |
--------------------------------------------------------------------------------
/Chapter08/writers/kml_writer.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import os
3 | import logging
4 |
5 | import simplekml
6 |
7 | """
8 | MIT License
9 | Copyright (c) 2018 Chapin Bryce, Preston Miller
10 | Please share comments and questions at:
11 | https://github.com/PythonForensics/Learning-Python-for-Forensics
12 | or email pyforcookbook@gmail.com
13 |
14 | Permission is hereby granted, free of charge, to any person
15 | obtaining a copy of this software and associated documentation
16 | files (the "Software"), to deal in the Software without
17 | restriction, including without limitation the rights to use,
18 | copy, modify, merge, publish, distribute, sublicense, and/or
19 | sell copies of the Software, and to permit persons to whom the
20 | Software is furnished to do so, subject to the following
21 | conditions:
22 |
23 | The above copyright notice and this permission notice shall be
24 | included in all copies or substantial portions of the Software.
25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
27 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
29 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
30 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
31 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
32 | OTHER DEALINGS IN THE SOFTWARE.
33 | """
34 |
35 |
36 | def kml_writer(output_data, output_dir, output_name):
37 | """
38 | The kml_writer function writes JPEG and TIFF EXIF GPS data to
39 | a Google Earth KML file. This file can be opened
40 | in Google Earth and will use the GPS coordinates to create
41 | 'pins' on the map of the taken photo's location.
42 | :param output_data: The embedded EXIF metadata to be written
43 | :param output_dir: The output directory to write the KML file.
44 | :param output_name: The name of the output KML file.
45 | :return:
46 | """
47 | msg = 'Writing ' + output_name + ' KML output.'
48 | print('[+]', msg)
49 | logging.info(msg)
50 | # Instantiate a Kml object and pass along the output filename
51 | kml = simplekml.Kml(name=output_name)
52 | for exif in output_data:
53 | if ('Latitude' in exif.keys() and
54 | 'Latitude Reference' in exif.keys() and
55 | 'Longitude Reference' in exif.keys() and
56 | 'Longitude' in exif.keys()):
57 |
58 | if 'Original Date' in exif.keys():
59 | dt = exif['Original Date']
60 | else:
61 | dt = 'N/A'
62 |
63 | if exif['Latitude Reference'] == 'S':
64 | latitude = '-' + exif['Latitude']
65 | else:
66 | latitude = exif['Latitude']
67 |
68 | if exif['Longitude Reference'] == 'W':
69 | longitude = '-' + exif['Longitude']
70 | else:
71 | longitude = exif['Longitude']
72 |
73 | kml.newpoint(name=exif['Name'],
74 | description='Originally Created: ' + dt,
75 | coords=[(longitude, latitude)])
76 | else:
77 | pass
78 | kml.save(os.path.join(output_dir, output_name))
--------------------------------------------------------------------------------
/Chapter11/Dockerfile:
--------------------------------------------------------------------------------
1 | # This Dockerfile was created for use with Learning Python for
2 | # Forensics, Second Edition. It is intended to configure the
3 | # environment to run the PST Indexing script.
4 |
5 | # Install tested base image
6 | FROM ubuntu:18.04
7 |
8 | # Create and navigate to working directory
9 | RUN ["mkdir", "/opt/book"]
10 | WORKDIR /opt/book
11 |
12 | # Install Dependencies
13 | RUN ["apt-get", "update"]
14 | RUN ["apt-get", "install", "-y", "python-dev", "python-pip", "lighttpd"]
15 | EXPOSE 80/tcp
16 |
17 | # Copy across the libpff and pypff built library, v. 20180812, fecb691
18 | COPY --chown=root:root docker_libs/ /
19 | RUN ["ldconfig"]
20 |
21 | # Copy across scripts
22 | COPY "pst_indexer.py" "/opt/book"
23 | COPY "stats_template.html" "/opt/book"
24 |
25 | # Pip install tested dependency versions
26 | RUN ["pip", "install", "unicodecsv==0.14.1", "jinja2==2.10"]
--------------------------------------------------------------------------------
/Chapter11/docker_libs/usr/lib/python2.7/dist-packages/pypff.a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter11/docker_libs/usr/lib/python2.7/dist-packages/pypff.a
--------------------------------------------------------------------------------
/Chapter11/docker_libs/usr/lib/python2.7/dist-packages/pypff.la:
--------------------------------------------------------------------------------
1 | # pypff.la - a libtool library file
2 | # Generated by libtool (GNU libtool) 2.4.6 Debian-2.4.6-2
3 | #
4 | # Please DO NOT delete this file!
5 | # It is necessary for linking the library.
6 |
7 | # The name that we can dlopen(3).
8 | dlname='pypff.so'
9 |
10 | # Names of this library.
11 | library_names='pypff.so pypff.so pypff.so'
12 |
13 | # The name of the static archive.
14 | old_library='pypff.a'
15 |
16 | # Linker flags that cannot go in dependency_libs.
17 | inherited_linker_flags=''
18 |
19 | # Libraries that this one depends upon.
20 | dependency_libs=' -L/usr/lib/python2.7/config-x86_64-linux-gnu -L/usr/lib -lpython2.7 -lutil -lm /usr/local/lib/libpff.la -lpthread -ldl'
21 |
22 | # Names of additional weak libraries provided by this library
23 | weak_library_names=''
24 |
25 | # Version information for pypff.
26 | current=0
27 | age=0
28 | revision=0
29 |
30 | # Is this an already installed library?
31 | installed=yes
32 |
33 | # Should we warn about portability when linking against -modules?
34 | shouldnotlink=yes
35 |
36 | # Files to dlopen/dlpreopen
37 | dlopen=''
38 | dlpreopen=''
39 |
40 | # Directory that this library needs to be installed in:
41 | libdir='/usr/local/lib/python2.7/dist-packages'
42 |
--------------------------------------------------------------------------------
/Chapter11/docker_libs/usr/lib/python2.7/dist-packages/pypff.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter11/docker_libs/usr/lib/python2.7/dist-packages/pypff.so
--------------------------------------------------------------------------------
/Chapter11/docker_libs/usr/local/lib/libpff.a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter11/docker_libs/usr/local/lib/libpff.a
--------------------------------------------------------------------------------
/Chapter11/docker_libs/usr/local/lib/libpff.la:
--------------------------------------------------------------------------------
1 | # libpff.la - a libtool library file
2 | # Generated by libtool (GNU libtool) 2.4.6 Debian-2.4.6-2
3 | #
4 | # Please DO NOT delete this file!
5 | # It is necessary for linking the library.
6 |
7 | # The name that we can dlopen(3).
8 | dlname='libpff.so.1'
9 |
10 | # Names of this library.
11 | library_names='libpff.so.1.0.0 libpff.so.1 libpff.so'
12 |
13 | # The name of the static archive.
14 | old_library='libpff.a'
15 |
16 | # Linker flags that cannot go in dependency_libs.
17 | inherited_linker_flags=''
18 |
19 | # Libraries that this one depends upon.
20 | dependency_libs=' -lpthread -ldl'
21 |
22 | # Names of additional weak libraries provided by this library
23 | weak_library_names=''
24 |
25 | # Version information for libpff.
26 | current=1
27 | age=0
28 | revision=0
29 |
30 | # Is this an already installed library?
31 | installed=yes
32 |
33 | # Should we warn about portability when linking against -modules?
34 | shouldnotlink=no
35 |
36 | # Files to dlopen/dlpreopen
37 | dlopen=''
38 | dlpreopen=''
39 |
40 | # Directory that this library needs to be installed in:
41 | libdir='/usr/local/lib'
42 |
--------------------------------------------------------------------------------
/Chapter11/docker_libs/usr/local/lib/libpff.so.1.0.0:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter11/docker_libs/usr/local/lib/libpff.so.1.0.0
--------------------------------------------------------------------------------
/Chapter12/places.sqlite-wal:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PythonForensics/Learning-Python-for-Forensics-Second-Edition/01b348bc831055c3571f8ef203e75d733beac907/Chapter12/places.sqlite-wal
--------------------------------------------------------------------------------
/Chapter13/chapter_13/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.append(os.path.join(os.path.dirname(__file__)))
4 |
5 | import wal_crawler
6 | import setupapi
7 | import userassist
8 | import exif
9 | import id3
10 | import office
11 | import pst_indexer
12 |
13 | """
14 | MIT License
15 | Copyright (c) 2018 Chapin Bryce, Preston Miller
16 | Please share comments and questions at:
17 | https://github.com/PythonForensics/Learning-Python-for-Forensics
18 | or email pyforcookbook@gmail.com
19 |
20 | Permission is hereby granted, free of charge, to any person
21 | obtaining a copy of this software and associated documentation
22 | files (the "Software"), to deal in the Software without
23 | restriction, including without limitation the rights to use,
24 | copy, modify, merge, publish, distribute, sublicense, and/or
25 | sell copies of the Software, and to permit persons to whom the
26 | Software is furnished to do so, subject to the following
27 | conditions:
28 |
29 | The above copyright notice and this permission notice shall be
30 | included in all copies or substantial portions of the Software.
31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
32 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
33 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
34 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
35 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
36 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
37 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
38 | OTHER DEALINGS IN THE SOFTWARE.
39 | """
--------------------------------------------------------------------------------
/Chapter13/chapter_13/plugins/helper/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.append(os.path.join(os.path.dirname(__file__)))
4 |
5 | import utility
6 | import usb_lookup
7 |
8 | """
9 | MIT License
10 | Copyright (c) 2018 Chapin Bryce, Preston Miller
11 | Please share comments and questions at:
12 | https://github.com/PythonForensics/Learning-Python-for-Forensics
13 | or email pyforcookbook@gmail.com
14 |
15 | Permission is hereby granted, free of charge, to any person
16 | obtaining a copy of this software and associated documentation
17 | files (the "Software"), to deal in the Software without
18 | restriction, including without limitation the rights to use,
19 | copy, modify, merge, publish, distribute, sublicense, and/or
20 | sell copies of the Software, and to permit persons to whom the
21 | Software is furnished to do so, subject to the following
22 | conditions:
23 |
24 | The above copyright notice and this permission notice shall be
25 | included in all copies or substantial portions of the Software.
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
28 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
31 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33 | OTHER DEALINGS IN THE SOFTWARE.
34 | """
--------------------------------------------------------------------------------
/Chapter13/chapter_13/plugins/helper/usb_lookup.py:
--------------------------------------------------------------------------------
1 | """Updated USB Lookup script based on the version found in Chapter 2."""
2 | from __future__ import print_function
3 | import argparse
4 | import sys
5 | try:
6 | from urllib2 import urlopen
7 | except ImportError:
8 | from urllib.request import urlopen
9 |
10 | """
11 | MIT License
12 | Copyright (c) 2018 Chapin Bryce, Preston Miller
13 | Please share comments and questions at:
14 | https://github.com/PythonForensics/Learning-Python-for-Forensics
15 | or email pyforcookbook@gmail.com
16 |
17 | Permission is hereby granted, free of charge, to any person obtaining a copy
18 | of this software and associated documentation files (the "Software"), to deal
19 | in the Software without restriction, including without limitation the rights
20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21 | copies of the Software, and to permit persons to whom the Software is
22 | furnished to do so, subject to the following conditions:
23 | The above copyright notice and this permission notice shall be included in
24 | all copies or substantial portions of the Software.
25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31 | SOFTWARE.
32 | """
33 |
34 | def main(vid, pid, ids_file=None):
35 | """
36 | Main function to control operation. Requires arguments passed as VID PID
37 | on the command line. If discovered in data set, the common names will be
38 | printed to stdout
39 | :return: None
40 | """
41 | if ids_file:
42 | usb_file = open(ids_file, encoding='latin1')
43 | else:
44 | usb_file = get_usb_file()
45 | usbs = parse_file(usb_file)
46 | results = search_key(usbs, (vid, pid))
47 | print("Vendor: {}\nProduct: {}".format(results[0], results[1]))
48 |
49 |
50 | def get_usb_file():
51 | """
52 | Retrieves USB.ids database from the web.
53 | """
54 | url = 'http://www.linux-usb.org/usb.ids'
55 | return urlopen(url)
56 |
57 |
58 | def parse_file(usb_file):
59 | """
60 | Parses the USB.ids file. If this is run offline, please
61 | download the USB.ids and pass the open file to this function.
62 | ie: parse_file(open('path/to/USB.ids', 'r'))
63 | :return: dictionary of entires for querying
64 | """
65 | usbs = {}
66 | curr_id = ''
67 | for line in usb_file:
68 | if isinstance(line, bytes):
69 | line = line.decode('latin-1')
70 | if line.startswith('#') or line in ('\n', '\t'):
71 | continue
72 | else:
73 | if not line.startswith('\t') and (line[0].isdigit() or
74 | line[0].islower()):
75 | uid, name = get_record(line.strip())
76 | curr_id = uid
77 | usbs[uid] = [name.strip(), {}]
78 | elif line.startswith('\t') and line.count('\t') == 1:
79 | uid, name = get_record(line.strip())
80 | usbs[curr_id][1][uid] = name.strip()
81 | return usbs
82 |
83 |
84 | def get_record(record_line):
85 | """
86 | Split records out by dynamic position. By finding the space,
87 | we can determine the location to split the record for
88 | extraction. To learn more about this, uncomment the print
89 | statements and see what the code is doing behind the scenes!
90 | """
91 | # print("Line: {}".format(record_line))
92 | split = record_line.find(' ')
93 | # print("Split: {}".format(split))
94 | record_id = record_line[:split]
95 | # print("Record ID: ".format(record_id))
96 | record_name = record_line[split + 1:]
97 | # print("Record Name: ".format(record_name))
98 | return record_id, record_name
99 |
100 |
101 | def search_key(usb_dict, ids):
102 | """
103 | Compare provided IDs to the built USB dictionary. If found,
104 | it will return the common name, otherwise returns the string
105 | "unknown".
106 | """
107 | vendor_key = ids[0]
108 | product_key = ids[1]
109 |
110 | vendor, vendor_data = usb_dict.get(vendor_key, ['unknown', {}])
111 | product = 'unknown'
112 | if vendor != 'unknown':
113 | product = vendor_data.get(product_key, 'unknown')
114 |
115 | return vendor, product
116 |
--------------------------------------------------------------------------------
/Chapter13/chapter_13/plugins/helper/utility.py:
--------------------------------------------------------------------------------
1 | import binascii
2 | from datetime import datetime, timedelta
3 | import logging
4 |
5 | """
6 | MIT License
7 | Copyright (c) 2018 Chapin Bryce, Preston Miller
8 | Please share comments and questions at:
9 | https://github.com/PythonForensics/Learning-Python-for-Forensics
10 | or email pyforcookbook@gmail.com
11 |
12 | Permission is hereby granted, free of charge, to any person
13 | obtaining a copy of this software and associated documentation
14 | files (the "Software"), to deal in the Software without
15 | restriction, including without limitation the rights to use,
16 | copy, modify, merge, publish, distribute, sublicense, and/or
17 | sell copies of the Software, and to permit persons to whom the
18 | Software is furnished to do so, subject to the following
19 | conditions:
20 |
21 | The above copyright notice and this permission notice shall be
22 | included in all copies or substantial portions of the Software.
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
25 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
27 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
28 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
29 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 | OTHER DEALINGS IN THE SOFTWARE.
31 | """
32 |
33 |
34 | def check_header(filename, headers, size):
35 | """
36 | The check_header function reads a supplied size of the file
37 | and checks against known signatures to determine the file
38 | type.
39 | :param filename: The name of the file.
40 | :param headers: A list of known file signatures for the
41 | file type(s).
42 | :param size: The amount of data to read from the file for
43 | signature verification.
44 | :return: Boolean, True if the signatures match;
45 | otherwise, False.
46 | """
47 | with open(filename, 'rb') as infile:
48 | header = infile.read(size)
49 | hex_header = binascii.hexlify(header).decode('utf-8')
50 | for signature in headers:
51 | if hex_header == signature:
52 | return True
53 | else:
54 | pass
55 | logging.warn(('The signature for {} ({}) does not match '
56 | 'known signatures: {}').format(
57 | filename, hex_header, headers))
58 | return False
59 |
60 |
61 | def convert_size(size):
62 | """
63 | The convert_size function converts an integer representing
64 | bytes into a human-readable format.
65 | :param size: The size in bytes of a file
66 | :return: The human-readable size.
67 | """
68 | sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
69 | index = 0
70 | while size > 1024:
71 | size /= 1024.
72 | index += 1
73 | return '{:.2f} {}'.format(size, sizes[index])
74 |
75 |
76 | def file_time(ft):
77 | """
78 | The fileTime function converts Windows FILETIME objects into
79 | human readable value
80 | :param ft: the FILETIME to convert
81 | :return: date_str, the human readable datetime value
82 | """
83 | if ft is not None and ft != 0:
84 | return datetime(1601, 1, 1) + timedelta(microseconds=ft / 10)
85 | else:
86 | return 0
--------------------------------------------------------------------------------
/Chapter13/chapter_13/plugins/id3.py:
--------------------------------------------------------------------------------
1 | import os
2 | from time import gmtime, strftime
3 | from helper import utility
4 | from mutagen import mp3, id3
5 |
6 | """
7 | MIT License
8 | Copyright (c) 2018 Chapin Bryce, Preston Miller
9 | Please share comments and questions at:
10 | https://github.com/PythonForensics/Learning-Python-for-Forensics
11 | or email pyforcookbook@gmail.com
12 |
13 | Permission is hereby granted, free of charge, to any person
14 | obtaining a copy of this software and associated documentation
15 | files (the "Software"), to deal in the Software without
16 | restriction, including without limitation the rights to use,
17 | copy, modify, merge, publish, distribute, sublicense, and/or
18 | sell copies of the Software, and to permit persons to whom the
19 | Software is furnished to do so, subject to the following
20 | conditions:
21 |
22 | The above copyright notice and this permission notice shall be
23 | included in all copies or substantial portions of the Software.
24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
26 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
28 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
29 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
30 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
31 | OTHER DEALINGS IN THE SOFTWARE.
32 | """
33 |
34 |
35 | def main(filename):
36 | """
37 | The main function confirms the file type and sends it to
38 | be processed.
39 | :param filename: name of the file potentially containing exif
40 | metadata.
41 | :return: A dictionary from get_tags, containing the embedded
42 | EXIF metadata.
43 | """
44 |
45 | # MP3 signatures
46 | signatures = ['494433']
47 | if utility.check_header(filename, signatures, 3) is True:
48 | return get_tags(filename)
49 | else:
50 | raise TypeError
51 |
52 |
53 | def get_tags(filename):
54 | """
55 | The get_tags function extracts the ID3 metadata from the data
56 | object.
57 | :param filename: the path and name to the data object.
58 | :return: tags and headers, tags is a dictionary containing ID3
59 | metadata and headers are the order of keys for the CSV output.
60 | """
61 |
62 | # Set up CSV headers
63 | header = ['Path', 'Name', 'Size', 'Filesystem CTime',
64 | 'Filesystem MTime', 'Title', 'Subtitle', 'Artist', 'Album',
65 | 'Album/Artist', 'Length (Sec)', 'Year', 'Category',
66 | 'Track Number', 'Comments', 'Publisher', 'Bitrate',
67 | 'Sample Rate', 'Encoding', 'Channels', 'Audio Layer']
68 | tags = {}
69 | tags['Path'] = filename
70 | tags['Name'] = os.path.basename(filename)
71 | tags['Size'] = utility.convert_size(
72 | os.path.getsize(filename))
73 | tags['Filesystem CTime'] = strftime('%m/%d/%Y %H:%M:%S',
74 | gmtime(os.path.getctime(filename)))
75 | tags['Filesystem MTime'] = strftime('%m/%d/%Y %H:%M:%S',
76 | gmtime(os.path.getmtime(filename)))
77 |
78 | # MP3 Specific metadata
79 | audio = mp3.MP3(filename)
80 | if 'TENC' in audio.keys():
81 | tags['Encoding'] = audio['TENC'][0]
82 | tags['Bitrate'] = audio.info.bitrate
83 | tags['Channels'] = audio.info.channels
84 | tags['Audio Layer'] = audio.info.layer
85 | tags['Length (Sec)'] = audio.info.length
86 | tags['Sample Rate'] = audio.info.sample_rate
87 |
88 | # ID3 embedded metadata tags
89 | id = id3.ID3(filename)
90 | if 'TPE1' in id.keys():
91 | tags['Artist'] = id['TPE1'][0]
92 | if 'TRCK' in id.keys():
93 | tags['Track Number'] = id['TRCK'][0]
94 | if 'TIT3' in id.keys():
95 | tags['Subtitle'] = id['TIT3'][0]
96 | if 'COMM::eng' in id.keys():
97 | tags['Comments'] = id['COMM::eng'][0]
98 | if 'TDRC' in id.keys():
99 | tags['Year'] = id['TDRC'][0]
100 | if 'TALB' in id.keys():
101 | tags['Album'] = id['TALB'][0]
102 | if 'TIT2' in id.keys():
103 | tags['Title'] = id['TIT2'][0]
104 | if 'TCON' in id.keys():
105 | tags['Category'] = id['TCON'][0]
106 | if 'TPE2' in id.keys():
107 | tags['Album/Artist'] = id['TPE2'][0]
108 | if 'TPUB' in id.keys():
109 | tags['Publisher'] = id['TPUB'][0]
110 |
111 | return tags, header
112 |
--------------------------------------------------------------------------------
/Chapter13/chapter_13/plugins/office.py:
--------------------------------------------------------------------------------
1 | import zipfile
2 | import os
3 | from time import gmtime, strftime
4 | from helper import utility
5 |
6 | from lxml import etree
7 |
8 | """
9 | MIT License
10 | Copyright (c) 2018 Chapin Bryce, Preston Miller
11 | Please share comments and questions at:
12 | https://github.com/PythonForensics/Learning-Python-for-Forensics
13 | or email pyforcookbook@gmail.com
14 |
15 | Permission is hereby granted, free of charge, to any person
16 | obtaining a copy of this software and associated documentation
17 | files (the "Software"), to deal in the Software without
18 | restriction, including without limitation the rights to use,
19 | copy, modify, merge, publish, distribute, sublicense, and/or
20 | sell copies of the Software, and to permit persons to whom the
21 | Software is furnished to do so, subject to the following
22 | conditions:
23 |
24 | The above copyright notice and this permission notice shall be
25 | included in all copies or substantial portions of the Software.
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
28 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
31 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33 | OTHER DEALINGS IN THE SOFTWARE.
34 | """
35 |
36 |
37 | def main(filename):
38 |
39 | """
40 | The main function confirms the file type and sends it
41 | to be processed.
42 | :param filename: name of the file potentially containing
43 | embedded metadata.
44 | :return: A dictionary from getTags, containing the embedded
45 | metadata.
46 | """
47 |
48 | # DOCX, XLSX, and PPTX signatures
49 | signatures = ['504b030414000600']
50 | if utility.check_header(filename, signatures, 8) is True:
51 | return get_tags(filename)
52 | else:
53 | raise TypeError
54 |
55 |
56 | def get_tags(filename):
57 | """
58 | The get_tags function extracts the office metadata from the
59 | data object.
60 | :param filename: the path and name to the data object.
61 | :return: tags and headers, tags is a dictionary containing
62 | office metadata and headers are the order of keys for the CSV
63 | output.
64 | """
65 |
66 | # Set up CSV headers
67 | headers = ['Path', 'Name', 'Size', 'Filesystem CTime',
68 | 'Filesystem MTime', 'Title', 'Author(s)','Create Date',
69 | 'Modify Date', 'Last Modified By Date', 'Subject', 'Keywords',
70 | 'Description', 'Category', 'Status', 'Revision',
71 | 'Edit Time (Min)', 'Page Count', 'Word Count',
72 | 'Character Count', 'Line Count',
73 | 'Paragraph Count', 'Slide Count', 'Note Count',
74 | 'Hidden Slide Count', 'Company', 'Hyperlink Base']
75 |
76 | # Create a ZipFile class from the input object
77 | # This allows us to read or write to the 'Zip archive'
78 | try:
79 | zf = zipfile.ZipFile(filename)
80 | except zipfile.BadZipfile:
81 | return {}, headers
82 |
83 | # These two XML files contain the embedded metadata of
84 | # interest
85 | try:
86 | core = etree.fromstring(zf.read('docProps/core.xml'))
87 | app = etree.fromstring(zf.read('docProps/app.xml'))
88 | except KeyError as e:
89 | assert Warning(e)
90 | return {}, headers
91 |
92 | tags = {}
93 | tags['Path'] = filename
94 | tags['Name'] = os.path.basename(filename)
95 | tags['Size'] = utility.convert_size(
96 | os.path.getsize(filename))
97 | tags['Filesystem CTime'] = strftime('%m/%d/%Y %H:%M:%S',
98 | gmtime(os.path.getctime(filename)))
99 | tags['Filesystem MTime'] = strftime('%m/%d/%Y %H:%M:%S',
100 | gmtime(os.path.getmtime(filename)))
101 |
102 | # Core Tags
103 |
104 | for child in core.iterchildren():
105 |
106 | if 'title' in child.tag:
107 | tags['Title'] = child.text
108 | if 'subject' in child.tag:
109 | tags['Subject'] = child.text
110 | if 'creator' in child.tag:
111 | tags['Author(s)'] = child.text
112 | if 'keywords' in child.tag:
113 | tags['Keywords'] = child.text
114 | if 'description' in child.tag:
115 | tags['Description'] = child.text
116 | if 'lastModifiedBy' in child.tag:
117 | tags['Last Modified By Date'] = child.text
118 | if 'created' in child.tag:
119 | tags['Create Date'] = child.text
120 | if 'modified' in child.tag:
121 | tags['Modify Date'] = child.text
122 | if 'category' in child.tag:
123 | tags['Category'] = child.text
124 | if 'contentStatus' in child.tag:
125 | tags['Status'] = child.text
126 |
127 | if (filename.endswith('.docx') or
128 | filename.endswith('.pptx')):
129 | if 'revision' in child.tag:
130 | tags['Revision'] = child.text
131 |
132 | # App Tags
133 | for child in app.iterchildren():
134 |
135 | if filename.endswith('.docx'):
136 | if 'TotalTime' in child.tag:
137 | tags['Edit Time (Min)'] = child.text
138 | if 'Pages' in child.tag:
139 | tags['Page Count'] = child.text
140 | if 'Words' in child.tag:
141 | tags['Word Count'] = child.text
142 | if 'Characters' in child.tag:
143 | tags['Character Count'] = child.text
144 | if 'Lines' in child.tag:
145 | tags['Line Count'] = child.text
146 | if 'Paragraphs' in child.tag:
147 | tags['Paragraph Count'] = child.text
148 | if 'Company' in child.tag:
149 | tags['Company'] = child.text
150 | if 'HyperlinkBase' in child.tag:
151 | tags['Hyperlink Base'] = child.text
152 |
153 | elif filename.endswith('.pptx'):
154 | if 'TotalTime' in child.tag:
155 | tags['Edit Time (Min)'] = child.text
156 | if 'Words' in child.tag:
157 | tags['Word Count'] = child.text
158 | if 'Paragraphs' in child.tag:
159 | tags['Paragraph Count'] = child.text
160 | if 'Slides' in child.tag:
161 | tags['Slide Count'] = child.text
162 | if 'Notes' in child.tag:
163 | tags['Note Count'] = child.text
164 | if 'HiddenSlides' in child.tag:
165 | tags['Hidden Slide Count'] = child.text
166 | if 'Company' in child.tag:
167 | tags['Company'] = child.text
168 | if 'HyperlinkBase' in child.tag:
169 | tags['Hyperlink Base'] = child.text
170 | else:
171 | if 'Company' in child.tag:
172 | tags['Company'] = child.text
173 | if 'HyperlinkBase' in child.tag:
174 | tags['Hyperlink Base'] = child.text
175 |
176 | return tags, headers
177 |
--------------------------------------------------------------------------------
/Chapter13/chapter_13/plugins/pst_indexer.py:
--------------------------------------------------------------------------------
1 | import pypff
2 |
3 | """
4 | MIT License
5 | Copyright (c) 2018 Chapin Bryce, Preston Miller
6 | Please share comments and questions at:
7 | https://github.com/PythonForensics/Learning-Python-for-Forensics
8 | or email pyforcookbook@gmail.com
9 |
10 | Permission is hereby granted, free of charge, to any person
11 | obtaining a copy of this software and associated documentation
12 | files (the "Software"), to deal in the Software without
13 | restriction, including without limitation the rights to use,
14 | copy, modify, merge, publish, distribute, sublicense, and/or
15 | sell copies of the Software, and to permit persons to whom the
16 | Software is furnished to do so, subject to the following
17 | conditions:
18 |
19 | The above copyright notice and this permission notice shall be
20 | included in all copies or substantial portions of the Software.
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
28 | OTHER DEALINGS IN THE SOFTWARE.
29 | """
30 |
31 |
32 | def main(pst_file):
33 | """
34 | The main function opens a PST and calls functions to parse
35 | and report data from the PST
36 | :param pst_file: A string representing the path to the PST
37 | file to analyze
38 | :return: None
39 | """
40 | opst = pypff.open(pst_file)
41 | root = opst.get_root_folder()
42 |
43 | message_data = folder_traverse(root, [],
44 | **{'pst_name': pst_file, 'folder_name': 'root'})
45 |
46 | header = ['pst_name', 'folder_name', 'creation_time',
47 | 'submit_time', 'delivery_time', 'sender', 'subject', 'attachment_count']
48 |
49 | return message_data, header
50 |
51 |
52 | def folder_traverse(base, message_data, pst_name, folder_name):
53 | """
54 | The folder_traverse function walks through the base of the
55 | folder and scans for sub-folders and messages
56 | :param base: Base folder to scan for new items within the
57 | folder
58 | :param message_data: A list of data for output
59 | :param pst_name: A string representing the name of the PST
60 | file
61 | :param folder_name: A string representing the name of the
62 | folder
63 | :return: None
64 | """
65 | for folder in base.sub_folders:
66 | if folder.number_of_sub_folders:
67 | message_data = folder_traverse(folder, message_data,
68 | pst_name, folder.name)
69 | message_data = check_for_messages(folder, message_data,
70 | pst_name, folder.name)
71 | return message_data
72 |
73 |
74 | def check_for_messages(folder, message_data, pst_name, folder_name):
75 | """
76 | The check_for_messages function reads folder messages if
77 | present and passes them to the report function
78 | :param folder: pypff.Folder object
79 | :param message_data: list to pass and extend with message info
80 | :param pst_name: A string representing the name of the PST
81 | file
82 | :param folder_name: A string representing the name of the
83 | folder
84 | :return: Dictionary of results by folder
85 | """
86 | for message in folder.sub_messages:
87 | message_dict = process_message(message)
88 | message_dict['pst_name'] = pst_name
89 | message_dict['folder_name'] = folder_name
90 | message_data.append(message_dict)
91 | return message_data
92 |
93 |
94 | def process_message(message):
95 | """
96 | The process_message function processes multi-field messages to
97 | simplify collection of information
98 | :param message: The pypff.Message object
99 | :return: A dictionary with message fields (values) and their
100 | data (keys)
101 | """
102 | return {
103 | "subject": message.subject,
104 | "sender": message.sender_name,
105 | "header": message.transport_headers,
106 | "body": message.plain_text_body,
107 | "creation_time": message.creation_time,
108 | "submit_time": message.client_submit_time,
109 | "delivery_time": message.delivery_time,
110 | "attachment_count": message.number_of_attachments,
111 | }
112 |
--------------------------------------------------------------------------------
/Chapter13/chapter_13/plugins/setupapi.py:
--------------------------------------------------------------------------------
1 | from helper import usb_lookup
2 |
3 | """
4 | MIT License
5 | Copyright (c) 2018 Chapin Bryce, Preston Miller
6 | Please share comments and questions at:
7 | https://github.com/PythonForensics/Learning-Python-for-Forensics
8 | or email pyforcookbook@gmail.com
9 |
10 | Permission is hereby granted, free of charge, to any person
11 | obtaining a copy of this software and associated documentation
12 | files (the "Software"), to deal in the Software without
13 | restriction, including without limitation the rights to use,
14 | copy, modify, merge, publish, distribute, sublicense, and/or
15 | sell copies of the Software, and to permit persons to whom the
16 | Software is furnished to do so, subject to the following
17 | conditions:
18 |
19 | The above copyright notice and this permission notice shall be
20 | included in all copies or substantial portions of the Software.
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
28 | OTHER DEALINGS IN THE SOFTWARE.
29 | """
30 |
31 |
32 | def main(in_file):
33 | """
34 | Main function to handle operation
35 | :param in_file: Str - Path to setupapi log to analyze
36 | :return: list of USB data and list of headers for output
37 | """
38 | headers = ['Vendor ID', 'Vendor Name', 'Product ID',
39 | 'Product Name', 'Revision', 'UID',
40 | 'First Installation Date']
41 | data = []
42 |
43 | device_information = parse_setupapi(in_file)
44 | usb_ids = prep_usb_lookup()
45 | for device in device_information:
46 | parsed_info = parse_device_info(device)
47 | if isinstance(parsed_info, dict):
48 | parsed_info = get_device_names(usb_ids, parsed_info)
49 | data.append(parsed_info)
50 | else:
51 | pass
52 | return data, headers
53 |
54 |
55 | def parse_setupapi(setup_log):
56 | """
57 | Read data from provided file for Device Install Events for
58 | USB Devices
59 | :param setup_log: str - Path to valid setup api log
60 | :return: tuple of str - Device name and date
61 | """
62 | device_list = list()
63 | unique_list = set()
64 | with open(setup_log) as in_file:
65 | for line in in_file:
66 | lower_line = line.lower()
67 | if 'device install (hardware initiated)' in \
68 | lower_line and ('vid' in lower_line or
69 | 'ven' in lower_line):
70 | device_name = line.split('-')[1].strip()
71 | date = next(in_file).split('start')[1].strip()
72 | if device_name not in unique_list:
73 | device_list.append((device_name, date))
74 | unique_list.add(device_name)
75 |
76 | return device_list
77 |
78 |
79 | def parse_device_info(device_info):
80 | """
81 | Parses Vendor, Product, Revision and UID from a Setup API
82 | entry
83 | :param device_info: string of device information to parse
84 | :return: dictionary of parsed information or original string
85 | if error
86 | """
87 | # Initialize variables
88 | vid = ''
89 | pid = ''
90 | rev = ''
91 | uid = ''
92 |
93 | # Split string into segments on \\
94 | segments = device_info[0].split('\\')
95 |
96 | if 'usb' not in segments[0].lower():
97 | return None
98 | # Eliminate non-USB devices from output
99 | # May hide other storage devices
100 |
101 | for item in segments[1].split('&'):
102 | lower_item = item.lower()
103 | if 'ven' in lower_item or 'vid' in lower_item:
104 | vid = item.split('_', 1)[-1]
105 | elif 'dev' in lower_item or 'pid' in lower_item or \
106 | 'prod' in lower_item:
107 | pid = item.split('_', 1)[-1]
108 | elif 'rev' in lower_item or 'mi' in lower_item:
109 | rev = item.split('_', 1)[-1]
110 |
111 | if len(segments) >= 3:
112 | uid = segments[2].strip(']')
113 |
114 | if vid != '' or pid != '':
115 | return {'Vendor ID': vid.lower(),
116 | 'Product ID': pid.lower(),
117 | 'Revision': rev,
118 | 'UID': uid,
119 | 'First Installation Date': device_info[1]}
120 | # Unable to parse data, returning whole string
121 | return device_info
122 |
123 |
124 | def prep_usb_lookup(local_usb_ids=None):
125 | """
126 | Prepare the lookup of USB devices through accessing the most
127 | recent copy of the database at http://linux-usb.org/usb.ids
128 | or using the provided file and parsing it into a queriable
129 | dictionary format.
130 | """
131 | if local_usb_ids:
132 | usb_file = open(local_usb_ids, encoding='latin1')
133 | else:
134 | usb_file = usb_lookup.get_usb_file()
135 | return usb_lookup.parse_file(usb_file)
136 |
137 |
138 | def get_device_names(usb_dict, device_info):
139 | """
140 | Query `usb_lookup.py` for device information based on VID/PID.
141 | :param usb_dict: Dictionary from usb_lookup.py of known
142 | devices.
143 | :param device_info: Dictionary containing 'Vendor ID' and
144 | 'Product ID' keys and values.
145 | :return: original dictionary with 'Vendor Name' and
146 | 'Product Name' keys and values
147 | """
148 | device_name = usb_lookup.search_key(
149 | usb_dict, [device_info['Vendor ID'],
150 | device_info['Product ID']])
151 |
152 | device_info['Vendor Name'] = device_name[0]
153 | device_info['Product Name'] = device_name[1]
154 |
155 | return device_info
156 |
157 |
158 | def print_output(usb_information):
159 | """
160 | Print formatted information about USB Device
161 | :param usb_information: dictionary containing key/value
162 | data about each device or tuple of device information
163 | :return: None
164 | """
165 | print('{:-^15}'.format(''))
166 |
167 | if isinstance(usb_information, dict):
168 | for key_name, value_name in usb_information.items():
169 | print('{}: {}'.format(key_name, value_name))
170 | elif isinstance(usb_information, tuple):
171 | print('Device: {}'.format(usb_information[0]))
172 | print('Date: {}'.format(usb_information[1]))
173 |
174 |
--------------------------------------------------------------------------------
/Chapter13/chapter_13/plugins/userassist.py:
--------------------------------------------------------------------------------
1 | """UserAssist parser leveraging the YARP library."""
2 | import struct
3 | import sys
4 | import logging
5 |
6 | from helper import utility
7 |
8 | from yarp import Registry
9 |
10 | """
11 | MIT License
12 | Copyright (c) 2018 Chapin Bryce, Preston Miller
13 | Please share comments and questions at:
14 | https://github.com/PythonForensics/Learning-Python-for-Forensics
15 | or email pyforcookbook@gmail.com
16 |
17 | Permission is hereby granted, free of charge, to any person
18 | obtaining a copy of this software and associated documentation
19 | files (the "Software"), to deal in the Software without
20 | restriction, including without limitation the rights to use,
21 | copy, modify, merge, publish, distribute, sublicense, and/or
22 | sell copies of the Software, and to permit persons to whom the
23 | Software is furnished to do so, subject to the following
24 | conditions:
25 |
26 | The above copyright notice and this permission notice shall be
27 | included in all copies or substantial portions of the Software.
28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
29 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
30 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
31 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
32 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
33 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
34 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
35 | OTHER DEALINGS IN THE SOFTWARE.
36 | """
37 |
38 | # KEYS will contain sub-lists of each parsed UserAssist (UA) key
39 | KEYS = []
40 |
41 |
42 | def main(registry, **kwargs):
43 | """
44 | The main function handles main logic of script.
45 | :param registry: Registry hive to process
46 | :return: Nothing.
47 | """
48 | if utility.check_header(registry, ['72656766'], 4) is not True:
49 | logging.error('Incorrect file detected based on name')
50 | raise TypeError
51 | # Create dictionary of ROT-13 decoded UA key and its value
52 | apps = create_dictionary(registry)
53 | ua_type = parse_values(apps)
54 |
55 | if ua_type == 0:
56 | logging.info('Detected XP based Userassist values.')
57 |
58 | else:
59 | logging.info('Detected Win7 based Userassist values.')
60 |
61 | headers = ['Name', 'Path', 'Session ID', 'Count',
62 | 'Last Used Date (UTC)', 'Focus Time (ms)', 'Focus Count']
63 | return KEYS, headers
64 |
65 |
66 | def create_dictionary(registry):
67 | """
68 | The create_dictionary function creates a list of dictionaries
69 | where keys are the ROT-13 decoded app names and values are
70 | the raw hex data of said app.
71 | :param registry: Registry Hive to process
72 | :return: apps_list, A list containing dictionaries for
73 | each app
74 | """
75 | try:
76 | # Open the registry file to be parsed
77 | registry_file = open(registry, "rb")
78 | reg = Registry.RegistryHive(registry_file)
79 | except (IOError, UnicodeDecodeError) as e:
80 | msg = 'Invalid NTUSER.DAT path or Registry ID.'
81 | logging.error(msg)
82 | raise TypeError
83 |
84 | # Navigate to the UserAssist key
85 | ua_key = reg.find_key(
86 | ('SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer'
87 | '\\UserAssist'))
88 | if ua_key is None:
89 | msg = 'UserAssist Key not found in Registry file.'
90 | logging.error(msg)
91 | raise TypeError
92 | apps_list = []
93 | # Loop through each subkey in the UserAssist key
94 | for ua_subkey in ua_key.subkeys():
95 | # For each subkey in the UserAssist key, detect a subkey
96 | # called Count that has more than 0 values to parse.
97 | if(ua_subkey.subkey('Count') and
98 | ua_subkey.subkey('Count').values_count() > 0):
99 | apps = {}
100 | for v in ua_subkey.subkey('Count').values():
101 | if sys.version_info[0] == 2:
102 | apps[v.name().encode('utf-8').decode(
103 | 'rot-13')] = v.data_raw()
104 | elif sys.version_info[0] == 3:
105 | import codecs
106 | enc = codecs.getencoder('rot-13')
107 | apps[enc(str(v.name()))[0]] = v.data_raw()
108 |
109 | apps_list.append(apps)
110 | return apps_list
111 |
112 |
113 | def parse_values(data):
114 | """
115 | The parse_values function uses struct to unpack the raw value
116 | data from the UA key
117 | :param data: A list containing dictionaries of UA
118 | application data
119 | :return: ua_type, based on the size of the raw data from
120 | the dictionary values.
121 | """
122 | ua_type = -1
123 | msg = 'Parsing UserAssist values.'
124 | logging.info(msg)
125 |
126 | for dictionary in data:
127 | for v in dictionary.keys():
128 | # WinXP based UA keys are 16 bytes
129 | if len(dictionary[v]) == 16:
130 | raw = struct.unpack('<2iq', dictionary[v])
131 | ua_type = 0
132 | KEYS.append({'Name': get_name(v), 'Path': v,
133 | 'Session ID': raw[0], 'Count': raw[1],
134 | 'Last Used Date (UTC)': utility.file_time(raw[2]),
135 | 'Focus Time (ms)': '', 'Focus Count': ''})
136 | # Win7 based UA keys are 72 bytes
137 | elif len(dictionary[v]) == 72:
138 | raw = struct.unpack('<4i44xq4x', dictionary[v])
139 | ua_type = 1
140 | KEYS.append({'Name': get_name(v), 'Path': v,
141 | 'Session ID': raw[0], 'Count': raw[1],
142 | 'Last Used Date (UTC)': utility.file_time(raw[4]),
143 | 'Focus Time (ms)': raw[3],'Focus Count': raw[2]})
144 | else:
145 | # If the key is not WinXP or Win7 based -- ignore.
146 | msg = 'Ignoring {} value that is {} bytes'.format(
147 | str(v), str(len(dictionary[v])))
148 | logging.info(msg)
149 | continue
150 | return ua_type
151 |
152 |
153 | def get_name(full_name):
154 | """
155 | the get_name function splits the name of the application
156 | returning the executable name and ignoring the
157 | path details.
158 | :param full_name: the path and executable name
159 | :return: the executable name
160 | """
161 | # Determine if '\\' and ':' are within the full_name
162 | if ':' in full_name and '\\' in full_name:
163 | # Find if ':' comes before '\\'
164 | if full_name.rindex(':') > full_name.rindex('\\'):
165 | # Split on ':' and return the last element
166 | # (the executable)
167 | return full_name.split(':')[-1]
168 | else:
169 | # Otherwise split on '\\'
170 | return full_name.split('\\')[-1]
171 | # When just ':' or '\\' is in the full_name, split on
172 | # that item and return the last element (the executable)
173 | elif ':' in full_name:
174 | return full_name.split(':')[-1]
175 | else:
176 | return full_name.split('\\')[-1]
177 |
--------------------------------------------------------------------------------
/Chapter13/chapter_13/requirements.txt:
--------------------------------------------------------------------------------
1 | xlsxwriter==1.1.2
2 | colorama==0.4.1
3 | pyfiglet==0.8.post0
4 | mutagen==1.42.0
5 | pillow==5.4.0
6 | simplekml==1.3.1
7 | tqdm==4.28.1
8 | unicodecsv==0.14.1
9 | jinja2==2.10
10 | lxml==4.3.0
11 |
--------------------------------------------------------------------------------
/Chapter13/chapter_13/writers/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.append(os.path.join(os.path.dirname(__file__)))
4 |
5 | import csv_writer
6 | import kml_writer
7 | import xlsx_writer
8 |
9 | """
10 | MIT License
11 | Copyright (c) 2018 Chapin Bryce, Preston Miller
12 | Please share comments and questions at:
13 | https://github.com/PythonForensics/Learning-Python-for-Forensics
14 | or email pyforcookbook@gmail.com
15 |
16 | Permission is hereby granted, free of charge, to any person
17 | obtaining a copy of this software and associated documentation
18 | files (the "Software"), to deal in the Software without
19 | restriction, including without limitation the rights to use,
20 | copy, modify, merge, publish, distribute, sublicense, and/or
21 | sell copies of the Software, and to permit persons to whom the
22 | Software is furnished to do so, subject to the following
23 | conditions:
24 |
25 | The above copyright notice and this permission notice shall be
26 | included in all copies or substantial portions of the Software.
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
34 | OTHER DEALINGS IN THE SOFTWARE.
35 | """
--------------------------------------------------------------------------------
/Chapter13/chapter_13/writers/csv_writer.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import sys
3 | import os
4 | if sys.version_info[0] == 2:
5 | import unicodecsv as csv
6 | elif sys.version_info[0] == 3:
7 | import csv
8 |
9 | """
10 | MIT License
11 | Copyright (c) 2018 Chapin Bryce, Preston Miller
12 | Please share comments and questions at:
13 | https://github.com/PythonForensics/Learning-Python-for-Forensics
14 | or email pyforcookbook@gmail.com
15 |
16 | Permission is hereby granted, free of charge, to any person
17 | obtaining a copy of this software and associated documentation
18 | files (the "Software"), to deal in the Software without
19 | restriction, including without limitation the rights to use,
20 | copy, modify, merge, publish, distribute, sublicense, and/or
21 | sell copies of the Software, and to permit persons to whom the
22 | Software is furnished to do so, subject to the following
23 | conditions:
24 |
25 | The above copyright notice and this permission notice shall be
26 | included in all copies or substantial portions of the Software.
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
34 | OTHER DEALINGS IN THE SOFTWARE.
35 | """
36 |
37 |
38 | def writer(output, headers, output_data, **kwargs):
39 | """
40 | The writer function uses the csv.DictWriter module to write
41 | list(s) of dictionaries. The DictWriter can take a fieldnames
42 | argument, as a list, which represents the desired order of
43 | columns.
44 | :param output: The name of the output CSV.
45 | :param headers: A list of keys in the dictionary that
46 | represent the desired order of columns in the output.
47 | :param output_data: The list of dictionaries containing
48 | embedded metadata.
49 | :return: None
50 | """
51 |
52 | if sys.version_info[0] == 2:
53 | csvfile = open(output, "wb")
54 | elif sys.version_info[0] == 3:
55 | csvfile = open(output, "w", newline='',
56 | encoding='utf-8')
57 |
58 | with csvfile:
59 | # We use DictWriter instead of writer to write
60 | # dictionaries to CSV.
61 | w = csv.DictWriter(csvfile, fieldnames=headers,
62 | extrasaction='ignore')
63 |
64 | # Writerheader writes the header based on the supplied
65 | # headers object
66 | try:
67 | w.writeheader()
68 | except TypeError:
69 | print(('[-] Received empty headers...\n'
70 | '[-] Skipping writing output.'))
71 | return
72 |
73 | if 'recursion' in kwargs.keys():
74 | for l in output_data:
75 | for data in l:
76 | if data:
77 | w.writerow(data)
78 | else:
79 | for data in output_data:
80 | if data:
81 | w.writerow(data)
--------------------------------------------------------------------------------
/Chapter13/chapter_13/writers/kml_writer.py:
--------------------------------------------------------------------------------
1 | import os
2 | import simplekml
3 |
4 | """
5 | MIT License
6 | Copyright (c) 2018 Chapin Bryce, Preston Miller
7 | Please share comments and questions at:
8 | https://github.com/PythonForensics/Learning-Python-for-Forensics
9 | or email pyforcookbook@gmail.com
10 |
11 | Permission is hereby granted, free of charge, to any person
12 | obtaining a copy of this software and associated documentation
13 | files (the "Software"), to deal in the Software without
14 | restriction, including without limitation the rights to use,
15 | copy, modify, merge, publish, distribute, sublicense, and/or
16 | sell copies of the Software, and to permit persons to whom the
17 | Software is furnished to do so, subject to the following
18 | conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 | OTHER DEALINGS IN THE SOFTWARE.
30 | """
31 |
32 | def writer(output, output_name, output_data):
33 | """
34 | The writer function writes JPEG and TIFF EXIF GPS data to a
35 | Google Earth KML file. This file can be opened in Google
36 | Earth and will use the GPS coordinates to create 'pins' on
37 | the map of the taken photo's location.
38 | :param output: The output directory to write the KML file.
39 | :param output_name: The name of the output KML file.
40 | :param output_data: The embedded EXIF metadata to be written
41 | :return:
42 | """
43 |
44 | kml = simplekml.Kml(name=output_name)
45 | for exif in output_data:
46 | if('Latitude' in exif.keys() and
47 | 'Latitude Reference' in exif.keys() and
48 | 'Longitude Reference' in exif.keys() and
49 | 'Longitude' in exif.keys()):
50 |
51 | if 'Original Date' in exif.keys():
52 | dt = exif['Original Date']
53 | else:
54 | dt = 'N/A'
55 |
56 | if exif['Latitude Reference'] == 'S':
57 | latitude = '-' + exif['Latitude']
58 | else:
59 | latitude = exif['Latitude']
60 |
61 | if exif['Longitude Reference'] == 'W':
62 | longitude = '-' + exif['Longitude']
63 | else:
64 | longitude = exif['Longitude']
65 |
66 | kml.newpoint(name=exif['Name'],
67 | description='Originally Created: ' + dt,
68 | coords=[(longitude, latitude)])
69 | else:
70 | pass
71 | kml.save(os.path.join(output, output_name))
72 |
--------------------------------------------------------------------------------
/Chapter13/chapter_13/writers/xlsx_writer.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import xlsxwriter
3 |
4 | """
5 | MIT License
6 | Copyright (c) 2018 Chapin Bryce, Preston Miller
7 | Please share comments and questions at:
8 | https://github.com/PythonForensics/Learning-Python-for-Forensics
9 | or email pyforcookbook@gmail.com
10 |
11 | Permission is hereby granted, free of charge, to any person
12 | obtaining a copy of this software and associated documentation
13 | files (the "Software"), to deal in the Software without
14 | restriction, including without limitation the rights to use,
15 | copy, modify, merge, publish, distribute, sublicense, and/or
16 | sell copies of the Software, and to permit persons to whom the
17 | Software is furnished to do so, subject to the following
18 | conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 | OTHER DEALINGS IN THE SOFTWARE.
30 | """
31 |
32 | ALPHABET = [chr(i) for i in range(ord('A'), ord('Z') + 1)]
33 |
34 |
35 | def writer(output, headers, output_data, **kwargs):
36 | """
37 | The writer function writes excel output for the framework
38 | :param output: the output filename for the excel spreadsheet
39 | :param headers: the name of the spreadsheet columns
40 | :param output_data: the data to be written to the excel
41 | spreadsheet
42 | :return: Nothing
43 | """
44 | wb = xlsxwriter.Workbook(output)
45 |
46 | if headers is None:
47 | print('[-] Received empty headers... \n'
48 | '[-] Skipping writing output.')
49 | return
50 |
51 | if len(headers) <= 26:
52 | title_length = ALPHABET[len(headers) - 1]
53 | else:
54 | title_length = 'Z'
55 |
56 | ws = add_worksheet(wb, title_length)
57 |
58 | if 'recursion' in kwargs.keys():
59 | for i, data in enumerate(output_data):
60 | if i > 0:
61 | ws = add_worksheet(wb, title_length)
62 | cell_length = len(data)
63 | tmp = []
64 | for dictionary in data:
65 | tmp.append(
66 | [str(dictionary[x]) if x in dictionary.keys() else '' for x in headers]
67 | )
68 |
69 | ws.add_table(
70 | 'A3:' + title_length + str(3 + cell_length),
71 | {'data': tmp,
72 | 'columns': [{'header': x} for x in headers]})
73 |
74 | else:
75 | cell_length = len(output_data)
76 | tmp = []
77 | for data in output_data:
78 | tmp.append([str(data[x]) if x in data.keys() else '' for x in headers])
79 | ws.add_table(
80 | 'A3:' + title_length + str(3 + cell_length),
81 | {'data': tmp,
82 | 'columns': [{'header': x} for x in headers]})
83 |
84 | wb.close()
85 |
86 |
87 | def add_worksheet(wb, length, name=None):
88 | """
89 | The add_worksheet function creates a new formatted worksheet
90 | in the workbook
91 | :param wb: The workbook object
92 | :param length: The range of rows to merge
93 | :param name: The name of the worksheet
94 | :return: ws, the worksheet
95 | """
96 | title_format = wb.add_format({'bold': True,
97 | 'font_color': 'black', 'bg_color': 'white', 'font_size': 30,
98 | 'font_name': 'Arial', 'align': 'center'})
99 | ws = wb.add_worksheet(name)
100 |
101 | ws.merge_range('A1:' + length + '1', 'XYZ Corp',
102 | title_format)
103 | ws.merge_range('A2:' + length + '2', 'Case ####',
104 | title_format)
105 | return ws
--------------------------------------------------------------------------------
/Chapter13/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.append(os.path.join(os.path.dirname(__file__)))
4 |
5 | import wal_crawler
6 | import setupapi
7 | import userassist
8 | import exif
9 | import id3
10 | import office
11 | import pst_indexer
12 |
13 | """
14 | MIT License
15 | Copyright (c) 2018 Chapin Bryce, Preston Miller
16 | Please share comments and questions at:
17 | https://github.com/PythonForensics/Learning-Python-for-Forensics
18 | or email pyforcookbook@gmail.com
19 |
20 | Permission is hereby granted, free of charge, to any person
21 | obtaining a copy of this software and associated documentation
22 | files (the "Software"), to deal in the Software without
23 | restriction, including without limitation the rights to use,
24 | copy, modify, merge, publish, distribute, sublicense, and/or
25 | sell copies of the Software, and to permit persons to whom the
26 | Software is furnished to do so, subject to the following
27 | conditions:
28 |
29 | The above copyright notice and this permission notice shall be
30 | included in all copies or substantial portions of the Software.
31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
32 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
33 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
34 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
35 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
36 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
37 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
38 | OTHER DEALINGS IN THE SOFTWARE.
39 | """
--------------------------------------------------------------------------------
/Chapter13/plugins/helper/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.append(os.path.join(os.path.dirname(__file__)))
4 |
5 | import utility
6 | import usb_lookup
7 |
8 | """
9 | MIT License
10 | Copyright (c) 2018 Chapin Bryce, Preston Miller
11 | Please share comments and questions at:
12 | https://github.com/PythonForensics/Learning-Python-for-Forensics
13 | or email pyforcookbook@gmail.com
14 |
15 | Permission is hereby granted, free of charge, to any person
16 | obtaining a copy of this software and associated documentation
17 | files (the "Software"), to deal in the Software without
18 | restriction, including without limitation the rights to use,
19 | copy, modify, merge, publish, distribute, sublicense, and/or
20 | sell copies of the Software, and to permit persons to whom the
21 | Software is furnished to do so, subject to the following
22 | conditions:
23 |
24 | The above copyright notice and this permission notice shall be
25 | included in all copies or substantial portions of the Software.
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
28 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
31 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33 | OTHER DEALINGS IN THE SOFTWARE.
34 | """
--------------------------------------------------------------------------------
/Chapter13/plugins/helper/usb_lookup.py:
--------------------------------------------------------------------------------
1 | """Updated USB Lookup script based on the version found in Chapter 2."""
2 | from __future__ import print_function
3 | import argparse
4 | import sys
5 | try:
6 | from urllib2 import urlopen
7 | except ImportError:
8 | from urllib.request import urlopen
9 |
10 | """
11 | MIT License
12 | Copyright (c) 2018 Chapin Bryce, Preston Miller
13 | Please share comments and questions at:
14 | https://github.com/PythonForensics/Learning-Python-for-Forensics
15 | or email pyforcookbook@gmail.com
16 |
17 | Permission is hereby granted, free of charge, to any person obtaining a copy
18 | of this software and associated documentation files (the "Software"), to deal
19 | in the Software without restriction, including without limitation the rights
20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21 | copies of the Software, and to permit persons to whom the Software is
22 | furnished to do so, subject to the following conditions:
23 | The above copyright notice and this permission notice shall be included in
24 | all copies or substantial portions of the Software.
25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31 | SOFTWARE.
32 | """
33 |
34 | def main(vid, pid, ids_file=None):
35 | """
36 | Main function to control operation. Requires arguments passed as VID PID
37 | on the command line. If discovered in data set, the common names will be
38 | printed to stdout
39 | :return: None
40 | """
41 | if ids_file:
42 | usb_file = open(ids_file, encoding='latin1')
43 | else:
44 | usb_file = get_usb_file()
45 | usbs = parse_file(usb_file)
46 | results = search_key(usbs, (vid, pid))
47 | print("Vendor: {}\nProduct: {}".format(results[0], results[1]))
48 |
49 |
50 | def get_usb_file():
51 | """
52 | Retrieves USB.ids database from the web.
53 | """
54 | url = 'http://www.linux-usb.org/usb.ids'
55 | return urlopen(url)
56 |
57 |
58 | def parse_file(usb_file):
59 | """
60 | Parses the USB.ids file. If this is run offline, please
61 | download the USB.ids and pass the open file to this function.
62 | ie: parse_file(open('path/to/USB.ids', 'r'))
63 | :return: dictionary of entires for querying
64 | """
65 | usbs = {}
66 | curr_id = ''
67 | for line in usb_file:
68 | if isinstance(line, bytes):
69 | line = line.decode('latin-1')
70 | if line.startswith('#') or line in ('\n', '\t'):
71 | continue
72 | else:
73 | if not line.startswith('\t') and (line[0].isdigit() or
74 | line[0].islower()):
75 | uid, name = get_record(line.strip())
76 | curr_id = uid
77 | usbs[uid] = [name.strip(), {}]
78 | elif line.startswith('\t') and line.count('\t') == 1:
79 | uid, name = get_record(line.strip())
80 | usbs[curr_id][1][uid] = name.strip()
81 | return usbs
82 |
83 |
84 | def get_record(record_line):
85 | """
86 | Split records out by dynamic position. By finding the space,
87 | we can determine the location to split the record for
88 | extraction. To learn more about this, uncomment the print
89 | statements and see what the code is doing behind the scenes!
90 | """
91 | # print("Line: {}".format(record_line))
92 | split = record_line.find(' ')
93 | # print("Split: {}".format(split))
94 | record_id = record_line[:split]
95 | # print("Record ID: ".format(record_id))
96 | record_name = record_line[split + 1:]
97 | # print("Record Name: ".format(record_name))
98 | return record_id, record_name
99 |
100 |
101 | def search_key(usb_dict, ids):
102 | """
103 | Compare provided IDs to the built USB dictionary. If found,
104 | it will return the common name, otherwise returns the string
105 | "unknown".
106 | """
107 | vendor_key = ids[0]
108 | product_key = ids[1]
109 |
110 | vendor, vendor_data = usb_dict.get(vendor_key, ['unknown', {}])
111 | product = 'unknown'
112 | if vendor != 'unknown':
113 | product = vendor_data.get(product_key, 'unknown')
114 |
115 | return vendor, product
116 |
--------------------------------------------------------------------------------
/Chapter13/plugins/helper/utility.py:
--------------------------------------------------------------------------------
1 | import binascii
2 | from datetime import datetime, timedelta
3 | import logging
4 |
5 | """
6 | MIT License
7 | Copyright (c) 2018 Chapin Bryce, Preston Miller
8 | Please share comments and questions at:
9 | https://github.com/PythonForensics/Learning-Python-for-Forensics
10 | or email pyforcookbook@gmail.com
11 |
12 | Permission is hereby granted, free of charge, to any person
13 | obtaining a copy of this software and associated documentation
14 | files (the "Software"), to deal in the Software without
15 | restriction, including without limitation the rights to use,
16 | copy, modify, merge, publish, distribute, sublicense, and/or
17 | sell copies of the Software, and to permit persons to whom the
18 | Software is furnished to do so, subject to the following
19 | conditions:
20 |
21 | The above copyright notice and this permission notice shall be
22 | included in all copies or substantial portions of the Software.
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
25 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
27 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
28 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
29 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 | OTHER DEALINGS IN THE SOFTWARE.
31 | """
32 |
33 |
34 | def check_header(filename, headers, size):
35 | """
36 | The check_header function reads a supplied size of the file
37 | and checks against known signatures to determine the file
38 | type.
39 | :param filename: The name of the file.
40 | :param headers: A list of known file signatures for the
41 | file type(s).
42 | :param size: The amount of data to read from the file for
43 | signature verification.
44 | :return: Boolean, True if the signatures match;
45 | otherwise, False.
46 | """
47 | with open(filename, 'rb') as infile:
48 | header = infile.read(size)
49 | hex_header = binascii.hexlify(header).decode('utf-8')
50 | for signature in headers:
51 | if hex_header == signature:
52 | return True
53 | else:
54 | pass
55 | logging.warn(('The signature for {} ({}) does not match '
56 | 'known signatures: {}').format(
57 | filename, hex_header, headers))
58 | return False
59 |
60 |
61 | def convert_size(size):
62 | """
63 | The convert_size function converts an integer representing
64 | bytes into a human-readable format.
65 | :param size: The size in bytes of a file
66 | :return: The human-readable size.
67 | """
68 | sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
69 | index = 0
70 | while size > 1024:
71 | size /= 1024.
72 | index += 1
73 | return '{:.2f} {}'.format(size, sizes[index])
74 |
75 |
76 | def file_time(ft):
77 | """
78 | The fileTime function converts Windows FILETIME objects into
79 | human readable value
80 | :param ft: the FILETIME to convert
81 | :return: date_str, the human readable datetime value
82 | """
83 | if ft is not None and ft != 0:
84 | return datetime(1601, 1, 1) + timedelta(microseconds=ft / 10)
85 | else:
86 | return 0
--------------------------------------------------------------------------------
/Chapter13/plugins/id3.py:
--------------------------------------------------------------------------------
1 | import os
2 | from time import gmtime, strftime
3 | from helper import utility
4 | from mutagen import mp3, id3
5 |
6 | """
7 | MIT License
8 | Copyright (c) 2018 Chapin Bryce, Preston Miller
9 | Please share comments and questions at:
10 | https://github.com/PythonForensics/Learning-Python-for-Forensics
11 | or email pyforcookbook@gmail.com
12 |
13 | Permission is hereby granted, free of charge, to any person
14 | obtaining a copy of this software and associated documentation
15 | files (the "Software"), to deal in the Software without
16 | restriction, including without limitation the rights to use,
17 | copy, modify, merge, publish, distribute, sublicense, and/or
18 | sell copies of the Software, and to permit persons to whom the
19 | Software is furnished to do so, subject to the following
20 | conditions:
21 |
22 | The above copyright notice and this permission notice shall be
23 | included in all copies or substantial portions of the Software.
24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
26 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
28 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
29 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
30 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
31 | OTHER DEALINGS IN THE SOFTWARE.
32 | """
33 |
34 |
35 | def main(filename):
36 | """
37 | The main function confirms the file type and sends it to
38 | be processed.
39 | :param filename: name of the file potentially containing exif
40 | metadata.
41 | :return: A dictionary from get_tags, containing the embedded
42 | EXIF metadata.
43 | """
44 |
45 | # MP3 signatures
46 | signatures = ['494433']
47 | if utility.check_header(filename, signatures, 3) is True:
48 | return get_tags(filename)
49 | else:
50 | raise TypeError
51 |
52 |
53 | def get_tags(filename):
54 | """
55 | The get_tags function extracts the ID3 metadata from the data
56 | object.
57 | :param filename: the path and name to the data object.
58 | :return: tags and headers, tags is a dictionary containing ID3
59 | metadata and headers are the order of keys for the CSV output.
60 | """
61 |
62 | # Set up CSV headers
63 | header = ['Path', 'Name', 'Size', 'Filesystem CTime',
64 | 'Filesystem MTime', 'Title', 'Subtitle', 'Artist', 'Album',
65 | 'Album/Artist', 'Length (Sec)', 'Year', 'Category',
66 | 'Track Number', 'Comments', 'Publisher', 'Bitrate',
67 | 'Sample Rate', 'Encoding', 'Channels', 'Audio Layer']
68 | tags = {}
69 | tags['Path'] = filename
70 | tags['Name'] = os.path.basename(filename)
71 | tags['Size'] = utility.convert_size(
72 | os.path.getsize(filename))
73 | tags['Filesystem CTime'] = strftime('%m/%d/%Y %H:%M:%S',
74 | gmtime(os.path.getctime(filename)))
75 | tags['Filesystem MTime'] = strftime('%m/%d/%Y %H:%M:%S',
76 | gmtime(os.path.getmtime(filename)))
77 |
78 | # MP3 Specific metadata
79 | audio = mp3.MP3(filename)
80 | if 'TENC' in audio.keys():
81 | tags['Encoding'] = audio['TENC'][0]
82 | tags['Bitrate'] = audio.info.bitrate
83 | tags['Channels'] = audio.info.channels
84 | tags['Audio Layer'] = audio.info.layer
85 | tags['Length (Sec)'] = audio.info.length
86 | tags['Sample Rate'] = audio.info.sample_rate
87 |
88 | # ID3 embedded metadata tags
89 | id = id3.ID3(filename)
90 | if 'TPE1' in id.keys():
91 | tags['Artist'] = id['TPE1'][0]
92 | if 'TRCK' in id.keys():
93 | tags['Track Number'] = id['TRCK'][0]
94 | if 'TIT3' in id.keys():
95 | tags['Subtitle'] = id['TIT3'][0]
96 | if 'COMM::eng' in id.keys():
97 | tags['Comments'] = id['COMM::eng'][0]
98 | if 'TDRC' in id.keys():
99 | tags['Year'] = id['TDRC'][0]
100 | if 'TALB' in id.keys():
101 | tags['Album'] = id['TALB'][0]
102 | if 'TIT2' in id.keys():
103 | tags['Title'] = id['TIT2'][0]
104 | if 'TCON' in id.keys():
105 | tags['Category'] = id['TCON'][0]
106 | if 'TPE2' in id.keys():
107 | tags['Album/Artist'] = id['TPE2'][0]
108 | if 'TPUB' in id.keys():
109 | tags['Publisher'] = id['TPUB'][0]
110 |
111 | return tags, header
112 |
--------------------------------------------------------------------------------
/Chapter13/plugins/office.py:
--------------------------------------------------------------------------------
1 | import zipfile
2 | import os
3 | from time import gmtime, strftime
4 | from helper import utility
5 |
6 | from lxml import etree
7 |
8 | """
9 | MIT License
10 | Copyright (c) 2018 Chapin Bryce, Preston Miller
11 | Please share comments and questions at:
12 | https://github.com/PythonForensics/Learning-Python-for-Forensics
13 | or email pyforcookbook@gmail.com
14 |
15 | Permission is hereby granted, free of charge, to any person
16 | obtaining a copy of this software and associated documentation
17 | files (the "Software"), to deal in the Software without
18 | restriction, including without limitation the rights to use,
19 | copy, modify, merge, publish, distribute, sublicense, and/or
20 | sell copies of the Software, and to permit persons to whom the
21 | Software is furnished to do so, subject to the following
22 | conditions:
23 |
24 | The above copyright notice and this permission notice shall be
25 | included in all copies or substantial portions of the Software.
26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
28 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
30 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
31 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
33 | OTHER DEALINGS IN THE SOFTWARE.
34 | """
35 |
36 |
37 | def main(filename):
38 |
39 | """
40 | The main function confirms the file type and sends it
41 | to be processed.
42 | :param filename: name of the file potentially containing
43 | embedded metadata.
44 | :return: A dictionary from getTags, containing the embedded
45 | metadata.
46 | """
47 |
48 | # DOCX, XLSX, and PPTX signatures
49 | signatures = ['504b030414000600']
50 | if utility.check_header(filename, signatures, 8) is True:
51 | return get_tags(filename)
52 | else:
53 | raise TypeError
54 |
55 |
56 | def get_tags(filename):
57 | """
58 | The get_tags function extracts the office metadata from the
59 | data object.
60 | :param filename: the path and name to the data object.
61 | :return: tags and headers, tags is a dictionary containing
62 | office metadata and headers are the order of keys for the CSV
63 | output.
64 | """
65 |
66 | # Set up CSV headers
67 | headers = ['Path', 'Name', 'Size', 'Filesystem CTime',
68 | 'Filesystem MTime', 'Title', 'Author(s)','Create Date',
69 | 'Modify Date', 'Last Modified By Date', 'Subject', 'Keywords',
70 | 'Description', 'Category', 'Status', 'Revision',
71 | 'Edit Time (Min)', 'Page Count', 'Word Count',
72 | 'Character Count', 'Line Count',
73 | 'Paragraph Count', 'Slide Count', 'Note Count',
74 | 'Hidden Slide Count', 'Company', 'Hyperlink Base']
75 |
76 | # Create a ZipFile class from the input object
77 | # This allows us to read or write to the 'Zip archive'
78 | try:
79 | zf = zipfile.ZipFile(filename)
80 | except zipfile.BadZipfile:
81 | return {}, headers
82 |
83 | # These two XML files contain the embedded metadata of
84 | # interest
85 | try:
86 | core = etree.fromstring(zf.read('docProps/core.xml'))
87 | app = etree.fromstring(zf.read('docProps/app.xml'))
88 | except KeyError as e:
89 | assert Warning(e)
90 | return {}, headers
91 |
92 | tags = {}
93 | tags['Path'] = filename
94 | tags['Name'] = os.path.basename(filename)
95 | tags['Size'] = utility.convert_size(
96 | os.path.getsize(filename))
97 | tags['Filesystem CTime'] = strftime('%m/%d/%Y %H:%M:%S',
98 | gmtime(os.path.getctime(filename)))
99 | tags['Filesystem MTime'] = strftime('%m/%d/%Y %H:%M:%S',
100 | gmtime(os.path.getmtime(filename)))
101 |
102 | # Core Tags
103 |
104 | for child in core.iterchildren():
105 |
106 | if 'title' in child.tag:
107 | tags['Title'] = child.text
108 | if 'subject' in child.tag:
109 | tags['Subject'] = child.text
110 | if 'creator' in child.tag:
111 | tags['Author(s)'] = child.text
112 | if 'keywords' in child.tag:
113 | tags['Keywords'] = child.text
114 | if 'description' in child.tag:
115 | tags['Description'] = child.text
116 | if 'lastModifiedBy' in child.tag:
117 | tags['Last Modified By Date'] = child.text
118 | if 'created' in child.tag:
119 | tags['Create Date'] = child.text
120 | if 'modified' in child.tag:
121 | tags['Modify Date'] = child.text
122 | if 'category' in child.tag:
123 | tags['Category'] = child.text
124 | if 'contentStatus' in child.tag:
125 | tags['Status'] = child.text
126 |
127 | if (filename.endswith('.docx') or
128 | filename.endswith('.pptx')):
129 | if 'revision' in child.tag:
130 | tags['Revision'] = child.text
131 |
132 | # App Tags
133 | for child in app.iterchildren():
134 |
135 | if filename.endswith('.docx'):
136 | if 'TotalTime' in child.tag:
137 | tags['Edit Time (Min)'] = child.text
138 | if 'Pages' in child.tag:
139 | tags['Page Count'] = child.text
140 | if 'Words' in child.tag:
141 | tags['Word Count'] = child.text
142 | if 'Characters' in child.tag:
143 | tags['Character Count'] = child.text
144 | if 'Lines' in child.tag:
145 | tags['Line Count'] = child.text
146 | if 'Paragraphs' in child.tag:
147 | tags['Paragraph Count'] = child.text
148 | if 'Company' in child.tag:
149 | tags['Company'] = child.text
150 | if 'HyperlinkBase' in child.tag:
151 | tags['Hyperlink Base'] = child.text
152 |
153 | elif filename.endswith('.pptx'):
154 | if 'TotalTime' in child.tag:
155 | tags['Edit Time (Min)'] = child.text
156 | if 'Words' in child.tag:
157 | tags['Word Count'] = child.text
158 | if 'Paragraphs' in child.tag:
159 | tags['Paragraph Count'] = child.text
160 | if 'Slides' in child.tag:
161 | tags['Slide Count'] = child.text
162 | if 'Notes' in child.tag:
163 | tags['Note Count'] = child.text
164 | if 'HiddenSlides' in child.tag:
165 | tags['Hidden Slide Count'] = child.text
166 | if 'Company' in child.tag:
167 | tags['Company'] = child.text
168 | if 'HyperlinkBase' in child.tag:
169 | tags['Hyperlink Base'] = child.text
170 | else:
171 | if 'Company' in child.tag:
172 | tags['Company'] = child.text
173 | if 'HyperlinkBase' in child.tag:
174 | tags['Hyperlink Base'] = child.text
175 |
176 | return tags, headers
177 |
--------------------------------------------------------------------------------
/Chapter13/plugins/pst_indexer.py:
--------------------------------------------------------------------------------
1 | import pypff
2 |
3 | """
4 | MIT License
5 | Copyright (c) 2018 Chapin Bryce, Preston Miller
6 | Please share comments and questions at:
7 | https://github.com/PythonForensics/Learning-Python-for-Forensics
8 | or email pyforcookbook@gmail.com
9 |
10 | Permission is hereby granted, free of charge, to any person
11 | obtaining a copy of this software and associated documentation
12 | files (the "Software"), to deal in the Software without
13 | restriction, including without limitation the rights to use,
14 | copy, modify, merge, publish, distribute, sublicense, and/or
15 | sell copies of the Software, and to permit persons to whom the
16 | Software is furnished to do so, subject to the following
17 | conditions:
18 |
19 | The above copyright notice and this permission notice shall be
20 | included in all copies or substantial portions of the Software.
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
28 | OTHER DEALINGS IN THE SOFTWARE.
29 | """
30 |
31 |
32 | def main(pst_file):
33 | """
34 | The main function opens a PST and calls functions to parse
35 | and report data from the PST
36 | :param pst_file: A string representing the path to the PST
37 | file to analyze
38 | :return: None
39 | """
40 | opst = pypff.open(pst_file)
41 | root = opst.get_root_folder()
42 |
43 | message_data = folder_traverse(root, [],
44 | **{'pst_name': pst_file, 'folder_name': 'root'})
45 |
46 | header = ['pst_name', 'folder_name', 'creation_time',
47 | 'submit_time', 'delivery_time', 'sender', 'subject', 'attachment_count']
48 |
49 | return message_data, header
50 |
51 |
52 | def folder_traverse(base, message_data, pst_name, folder_name):
53 | """
54 | The folder_traverse function walks through the base of the
55 | folder and scans for sub-folders and messages
56 | :param base: Base folder to scan for new items within the
57 | folder
58 | :param message_data: A list of data for output
59 | :param pst_name: A string representing the name of the PST
60 | file
61 | :param folder_name: A string representing the name of the
62 | folder
63 | :return: None
64 | """
65 | for folder in base.sub_folders:
66 | if folder.number_of_sub_folders:
67 | message_data = folder_traverse(folder, message_data,
68 | pst_name, folder.name)
69 | message_data = check_for_messages(folder, message_data,
70 | pst_name, folder.name)
71 | return message_data
72 |
73 |
74 | def check_for_messages(folder, message_data, pst_name, folder_name):
75 | """
76 | The check_for_messages function reads folder messages if
77 | present and passes them to the report function
78 | :param folder: pypff.Folder object
79 | :param message_data: list to pass and extend with message info
80 | :param pst_name: A string representing the name of the PST
81 | file
82 | :param folder_name: A string representing the name of the
83 | folder
84 | :return: Dictionary of results by folder
85 | """
86 | for message in folder.sub_messages:
87 | message_dict = process_message(message)
88 | message_dict['pst_name'] = pst_name
89 | message_dict['folder_name'] = folder_name
90 | message_data.append(message_dict)
91 | return message_data
92 |
93 |
94 | def process_message(message):
95 | """
96 | The process_message function processes multi-field messages to
97 | simplify collection of information
98 | :param message: The pypff.Message object
99 | :return: A dictionary with message fields (values) and their
100 | data (keys)
101 | """
102 | return {
103 | "subject": message.subject,
104 | "sender": message.sender_name,
105 | "header": message.transport_headers,
106 | "body": message.plain_text_body,
107 | "creation_time": message.creation_time,
108 | "submit_time": message.client_submit_time,
109 | "delivery_time": message.delivery_time,
110 | "attachment_count": message.number_of_attachments,
111 | }
112 |
--------------------------------------------------------------------------------
/Chapter13/plugins/setupapi.py:
--------------------------------------------------------------------------------
1 | from helper import usb_lookup
2 |
3 | """
4 | MIT License
5 | Copyright (c) 2018 Chapin Bryce, Preston Miller
6 | Please share comments and questions at:
7 | https://github.com/PythonForensics/Learning-Python-for-Forensics
8 | or email pyforcookbook@gmail.com
9 |
10 | Permission is hereby granted, free of charge, to any person
11 | obtaining a copy of this software and associated documentation
12 | files (the "Software"), to deal in the Software without
13 | restriction, including without limitation the rights to use,
14 | copy, modify, merge, publish, distribute, sublicense, and/or
15 | sell copies of the Software, and to permit persons to whom the
16 | Software is furnished to do so, subject to the following
17 | conditions:
18 |
19 | The above copyright notice and this permission notice shall be
20 | included in all copies or substantial portions of the Software.
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
28 | OTHER DEALINGS IN THE SOFTWARE.
29 | """
30 |
31 |
32 | def main(in_file):
33 | """
34 | Main function to handle operation
35 | :param in_file: Str - Path to setupapi log to analyze
36 | :return: list of USB data and list of headers for output
37 | """
38 | headers = ['Vendor ID', 'Vendor Name', 'Product ID',
39 | 'Product Name', 'Revision', 'UID',
40 | 'First Installation Date']
41 | data = []
42 |
43 | device_information = parse_setupapi(in_file)
44 | usb_ids = prep_usb_lookup()
45 | for device in device_information:
46 | parsed_info = parse_device_info(device)
47 | if isinstance(parsed_info, dict):
48 | parsed_info = get_device_names(usb_ids, parsed_info)
49 | data.append(parsed_info)
50 | else:
51 | pass
52 | return data, headers
53 |
54 |
55 | def parse_setupapi(setup_log):
56 | """
57 | Read data from provided file for Device Install Events for
58 | USB Devices
59 | :param setup_log: str - Path to valid setup api log
60 | :return: tuple of str - Device name and date
61 | """
62 | device_list = list()
63 | unique_list = set()
64 | with open(setup_log) as in_file:
65 | for line in in_file:
66 | lower_line = line.lower()
67 | if 'device install (hardware initiated)' in \
68 | lower_line and ('vid' in lower_line or
69 | 'ven' in lower_line):
70 | device_name = line.split('-')[1].strip()
71 | date = next(in_file).split('start')[1].strip()
72 | if device_name not in unique_list:
73 | device_list.append((device_name, date))
74 | unique_list.add(device_name)
75 |
76 | return device_list
77 |
78 |
79 | def parse_device_info(device_info):
80 | """
81 | Parses Vendor, Product, Revision and UID from a Setup API
82 | entry
83 | :param device_info: string of device information to parse
84 | :return: dictionary of parsed information or original string
85 | if error
86 | """
87 | # Initialize variables
88 | vid = ''
89 | pid = ''
90 | rev = ''
91 | uid = ''
92 |
93 | # Split string into segments on \\
94 | segments = device_info[0].split('\\')
95 |
96 | if 'usb' not in segments[0].lower():
97 | return None
98 | # Eliminate non-USB devices from output
99 | # May hide other storage devices
100 |
101 | for item in segments[1].split('&'):
102 | lower_item = item.lower()
103 | if 'ven' in lower_item or 'vid' in lower_item:
104 | vid = item.split('_', 1)[-1]
105 | elif 'dev' in lower_item or 'pid' in lower_item or \
106 | 'prod' in lower_item:
107 | pid = item.split('_', 1)[-1]
108 | elif 'rev' in lower_item or 'mi' in lower_item:
109 | rev = item.split('_', 1)[-1]
110 |
111 | if len(segments) >= 3:
112 | uid = segments[2].strip(']')
113 |
114 | if vid != '' or pid != '':
115 | return {'Vendor ID': vid.lower(),
116 | 'Product ID': pid.lower(),
117 | 'Revision': rev,
118 | 'UID': uid,
119 | 'First Installation Date': device_info[1]}
120 | # Unable to parse data, returning whole string
121 | return device_info
122 |
123 |
124 | def prep_usb_lookup(local_usb_ids=None):
125 | """
126 | Prepare the lookup of USB devices through accessing the most
127 | recent copy of the database at http://linux-usb.org/usb.ids
128 | or using the provided file and parsing it into a queriable
129 | dictionary format.
130 | """
131 | if local_usb_ids:
132 | usb_file = open(local_usb_ids, encoding='latin1')
133 | else:
134 | usb_file = usb_lookup.get_usb_file()
135 | return usb_lookup.parse_file(usb_file)
136 |
137 |
138 | def get_device_names(usb_dict, device_info):
139 | """
140 | Query `usb_lookup.py` for device information based on VID/PID.
141 | :param usb_dict: Dictionary from usb_lookup.py of known
142 | devices.
143 | :param device_info: Dictionary containing 'Vendor ID' and
144 | 'Product ID' keys and values.
145 | :return: original dictionary with 'Vendor Name' and
146 | 'Product Name' keys and values
147 | """
148 | device_name = usb_lookup.search_key(
149 | usb_dict, [device_info['Vendor ID'],
150 | device_info['Product ID']])
151 |
152 | device_info['Vendor Name'] = device_name[0]
153 | device_info['Product Name'] = device_name[1]
154 |
155 | return device_info
156 |
157 |
158 | def print_output(usb_information):
159 | """
160 | Print formatted information about USB Device
161 | :param usb_information: dictionary containing key/value
162 | data about each device or tuple of device information
163 | :return: None
164 | """
165 | print('{:-^15}'.format(''))
166 |
167 | if isinstance(usb_information, dict):
168 | for key_name, value_name in usb_information.items():
169 | print('{}: {}'.format(key_name, value_name))
170 | elif isinstance(usb_information, tuple):
171 | print('Device: {}'.format(usb_information[0]))
172 | print('Date: {}'.format(usb_information[1]))
173 |
174 |
--------------------------------------------------------------------------------
/Chapter13/plugins/userassist.py:
--------------------------------------------------------------------------------
1 | """UserAssist parser leveraging the YARP library."""
2 | import struct
3 | import sys
4 | import logging
5 |
6 | from helper import utility
7 |
8 | from yarp import Registry
9 |
10 | """
11 | MIT License
12 | Copyright (c) 2018 Chapin Bryce, Preston Miller
13 | Please share comments and questions at:
14 | https://github.com/PythonForensics/Learning-Python-for-Forensics
15 | or email pyforcookbook@gmail.com
16 |
17 | Permission is hereby granted, free of charge, to any person
18 | obtaining a copy of this software and associated documentation
19 | files (the "Software"), to deal in the Software without
20 | restriction, including without limitation the rights to use,
21 | copy, modify, merge, publish, distribute, sublicense, and/or
22 | sell copies of the Software, and to permit persons to whom the
23 | Software is furnished to do so, subject to the following
24 | conditions:
25 |
26 | The above copyright notice and this permission notice shall be
27 | included in all copies or substantial portions of the Software.
28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
29 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
30 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
31 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
32 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
33 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
34 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
35 | OTHER DEALINGS IN THE SOFTWARE.
36 | """
37 |
38 | # KEYS will contain sub-lists of each parsed UserAssist (UA) key
39 | KEYS = []
40 |
41 |
42 | def main(registry, **kwargs):
43 | """
44 | The main function handles main logic of script.
45 | :param registry: Registry hive to process
46 | :return: Nothing.
47 | """
48 | if utility.check_header(registry, ['72656766'], 4) is not True:
49 | logging.error('Incorrect file detected based on name')
50 | raise TypeError
51 | # Create dictionary of ROT-13 decoded UA key and its value
52 | apps = create_dictionary(registry)
53 | ua_type = parse_values(apps)
54 |
55 | if ua_type == 0:
56 | logging.info('Detected XP based Userassist values.')
57 |
58 | else:
59 | logging.info('Detected Win7 based Userassist values.')
60 |
61 | headers = ['Name', 'Path', 'Session ID', 'Count',
62 | 'Last Used Date (UTC)', 'Focus Time (ms)', 'Focus Count']
63 | return KEYS, headers
64 |
65 |
66 | def create_dictionary(registry):
67 | """
68 | The create_dictionary function creates a list of dictionaries
69 | where keys are the ROT-13 decoded app names and values are
70 | the raw hex data of said app.
71 | :param registry: Registry Hive to process
72 | :return: apps_list, A list containing dictionaries for
73 | each app
74 | """
75 | try:
76 | # Open the registry file to be parsed
77 | registry_file = open(registry, "rb")
78 | reg = Registry.RegistryHive(registry_file)
79 | except (IOError, UnicodeDecodeError) as e:
80 | msg = 'Invalid NTUSER.DAT path or Registry ID.'
81 | logging.error(msg)
82 | raise TypeError
83 |
84 | # Navigate to the UserAssist key
85 | ua_key = reg.find_key(
86 | ('SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer'
87 | '\\UserAssist'))
88 | if ua_key is None:
89 | msg = 'UserAssist Key not found in Registry file.'
90 | logging.error(msg)
91 | raise TypeError
92 | apps_list = []
93 | # Loop through each subkey in the UserAssist key
94 | for ua_subkey in ua_key.subkeys():
95 | # For each subkey in the UserAssist key, detect a subkey
96 | # called Count that has more than 0 values to parse.
97 | if(ua_subkey.subkey('Count') and
98 | ua_subkey.subkey('Count').values_count() > 0):
99 | apps = {}
100 | for v in ua_subkey.subkey('Count').values():
101 | if sys.version_info[0] == 2:
102 | apps[v.name().encode('utf-8').decode(
103 | 'rot-13')] = v.data_raw()
104 | elif sys.version_info[0] == 3:
105 | import codecs
106 | enc = codecs.getencoder('rot-13')
107 | apps[enc(str(v.name()))[0]] = v.data_raw()
108 |
109 | apps_list.append(apps)
110 | return apps_list
111 |
112 |
113 | def parse_values(data):
114 | """
115 | The parse_values function uses struct to unpack the raw value
116 | data from the UA key
117 | :param data: A list containing dictionaries of UA
118 | application data
119 | :return: ua_type, based on the size of the raw data from
120 | the dictionary values.
121 | """
122 | ua_type = -1
123 | msg = 'Parsing UserAssist values.'
124 | logging.info(msg)
125 |
126 | for dictionary in data:
127 | for v in dictionary.keys():
128 | # WinXP based UA keys are 16 bytes
129 | if len(dictionary[v]) == 16:
130 | raw = struct.unpack('<2iq', dictionary[v])
131 | ua_type = 0
132 | KEYS.append({'Name': get_name(v), 'Path': v,
133 | 'Session ID': raw[0], 'Count': raw[1],
134 | 'Last Used Date (UTC)': utility.file_time(raw[2]),
135 | 'Focus Time (ms)': '', 'Focus Count': ''})
136 | # Win7 based UA keys are 72 bytes
137 | elif len(dictionary[v]) == 72:
138 | raw = struct.unpack('<4i44xq4x', dictionary[v])
139 | ua_type = 1
140 | KEYS.append({'Name': get_name(v), 'Path': v,
141 | 'Session ID': raw[0], 'Count': raw[1],
142 | 'Last Used Date (UTC)': utility.file_time(raw[4]),
143 | 'Focus Time (ms)': raw[3],'Focus Count': raw[2]})
144 | else:
145 | # If the key is not WinXP or Win7 based -- ignore.
146 | msg = 'Ignoring {} value that is {} bytes'.format(
147 | str(v), str(len(dictionary[v])))
148 | logging.info(msg)
149 | continue
150 | return ua_type
151 |
152 |
153 | def get_name(full_name):
154 | """
155 | the get_name function splits the name of the application
156 | returning the executable name and ignoring the
157 | path details.
158 | :param full_name: the path and executable name
159 | :return: the executable name
160 | """
161 | # Determine if '\\' and ':' are within the full_name
162 | if ':' in full_name and '\\' in full_name:
163 | # Find if ':' comes before '\\'
164 | if full_name.rindex(':') > full_name.rindex('\\'):
165 | # Split on ':' and return the last element
166 | # (the executable)
167 | return full_name.split(':')[-1]
168 | else:
169 | # Otherwise split on '\\'
170 | return full_name.split('\\')[-1]
171 | # When just ':' or '\\' is in the full_name, split on
172 | # that item and return the last element (the executable)
173 | elif ':' in full_name:
174 | return full_name.split(':')[-1]
175 | else:
176 | return full_name.split('\\')[-1]
177 |
--------------------------------------------------------------------------------
/Chapter13/requirements.txt:
--------------------------------------------------------------------------------
1 | xlsxwriter==1.1.2
2 | colorama==0.4.1
3 | pyfiglet==0.8.post0
4 | mutagen==1.42.0
5 | pillow==5.4.0
6 | simplekml==1.3.1
7 | tqdm==4.28.1
8 | unicodecsv==0.14.1
9 | jinja2==2.10
10 | lxml==4.3.0
11 |
--------------------------------------------------------------------------------
/Chapter13/writers/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | sys.path.append(os.path.join(os.path.dirname(__file__)))
4 |
5 | import csv_writer
6 | import kml_writer
7 | import xlsx_writer
8 |
9 | """
10 | MIT License
11 | Copyright (c) 2018 Chapin Bryce, Preston Miller
12 | Please share comments and questions at:
13 | https://github.com/PythonForensics/Learning-Python-for-Forensics
14 | or email pyforcookbook@gmail.com
15 |
16 | Permission is hereby granted, free of charge, to any person
17 | obtaining a copy of this software and associated documentation
18 | files (the "Software"), to deal in the Software without
19 | restriction, including without limitation the rights to use,
20 | copy, modify, merge, publish, distribute, sublicense, and/or
21 | sell copies of the Software, and to permit persons to whom the
22 | Software is furnished to do so, subject to the following
23 | conditions:
24 |
25 | The above copyright notice and this permission notice shall be
26 | included in all copies or substantial portions of the Software.
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
34 | OTHER DEALINGS IN THE SOFTWARE.
35 | """
--------------------------------------------------------------------------------
/Chapter13/writers/csv_writer.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import sys
3 | import os
4 | if sys.version_info[0] == 2:
5 | import unicodecsv as csv
6 | elif sys.version_info[0] == 3:
7 | import csv
8 |
9 | """
10 | MIT License
11 | Copyright (c) 2018 Chapin Bryce, Preston Miller
12 | Please share comments and questions at:
13 | https://github.com/PythonForensics/Learning-Python-for-Forensics
14 | or email pyforcookbook@gmail.com
15 |
16 | Permission is hereby granted, free of charge, to any person
17 | obtaining a copy of this software and associated documentation
18 | files (the "Software"), to deal in the Software without
19 | restriction, including without limitation the rights to use,
20 | copy, modify, merge, publish, distribute, sublicense, and/or
21 | sell copies of the Software, and to permit persons to whom the
22 | Software is furnished to do so, subject to the following
23 | conditions:
24 |
25 | The above copyright notice and this permission notice shall be
26 | included in all copies or substantial portions of the Software.
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
29 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
31 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
32 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
33 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
34 | OTHER DEALINGS IN THE SOFTWARE.
35 | """
36 |
37 |
38 | def writer(output, headers, output_data, **kwargs):
39 | """
40 | The writer function uses the csv.DictWriter module to write
41 | list(s) of dictionaries. The DictWriter can take a fieldnames
42 | argument, as a list, which represents the desired order of
43 | columns.
44 | :param output: The name of the output CSV.
45 | :param headers: A list of keys in the dictionary that
46 | represent the desired order of columns in the output.
47 | :param output_data: The list of dictionaries containing
48 | embedded metadata.
49 | :return: None
50 | """
51 |
52 | if sys.version_info[0] == 2:
53 | csvfile = open(output, "wb")
54 | elif sys.version_info[0] == 3:
55 | csvfile = open(output, "w", newline='',
56 | encoding='utf-8')
57 |
58 | with csvfile:
59 | # We use DictWriter instead of writer to write
60 | # dictionaries to CSV.
61 | w = csv.DictWriter(csvfile, fieldnames=headers,
62 | extrasaction='ignore')
63 |
64 | # Writerheader writes the header based on the supplied
65 | # headers object
66 | try:
67 | w.writeheader()
68 | except TypeError:
69 | print(('[-] Received empty headers...\n'
70 | '[-] Skipping writing output.'))
71 | return
72 |
73 | if 'recursion' in kwargs.keys():
74 | for l in output_data:
75 | for data in l:
76 | if data:
77 | w.writerow(data)
78 | else:
79 | for data in output_data:
80 | if data:
81 | w.writerow(data)
--------------------------------------------------------------------------------
/Chapter13/writers/kml_writer.py:
--------------------------------------------------------------------------------
1 | import os
2 | import simplekml
3 |
4 | """
5 | MIT License
6 | Copyright (c) 2018 Chapin Bryce, Preston Miller
7 | Please share comments and questions at:
8 | https://github.com/PythonForensics/Learning-Python-for-Forensics
9 | or email pyforcookbook@gmail.com
10 |
11 | Permission is hereby granted, free of charge, to any person
12 | obtaining a copy of this software and associated documentation
13 | files (the "Software"), to deal in the Software without
14 | restriction, including without limitation the rights to use,
15 | copy, modify, merge, publish, distribute, sublicense, and/or
16 | sell copies of the Software, and to permit persons to whom the
17 | Software is furnished to do so, subject to the following
18 | conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 | OTHER DEALINGS IN THE SOFTWARE.
30 | """
31 |
32 | def writer(output, output_name, output_data):
33 | """
34 | The writer function writes JPEG and TIFF EXIF GPS data to a
35 | Google Earth KML file. This file can be opened in Google
36 | Earth and will use the GPS coordinates to create 'pins' on
37 | the map of the taken photo's location.
38 | :param output: The output directory to write the KML file.
39 | :param output_name: The name of the output KML file.
40 | :param output_data: The embedded EXIF metadata to be written
41 | :return:
42 | """
43 |
44 | kml = simplekml.Kml(name=output_name)
45 | for exif in output_data:
46 | if('Latitude' in exif.keys() and
47 | 'Latitude Reference' in exif.keys() and
48 | 'Longitude Reference' in exif.keys() and
49 | 'Longitude' in exif.keys()):
50 |
51 | if 'Original Date' in exif.keys():
52 | dt = exif['Original Date']
53 | else:
54 | dt = 'N/A'
55 |
56 | if exif['Latitude Reference'] == 'S':
57 | latitude = '-' + exif['Latitude']
58 | else:
59 | latitude = exif['Latitude']
60 |
61 | if exif['Longitude Reference'] == 'W':
62 | longitude = '-' + exif['Longitude']
63 | else:
64 | longitude = exif['Longitude']
65 |
66 | kml.newpoint(name=exif['Name'],
67 | description='Originally Created: ' + dt,
68 | coords=[(longitude, latitude)])
69 | else:
70 | pass
71 | kml.save(os.path.join(output, output_name))
72 |
--------------------------------------------------------------------------------
/Chapter13/writers/xlsx_writer.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import xlsxwriter
3 |
4 | """
5 | MIT License
6 | Copyright (c) 2018 Chapin Bryce, Preston Miller
7 | Please share comments and questions at:
8 | https://github.com/PythonForensics/Learning-Python-for-Forensics
9 | or email pyforcookbook@gmail.com
10 |
11 | Permission is hereby granted, free of charge, to any person
12 | obtaining a copy of this software and associated documentation
13 | files (the "Software"), to deal in the Software without
14 | restriction, including without limitation the rights to use,
15 | copy, modify, merge, publish, distribute, sublicense, and/or
16 | sell copies of the Software, and to permit persons to whom the
17 | Software is furnished to do so, subject to the following
18 | conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 | OTHER DEALINGS IN THE SOFTWARE.
30 | """
31 |
32 | ALPHABET = [chr(i) for i in range(ord('A'), ord('Z') + 1)]
33 |
34 |
35 | def writer(output, headers, output_data, **kwargs):
36 | """
37 | The writer function writes excel output for the framework
38 | :param output: the output filename for the excel spreadsheet
39 | :param headers: the name of the spreadsheet columns
40 | :param output_data: the data to be written to the excel
41 | spreadsheet
42 | :return: Nothing
43 | """
44 | wb = xlsxwriter.Workbook(output)
45 |
46 | if headers is None:
47 | print('[-] Received empty headers... \n'
48 | '[-] Skipping writing output.')
49 | return
50 |
51 | if len(headers) <= 26:
52 | title_length = ALPHABET[len(headers) - 1]
53 | else:
54 | title_length = 'Z'
55 |
56 | ws = add_worksheet(wb, title_length)
57 |
58 | if 'recursion' in kwargs.keys():
59 | for i, data in enumerate(output_data):
60 | if i > 0:
61 | ws = add_worksheet(wb, title_length)
62 | cell_length = len(data)
63 | tmp = []
64 | for dictionary in data:
65 | tmp.append(
66 | [str(dictionary[x]) if x in dictionary.keys() else '' for x in headers]
67 | )
68 |
69 | ws.add_table(
70 | 'A3:' + title_length + str(3 + cell_length),
71 | {'data': tmp,
72 | 'columns': [{'header': x} for x in headers]})
73 |
74 | else:
75 | cell_length = len(output_data)
76 | tmp = []
77 | for data in output_data:
78 | tmp.append([str(data[x]) if x in data.keys() else '' for x in headers])
79 | ws.add_table(
80 | 'A3:' + title_length + str(3 + cell_length),
81 | {'data': tmp,
82 | 'columns': [{'header': x} for x in headers]})
83 |
84 | wb.close()
85 |
86 |
87 | def add_worksheet(wb, length, name=None):
88 | """
89 | The add_worksheet function creates a new formatted worksheet
90 | in the workbook
91 | :param wb: The workbook object
92 | :param length: The range of rows to merge
93 | :param name: The name of the worksheet
94 | :return: ws, the worksheet
95 | """
96 | title_format = wb.add_format({'bold': True,
97 | 'font_color': 'black', 'bg_color': 'white', 'font_size': 30,
98 | 'font_name': 'Arial', 'align': 'center'})
99 | ws = wb.add_worksheet(name)
100 |
101 | ws.merge_range('A1:' + length + '1', 'XYZ Corp',
102 | title_format)
103 | ws.merge_range('A2:' + length + '2', 'Case ####',
104 | title_format)
105 | return ws
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Packt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Learning Python for Forensics - Second Edition
2 |
3 |
4 |
5 | This is the code repository for [Learning Python for Forensics - Second Edition](https://www.packtpub.com/networking-and-servers/learning-python-forensics-second-edition?utm_source=github&utm_medium=repository&utm_campaign=9781789341690), published by Packt.
6 |
7 | **Leverage the power of Python in forensic investigations**
8 |
9 | ## What is this book about?
10 | Digital forensics plays an integral role in solving complex cybercrimes and helping organizations make sense of cybersecurity incidents. This second edition of Learning Python for Forensics illustrates how Python can be used to support these digital investigations and permits the examiner to automate the parsing of forensic artifacts to spend more time examining actionable data.
11 |
12 | This book covers the following exciting features:
13 | * Learn how to develop Python scripts to solve complex forensic problems
14 | * Build scripts using an iterative design
15 | * Design code to accommodate present and future hurdles
16 | * Leverage built-in and community-sourced libraries
17 | * Understand the best practices in forensic programming
18 |
19 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1789341698) today!
20 |
21 |
23 |
24 |
25 | ## Instructions and Navigations
26 | All of the code is organized into folders. For example, Chapter02.
27 |
28 | The code will look like the following:
29 | ```
30 | >>> epoch_timestamp = 874281600
31 | >>> datetime_timestamp = datetime.datetime.utcfromtimestamp(epoch_timestamp)
32 | ```
33 |
34 | **Following is what you need for this book:**
35 | If you are a forensics student, hobbyist, or professional seeking to increase your understanding in forensics through the use of a programming language, then Learning Python for Forensics is for you. You are not required to have previous experience in programming to learn and master the content within this book. This material, created by forensic professionals, was written with a unique perspective and understanding for examiners who wish to learn programming.
36 |
37 | With the following software and hardware list you can run all code files present in the book.
38 |
39 | ### Software and Hardware List
40 |
41 | | Chapter | Software required | OS required |
42 | | -------- | ------------------------------------| -------------------|
43 | | 1 - 13 | Python 2.7 or 3.7, IDE for Python, | Windows, Linux |
44 | | | and SQLite | |
45 |
46 |
47 |
48 | We also provide a PDF file that has color images of the screenshots/diagrams used in this book. [Click here to download it](https://www.packtpub.com/sites/default/files/downloads/9781789341690_ColorImages.pdf).
49 |
50 |
51 | ### Related products
52 | * Mastering Reverse Engineering [[Packt]](https://www.packtpub.com/networking-and-servers/mastering-reverse-engineering?utm_source=github&utm_medium=repository&utm_campaign=9781788838849) [[Amazon]](https://www.amazon.com/dp/B07BXTBP8W)
53 |
54 | * Learning Android Forensics - Second Edition [[Packt]](https://www.packtpub.com/networking-and-servers/learning-android-forensics-second-edition?utm_source=github&utm_medium=repository&utm_campaign=9781789131017) [[Amazon]](https://www.amazon.com/dp/1789131014)
55 |
56 | ## Get to Know the Author(s)
57 | **Preston Miller**
58 | is a consultant at an internationally recognized risk management firm. Preston holds an undergraduate degree from Vassar College and a master's degree in digital forensics from Marshall University. While at Marshall, Preston unanimously received the prestigious J. Edgar Hoover Foundation's scientific scholarship. Preston is a published author, recently of Python Digital Forensics Cookbook, which won the Forensic 4:cast Digital Forensics Book of the Year award in 2018. Preston is a member of the GIAC advisory board and holds multiple industry-recognized certifications in his field.
59 |
60 | **Chapin Bryce**
61 | is a consultant at a global firm that is a leader in digital forensics and incident response investigations. After graduating from Champlain College with a bachelor's degree in computer and digital forensics, Chapin dove into the field of digital forensics and incident response joining the GIAC advisory board and earning four GIAC certifications: GCIH, GCFE, GCFA, and GNFA. As a member of multiple ongoing research and development projects, he has authored several books and articles in professional and academic publications, including Python Digital Forensics Cookbook (Forensic 4:Cast Digital Forensics Book of the Year, 2018), Learning Python for Forensics, First Edition, and Digital Forensic Magazine.
62 |
63 |
64 | ## Other books by the authors
65 | * [Python Digital Forensics Cookbook](https://www.packtpub.com/networking-and-servers/python-digital-forensics-cookbook?utm_source=github&utm_medium=repository&utm_campaign=9781783987467)
66 | * [Learning Python for Forensics](https://www.packtpub.com/networking-and-servers/learning-python-forensics?utm_source=github&utm_medium=repository&utm_campaign=9781783285235)
67 |
68 | ### Suggestions and Feedback
69 | [Click here](https://docs.google.com/forms/d/e/1FAIpQLSdy7dATC6QmEL81FIUuymZ0Wy9vH1jHkvpY57OiMeKGqib_Ow/viewform) if you have any feedback or suggestions.
70 |
--------------------------------------------------------------------------------