Skip to content

Instantly share code, notes, and snippets.

@hevlastka
Last active May 18, 2022 14:18
Show Gist options
  • Save hevlastka/dc9b703a0c4ff1114b8bb5e3bb40e04d to your computer and use it in GitHub Desktop.
Save hevlastka/dc9b703a0c4ff1114b8bb5e3bb40e04d to your computer and use it in GitHub Desktop.
#!/bin/python
import os
import re
import sys
import json
import socket
import typing
import shutil
import logging
import subprocess # pragma: nosec
logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
hostname = socket.gethostname()
def get_distribution():
if os.path.exists("/etc/os-release"):
id_like = None
id = None
with open("/etc/os-release", "r") as _f:
context = _f.readlines()
for line in context:
line = line.replace("\n", "")
if line.startswith("ID_LIKE="):
id_like = line.replace("ID_LIKE=", "")
if line.startswith("ID="):
id = line.replace("ID=", "")
return id_like or id
def get_bitness():
return "x64" if sys.maxsize > 2**32 else "x86"
def default_parser(item) -> typing.Tuple[str, str, str]:
package, version = item.split(" ", 1)
return package, version, item
def snap_parser(item) -> typing.Tuple[str, str, str]:
if item.replace(" ", "") == "NameVersionRevTrackingPublisherNotes":
raise ValueError("snap Header provided")
package, version_detail = item.split(" ", 1)
version, detail = version_detail.strip().split(" ", 1)
return package, version, item
def apt_parser(
item,
) -> typing.Tuple[str, str, str]:
"""apt Package names and versions need cleaning up"""
universe_package, version_detail = item.split(" ", 1)
package, universe = universe_package.split("/", 1)
version, detail = version_detail.split(" ", 1)
match = re.match(
r"(\d[:-])?(?P<major>\d{1,8})(\.(?P<minor>\d*))?(\.?(?P<patch>\d*))?(([\.-])(?P<build>[a-zA-Z0-9]*))?",
version,
)
if match:
version = match.group(0)
if ":" in version[1:3]:
version = version.split(":")[1]
return package, version, item
def apk_parser(item) -> typing.Tuple[str, str, str]:
match = re.match(
r"(?P<package>.*)\-(?P<version>[^\-r]*)(\-(?P<build>r[0-9]))?$", item
)
if not match:
return ("", "", "")
return match.group("package"), match.group("version"), item
def pacman_parser(item) -> typing.Tuple[str, str, str]:
package, version = item.split(" ")
if ":" in version[1:3]:
version = version.split(":")[1]
return package, version, item
tools = {
"rpm": {
"args": "-qa",
},
"dnf": {
"args": "list installed",
},
"zypper": {
"args": "se --installed",
},
"pacman": {"args": "-Q", "parser": pacman_parser},
"apt": {
"args": "list --installed",
"parser": apt_parser,
},
"dpkg-query": {
"args": "-f '${Package} ${Version}\n' -W '*'",
},
"flatpack": {"args": "list"},
"snap": {"args": "list", "parser": snap_parser},
"apk": {"args": "info -v", "parser": apk_parser},
}
# 'name' 'version' 'tool' 'original'
installed_packages = []
def is_tool(name):
"""Check whether `name` is on PATH and marked as executable."""
# from whichcraft import which
from shutil import which
return which(name) is not None
def get_installed():
for tool, parameters in tools.items():
if is_tool(tool):
logging.info(f"{tool} present, checking reported packages")
process = subprocess.Popen(
[tool, *parameters["args"].split(" ")],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout, stderr = process.communicate()
for item in stdout.decode().split("\n"):
if len(item) == 0: # missing or EOL
continue
try:
parser = tools[tool].get("parser", default_parser)
package, version, original = parser(item)
installed_packages.append((package, version, tool, original))
except ValueError as e:
print(f'Ignoring line "{item}"...')
with open("pkg.manifest", "w") as _f:
_f.write(
json.dumps(
{
"distribution": get_distribution(),
"host": hostname,
"packages": installed_packages,
"architecture": get_bitness(),
},
indent=2,
)
)
logging.info("Results saved to pkg.manifest")
return
if __name__ == "__main__":
get_installed()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment