From c9c550d0a0727fa05b18fd4d62b0e0c3fd46f8ba Mon Sep 17 00:00:00 2001 From: Marcus Furlong Date: Sat, 11 Apr 2026 15:57:35 -0400 Subject: [PATCH 1/7] auto-enable wal mode for sqlite backend - override get_new_connection to set journal_mode=wal automatically - eliminates need for manual pragma calls in post-install scripts --- patchman/sqlite3/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/patchman/sqlite3/base.py b/patchman/sqlite3/base.py index c7ba0c6f..8e1ea030 100644 --- a/patchman/sqlite3/base.py +++ b/patchman/sqlite3/base.py @@ -20,6 +20,11 @@ class DatabaseWrapper(base.DatabaseWrapper): + def get_new_connection(self, conn_params): + conn = super().get_new_connection(conn_params) + conn.execute('PRAGMA journal_mode=WAL') + return conn + def _start_transaction_under_autocommit(self): # Acquire a write lock immediately for transactions self.cursor().execute('BEGIN IMMEDIATE') From 07e1912b650723cd29770daddee87a9cdec81913 Mon Sep 17 00:00:00 2001 From: Marcus Furlong Date: Tue, 7 Apr 2026 15:28:22 -0400 Subject: [PATCH 2/7] fix duplicate verbose_name_plural in report model meta first assignment was meant to be verbose_name (singular). --- reports/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reports/models.py b/reports/models.py index a0f2bbef..578e2c46 100644 --- a/reports/models.py +++ b/reports/models.py @@ -46,7 +46,7 @@ class Report(models.Model): reboot = models.TextField(null=True, blank=True) class Meta: - verbose_name_plural = 'Report' + verbose_name = 'Report' verbose_name_plural = 'Reports' ordering = ['-created'] From 5d034418222c9841b72155d92d830c5312d974dc Mon Sep 17 00:00:00 2001 From: Marcus Furlong Date: Tue, 7 Apr 2026 15:28:28 -0400 Subject: [PATCH 3/7] sanitize filter_params in bulk action views five bulk action views passed raw POST filter_params into redirects without calling sanitize_filter_params(), unlike the rest of the codebase. --- operatingsystems/views.py | 4 ++-- reports/views.py | 2 +- repos/views.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/operatingsystems/views.py b/operatingsystems/views.py index 1a6b3fda..40824d0d 100644 --- a/operatingsystems/views.py +++ b/operatingsystems/views.py @@ -269,7 +269,7 @@ def osvariant_bulk_action(request): action = request.POST.get('action', '') select_all_filtered = request.POST.get('select_all_filtered') == '1' - filter_params = request.POST.get('filter_params', '') + filter_params = sanitize_filter_params(request.POST.get('filter_params', '')) if not action: messages.warning(request, 'Please select an action') @@ -310,7 +310,7 @@ def osrelease_bulk_action(request): action = request.POST.get('action', '') select_all_filtered = request.POST.get('select_all_filtered') == '1' - filter_params = request.POST.get('filter_params', '') + filter_params = sanitize_filter_params(request.POST.get('filter_params', '')) if not action: messages.warning(request, 'Please select an action') diff --git a/reports/views.py b/reports/views.py index e5d1d438..2aab3e54 100644 --- a/reports/views.py +++ b/reports/views.py @@ -220,7 +220,7 @@ def report_bulk_action(request): action = request.POST.get('action', '') select_all_filtered = request.POST.get('select_all_filtered') == '1' - filter_params = request.POST.get('filter_params', '') + filter_params = sanitize_filter_params(request.POST.get('filter_params', '')) if not action: messages.warning(request, 'Please select an action') diff --git a/repos/views.py b/repos/views.py index 1a796c11..7a804a19 100644 --- a/repos/views.py +++ b/repos/views.py @@ -452,7 +452,7 @@ def repo_bulk_action(request): action = request.POST.get('action', '') select_all_filtered = request.POST.get('select_all_filtered') == '1' - filter_params = request.POST.get('filter_params', '') + filter_params = sanitize_filter_params(request.POST.get('filter_params', '')) if not action: messages.warning(request, 'Please select an action') @@ -531,7 +531,7 @@ def mirror_bulk_action(request): action = request.POST.get('action', '') select_all_filtered = request.POST.get('select_all_filtered') == '1' - filter_params = request.POST.get('filter_params', '') + filter_params = sanitize_filter_params(request.POST.get('filter_params', '')) if not action: messages.warning(request, 'Please select an action') From f87056d3c725b77ed38d50e2139c034fc3acd8ae Mon Sep 17 00:00:00 2001 From: Marcus Furlong Date: Tue, 7 Apr 2026 15:28:17 -0400 Subject: [PATCH 4/7] add null guard for missing references element in updateinfo xml find() returns None when the element doesn't exist, causing AttributeError on the subsequent findall() call. --- errata/sources/repos/yum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/errata/sources/repos/yum.py b/errata/sources/repos/yum.py index 992830c4..72813436 100644 --- a/errata/sources/repos/yum.py +++ b/errata/sources/repos/yum.py @@ -136,6 +136,8 @@ def add_updateinfo_erratum_references(e, update, ref_type, urls): for url in urls: e.add_reference(ref_type, url) references = update.find('references') + if references is None: + return for reference in references.findall('reference'): if reference.attrib.get('type') == 'cve': cve_id = reference.attrib.get('id') From 4bb86e78803f5d814c111a05bf00087dc8ceb39a Mon Sep 17 00:00:00 2001 From: Marcus Furlong Date: Tue, 7 Apr 2026 15:28:13 -0400 Subject: [PATCH 5/7] return early on yaml parse error in extract_module_metadata missing return after except meant code fell through to iterate an unassigned variable, causing UnboundLocalError. --- repos/repo_types/yum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/repos/repo_types/yum.py b/repos/repo_types/yum.py index 1e96db39..24584fe9 100644 --- a/repos/repo_types/yum.py +++ b/repos/repo_types/yum.py @@ -68,6 +68,7 @@ def extract_module_metadata(data, url, repo): modules_yaml = yaml.safe_load_all(extracted) except yaml.YAMLError as e: error_message(text=f'Error parsing modules.yaml: {e}') + return modules mlen = len(re.findall(r'---', yaml.dump(extracted.decode()))) pbar_start.send(sender=None, ptext=f'Extracting {mlen} Modules ', plen=mlen) From 7a817b98e99376da7396066f6b0a2f60279ea900 Mon Sep 17 00:00:00 2001 From: Marcus Furlong Date: Wed, 1 Apr 2026 23:27:55 -0400 Subject: [PATCH 6/7] fix null url handling in osv.dev cve references - skip references with null urls in parse_osv_dev_cve_data - bail early from fixup_reference when urlparse has no hostname --- security/models.py | 4 +++- security/utils.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/security/models.py b/security/models.py index 4a19f9ca..aab22882 100644 --- a/security/models.py +++ b/security/models.py @@ -179,8 +179,10 @@ def parse_osv_dev_cve_data(self, cve_json): references = cve_json.get('references') if references: for reference in references: - ref_type = reference.get('type').capitalize() url = reference.get('url') + if not url: + continue + ref_type = reference.get('type').capitalize() get_or_create_reference(ref_type, url) scores = cve_json.get('severity') if scores: diff --git a/security/utils.py b/security/utils.py index 745cf1f8..4033fc68 100644 --- a/security/utils.py +++ b/security/utils.py @@ -95,15 +95,19 @@ def fixup_reference(ref): """ Fix up a Security Reference object to normalize the URL and type """ url = urlparse(ref.get('url')) + if not url.hostname: + return ref ref_type = ref.get('ref_type') - if 'lists' in url.hostname or 'lists' in url.path: + hostname = url.hostname + if 'lists' in hostname or 'lists' in url.path: ref_type = 'Mailing List' - if ref_type == 'bugzilla' or 'bug' in url.hostname or 'bugs' in url.path: + if ref_type == 'bugzilla' or 'bug' in hostname or 'bugs' in url.path: ref_type = 'Bug Tracker' url = fixup_ubuntu_usn_url(url) - if url.hostname == 'ubuntu.com' and url.path.startswith('/security/notices/USN'): + hostname = url.hostname + if hostname == 'ubuntu.com' and url.path.startswith('/security/notices/USN'): ref_type = 'USN' - if 'launchpad.net' in url.hostname: + if 'launchpad.net' in hostname: ref_type = 'Bug Tracker' netloc = url.netloc.replace('bugs.', '') bug = url.path.split('/')[-1] From 95eb82799a84100a932b84a3941b40ac8f258b7c Mon Sep 17 00:00:00 2001 From: Marcus Furlong Date: Tue, 31 Mar 2026 23:27:34 -0400 Subject: [PATCH 7/7] send info messages to stdout instead of stderr tqdm.write(file=sys.stdout) replaces logger.info() for info messages so that patchman -lh, -lr etc. can be piped through grep and other standard unix tools. warnings and errors remain on stderr. --- patchman/receivers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/patchman/receivers.py b/patchman/receivers.py index 9393d891..99a544c9 100644 --- a/patchman/receivers.py +++ b/patchman/receivers.py @@ -15,9 +15,12 @@ # You should have received a copy of the GNU General Public License # along with Patchman. If not, see +import sys + from colorama import Fore, Style, init from django.conf import settings from django.dispatch import receiver +from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm from patchman.signals import ( @@ -54,9 +57,7 @@ def print_info_message(**kwargs): """ text = str(kwargs.get('text')) if not get_quiet_mode(): - with logging_redirect_tqdm(loggers=[logger]): - for line in text.splitlines(): - logger.info(Style.RESET_ALL + Fore.RESET + line) + tqdm.write(text, file=sys.stdout) @receiver(warning_message_s)