diff --git a/.gitignore b/.gitignore index 3a1397f58..9b30cd2b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,48 @@ -*~ -*.orig -*.rej -tmp* -*.pyc -*.pyo -*~ -#* -.svn -.tox -patchman.egg-info -build -dist -run -pyvenv.cfg -.vscode +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Django +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +media/ +static/ + +# Virtual Environment +.env .venv -*.xml +env/ +venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Project specific +/etc/patchman/local_settings.py +run/ +*.db diff --git a/README.md b/README.md index d425c5fda..689a423f1 100644 --- a/README.md +++ b/README.md @@ -202,3 +202,27 @@ Errata for CentOS can be downloaded from https://cefs.steve-meier.de/ . These errata are parsed and stored in the database. If a PackageUpdate contains a package that is a security update in the errata, then that update is marked as being a security update. + +## Local Settings Configuration + +The project uses a local settings file for configuration. To set up your local environment: + +1. Copy the template file to create your local settings: + ```bash + cp etc/patchman/local_settings.py.template /etc/patchman/local_settings.py + ``` + +2. Edit `/etc/patchman/local_settings.py` with your specific configuration: + - Set a secure `SECRET_KEY` + - Configure your database settings + - Set appropriate `ALLOWED_HOSTS` + - Configure email settings + - Set up Celery and caching if needed + +3. Make sure the settings file has the correct permissions: + ```bash + sudo chown www-data /etc/patchman/local_settings.py + sudo chmod 640 /etc/patchman/local_settings.py + ``` + +Note: The local settings file is not tracked in git to protect sensitive information. Make sure to keep your local settings file secure and never commit it to version control. diff --git a/etc/patchman/local_settings.py b/etc/patchman/local_settings.py.template similarity index 86% rename from etc/patchman/local_settings.py rename to etc/patchman/local_settings.py.template index 33a7d52f1..7903095cf 100644 --- a/etc/patchman/local_settings.py +++ b/etc/patchman/local_settings.py.template @@ -1,16 +1,15 @@ # Django settings for patchman project. -DEBUG = False +DEBUG = True ADMINS = ( - ('Your Name', 'you@example.com'), + ('Admin', 'admin@example.com'), ) DATABASES = { 'default': { -# 'ENGINE': 'django.db.backends.sqlite3', # noqa disabled until django 5.1 is in use, see https://blog.pecar.me/django-sqlite-dblock - 'ENGINE': 'patchman.sqlite3', - 'NAME': '/var/lib/patchman/db/patchman.db', + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'patchman.db', 'OPTIONS': { 'timeout': 30 } @@ -26,11 +25,11 @@ LANGUAGE_CODE = 'en-us' # Create a unique string here, and don't share it with anybody. -SECRET_KEY = '' +SECRET_KEY = 'change-this-in-production' # Add the IP addresses that your web server will be listening on, # instead of '*' -ALLOWED_HOSTS = ['127.0.0.1', '*'] +ALLOWED_HOSTS = ['127.0.0.1'] # Maximum number of mirrors to add or refresh per repo MAX_MIRRORS = 2 @@ -57,6 +56,11 @@ # } # } +# Celery settings +CELERY_BROKER_URL = 'memory://' +CELERY_RESULT_BACKEND = 'cache' +CELERY_CACHE_BACKEND = 'memory' + from datetime import timedelta # noqa from celery.schedules import crontab # noqa CELERY_BEAT_SCHEDULE = { @@ -84,4 +88,4 @@ 'task': 'hosts.tasks.find_all_host_updates_homogenous', 'schedule': timedelta(hours=24), }, -} +} \ No newline at end of file diff --git a/manage.py b/manage.py index 19bd04b46..fe0f0725d 100755 --- a/manage.py +++ b/manage.py @@ -1,34 +1,19 @@ -#!/usr/bin/env python3 - -# Copyright 2019-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, version 3 only. -# -# Patchman is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# 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 - +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" import os import sys def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'patchman.settings') + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'patchman.settings.local') try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( - 'Could not import Django. Are you sure it is installed and ' - 'available on your PYTHONPATH environment variable? Did you ' - 'forget to activate a virtual environment?' + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) diff --git a/patchman/settings.py b/patchman/settings.py index 557e8c687..bb639ec19 100644 --- a/patchman/settings.py +++ b/patchman/settings.py @@ -7,9 +7,51 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-development-key-change-in-production' + +# SECURITY WARNING: don't run with debug turned on in production! DEBUG = True + ALLOWED_HOSTS = ['127.0.0.1'] +# Application definition +DEFAULT_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.humanize', + 'django.contrib.admindocs', + 'django.contrib.sites', +] + +THIRD_PARTY_APPS = [ + 'django_extensions', + 'taggit', + 'bootstrap3', + 'rest_framework', + 'django_filters', +] + +LOCAL_APPS = [ + 'arch.apps.ArchConfig', + 'domains.apps.DomainsConfig', + 'errata.apps.ErrataConfig', + 'hosts.apps.HostsConfig', + 'modules.apps.ModulesConfig', + 'operatingsystems.apps.OperatingsystemsConfig', + 'packages.apps.PackagesConfig', + 'repos.apps.ReposConfig', + 'security.apps.SecurityConfig', + 'reports.apps.ReportsConfig', + 'util.apps.UtilConfig', +] + +INSTALLED_APPS = DEFAULT_APPS + THIRD_PARTY_APPS + LOCAL_APPS + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.middleware.cache.UpdateCacheMiddleware', @@ -23,14 +65,6 @@ 'django.middleware.cache.FetchFromCacheMiddleware', ] -# SECURE_BROWSER_XSS_FILTER = True -# SECURE_CONTENT_TYPE_NOSNIFF = True -# CSRF_COOKIE_SECURE = True -# SESSION_COOKIE_SECURE = True -# X_FRAME_OPTIONS = 'DENY' - -SITE_ID = 1 - ROOT_URLCONF = 'patchman.urls' TEMPLATES = [ @@ -54,115 +88,68 @@ }, ] +# Database +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'patchman.db'), + } +} + # Internationalization LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'America/NewYork' +TIME_ZONE = 'America/New_York' USE_I18N = True USE_L10N = True USE_TZ = True -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +# Static files +STATIC_ROOT = os.path.join(BASE_DIR, 'run/static') +STATIC_URL = '/static/' -DEFAULT_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.humanize', - 'django.contrib.admindocs', - 'django.contrib.sites', -] +# Media files +MEDIA_ROOT = os.path.join(BASE_DIR, 'run/media') +MEDIA_URL = '/media/' -THIRD_PARTY_APPS = [ - 'django_extensions', - 'taggit', - 'bootstrap3', - 'rest_framework', - 'django_filters', - 'celery', - 'django_celery_beat', -] +# Email configuration (console backend for development) +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' -LOCAL_APPS = [ - 'arch.apps.ArchConfig', - 'domains.apps.DomainsConfig', - 'errata.apps.ErrataConfig', - 'hosts.apps.HostsConfig', - 'modules.apps.ModulesConfig', - 'operatingsystems.apps.OperatingsystemsConfig', - 'packages.apps.PackagesConfig', - 'repos.apps.ReposConfig', - 'security.apps.SecurityConfig', - 'reports.apps.ReportsConfig', - 'util.apps.UtilConfig', -] +# Admin email +ADMINS = [('Admin', 'admin@example.com')] +# Site ID +SITE_ID = 1 + +# REST Framework settings REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticatedOrReadOnly'], # noqa - 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], # noqa - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', # noqa + 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticatedOrReadOnly'], + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 100, } +# Taggit settings TAGGIT_CASE_INSENSITIVE = True -CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0' - +# Login URLs LOGIN_REDIRECT_URL = '/patchman/' LOGOUT_REDIRECT_URL = '/patchman/login/' LOGIN_URL = '/patchman/login/' -# URL prefix for static files. -STATIC_URL = '/patchman/static/' - -# Additional dirs where the media should be copied from -STATICFILES_DIRS = [os.path.abspath(os.path.join(BASE_DIR, 'patchman/static'))] - -# Absolute path to the directory static files should be collected to. -STATIC_ROOT = '/var/lib/patchman/static/' - -if sys.prefix == '/usr': - conf_path = '/etc/patchman' -else: - conf_path = os.path.join(sys.prefix, 'etc/patchman') - # if sys.prefix + conf_path doesn't exist, try ./etc/patchman (source) - if not os.path.isdir(conf_path): - conf_path = './etc/patchman' - # if ./etc/patchman doesn't exist, try site.getsitepackages() (pip) - if not os.path.isdir(conf_path): - try: - sitepackages = site.getsitepackages() - except AttributeError: - # virtualenv, try site-packages in sys.path - sp = 'site-packages' - sitepackages = [s for s in sys.path if s.endswith(sp)][0] - conf_path = os.path.join(sitepackages, 'etc/patchman') -local_settings = os.path.join(conf_path, 'local_settings.py') -with open(local_settings, 'r', encoding='utf_8') as ls: - exec(compile(ls.read(), local_settings, 'exec')) +# Default primary key field type +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' -INSTALLED_APPS = DEFAULT_APPS + THIRD_PARTY_APPS + LOCAL_APPS +# Maximum number of mirrors to add or refresh per repo +MAX_MIRRORS = 2 + +# Number of days to wait before raising that a host has not reported +DAYS_WITHOUT_REPORT = 14 -if RUN_GUNICORN or (len(sys.argv) > 1 and sys.argv[1] == 'runserver'): # noqa - LOGIN_REDIRECT_URL = '/' - LOGOUT_REDIRECT_URL = '/login/' - LOGIN_URL = '/login/' - STATICFILES_DIRS = [os.path.abspath(os.path.join(BASE_DIR, 'patchman/static'))] # noqa - STATIC_ROOT = os.path.abspath(os.path.join(BASE_DIR, 'run/static')) - STATIC_URL = '/static/' - MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'django.middleware.cache.UpdateCacheMiddleware', - 'django.middleware.http.ConditionalGetMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.cache.FetchFromCacheMiddleware', - ] - STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' # noqa +# Whether to run patchman under the gunicorn web server +RUN_GUNICORN = False + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + } +} diff --git a/patchman/settings/__init__.py b/patchman/settings/__init__.py new file mode 100644 index 000000000..76b52b31d --- /dev/null +++ b/patchman/settings/__init__.py @@ -0,0 +1 @@ +from .local import * \ No newline at end of file diff --git a/patchman/settings/local.py b/patchman/settings/local.py new file mode 100644 index 000000000..897b5d32f --- /dev/null +++ b/patchman/settings/local.py @@ -0,0 +1,199 @@ +# Django settings for patchman project. + +import os +import site +import sys + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-development-key-change-in-production' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = ['127.0.0.1'] + +# Application definition +DEFAULT_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.humanize', + 'django.contrib.admindocs', + 'django.contrib.sites', +] + +THIRD_PARTY_APPS = [ + 'django_extensions', + 'taggit', + 'bootstrap3', + 'rest_framework', + 'django_filters', + 'celery', + 'django_celery_beat', +] + +LOCAL_APPS = [ + 'arch.apps.ArchConfig', + 'domains.apps.DomainsConfig', + 'errata.apps.ErrataConfig', + 'hosts.apps.HostsConfig', + 'modules.apps.ModulesConfig', + 'operatingsystems.apps.OperatingsystemsConfig', + 'packages.apps.PackagesConfig', + 'repos.apps.ReposConfig', + 'security.apps.SecurityConfig', + 'reports.apps.ReportsConfig', + 'util.apps.UtilConfig', +] + +INSTALLED_APPS = DEFAULT_APPS + THIRD_PARTY_APPS + LOCAL_APPS + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.http.ConditionalGetMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.cache.FetchFromCacheMiddleware', +] + +ROOT_URLCONF = 'patchman.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + ], + 'debug': DEBUG, + }, + }, +] + +# Database +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'patchman.db'), + } +} + +# Internationalization +LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'America/New_York' +USE_I18N = True +USE_L10N = True +USE_TZ = True + +# Static files +STATIC_ROOT = os.path.join(BASE_DIR, 'run/static') +STATIC_URL = '/static/' + +# Media files +MEDIA_ROOT = os.path.join(BASE_DIR, 'run/media') +MEDIA_URL = '/media/' + +# Email configuration (console backend for development) +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + +# Admin email +ADMINS = [('Admin', 'admin@example.com')] + +# Site ID +SITE_ID = 1 + +# REST Framework settings +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticatedOrReadOnly'], + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 100, +} + +# Taggit settings +TAGGIT_CASE_INSENSITIVE = True + +# Celery settings +CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0' + +# Login URLs +LOGIN_REDIRECT_URL = '/patchman/' +LOGOUT_REDIRECT_URL = '/patchman/login/' +LOGIN_URL = '/patchman/login/' + +# Default primary key field type +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' + +# Maximum number of mirrors to add or refresh per repo +MAX_MIRRORS = 2 + +# Number of days to wait before raising that a host has not reported +DAYS_WITHOUT_REPORT = 14 + +# Whether to run patchman under the gunicorn web server +RUN_GUNICORN = False + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + } +} + +# Uncomment to enable redis caching for e.g. 30 seconds +# Note that the UI results may be out of date for this amount of time +# CACHES = { +# 'default': { +# 'BACKEND': 'django.core.cache.backends.redis.RedisCache', +# 'LOCATION': 'redis://127.0.0.1:6379', +# 'TIMEOUT': 30, +# } +# } + +from datetime import timedelta # noqa +from celery.schedules import crontab # noqa +CELERY_BEAT_SCHEDULE = { + 'process_all_unprocessed_reports': { + 'task': 'reports.tasks.process_reports', + 'schedule': crontab(minute='*/5'), + }, + 'refresh_repos_daily': { + 'task': 'repos.tasks.refresh_repos', + 'schedule': crontab(hour=4, minute=00), + }, + 'update_errata_cves_cwes_every_12_hours': { + 'task': 'errata.tasks.update_errata_and_cves', + 'schedule': timedelta(hours=12), + }, + 'run_database_maintenance_daily': { + 'task': 'util.tasks.clean_database', + 'schedule': crontab(hour=6, minute=00), + }, + 'remove_old_reports': { + 'task': 'reports.tasks.remove_reports_with_no_hosts', + 'schedule': timedelta(days=7), + }, + 'find_host_updates': { + 'task': 'hosts.tasks.find_all_host_updates_homogenous', + 'schedule': timedelta(hours=24), + }, +}