16 Gathering System-Level Information
Introduction
In this chapter, you’ll collect a practical cheat sheet for gathering system-level information with Python. Some functions are built into the standard library, and some come from third-party tools such as psutil.
For various reasons when writing applications, you’ll want to gather system-level information. It might be to fingerprint the environment, produce debugging output, or tell the user which system your application is having issues with.
Concepts Covered
Why This Matters
When you’re writing security tools, support scripts, or diagnostics, you’ll often need to understand where your code is running. Good system information helps you debug weird failures, produce useful reports, and avoid making assumptions that only work on your own machine.
Objective
Use Python to retrieve operating system, path, network, process, and environment information in a way that’s readable enough to reuse in your own tools.
Operating System and Version
platform.machine()-
Returns the machine type, e.g.
i386. You’ll get an empty string if Python can’t determine the value.>>> import platform >>> platform.machine() 'x86_64' platform.node()-
Returns the computer’s network name. You’ll get an empty string if Python can’t determine the value.
The network name may not be fully qualified!
>>> import platform
>>> platform.node()
'salem'platform.processor()-
Returns the processor name, e.g.
'amdk6'. You’ll get an empty string if Python can’t determine the value.>>> import platform >>> platform.processor() ''Empty strings aren’t automatically errors here. Sometimes the operating system simply doesn’t expose the detail Python asked for, so treat these helpers as useful clues rather than sworn testimony.
platform.python_build()-
Returns a tuple
(buildno, builddate)stating the Python build number and date as strings.>>> import platform >>> platform.python_build() ('default', 'Jan 22 2019 21:57:15') platform.python_compiler()-
Returns a string identifying the compiler used for compiling Python.
>>> import platform >>> platform.python_compiler() 'GCC 6.3.0 20170516' platform.python_version()-
Returns the Python version as a
'major.minor.patchlevel'string.>>> import platform >>> platform.python_version() '3.7.1' platform.system()-
Returns the system/OS name, e.g.
'Linux','Windows', or'Java'. You’ll get an empty string if Python can’t determine the value.>>> import platform >>> platform.system() 'Linux' platform.uname()-
Gives you a fairly portable uname interface. It returns a
namedtuple()containing six attributes:system,node,release,version,machine, andprocessor.>>> import platform >>> platform.uname() uname_result(system='Linux', node='salem', release='4.9.0-8-amd64', version='#1 SMP Debian 4.9.144-3.1 (2019-02-19)', machine='x86_64', processor='')
Networking
Some behaviour may be platform-dependent, because these calls use the operating system’s socket APIs.
Networking helpers are especially good at reminding us that Python is often politely asking the operating system for an answer. If the OS resolver, host file, or service database differs, Python’s answer may differ too.
socket.getfqdn([name])-
Returns a fully qualified domain name for
name. Ifnameis omitted or empty, Python treats it as the local host.>>> import socket >>> socket.getfqdn("localhost") 'localhost' socket.gethostbyname(hostname)-
Translates a host name to IPv4 address format. The IPv4 address comes back as a string, such as
'100.50.200.5'. If the host name is already an IPv4 address, Python returns it unchanged.>>> import socket >>> socket.gethostbyname("localhost") '127.0.0.1' socket.gethostname()-
Returns a string containing the hostname of the machine where the Python interpreter is currently running.
gethostname() doesn’t always return the FQDN (Fully Qualified Domain Name). If you need an FQDN, use socket.getfqdn().
>>> import socket
>>> socket.gethostname()
'salem'socket.gethostbyaddr(ip_address)-
Returns a tuple
(hostname, aliaslist, ipaddrlist).hostnameis the primary host name responding to the given IP (Internet Protocol) address,aliaslistcontains alternative host names for the same address, andipaddrlistcontains IP v4/v6 addresses for the same interface on the same host.>>> import socket >>> socket.gethostbyaddr("127.0.0.1") ('localhost', [], ['127.0.0.1']) socket.getservbyname(servicename[, protocolname])-
Translate an Internet service name and protocol name to a port number for that service. Python gets these mappings from the operating system, which usually tracks the IANA Service Name and Transport Protocol Port Number Registry.
If you provide the protocol name, use
tcporudp; otherwise Python can match any protocol.>>> import socket >>> socket.getservbyname("gopher", 'tcp') 70 >>> socket.getservbyname("pop3") 110 socket.getservbyport(port[, protocolname])-
Translate an Internet port number and protocol name to a service name for that service.
If you provide the protocol name, use
tcporudp; otherwise Python can match any protocol.>>> import socket >>> socket.getservbyport(70) 'gopher' >>> socket.getservbyport(70, 'udp') Traceback (most recent call last): File "<input>", line 1, in <module> OSError: port/proto not found
Operating System Interfaces
The os module gets us closer to the process Python is running inside: real and effective IDs, login names, groups, and other operating-system details. These are the kinds of calls that are boring right up until a permissions bug makes them fascinating.
os.getegid()-
Returns the effective group id of the current process. This corresponds to the “set id” bit on the file being executed in the current process.
NoteOnly available on UNIX (and macOS).
>>> import os >>> os.getegid() 1000 os.getuid()-
Returns the current process’s real user id.
NoteOnly available on UNIX (and macOS).
>>> import os >>> os.getuid() 1000 os.getgid()-
Returns the real group id of the current process.
NoteOnly available on UNIX (and macOS).
>>> import os >>> os.getgid() 1000 os.getgrouplist()-
Returns the list of group ids that a user belongs to. If
groupisn’t already in the list, Python includes it; typically,groupis the group ID field from the password record foruser.NoteOnly available on UNIX (and macOS).
>>> import os >>> os.getgrouplist('alzion', 1) [1, 24, 25, 27, 29, 30, 44, 46, 108, 113, 114] os.getlogin()-
Returns the name of the user logged in on the controlling terminal of the process.
NoteOnly available on UNIX (and macOS) and Windows.
>>> import os >>> os.getlogin() 'alzion'
Using pathlib
The built-in pathlib module gives you a readable, object-oriented way to manipulate paths and find pathname information. Instead of treating paths as plain strings, you create a Path object and ask it questions.
Path doesn’t expand ~ automatically. Use .expanduser() when you want a user-relative path to become a real home-directory path.
This is another place where being explicit saves pain. A path that works from your terminal may not work from a daemon, notebook, cron job, service file, or test runner if the working directory is different.
Path.cwd()andPath(path).resolve()-
Return absolute path information.
Path.cwd()gives you the current working directory.Path(path).resolve()returns an absolute version ofpathand resolves symlinks it can follow.>>> from pathlib import Path >>> Path.cwd() PosixPath('/current/project') >>> Path('imgs').resolve() PosixPath('/current/project/imgs') Path(path).exists()-
Returns
Trueif the path refers to an existing path or an open file. ReturnsFalsefor broken symbolic links. On some platforms, if you don’t have permission to inspect the path, it may returnFalseeven if the path physically exists.>>> from pathlib import Path >>> Path('missing-report.txt').exists() False >>> Path('imgs').exists() True Path(path).expanduser()-
Expands an initial
~to the user’s home directory.Python uses the platform’s usual home-directory rules: on UNIX-like systems, it starts with the
HOMEenvironment variable when available; on Windows, it checks the usual user-profile environment variables.>>> from pathlib import Path >>> Path('~').expanduser() PosixPath('/home/alzion') >>> Path('~/tmp/learning-python-for-offensive-security').expanduser() PosixPath('/home/alzion/tmp/learning-python-for-offensive-security') Path(path).stat().st_atime-
Returns the last access time for
path. The return value is the number of seconds since EPOCH, e.g.1551832874.TipEPOCH is also known as POSIX time or UNIX Epoch time. It describes a point in time as the number of seconds that have elapsed since 00:00:00 Thursday, 1 January 1970 (UTC), minus leap seconds.
Raises an
OSErrorif the file doesn’t exist or is inaccessible.>>> from pathlib import Path >>> Path('.').stat().st_atime 1551834369.9661243 >>> Path('requirements.txt').stat().st_atime 1551832462.8749728 Path(path).stat().st_mtime-
Returns the last modified time for
path. The return value is the number of seconds since EPOCH, e.g.1551832874.Raises an
OSErrorif the file doesn’t exist or is inaccessible.>>> from pathlib import Path >>> Path('.').stat().st_mtime 1551834369.9661243 >>> Path('requirements.txt').stat().st_mtime 1551832462.8749728 Path(path).stat().st_size-
Returns the size of a path in bytes. Raises an
OSErrorif the file doesn’t exist or is inaccessible.>>> from pathlib import Path >>> Path('.').stat().st_size 320 >>> Path('requirements.txt').stat().st_size 73 Path(path).is_absolute()-
Returns
Trueifpathis an absolute pathname. On Unix, that means it begins with a slash. On Windows, it begins with a slash or backslash after Python accounts for a possible drive letter.>>> from pathlib import Path >>> Path('.').is_absolute() False >>> Path('/home/alzion/sauce/learning-python-for-offensive-security').is_absolute() True Path(path).is_file()-
Returns
Trueif path is an existing regular file.>>> from pathlib import Path >>> Path('.').is_file() False >>> Path('/home/alzion/sauce/learning-python-for-offensive-security/requirements.txt').is_file() True Path(path).is_dir()-
Returns
Trueif path is an existing directory.>>> from pathlib import Path >>> Path('.').is_dir() True >>> Path('/home/alzion/sauce/learning-python-for-offensive-security/requirements.txt').is_dir() False Path(path).is_mount()-
Returns
Trueifpathis a mount point: a place in a file system where a different file system has been mounted.WarningMount-point detection has platform-specific edge cases, especially with bind mounts and unusual filesystem layouts. Treat
Path.is_mount()as a useful helper, not a complete storage-inventory tool.
psutil
psutil (process and system utilities) is a third-party cross-platform library for retrieving information about running processes and system utilisation (CPU, memory, disks, network, and sensors) in Python. It implements a number of UNIX-based command-line tools and works on Linux, Windows, macOS, FreeBSD, OpenBSD, NetBSD, Sun Solaris, and AIX.
You can install it with python -m pip install psutil in the environment where you plan to run the examples. It’s really handy when you need to inspect what’s running, what resources exist, and what your process can see.
psutil.pids()-
Returns a sorted list of currently running PIDs. To iterate over all processes and avoid race conditions, prefer
process_iter().>>> import psutil >>> psutil.pids() [0, 1, 40, 41, 44, 45, 46, 48, 51, 52, 54, 55, 58, 59, 65, 66, 67, 70, 71, 75, 76, 77, 78, 79, 80, 81, 82, 84, 86, 87, 89, 91, 93, 94, 95, 96, 98, 100, 101, 102, 103, 104, 107, 109, 114, 115, 125, 148, 162, 164, 173, 175, 176, 177, 178, 179, 186, 192, 193, 197, 199, 200, 201, 202, 203, 204, 206, 207, 208, 209, 210, 211, 212, 213, 215, 216, 217, 218, 219, 224, 228, 229, 230, 231, 232, 233, 236, 238, 239, 240, 241, 242, 250, 251, 253, 254, 259, 263, 264, 265, 268, 271, 272, 274, 276, 277, 278, 279, 280, 281, 282, 284, 286, 287, 288, 289, 290, 291, 292, 293, 296, 297, 298, 303, 304, 305, 306, 307, 308, 311, 314, 315, 316, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 359, 361, 363, 364, 365, 366, 367, 370, 372, 373, 374, 375, 377, 378, 379, 380, 381, 382, 384, 385, 389, 392, 393, 394, 397, 398, 400, 401, 405, 406, 407, 409, 411, 412, 415, 416, 417, 421, 423, 425, 426, 427, 428, 429, 430, 431, 432, 434, 435, 448, 450, 451, 462, 463, 464, 465, 466, 469, 479, 480, 481, 483, 484, 485, 489, 495, 497, 555, 557, 562, 565, 567, 568, 641, 642, 653, 656, 662, 665, 666, 673, 674, 675, 676, 689, 690, 691, 694, 695, 697, 699, 700, 701, 705, 708, 709, 711, 712, 719, 727, 729, 730, 731, 732, 733, 735, 737, 738, 739, 740, 744, 745, 746, 747, 750, 751, 752, 755, 756, 757, 758, 762, 763, 835, 838, 839, 842, 844, 872, 918, 1074, 1080, 1081, 1082, 1083, 1113, 1126, 1128, 1135, 1146, 1149, 1150, 1161, 1163, 1164, 1165, 1237, 1281, 1283, 1284, 1286, 1289, 1290, 1293, 1350, 1351, 1396, 1416, 1439, 1461, 1467, 1471, 1472, 1473, 1474, 1475, 1477, 1481, 1506, 1511, 1512, 1514, 1584, 1593, 1662, 1664, 1666, 1667, 1671, 1673, 1690, 1710, 1775, 1928, 2112, 2117, 2118, 2119, 2125, 2126, 2127, 2128, 2163, 2164, 2165, 2166, 2167, 2168, 2171, 2176, 2183, 2184, 2185, 2188, 2190, 2218, 2219] psutil.pid_exists(pid)-
Check whether the given PID exists in the current process list.
>>> import psutil >>> psutil.pid_exists(415) True >>> psutil.pid_exists(20) False psutil.Process(pid=None)-
Represents an OS process with the given pid. If
pidis omitted, Python uses the current process pid (os.getpid).>>> process = psutil.Process(740) psutil.Process(pid=None).name()-
Returns the process name. On Windows, the return value is cached after the first call. On POSIX, it isn’t cached because the process name may change.
>>> process = psutil.Process(740) >>> process.name() 'Brave Browser Helper' psutil.Process().environ()-
Returns the process environment variables as a dict.
May not reflect changes made after the process started.
>>> import psutil
>>> env = psutil.Process().environ()
>>> sorted(env)[:4]
['HOME', 'LC_CTYPE', 'PATH', 'SHELL']psutil.Process().environ()-
Returns the process current working directory as an absolute path.
>>> psutil.Process().cwd() '/Volumes/0X00/python/heartbleed' psutil.cpu_count()-
Returns the number of CPUs available on the machine.
>>> import psutil >>> psutil.cpu_count() 4 psutil.disk_partitions()-
Returns all mounted disk partitions as a list of named tuples including device, mount point, and filesystem type. The optional
allargument tries to distinguish physical devices such as hard drives, CD-ROMs, and USB devices from memory partitions when set toFalse.>>> import psutil >>> psutil.disk_partitions(all=False) [sdiskpart(device='/dev/disk1s1', mountpoint='/', fstype='apfs', opts='rw,local,rootfs,dovolfs,journaled,multilabel'), sdiskpart(device='/dev/disk1s4', mountpoint='/private/var/vm', fstype='apfs', opts='rw,noexec,local,dovolfs,dontbrowse,journaled,multilabel,noatime'), sdiskpart(device='/dev/disk2s1', mountpoint='/Volumes/0X00', fstype='exfat', opts='rw,nosuid,local,ignore-ownership')] >>> psutil.disk_partitions(all=False) [sdiskpart(device='/dev/disk1s1', mountpoint='/', fstype='apfs', opts='rw,local,rootfs,dovolfs,journaled,multilabel'), sdiskpart(device='/dev/disk1s4', mountpoint='/private/var/vm', fstype='apfs', opts='rw,noexec,local,dovolfs,dontbrowse,journaled,multilabel,noatime'), sdiskpart(device='/dev/disk2s1', mountpoint='/Volumes/0X00', fstype='exfat', opts='rw,nosuid,local,ignore-ownership')] psutil.Popen(*args, **kwargs)-
A more convenient interface to the built-in
subprocess.Popen.>>> import psutil >>> from subprocess import PIPE >>> p = psutil.Popen(["/usr/bin/python", "-c", "print('hello')"], stdout=PIPE) >>> p.name() 'Python' >>> p.username() 'rebecca' >>> p.communicate() (b'hello\n', None)psutil.Popenalso works as a context manager (withis supported). On exit, file descriptors are closed and the process is waited for, which gives you a cleaner shutdown:>>> import psutil >>> import subprocess >>> with psutil.Popen(["ifconfig"], stdout=subprocess.PIPE) as proc: >>> log.write(proc.stdout.read())
Find a Process by Name
This tiny helper is the start of a useful pattern: gather a list of processes, filter it by something meaningful, and report the result. In a real tool, remember that process lists can change while you’re looking at them, so expect the occasional race condition.
import psutil
def find_procs_by_name(name: str):
ls = []
for process in psutil.process_iter(attrs=['name']):
if process.info['name'] == name:
ls.append(process)
return lsIdentify an IP Address Location
This example shows how a command-line tool can combine argparse, an HTTP client, and JSON parsing. Use documentation addresses such as 203.0.113.42 from RFC 5737 while practising so the example stays harmless and repeatable.
The json part of the URL is the API endpoint path that asks the service to return JSON. It isn’t an HTTP Content-Type, and the IP address isn’t being sent as JSON.
If you’re the decorating and/or customisation type, this is also another chance to use a sweet argparse description. Just keep the target address safe while practising.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__version__ = "0.0.0"
import json
import urllib3
import argparse
def build_argument_parser():
parser = argparse.ArgumentParser(description=f"""
███▄ ▄███▓▄▄▄ ▄▄▄█████▓▄▄▄ ██░ ██ ▄▄▄ ██▀███ ██▓
▓██▒▀█▀ ██▒████▄ ▓ ██▒ ▓▒████▄ ▓██░ ██▒████▄ ▓██ ▒ ██▓██▒
▓██ ▓██▒██ ▀█▄▒ ▓██░ ▒▒██ ▀█▄ ▒██▀▀██▒██ ▀█▄ ▓██ ░▄█ ▒██▒
▒██ ▒██░██▄▄▄▄█░ ▓██▓ ░░██▄▄▄▄██ ░▓█ ░██░██▄▄▄▄██▒██▀▀█▄ ░██░
▒██▒ ░██▒▓█ ▓██▒▒██▒ ░ ▓█ ▓██▒ ░▓█▒░██▓▓█ ▓██░██▓ ▒██░██░
░ ▒░ ░ ░▒▒ ▓▒█░▒ ░░ ▒▒ ▓▒█░ ▒ ░░▒░▒▒▒ ▓▒█░ ▒▓ ░▒▓░▓
░ ░ ░ ▒ ▒▒ ░ ░ ▒ ▒▒ ░ ▒ ░▒░ ░ ▒ ▒▒ ░ ░▒ ░ ▒░▒ ░
░ ░ ░ ▒ ░ ░ ▒ ░ ░░ ░ ░ ▒ ░░ ░ ▒ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
Version: {__version__}
""", formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument(dest="ip_address",
help="the IP address you wish to find the location of")
return parser.parse_args()
def main(arguments: argparse.Namespace):
http = urllib3.PoolManager()
url = f"http://ip-api.com/json/{arguments.ip_address}"
response = http.request("GET", url)
data = json.loads(response.data.decode("utf-8"))
print(f"The host {arguments.ip_address} resides "
f"in {data['country']} ({data['lat']}, {data['lon']}).")
if __name__ == "__main__":
args = build_argument_parser()
main(args)Checkpoint
Write down three facts your system can report without making a network request: one from platform, one from os or pathlib, and one from socket. Which of those facts would be useful in a bug report?
Extension Activity
Turn the find_procs_by_name() example into a tiny report that prints each matching process PID and name. If you try it on your own machine, choose a harmless process name and handle the case where no matches are found.
Further Reading
- platform — Access to underlying platform’s identifying data.
- socket — Low-level networking interface.
- os — Miscellaneous operating system interfaces.
- pathlib — Object-oriented filesystem paths.
- argparse — Parser for command-line options, arguments and subcommands.
- psutil documentation.
- IANA Service Name and Transport Protocol Port Number Registry.