Restructure of code for easier reference.
* Updated doc strings * Added examples * Addressed linting issues
This commit is contained in:
parent
77c41e1a55
commit
46cced1881
|
|
@ -1,4 +1,4 @@
|
||||||
Section 1 - Essential Scripts
|
Chapter 1 - Essential Scripts
|
||||||
==============================
|
==============================
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
@ -6,27 +6,27 @@ Section 1 - Essential Scripts
|
||||||
|
|
||||||
Section 1.1 - Argparse Example
|
Section 1.1 - Argparse Example
|
||||||
--------------------------------
|
--------------------------------
|
||||||
.. automodule:: pyforhandbook.section_01.argparse_example
|
.. automodule:: pyforhandbook.ch01_essentials.argparse_example
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Section 1.2 - Logging Example
|
Section 1.2 - Logging Example
|
||||||
--------------------------------
|
--------------------------------
|
||||||
.. automodule:: pyforhandbook.section_01.logging_example
|
.. automodule:: pyforhandbook.ch01_essentials.logging_example
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Section 1.3 - Open Files
|
Section 1.3 - Open Files
|
||||||
------------------------
|
------------------------
|
||||||
.. automodule:: pyforhandbook.section_01.open_files
|
.. automodule:: pyforhandbook.ch01_essentials.open_files
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Section 1.4 - CSV Example
|
Section 1.4 - CSV Example
|
||||||
--------------------------------
|
--------------------------------
|
||||||
.. automodule:: pyforhandbook.section_01.csv_example
|
.. automodule:: pyforhandbook.ch01_essentials.csv_example
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Section 1.5 - Directory Recursion
|
Section 1.5 - Directory Recursion
|
||||||
---------------------------------
|
---------------------------------
|
||||||
.. automodule:: pyforhandbook.section_01.recursion_example
|
.. automodule:: pyforhandbook.ch01_essentials.recursion_example
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
Section 2 - Registry Parsing
|
Chapter 2 - Registry Parsing
|
||||||
==============================
|
==============================
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
@ -6,12 +6,12 @@ Section 2 - Registry Parsing
|
||||||
|
|
||||||
Section 2.1 - Opening a Hive
|
Section 2.1 - Opening a Hive
|
||||||
--------------------------------
|
--------------------------------
|
||||||
.. automodule:: pyforhandbook.section_02.yarp_base
|
.. automodule:: pyforhandbook.ch02_registry.yarp_base
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Section 2.2 - Parsing Hive Values
|
Section 2.2 - Parsing Hive Values
|
||||||
----------------------------------
|
----------------------------------
|
||||||
.. automodule:: pyforhandbook.section_02.yarp_ntuser
|
.. automodule:: pyforhandbook.ch02_registry.yarp_ntuser
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
Section 3 - Windows Event Log Parsing
|
Chapter 3 - Windows Event Log Parsing
|
||||||
=====================================
|
=====================================
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
@ -6,7 +6,7 @@ Section 3 - Windows Event Log Parsing
|
||||||
|
|
||||||
Section 3.1 - Using python-evtx
|
Section 3.1 - Using python-evtx
|
||||||
----------------------------------
|
----------------------------------
|
||||||
.. automodule:: pyforhandbook.section_03.using_python_evtx
|
.. automodule:: pyforhandbook.ch03_event_logs.using_python_evtx
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
Section 6 - Sqlite & MacOS/Mobile/Browsers
|
Chapter 6 - Sqlite & MacOS/Mobile/Browsers
|
||||||
==========================================
|
==========================================
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
@ -6,7 +6,7 @@ Section 6 - Sqlite & MacOS/Mobile/Browsers
|
||||||
|
|
||||||
Section 6.1 - Opening Sqlite
|
Section 6.1 - Opening Sqlite
|
||||||
--------------------------------
|
--------------------------------
|
||||||
.. automodule:: pyforhandbook.section_06.opening_sqlite
|
.. automodule:: pyforhandbook.ch06_databases.opening_sqlite
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|
@ -5,18 +5,18 @@
|
||||||
|
|
||||||
The Python Forensics Handbook
|
The Python Forensics Handbook
|
||||||
=============================
|
=============================
|
||||||
---------------------------------------------
|
--------------------------------------------------------
|
||||||
A guide for developing Python scripts in DFIR
|
A reference guide for developing Python scripts in DFIR
|
||||||
---------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
:caption: Table of Contents:
|
:caption: Table of Contents:
|
||||||
|
|
||||||
section1
|
ch01_essentials
|
||||||
section2
|
ch02_registry
|
||||||
section3
|
ch03_event_logs
|
||||||
section6
|
ch06_databases
|
||||||
|
|
||||||
Handbook Sections
|
Handbook Sections
|
||||||
==============================
|
==============================
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
"""
|
"""
|
||||||
IN DEVELOPMENT - More sections will release over the coming weeks/months/as
|
.. note::
|
||||||
time permits. Feel free to contribute as you have an idea or time to assist,
|
IN DEVELOPMENT - More sections will release over the coming weeks/months/as
|
||||||
otherwise stay tuned!
|
time permits. Feel free to contribute as you have an idea or time to assist,
|
||||||
|
otherwise stay tuned!
|
||||||
|
|
||||||
This handbook has 7 sections covering common tasks for developing
|
This handbook consists of 7 sections covering common tasks for developing
|
||||||
Python scripts for use in DFIR. Each section contains short,
|
Python scripts for use in DFIR. Each section contains short,
|
||||||
portable code blocks that can drop into a new script with minimal
|
portable code blocks that can drop into a new script with minimal
|
||||||
tweaking. This way, you can quickly build out your custom script
|
tweaking. This way, you can quickly build out your custom script
|
||||||
|
|
@ -19,7 +20,7 @@ is hosted on GitHub at https://github.com/chapinb/python-forensics-handbook and
|
||||||
available to read online at https://chapinb.com/python-forensics-handbook.
|
available to read online at https://chapinb.com/python-forensics-handbook.
|
||||||
Please consider submitting a pull request with your additions!
|
Please consider submitting a pull request with your additions!
|
||||||
|
|
||||||
Section 1 - Essential Script Elements
|
Chapter 1 - Essential Script Elements
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
This chapter covers code blocks that are useful across scripts
|
This chapter covers code blocks that are useful across scripts
|
||||||
|
|
@ -43,9 +44,12 @@ projects to allow for uniformity.
|
||||||
* Parallel Processing
|
* Parallel Processing
|
||||||
- Simple implementation of multithreading and multiprocessing
|
- Simple implementation of multithreading and multiprocessing
|
||||||
|
|
||||||
Section 2 - Registry Hives with YARP
|
Chapter 2 - Registry Hives
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
|
In this chapter, we demonstrate how to open a registry hive, navigate through
|
||||||
|
its keys, and interact with values to expose information for analysis.
|
||||||
|
|
||||||
* Using yarp to open a single hive
|
* Using yarp to open a single hive
|
||||||
- Opening a hive and recovering data available in transaction logs
|
- Opening a hive and recovering data available in transaction logs
|
||||||
* Parse registry hive keys and values
|
* Parse registry hive keys and values
|
||||||
|
|
@ -56,16 +60,20 @@ Section 2 - Registry Hives with YARP
|
||||||
* Searching for a pattern across hive keys and values.
|
* Searching for a pattern across hive keys and values.
|
||||||
- Looking for a provided pattern across the entire hive.
|
- Looking for a provided pattern across the entire hive.
|
||||||
|
|
||||||
Section 3 - Event Logs
|
Chapter 3 - Event Logs
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
The functions showcased in this chapter highlight methods to access events
|
||||||
|
within Windows event log files, iterating over the events, and extracting
|
||||||
|
useful records for further examination.
|
||||||
|
|
||||||
* Using python-evtx
|
* Using python-evtx
|
||||||
- Opening evtx files
|
- Opening evtx files
|
||||||
- Iterating over events
|
- Iterating over events
|
||||||
* Parsing Logins (with types, levels, privs)
|
* Parsing Logins
|
||||||
- Parse out the commonly investigated 4624/4672 events
|
- Parse out the commonly investigated 4624/4672 events
|
||||||
|
|
||||||
Section 4 - Text logs
|
Chapter 4 - Text logs
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
* Handling IIS Logs
|
* Handling IIS Logs
|
||||||
|
|
@ -75,30 +83,41 @@ Section 4 - Text logs
|
||||||
* Adding in GeoIP
|
* Adding in GeoIP
|
||||||
- Function to add GeoIP recognition
|
- Function to add GeoIP recognition
|
||||||
|
|
||||||
Section 5 - API calls & JSON data
|
Chapter 5 - API calls & JSON data
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
* VirusTotal
|
* VirusTotal
|
||||||
* HybridAnalysis
|
* HybridAnalysis
|
||||||
* Manipulating JSON
|
* Manipulating JSON
|
||||||
|
|
||||||
Section 6 - SQLite & macOS/mobile/browsers
|
Chapter 6 - Databases
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
|
Databases are found within many applications and operating systems. This chapter
|
||||||
|
covers methods to extract information from these common databases, along with
|
||||||
|
functions that are purpose built to parse information from frequently seen
|
||||||
|
database tables.
|
||||||
|
|
||||||
* macOS Activity
|
* macOS Activity
|
||||||
- KnowledgeC
|
- KnowledgeC
|
||||||
* Andriod SMS
|
* Android SMS
|
||||||
* Google Chome History DB
|
* Google Chrome History DB
|
||||||
|
|
||||||
Section 7 - Opening forensic images
|
Chapter 7 - Opening forensic images
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
|
Media acquisition and preservation formats are very common within DFIR and
|
||||||
|
the ability to extract specific contents from these files leads to faster
|
||||||
|
analysis and simplified usage of the tool you are building. With these functions
|
||||||
|
you can read files from a forensic image and pass them straight to your other
|
||||||
|
utilities for further parsing.
|
||||||
|
|
||||||
* LibEWF
|
* LibEWF
|
||||||
- Expose an E01 as a raw image
|
- Expose an E01 as a raw image
|
||||||
* PyTSK
|
* PyTSK
|
||||||
- Read data from a raw image (MBR)
|
- Read data from a raw image (MBR)
|
||||||
- Read data from a file (hashing)
|
- Read data from a file (hashing)
|
||||||
- Iterate through folders (file listing)
|
- Iterate through folders (file listing)
|
||||||
- Perform targetted reads (file sigs)
|
- Perform targeted reads (file signatures)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ how to set default values and boolean arguments. the ``argparse``
|
||||||
module has many more features documented at
|
module has many more features documented at
|
||||||
https://docs.python.org/3/library/argparse.html
|
https://docs.python.org/3/library/argparse.html
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_01/argparse_example.py
|
.. literalinclude:: ../pyforhandbook/ch01_essentials/argparse_example.py
|
||||||
:pyobject: setup_argparse
|
:pyobject: setup_argparse
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -51,44 +51,51 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'Chapin Bryce'
|
__author__ = "Chapin Bryce"
|
||||||
__date__ = 20190527
|
__date__ = 20190527
|
||||||
__license__ = 'MIT Copyright 2019 Chapin Bryce'
|
__license__ = "MIT Copyright 2019 Chapin Bryce"
|
||||||
__desc__ = '''Sample script to accept command line arguments.'''
|
__desc__ = """Sample script to accept command line arguments."""
|
||||||
__docs__ = [
|
__docs__ = [
|
||||||
'https://docs.python.org/3/library/argparse.html',
|
"https://docs.python.org/3/library/argparse.html",
|
||||||
'https://docs.python.org/3/library/os.html',
|
"https://docs.python.org/3/library/os.html",
|
||||||
'https://docs.python.org/3/library/pathlib.html'
|
"https://docs.python.org/3/library/pathlib.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def setup_argparse():
|
def setup_argparse():
|
||||||
# Setup a parser instance with common fields including a
|
# Setup a parser instance with common fields including a
|
||||||
# description and epilog. The `formatter_class` instructs
|
# description and epilog. The `formatter_class` instructs
|
||||||
# argparse to show default values set for parameters.
|
# argparse to show default values set for parameters.
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Sample Argparse',
|
description="Sample Argparse",
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
epilog=f"Built by {__author__}, v.{__date__}"
|
epilog=f"Built by {__author__}, v.{__date__}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# The simplest form of adding an argument, the name of the
|
# The simplest form of adding an argument, the name of the
|
||||||
# parameter and a description of its form.
|
# parameter and a description of its form.
|
||||||
parser.add_argument('INPUT_FILE', help="Input file to parse")
|
parser.add_argument("INPUT_FILE", help="Input file to parse")
|
||||||
parser.add_argument('OUTPUT_FOLDER',
|
parser.add_argument("OUTPUT_FOLDER", help="Folder to store output")
|
||||||
help="Folder to store output")
|
|
||||||
|
|
||||||
# An optional argument with multiple methods of specifying
|
# An optional argument with multiple methods of specifying
|
||||||
# the parameter. Includes a default value
|
# the parameter. Includes a default value
|
||||||
parser.add_argument('-l', '--log', help="Path to log file",
|
parser.add_argument(
|
||||||
default=os.path.abspath(os.path.join(
|
"-l",
|
||||||
PurePath(__file__).parent,
|
"--log",
|
||||||
PurePath(__file__).name.rsplit('.', 1)[0] + '.log'))
|
help="Path to log file",
|
||||||
|
default=os.path.abspath(
|
||||||
|
os.path.join(
|
||||||
|
PurePath(__file__).parent,
|
||||||
|
PurePath(__file__).name.rsplit(".", 1)[0] + ".log",
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# An optional argument which does not accept a value, instead
|
# An optional argument which does not accept a value, instead
|
||||||
# just modifies functionality.
|
# just modifies functionality.
|
||||||
parser.add_argument('-v', '--verbose', action='store_true',
|
parser.add_argument(
|
||||||
help='Include debug log messages')
|
"-v", "--verbose", action="store_true", help="Include debug log messages"
|
||||||
|
)
|
||||||
|
|
||||||
# Once we've specified our arguments we can parse them for
|
# Once we've specified our arguments we can parse them for
|
||||||
# reference
|
# reference
|
||||||
|
|
@ -97,12 +104,13 @@ def setup_argparse():
|
||||||
# Returning our parsed arguments for further use.
|
# Returning our parsed arguments for further use.
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
# Only run if called directly (not imported)
|
# Only run if called directly (not imported)
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
args = setup_argparse()
|
cli_args = setup_argparse()
|
||||||
|
|
||||||
# Show arguments
|
# Show arguments
|
||||||
print(f'Input file: {args.INPUT_FILE}')
|
print(f"Input file: {cli_args.INPUT_FILE}")
|
||||||
print(f'Output folder: {args.OUTPUT_FOLDER}')
|
print(f"Output folder: {cli_args.OUTPUT_FOLDER}")
|
||||||
print(f'Log file: {args.log}')
|
print(f"Log file: {cli_args.log}")
|
||||||
print(f'Be verbose?: {args.verbose}')
|
print(f"Be verbose?: {cli_args.verbose}")
|
||||||
|
|
@ -31,10 +31,10 @@ multiple dictionaries to a CSV file. You can optionally provide
|
||||||
an ordered list of headers to filter what rows to show, or let the
|
an ordered list of headers to filter what rows to show, or let the
|
||||||
function use the keys of the first dictionary in the list to
|
function use the keys of the first dictionary in the list to
|
||||||
generate the header information. The latter option may produce
|
generate the header information. The latter option may produce
|
||||||
a new order each iteration and is not prefered if you can
|
a new order each iteration and is not preferred if you can
|
||||||
determine the headers in advance.
|
determine the headers in advance.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_01/csv_example.py
|
.. literalinclude:: ../pyforhandbook/ch01_essentials/csv_example.py
|
||||||
:pyobject: write_csv_dicts
|
:pyobject: write_csv_dicts
|
||||||
|
|
||||||
List of ordered lists to CSV
|
List of ordered lists to CSV
|
||||||
|
|
@ -58,7 +58,7 @@ information. Unlike the dictionary option, you cannot filter
|
||||||
column data by adjusting the provided headers, you must write all
|
column data by adjusting the provided headers, you must write all
|
||||||
columns to the CSV.
|
columns to the CSV.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_01/csv_example.py
|
.. literalinclude:: ../pyforhandbook/ch01_essentials/csv_example.py
|
||||||
:pyobject: write_csv_lists
|
:pyobject: write_csv_lists
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -92,13 +92,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'Chapin Bryce'
|
__author__ = "Chapin Bryce"
|
||||||
__date__ = 20190527
|
__date__ = 20190527
|
||||||
__license__ = 'MIT Copyright 2019 Chapin Bryce'
|
__license__ = "MIT Copyright 2019 Chapin Bryce"
|
||||||
__desc__ = '''Sample script to write to CSV files.'''
|
__desc__ = """Sample script to write to CSV files."""
|
||||||
__docs__ = [
|
__docs__ = [
|
||||||
'https://docs.python.org/3/library/csv.html',
|
"https://docs.python.org/3/library/csv.html",
|
||||||
'https://docs.python.org/3/library/os.html'
|
"https://docs.python.org/3/library/os.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -110,18 +110,24 @@ def write_csv_dicts(outfile, data, headers=None):
|
||||||
data (list): List of dictionaries to write to file
|
data (list): List of dictionaries to write to file
|
||||||
headers (list): Header row to use. If empty, will use the
|
headers (list): Header row to use. If empty, will use the
|
||||||
first dictionary in the `data` list.
|
first dictionary in the `data` list.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> list_of_dicts = [
|
||||||
|
>>> {'name': 'apple', 'quantity': 10, 'location': 'VT'},
|
||||||
|
>>> {'name': 'orange', 'quantity': 5, 'location': 'FL'}
|
||||||
|
>>> ]
|
||||||
|
>>> write_csv_dicts('dict_test.csv', list_of_dicts)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not headers:
|
if not headers:
|
||||||
# Use the first line of data
|
# Use the first line of data
|
||||||
headers = [str(x) for x in data[0].keys()]
|
headers = [str(x) for x in data[0].keys()]
|
||||||
|
|
||||||
with open(outfile, 'w', newline="") as open_file:
|
with open(outfile, "w", newline="") as open_file:
|
||||||
# Write only provided headers, ignore others
|
# Write only provided headers, ignore others
|
||||||
csvfile = csv.DictWriter(open_file, headers,
|
csv_file = csv.DictWriter(open_file, headers, extrasaction="ignore")
|
||||||
extrasaction='ignore')
|
csv_file.writeheader()
|
||||||
csvfile.writeheader()
|
csv_file.writerows(data)
|
||||||
csvfile.writerows(data)
|
|
||||||
|
|
||||||
|
|
||||||
def write_csv_lists(outfile, data, headers=None):
|
def write_csv_lists(outfile, data, headers=None):
|
||||||
|
|
@ -132,35 +138,41 @@ def write_csv_lists(outfile, data, headers=None):
|
||||||
data (list): List of lists to write to file
|
data (list): List of lists to write to file
|
||||||
headers (list): Header row to use. If empty, will use the
|
headers (list): Header row to use. If empty, will use the
|
||||||
first list in the `data` list.
|
first list in the `data` list.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> fields = ['name', 'quantity', 'location']
|
||||||
|
>>> list_of_lists = [
|
||||||
|
>>> ['apple', 10, 'VT'],
|
||||||
|
>>> ['orange', 5, 'FL']
|
||||||
|
>>> ]
|
||||||
|
>>> write_csv_lists('list_test.csv', list_of_lists, headers=fields)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with open(outfile, 'w', newline="") as open_file:
|
with open(outfile, "w", newline="") as open_file:
|
||||||
# Write only provided headers, ignore others
|
# Write only provided headers, ignore others
|
||||||
csvfile = csv.writer(open_file)
|
csv_file = csv.writer(open_file)
|
||||||
for count, entry in enumerate(data):
|
for count, entry in enumerate(data):
|
||||||
if count == 0 and headers:
|
if count == 0 and headers:
|
||||||
# If headers are defined, write them, otherwise
|
# If headers are defined, write them, otherwise
|
||||||
# continue as they will be written anyways
|
# continue as they will be written anyways
|
||||||
csvfile.writerow(headers)
|
csv_file.writerow(headers)
|
||||||
csvfile.writerow(entry)
|
csv_file.writerow(entry)
|
||||||
|
|
||||||
sample_dict_data = [
|
|
||||||
{'id': '0', 'city': 'Boston', 'state': 'MA',
|
|
||||||
'country': 'USA'},
|
|
||||||
{'id': '1', 'city': 'New York', 'state': 'NY',
|
|
||||||
'country': 'USA'},
|
|
||||||
{'id': '2', 'city': 'Washington', 'state': 'DC',
|
|
||||||
'country': 'USA'},
|
|
||||||
]
|
|
||||||
|
|
||||||
write_csv_dicts('dict_test.csv', sample_dict_data)
|
if __name__ == "__main__":
|
||||||
|
sample_dict_data = [
|
||||||
|
{"id": "0", "city": "Boston", "state": "MA", "country": "USA"},
|
||||||
|
{"id": "1", "city": "New York", "state": "NY", "country": "USA"},
|
||||||
|
{"id": "2", "city": "Washington", "state": "DC", "country": "USA"},
|
||||||
|
]
|
||||||
|
|
||||||
header_row = ['id', 'city', 'state', 'country']
|
write_csv_dicts("dict_test.csv", sample_dict_data)
|
||||||
sample_list_data = [
|
|
||||||
['0', 'Boston', 'MA', 'USA'],
|
|
||||||
['1', 'New York', 'NY', 'USA'],
|
|
||||||
['2', 'Washington', 'DC', 'USA']
|
|
||||||
]
|
|
||||||
|
|
||||||
write_csv_lists('list_test.csv', sample_list_data,
|
header_row = ["id", "city", "state", "country"]
|
||||||
headers=header_row)
|
sample_list_data = [
|
||||||
|
["0", "Boston", "MA", "USA"],
|
||||||
|
["1", "New York", "NY", "USA"],
|
||||||
|
["2", "Washington", "DC", "USA"],
|
||||||
|
]
|
||||||
|
|
||||||
|
write_csv_lists("list_test.csv", sample_list_data, headers=header_row)
|
||||||
|
|
@ -20,7 +20,7 @@ set different logging levels for the two handlers - generally you
|
||||||
keep debugging information in the log file while writing more
|
keep debugging information in the log file while writing more
|
||||||
critical messages to the console in STDERR.
|
critical messages to the console in STDERR.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_01/logging_example.py
|
.. literalinclude:: ../pyforhandbook/ch01_essentials/logging_example.py
|
||||||
:pyobject: setup_logging
|
:pyobject: setup_logging
|
||||||
|
|
||||||
Docstring References
|
Docstring References
|
||||||
|
|
@ -53,61 +53,64 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'Chapin Bryce'
|
__author__ = "Chapin Bryce"
|
||||||
__date__ = 20190527
|
__date__ = 20190527
|
||||||
__license__ = 'MIT Copyright 2019 Chapin Bryce'
|
__license__ = "MIT Copyright 2019 Chapin Bryce"
|
||||||
__desc__ = '''Sample script to display and write logging
|
__desc__ = """Sample script to display and write logging
|
||||||
messages.'''
|
messages."""
|
||||||
__docs__ = [
|
__docs__ = [
|
||||||
'https://docs.python.org/3/library/logging.html',
|
"https://docs.python.org/3/library/logging.html",
|
||||||
'https://docs.python.org/3/library/os.html'
|
"https://docs.python.org/3/library/os.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
logger = None
|
logger = logging.getLogger(name=__name__)
|
||||||
|
|
||||||
def setup_logging():
|
|
||||||
"""Function to setup logging configuration and test it."""
|
|
||||||
# Allow us to modify the `logger` variable within a function
|
|
||||||
global logger
|
|
||||||
|
|
||||||
# Set logger object, uses module's name
|
def setup_logging(logging_obj, verbose=False):
|
||||||
logger = logging.getLogger(name=__name__)
|
"""Function to setup logging configuration and test it.
|
||||||
|
|
||||||
# Set default logger level to DEBUG. You can change this later
|
Args:
|
||||||
logger.setLevel(logging.DEBUG)
|
logging_obj: A logging instance, returned from logging.getLogger().
|
||||||
|
verbose: Whether or not to enable the debug level in STDERR output.
|
||||||
|
|
||||||
# Logging formatter. Best to keep consistent for most usecases
|
Examples:
|
||||||
|
>>> sample_logger = logging.getLogger(name=__name__)
|
||||||
|
>>> sample_logger = setup_logging(sample_logger)
|
||||||
|
>>> sample_logger.debug("This is a debug message")
|
||||||
|
>>> sample_logger.info("This is an info message")
|
||||||
|
>>> sample_logger.warning("This is a warning message")
|
||||||
|
>>> sample_logger.error("This is a error message")
|
||||||
|
>>> sample_logger.critical("This is a critical message")
|
||||||
|
"""
|
||||||
|
logging_obj.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Logging formatter. Best to keep consistent for most use cases
|
||||||
log_format = logging.Formatter(
|
log_format = logging.Formatter(
|
||||||
'%(asctime)s %(filename)s %(levelname)s %(module)s '
|
"%(asctime)s %(filename)s %(levelname)s %(module)s "
|
||||||
'%(funcName)s %(lineno)d %(message)s')
|
"%(funcName)s %(lineno)d %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
# Setup STDERR logging, allowing you uninterrupted
|
# Setup STDERR logging, allowing you uninterrupted
|
||||||
# STDOUT redirection
|
# STDOUT redirection
|
||||||
stderr_handle = logging.StreamHandler(stream=sys.stderr)
|
stderr_handle = logging.StreamHandler(stream=sys.stderr)
|
||||||
stderr_handle.setLevel(logging.INFO)
|
if verbose:
|
||||||
|
stderr_handle.setLevel(logging.DEBUG)
|
||||||
|
else:
|
||||||
|
stderr_handle.setLevel(logging.INFO)
|
||||||
stderr_handle.setFormatter(log_format)
|
stderr_handle.setFormatter(log_format)
|
||||||
|
|
||||||
# Setup file logging
|
# Setup file logging
|
||||||
file_handle = logging.FileHandler('sample.log', 'a')
|
file_handle = logging.FileHandler("sample.log", "a")
|
||||||
file_handle.setLevel(logging.DEBUG)
|
file_handle.setLevel(logging.DEBUG)
|
||||||
file_handle.setFormatter(log_format)
|
file_handle.setFormatter(log_format)
|
||||||
|
|
||||||
# Add handles
|
# Add handles
|
||||||
logger.addHandler(stderr_handle)
|
logging_obj.addHandler(stderr_handle)
|
||||||
logger.addHandler(file_handle)
|
logging_obj.addHandler(file_handle)
|
||||||
|
|
||||||
# Sample log messages
|
return logging_obj
|
||||||
logger.debug("This is a debug message")
|
|
||||||
logger.info("This is an info message")
|
|
||||||
logger.warning("This is a warning message")
|
|
||||||
logger.error("This is a error message")
|
|
||||||
logger.critical("This is a critical message")
|
|
||||||
|
|
||||||
def sample_function():
|
|
||||||
"""Sample function to demonstrate logging formatting."""
|
|
||||||
logger.info("Called from a function")
|
|
||||||
|
|
||||||
sample_function()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
setup_logging()
|
setup_logging(logger)
|
||||||
|
logger.warning("This is a warning!")
|
||||||
|
|
@ -22,7 +22,7 @@ magic value/file signature, this low-tech method will help with parsing a
|
||||||
collection of files that may be UTF-8, UTF-16-LE, and UTF-16-BE, three very
|
collection of files that may be UTF-8, UTF-16-LE, and UTF-16-BE, three very
|
||||||
common text file encodings. Feel free to build and share on this.
|
common text file encodings. Feel free to build and share on this.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_01/open_files.py
|
.. literalinclude:: ../pyforhandbook/ch01_essentials/open_files.py
|
||||||
:pyobject: open_file
|
:pyobject: open_file
|
||||||
|
|
||||||
Docstring References
|
Docstring References
|
||||||
|
|
@ -30,7 +30,6 @@ Docstring References
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from io import open
|
from io import open
|
||||||
import os
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Copyright 2019 Chapin Bryce
|
Copyright 2019 Chapin Bryce
|
||||||
|
|
@ -56,13 +55,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'Chapin Bryce'
|
__author__ = "Chapin Bryce"
|
||||||
__date__ = 20191103
|
__date__ = 20191103
|
||||||
__license__ = 'MIT Copyright 2019 Chapin Bryce'
|
__license__ = "MIT Copyright 2019 Chapin Bryce"
|
||||||
__desc__ = '''Sample script to read encoded text files.'''
|
__desc__ = """Sample script to read encoded text files."""
|
||||||
__docs__ = [
|
__docs__ = [
|
||||||
'https://docs.python.org/3/library/csv.html',
|
"https://docs.python.org/3/library/csv.html",
|
||||||
'https://docs.python.org/3/library/os.html'
|
"https://docs.python.org/3/library/os.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -73,28 +72,29 @@ def open_file(input_file):
|
||||||
input_file (str): Path to file to open
|
input_file (str): Path to file to open
|
||||||
"""
|
"""
|
||||||
|
|
||||||
test_encoding = open(input_file, 'rb')
|
test_encoding = open(input_file, "rb")
|
||||||
bom = test_encoding.read(2)
|
bom = test_encoding.read(2)
|
||||||
file_encoding = 'utf-8'
|
file_encoding = "utf-8"
|
||||||
if bom == b'FEFF':
|
if bom == b"FEFF":
|
||||||
file_encoding = 'utf-16-le'
|
file_encoding = "utf-16-le"
|
||||||
elif bom == b'FFFE':
|
elif bom == b"FFFE":
|
||||||
file_encoding = 'utf-16-be'
|
file_encoding = "utf-16-be"
|
||||||
|
|
||||||
with open(input_file, 'r', encoding=file_encoding) as fopen:
|
with open(input_file, "r", encoding=file_encoding) as open_input_file:
|
||||||
for raw_line in fopen:
|
for raw_line in open_input_file:
|
||||||
line = raw_line.strip()
|
line = raw_line.strip()
|
||||||
|
|
||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=__desc__,
|
description=__desc__,
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
epilog=f"Built by {__author__}, v.{__date__}"
|
epilog=f"Built by {__author__}, v.{__date__}",
|
||||||
)
|
)
|
||||||
parser.add_argument('INPUT_FILE', help="Text file to read")
|
parser.add_argument("INPUT_FILE", help="Text file to read")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
open_file(args.INPUT_FILE)
|
open_file(args.INPUT_FILE)
|
||||||
|
|
@ -19,7 +19,7 @@ folders within a single directory. From here you can further
|
||||||
interact with individual files and folders or iterate recursively
|
interact with individual files and folders or iterate recursively
|
||||||
by calling the function on identified subdirectories.
|
by calling the function on identified subdirectories.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_01/recursion_example.py
|
.. literalinclude:: ../pyforhandbook/ch01_essentials/recursion_example.py
|
||||||
:pyobject: list_directory
|
:pyobject: list_directory
|
||||||
|
|
||||||
List a directory recursively
|
List a directory recursively
|
||||||
|
|
@ -33,7 +33,7 @@ handling the processing of files. This sample shows a method of
|
||||||
counting the number of files, subdirectories, and files ending in
|
counting the number of files, subdirectories, and files ending in
|
||||||
".py" as an example.
|
".py" as an example.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_01/recursion_example.py
|
.. literalinclude:: ../pyforhandbook/ch01_essentials/recursion_example.py
|
||||||
:pyobject: iterate_files
|
:pyobject: iterate_files
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -63,16 +63,24 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'Chapin Bryce'
|
__author__ = "Chapin Bryce"
|
||||||
__date__ = 20190527
|
__date__ = 20190527
|
||||||
__license__ = 'MIT Copyright 2019 Chapin Bryce'
|
__license__ = "MIT Copyright 2019 Chapin Bryce"
|
||||||
__desc__ = '''Sample script to iterate over a folder of files.'''
|
__desc__ = """Sample script to iterate over a folder of files."""
|
||||||
__docs__ = [
|
__docs__ = ["https://docs.python.org/3/library/os.html"]
|
||||||
'https://docs.python.org/3/library/os.html'
|
|
||||||
]
|
|
||||||
|
|
||||||
def list_directory(path):
|
def list_directory(path):
|
||||||
"""List all file and folder entries in `path`."""
|
"""List all file and folder entries in `path`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): A directory within a mounted file system. May be relative or
|
||||||
|
absolute.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> list_directory('.')
|
||||||
|
|
||||||
|
"""
|
||||||
print(f"Files and folders in '{os.path.abspath(path)}':")
|
print(f"Files and folders in '{os.path.abspath(path)}':")
|
||||||
# Quick and easy method for listing items within a single
|
# Quick and easy method for listing items within a single
|
||||||
# folder.
|
# folder.
|
||||||
|
|
@ -80,7 +88,23 @@ def list_directory(path):
|
||||||
# Print all entry names
|
# Print all entry names
|
||||||
print(f"\t{entry}")
|
print(f"\t{entry}")
|
||||||
|
|
||||||
|
|
||||||
def iterate_files(path):
|
def iterate_files(path):
|
||||||
|
"""Recursively iterate over a path, findings all files within the folder
|
||||||
|
and its subdirectories.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): A directory within a mounted file system. May be relative or
|
||||||
|
absolute.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> number_of_py_files = 0
|
||||||
|
>>> for f in iterate_files('../'):
|
||||||
|
... if f.endswith('.py'):
|
||||||
|
... number_of_py_files += 1
|
||||||
|
>>> print(f"\t{number_of_py_files} python files found "
|
||||||
|
... f"in {os.path.abspath('../')}")
|
||||||
|
"""
|
||||||
# Though `os.walk()` exposes a list of directories in the
|
# Though `os.walk()` exposes a list of directories in the
|
||||||
# current `root`, it is rarely used since we are generally
|
# current `root`, it is rarely used since we are generally
|
||||||
# interested in the files found within the subdirectories.
|
# interested in the files found within the subdirectories.
|
||||||
|
|
@ -89,23 +113,21 @@ def iterate_files(path):
|
||||||
for root, dirs, files in os.walk(os.path.abspath(path)):
|
for root, dirs, files in os.walk(os.path.abspath(path)):
|
||||||
# Both `dirs` and `files` are lists containing all entries
|
# Both `dirs` and `files` are lists containing all entries
|
||||||
# at the current `root`.
|
# at the current `root`.
|
||||||
for fentry in files:
|
for file_name in files:
|
||||||
# To effectively reference a file, you should include
|
# To effectively reference a file, you should include
|
||||||
# the below line which creates a full path reference
|
# the below line which creates a full path reference
|
||||||
# to the specific file, regardless of how nested it is
|
# to the specific file, regardless of how nested it is
|
||||||
file_entry = os.path.join(root, fentry)
|
|
||||||
# We can then hand `file_entry` off to other functions.
|
# We can then hand `file_entry` off to other functions.
|
||||||
yield file_entry
|
yield os.path.join(root, file_name)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
abspath = os.path.abspath
|
abspath = os.path.abspath
|
||||||
print(f"Listing {abspath('.')}")
|
print(f"Listing {abspath('.')}")
|
||||||
list_directory('.')
|
list_directory(".")
|
||||||
print(f"\nRecurively counting files in {abspath('../../')}")
|
print(f"\nRecursively counting files in {abspath('../../')}")
|
||||||
num_py_files = 0
|
num_py_files = 0
|
||||||
for file_entry in iterate_files('../../'):
|
for file_entry in iterate_files("../"):
|
||||||
if file_entry.endswith('.py'):
|
if file_entry.endswith(".py"):
|
||||||
num_py_files += 1
|
num_py_files += 1
|
||||||
print(f"\t{num_py_files} python files found "
|
print(f"\t{num_py_files} python files found in {abspath('../')}")
|
||||||
f"in {abspath('../../')}")
|
|
||||||
|
|
@ -21,14 +21,12 @@ leverage any available transaction logs to include additional information
|
||||||
otherwise available on the Window's system. This class handles both the opening
|
otherwise available on the Window's system. This class handles both the opening
|
||||||
of the primary hive and attempted recovery of the transaction logs.
|
of the primary hive and attempted recovery of the transaction logs.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_02/yarp_base.py
|
.. literalinclude:: ../pyforhandbook/ch02_registry/yarp_base.py
|
||||||
:pyobject: RegistryBase
|
:pyobject: RegistryBase
|
||||||
|
|
||||||
Docstring References
|
Docstring References
|
||||||
====================
|
====================
|
||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import struct
|
|
||||||
# Installed via:
|
# Installed via:
|
||||||
# pip install https://github.com/msuhanov/yarp/archive/1.0.28.tar.gz
|
# pip install https://github.com/msuhanov/yarp/archive/1.0.28.tar.gz
|
||||||
from yarp import Registry, RegistryHelpers
|
from yarp import Registry, RegistryHelpers
|
||||||
|
|
@ -58,20 +56,21 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'Chapin Bryce'
|
__author__ = "Chapin Bryce"
|
||||||
__date__ = 20190707
|
__date__ = 20190707
|
||||||
__license__ = 'MIT Copyright 2019 Chapin Bryce'
|
__license__ = "MIT Copyright 2019 Chapin Bryce"
|
||||||
__desc__ = '''Registry parsing class that opens an offline hive.'''
|
__desc__ = """Registry parsing class that opens an offline hive."""
|
||||||
__docs__ = [
|
__docs__ = [
|
||||||
'https://github.com/msuhanov/yarp',
|
"https://github.com/msuhanov/yarp",
|
||||||
'https://docs.python.org/3/library/datetime.html',
|
"https://docs.python.org/3/library/datetime.html",
|
||||||
'https://docs.python.org/3/library/struct.html'
|
"https://docs.python.org/3/library/struct.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RegistryBase():
|
class RegistryBase:
|
||||||
"""Base class containing common registry parsing code. Will open a hive
|
"""Base class containing common registry parsing code. Will open a hive
|
||||||
and attempt recovery using available transaction logs"""
|
and attempt recovery using available transaction logs"""
|
||||||
|
|
||||||
def __init__(self, reg_file):
|
def __init__(self, reg_file):
|
||||||
"""Base __init__ method, responsible for opening a hive."""
|
"""Base __init__ method, responsible for opening a hive."""
|
||||||
self.reg_file = reg_file
|
self.reg_file = reg_file
|
||||||
|
|
@ -92,10 +91,10 @@ class RegistryBase():
|
||||||
hive_path = self.hive.registry_file.file_object.name
|
hive_path = self.hive.registry_file.file_object.name
|
||||||
tx_logs = RegistryHelpers.DiscoverLogFiles(hive_path)
|
tx_logs = RegistryHelpers.DiscoverLogFiles(hive_path)
|
||||||
self.tx_log_files = []
|
self.tx_log_files = []
|
||||||
for tx_path in ['log_path', 'log1_path', 'log2_path']:
|
for tx_path in ["log_path", "log1_path", "log2_path"]:
|
||||||
log_obj = None
|
log_obj = None
|
||||||
if getattr(tx_logs, tx_path, None):
|
if getattr(tx_logs, tx_path, None):
|
||||||
log_obj = open(getattr(tx_logs, tx_path), 'rb')
|
log_obj = open(getattr(tx_logs, tx_path), "rb")
|
||||||
self.tx_log_files.append(log_obj)
|
self.tx_log_files.append(log_obj)
|
||||||
self.hive.recover_auto(*self.tx_log_files)
|
self.hive.recover_auto(*self.tx_log_files)
|
||||||
|
|
||||||
|
|
@ -117,12 +116,14 @@ def main(reg_file):
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Registry Parsing',
|
description="Registry Parsing",
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
epilog=f"Built by {__author__}, v.{__date__}"
|
epilog=f"Built by {__author__}, v.{__date__}",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"REG_FILE", help="Path to registry file", type=argparse.FileType("rb")
|
||||||
)
|
)
|
||||||
parser.add_argument('REG_FILE', help='Path to registry file',
|
|
||||||
type=argparse.FileType('rb'))
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
main(args.REG_FILE)
|
main(args.REG_FILE)
|
||||||
|
|
@ -22,7 +22,7 @@ NTUSER.DAT files, though could get more specific on Windows versions, etc. In
|
||||||
this class we store a few useful details including fixed values used by other
|
this class we store a few useful details including fixed values used by other
|
||||||
methods and metadata about the class.
|
methods and metadata about the class.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_02/yarp_ntuser.py
|
.. literalinclude:: ../pyforhandbook/ch02_registry/yarp_ntuser.py
|
||||||
:pyobject: NTUSER.__init__
|
:pyobject: NTUSER.__init__
|
||||||
|
|
||||||
Reading Hive String Values
|
Reading Hive String Values
|
||||||
|
|
@ -30,14 +30,14 @@ Reading Hive String Values
|
||||||
|
|
||||||
With an open hive, we can begin to parse values from a known key location
|
With an open hive, we can begin to parse values from a known key location
|
||||||
within the hive. This method allows us to specify a key path and inspect each
|
within the hive. This method allows us to specify a key path and inspect each
|
||||||
of the subkeys. For each of the subkeys, we can then get the names and data
|
of the sub-keys. For each of the sub-keys, we can then get the names and data
|
||||||
associated with each value in the key. Additionally we could - if needed -
|
associated with each value in the key. Additionally we could - if needed -
|
||||||
continue to recurse on subkeys here. Instead we return this cursory information
|
continue to recurse on sub-keys here. Instead we return this cursory information
|
||||||
for the caller to display as they wish. Since the values within MountPoints2
|
for the caller to display as they wish. Since the values within MountPoints2
|
||||||
store string data, we don't need to perform further parsing of the record.
|
store string data, we don't need to perform further parsing of the record.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_02/yarp_ntuser.py
|
.. literalinclude:: ../pyforhandbook/ch02_registry/yarp_ntuser.py
|
||||||
:pyobject: NTUSER.parse_mountpoints2
|
:pyobject: NTUSER.parse_mount_points2
|
||||||
|
|
||||||
Reading Hive Binary Values
|
Reading Hive Binary Values
|
||||||
==========================
|
==========================
|
||||||
|
|
@ -51,8 +51,8 @@ method) using Struct to extract a timestamp and integer marking whether a
|
||||||
trusted macro was used. These parsed attributes are then returned to the caller
|
trusted macro was used. These parsed attributes are then returned to the caller
|
||||||
to be displayed.
|
to be displayed.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_02/yarp_ntuser.py
|
.. literalinclude:: ../pyforhandbook/ch02_registry/yarp_ntuser.py
|
||||||
:pyobject: NTUSER.parse_trustrecords
|
:pyobject: NTUSER.parse_trust_records
|
||||||
|
|
||||||
Docstring References
|
Docstring References
|
||||||
====================
|
====================
|
||||||
|
|
@ -61,11 +61,8 @@ Docstring References
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
import yarp
|
from pyforhandbook.ch02_registry.yarp_base import RegistryBase
|
||||||
try:
|
|
||||||
from pyforhandbook.section_02.yarp_base import RegistryBase
|
|
||||||
except ImportError:
|
|
||||||
from yarp_base import RegistryBase
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Copyright 2019 Chapin Bryce
|
Copyright 2019 Chapin Bryce
|
||||||
|
|
@ -91,34 +88,37 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'Chapin Bryce'
|
__author__ = "Chapin Bryce"
|
||||||
__date__ = 20190707
|
__date__ = 20190707
|
||||||
__license__ = 'MIT Copyright 2019 Chapin Bryce'
|
__license__ = "MIT Copyright 2019 Chapin Bryce"
|
||||||
__desc__ = '''Registry parsing class that parses the NTUSER.DAT hive.'''
|
__desc__ = """Registry parsing class that parses the NTUSER.DAT hive."""
|
||||||
__docs__ = [
|
__docs__ = [
|
||||||
'https://github.com/msuhanov/yarp',
|
"https://github.com/msuhanov/yarp",
|
||||||
'https://docs.python.org/3/library/datetime.html',
|
"https://docs.python.org/3/library/datetime.html",
|
||||||
'https://docs.python.org/3/library/struct.html'
|
"https://docs.python.org/3/library/struct.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NTUSER(RegistryBase):
|
class NTUSER(RegistryBase):
|
||||||
"""Class to handle the parsing of the NTUSER.DAT hive."""
|
"""Class to handle the parsing of the NTUSER.DAT hive."""
|
||||||
|
|
||||||
def __init__(self, reg_path):
|
def __init__(self, reg_path):
|
||||||
super().__init__(reg_path)
|
super().__init__(reg_path)
|
||||||
self.hive_type = 'NTUSER.DAT'
|
self.hive_type = "NTUSER.DAT"
|
||||||
self.macro_enabled_val = 2147483647
|
self.macro_enabled_val = 2147483647
|
||||||
|
|
||||||
def parse_mountpoints2(self):
|
def parse_mount_points2(self):
|
||||||
"""Demonstration of parsing values from a key by path."""
|
"""Demonstration of parsing values from a key by path."""
|
||||||
key_path = ('Software\\Microsoft\\Windows\\CurrentVersion'
|
key_path = (
|
||||||
'\\Explorer\\MountPoints2')
|
"Software\\Microsoft\\Windows\\CurrentVersion"
|
||||||
|
"\\Explorer\\MountPoints2"
|
||||||
|
)
|
||||||
for mp in self.hive.find_key(key_path).subkeys():
|
for mp in self.hive.find_key(key_path).subkeys():
|
||||||
mp_data = {}
|
yield {
|
||||||
mp_data['name'] = mp.name().replace('#', '\\')
|
"name": mp.name().replace("#", "\\"),
|
||||||
mp_data['values'] = {x.name(): x.data() for x in mp.values()}
|
"values": {x.name(): x.data() for x in mp.values()},
|
||||||
mp_data['last_written'] = mp.last_written_timestamp()
|
"last_written": mp.last_written_timestamp(),
|
||||||
yield mp_data
|
}
|
||||||
|
|
||||||
def parse_office_versions(self):
|
def parse_office_versions(self):
|
||||||
"""Get Office versions within an open Registry hive.
|
"""Get Office versions within an open Registry hive.
|
||||||
|
|
@ -126,10 +126,9 @@ class NTUSER(RegistryBase):
|
||||||
Yields:
|
Yields:
|
||||||
(str): Office version number (ie. '15.0')
|
(str): Office version number (ie. '15.0')
|
||||||
"""
|
"""
|
||||||
office_versions = self.hive.find_key('Software\\Microsoft\\Office')
|
office_versions = self.hive.find_key("Software\\Microsoft\\Office")
|
||||||
for subkey in office_versions.subkeys():
|
for sub_key in office_versions.subkeys():
|
||||||
key_name = subkey.name()
|
key_name = sub_key.name()
|
||||||
is_ver_num = False
|
|
||||||
try:
|
try:
|
||||||
_ = float(key_name)
|
_ = float(key_name)
|
||||||
is_ver_num = True
|
is_ver_num = True
|
||||||
|
|
@ -139,52 +138,61 @@ class NTUSER(RegistryBase):
|
||||||
if is_ver_num:
|
if is_ver_num:
|
||||||
yield key_name
|
yield key_name
|
||||||
|
|
||||||
def parse_trustrecords(self):
|
def parse_trust_records(self):
|
||||||
"""Demonstration of parsing binary values within a key."""
|
"""Demonstration of parsing binary values within a key."""
|
||||||
trust_record_path = 'Software\\Microsoft\\Office\\{OFFICE_VERSION}' \
|
trust_record_path = (
|
||||||
'\\Word\\Security\\Trusted Documents\\TrustRecords'
|
"Software\\Microsoft\\Office\\{OFFICE_VERSION}"
|
||||||
|
"\\Word\\Security\\Trusted Documents\\TrustRecords"
|
||||||
|
)
|
||||||
for office_version in self.parse_office_versions():
|
for office_version in self.parse_office_versions():
|
||||||
trust_rec_key = self.hive.find_key(
|
trust_rec_key = self.hive.find_key(
|
||||||
trust_record_path.format(OFFICE_VERSION=office_version))
|
trust_record_path.format(OFFICE_VERSION=office_version)
|
||||||
|
)
|
||||||
if not trust_rec_key:
|
if not trust_rec_key:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for rec in trust_rec_key.values():
|
for rec in trust_rec_key.values():
|
||||||
date_val, macro_enabled = struct.unpack('q12xI', rec.data())
|
date_val, macro_enabled = struct.unpack("q12xI", rec.data())
|
||||||
ms = date_val/10.0
|
ms = date_val / 10.0
|
||||||
dt_date = datetime(1601, 1, 1) + timedelta(microseconds=ms)
|
dt_date = datetime(1601, 1, 1) + timedelta(microseconds=ms)
|
||||||
yield {
|
yield {
|
||||||
'doc': rec.name(),
|
"doc": rec.name(),
|
||||||
'dt': dt_date.isoformat(),
|
"dt": dt_date.isoformat(),
|
||||||
'macro': macro_enabled == self.macro_enabled_val
|
"macro": macro_enabled == self.macro_enabled_val,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def main(reg_file):
|
def main(reg_file):
|
||||||
reg = NTUSER(reg_file)
|
reg = NTUSER(reg_file)
|
||||||
# Call an example parsing method and display the values from NTUSER keys
|
# Call an example parsing method and display the values from NTUSER keys
|
||||||
print("{:=^30}".format(' MountPoints2 '))
|
print("{:=^30}".format(" MountPoints2 "))
|
||||||
for mount_point in reg.parse_mountpoints2():
|
for mount_point in reg.parse_mount_points2():
|
||||||
print(f"Found MountPoints2 path '{mount_point['name']}' with values:")
|
print(f"Found MountPoints2 path '{mount_point['name']}' with values:")
|
||||||
value_str = '\tlast written time: {}\n'.format(
|
value_str = "\tlast written time: {}\n".format(
|
||||||
mount_point["last_written"].isoformat())
|
mount_point["last_written"].isoformat()
|
||||||
|
)
|
||||||
value_str += "\n".join(
|
value_str += "\n".join(
|
||||||
[f"\t{x}: {y}" for x, y in mount_point['values'].items()])
|
[f"\t{x}: {y}" for x, y in mount_point["values"].items()]
|
||||||
|
)
|
||||||
print(value_str)
|
print(value_str)
|
||||||
|
|
||||||
print("{:=^30}".format(' TrustRecords '))
|
print("{:=^30}".format(" TrustRecords "))
|
||||||
for tr in reg.parse_trustrecords():
|
for tr in reg.parse_trust_records():
|
||||||
print(f"Document: {tr['doc']}")
|
print(f"Document: {tr['doc']}")
|
||||||
print(f"\tCreated Date: {tr['dt']}")
|
print(f"\tCreated Date: {tr['dt']}")
|
||||||
print(f"\tMacro Enabled: {tr['macro']}")
|
print(f"\tMacro Enabled: {tr['macro']}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Registry Parsing',
|
description="Registry Parsing",
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
epilog=f"Built by {__author__}, v.{__date__}"
|
epilog=f"Built by {__author__}, v.{__date__}",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"REG_FILE", help="Path to registry file", type=argparse.FileType("rb")
|
||||||
)
|
)
|
||||||
parser.add_argument('REG_FILE', help='Path to registry file',
|
|
||||||
type=argparse.FileType('rb'))
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
main(args.REG_FILE)
|
main(args.REG_FILE)
|
||||||
|
|
@ -22,7 +22,7 @@ Open Windows Event Logs (EVTX)
|
||||||
This function shows an example of opening an EVTX file and parsing out several
|
This function shows an example of opening an EVTX file and parsing out several
|
||||||
header metadata parameters about the file.
|
header metadata parameters about the file.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_03/using_python_evtx.py
|
.. literalinclude:: ../pyforhandbook/ch03_event_logs/using_python_evtx.py
|
||||||
:pyobject: open_evtx
|
:pyobject: open_evtx
|
||||||
|
|
||||||
Iterate over record XML data (EVTX)
|
Iterate over record XML data (EVTX)
|
||||||
|
|
@ -38,7 +38,7 @@ which will then call the ``.lxml()`` method on the individual event record.
|
||||||
This requires the installation of the lxml Library, as it returns a lxml.etree
|
This requires the installation of the lxml Library, as it returns a lxml.etree
|
||||||
object that you can interact with.
|
object that you can interact with.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_03/using_python_evtx.py
|
.. literalinclude:: ../pyforhandbook/ch03_event_logs/using_python_evtx.py
|
||||||
:pyobject: get_events
|
:pyobject: get_events
|
||||||
|
|
||||||
Filtering records within events logs
|
Filtering records within events logs
|
||||||
|
|
@ -55,7 +55,7 @@ filters, and return values. Some examples include:
|
||||||
- Identify PowerShell events and expose arguments for further processing
|
- Identify PowerShell events and expose arguments for further processing
|
||||||
(ie. Base64 decoding, shellcode analysis)
|
(ie. Base64 decoding, shellcode analysis)
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_03/using_python_evtx.py
|
.. literalinclude:: ../pyforhandbook/ch03_event_logs/using_python_evtx.py
|
||||||
:pyobject: filter_events_json
|
:pyobject: filter_events_json
|
||||||
|
|
||||||
Docstring References
|
Docstring References
|
||||||
|
|
@ -90,13 +90,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'Chapin Bryce'
|
__author__ = "Chapin Bryce"
|
||||||
__date__ = 20191103
|
__date__ = 20191103
|
||||||
__license__ = 'MIT Copyright 2019 Chapin Bryce'
|
__license__ = "MIT Copyright 2019 Chapin Bryce"
|
||||||
__desc__ = '''Sample script to read EVTX files.'''
|
__desc__ = """Sample script to read EVTX files."""
|
||||||
__docs__ = [
|
__docs__ = ["https://github.com/williballenthin/python-evtx"]
|
||||||
'https://github.com/williballenthin/python-evtx'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def open_evtx(input_file):
|
def open_evtx(input_file):
|
||||||
|
|
@ -117,13 +115,15 @@ def open_evtx(input_file):
|
||||||
|
|
||||||
with evtx.Evtx(input_file) as open_log:
|
with evtx.Evtx(input_file) as open_log:
|
||||||
header = open_log.get_file_header()
|
header = open_log.get_file_header()
|
||||||
properties = OrderedDict([
|
properties = OrderedDict(
|
||||||
('major_version', 'File version (major)'),
|
[
|
||||||
('minor_version', 'File version (minor)'),
|
("major_version", "File version (major)"),
|
||||||
('is_dirty', 'File is ditry'),
|
("minor_version", "File version (minor)"),
|
||||||
('is_full', 'File is full'),
|
("is_dirty", "File is dirty"),
|
||||||
('next_record_number', 'Next record number')
|
("is_full", "File is full"),
|
||||||
])
|
("next_record_number", "Next record number"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
for key, value in properties.items():
|
for key, value in properties.items():
|
||||||
print(f"{value}: {getattr(header, key)()}")
|
print(f"{value}: {getattr(header, key)()}")
|
||||||
|
|
@ -192,22 +192,23 @@ def filter_events_json(event_data, event_ids, fields=None):
|
||||||
event_data = evt.find("EventData", evt.nsmap)
|
event_data = evt.find("EventData", evt.nsmap)
|
||||||
json_data = {}
|
json_data = {}
|
||||||
for data in event_data.getchildren():
|
for data in event_data.getchildren():
|
||||||
if not fields or data.attrib['Name'] in fields:
|
if not fields or data.attrib["Name"] in fields:
|
||||||
# If we don't have a specified field filter list, print all
|
# If we don't have a specified field filter list, print all
|
||||||
# Otherwise filter for only those fields within the list
|
# Otherwise filter for only those fields within the list
|
||||||
json_data[data.attrib['Name']] = data.text
|
json_data[data.attrib["Name"]] = data.text
|
||||||
|
|
||||||
yield json_data
|
yield json_data
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=__desc__,
|
description=__desc__,
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
epilog=f"Built by {__author__}, v.{__date__}"
|
epilog=f"Built by {__author__}, v.{__date__}",
|
||||||
)
|
)
|
||||||
parser.add_argument('EVTX_FILE', help="EVTX file to read")
|
parser.add_argument("EVTX_FILE", help="EVTX file to read")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
print("EVTX File Header Information")
|
print("EVTX File Header Information")
|
||||||
|
|
@ -221,10 +222,19 @@ if __name__ == "__main__":
|
||||||
print("Filter for Login events")
|
print("Filter for Login events")
|
||||||
logins = filter_events_json(
|
logins = filter_events_json(
|
||||||
get_events(args.EVTX_FILE, parse_xml=True),
|
get_events(args.EVTX_FILE, parse_xml=True),
|
||||||
event_ids=['4624'],
|
event_ids=["4624"],
|
||||||
fields=["SubjectUserName", "SubjectUserSid", "SubjectDomainName",
|
fields=[
|
||||||
"TargetUserName", "TargetUserSid", "TargetDomainName",
|
"SubjectUserName",
|
||||||
"WorkstationName", "IpAddress", "IpPort", "ProcessName"]
|
"SubjectUserSid",
|
||||||
|
"SubjectDomainName",
|
||||||
|
"TargetUserName",
|
||||||
|
"TargetUserSid",
|
||||||
|
"TargetDomainName",
|
||||||
|
"WorkstationName",
|
||||||
|
"IpAddress",
|
||||||
|
"IpPort",
|
||||||
|
"ProcessName",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
for login in logins:
|
for login in logins:
|
||||||
print(json.dumps(login, indent=2))
|
print(json.dumps(login, indent=2))
|
||||||
|
|
@ -17,19 +17,19 @@ This function shows an example of opening a Sqlite database with Python.
|
||||||
Additional information regarding Sqlite modules can be
|
Additional information regarding Sqlite modules can be
|
||||||
seen at https://docs.python.org/3/library/sqlite3.html.
|
seen at https://docs.python.org/3/library/sqlite3.html.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_06/opening_sqlite.py
|
.. literalinclude:: ../pyforhandbook/ch06_databases/opening_sqlite.py
|
||||||
:pyobject: open_sqlite
|
:pyobject: open_sqlite
|
||||||
|
|
||||||
Listing Tables configuration
|
Listing Tables configuration
|
||||||
============================
|
============================
|
||||||
|
|
||||||
This function shows an example of listing available tables in an opened Sqlite database.
|
This function shows an example of listing available tables in an opened Sqlite
|
||||||
|
database.
|
||||||
|
|
||||||
.. literalinclude:: ../pyforhandbook/section_06/opening_sqlite.py
|
.. literalinclude:: ../pyforhandbook/ch06_databases/opening_sqlite.py
|
||||||
:pyobject: list_tables
|
:pyobject: list_tables
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -56,39 +56,53 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
DEALINGS IN THE SOFTWARE.
|
DEALINGS IN THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = 'Brittney Argirakis'
|
__author__ = "Brittney Argirakis"
|
||||||
__date__ = 20191126
|
__date__ = 20191126
|
||||||
__license__ = 'MIT Copyright 2019 Brittney Argirakis'
|
__license__ = "MIT Copyright 2019 Brittney Argirakis"
|
||||||
__desc__ = '''Sample script to open a SqLite DB.'''
|
__desc__ = """Sample script to open a SqLite DB."""
|
||||||
__docs__ = [
|
__docs__ = [
|
||||||
'https://docs.python.org/3/library/argparse.html',
|
"https://docs.python.org/3/library/argparse.html",
|
||||||
'https://docs.python.org/3/library/os.html',
|
"https://docs.python.org/3/library/os.html",
|
||||||
'https://docs.python.org/3/library/sqlite3.html'
|
"https://docs.python.org/3/library/sqlite3.html",
|
||||||
]
|
]
|
||||||
|
|
||||||
def open_sqlite(inputdb):
|
|
||||||
print("Provided Database: {}".format(inputdb))
|
def open_sqlite(input_db):
|
||||||
return sqlite3.connect(inputdb)
|
"""Open a SQLite database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_db: Path to a SQLite database to open
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A connection to a SQLite database
|
||||||
|
"""
|
||||||
|
print("Provided Database: {}".format(input_db))
|
||||||
|
return sqlite3.connect(input_db)
|
||||||
|
|
||||||
|
|
||||||
def list_tables(conn):
|
def list_tables(conn):
|
||||||
|
"""List all tables in a SQLite database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conn: An open connection from a SQLite database
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of table names found in the database
|
||||||
|
"""
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cur.execute("SELECT name FROM sqlite_master")
|
cur.execute("SELECT name FROM sqlite_master")
|
||||||
table_list = []
|
return [i[0] for i in cur.fetchall()]
|
||||||
for i in cur.fetchall():
|
|
||||||
table_list.append(i[0])
|
|
||||||
|
|
||||||
return table_list
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=__desc__,
|
description=__desc__,
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
epilog=f"Built by {__author__}, v.{__date__}"
|
epilog=f"Built by {__author__}, v.{__date__}",
|
||||||
)
|
)
|
||||||
parser.add_argument("db", help="path to the database to read")
|
parser.add_argument("db", help="path to the database to read")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
conn = open_sqlite(args.db)
|
connection = open_sqlite(args.db)
|
||||||
listed_tables = list_tables(conn)
|
listed_tables = list_tables(connection)
|
||||||
|
|
||||||
print(listed_tables)
|
print(listed_tables)
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
__author__ = 'Chapin Bryce'
|
__author__ = "Chapin Bryce"
|
||||||
__authors__ = ['Chapin Bryce', 'Brittney Argirakis']
|
__authors__ = ["Chapin Bryce", "Brittney Argirakis"]
|
||||||
__license__ = ''
|
__license__ = ""
|
||||||
__version__ = '0.1.0'
|
__version__ = "0.1.0"
|
||||||
__copyright__ = '2020, Chapin Bryce'
|
__copyright__ = "2020, Chapin Bryce"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue