diff --git a/.github/workflows/pyblish.yml b/.github/workflows/pyblish.yml new file mode 100644 index 0000000..120af5a --- /dev/null +++ b/.github/workflows/pyblish.yml @@ -0,0 +1,67 @@ +name: Build and Publish to PyPI + +on: + push: + tags: + - "v*" + release: + types: [published] + +permissions: + contents: read + id-token: write # Required for OIDC + actions: read + +jobs: + build: + name: Build sdist and wheel + runs-on: ubuntu-latest + + steps: + - name: Check out source + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Important for setuptools_scm + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install build tool + run: | + python -m pip install --upgrade pip + pip install build setuptools_scm + + - name: Build package + run: python -m build --sdist --wheel --outdir dist + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + name: Publish to PyPI (OIDC) + needs: build + runs-on: ubuntu-latest + environment: pypi + + + permissions: + id-token: write # Required for trusted publishing + contents: read + + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI via OIDC + uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: true + skip-existing: false diff --git a/.gitignore b/.gitignore index 890d7c8..eab43d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,98 @@ -# Byte-compiled python files +.DS_Store +# Byte-compiled / optimized / DLL files __pycache__/ - -# Emacs autosaves. +*.py[cod] +*$py.class *~ -# Python Packaging +# C extensions +*.so + +# Distribution / packaging +.Python build/ +develop-eggs/ dist/ -uefi_parser.egg-info/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# PyBuilder +target/ + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +*venv/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json +# Pyre type checker +.pyre/ +_version.py +node_modules +tmp diff --git a/README.md b/README.md index 4c8e110..169c656 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,60 @@ CERT UEFI Parser ================ -The CERT UEFI Parser is a tool for inspecting firmware ROM images, -installers, and related files, especially those related to UEFI. It -pulls together information from the UEFI standards and a variety of -independent research (e.g. Igor Skochinsky's Intel ME work). - -Written in Python version 3 and using the Construct module, it's less -rigid than the EDK2 UEFI reference implementation and more easily -extended to support proprietary and experimental data structure -parsing. CERT UEFI Parser is intended to parse all related data -formats including standard file formats commonly found inside UEFI -ROMs such as Portable Executables (PEs) and images. CERT UEFI Parser -is free from NDAs or other restrictions on proprietary formats, having -been reverse engineered from widely available public information and -original work. +The CERT UEFI Parser is a Python-based tool for inspecting firmware ROM +images, installers, and related files, especially those associated with UEFI. +It combines information from the UEFI specifications with insights from +independent firmware research (for example, Igor Skochinsky’s Intel ME work). + +Written for Python 3 and built on the Construct parsing framework, the parser +is more flexible than the EDK2 reference implementation and is easier to extend +to proprietary or experimental data structures. CERT UEFI Parser aims to +support all data formats commonly found inside UEFI ROMs, including Portable +Executables (PEs) and image structures. The project is free of NDAs or other +restrictions; all proprietary formats have been reverse engineered from public +information and original analysis. Installation ------------ -To install CERT UEFI Parser, you'll need a Python virtual environment -containing not only this repository, but also a related support -library, CERT UEFI support. The commands to install everything are: +The parser depends on the **cert-uefi-support** package, which provides +lower-level decompression and binary utilities. Both packages are now +available on PyPI. + +### Basic installation: ``` $ python3 -m venv cert-venv + $ ./cert-venv/bin/pip install cert-uefi-support cert-uefi-parser ``` -You may activate the cert-venv if you choose. +### Optional GUI Support (Qt) -Then install the packages directly from GitHub: +GUI support is optional and provided via the PySide6 package. It is a +large dependency, so it is not installed by default. To install with the GUI +extras: ``` - $ ./cert-venv/bin/pip install git+https://github.com/cmu-sei/cert-uefi-support - $ ./cert-venv/bin/pip install git+https://github.com/cmu-sei/cert-uefi-parser + $ python3 -m venv cert-venv + $ ./cert-venv/bin/pip install cert-uefi-support cert-uefi-parser[qt] +``` + +### Installing from the Official Git Repositories + +``` + $ python3 -m venv cert-venv + $ ./cert-venv/bin/pip install \ + git+https://github.com/cmu-sei/cert-uefi-support \ + "cert-uefi-parser[qt] @ git+https://github.com/cmu-sei/cert-uefi-parser.git" ``` Usage ----- -CERT UEFI Parser has four primary modes: a GUI display, an ASCII text -display (including ANSI color codes by default), a JSON format, and a -filtered JSON format containing fields that might be interest for -generating an SBOM. +CERT UEFI Parser provides four primary output modes: a graphical interface, an +ASCII text display (with ANSI color output enabled by default), a full JSON +representation, and a filtered JSON representation containing fields that are +useful for generating a Software Bill of Materials (SBOM). ``` $ ./cert-venv/bin/cert-uefi-parser --gui {firmware-related-file} @@ -51,9 +63,9 @@ generating an SBOM. $ ./cert-venv/bin/cert-uefi-parser --sbom {firmware-related-file} >output.json ``` -For sample input files, we recommend downloading the BIOS updater -executable that your hardware vendor ships through their normal -support channels. While not all models are guaranteed to be -supported, many of the popular vendors are, and examining a ROM update -is a good place to get started exploring on exploring the capabilities -of CERT UEFI Parser. +Sample firmware files can typically be obtained by downloading the BIOS or +UEFI update tools from your system vendor’s support site. While not all +models are guaranteed to be fully supported, many common vendor formats parse +successfully, and examining these update files is a good way to begin exploring +the parser’s capabilities. + diff --git a/pyproject.toml b/pyproject.toml index 47693b4..4439b50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,42 +1,56 @@ [build-system] -requires = ["setuptools"] +requires = ["setuptools>=61.0", "setuptools_scm[toml]>=6.0", "wheel"] build-backend = "setuptools.build_meta" [tool.setuptools] packages = [ "uefi_parser" ] [project] -name = "uefi_parser" -version = "1.0" -authors = [ - { name = "Cory Cohen", email = "cfc@cert.org"}, - { name = "Michael Duggan", email = "mwd@cert.org"} -] +name = "cert-uefi-parser" +authors = [{ name = "CERT Threat Analysis Team", email = "cert@cert.org"}] description = "Various data structures and parsing tools for UEFI firmware." -readme = "README.md" requires-python = ">=3.10" dependencies = [ "typing_extensions", "construct>=2.10.70", "pefile", - "PySide6", "asn1crypto", "uswid", - "uefi_support", + "cert-uefi-support", ] -license-files = [ "LICENSE.md" ] -keywords = [ "security", "uefi", "firmware", "parsing", "bios" ] +readme = {file="README.md", content-type="text/markdown"} +dynamic = ["version"] +keywords = ["uefi", "firmware", "uefi-parser", "support-tools", "security", "bios"] +license = { file = "LICENSE.md" } classifiers = [ - # https://pypi.python.org/pypi?%3Aaction=list_classifiers 'Development Status :: 4 - Beta', 'Intended Audience :: System Administrators', 'Topic :: Security', - 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3', + 'License :: Other/Proprietary License', + 'Topic :: Security', + 'Topic :: System :: Hardware' +] + +[project.optional-dependencies] +qt = [ + "PySide6" ] + [project.urls] Homepage = "https://github.com/cmu-sei/cert-uefi-parser" Repository = "https://github.com/cmu-sei/cert-uefi-parser" +Issues = "https://github.com/cmu-sei/cert-uefi-parser/issues" + [project.scripts] cert-uefi-parser = "uefi_parser.cmds:cert_uefi_parser" + +[tool.setuptools.package-dir] +uefi_parser = "uefi_parser" + +#version control using git tags +[tool.setuptools_scm] +version_scheme = "guess-next-dev" +local_scheme = "node-and-date" diff --git a/uefi_parser/cmds.py b/uefi_parser/cmds.py index 1003d63..ae57d38 100755 --- a/uefi_parser/cmds.py +++ b/uefi_parser/cmds.py @@ -36,7 +36,7 @@ from .base import FirmwareStructure from .auto import AutoObject, BruteForceFinder from .utils import red, blue -from .gui import run_gui +from .gui_helper import ensure_gui_environment # Hacky solution for --no-color option, using a global. nocolor = False @@ -79,6 +79,7 @@ def process_file(filename: str, args: argparse.Namespace) -> bool: elif args.sbom: emit(json.dumps(result.sbom(), indent=2)) elif args.gui: + from .gui import run_gui run_gui(result, args) else: result.report() @@ -161,6 +162,11 @@ def cert_uefi_parser() -> None: if args.json or args.sbom: nocolor = True + # If we're going to open the GUI the environment needs to be correct. + # Do this check before parsing the file to avoid wasted effort. + if args.gui and not ensure_gui_environment(argparser): + sys.exit(3) + try: succeeded = process_file(args.file, args) # Continue processing the next file, even if this one failed, but remeber thet diff --git a/uefi_parser/gui_helper.py b/uefi_parser/gui_helper.py new file mode 100644 index 0000000..e749eae --- /dev/null +++ b/uefi_parser/gui_helper.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# CERT UEFI Parser +# +# Copyright 2025 Carnegie Mellon University. +# +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE ENGINEERING INSTITUTE MATERIAL +# IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY +# KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, +# WARRANTY OF FITNESS FOR PURPOSE OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED +# FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE ANY WARRANTY OF ANY +# KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# +# Licensed under a BSD (SEI)-style license, please see license.txt or contact +# permission@sei.cmu.edu for full terms. +# +# [DISTRIBUTION STATEMENT A] This material has been approved for public release and +# unlimited distribution. Please see Copyright notice for non-US Government use and +# distribution. +# +# This Software includes and/or makes use of Third-Party Software each subject to its own +# license. +# +# DM25-1401 +""" +Cross-platform environment checker for GUI (PySide6). +Use before importing Qt modules or creating QApplication. + +Example: + from cert_uefi_parser.environment import ensure_gui_environment + ensure_gui_environment(require_gui=True) +""" + +import sys +import os +import ctypes.util +from argparse import ArgumentParser + +# ---------------------------- +# Helper: check if PySide6 exists +# ---------------------------- +def _pyside6_installed() -> bool: + try: + import PySide6 # noqa [F401] + return True + except ImportError: + return False + + +# ---------------------------- +# Linux-specific checks +# ---------------------------- +def _check_linux_graphics(argparser: ArgumentParser) -> bool: + # 1. Check for a graphical environment + display = os.environ.get("DISPLAY") + wayland = os.environ.get("WAYLAND_DISPLAY") + + if not display and not wayland: + argparser.error( + "No graphical environment detected.\n\n" + "This GUI requires one of the following:\n" + " - X11 (DISPLAY)\n" + " - Wayland (WAYLAND_DISPLAY)\n\n" + "If running over SSH, use: ssh -X or ssh -Y\n" + "If running on a server, you may need: xvfb-run python yourapp.py" + ) + return False + + # 2. Check for required XCB libraries used by Qt + required_xcb = [ + "xcb", "xcb-render", "xcb-shm", "xcb-cursor", "xcb-icccm", + "xcb-keysyms", "xcb-randr", "xcb-xinerama", "xcb-xfixes" + ] + + missing = [lib for lib in required_xcb if ctypes.util.find_library(lib) is None] + + if missing: + argparser.error( + "Missing required system libraries for Qt (XCB backend):\n" + + "".join(f" - {lib}\n" for lib in missing) + + "\nInstall them using your system package manager.\n\n" + "Ubuntu/Debian example:\n" + " sudo apt install libxcb-cursor0 libxcb-icccm4 " + "libxcb-keysyms1 libxcb-shape0 libxcb-xinerama0 " + "libxcb-render-util0" + ) + return False + return True + +# ---------------------------- +# Main cross-platform entry point +# ---------------------------- +def ensure_gui_environment(argparser: ArgumentParser) -> bool: + """ + Verifies that the runtime environment supports GUI operations. + """ + # Check if PySide6 is installed at all. + if True or not _pyside6_installed(): + argparser.error( + "GUI support requested but PySide6 is not installed.\n\n" + "Install with: pip install cert-uefi-parser[qt]") + return False + + # Per-OS behavior + if sys.platform.startswith("win"): + # Windows always works — Qt uses native backend. + return True + + if sys.platform == "darwin": + # macOS also always works — uses Cocoa backend. + return True + + if sys.platform.startswith("linux"): + if _check_linux_graphics(argparser): + return True + return False + + # Unsupported OS + argparser.error(f"Unsupported OS for GUI: {sys.platform}") + return False