Rapidly triaging systems – pysysinfo.py

We are now ready to dive into the focus of this chapter, the pysysinfo.py script after having already covered the importance of collecting volatile information and the libraries we will use. This script is composed of a number of functions, most of which have to do with the psutil library, but at its heart identifies early on what type of system it is running on and, if that system is using the Windows operating system, runs an additional function using the WMI API, discussed previously. You can see in the following diagram how the various functions interact with each other and make up the code discussed throughout the remainder of this chapter:

This script was developed and tested on Python 2.7.15 and 3.7.1. As with any script we develop, we must start with the imports necessary to successfully execute the code we've developed. You'll notice a number of the usual imports; however, a few stand out—notably the platform module and psutil on lines 5 and 8. You may also notice that wmi is missing from this set of imports. You will understand why this is imported later in the script in a few paragraphs. This script contains seven different functions, most of which are used to process the data from the psutil library.

Note that the return_none() function is covered in the following code block instead of in a new section, as it is a one-line function that simply returns None to the calling code:

002 from __future__ import print_function
003 import argparse
004 import os
005 import platform
006 import sys
007
008 import psutil
009 if sys.version_info[0] == 2:
010 import unicodecsv as csv
011 elif sys.version_info[0] == 3:
012 import csv
...
050 def return_none():
051 """
052 Returns a None value, but is callable.
053 :return: None.
054 """
055 return None
...
058 def read_proc_connections(proc):
...
081 def read_proc_files(proc):
...
101 def get_pid_details(pid):
...
158 def get_process_info():
...
172 def wmi_info(outdir):
...
279 def csv_writer(data, outdir, name, headers, **kwargs):

The platform module, which we have not touched on previously, is part of the standard library and also provides some information about the system it is running on. In this case, we only use this library to determine the operating system of the host system executing the script.

Learn more about the platform module by reading the documentation page at https://docs.python.org/3/library/platform.html.

Moving on to the script setup, we have the argument parser, which is decidedly bland compared to some other chapters, featuring only one positional argument, OUTPUT_DIR, the output directory to write the processed data to.

If the desired output directory does not exist, we create it using the os.makedirs() function on line 323:

313 if __name__ == '__main__':
314 parser = argparse.ArgumentParser(description=__description__,
315 epilog='Developed by ' +
316 __author__ + ' on ' +
317 __date__)
318 parser.add_argument('OUTPUT_DIR',
319 help="Path to output directory. Will create if not found.")
320 args = parser.parse_args()
321
322 if not os.path.exists(args.OUTPUT_DIR):
323 os.makedirs(args.OUTPUT_DIR)

Here's where things differ a little from normal. On line 325, using the platform.system() function, we check to see if the script is being executed on a Windows system. If so, we try to import the wmi module and, if successful, call the wmi_info() method. As alluded to earlier, we import the wmi library here for a reason. When the wmi library is imported, it also loads the pywin32 module, specifically the win32com.client module. On a non-Windows system, where the pywin32 library won't be installed, this can then cause an ImportError exception. For that reason, we do not try importing wmi until we know the script is executing on a Windows machine. It's also not a bad idea to only import libraries once they are needed:

325     if 'windows' in platform.system().lower():
326 try:
327 import wmi
328 except ImportError:
329 print("Install the wmi and pywin32 modules. "
330 "Exiting...")
331 sys.exit(1)
332 wmi_info(args.OUTPUT_DIR)

Regardless of whether the system is Windows or not, we run the code captured in the next code block. On line 336, we call the get_process_info() method, which ultimately returns process data in the form of a dictionary. On line 337, we create a list containing the desired column names and keys of our pid_data dictionary. Lastly, on line 341, we call the csv_writer() method and pass in the data, desired output directory, output name, the fields list, and a keyword argument.

We will see what that keyword-argument does in a little bit:

334     # Run data gathering function
335 print("[+] Gathering current active processes information")
336 pid_data = get_process_info()
337 fields = ['pid', 'name', 'exe', 'ppid', 'cmdline',
338 'username', 'cwd', 'create_time', '_errors']
339
340 # Generate reports from gathered details
341 csv_writer(pid_data, args.OUTPUT_DIR, 'pid_summary.csv',
342 fields, type='DictWriter')

As you may have noticed, we do not have a main() function for this script and will, instead, jump right into a review of the get_process_info() method. We will cover the Windows-specific function, wmi_info(), toward the end of this chapter.