diff --git a/debian/copyright b/debian/copyright index ab051037..5202ff0e 100644 --- a/debian/copyright +++ b/debian/copyright @@ -6,7 +6,7 @@ Source: https://github.com/furlongm/patchman Files: * Copyright: 2011-2012 VPAC http://www.vpac.org 2013-2021 Marcus Furlong -License: GPL-3.0 +License: GPL-3.0-only This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 3 only. diff --git a/errata/migrations/0001_initial.py b/errata/migrations/0001_initial.py index 85fe88b4..d02a7dc8 100644 --- a/errata/migrations/0001_initial.py +++ b/errata/migrations/0001_initial.py @@ -19,7 +19,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('er_type', models.CharField(max_length=255)), - ('url', models.URLField(max_length=2000)), + ('url', models.URLField(max_length=765)), ], ), migrations.CreateModel( diff --git a/hosts/templatetags/report_alert.py b/hosts/templatetags/report_alert.py index 3a3e3a9a..a28c5058 100644 --- a/hosts/templatetags/report_alert.py +++ b/hosts/templatetags/report_alert.py @@ -1,11 +1,10 @@ -# Copyright 2016-2021 Marcus Furlong +# Copyright 2016-2025 Marcus Furlong # # This file is part of Patchman. # # Patchman is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# the Free Software Foundation, version 3 only. # # Patchman is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -13,7 +12,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with Patchman If not, see . +# along with Patchman. If not, see from datetime import timedelta diff --git a/modules/utils.py b/modules/utils.py index 817a610c..f56a0f62 100644 --- a/modules/utils.py +++ b/modules/utils.py @@ -23,7 +23,7 @@ def get_or_create_module(name, stream, version, context, arch, repo): """ Get or create a module object - Returns the module and a boolean for created + Returns the module """ created = False m_arch, c = PackageArchitecture.objects.get_or_create(name=arch) @@ -46,7 +46,7 @@ def get_or_create_module(name, stream, version, context, arch, repo): arch=m_arch, repo=repo, ) - return module, created + return module def get_matching_modules(name, stream, version, context, arch): diff --git a/packages/models.py b/packages/models.py index f4c9c59e..74a83c0c 100644 --- a/packages/models.py +++ b/packages/models.py @@ -195,11 +195,11 @@ def __str__(self): rel = f'-{self.release}' else: rel = '' - if self.packagetype == self.GENTOO: + if self.packagetype == Package.GENTOO: return f'{self.category}/{self.name}-{epo}{self.version}{rel}-{self.arch}.{self.get_packagetype_display()}' - elif self.packagetype in [self.DEB, self.ARCH]: + elif self.packagetype in [Package.DEB, Package.ARCH]: return f'{self.name}_{epo}{self.version}{rel}_{self.arch}.{self.get_packagetype_display()}' - elif self.packagetype == self.RPM: + elif self.packagetype == Package.RPM: return f'{self.name}-{epo}{self.version}{rel}-{self.arch}.{self.get_packagetype_display()}' else: return f'{self.name}-{epo}{self.version}{rel}-{self.arch}.{self.get_packagetype_display()}' diff --git a/patchman/apps.py b/patchman/apps.py new file mode 100644 index 00000000..4a9c3f7b --- /dev/null +++ b/patchman/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class PatchmanConfig(AppConfig): + name = 'patchman' diff --git a/patchman/management/__init__.py b/patchman/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patchman/management/commands/__init__.py b/patchman/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/patchman/management/commands/createsuperuser_with_password.py b/patchman/management/commands/createsuperuser_with_password.py new file mode 100644 index 00000000..63d3c721 --- /dev/null +++ b/patchman/management/commands/createsuperuser_with_password.py @@ -0,0 +1,33 @@ +from django.contrib.auth.management.commands import createsuperuser +from django.core.management import CommandError + + +class Command(createsuperuser.Command): + help = 'Crate a superuser, and allow password to be provided' + + def add_arguments(self, parser): + super(Command, self).add_arguments(parser) + parser.add_argument( + '--password', dest='password', default=None, + help='Specifies the password for the superuser.', + ) + + def handle(self, *args, **options): + password = options.get('password') + username = options.get('username') + database = options.get('database') + + if options['interactive']: + raise CommandError( + 'Command is required to run with --no-input option') + if password and not username: + raise CommandError( + '--username is required if specifying --password') + + super(Command, self).handle(*args, **options) + + if password: + user = self.UserModel._default_manager.db_manager( + database).get(username=username) + user.set_password(password) + user.save() diff --git a/patchman/management/commands/set_rdns_check.py b/patchman/management/commands/set_rdns_check.py new file mode 100644 index 00000000..0e5250a7 --- /dev/null +++ b/patchman/management/commands/set_rdns_check.py @@ -0,0 +1,17 @@ +from django.core.management.base import BaseCommand, CommandError +from hosts.models import Host + + +class Command(BaseCommand): + help = 'Enable/Disable rDNS check for hosts' + + def add_arguments(self, parser): + parser.add_argument( + '--disable', action='store_false', default=True, dest='rdns_check', + help='If set, disables rDNS check') + + def handle(self, *args, **options): + try: + Host.objects.all().update(check_dns=options['rdns_check']) + except Exception as e: + raise CommandError('Failed to update rDNS check', str(e)) diff --git a/patchman/management/commands/set_secret_key.py b/patchman/management/commands/set_secret_key.py new file mode 100644 index 00000000..870d1475 --- /dev/null +++ b/patchman/management/commands/set_secret_key.py @@ -0,0 +1,52 @@ +import os +import re +import sys +import codecs +from random import choice +from tempfile import NamedTemporaryFile +from shutil import copy + +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + help = 'Set SECRET_KEY of Patchman Application.' + + def add_arguments(self, parser): + parser.add_argument( + '--key', help=( + 'The SECRET_KEY to be used by Patchman. If not set, a random ' + 'key of length 50 will be created.')) + + @staticmethod + def get_random_key(): + chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' + return ''.join([choice(chars) for i in range(50)]) + + def handle(self, *args, **options): + secret_key = options.get('key', self.get_random_key()) + + if sys.prefix == '/usr': + conf_path = '/etc/patchman' + else: + conf_path = os.path.join(sys.prefix, 'etc/patchman') + # if conf_path doesn't exist, try ./etc/patchman + if not os.path.isdir(conf_path): + conf_path = './etc/patchman' + local_settings = os.path.join(conf_path, 'local_settings.py') + + settings_contents = codecs.open( + local_settings, 'r', encoding='utf-8').read() + settings_contents = re.sub( + r"(?<=SECRET_KEY = ')'", secret_key + "'", settings_contents) + + f = NamedTemporaryFile(delete=False) + temp = f.name + f.close() + + fh = codecs.open(temp, 'w+b', encoding='utf-8') + fh.write(settings_contents) + fh.close() + + copy(temp, local_settings) + os.remove(temp) diff --git a/patchman/management/commands/set_site.py b/patchman/management/commands/set_site.py new file mode 100644 index 00000000..3ad97a1b --- /dev/null +++ b/patchman/management/commands/set_site.py @@ -0,0 +1,23 @@ +from django.core.management.base import BaseCommand, CommandError +from django.contrib.sites.models import Site +from django.conf import settings + + +class Command(BaseCommand): + help = 'Set Patchman Site Name' + + def add_arguments(self, parser): + parser.add_argument( + '-n', '--name', dest='site_name', help='Site name') + parser.add_argument( + '--clear-cache', action='store_true', default=False, + dest='clear_cache', help='Clear Site cache') + + def handle(self, *args, **options): + try: + Site.objects.filter(pk=settings.SITE_ID).update( + name=options['site_name'], domain=options['site_name']) + if options['clear_cache']: + Site.objects.clear_cache() + except Exception as e: + raise CommandError('Failed to update Site name', str(e)) diff --git a/patchman/settings.py b/patchman/settings.py index 557e8c68..e611f3a1 100644 --- a/patchman/settings.py +++ b/patchman/settings.py @@ -97,6 +97,7 @@ 'security.apps.SecurityConfig', 'reports.apps.ReportsConfig', 'util.apps.UtilConfig', + 'patchman.apps.PatchmanConfig', ] REST_FRAMEWORK = { diff --git a/repos/repo_types/yum.py b/repos/repo_types/yum.py index d08c7393..7ac85816 100644 --- a/repos/repo_types/yum.py +++ b/repos/repo_types/yum.py @@ -91,7 +91,7 @@ def extract_module_metadata(data, url, repo): packages.add(package) from modules.utils import get_or_create_module - module, created = get_or_create_module(m_name, m_stream, m_version, m_context, arch, repo) + module = get_or_create_module(m_name, m_stream, m_version, m_context, arch, repo) package_ids = [] for package in packages: diff --git a/requirements.txt b/requirements.txt index dca4fe03..2f264c9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,11 @@ -Django==4.2.20 +Django==4.2.25 django-taggit==4.0.0 django-extensions==3.2.3 django-bootstrap3==23.1 python-debian==1.0.1 defusedxml==0.7.1 PyYAML==6.0.2 -chardet==5.2.0 -requests==2.32.3 +requests==2.32.4 colorama==0.4.6 djangorestframework==3.15.2 django-filter==25.1 @@ -16,7 +15,7 @@ python-magic==0.4.27 gitpython==3.1.44 tenacity==8.2.3 celery==5.4.0 -redis==5.2.1 +redis==6.4.0 django-celery-beat==2.7.0 tqdm==4.67.1 cvss==3.4 diff --git a/security/migrations/0001_initial.py b/security/migrations/0001_initial.py index 5655f8b0..5f922c9a 100644 --- a/security/migrations/0001_initial.py +++ b/security/migrations/0001_initial.py @@ -27,7 +27,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('cwe_id', models.CharField(max_length=255, unique=True)), ('name', models.CharField(blank=True, max_length=255, null=True)), - ('description', models.CharField(blank=True, max_length=65535, null=True)), + ('description', models.CharField(blank=True, max_length=255, null=True)), ], ), migrations.CreateModel( @@ -36,7 +36,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('cve_id', models.CharField(max_length=255, unique=True)), ('title', models.CharField(blank=True, max_length=255, null=True)), - ('description', models.CharField(max_length=65535)), + ('description', models.CharField(max_length=255)), ('reserved_date', models.DateTimeField(blank=True, null=True)), ('published_date', models.DateTimeField(blank=True, null=True)), ('rejected_date', models.DateTimeField(blank=True, null=True)), diff --git a/security/migrations/0005_reference_cve_references.py b/security/migrations/0005_reference_cve_references.py index 97251add..f94cf7d5 100644 --- a/security/migrations/0005_reference_cve_references.py +++ b/security/migrations/0005_reference_cve_references.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('ref_type', models.CharField(max_length=255)), - ('url', models.URLField(max_length=2000)), + ('url', models.URLField(max_length=765)), ], options={ 'unique_together': {('ref_type', 'url')}, diff --git a/security/models.py b/security/models.py index a847ab02..9c097eed 100644 --- a/security/models.py +++ b/security/models.py @@ -29,7 +29,7 @@ class Reference(models.Model): ref_type = models.CharField(max_length=255) - url = models.URLField(max_length=2000) + url = models.URLField(max_length=765) class Meta: unique_together = ['ref_type', 'url'] @@ -125,19 +125,20 @@ def add_cvss_score(self, vector_string, score=None, severity=None, version=None) score = cvss_score.base_score if not severity: severity = cvss_score.severities()[0] - existing = self.cvss_scores.filter(version=version, vector_string=vector_string) - if existing: - cvss = existing.first() - else: + try: cvss, created = CVSS.objects.get_or_create( version=version, vector_string=vector_string, score=score, severity=severity, ) - cvss.score = score - cvss.severity = severity - cvss.save() + except CVSS.MultipleObjectsReturned: + matching_cvsses = CVSS.objects.filter( + version=version, + vector_string=vector_string, + ) + cvss = matching_cvsses.first() + matching_cvsses.exclude(id=cvss.id).delete() self.cvss_scores.add(cvss) def fetch_cve_data(self, fetch_nist_data=False, sleep_secs=6): diff --git a/setup.py b/setup.py index d8249a67..6ec6d974 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2013-2021 Marcus Furlong +# Copyright 2013-2025 Marcus Furlong # # This file is part of Patchman. # diff --git a/util/__init__.py b/util/__init__.py index a56ed3b6..c62112b9 100644 --- a/util/__init__.py +++ b/util/__init__.py @@ -20,6 +20,7 @@ import magic import zlib import lzma +import os from datetime import datetime, timezone from enum import Enum from hashlib import md5, sha1, sha256, sha512 @@ -28,17 +29,23 @@ from time import time from tqdm import tqdm -from patchman.signals import error_message, info_message, debug_message - from django.utils.timezone import make_aware from django.utils.dateparse import parse_datetime from django.conf import settings +from patchman.signals import error_message, info_message, debug_message pbar = None verbose = None Checksum = Enum('Checksum', 'md5 sha sha1 sha256 sha512') +http_proxy = os.getenv('http_proxy') +https_proxy = os.getenv('https_proxy') +proxies = { + 'http': http_proxy, + 'https': https_proxy, +} + def get_verbosity(): """ Get the global verbosity level @@ -113,7 +120,7 @@ def get_url(url, headers={}, params={}): response = None try: debug_message.send(sender=None, text=f'Trying {url} headers:{headers} params:{params}') - response = requests.get(url, headers=headers, params=params, stream=True, timeout=30) + response = requests.get(url, headers=headers, params=params, stream=True, proxies=proxies, timeout=30) debug_message.send(sender=None, text=f'{response.status_code}: {response.headers}') if response.status_code in [403, 404]: return response diff --git a/util/filterspecs.py b/util/filterspecs.py index 1c845ff3..722b45df 100644 --- a/util/filterspecs.py +++ b/util/filterspecs.py @@ -1,12 +1,11 @@ # Copyright 2010 VPAC -# Copyright 2014-2021 Marcus Furlong +# Copyright 2014-2025 Marcus Furlong # # This file is part of Patchman. # # Patchman is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# the Free Software Foundation, version 3 only. # # Patchman is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -14,7 +13,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with Patchman If not, see . +# along with Patchman. If not, see from django.utils.safestring import mark_safe from django.db.models.query import QuerySet diff --git a/util/templatetags/common.py b/util/templatetags/common.py index 6737c438..2aea1e5e 100644 --- a/util/templatetags/common.py +++ b/util/templatetags/common.py @@ -1,12 +1,10 @@ -# Copyright 2010 VPAC -# Copyright 2013-2021 Marcus Furlong +# Copyright 2013-2025 Marcus Furlong # # This file is part of Patchman. # # Patchman is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# the Free Software Foundation, version 3 only. # # Patchman is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of @@ -14,7 +12,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with Patchman If not, see . +# along with Patchman. If not, see import re