From 7c328ab48bfe07b99c896c8695b7e51edf431ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Srokosz?= Date: Wed, 19 Aug 2026 17:36:21 +0200 Subject: [PATCH] Bump version: v0.21.0 (#1220) --- docs/conf.py | 3 +- docs/whats_changed.rst | 30 +++ drakrun/version.py | 2 +- drakrun/web/frontend/package-lock.json | 4 +- drakrun/web/frontend/package.json | 2 +- scripts/bump_version.json | 9 - scripts/bump_version.py | 274 ++++++++++++++++++++++--- scripts/bump_version.toml | 13 ++ 8 files changed, 293 insertions(+), 44 deletions(-) delete mode 100644 scripts/bump_version.json create mode 100644 scripts/bump_version.toml diff --git a/docs/conf.py b/docs/conf.py index 93ddb258..4f0b7c06 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ copyright = '2025, CERT Polska' author = 'CERT Polska' # The full version, including alpha/beta/rc tags -release = 'v0.20.0' +release = 'v0.21.0' latex_engine = 'xelatex' @@ -41,6 +41,7 @@ def setup(app): # ones. extensions = [ "sphinx_rtd_theme", + "sphinx.ext.autosectionlabel", "sphinx.ext.graphviz", ] diff --git a/docs/whats_changed.rst b/docs/whats_changed.rst index a09e07b4..08ada474 100644 --- a/docs/whats_changed.rst +++ b/docs/whats_changed.rst @@ -2,6 +2,36 @@ What's changed, how to upgrade? =============================== +v0.21.0 +------- + +This release comes with multiple bugfixes and additional web application features. + +Most important changes: + +- Web: All analysis files can be downloaded as .zip archive +- Added offline HTML analysis reports that can be downloaded and shared with someone without direct access to Drakvuf Sandbox +- Support for "shellexec" injection method and runas elevation +- Improved iptables rules, added conntrack drop after analysis + +Known issues: + +- Analysis may hang on Windows 7 with pre-installed Powershell 2.0. It's recommended to upgrade Powershell to at least 5.1. See also `the issue #1199 on Github `_. + +This version was tested using `DRAKVUF v1.1-8107f115 `_ and may not work correctly with older versions. +It's recommended to upgrade DRAKVUF to this version or newer before running analyses. + +After upgrading Drakvuf engine, it's also recommended to regenerate VMI profiles to include the 64-bit .NET ``clr.dll`` profile. This can be +done by doing the following operations: + +- shut down all of the workers and ensure that all of the guest VMs are stopped +- run ``drakrun modify-vm0 begin`` to start VM-0 +- after ensuring that VM started correctly, run ``drakrun modify-vm0 commit`` to create a new snapshot and refresh the VMI profiles. + +It's recommended to read the :ref:`Managing snapshots` documentation section before running these operations. + +Complete changelog can be found here: `v0.21.0 changelog `_. + v0.20.0 ------- diff --git a/drakrun/version.py b/drakrun/version.py index 5f4bb0b3..6a726d85 100644 --- a/drakrun/version.py +++ b/drakrun/version.py @@ -1 +1 @@ -__version__ = "0.20.0" +__version__ = "0.21.0" diff --git a/drakrun/web/frontend/package-lock.json b/drakrun/web/frontend/package-lock.json index 2afaa81c..753590f1 100644 --- a/drakrun/web/frontend/package-lock.json +++ b/drakrun/web/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "@drakvuf-sandbox/web", - "version": "0.20.0", + "version": "0.21.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@drakvuf-sandbox/web", - "version": "0.20.0", + "version": "0.21.0", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2", diff --git a/drakrun/web/frontend/package.json b/drakrun/web/frontend/package.json index 1bee49bf..207f3364 100644 --- a/drakrun/web/frontend/package.json +++ b/drakrun/web/frontend/package.json @@ -1,7 +1,7 @@ { "name": "@drakvuf-sandbox/web", "private": true, - "version": "0.20.0", + "version": "0.21.0", "type": "module", "scripts": { "dev": "vite", diff --git a/scripts/bump_version.json b/scripts/bump_version.json deleted file mode 100644 index d0e2e933..00000000 --- a/scripts/bump_version.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "files": { - "drakrun/web/frontend/package.json": "\"version\": \"$VERSION\"", - "drakrun/web/frontend/package-lock.json": "\"version\": \"$VERSION\"", - "drakrun/version.py": "__version__ = \"$VERSION\"", - "docs/conf.py": "release = 'v$VERSION'" - }, - "regex": "(\\d+\\.\\d+\\.\\d+(?:-(post|dev|alpha|beta|rc)[0-9]?)?)" -} diff --git a/scripts/bump_version.py b/scripts/bump_version.py index eeba7518..ca0a797b 100755 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -1,27 +1,161 @@ #! /usr/bin/env python3 -# bump_version.py 0.1.0 +# Copyright (c) 2026, CERT Polska. All rights reserved. +# +# This file's content is free software; you can redistribute and/or +# modify it under the terms of the *BSD 3-Clause "New" or "Revised" +# +# bump_version 0.2.0 - Utility for version bump and management +# +# This file automates the version bumping in various files and +# assists CI workflow in determining the version information. +# +# Usage: python3 bump_version.py --help + +import argparse import difflib -import json +import itertools +import tomllib import re import sys + from pathlib import Path -CONFIG = json.loads((Path(__file__).parent / "bump_version.json").read_text()) -CURRENT_DIR = Path.cwd() +SCRIPT_ROOT = Path(__file__).parent +PROJECT_ROOT = SCRIPT_ROOT.parent + +CONFIG = tomllib.loads((SCRIPT_ROOT / "bump_version.toml").read_text()) VERSION_FILES = { - (CURRENT_DIR / path): pattern for path, pattern in CONFIG["files"].items() + (PROJECT_ROOT / path): patterns for path, patterns in CONFIG["files"].items() } -VERSION_REGEX = CONFIG["regex"] +# https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers +VERSION_REGEX = ( + r"(?P" + r"(?P\d+)[.]" + r"(?P\d+)[.]" + r"(?P\d+)" + r"(?P(?:a|b|rc)\d+)?" + r"(?P(?:.post)\d+)?" + r"(?P(?:.dev)\d+)?" + r")" +) +CHANGELOG_FILE = PROJECT_ROOT / "CHANGELOG.md" -def main(new_version): +def get_changelog_header_pattern() -> str: + """Load and validate changelog header from configuration""" + header_pattern = CONFIG.get("changelog", {}).get("header_pattern") + + if not header_pattern: + raise RuntimeError( + "[changelog].header_pattern is undefined in bump_version.toml" + ) + + if not "$VERSION" in header_pattern: + raise RuntimeError("[changelog].header_pattern must have $VERSION mark") + + return header_pattern + + +def load_changelog() -> str: + """Load changelog file contents""" + if not CHANGELOG_FILE.exists(): + raise RuntimeError(f"Changelog file '{CHANGELOG_FILE}' doesn't exist") + + return CHANGELOG_FILE.read_text() + + +def extract_changelog() -> dict[str, str]: + """Load changelog entries for each version""" + changelog = load_changelog() + header_pattern = get_changelog_header_pattern() + + version_pattern = header_pattern.replace("$VERSION", VERSION_REGEX) + matches = re.finditer(version_pattern, changelog) + entries = {} + for cur_version, next_version in itertools.pairwise( + itertools.chain(matches, [None]) + ): + start = changelog.index("\n", cur_version.end()) + end = ( + len(changelog) + if not next_version + else changelog.rindex("\n", start, next_version.start()) + ) + entries[cur_version.group("version")] = changelog[start:end].strip() + return entries + + +def find_latest_version() -> str: + """Find current latest version from version files""" + current_version = None + + for path in VERSION_FILES.keys(): + if not path.exists(): + raise RuntimeError(f"File {path} is missing in project root dir?") + + content = path.read_text() + + for pattern in VERSION_FILES[path]: + pattern = pattern.replace("$VERSION", VERSION_REGEX) + version = next(re.finditer(pattern, content)).group("version") + if current_version is not None and version != current_version: + raise RuntimeError( + f"{path} contains different version than other files " + f"({version} != {current_version})" + ) + current_version = version + if not current_version: + raise RuntimeError("Version files not found or undefined") + return current_version + + +def increment_version(version: str, major: bool, minor: bool, patch: bool) -> str: + """ + Increment version number. + + Raises RuntimeError when version is a pre-release or dev-release. + Major, minor and patch flags are treated as mutually exclusive. + + :param version: Version number to increment + :param major: Increment major + :param minor: Increment minor + :param patch: Increment patch + :return: Incremented version number + """ + version_match = re.fullmatch(VERSION_REGEX, version) + if not version_match: + raise RuntimeError(f"'{version}' doesn't match the regex: {VERSION_REGEX}") + if version_match.group("pre_release") or version_match.group("dev_release"): + raise RuntimeError( + f"'{version}' is a pre-release version, please provide version manually" + ) + + vmajor = int(version_match.group("major")) + vminor = int(version_match.group("minor")) + vpatch = int(version_match.group("patch")) + if major: + vmajor = vmajor + 1 + vminor = vpatch = 0 + elif minor: + vminor = vminor + 1 + vpatch = 0 + elif patch: + vpatch = vpatch + 1 + return f"{vmajor}.{vminor}.{vpatch}" + + +def bump_version(new_version: str) -> None: + """ + Interactively bumps version, prompting user for confirmation + + :param new_version: New version specified by user + """ input_files = {} output_files = {} old_version = None - if not re.match(fr"^{VERSION_REGEX}$", new_version): - print(f"[!] '{new_version}' doesn't match the regex: {VERSION_REGEX}") - return + if not re.fullmatch(VERSION_REGEX, new_version): + raise RuntimeError(f"'{new_version}' doesn't match the regex: {VERSION_REGEX}") def subst_version(repl): return ( @@ -32,34 +166,55 @@ def main(new_version): for path in VERSION_FILES.keys(): if not path.exists(): - print(f"[!] File {str(path)} is missing. Are you in project root dir?") - return False + raise RuntimeError(f"File {path} is missing in project root dir?") - with open(path, "r") as f: - content = input_files[path] = f.read() + content = input_files[path] = path.read_text() - pattern = VERSION_FILES[path].replace("$VERSION", VERSION_REGEX) - print(pattern) - version = next(re.finditer(pattern, content)).group(1) - output_files[path] = re.sub(pattern, subst_version, content, count=1) + for pattern in VERSION_FILES[path]: + pattern = pattern.replace("$VERSION", VERSION_REGEX) + version = next(re.finditer(pattern, content)).group("version") + content = re.sub(pattern, subst_version, content) + output_files[path] = content - if old_version is not None and version != old_version: - print( - f"[!] {str(path)} contains different version than other files " - f"({version} != {old_version})" + if old_version is not None and version != old_version: + raise RuntimeError( + f"{path} contains different version than other files " + f"({version} != {old_version})" + ) + old_version = version + + if CHANGELOG_FILE.exists(): + changelog = load_changelog() + header_pattern = get_changelog_header_pattern() + changelog_entries = extract_changelog() + if new_version not in changelog_entries: + if "new_header_pattern" in CONFIG.get("changelog", {}): + version_header = CONFIG["changelog"]["new_header_pattern"].replace( + "$VERSION", new_version + ) + else: + version_header = header_pattern.replace("$VERSION", new_version) + input_files[CHANGELOG_FILE] = changelog + output_files[CHANGELOG_FILE] = ( + changelog.rstrip() + "\n\n" + version_header + "\n\n" ) - old_version = version - for path in VERSION_FILES.keys(): + changes = False + + for path in output_files.keys(): input_lines = input_files[path].splitlines() output_lines = output_files[path].splitlines() if input_lines == output_lines: - print("[*] No changes detected.") - return + continue + changes = True print("=== " + str(path)) for line in difflib.unified_diff(input_lines, output_lines, lineterm=""): print(line) + if not changes: + print("[*] No changes detected.") + return + response = "" while response.lower() not in {"y", "n", "yes", "no"}: response = input("[*] Check above diff ^ Is it correct? (y/n): ") @@ -69,12 +224,71 @@ def main(new_version): with open(path, "w") as f: f.write(content) print("[+] Changes applied!") + if CHANGELOG_FILE in output_files: + print( + "[+] New changelog entry was created. Make sure to fill it before applying version bump." + ) else: print("[-] Changes discarded.") +def parse_args(args: tuple[str, ...]) -> argparse.Namespace: + parser = argparse.ArgumentParser(prog=Path(__file__).name) + subparsers = parser.add_subparsers(dest="command") + + bump = subparsers.add_parser("bump", help="bump the project version") + group = bump.add_mutually_exclusive_group(required=True) + group.add_argument("--major", action="store_true", help="bump the major version") + group.add_argument("--minor", action="store_true", help="bump the minor version") + group.add_argument("--patch", action="store_true", help="bump the patch version") + group.add_argument( + "version", nargs="?", default=None, help="set an explicit version" + ) + + subparsers.add_parser("version", help="print the current version") + + changelog = subparsers.add_parser("changelog", help="print the changelog") + changelog.add_argument( + "version", nargs="?", default=None, help="version to show (default: latest)" + ) + + if not len(args) or args[0] not in [*subparsers.choices, "-h", "--help"]: + args = "bump", *args + return parser.parse_args(args) + + +def main(*argv: str) -> int: + args = parse_args(argv) + + if args.command == "bump": + flag_set = args.major or args.minor or args.patch + if flag_set: + version = find_latest_version() + next_version = increment_version( + version, major=args.major, minor=args.minor, patch=args.patch + ) + else: + next_version = args.version + try: + bump_version(next_version) + except RuntimeError as e: + sys.exit(f"[!] {e!s}") + elif args.command == "version": + try: + print(find_latest_version(), end="") + except RuntimeError as e: + sys.exit(f"[!] {e!s}") + elif args.command == "changelog": + try: + version = find_latest_version() if args.version is None else args.version + changelog = extract_changelog() + except RuntimeError as e: + sys.exit(f"[!] {e!s}") + if version not in changelog: + sys.exit(f"[!] {version} not in changelog") + print(changelog[version], end="") + return 0 + + if __name__ == "__main__": - if not sys.argv[1:]: - print("Usage: bump_version.py [new_version]") - else: - main(sys.argv[1]) + sys.exit(main(*sys.argv[1:])) diff --git a/scripts/bump_version.toml b/scripts/bump_version.toml new file mode 100644 index 00000000..31900c7a --- /dev/null +++ b/scripts/bump_version.toml @@ -0,0 +1,13 @@ +[files] +"drakrun/web/frontend/package.json" = [ + "\"version\": \"$VERSION\"", +] +"drakrun/web/frontend/package-lock.json" = [ + "\"name\": \"@drakvuf-sandbox/web\",\\s+\"version\": \"$VERSION\"", +] +"drakrun/version.py" = [ + "__version__ = \"$VERSION\"", +] +"docs/conf.py" = [ + "release = 'v$VERSION'", +]