diff --git a/CHANGELOG.md b/CHANGELOG.md index e722463..6d5ec4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,19 @@ # VINCE Changelog +CHANGELOG +VINCE Coordination platform code + +Version 3.0.0 2024-04-10 + +* Made the Vendor Association button to track and populate ticket id & (if appropriate) vendor name. +* Upgraded `Django` 4.2 - Django 3 is end-of-life +* Restructured code for preparing vendors table data on VINCE Track case page so as to reduce load time +* Refactored certain queries for the VINCE Track reports page in support of the long term goal of reducing its load time + + Version 2.1.11 2024-03-14 -* Dependabot update recommendations: `cryptography` 41.0.6 to 42.0.4 and `django` from 3.2.23 to 3.2.24 +* Dependabot update recommendations: `cryptography` 41.0.6 to 42.0.4 and `django` from 3.2.23 to 3.2.24 * Added code to ensure comments entered into comment box will be preserved when user uploads a file * Fixed filters above vendor table in vendor tab of case page to ensure consistency with data in vendor table * Added logging to make it easier to track user deactivation & MFA resetting processes diff --git a/bakery/__init__.py b/bakery/__init__.py index 0b9de91..2b7a685 100644 --- a/bakery/__init__.py +++ b/bakery/__init__.py @@ -1,4 +1,8 @@ -default_app_config = 'bakery.apps.BakeryConfig' +import django + +if django.VERSION < (3, 2): + default_app_config = "bakery.apps.BakeryConfig" + DEFAULT_GZIP_CONTENT_TYPES = ( "application/atom+xml", "application/javascript", @@ -31,5 +35,5 @@ "text/vtt", "text/x-component", "text/x-cross-domain-policy", - "text/xml" + "text/xml", ) diff --git a/bakery/management/commands/build.py b/bakery/management/commands/build.py index a988bfc..4a3bc1a 100644 --- a/bakery/management/commands/build.py +++ b/bakery/management/commands/build.py @@ -15,7 +15,7 @@ # Filesystem from fs import path from fs import copy -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str as smart_text # Pooling import multiprocessing @@ -25,6 +25,7 @@ from django.apps import apps from django.conf import settings from django.core import management + try: from django.core.urlresolvers import get_callable except ImportError: @@ -34,57 +35,54 @@ # Logging import logging + logger = logging.getLogger(__name__) class Command(BaseCommand): - help = 'Bake out a site as flat files in the build directory' + help = "Bake out a site as flat files in the build directory" build_unconfig_msg = "Build directory unconfigured. Set BUILD_DIR in settings.py or provide it with --build-dir" views_unconfig_msg = "Bakery views unconfigured. Set BAKERY_VIEWS in settings.py or provide a list as arguments." # regex to match against for gzipping. CSS, JS, JSON, HTML, etc. - gzip_file_match = getattr( - settings, - 'GZIP_CONTENT_TYPES', - DEFAULT_GZIP_CONTENT_TYPES - ) + gzip_file_match = getattr(settings, "GZIP_CONTENT_TYPES", DEFAULT_GZIP_CONTENT_TYPES) def add_arguments(self, parser): - parser.add_argument('view_list', nargs='*', type=str, default=[]) + parser.add_argument("view_list", nargs="*", type=str, default=[]) parser.add_argument( "--build-dir", action="store", dest="build_dir", - default='', + default="", help="Specify the path of the build directory. \ -Will use settings.BUILD_DIR by default." +Will use settings.BUILD_DIR by default.", ) parser.add_argument( "--keep-build-dir", action="store_true", dest="keep_build_dir", default=False, - help="Skip initializing the build directory before building files." + help="Skip initializing the build directory before building files.", ) parser.add_argument( "--skip-static", action="store_true", dest="skip_static", default=False, - help="Skip collecting the static files when building." + help="Skip collecting the static files when building.", ) parser.add_argument( "--skip-media", action="store_true", dest="skip_media", default=False, - help="Skip collecting the media files when building." + help="Skip collecting the media files when building.", ) parser.add_argument( "--pooling", action="store_true", dest="pooling", default=False, - help=("Pool builds to run concurrently rather than running them one by one.") + help=("Pool builds to run concurrently rather than running them one by one."), ) def handle(self, *args, **options): @@ -118,14 +116,14 @@ def set_options(self, *args, **options): """ Configure a few global options before things get going. """ - self.verbosity = int(options.get('verbosity', 1)) + self.verbosity = int(options.get("verbosity", 1)) # Figure out what build directory to use if options.get("build_dir"): self.build_dir = options.get("build_dir") settings.BUILD_DIR = self.build_dir else: - if not hasattr(settings, 'BUILD_DIR'): + if not hasattr(settings, "BUILD_DIR"): raise CommandError(self.build_unconfig_msg) self.build_dir = settings.BUILD_DIR @@ -144,15 +142,15 @@ def set_options(self, *args, **options): self.fs.makedirs(self.build_dir) # Figure out what views we'll be using - if options.get('view_list'): - self.view_list = options['view_list'] + if options.get("view_list"): + self.view_list = options["view_list"] else: - if not hasattr(settings, 'BAKERY_VIEWS'): + if not hasattr(settings, "BAKERY_VIEWS"): raise CommandError(self.views_unconfig_msg) self.view_list = settings.BAKERY_VIEWS # Are we pooling? - self.pooling = options.get('pooling') + self.pooling = options.get("pooling") def init_build_dir(self): """ @@ -174,20 +172,13 @@ def build_static(self, *args, **options): logger.debug("Building static directory") if self.verbosity > 1: self.stdout.write("Building static directory") - management.call_command( - "collectstatic", - interactive=False, - verbosity=0 - ) + management.call_command("collectstatic", interactive=False, verbosity=0) # Set the target directory inside the filesystem. - target_dir = path.join( - self.build_dir, - settings.STATIC_URL.lstrip('/') - ) + target_dir = path.join(self.build_dir, settings.STATIC_URL.lstrip("/")) target_dir = smart_text(target_dir) if os.path.exists(self.static_root) and settings.STATIC_URL: - if getattr(settings, 'BAKERY_GZIP', False): + if getattr(settings, "BAKERY_GZIP", False): self.copytree_and_gzip(self.static_root, target_dir) # if gzip isn't enabled, just copy the tree straight over else: @@ -197,15 +188,15 @@ def build_static(self, *args, **options): # If they exist in the static directory, copy the robots.txt # and favicon.ico files down to the root so they will work # on the live website. - robots_src = path.join(target_dir, 'robots.txt') + robots_src = path.join(target_dir, "robots.txt") if self.fs.exists(robots_src): - robots_target = path.join(self.build_dir, 'robots.txt') + robots_target = path.join(self.build_dir, "robots.txt") logger.debug("Copying {}{} to {}{}".format(self.fs_name, robots_src, self.fs_name, robots_target)) self.fs.copy(robots_src, robots_target) - favicon_src = path.join(target_dir, 'favicon.ico') + favicon_src = path.join(target_dir, "favicon.ico") if self.fs.exists(favicon_src): - favicon_target = path.join(self.build_dir, 'favicon.ico') + favicon_target = path.join(self.build_dir, "favicon.ico") logger.debug("Copying {}{} to {}{}".format(self.fs_name, favicon_src, self.fs_name, favicon_target)) self.fs.copy(favicon_src, favicon_target) @@ -217,10 +208,9 @@ def build_media(self): if self.verbosity > 1: self.stdout.write("Building media directory") if os.path.exists(self.media_root) and settings.MEDIA_URL: - target_dir = path.join(self.build_dir, settings.MEDIA_URL.lstrip('/')) + target_dir = path.join(self.build_dir, settings.MEDIA_URL.lstrip("/")) logger.debug("Copying {}{} to {}{}".format("osfs://", self.media_root, self.fs_name, target_dir)) copy.copy_dir("osfs:///", smart_text(self.media_root), self.fs, smart_text(target_dir)) - def get_view_instance(self, view): """ @@ -249,7 +239,7 @@ def copytree_and_gzip(self, source_dir, target_dir): # Figure out what we're building... build_list = [] # Walk through the source directory... - for (dirpath, dirnames, filenames) in os.walk(source_dir): + for dirpath, dirnames, filenames in os.walk(source_dir): for f in filenames: # Figure out what is going where source_path = os.path.join(dirpath, f) @@ -261,7 +251,7 @@ def copytree_and_gzip(self, source_dir, target_dir): logger.debug("Gzipping {} files".format(len(build_list))) # Build em all - if not getattr(self, 'pooling', False): + if not getattr(self, "pooling", False): [self.copyfile_and_gzip(*u) for u in build_list] else: cpu_count = multiprocessing.cpu_count() @@ -299,48 +289,37 @@ def copyfile_and_gzip(self, source_path, target_path): # If it isn't a file want to gzip... if content_type not in self.gzip_file_match: # just copy it to the target. - logger.debug("Copying {}{} to {}{} because its filetype isn't on the whitelist".format( - "osfs://", - source_path, - self.fs_name, - target_path - )) + logger.debug( + "Copying {}{} to {}{} because its filetype isn't on the whitelist".format( + "osfs://", source_path, self.fs_name, target_path + ) + ) copy.copy_file("osfs:///", smart_text(source_path), self.fs, smart_text(target_path)) # # if the file is already gzipped - elif encoding == 'gzip': - logger.debug("Copying {}{} to {}{} because it's already gzipped".format( - "osfs://", - source_path, - self.fs_name, - target_path - )) + elif encoding == "gzip": + logger.debug( + "Copying {}{} to {}{} because it's already gzipped".format( + "osfs://", source_path, self.fs_name, target_path + ) + ) copy.copy_file("osfs:///", smart_text(source_path), self.fs, smart_text(target_path)) # If it is one we want to gzip... else: # ... let the world know ... - logger.debug("Gzipping {}{} to {}{}".format( - "osfs://", - source_path, - self.fs_name, - target_path - )) + logger.debug("Gzipping {}{} to {}{}".format("osfs://", source_path, self.fs_name, target_path)) # Open up the source file from the OS - with open(source_path, 'rb') as source_file: + with open(source_path, "rb") as source_file: # Write GZIP data to an in-memory buffer data_buffer = six.BytesIO() - kwargs = dict( - filename=path.basename(target_path), - mode='wb', - fileobj=data_buffer - ) + kwargs = dict(filename=path.basename(target_path), mode="wb", fileobj=data_buffer) if float(sys.version[:3]) >= 2.7: - kwargs['mtime'] = 0 + kwargs["mtime"] = 0 with gzip.GzipFile(**kwargs) as f: f.write(six.binary_type(source_file.read())) # Write that buffer out to the filesystem - with self.fs.open(smart_text(target_path), 'wb') as outfile: + with self.fs.open(smart_text(target_path), "wb") as outfile: outfile.write(data_buffer.getvalue()) outfile.close() diff --git a/bakery/management/commands/publish.py b/bakery/management/commands/publish.py index f32e08e..0d623c7 100644 --- a/bakery/management/commands/publish.py +++ b/bakery/management/commands/publish.py @@ -7,19 +7,15 @@ from django.conf import settings from multiprocessing.pool import ThreadPool from bakery import DEFAULT_GZIP_CONTENT_TYPES -from bakery.management.commands import ( - BasePublishCommand, - get_s3_client, - get_bucket_page -) +from bakery.management.commands import BasePublishCommand, get_s3_client, get_bucket_page -# Filesystem +# Filesystem import fs from fs import path from fs import copy from fs_s3fs import S3FS from fs.copy import copy_file -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str as smart_text from django.apps import apps @@ -32,16 +28,19 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) + class Command(BasePublishCommand): help = "Syncs the build directory with Amazon s3 bucket" # Default permissions for the files published to s3 - DEFAULT_ACL = 'public-read' + DEFAULT_ACL = "public-read" # Error messages we might use below build_missing_msg = "Build directory does not exist. Cannot publish something before you build it." build_unconfig_msg = "Build directory unconfigured. Set BUILD_DIR in settings.py or provide it with --build-dir" - bucket_unconfig_msg = "Bucket unconfigured. Set AWS_BUCKET_NAME in settings.py or provide it with --aws-bucket-name" + bucket_unconfig_msg = ( + "Bucket unconfigured. Set AWS_BUCKET_NAME in settings.py or provide it with --aws-bucket-name" + ) views_unconfig_msg = "Bakery views unconfigured. Set BAKERY_VIEWS in settings.py or provide a list as arguments." def add_arguments(self, parser): @@ -49,57 +48,57 @@ def add_arguments(self, parser): "--build-dir", action="store", dest="build_dir", - default='', - help="Specify the path of the build directory. Will use settings.BUILD_DIR by default." + default="", + help="Specify the path of the build directory. Will use settings.BUILD_DIR by default.", ) parser.add_argument( "--aws-bucket-name", action="store", dest="aws_bucket_name", - default='', - help="Specify the AWS bucket to sync with. Will use settings.AWS_BUCKET_NAME by default." + default="", + help="Specify the AWS bucket to sync with. Will use settings.AWS_BUCKET_NAME by default.", ) parser.add_argument( "--aws-bucket-prefix", action="store", dest="aws_bucket_prefix", - default='', - help="Specify a prefix for the AWS bucket keys to sync with. None by default." + default="", + help="Specify a prefix for the AWS bucket keys to sync with. None by default.", ) parser.add_argument( "--force", action="store_true", dest="force", default="", - help="Force a republish of all items in the build directory" + help="Force a republish of all items in the build directory", ) parser.add_argument( "--dry-run", action="store_true", dest="dry_run", default="", - help="Display the output of what would have been uploaded removed, but without actually publishing." + help="Display the output of what would have been uploaded removed, but without actually publishing.", ) parser.add_argument( "--no-delete", action="store_true", dest="no_delete", default=False, - help=("Keep files in S3, even if they do not exist in the build directory.") + help=("Keep files in S3, even if they do not exist in the build directory."), ) parser.add_argument( "--no-pooling", action="store_true", dest="no_pooling", default=False, - help=("Run uploads one by one rather than pooling them to run concurrently.") + help=("Run uploads one by one rather than pooling them to run concurrently."), ) parser.add_argument( "--s3fs", action="store_true", dest="s3fs", default=False, - help=("Use s3fs to do copy, which is required for certain filesystems (like MemoryFS)") + help=("Use s3fs to do copy, which is required for certain filesystems (like MemoryFS)"), ) def handle(self, *args, **options): @@ -162,25 +161,22 @@ def handle(self, *args, **options): logger.debug("Deleting %s keys" % self.deleted_files) if self.verbosity > 0: self.stdout.write("Deleting %s keys" % self.deleted_files) - self.batch_delete_s3_objects( - self.deleted_file_list, - self.aws_bucket_name - ) + self.batch_delete_s3_objects(self.deleted_file_list, self.aws_bucket_name) # Run any post publish hooks on the views - if not hasattr(settings, 'BAKERY_VIEWS'): + if not hasattr(settings, "BAKERY_VIEWS"): raise CommandError(self.views_unconfig_msg) for view_str in settings.BAKERY_VIEWS: view = get_callable(view_str)() - if hasattr(view, 'post_publish'): - getattr(view, 'post_publish')(self.bucket) + if hasattr(view, "post_publish"): + getattr(view, "post_publish")(self.bucket) # We're finished, print the final output elapsed_time = time.time() - self.start_time msg = "Publish completed, %d uploaded and %d deleted files in %.2f seconds" % ( self.uploaded_files, self.deleted_files, - elapsed_time + elapsed_time, ) logger.info(msg) if self.verbosity > 0: @@ -195,59 +191,55 @@ def set_options(self, options): """ Configure all the many options we'll need to make this happen. """ - self.verbosity = int(options.get('verbosity')) + self.verbosity = int(options.get("verbosity")) # Will we be gzipping? - self.gzip = getattr(settings, 'BAKERY_GZIP', False) + self.gzip = getattr(settings, "BAKERY_GZIP", False) # And if so what content types will we be gzipping? - self.gzip_content_types = getattr( - settings, - 'GZIP_CONTENT_TYPES', - DEFAULT_GZIP_CONTENT_TYPES - ) + self.gzip_content_types = getattr(settings, "GZIP_CONTENT_TYPES", DEFAULT_GZIP_CONTENT_TYPES) # What ACL (i.e. security permissions) will be giving the files on S3? - self.acl = getattr(settings, 'DEFAULT_ACL', self.DEFAULT_ACL) + self.acl = getattr(settings, "DEFAULT_ACL", self.DEFAULT_ACL) # Should we set cache-control headers? - self.cache_control = getattr(settings, 'BAKERY_CACHE_CONTROL', {}) + self.cache_control = getattr(settings, "BAKERY_CACHE_CONTROL", {}) # If the user specifies a build directory... - if options.get('build_dir'): + if options.get("build_dir"): # ... validate that it is good. - #if not os.path.exists(options.get('build_dir')): + # if not os.path.exists(options.get('build_dir')): # raise CommandError(self.build_missing_msg) # Go ahead and use it self.build_dir = options.get("build_dir") # If the user does not specify a build dir... else: # Check if it is set in settings.py - if not hasattr(settings, 'BUILD_DIR'): + if not hasattr(settings, "BUILD_DIR"): raise CommandError(self.build_unconfig_msg) # Then make sure it actually exists - #if not os.path.exists(settings.BUILD_DIR): + # if not os.path.exists(settings.BUILD_DIR): # raise CommandError(self.build_missing_msg) # Go ahead and use it self.build_dir = settings.BUILD_DIR self.build_dir = smart_text(self.build_dir) - # Connect the BUILD_DIR with our filesystem backend + # Connect the BUILD_DIR with our filesystem backend self.app = apps.get_app_config("bakery") self.fs = self.app.filesystem self.fs_name = self.app.filesystem_name - # If the build dir doesn't exist make it + # If the build dir doesn't exist make it if not self.fs.exists(self.build_dir): raise CommandError(self.build_missing_msg) - + # If the user provides a bucket name, use that. if options.get("aws_bucket_name"): self.aws_bucket_name = options.get("aws_bucket_name") else: # Otherwise try to find it the settings - if not hasattr(settings, 'AWS_BUCKET_NAME'): + if not hasattr(settings, "AWS_BUCKET_NAME"): raise CommandError(self.bucket_unconfig_msg) self.aws_bucket_name = settings.AWS_BUCKET_NAME @@ -255,22 +247,22 @@ def set_options(self, options): self.aws_bucket_prefix = options.get("aws_bucket_prefix") # If the user sets the --force option - if options.get('force'): + if options.get("force"): self.force_publish = True else: self.force_publish = False # set the --dry-run option - if options.get('dry_run'): + if options.get("dry_run"): self.dry_run = True if self.verbosity > 0: logger.info("Executing with the --dry-run option set.") else: self.dry_run = False - self.no_delete = options.get('no_delete') - self.no_pooling = options.get('no_pooling') - self.s3fs = options.get('s3fs') + self.no_delete = options.get("no_delete") + self.no_pooling = options.get("no_pooling") + self.s3fs = options.get("s3fs") def get_bucket_file_list(self): """ @@ -279,13 +271,11 @@ def get_bucket_file_list(self): """ logger.debug("Retrieving bucket object list") - paginator = self.s3_client.get_paginator('list_objects') - options = { - 'Bucket': self.aws_bucket_name - } + paginator = self.s3_client.get_paginator("list_objects") + options = {"Bucket": self.aws_bucket_name} if self.aws_bucket_prefix: logger.debug("Adding prefix {} to bucket list as a filter".format(self.aws_bucket_prefix)) - options['Prefix'] = self.aws_bucket_prefix + options["Prefix"] = self.aws_bucket_prefix page_iterator = paginator.paginate(**options) obj_dict = {} @@ -300,13 +290,10 @@ def get_local_file_list(self): absolute paths to files. """ file_list = [] - for (dirpath, dirnames, filenames) in self.fs.walk(self.build_dir): + for dirpath, dirnames, filenames in self.fs.walk(self.build_dir): for fname in filenames: - - local_key = path.combine( - path.frombase(path.abspath(self.build_dir), dirpath), - fname.name - ) + + local_key = path.combine(path.frombase(path.abspath(self.build_dir), dirpath), fname.name) local_key = path.relpath(local_key) file_list.append(smart_text(local_key)) return file_list @@ -320,10 +307,11 @@ def sync_with_s3(self): self.update_list = [] # Figure out which files need to be updated and upload all these files - logger.debug("Comparing {} local files with {} bucket files".format( - len(self.local_file_list), - len(self.s3_obj_dict.keys()) - )) + logger.debug( + "Comparing {} local files with {} bucket files".format( + len(self.local_file_list), len(self.s3_obj_dict.keys()) + ) + ) if self.no_pooling: [self.compare_local_file(f) for f in self.local_file_list] else: @@ -344,7 +332,7 @@ def get_md5(self, filename): """ Returns the md5 checksum of the provided file name. """ - with self.fs.open(filename, 'rb') as f: + with self.fs.open(filename, "rb") as f: m = hashlib.md5(f.read()) return m.hexdigest() @@ -356,7 +344,7 @@ def get_multipart_md5(self, filename, chunk_size=8 * 1024 * 1024): """ # Loop through the file contents ... md5s = [] - with self.fs.open(filename, 'rb') as fp: + with self.fs.open(filename, "rb") as fp: while True: # Break it into chunks data = fp.read(chunk_size) @@ -386,7 +374,7 @@ def compare_local_file(self, file_key): """ # Where is the file? file_path = path.combine(self.build_dir, file_key) - #file_path = file_key + # file_path = file_key # If we're in force_publish mode just add it if self.force_publish: self.update_list.append((file_key, file_path)) @@ -397,7 +385,7 @@ def compare_local_file(self, file_key): if file_key in self.s3_obj_dict: # Get the md5 stored in Amazon's header - s3_md5 = self.s3_obj_dict[file_key].get('ETag').strip('"').strip("'") + s3_md5 = self.s3_obj_dict[file_key].get("ETag").strip('"').strip("'") # If there is a multipart ETag on S3, compare that to our local file after its chunked up. # We are presuming this file was uploaded in multiple parts. @@ -437,25 +425,22 @@ def upload_to_s3(self, key, filename): Set the content type and gzip headers if applicable and upload the item to S3 """ - extra_args = {'ACL': self.acl} + extra_args = {"ACL": self.acl} # determine the mimetype of the file guess = mimetypes.guess_type(filename) content_type = guess[0] encoding = guess[1] if content_type: - extra_args['ContentType'] = content_type + extra_args["ContentType"] = content_type # add the gzip headers, if necessary - if (self.gzip and content_type in self.gzip_content_types) or encoding == 'gzip': - extra_args['ContentEncoding'] = 'gzip' + if (self.gzip and content_type in self.gzip_content_types) or encoding == "gzip": + extra_args["ContentEncoding"] = "gzip" # add the cache-control headers if necessary if content_type in self.cache_control: - extra_args['CacheControl'] = ''.join(( - 'max-age=', - str(self.cache_control[content_type]) - )) + extra_args["CacheControl"] = "".join(("max-age=", str(self.cache_control[content_type]))) # access and write the contents from the file if not self.dry_run: @@ -467,7 +452,7 @@ def upload_to_s3(self, key, filename): try: copy_file(self.fs, filename, s3fs, key) except fs.errors.ResourceNotFound as e: - #s3fs won't make directories if it doesn't exist, so have to do it explicitly + # s3fs won't make directories if it doesn't exist, so have to do it explicitly s3fs.makedirs(path.dirname(key)) copy_file(self.fs, filename, s3fs, key) else: diff --git a/bakery/static_urls.py b/bakery/static_urls.py index 0841393..f7ee711 100644 --- a/bakery/static_urls.py +++ b/bakery/static_urls.py @@ -1,12 +1,8 @@ from django.conf import settings -from django.conf.urls import url +from django.urls import include, re_path from bakery.static_views import serve urlpatterns = [ - url(r"^(.*)$", serve, { - "document_root": settings.BUILD_DIR, - 'show_indexes': True, - 'default': 'index.html' - }), + re_path(r"^(.*)$", serve, {"document_root": settings.BUILD_DIR, "show_indexes": True, "default": "index.html"}), ] diff --git a/bakery/static_views.py b/bakery/static_views.py index 8b8cf20..43a5be4 100644 --- a/bakery/static_views.py +++ b/bakery/static_views.py @@ -2,6 +2,7 @@ Views and functions for serving static files. These are only to be used during development, and SHOULD NOT be used in a production setting. """ + import django import mimetypes import os @@ -15,9 +16,10 @@ from django.template import Template, Context, TemplateDoesNotExist from django.utils.http import http_date, parse_http_date from django.conf import settings -from django.utils.http import is_same_domain, is_safe_url +from django.utils.http import is_same_domain, url_has_allowed_host_and_scheme as is_safe_url + -def serve(request, path, document_root=None, show_indexes=False, default=''): +def serve(request, path, document_root=None, show_indexes=False, default=""): """ Serve static files below a given point in the directory structure. @@ -40,9 +42,9 @@ def serve(request, path, document_root=None, show_indexes=False, default=''): # Clean up given path to only allow serving files below document_root. path = posixpath.normpath(unquote(path)) - path = path.lstrip('/') - newpath = '' - for part in path.split('/'): + path = path.lstrip("/") + newpath = "" + for part in path.split("/"): if not part: # Strip empty path components. continue @@ -51,9 +53,9 @@ def serve(request, path, document_root=None, show_indexes=False, default=''): if part in (os.curdir, os.pardir): # Strip '.' and '..' in path. continue - newpath = os.path.join(newpath, part).replace('\\', '/') + newpath = os.path.join(newpath, part).replace("\\", "/") if newpath and path != newpath: - if is_safe_url(newpath,set(settings.ALLOWED_HOSTS),True): + if is_safe_url(newpath, set(settings.ALLOWED_HOSTS), True): return HttpResponseRedirect(newpath) else: raise Http404("Invalid or Incorrect path found") @@ -70,14 +72,15 @@ def serve(request, path, document_root=None, show_indexes=False, default=''): raise Http404('"%s" does not exist' % fullpath) # Respect the If-Modified-Since header. statobj = os.stat(fullpath) - mimetype = mimetypes.guess_type(fullpath)[0] or 'application/octet-stream' - if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), - statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]): + mimetype = mimetypes.guess_type(fullpath)[0] or "application/octet-stream" + if not was_modified_since( + request.META.get("HTTP_IF_MODIFIED_SINCE"), statobj[stat.ST_MTIME], statobj[stat.ST_SIZE] + ): if django.VERSION > (1, 6): return HttpResponseNotModified(content_type=mimetype) else: return HttpResponseNotModified(mimetype=mimetype) - contents = open(fullpath, 'rb').read() + contents = open(fullpath, "rb").read() if django.VERSION > (1, 6): response = HttpResponse(contents, content_type=mimetype) else: @@ -114,25 +117,21 @@ def serve(request, path, document_root=None, show_indexes=False, default=''): def directory_index(path, fullpath): try: - t = loader.select_template([ - 'static/directory_index.html', - 'static/directory_index' - ]) + t = loader.select_template(["static/directory_index.html", "static/directory_index"]) except TemplateDoesNotExist: - t = Template( - DEFAULT_DIRECTORY_INDEX_TEMPLATE, - name='Default directory index template' - ) + t = Template(DEFAULT_DIRECTORY_INDEX_TEMPLATE, name="Default directory index template") files = [] for f in os.listdir(fullpath): - if not f.startswith('.'): + if not f.startswith("."): if os.path.isdir(os.path.join(fullpath, f)): - f += '/' + f += "/" files.append(f) - c = Context({ - 'directory': path + '/', - 'file_list': files, - }) + c = Context( + { + "directory": path + "/", + "file_list": files, + } + ) return HttpResponse(t.render(c)) @@ -150,8 +149,7 @@ def was_modified_since(header=None, mtime=0, size=0): try: if header is None: raise ValueError - matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header, - re.IGNORECASE) + matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header, re.IGNORECASE) header_mtime = parse_http_date(matches.group(1)) header_len = matches.group(3) if header_len and int(header_len) != size: diff --git a/bakery/views/base.py b/bakery/views/base.py index 3ac34a1..819fb29 100644 --- a/bakery/views/base.py +++ b/bakery/views/base.py @@ -14,11 +14,12 @@ from fs import path from django.apps import apps from django.conf import settings -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str as smart_text from bakery import DEFAULT_GZIP_CONTENT_TYPES from django.test.client import RequestFactory from bakery.management.commands import get_s3_client from django.views.generic import RedirectView, TemplateView + try: from django.core.urlresolvers import reverse, NoReverseMatch except ImportError: # Starting with Django 2.0, django.core.urlresolvers does not exist anymore @@ -30,6 +31,7 @@ class BuildableMixin(object): """ Common methods we will use in buildable views. """ + fs_name = apps.get_app_config("bakery").filesystem_name fs = apps.get_app_config("bakery").filesystem @@ -74,7 +76,7 @@ def write_file(self, target_path, html): Writes out the provided HTML to the provided path. """ logger.debug("Building to {}{}".format(self.fs_name, target_path)) - with self.fs.open(smart_text(target_path), 'wb') as outfile: + with self.fs.open(smart_text(target_path), "wb") as outfile: outfile.write(six.binary_type(html)) outfile.close() @@ -84,14 +86,10 @@ def is_gzippable(self, path): for gzipping. """ # First check if gzipping is allowed by the global setting - if not getattr(settings, 'BAKERY_GZIP', False): + if not getattr(settings, "BAKERY_GZIP", False): return False # Then check if the content type of this particular file is gzippable - whitelist = getattr( - settings, - 'GZIP_CONTENT_TYPES', - DEFAULT_GZIP_CONTENT_TYPES - ) + whitelist = getattr(settings, "GZIP_CONTENT_TYPES", DEFAULT_GZIP_CONTENT_TYPES) return mimetypes.guess_type(path)[0] in whitelist def gzip_file(self, target_path, html): @@ -109,18 +107,14 @@ def gzip_file(self, target_path, html): # Write GZIP data to an in-memory buffer data_buffer = six.BytesIO() - kwargs = dict( - filename=path.basename(target_path), - mode='wb', - fileobj=data_buffer - ) + kwargs = dict(filename=path.basename(target_path), mode="wb", fileobj=data_buffer) if float(sys.version[:3]) >= 2.7: - kwargs['mtime'] = 0 + kwargs["mtime"] = 0 with gzip.GzipFile(**kwargs) as f: f.write(six.binary_type(html)) # Write that buffer out to the filesystem - with self.fs.open(smart_text(target_path), 'wb') as outfile: + with self.fs.open(smart_text(target_path), "wb") as outfile: outfile.write(data_buffer.getvalue()) outfile.close() @@ -139,6 +133,7 @@ class BuildableTemplateView(TemplateView, BuildableMixin): template_name: The name of the template you would like Django to render. """ + @property def build_method(self): return self.build @@ -152,15 +147,16 @@ def build(self): self.build_file(path, self.get_content()) def get_build_path(self): - return six.text_type(self.build_path).lstrip('/') + return six.text_type(self.build_path).lstrip("/") class Buildable404View(BuildableTemplateView): """ The default Django 404 page, but built out. """ - build_path = '404.html' - template_name = '404.html' + + build_path = "404.html" + template_name = "404.html" class BuildableRedirectView(RedirectView, BuildableMixin): @@ -177,6 +173,7 @@ class BuildableRedirectView(RedirectView, BuildableMixin): The URL where redirect will send the user. Operates in the same way as the standard generic RedirectView. """ + permanent = True def get_content(self): @@ -196,10 +193,7 @@ def build_method(self): return self.build def build(self): - logger.debug("Building redirect from %s to %s" % ( - self.build_path, - self.get_redirect_url() - )) + logger.debug("Building redirect from %s to %s" % (self.build_path, self.get_redirect_url())) self.request = self.create_request(self.build_path) path = os.path.join(settings.BUILD_DIR, self.build_path) self.prep_directory(self.build_path) @@ -223,19 +217,16 @@ def get_redirect_url(self, *args, **kwargs): return url def post_publish(self, bucket): - logger.debug("Adding S3 redirect header from {} to in {} to {}".format( - self.build_path, - bucket.name, - self.get_redirect_url() - )) + logger.debug( + "Adding S3 redirect header from {} to in {} to {}".format( + self.build_path, bucket.name, self.get_redirect_url() + ) + ) s3_client, s3_resource = get_s3_client() s3_client.copy_object( - ACL='public-read', + ACL="public-read", Bucket=bucket.name, - CopySource={ - 'Bucket': bucket.name, - 'Key': self.build_path - }, + CopySource={"Bucket": bucket.name, "Key": self.build_path}, Key=self.build_path, - WebsiteRedirectLocation=self.get_redirect_url() + WebsiteRedirectLocation=self.get_redirect_url(), ) diff --git a/bigvince/settings_.py b/bigvince/settings_.py index a8eab58..f2b33e1 100644 --- a/bigvince/settings_.py +++ b/bigvince/settings_.py @@ -54,7 +54,7 @@ ROOT_DIR = environ.Path(__file__) - 3 # any change that requires database migrations is a minor release -VERSION = "2.1.11" +VERSION = "3.0.0" # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ diff --git a/cogauth/backend.py b/cogauth/backend.py index 4deafb2..7fd521b 100644 --- a/cogauth/backend.py +++ b/cogauth/backend.py @@ -33,6 +33,7 @@ from django.contrib.auth.backends import ModelBackend from django.contrib.auth import get_user_model from django.contrib.auth.hashers import make_password + try: from django.utils.six import iteritems except: @@ -48,51 +49,52 @@ from vinny.models import VinceAPIToken from rest_framework import exceptions from rest_framework.authentication import BaseAuthentication, TokenAuthentication, get_authorization_header -from django.utils.encoding import smart_text -from django.utils.translation import ugettext as _ +from django.utils.encoding import smart_str as smart_text +from django.utils.translation import gettext as _ from bigvince.utils import get_cognito_url, get_cognito_pool_url import traceback from lib.vince import utils as vinceutils + logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) - class CognitoUser(Cognito): user_class = get_user_model() - COGNITO_ATTRS = getattr(settings, 'COGNITO_ATTR_MAPPING', - { 'username': 'username', - 'email':'email', - 'given_name' : 'first_name', - 'family_name':'last_name', - 'locale':'country' - } - ) + COGNITO_ATTRS = getattr( + settings, + "COGNITO_ATTR_MAPPING", + { + "username": "username", + "email": "email", + "given_name": "first_name", + "family_name": "last_name", + "locale": "country", + }, + ) def get_user_obj(self, username=None, attribute_list=[], metadata={}, attr_map={}): - user_attrs = cognito_to_dict(attribute_list,CognitoUser.COGNITO_ATTRS) + user_attrs = cognito_to_dict(attribute_list, CognitoUser.COGNITO_ATTRS) django_fields = [f.name for f in CognitoUser.user_class._meta.get_fields()] log_attrs = user_attrs.copy() - if 'api_key' in user_attrs: - log_attrs['api_key'] = "RESERVED" + if "api_key" in user_attrs: + log_attrs["api_key"] = "RESERVED" logger.debug(f"User attributes in Cognito is {log_attrs}") extra_attrs = {} # need to iterate over a copy for k, v in user_attrs.copy().items(): if k not in django_fields: - extra_attrs.update({k: user_attrs.pop(k, None) }) - if getattr(settings, 'COGNITO_CREATE_UNKNOWN_USERS', True): - user, created = CognitoUser.user_class.objects.update_or_create( - username=username, - defaults=user_attrs) + extra_attrs.update({k: user_attrs.pop(k, None)}) + if getattr(settings, "COGNITO_CREATE_UNKNOWN_USERS", True): + user, created = CognitoUser.user_class.objects.update_or_create(username=username, defaults=user_attrs) if user: if settings.VINCE_NAMESPACE == "vinny": try: for k, v in extra_attrs.items(): setattr(user.vinceprofile, k, v) - #logger.debug(f"{k}:{v}") + # logger.debug(f"{k}:{v}") user.vinceprofile.save() except Exception as e: logger.debug(f"Vinceprofile probably doesn't exist for user {username}, error returned {e}") @@ -120,13 +122,14 @@ def get_user_obj(self, username=None, attribute_list=[], metadata={}, attr_map={ except Exception as e: logger.debug(f"vinceprofile probably does not exist for {username}, returned error is {e}") try: - for k, v in extra_attrs.items(): + for k, v in extra_attrs.items(): setattr(user.usersettings, k, v) user.usersettings.save() except: logger.debug(f"usersettings probably doesn't exist for {username}") return user + class CognitoAuthenticate(ModelBackend): def authenticate(self, request, username=None, password=None): ip = vinceutils.get_ip(request) @@ -135,115 +138,122 @@ def authenticate(self, request, username=None, password=None): settings.COGNITO_USER_POOL_ID, settings.COGNITO_APP_ID, user_pool_region=settings.COGNITO_REGION, - access_key=getattr(settings, 'AWS_ACCESS_KEY_ID', None), - secret_key=getattr(settings, 'AWS_SECRET_ACCESS_KEY', None), - username=username) + access_key=getattr(settings, "AWS_ACCESS_KEY_ID", None), + secret_key=getattr(settings, "AWS_SECRET_ACCESS_KEY", None), + username=username, + ) try: logger.debug(f"trying to authenticate {username} from IP {ip}") cognito_user.authenticate(password) except ForceChangePasswordException: - request.session['FORCEPASSWORD']=True - request.session['username']=username + request.session["FORCEPASSWORD"] = True + request.session["username"] = username return None except SoftwareTokenException as e: - request.session['MFAREQUIRED']= "SOFTWARE_TOKEN_MFA" - request.session['username']=username - request.session['MFASession']=cognito_user.session - request.session['DEVICE_NAME'] = str(e) + request.session["MFAREQUIRED"] = "SOFTWARE_TOKEN_MFA" + request.session["username"] = username + request.session["MFASession"] = cognito_user.session + request.session["DEVICE_NAME"] = str(e) request.session.save() return None except SMSMFAException: - request.session['MFAREQUIRED']="SMS_MFA" - request.session['username']=username - request.session['MFASession']=cognito_user.session + request.session["MFAREQUIRED"] = "SMS_MFA" + request.session["username"] = username + request.session["MFASession"] = cognito_user.session request.session.save() return None except (Boto3Error, ClientError) as e: - error_code = e.response['Error']['Code'] + error_code = e.response["Error"]["Code"] logger.debug(f"error authenticating user {username} error: {e} {error_code} from IP {ip}") if error_code == "PasswordResetRequiredException": logger.debug(f"reset password needed for {username} from IP {ip}") - request.session['RESETPASSWORD']=True - request.session['username']=username + request.session["RESETPASSWORD"] = True + request.session["username"] = username return None if error_code == "UserNotConfirmedException": logger.debug(f"User {username} did not confirm their account from IP {ip}") - #get user + # get user user = User.objects.filter(username=username).first() if user: - request.session['NOTCONFIRMED'] = True - request.session['CONFIRM_ID'] = user.id + request.session["NOTCONFIRMED"] = True + request.session["CONFIRM_ID"] = user.id return None - if error_code in [ 'NotAuthorizedException', 'UserNotFoundException']: + if error_code in ["NotAuthorizedException", "UserNotFoundException"]: return None else: return None - elif request.session.get('ACCESS_TOKEN'): + elif request.session.get("ACCESS_TOKEN"): # no password means we are either getting the code and trading it in # for tokens or we already have tokens - in which case we just need to get # the user and return - client= boto3.client('cognito-idp', - endpoint_url=get_cognito_url(), region_name=settings.COGNITO_REGION) - user = client.get_user(AccessToken=request.session['ACCESS_TOKEN']) + client = boto3.client("cognito-idp", endpoint_url=get_cognito_url(), region_name=settings.COGNITO_REGION) + user = client.get_user(AccessToken=request.session["ACCESS_TOKEN"]) # the username returned is the unique id, which doesn't help us since we use # emails for username - so get email and return CognitoUser - email = list(filter(lambda email: email['Name'] == 'email', user['UserAttributes']))[0]['Value'] - username=email + email = list(filter(lambda email: email["Name"] == "email", user["UserAttributes"]))[0]["Value"] + username = email cognito_user = CognitoUser( settings.COGNITO_USER_POOL_ID, settings.COGNITO_APP_ID, user_pool_region=settings.COGNITO_REGION, - access_key=getattr(settings, 'AWS_ACCESS_KEY_ID', None), - secret_key=getattr(settings, 'AWS_SECRET_ACCESS_KEY', None), - username=username) - - cognito_user.access_token= request.session['ACCESS_TOKEN'] - cognito_user.refresh_token = request.session['REFRESH_TOKEN'] + access_key=getattr(settings, "AWS_ACCESS_KEY_ID", None), + secret_key=getattr(settings, "AWS_SECRET_ACCESS_KEY", None), + username=username, + ) + + cognito_user.access_token = request.session["ACCESS_TOKEN"] + cognito_user.refresh_token = request.session["REFRESH_TOKEN"] else: - headers={'Content-Type': 'application/x-www-form-urlencoded'} + headers = {"Content-Type": "application/x-www-form-urlencoded"} data = { - 'grant_type': 'authorization_code', - 'client_id': settings.COGNITO_APP_ID, - 'redirect_uri':settings.COGNITO_REDIRECT_TO, - 'code': username + "grant_type": "authorization_code", + "client_id": settings.COGNITO_APP_ID, + "redirect_uri": settings.COGNITO_REDIRECT_TO, + "code": username, } - r = requests.post(settings.COGNITO_OAUTH_URL, headers=headers,data=data) - if not(r == None or (r.status_code != requests.codes.ok)): + r = requests.post(settings.COGNITO_OAUTH_URL, headers=headers, data=data) + if not (r == None or (r.status_code != requests.codes.ok)): rj = r.json() - access_token = rj['access_token'] - refresh_token = rj['refresh_token'] - id_token=rj['id_token'] + access_token = rj["access_token"] + refresh_token = rj["refresh_token"] + id_token = rj["id_token"] - u = Cognito(settings.COGNITO_USER_POOL_ID, settings.COGNITO_APP_ID, - user_pool_region=settings.COGNITO_REGION, - id_token=id_token, refresh_token=refresh_token, - access_token=access_token) + u = Cognito( + settings.COGNITO_USER_POOL_ID, + settings.COGNITO_APP_ID, + user_pool_region=settings.COGNITO_REGION, + id_token=id_token, + refresh_token=refresh_token, + access_token=access_token, + ) u.check_token() - - client= boto3.client('cognito-idp', - endpoint_url=get_cognito_url(), region_name=settings.COGNITO_REGION) + + client = boto3.client( + "cognito-idp", endpoint_url=get_cognito_url(), region_name=settings.COGNITO_REGION + ) user = client.get_user(AccessToken=access_token) - username = user['Username'] + username = user["Username"] cognito_user = CognitoUser( settings.COGNITO_USER_POOL_ID, settings.COGNITO_APP_ID, user_pool_region=settings.COGNITO_REGION, - access_key=getattr(settings, 'AWS_ACCESS_KEY_ID', None), - secret_key=getattr(settings, 'AWS_SECRET_ACCESS_KEY', None), - username=username) - - cognito_user.verify_token(id_token, 'id_token', 'id') - cognito_user.access_token= access_token + access_key=getattr(settings, "AWS_ACCESS_KEY_ID", None), + secret_key=getattr(settings, "AWS_SECRET_ACCESS_KEY", None), + username=username, + ) + + cognito_user.verify_token(id_token, "id_token", "id") + cognito_user.access_token = access_token cognito_user.refresh_token = refresh_token - cognito_user.token_type = rj['token_type'] - + cognito_user.token_type = rj["token_type"] + else: return None - + # now we have a cognito user - set session variables and return if cognito_user: user = cognito_user.get_user() @@ -253,10 +263,10 @@ def authenticate(self, request, username=None, password=None): return None if user: - request.session['ACCESS_TOKEN'] = cognito_user.access_token - request.session['ID_TOKEN'] = cognito_user.id_token - request.session['REFRESH_TOKEN'] = cognito_user.refresh_token - #request.session.save() + request.session["ACCESS_TOKEN"] = cognito_user.access_token + request.session["ID_TOKEN"] = cognito_user.id_token + request.session["REFRESH_TOKEN"] = cognito_user.refresh_token + # request.session.save() logger.info(f"USER {user} is authenticated from ip {ip}") return user @@ -267,10 +277,10 @@ class CognitoAuthenticateAPI(ModelBackend): def authenticate(self, request): """For rest_framework if successfully authenticated using CognitoAuth the response wil include a tuple (request.user,request.auth) - In case of Session based authentications + In case of Session based authentications request.user will be a Django User instance. request.auth will be None. - https://www.django-rest-framework.org/api-guide/authentication/ + https://www.django-rest-framework.org/api-guide/authentication/ """ try: ip = vinceutils.get_ip(request) @@ -280,12 +290,10 @@ def authenticate(self, request): return user, None else: logger.warn(f"Failed API authentication using session for User {user} from IP {ip}") - raise exceptions.AuthenticationFailed(_('Invalid API session attempted')) + raise exceptions.AuthenticationFailed(_("Invalid API session attempted")) except Exception as e: logger.warn(f"Failed API authentication for session error is {e}") - raise exceptions.AuthenticationFailed(_('Invalid API no session or token header was provided')) - - + raise exceptions.AuthenticationFailed(_("Invalid API no session or token header was provided")) class HashedTokenAuthentication(TokenAuthentication): @@ -295,12 +303,14 @@ class HashedTokenAuthentication(TokenAuthentication): HTTP header, prepended with the string "Token ". For example: Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a """ + model = VinceAPIToken def get_model(self): if self.model is not None: return self.model from rest_framework.authtoken.models import Token + return Token """ @@ -308,36 +318,36 @@ def get_model(self): * key -- The string identifying the token * user -- The user to which the token belongs """ + def authenticate(self, request): ip = vinceutils.get_ip(request) - setattr(self,"ip",ip) + setattr(self, "ip", ip) return super(HashedTokenAuthentication, self).authenticate(request) - def authenticate_credentials(self, key): - if hasattr(self,'ip'): + if hasattr(self, "ip"): ip = self.ip else: ip = "Unknown" model = self.get_model() hashed_key = make_password(key, settings.SECRET_KEY) try: - token = model.objects.select_related('user').get(key=hashed_key) + token = model.objects.select_related("user").get(key=hashed_key) except model.DoesNotExist: logger.warn(f"Failed API auth for token that does not exist {key} from IP {ip}") - raise exceptions.AuthenticationFailed(_('Invalid token.')) + raise exceptions.AuthenticationFailed(_("Invalid token.")) except Exception as e: logger.warn(f"Failed API auth for token Error {e} from IP {ip}") - raise exceptions.AuthenticationFailed(_('Unknown Token error.')) - + raise exceptions.AuthenticationFailed(_("Unknown Token error.")) + if not token.user.is_active: logger.warn(f"Failed API auth for {token.user} user is inactive or deleted from IP {ip}") - raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + raise exceptions.AuthenticationFailed(_("User inactive or deleted.")) logger.info(f"Success user {token.user} is authenticated using API Token from IP {ip}") return (token.user, token) - + class JSONWebTokenAuthentication(BaseAuthentication): """Token based authentication using the JSON Web Token standard.""" @@ -355,7 +365,7 @@ def authenticate(self, request): except TokenError: raise exceptions.AuthenticationFailed() logger.debug(f"JSONWeb returned payload is {jwt_payload}") - username=jwt_payload['email'] + username = jwt_payload["email"] user = User.objects.get(username=username) return (user, jwt_token) @@ -369,28 +379,21 @@ def get_jwt_token(self, request): msg = _("Invalid Authorization header. No credentials provided.") raise exceptions.AuthenticationFailed(msg) elif len(auth) > 2: - msg = _( - "Invalid Authorization header. Credentials string " - "should not contain spaces." - ) + msg = _("Invalid Authorization header. Credentials string " "should not contain spaces.") raise exceptions.AuthenticationFailed(msg) return auth[1] - def get_token_validator(self, request): return TokenValidator( settings.COGNITO_REGION, settings.COGNITO_USER_POOL_ID, settings.COGNITO_APP_ID, ) - + def authenticate_header(self, request): """ Method required by the DRF in order to return 401 responses for authentication failures, instead of 403. More details in https://www.django-rest-framework.org/api-guide/authentication/#custom-authentication. """ return "Bearer: api" - - - diff --git a/cogauth/views.py b/cogauth/views.py index ef5c900..6d43154 100644 --- a/cogauth/views.py +++ b/cogauth/views.py @@ -32,7 +32,7 @@ from django.forms.utils import ErrorList from django.http import Http404 from django.shortcuts import render, redirect, get_object_or_404 -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.utils.decorators import method_decorator from django.core.exceptions import PermissionDenied @@ -75,7 +75,7 @@ import traceback from boto3.exceptions import Boto3Error from botocore.exceptions import ClientError, ParamValidationError -from django.utils.http import is_safe_url +from django.utils.http import url_has_allowed_host_and_scheme as is_safe_url from django.http.response import JsonResponse from bigvince.utils import get_cognito_url, get_cognito_pool_url from vinny.models import VinceCommEmail diff --git a/kbworker/urls.py b/kbworker/urls.py index d351dda..27388d4 100644 --- a/kbworker/urls.py +++ b/kbworker/urls.py @@ -41,10 +41,9 @@ 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ -from django.conf.urls import url +from django.urls import include, re_path from kbworker.views import check_for_updates urlpatterns = [ - url(r'^check-for-updates/$', check_for_updates, name='checkupdate'), + re_path(r"^check-for-updates/$", check_for_updates, name="checkupdate"), ] - diff --git a/requirements.txt b/requirements.txt index 7885f80..cac846e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,11 @@ amqp==5.1.1 appdirs==1.4.4 -asgiref==3.5.2 +asgiref==3.6.0 asn1crypto==1.5.1 async-timeout==4.0.2 attrs==22.1.0 awscli==1.27.11 +backports.zoneinfo==0.2.1 beautifulsoup4==4.11.1 billiard==4.0.2 bleach==5.0.1 @@ -20,10 +21,10 @@ charset-normalizer==2.1.1 click==8.1.3 colorama==0.4.4 cryptography==42.0.4 -cvelib==1.1.0 +cvelib==1.3.0 Deprecated==1.2.13 dictdiffer==0.9.0 -Django==3.2.24 +Django==4.2 django-appconf==1.0.5 django-countries==7.4.2 django-environ==0.9.0 @@ -50,7 +51,8 @@ Markdown==3.1 packaging==21.3 pinax-messages==3.0.0 pip-autoremove==0.10.0 -pkgutil_resolve_name==1.3.10 +pkgutil-resolve-name==1.3.10 +psycopg2==2.9.9 psycopg2-binary==2.9.5 pyasn1==0.4.8 pycparser==2.21 @@ -74,7 +76,7 @@ simplejson==3.18.0 six==1.16.0 soupsieve==2.3.2.post1 sqlparse==0.4.4 -typing_extensions==4.4.0 +typing-extensions==4.4.0 urllib3==1.26.18 vine==5.0.0 watchtower==3.0.0 diff --git a/vince/__init__.py b/vince/__init__.py index c56af29..2961763 100644 --- a/vince/__init__.py +++ b/vince/__init__.py @@ -27,9 +27,11 @@ # DM21-1126 ######################################################################## from __future__ import absolute_import, unicode_literals -#from .celery import app as celery_app +import django -#__all__ = ['celery_app'] -default_app_config = 'vince.apps.VinceTrackConfig' +# from .celery import app as celery_app +# __all__ = ['celery_app'] +if django.VERSION < (3, 2): + default_app_config = "vince.apps.VinceTrackConfig" diff --git a/vince/admin.py b/vince/admin.py index 4a88467..bdff13d 100644 --- a/vince/admin.py +++ b/vince/admin.py @@ -35,9 +35,42 @@ from django.contrib.auth import get_user_model from django.contrib.auth import views as auth_views from django.contrib.admin.views.decorators import staff_member_required -from django.utils.translation import ugettext_lazy as _ -from vince.models import TicketQueue, Ticket, FollowUp, CaseTemplate, UserSettings, Contact, QueuePermissions, CasePermissions, TicketThread, CaseAssignment, CaseAction, ContactAssociation, CaseParticipant, CalendarEvent, VulNote, BounceEmailNotification -from vince.models import TicketChange, Attachment, VulnerabilityCase, EmailTemplate, EmailContact, AdminPGPEmail, Artifact, Vulnerability, VendorStatus, VulnerableVendor, VinceSMIMECertificate, UserRole, VinceReminder, GroupSettings, TagManager +from django.utils.translation import gettext_lazy as _ +from vince.models import ( + TicketQueue, + Ticket, + FollowUp, + CaseTemplate, + UserSettings, + Contact, + QueuePermissions, + CasePermissions, + TicketThread, + CaseAssignment, + CaseAction, + ContactAssociation, + CaseParticipant, + CalendarEvent, + VulNote, + BounceEmailNotification, +) +from vince.models import ( + TicketChange, + Attachment, + VulnerabilityCase, + EmailTemplate, + EmailContact, + AdminPGPEmail, + Artifact, + Vulnerability, + VendorStatus, + VulnerableVendor, + VinceSMIMECertificate, + UserRole, + VinceReminder, + GroupSettings, + TagManager, +) from vinny.models import Thread, Message, VTCaseRequest, CaseMember, VendorAction from django.contrib.admin.helpers import ActionForm from cogauth.views import COGLoginView @@ -54,12 +87,15 @@ def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False - + + @admin.register(TicketQueue) class QueueAdmin(admin.ModelAdmin): - list_display = ('title', 'slug', 'default_owner') + list_display = ("title", "slug", "default_owner") prepopulated_fields = {"slug": ("title",)} - inlines = [QueuePermissionInline,] + inlines = [ + QueuePermissionInline, + ] def has_delete_permission(self, request, obj=None): return False @@ -71,9 +107,8 @@ def get_form(self, request, obj=None, **kwargs): if not is_superuser: disabled_fields |= { - 'group', - 'group_read' - 'group_write', + "group", + "group_read" "group_write", } for f in disabled_fields: @@ -83,48 +118,51 @@ def get_form(self, request, obj=None, **kwargs): return form - class CasePermissionInline(admin.TabularInline): model = CasePermissions + class CaseParticipantInline(admin.TabularInline): model = CaseParticipant + def bulk_reassign(modeladmin, request, queryset): ct = queryset.count() - if int(request.POST['user']) == 0: + if int(request.POST["user"]) == 0: title = "Bulk unassign ticket by {request.user.usersettings.vince_username}" else: - assignee = User.objects.get(id=request.POST['user']).usersettings.vince_username + assignee = User.objects.get(id=request.POST["user"]).usersettings.vince_username title = f"Bulk reassign ticket to user {assignee} by {request.user.usersettings.vince_username}" for x in queryset: ca = FollowUp(ticket=x, title=title, user=request.user) ca.save() - - if int(request.POST['user']) == 0: + + if int(request.POST["user"]) == 0: queryset.update(assigned_to=None) else: - queryset.update(assigned_to=request.POST['user']) + queryset.update(assigned_to=request.POST["user"]) messages.success(request, f"Successfully updated {ct} tickets") - -bulk_reassign.short_description = 'Reassign tickets to another user' + + +bulk_reassign.short_description = "Reassign tickets to another user" def bulk_tktstatuschange(modeladmin, request, queryset): ct = queryset.count() status_dict = dict(Ticket.STATUS_CHOICES) - + for x in queryset: title = f"Bulk ticket status change to {status_dict[int(request.POST['status'])]} by {request.user.usersettings.vince_username}" ca = FollowUp(ticket=x, title=title, user=request.user) ca.save() - - queryset.update(status = request.POST['status']) - + + queryset.update(status=request.POST["status"]) + messages.success(request, f"Successfully updated {ct} tickets") -bulk_tktstatuschange.short_description = 'Change ticket status' + +bulk_tktstatuschange.short_description = "Change ticket status" def bulk_casestatuschange(modeladmin, request, queryset): @@ -134,15 +172,17 @@ def bulk_casestatuschange(modeladmin, request, queryset): title = f"Bulk case status change to {status_dict[int(request.POST['status'])]} by {request.user.usersettings.vince_username}" ca = CaseAction(case=x, user=request.user, title=title, action_type=0) ca.save() - queryset.update(status = request.POST['status']) + queryset.update(status=request.POST["status"]) messages.success(request, f"Successfully updated {ct} cases") -bulk_casestatuschange.short_description = 'Change case status' + +bulk_casestatuschange.short_description = "Change case status" + def bulk_moveticket(modeladmin, request, queryset): ct = queryset.count() - if request.POST.get('case') != "": - case = VulnerabilityCase.objects.filter(vuid=request.POST['case']).first() + if request.POST.get("case") != "": + case = VulnerabilityCase.objects.filter(vuid=request.POST["case"]).first() queue = TicketQueue.objects.filter(slug="case").first() if case and queue: queryset.update(case=case, queue=queue) @@ -151,34 +191,39 @@ def bulk_moveticket(modeladmin, request, queryset): messages.error(request, f"Case doesn't exist") else: messages.error(request, f"Case doesn't exist") + + bulk_moveticket.short_description = "Move Tickets to Case Queue" + def bulk_reassign_cases(modeladmin, request, queryset): ct = queryset.count() - if int(request.POST['user']) == 0: + if int(request.POST["user"]) == 0: assignee = "None" else: - assignee = User.objects.get(id=request.POST['user']).usersettings.vince_username + assignee = User.objects.get(id=request.POST["user"]).usersettings.vince_username for x in queryset: title = f"Bulk reassign owner to user {assignee} by {request.user.usersettings.vince_username}" ca = CaseAction(case=x, user=request.user, title=title, action_type=0) ca.save() - if int(request.POST['user']) == 0: + if int(request.POST["user"]) == 0: queryset.update(owner=None) else: - queryset.update(owner=request.POST['user']) + queryset.update(owner=request.POST["user"]) messages.success(request, f"Successfully updated {ct} cases") -bulk_reassign_cases.short_description = 'Change case ownership to another user' + +bulk_reassign_cases.short_description = "Change case ownership to another user" + def bulk_add_user_case(modeladmin, request, queryset): ct = queryset.count() - if int(request.POST['user']) > 0: + if int(request.POST["user"]) > 0: for x in queryset: - assignee = User.objects.get(id=request.POST['user']) + assignee = User.objects.get(id=request.POST["user"]) CaseAssignment.objects.get_or_create(assigned=assignee, case=x) title = f"Bulk assigned user {assignee.usersettings.vince_username} by {request.user.usersettings.vince_username}" ca = CaseAction(case=x, user=request.user, title=title, action_type=0) @@ -187,13 +232,15 @@ def bulk_add_user_case(modeladmin, request, queryset): else: messages.error(request, f"Use bulk unassigment action to unassign user from case") + bulk_add_user_case.short_description = "Add user to case assignment" + def bulk_unassign_user_case(modeladmin, request, queryset): ct = queryset.count() - if int(request.POST['user']) > 0: + if int(request.POST["user"]) > 0: for x in queryset: - assignee = User.objects.get(id=request.POST['user']) + assignee = User.objects.get(id=request.POST["user"]) CaseAssignment.objects.filter(assigned=assignee, case=x).delete() title = f"Bulk unassigned user {assignee.usersettings.vince_username} by {request.user.usersettings.vince_username}" ca = CaseAction(case=x, user=request.user, title=title, action_type=0) @@ -201,68 +248,75 @@ def bulk_unassign_user_case(modeladmin, request, queryset): messages.success(request, f"Successfully updated {ct} cases") else: messages.error(request, f"Please select a user to unassign from selected cases") + + bulk_unassign_user_case.short_description = "Unassign user from all selected cases" + class BulkAssignmentForm(ActionForm): try: - USER_CHOICES = [(0, '--------')] + [(q.id, q.usersettings.preferred_username) for q in get_user_model().objects.all()] + USER_CHOICES = [(0, "--------")] + [ + (q.id, q.usersettings.preferred_username) for q in get_user_model().objects.all() + ] except: USER_CHOICES = [] - - user = forms.ChoiceField(choices=USER_CHOICES, - label=_('Assign a User'), - required=False - ) - status = forms.ChoiceField(choices=Ticket.STATUS_CHOICES, - label=_('Change Ticket Status'), - required=False) + user = forms.ChoiceField(choices=USER_CHOICES, label=_("Assign a User"), required=False) + + status = forms.ChoiceField(choices=Ticket.STATUS_CHOICES, label=_("Change Ticket Status"), required=False) + + case = forms.CharField(label=_("Case ID Number"), required=False) - case = forms.CharField(label=_('Case ID Number'), - required=False) - class CaseBulkAssignmentForm(ActionForm): try: - USER_CHOICES = [(0, '--------')] + [(q.id, q.usersettings.preferred_username) for q in get_user_model().objects.all()] + USER_CHOICES = [(0, "--------")] + [ + (q.id, q.usersettings.preferred_username) for q in get_user_model().objects.all() + ] except: USER_CHOICES = [] - user = forms.ChoiceField(choices=USER_CHOICES, - label=_('Assign a User'), - required=False - ) + user = forms.ChoiceField(choices=USER_CHOICES, label=_("Assign a User"), required=False) + + status = forms.ChoiceField(choices=VulnerabilityCase.STATUS_CHOICES, label=_("Change Case Status"), required=False) - status = forms.ChoiceField(choices=VulnerabilityCase.STATUS_CHOICES, - label=_('Change Case Status'), - required=False - ) class CaseAssignedFilter(admin.SimpleListFilter): title = "Assigned" - parameter_name = 'assigned_to' + parameter_name = "assigned_to" def lookups(self, request, model_admin): - return [(0, '--------')] + [(q.id, q.username) for q in get_user_model().objects.all()] + return [(0, "--------")] + [(q.id, q.username) for q in get_user_model().objects.all()] def queryset(self, request, queryset): if self.value() == 0: - assignments = CaseAssignment.objects.all().values_list('case__id', flat=True) + assignments = CaseAssignment.objects.all().values_list("case__id", flat=True) return queryset.exclude(id__in=assignments) elif self.value(): - assignments = CaseAssignment.objects.filter(assigned=self.value()).values_list('case__id', flat=True) + assignments = CaseAssignment.objects.filter(assigned=self.value()).values_list("case__id", flat=True) return queryset.filter(id__in=assignments) else: return queryset - @admin.register(VulnerabilityCase) class VinceCaseAdmin(admin.ModelAdmin): - list_display = ('vuid', 'title', 'team_owner', 'created', 'owner', 'status', 'product_name', 'case_get_assigned_to') - inlines = [CasePermissionInline, CaseParticipantInline, ] - search_fields = ('vuid', 'title', 'product_name') - list_filter = ('team_owner', 'owner', 'status', CaseAssignedFilter) + list_display = ( + "vuid", + "title", + "team_owner", + "created", + "owner", + "status", + "product_name", + "case_get_assigned_to", + ) + inlines = [ + CasePermissionInline, + CaseParticipantInline, + ] + search_fields = ("vuid", "title", "product_name") + list_filter = ("team_owner", "owner", "status", CaseAssignedFilter) action_form = CaseBulkAssignmentForm actions = [bulk_reassign_cases, bulk_add_user_case, bulk_unassign_user_case, bulk_casestatuschange] @@ -270,38 +324,45 @@ def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False - + def case_get_assigned_to(self, obj): return obj.get_assigned_to - case_get_assigned_to.short_description = _('Users assigned') - + case_get_assigned_to.short_description = _("Users assigned") + + @admin.register(EmailTemplate) -class EmailTemplateAdmin (admin.ModelAdmin): - list_display = ('template_name', 'subject', 'heading', 'plain_text', 'locale' ) - search_fields = ('template_name', 'locale', 'heading') - list_filter = ('locale', ) - - def has_delete_permission(self, request, obj=None): +class EmailTemplateAdmin(admin.ModelAdmin): + list_display = ("template_name", "subject", "heading", "plain_text", "locale") + search_fields = ("template_name", "locale", "heading") + list_filter = ("locale",) + + def has_delete_permission(self, request, obj=None): return False class AssignedFilter(admin.SimpleListFilter): title = "Filter by Assigned" - parameter_name = 'assigned_to' - + parameter_name = "assigned_to" + def lookups(self, request, model_admin): - return [(0, '--------')] + [(q.id, q.username) for q in get_user_model().objects.all()] + return [(0, "--------")] + [(q.id, q.username) for q in get_user_model().objects.all()] + def queryset(self, request, queryset): return queryset.filter(assigned_to=self.value()) - - + + @admin.register(Ticket) class TicketAdmin(admin.ModelAdmin): - search_fields=['title', 'case__vuid'] - list_display = ('title', 'status', 'assigned_to', 'queue', ) - date_hierarchy = 'created' - list_filter = ('queue', 'assigned_to', 'status') + search_fields = ["title", "case__vuid"] + list_display = ( + "title", + "status", + "assigned_to", + "queue", + ) + date_hierarchy = "created" + list_filter = ("queue", "assigned_to", "status") action_form = BulkAssignmentForm list_per_page = 250 actions = [bulk_reassign, bulk_tktstatuschange, bulk_moveticket] @@ -310,7 +371,7 @@ def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False - + def hidden_submitter_email(self, ticket): if ticket.submitter_email: username, domain = ticket.submitter_email.split("@") @@ -320,54 +381,57 @@ def hidden_submitter_email(self, ticket): else: return ticket.submitter_email - + class TicketChangeInline(admin.StackedInline): model = TicketChange + class AttachmentInline(admin.StackedInline): model = Attachment class ReminderAdmin(admin.ModelAdmin): - list_display = ('title', 'user', 'created_by', 'alert_date') - list_filter = ('user', 'alert_date', 'created_by') + list_display = ("title", "user", "created_by", "alert_date") + list_filter = ("user", "alert_date", "created_by") list_per_page = 250 def has_delete_permission(self, request, obj=None): if request.user.is_staff: return True return False - + + @admin.register(FollowUp) class FollowUpAdmin(admin.ModelAdmin): inlines = [TicketChangeInline, AttachmentInline] - list_display = ('ticket_get_ticket_for_url', 'title', 'date', 'ticket', 'user', 'new_status') - list_filter = ('user', 'date', 'new_status') + list_display = ("ticket_get_ticket_for_url", "title", "date", "ticket", "user", "new_status") + list_filter = ("user", "date", "new_status") def ticket_get_ticket_for_url(self, obj): return obj.ticket.ticket_for_url - ticket_get_ticket_for_url.short_description = _('Slug') - + ticket_get_ticket_for_url.short_description = _("Slug") + + class UserSettingsInline(admin.StackedInline): - model=UserSettings - can_delete=False - verbose_name_plural='UserSettings' - fk_name='user' - fields=('org', 'preferred_username', 'case_template', 'contacts_read', 'contacts_write') + model = UserSettings + can_delete = False + verbose_name_plural = "UserSettings" + fk_name = "user" + fields = ("org", "preferred_username", "case_template", "contacts_read", "contacts_write") -class CustomUserAdmin(UserAdmin): - inlines=(UserSettingsInline,) - list_display = ('username', 'first_name', 'last_name', 'get_preferred_username') - list_select_related = ('usersettings',) - actions=['get_preferred_username'] +class CustomUserAdmin(UserAdmin): + inlines = (UserSettingsInline,) + list_display = ("username", "first_name", "last_name", "get_preferred_username") + list_select_related = ("usersettings",) + actions = ["get_preferred_username"] def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False - + def get_form(self, request, obj=None, **kwargs): form = super().get_form(request, obj, **kwargs) is_superuser = request.user.is_superuser @@ -375,81 +439,82 @@ def get_form(self, request, obj=None, **kwargs): if not is_superuser: disabled_fields |= { - 'username', - 'is_superuser', - 'email', - 'user_permissions', + "username", + "is_superuser", + "email", + "user_permissions", } - if (not is_superuser - and obj is not None - and obj == request.user - ): + if not is_superuser and obj is not None and obj == request.user: disabled_fields |= { - 'is_staff', - 'is_superuser', - 'groups', - 'user_permissions', + "is_staff", + "is_superuser", + "groups", + "user_permissions", } - + for f in disabled_fields: if f in form.base_fields: form.base_fields[f].disabled = True return form - + def get_preferred_username(self, instance): return instance.usersettings.preferred_username + get_preferred_username.short_description = "Visible" - + def get_inline_instances(self, request, obj=None): if not obj: return list() return super(CustomUserAdmin, self).get_inline_instances(request, obj) + class EmailContactInLine(admin.TabularInline): model = EmailContact - + class ContactAdmin(admin.ModelAdmin): - search_fields=['vendor_name'] - list_display=['vendor_name', 'vendor_type', 'active', "_emails"] + search_fields = ["vendor_name"] + list_display = ["vendor_name", "vendor_type", "active", "_emails"] + + inlines = [EmailContactInLine] - inlines = [ - EmailContactInLine - ] - def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False - + def _emails(self, obj): return obj.get_emails() - -#admin.site = CustomAdminSite("default") + + +# admin.site = CustomAdminSite("default") + class AdminPGPEmailAdmin(admin.ModelAdmin): - fields = ('pgp_key_data', 'pgp_key_id', 'email', 'name', 'active') - list_display = ('pgp_key_id', 'email', 'name', 'active') + fields = ("pgp_key_data", "pgp_key_id", "email", "name", "active") + list_display = ("pgp_key_id", "email", "name", "active") def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False + class VulAdmin(admin.ModelAdmin): - list_display = ('get_vul_id', 'cve', 'case', 'description') - search_fields=['description', 'case__vuid', 'cve'] - actions = ['get_vul_id'] + list_display = ("get_vul_id", "cve", "case", "description") + search_fields = ["description", "case__vuid", "cve"] + actions = ["get_vul_id"] title = "Deleted Vulnerabilities" - + def get_queryset(self, request): qs = super().get_queryset(request) return qs.filter(deleted=True) def get_vul_id(self, instance): return instance.vul + get_vul_id.short_description = "Vul ID" def has_delete_permission(self, request, obj=None): @@ -457,10 +522,11 @@ def has_delete_permission(self, request, obj=None): return True return False + class VulVendorAdmin(admin.ModelAdmin): - search_fields = ['case__vuid', 'case__title', 'vendor', 'contact__vendor_name'] - list_filter = ('deleted', ) - + search_fields = ["case__vuid", "case__title", "vendor", "contact__vendor_name"] + list_filter = ("deleted",) + def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True @@ -469,25 +535,30 @@ def has_delete_permission(self, request, obj=None): class MessageInline(admin.TabularInline): model = Message - fields = ('content', 'created') + fields = ("content", "created") def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False - + + class ThreadAdmin(admin.ModelAdmin): - list_display = ('id', 'subject', 'to_group', 'from_group', 'case') - inlines = [MessageInline,] - + list_display = ("id", "subject", "to_group", "from_group", "case") + inlines = [ + MessageInline, + ] + def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False + class CaseMemberAdmin(admin.ModelAdmin): - search_fields = ['case__vuid', 'case__title', 'group__groupcontact__contact__vendor_name', 'participant__email'] - + search_fields = ["case__vuid", "case__title", "group__groupcontact__contact__vendor_name", "participant__email"] + + class VUReportInline(admin.TabularInline): model = VUReport @@ -496,91 +567,122 @@ def has_delete_permission(self, request, obj=None): return True return False + class TrackVulNoteAdmin(admin.ModelAdmin): - search_fields = ['case__vuid', 'case__title'] - list_display = ['case'] + search_fields = ["case__vuid", "case__title"] + list_display = ["case"] def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False - + + class VulNoteAdmin(admin.ModelAdmin): - search_fields = ['vuid', 'title'] - list_display = ['vuid', 'title'] - fields = ['vuid', 'title', 'dateupdated', 'datefirstpublished', 'revision_number', 'publicdate', 'published'] - readonly_fields = ['vuid', 'title', 'dateupdated', 'datefirstpublished', 'revision_number'] + search_fields = ["vuid", "title"] + list_display = ["vuid", "title"] + fields = ["vuid", "title", "dateupdated", "datefirstpublished", "revision_number", "publicdate", "published"] + readonly_fields = ["vuid", "title", "dateupdated", "datefirstpublished", "revision_number"] def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False + class VUReportAdmin(admin.ModelAdmin): - search_fields = ['vuid', 'name', 'idnumber'] - list_display = ('vuid', 'name',) - readonly_fields = ['vuid', 'idnumber', 'name', 'overview', 'vulnote', 'search_vector', 'clean_desc', 'impact', 'resolution', 'workarounds', 'sysaffected', 'thanks', 'author', 'public'] - + search_fields = ["vuid", "name", "idnumber"] + list_display = ( + "vuid", + "name", + ) + readonly_fields = [ + "vuid", + "idnumber", + "name", + "overview", + "vulnote", + "search_vector", + "clean_desc", + "impact", + "resolution", + "workarounds", + "sysaffected", + "thanks", + "author", + "public", + ] + def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False + class VulPubAdmin(admin.ModelAdmin): def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False - + class VulPubVendorAdmin(admin.ModelAdmin): - search_fields = ['vendor'] - + search_fields = ["vendor"] + def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False + class VulPubVendorRecord(admin.ModelAdmin): - search_fields = ['vendor', 'vuid', 'idnumber'] + search_fields = ["vendor", "vuid", "idnumber"] def has_delete_permission(self, request, obj=None): if request.user.is_superuser: return True return False + class VTCaseRequestAdmin(admin.ModelAdmin): - list_display = ['vrf_id', 'product_name', 'vendor_name', 'user', 'new_vuid','date_submitted', 'coordinator'] - search_fields = ['vrf_id', 'product_name', 'new_vuid', 'vendor_name'] + list_display = ["vrf_id", "product_name", "vendor_name", "user", "new_vuid", "date_submitted", "coordinator"] + search_fields = ["vrf_id", "product_name", "new_vuid", "vendor_name"] class TagManagerAdmin(admin.ModelAdmin): - list_display = ['tag', 'description', 'tag_type', 'team'] - search_fields = ['tag', 'description'] - + list_display = ["tag", "description", "tag_type", "team"] + search_fields = ["tag", "description"] + + class GroupInline(admin.StackedInline): model = GroupSettings can_delete = False - verbose_name_plural = 'Group Settings' - + verbose_name_plural = "Group Settings" + + class GroupAdmin(BaseGroupAdmin): - inlines = (GroupInline, ) - list_display = ('name', 'get_org_name') + inlines = (GroupInline,) + list_display = ("name", "get_org_name") def get_org_name(self, instance): if instance.groupsettings: return instance.groupsettings.organization return "-" + get_org_name.short_description = "Organization Name" + class BounceAdmin(admin.ModelAdmin): - list_display = ['email', 'ticket', 'bounce_date', 'bounce_type', 'action_taken'] - search_fields = ['email', 'subject'] - -admin.site.login = staff_member_required(COGLoginView.as_view(template_name='vince/admin_login.html'), login_url = settings.LOGIN_URL) -admin.site.logout = auth_views.LogoutView.as_view(template_name='vince/tracklogout.html') + list_display = ["email", "ticket", "bounce_date", "bounce_type", "action_taken"] + search_fields = ["email", "subject"] + + +admin.site.login = staff_member_required( + COGLoginView.as_view(template_name="vince/admin_login.html"), login_url=settings.LOGIN_URL +) +admin.site.logout = auth_views.LogoutView.as_view(template_name="vince/tracklogout.html") admin.site.site_header = "VinceTrack Admin" admin.site.site_title = "VinceTrack Admin Portal" @@ -595,9 +697,9 @@ class BounceAdmin(admin.ModelAdmin): admin.site.register(Contact, ContactAdmin) admin.site.register(Vulnerability, VulAdmin) admin.site.register(VinceSMIMECertificate) -#admin.site.register(TicketThread) +# admin.site.register(TicketThread) admin.site.register(AdminPGPEmail, AdminPGPEmailAdmin) -#admin.site.register(Artifact) +# admin.site.register(Artifact) admin.site.register(VulnerableVendor, VulVendorAdmin) admin.site.register(Thread, ThreadAdmin) admin.site.register(VulnerabilityNote, VulNoteAdmin) diff --git a/vince/forms.py b/vince/forms.py index 09928ce..ebf1dda 100644 --- a/vince/forms.py +++ b/vince/forms.py @@ -55,7 +55,7 @@ from vince.permissions import get_user_gen_queue import traceback import os -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str as smart_text logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -1979,20 +1979,6 @@ def clean_ticket(self): except: raise forms.ValidationError("Invalid Ticket Selection. Use only numeric ID of Ticket.") - # def clean_email(self): - # email = self.cleaned_data["email"] - # logger.debug(f"email is {email}") - # internal = self.cleaned_data["internal"] - # logger.debug(f"internal is {internal}") - # if email in [None, "", "None"] and internal: - # logger.debug("we have reached the if block in which email is none and internal is truey") - # return - # try: - # logger.debug("we have passed the if block in which email is none and internal is truey") - # return email - # except: - # raise forms.ValidationError("Unacceptable email value.") - class ContactForm(forms.ModelForm): vtype = forms.ChoiceField( diff --git a/vince/lib.py b/vince/lib.py index 0167aea..9a4f2e8 100644 --- a/vince/lib.py +++ b/vince/lib.py @@ -46,7 +46,7 @@ from django.core.files import File from django.core.serializers.json import DjangoJSONEncoder from django.utils import timezone -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str as smart_text from django.template.loader import render_to_string, get_template from vince.models import VulnerabilityCase diff --git a/vince/static/vince/css/style.css b/vince/static/vince/css/style.css index 5ba2061..9c4d7ca 100644 --- a/vince/static/vince/css/style.css +++ b/vince/static/vince/css/style.css @@ -1425,12 +1425,13 @@ div.homelink a { width:100%; } +/* when putting up an announcement-banner, change padding-top for the following two selectors to 195 and 225 respectively: */ #TRoffCanvasLeft { background-color: #282829; border-right: 1px solid #c2c2c2; color: #f1f1f2; - padding-top:195px; + padding-top:130px; } @@ -1438,7 +1439,7 @@ div.homelink a { background-color: #f1f1f2; border-right: 1px solid #c2c2c2; color: #4d4d4f; - padding-top:225px; + padding-top:150px; } /*.position-left.reveal-for-medium ~ .off-canvas-content { diff --git a/vince/static/vince/js/case.js b/vince/static/vince/js/case.js index 8950743..98466da 100644 --- a/vince/static/vince/js/case.js +++ b/vince/static/vince/js/case.js @@ -1264,7 +1264,7 @@ $(document).ready(function() { function contactClickFunction(cell, formatterParams, onRendered) { var val = cell.getValue(); - if (cell.getRow().getData().users == 0) { + if (cell.getRow().getData().users == false) { val = " " + val } if (cell.getRow().getData().alert_tags.length) { @@ -1280,7 +1280,7 @@ $(document).ready(function() { return "This Vendor is tagged with an ALERT Tag: " + cell.getRow().getData().alert_tags[0] } - if (cell.getRow().getData().users == 0) { + if (cell.getRow().getData().users == false) { return "This vendor does not have any VINCE Users"; } else { return "This vendor has VINCE Users"; @@ -1391,7 +1391,7 @@ $(document).ready(function() { } function customNouserfilter(data, filterParams) { - return (data.users == 0); + return (data.users == false); } $(document).on("click", ".reqapproval", function(event) { @@ -1405,7 +1405,9 @@ $(document).ready(function() { $(document).on("click", ".vendorswithnousers", function(event) { event.preventDefault(); - vendors_table.setFilter("users", "=", "0"); + vendors_table.setFilter(function(data){ + return !data.users; + }); }); $(document).on("click", ".vendorapproved", function(event) { @@ -1454,7 +1456,7 @@ $(document).ready(function() { total_notified_vendors++ } - if (data[i].users == 0) { + if (data[i].users == false) { total_vendors_no_users++ } if (data[i].seen) { @@ -1492,6 +1494,7 @@ $(document).ready(function() { async function createVendorsTable() { let data = await ajaxVendorData() let vendors_data = data['data'] + console.log(vendors_data) populateFiltersWithValues(vendors_data) let vendors_total = vendors_data.length let pageSizeOptionsArray = [] diff --git a/vince/static/vince/js/contactverify.js b/vince/static/vince/js/contactverify.js index 121beeb..789dbd5 100644 --- a/vince/static/vince/js/contactverify.js +++ b/vince/static/vince/js/contactverify.js @@ -57,7 +57,13 @@ function getEmails(e, taggle) { for (let i=0; i< emails.length; i++) { taggle.add(emails[i]); } - } + }, + error: function(){ + console.log("ajax was erroneous") + }, + complete: function(){ + console.log("ajax was completed") + } }); } @@ -104,6 +110,8 @@ $(document).ready(function() { }); $.getJSON("/vince/ajax_calls/search/", function(data) { + console.log('the data entered into getJSON is'); + console.log(data); contact_auto(data); }); @@ -180,12 +188,13 @@ $(document).ready(function() { // } // } // } - // }); + // }); - // let user_to_verify_field = document.getElementById('id_user'); + // let user_to_verify_field = document.getElementById('id_user'); - // let temporarily_allowed_email = "" - // user_to_verify_field.addEventListener('change', function() { + // let temporarily_allowed_email = "" + // user_to_verify_field.addEventListener('change', function() { + // // remove whatever email was previously allowed as a result of this event listener: // let currently_allowed_emails = taggle.settings.allowedTags // for (let i=0; i < currently_allowed_emails.length; i++){ // if (currently_allowed_emails[i] == temporarily_allowed_email){ @@ -193,13 +202,14 @@ $(document).ready(function() { // taggle.remove(temporarily_allowed_email) // } // } + // // allow the new email and add it to the list of taggles: // temporarily_allowed_email = user_to_verify_field.value // currently_allowed_emails.push(temporarily_allowed_email) // if (internal_verification_checkbox.checked){ // taggle.settings.allowedTags = currently_allowed_emails; // taggle.add(temporarily_allowed_email); // } - // }); + // }); }); diff --git a/vince/static/vince/js/scontact.js b/vince/static/vince/js/scontact.js index ecee4ad..c79a11c 100644 --- a/vince/static/vince/js/scontact.js +++ b/vince/static/vince/js/scontact.js @@ -430,31 +430,31 @@ $(document).ready(function() { }); $(document).on("submit", "#addemailform", function(event) { - event.preventDefault(); - var url = $("#addemailform").attr("action"); - $.ajax({ + event.preventDefault(); + var url = $("#addemailform").attr("action"); + $.ajax({ url: url, type: "POST", - data: $("#addemailform").serialize(), + data: $("#addemailform").serialize(), success: function(data) { - console.log(data); - if (data['ticket']) { - location.href = data['ticket']; - } - else if (data['refresh']) { - window.location.reload(true); - } else if (data['msg_body']){ - $("#vendor-results").html("

" + data['text'] + " Or Request Authorization via Email

") + console.log(data); + if (data['ticket']) { + location.href = data['ticket']; + } + else if (data['refresh']) { + window.location.reload(true); + } else if (data['msg_body']){ + $("#vendor-results").html("

" + data['text'] + " Or Request Authorization via Email

") $("#id_msg").val(data['msg_body']); $("#msgvendor").removeClass("hidden"); - } else { - $("#vendor-results").html("

" + data['text'] +"

"); - if (data['bypass']) { - $("#vendor-results").append("

Request Internal Validation for this Email

"); - } - } + } else { + $("#vendor-results").html("

" + data['text'] +"

"); + if (data['bypass']) { + $("#vendor-results").append("

Request Internal Validation for this Email

"); + } + } } - }); + }); }); }); diff --git a/vince/templates/vince/base.html b/vince/templates/vince/base.html index 6559584..7afb823 100644 --- a/vince/templates/vince/base.html +++ b/vince/templates/vince/base.html @@ -175,7 +175,7 @@
Vulnerability INformation and Coordination Environment
--> -
ATTENTION: VINCE web interface and API interfaces will be down for maintenance from 1200 EDT on Tuesday, March 19, 2024, until no later than 0900 EDT Wednesday, March 20, 2024.
+
diff --git a/vince/templates/vince/base_public.html b/vince/templates/vince/base_public.html index c4d61ce..35c4eb1 100644 --- a/vince/templates/vince/base_public.html +++ b/vince/templates/vince/base_public.html @@ -157,7 +157,7 @@
Vulnerability INformation and Coordination Environment
--> -
ATTENTION: VINCE web interface and API interfaces will be down for maintenance from 1200 EDT on Tuesday, March 19, 2024, until no later than 0900 EDT Wednesday, March 20, 2024.
+
diff --git a/vince/templates/vince/cr_table.html b/vince/templates/vince/cr_table.html index 4fbe1b0..8d5783e 100644 --- a/vince/templates/vince/cr_table.html +++ b/vince/templates/vince/cr_table.html @@ -69,12 +69,12 @@

{{ ticket.title }}

+ {% else %} {% if noshowdeps %}{% else %}{% endif %}{% endif %} {{ ticket.resolution|force_escape|smarter_urlize:75|linebreaksbr }}
@@ -101,20 +101,33 @@

{{ ticket.title }}

-
+
{{ ticket.get_assigned_to }} + + + +
+
+ + {{ ticket.get_assigned_to }} + + +
diff --git a/vince/templates/vince/include/tabs/case_original_report_tab.html b/vince/templates/vince/include/tabs/case_original_report_tab.html index c003823..9833fc2 100644 --- a/vince/templates/vince/include/tabs/case_original_report_tab.html +++ b/vince/templates/vince/include/tabs/case_original_report_tab.html @@ -47,12 +47,12 @@

{{ cr.title }}

+ {% else %} {% if noshowdeps %}{% else %}{% endif %}{% endif %} {{ cr.resolution|force_escape|smarter_urlize:75|linebreaksbr }}
@@ -321,4 +321,4 @@
{% endif %} - \ No newline at end of file + diff --git a/vince/templates/vince/include/ticket_comment.html b/vince/templates/vince/include/ticket_comment.html index 5a204d9..6ce1865 100644 --- a/vince/templates/vince/include/ticket_comment.html +++ b/vince/templates/vince/include/ticket_comment.html @@ -16,40 +16,40 @@

Comment

{% if not ticket.can_be_resolved %}
{% trans "This ticket cannot be resolved or closed until the tickets it depends on are resolved." %}
{% endif %} - {% ifequal ticket.status 1 %} + {% if ticket.status == 1 %} - {% endifequal %} - {% ifequal ticket.status 2 %} + {% endif %} + {% if ticket.status == 2 %} - {% endifequal %} - {% ifequal ticket.status 3 %} + {% endif %} + {% if ticket.status == 3 %} - {% endifequal %} - {% ifequal ticket.status 4 %} + {% endif %} + {% if ticket.status == 4 %} - {% endifequal %} - {% ifequal ticket.status 5 %} + {% endif %} + {% if ticket.status == 5 %} - {% endifequal %} - {% ifequal ticket.status 6 %} + {% endif %} + {% if ticket.status == 6 %} - {% endifequal %} + {% endif %}
{% endif %} {% if case %} diff --git a/vince/templates/vince/ticket.html b/vince/templates/vince/ticket.html index 7a28b6c..a70d08a 100644 --- a/vince/templates/vince/ticket.html +++ b/vince/templates/vince/ticket.html @@ -24,7 +24,7 @@

Ticket [{{ ticket.queue }}-{{ ticket.id }}] {% if ticket.case %}({{ ticket.case.vu_vuid }}){% endif %} {% autoescape off %}{{ ticket.get_status_html }} {% if ticket.get_status_display == "Closed" %}{{ ticket.get_close_status_html }}{% endif %}{% endautoescape %}

- Vendor Association + Vendor Association {% if ticket.case %} View case {% if vincecomm_link %} diff --git a/vince/templates/vince/ticket_activity.html b/vince/templates/vince/ticket_activity.html index eeac2a5..6aa150b 100644 --- a/vince/templates/vince/ticket_activity.html +++ b/vince/templates/vince/ticket_activity.html @@ -1,104 +1,112 @@ {% load i18n humanize dashboard_tags %} {% if ticket.followup_set.all %} -{% load ticket_to_link %} -
-
-

- {% trans "Activity" %} - - {% if more %} - [Showing {{ ticket.MAX_ACTIVITY }} of - {{ ticket.followup_set.count }}] - - {% else %} - [{{ ticket.followup_set.count }}] - {% endif %} - -

-
-
-
-
-
-
- {% for followup in ticket.get_actions %} -
- -
- {% autoescape off %} - {% if followup.user %} - {{ followup.user|vtuserlogo:"profile-pic" }} - {% else %} - {{ followup.html_logo }} - {% endif %} - {% endautoescape %} -
-
-

{{ followup.title|escape|email_to_user }} {% if followup.title in "Comment,Closed" and followup.user == user %} {% elif "Email" in followup.title %}{% if followup.email_id %}{% endif %}{% endif %}

-

- {% if followup.comment|is_json %} -

-
-
- {{ followup.comment }} -
+ {% load ticket_to_link %} +
+
+

+ {% trans "Activity" %} + + {% if more %} + [Showing {{ ticket.MAX_ACTIVITY }} of + {{ ticket.followup_set.count }}] + + {% else %} + [{{ ticket.followup_set.count }}] + {% endif %} + +

-
- {% elif followup.comment %} -
-
-
- {{ followup.comment|force_escape|smarter_urlize:50|linebreaksbr }} -
-
-
Show More
-
Show Less
- -
- {% endif %} - {% if contact_link %} - View changes. - {% endif %} -

- {% for change in followup.ticketchange_set.all %} - {% if forloop.first %}
    {% endif %} -
  • {% blocktrans with change.field as field and change.old_value as old_value and change.new_value as new_value %}Changed {{field }} from {{ old_value }} to {{ new_value }}.{% endblocktrans %}
  • - {% if forloop.last %}
{% endif %} - {% endfor %} - {% for attachment in followup.attachment_set.all %}{% if forloop.first %}
    {% endif %} -
  • {{ attachment.filename }} ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }}) -
  • - {% if forloop.last %}
{% endif %} - {% endfor %} - {% if "commented on report" in followup.title %} - - {% endif %} - {% for message in followup.followupmessage_set.all %} - - {% endfor %} - {% if followup.title|review:followup and followup.ticket.review %} - - {% endif %} - - {% if followup.is_email %} -
- -
- {% endif %} -
+
+
+
+
+
+ {% for followup in ticket.get_actions %} +
+ +
+ {% autoescape off %} + {% if followup.user %} + {{ followup.user|vtuserlogo:"profile-pic" }} + {% else %} + {{ followup.html_logo }} + {% endif %} + {% endautoescape %} +
+
+

{{ followup.title|escape|email_to_user }} {% if followup.title in "Comment,Closed" and followup.user == user %} {% elif "Email" in followup.title %}{% if followup.email_id %}{% endif %}{% endif %}

+

+ {% if followup.comment|is_json %} +

+
+
+ {{ followup.comment }} +
+
+
+ {% elif followup.comment %} +
+
+
+ {{ followup.comment|force_escape|smarter_urlize:50|linebreaksbr }} +
+
+
Show More
+
Show Less
+ +
+ {% endif %} + {% if contact_link %} + View changes. + {% endif %} +

+ {% for change in followup.ticketchange_set.all %} + {% if forloop.first %}
    {% endif %} +
  • {% blocktrans with change.field as field and change.old_value as old_value and change.new_value as new_value %}Changed {{field }} from {{ old_value }} to {{ new_value }}.{% endblocktrans %}
  • + {% if forloop.last %}
{% endif %} + {% endfor %} + {% for attachment in followup.attachment_set.all %} + {% if forloop.first %}
    {% endif %} +
  • {{ attachment.filename }} ({{ attachment.mime_type }}, {{ attachment.size|filesizeformat }})
  • + {% if forloop.last %}
{% endif %} + {% endfor %} + {% if "commented on report" in followup.title %} + + {% endif %} + {% for message in followup.followupmessage_set.all %} + + {% endfor %} + {% if followup.title|review:followup and followup.ticket.review %} + + {% endif %} + + {% if followup.is_email %} +
+ +
+ {% endif %} +
+
+ {% endfor %} +
+
+
- {% endfor %} -
-
-
-
{% endif %} diff --git a/vince/templates/vince/ticket_table.html b/vince/templates/vince/ticket_table.html index c0c0848..07b06ea 100644 --- a/vince/templates/vince/ticket_table.html +++ b/vince/templates/vince/ticket_table.html @@ -48,11 +48,11 @@

{{ ticket.title|escape|email_to_user }}

+ {% else %} {% endif %} {{ ticket.resolution|force_escape|smarter_urlize:75|linebreaksbr }}
diff --git a/vince/ticket_update.py b/vince/ticket_update.py index 3ac9f9a..bb7c93b 100644 --- a/vince/ticket_update.py +++ b/vince/ticket_update.py @@ -34,16 +34,26 @@ from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.utils import timezone -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from vince.lib import process_attachments, get_oof_users from vince.mailer import safe_template_context, send_ticket_mail -from vince.models import Ticket, FollowUp, TicketChange, TicketCC, UserRole, UserAssignmentWeight, CalendarEvent, VinceReminder +from vince.models import ( + Ticket, + FollowUp, + TicketChange, + TicketCC, + UserRole, + UserAssignmentWeight, + CalendarEvent, + VinceReminder, +) logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) + def unsubscribe_ticket(ticket, user): try: TicketCC.objects.get(user=user, ticket=ticket).delete() @@ -51,6 +61,7 @@ def unsubscribe_ticket(ticket, user): logger.debug(f"User {user} is not subscribed to {ticket}") pass + def subscribe_ticket(ticket, user): if not TicketCC.objects.filter(user=user, ticket=ticket): tf = TicketCC(ticket=ticket, user=user) @@ -59,8 +70,8 @@ def subscribe_ticket(ticket, user): logger.debug(f"User {user} already subscribe to ticket {ticket}") -#this algorithm is based on the smooth weighted round robin here: -#https://github.com/nginx/nginx/commit/52327e0627f49dbda1e8db695e63a4b0af4448b1 +# this algorithm is based on the smooth weighted round robin here: +# https://github.com/nginx/nginx/commit/52327e0627f49dbda1e8db695e63a4b0af4448b1 def get_next_assignment(data): if len(data) == 0: return None @@ -69,7 +80,7 @@ def get_next_assignment(data): total_weight = 0 result = None - + for entry in data: entry.current_weight += entry.effective_weight total_weight += entry.effective_weight @@ -81,59 +92,57 @@ def get_next_assignment(data): if not result: # this should be unreachable, but check anyway logger.warning("Auto Assignment error") return None - + result.current_weight -= total_weight result.save() return result.user - + def auto_assignment(role, exclude=None): - #get users for this role + # get users for this role users = UserAssignmentWeight.objects.filter(role__id=role) - #are any of these users OOF today? + # are any of these users OOF today? oof_users = get_oof_users() if oof_users: users = users.exclude(user__in=oof_users) if exclude: - #these are users that should be excluded bc they are requesting it (ex. vulnote approval) + # these are users that should be excluded bc they are requesting it (ex. vulnote approval) users = users.exclude(user=exclude) - + if users: return get_next_assignment(users) return None - -def update_ticket(request, ticket_id): +def update_ticket(request, ticket_id): ticket = get_object_or_404(Ticket, id=ticket_id) logger.debug(f"Updating ticket: {ticket.id}") logger.debug(f"Ticket update post: {request.POST}") - date_re = re.compile( - r'(?P\d{1,2})/(?P\d{1,2})/(?P\d{4})$') - - comment = request.POST.get('comment', '') - new_status = int(request.POST.get('new_status', ticket.status)) - title = request.POST.get('title', '') - owner = int(request.POST.get('owner', -1)) - priority = int(request.POST.get('priority', ticket.priority)) - due_date_year = int(request.POST.get('due_date_year', 0)) - due_date_month = int(request.POST.get('due_date_month', 0)) - due_date_day = int(request.POST.get('due_date_day', 0)) - subscribe = bool(request.POST.get('subscribe', False)) - unsubscribe = bool(request.POST.get('unsubscribe', False)) - auto_assign = int(request.POST.get('auto', 0)) - ticket_status_changed=False + date_re = re.compile(r"(?P\d{1,2})/(?P\d{1,2})/(?P\d{4})$") + + comment = request.POST.get("comment", "") + new_status = int(request.POST.get("new_status", ticket.status)) + title = request.POST.get("title", "") + owner = int(request.POST.get("owner", -1)) + priority = int(request.POST.get("priority", ticket.priority)) + due_date_year = int(request.POST.get("due_date_year", 0)) + due_date_month = int(request.POST.get("due_date_month", 0)) + due_date_day = int(request.POST.get("due_date_day", 0)) + subscribe = bool(request.POST.get("subscribe", False)) + unsubscribe = bool(request.POST.get("unsubscribe", False)) + auto_assign = int(request.POST.get("auto", 0)) + ticket_status_changed = False # NOTE: jQuery's default for dates is mm/dd/yy # very US-centric but for now that's the only format supported # until we clean up code to internationalize a little more - due_date = request.POST.get('due_date', None) or None + due_date = request.POST.get("due_date", None) or None if due_date is not None: # based on Django code to parse dates: @@ -154,17 +163,19 @@ def update_ticket(request, ticket_id): due_date = timezone.now() due_date = due_date.replace(due_date_year, due_date_month, due_date_day) - - no_changes = all([ - not request.FILES, - not comment, - new_status == ticket.status, - title == ticket.title or title == '', - priority == int(ticket.priority), - due_date == ticket.due_date, - (owner == -1) or (not owner and not ticket.assigned_to) or - (owner and User.objects.get(id=owner) == ticket.assigned_to), - ]) + no_changes = all( + [ + not request.FILES, + not comment, + new_status == ticket.status, + title == ticket.title or title == "", + priority == int(ticket.priority), + due_date == ticket.due_date, + (owner == -1) + or (not owner and not ticket.assigned_to) + or (owner and User.objects.get(id=owner) == ticket.assigned_to), + ] + ) # Only change to the ticket is toggling the watcher if subscribe: @@ -182,26 +193,26 @@ def update_ticket(request, ticket_id): logger.debug(f"No changes to ticket {ticket.id}. Returning") return return_to_ticket(request.user, ticket) - - # We need to allow the 'ticket' and 'queue' contexts to be applied to the # comment. context = safe_template_context(ticket) from django.template import engines - template_func = engines['django'].from_string + + template_func = engines["django"].from_string # this prevents system from trying to render any template tags # broken into two stages to prevent changes from first replace being themselves # changed by the second replace due to conflicting syntax - comment = comment.replace('{%', 'X-HELPDESK-COMMENT-VERBATIM').replace('%}', 'X-HELPDESK-COMMENT-ENDVERBATIM') - comment = comment.replace('X-HELPDESK-COMMENT-VERBATIM', '{% verbatim %}{%').replace('X-HELPDESK-COMMENT-ENDVERBATIM', '%}{% endverbatim %}') + comment = comment.replace("{%", "X-HELPDESK-COMMENT-VERBATIM").replace("%}", "X-HELPDESK-COMMENT-ENDVERBATIM") + comment = comment.replace("X-HELPDESK-COMMENT-VERBATIM", "{% verbatim %}{%").replace( + "X-HELPDESK-COMMENT-ENDVERBATIM", "%}{% endverbatim %}" + ) # render the neutralized template comment = template_func(comment).render(context) if owner == -1 and ticket.assigned_to: owner = ticket.assigned_to.id - f = FollowUp(ticket=ticket, date=timezone.now(), comment=comment) f.user = request.user @@ -212,21 +223,21 @@ def update_ticket(request, ticket_id): if owner != 0 and ((ticket.assigned_to and owner != ticket.assigned_to.id) or not ticket.assigned_to): new_user = User.objects.get(id=owner) if auto_assign: - f.title = _('Auto Assigned to %(username)s') % { - 'username': new_user.get_username(), + f.title = _("Auto Assigned to %(username)s") % { + "username": new_user.get_username(), } else: - f.title = _('Assigned to %(username)s') % { - 'username': new_user.get_username(), + f.title = _("Assigned to %(username)s") % { + "username": new_user.get_username(), } ticket.assigned_to = new_user reassigned = True # user changed owner to 'unassign' elif owner == 0 and ticket.assigned_to is not None: - f.title = _('Unassigned') + f.title = _("Unassigned") ticket.assigned_to = None elif owner == -2: - #AUTO ASSIGN + # AUTO ASSIGN pass old_status_str = ticket.get_status_display() @@ -237,15 +248,15 @@ def update_ticket(request, ticket_id): f.new_status = new_status ticket_status_changed = True if f.title: - f.title += ' and %s' % ticket.get_status_display() + f.title += " and %s" % ticket.get_status_display() else: - f.title = '%s' % ticket.get_status_display() + f.title = "%s" % ticket.get_status_display() if not f.title: if f.comment: - f.title = _('Comment') + f.title = _("Comment") else: - f.title = _('Updated') + f.title = _("Updated") # Todo update this f.save() @@ -253,13 +264,13 @@ def update_ticket(request, ticket_id): # if reassignment, followup save will prevent sending emails to new # assignee even though the ticket.assigned_to hasn't been saved # the signal might take longer to process - - files = process_attachments(f, request.FILES.getlist('attachment')) + + files = process_attachments(f, request.FILES.getlist("attachment")) if title and title != ticket.title: c = TicketChange( followup=f, - field=_('Title'), + field=_("Title"), old_value=ticket.title, new_value=title, ) @@ -270,7 +281,7 @@ def update_ticket(request, ticket_id): logger.debug("IN TICKET CHANGE STATUS") c = TicketChange( followup=f, - field=_('Status'), + field=_("Status"), old_value=old_status_str, new_value=ticket.get_status_display(), ) @@ -279,7 +290,7 @@ def update_ticket(request, ticket_id): if ticket.assigned_to != old_owner: c = TicketChange( followup=f, - field=_('Owner'), + field=_("Owner"), old_value=old_owner, new_value=ticket.assigned_to, ) @@ -288,7 +299,7 @@ def update_ticket(request, ticket_id): if priority != ticket.priority: c = TicketChange( followup=f, - field=_('Priority'), + field=_("Priority"), old_value=ticket.priority, new_value=priority, ) @@ -298,7 +309,7 @@ def update_ticket(request, ticket_id): if due_date != ticket.due_date: c = TicketChange( followup=f, - field=_('Due on'), + field=_("Due on"), old_value=ticket.due_date, new_value=due_date, ) @@ -320,21 +331,18 @@ def update_ticket(request, ticket_id): ) # Send comment or new status emails to the submitter and the cc - if (f.comment or ( - f.new_status in (Ticket.RESOLVED_STATUS, - Ticket.CLOSED_STATUS))): + if f.comment or (f.new_status in (Ticket.RESOLVED_STATUS, Ticket.CLOSED_STATUS)): if f.new_status == Ticket.RESOLVED_STATUS: - template = 'resolved_' + template = "resolved_" elif f.new_status == Ticket.CLOSED_STATUS: - template = 'closed_' + template = "closed_" else: - template = 'updated_' - - template_suffix = 'submitter' + template = "updated_" + template_suffix = "submitter" if ticket.submitter_email != request.user.email: - #Is this a vincetrack user? + # Is this a vincetrack user? vt_user = User.objects.filter(username=ticket.submitter_email).first() if vt_user: send_ticket_mail( @@ -347,33 +355,32 @@ def update_ticket(request, ticket_id): ) messages_sent_to.append(ticket.submitter_email) - template_suffix = 'cc' + template_suffix = "cc" messages_sent_to = email_ticketcc(ticket, template + template_suffix, context, files, messages_sent_to) # Send email to ticket owner (assignee) - if ticket.assigned_to and \ - request.user != ticket.assigned_to and \ - ticket.assigned_to.email and \ - ticket.assigned_to.email not in messages_sent_to: + if ( + ticket.assigned_to + and request.user != ticket.assigned_to + and ticket.assigned_to.email + and ticket.assigned_to.email not in messages_sent_to + ): # We only send e-mails to staff members if the ticket is updated by # another user. The actual template varies, depending on what has been # changed. if reassigned: - template_staff = 'assigned_owner' + template_staff = "assigned_owner" elif f.new_status == Ticket.RESOLVED_STATUS: - template_staff = 'resolved_owner' + template_staff = "resolved_owner" elif f.new_status == Ticket.CLOSED_STATUS: - template_staff = 'closed_owner' + template_staff = "closed_owner" else: - template_staff = 'updated_owner' - - if (not reassigned or - (reassigned and - ticket.assigned_to.usersettings.settings.get( - 'email_on_ticket_assign', False))) or \ - (not reassigned and - ticket.assigned_to.usersettings.settings.get( - 'email_on_ticket_change', False)): + template_staff = "updated_owner" + + if ( + not reassigned + or (reassigned and ticket.assigned_to.usersettings.settings.get("email_on_ticket_assign", False)) + ) or (not reassigned and ticket.assigned_to.usersettings.settings.get("email_on_ticket_change", False)): send_ticket_mail( template_staff, context, @@ -390,22 +397,21 @@ def update_ticket(request, ticket_id): ticket.assigned_to = request.user c = TicketChange( followup=f, - field=_('Owner'), + field=_("Owner"), old_value=None, new_value=ticket.assigned_to, ) c.save() - #if ticket unassigned and ticket status changed + # if ticket unassigned and ticket status changed # auto assign person closing ticket ticket.save() if f.new_status == Ticket.CLOSED_STATUS: - #check to see if there are reminders about this ticket + # check to see if there are reminders about this ticket reminders = VinceReminder.objects.filter(ticket=ticket) for r in reminders: r.delete() - return return_to_ticket(request.user, ticket) @@ -414,6 +420,7 @@ def return_to_ticket(user, ticket): """Helper function for update_ticket""" return HttpResponseRedirect(ticket.get_absolute_url()) + def email_ticketcc(ticket, template, context, files, messages_sent_to): # Send email to people that are cc'd on ticket (watch list) for cc in ticket.ticketcc_set.all(): @@ -429,4 +436,3 @@ def email_ticketcc(ticket, template, context, files, messages_sent_to): messages_sent_to.append(cc.email_address) return messages_sent_to - diff --git a/vince/views.py b/vince/views.py index 4975825..33805a3 100644 --- a/vince/views.py +++ b/vince/views.py @@ -99,7 +99,7 @@ from django.urls import reverse from django.forms.utils import ErrorList from django.utils.decorators import method_decorator -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views import generic from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.generic.edit import FormView, FormMixin @@ -167,7 +167,7 @@ from bigvince.storage_backends import PrivateMediaStorage from vince.settings import VULNOTE_TEMPLATE from collections import OrderedDict -from django.utils.http import is_safe_url +from django.utils.http import url_has_allowed_host_and_scheme as is_safe_url from django.utils import timezone from lib.vince import utils as vinceutils from rest_framework.views import APIView @@ -625,10 +625,10 @@ def calendar_events(request): def autocomplete_casevendors(request, pk): case = get_object_or_404(VulnerabilityCase, id=pk) page = request.GET.get("page", 1) - size = request.GET.get("size", 20) + size = request.GET.get("size", 0) vendors = VulnerableVendor.casevendors(case).order_by("vendor") user_filter = False - logger.debug(request.GET) + logger.debug(f"in autocomplete_casevendors, request.GET is {request.GET}") for key in request.GET: if "field" in key: field = request.GET[key] @@ -663,29 +663,33 @@ def autocomplete_casevendors(request, pk): elif field == "users": user_filter = True + if size < 1: + size = vendors.count() paginator = Paginator(vendors, size) # Until Nov 8 2023, we had this pagination calculated on the backend. Now, we calculate it using Tabulator ('pagination: "local"') in vince/case.js. # With corresponding changes in case.js, this can be reversed by switching back to vendorsjs = [obj.as_dict() for obj in paginator.page(page)] here. # vendorsjs = [obj.as_dict() for obj in paginator.page(page)] - vendorsjs = [obj.as_dict() for obj in vendors] - logger.debug(f"vendorsjs is {vendorsjs}") + + vendorsjs = [] alert_tags = list(TagManager.objects.filter(tag_type=2, alert_on_add=True).values_list("tag", flat=True)) logger.debug(f"ALERT TAGS: {alert_tags}") - for vjs in vendorsjs: + for vendor in paginator.page(page): + vjs = vendor.as_dict() + vendorsjs.append(vjs) cid = vjs["contact_id"] vc_contact = VinceCommContact.objects.using("vincecomm").filter(vendor_id=cid).first() if vc_contact: groupcontact = GroupContact.objects.using("vincecomm").filter(contact=vc_contact).first() if groupcontact: - count = User.objects.using("vincecomm").filter(groups=groupcontact.group).count() - vjs.update({"users": count}) + hasusers = User.objects.using("vincecomm").filter(groups=groupcontact.group).exists() + vjs.update({"users": hasusers}) # check alert tags if vjs["alert_tags"] and alert_tags: vjs["alert_tags"] = list(set(vjs["alert_tags"]) & set(alert_tags)) continue - vjs.update({"users": 0}) + vjs.update({"users": False}) # check alert tags if vjs["alert_tags"] and alert_tags: vjs["alert_tags"] = list(set(vjs["alert_tags"]) & set(alert_tags)) @@ -9402,6 +9406,7 @@ def get_context_data(self, **kwargs): # do we have a user verficiation template? initial = {} if self.kwargs.get("pk"): + logger.debug(f"ContactVerifyInit has a pk and it is {self.kwargs.get('pk')}") contact = get_object_or_404(Contact, id=self.kwargs.get("pk")) initial["contact"] = contact.vendor_name # get emails for this contact @@ -9430,12 +9435,15 @@ def get_context_data(self, **kwargs): ca = ContactAssociation.objects.filter(ticket=ticket).first() if ca: context["ca"] = ca + initial["contact"] = ca.contact.vendor_name tmpl = EmailTemplate.objects.filter(template_name="user_verification").first() if tmpl: team_sig = get_team_sig(self.request.user) initial["email_body"] = tmpl.plain_text.replace("[team_signature]", team_sig) initial["subject"] = tmpl.subject + logger.debug(f"initial is {initial}") + logger.debug(f"context is {context}") if context.get("ca"): context["form"] = InitContactForm(initial=initial, instance=context["ca"]) else: @@ -14941,9 +14949,16 @@ def get_context_data(self, **kwargs): context["show_next"] = 1 elif year < datetime.now().year: context["show_next"] = 1 - context["total_tickets"] = Ticket.objects.filter( - queue__in=my_queues, created__year=year, created__month=month - ).count() + + ticketdata = Ticket.objects.filter(queue__in=my_queues, created__year=year, created__month=month).aggregate( + total_tickets=Count("id"), total_closed=Count("id", filter=Q(status=Ticket.CLOSED_STATUS)) + ) + logger.debug(f"ticketdata is {ticketdata}") + context["total_tickets"] = ticketdata["total_tickets"] + context["total_closed"] = ticketdata["total_closed"] + # context["total_tickets"] = Ticket.objects.filter( + # queue__in=my_queues, created__year=year, created__month=month + # ).count() context["ticket_stats"] = ( Ticket.objects.filter(queue__in=my_queues, created__year=year, created__month=month) .values("queue__title") @@ -14951,9 +14966,9 @@ def get_context_data(self, **kwargs): .annotate(count=Count("queue__title")) .order_by("-count") ) - context["total_closed"] = Ticket.objects.filter( - queue__in=my_queues, created__year=year, created__month=month, status=Ticket.CLOSED_STATUS - ).count() + # context["total_closed"] = Ticket.objects.filter( + # queue__in=my_queues, created__year=year, created__month=month, status=Ticket.CLOSED_STATUS + # ).count() context["closed_ticket_stats"] = ( Ticket.objects.filter( queue__in=my_queues, created__year=year, created__month=month, status=Ticket.CLOSED_STATUS @@ -15063,6 +15078,7 @@ def get_context_data(self, **kwargs): title__icontains="Successfully forwarded", ticket__queue__in=my_queues, date__month=month, date__year=year ) + logger.debug(f"ReportsView context is {context}") return context diff --git a/vincecommworker/urls.py b/vincecommworker/urls.py index 6662c61..5299228 100644 --- a/vincecommworker/urls.py +++ b/vincecommworker/urls.py @@ -41,11 +41,11 @@ 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ -from django.conf.urls import url +from django.urls import include, re_path from vincecommworker.views import vincecomm_send_email, send_daily_digest urlpatterns = [ - url(r'^ingest/$', vincecomm_send_email, name='comm_email'), - url(r'^daily/$', send_daily_digest, name='daily_digest'), + re_path(r"^ingest/$", vincecomm_send_email, name="comm_email"), + re_path(r"^daily/$", send_daily_digest, name="daily_digest"), ] diff --git a/vincepub/templates/vincepub/base_public.html b/vincepub/templates/vincepub/base_public.html index e1b4869..6e50b98 100644 --- a/vincepub/templates/vincepub/base_public.html +++ b/vincepub/templates/vincepub/base_public.html @@ -41,7 +41,7 @@ -
ATTENTION: VINCE web interface and API interfaces will be down for maintenance from 1200 EDT on Tuesday, March 19, 2024, until no later than 0900 EDT Wednesday, March 20, 2024.
+
diff --git a/vincepub/views.py b/vincepub/views.py index cee581a..4c76545 100644 --- a/vincepub/views.py +++ b/vincepub/views.py @@ -44,7 +44,7 @@ from django.contrib.postgres.search import SearchVector, SearchRank from django.urls import reverse from django.utils import timezone -from django.utils.encoding import force_text +from django.utils.encoding import force_str as force_text from django.core.paginator import Paginator from django.db import connection from django.db.models import Q diff --git a/vinceworker/urls.py b/vinceworker/urls.py index 64dc53f..1b02500 100644 --- a/vinceworker/urls.py +++ b/vinceworker/urls.py @@ -41,12 +41,12 @@ 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ -from django.conf.urls import url +from django.urls import include, re_path from vinceworker.views import ingest_vulreport, send_daily_digest, generate_reminders, send_weekly_report urlpatterns = [ - url(r'^ingest-vulreport/$', ingest_vulreport, name='ingest_vulreport'), - url(r'^daily/$', send_daily_digest, name='daily_digest'), - url(r'^reminder/$', generate_reminders, name="reminders"), - url(r'^weeklyreport/$', send_weekly_report, name='weeklyreport'), + re_path(r"^ingest-vulreport/$", ingest_vulreport, name="ingest_vulreport"), + re_path(r"^daily/$", send_daily_digest, name="daily_digest"), + re_path(r"^reminder/$", generate_reminders, name="reminders"), + re_path(r"^weeklyreport/$", send_weekly_report, name="weeklyreport"), ] diff --git a/vinny/__init__.py b/vinny/__init__.py index 2aad82d..72a81e8 100644 --- a/vinny/__init__.py +++ b/vinny/__init__.py @@ -27,6 +27,7 @@ # DM21-1126 ######################################################################## from __future__ import absolute_import, unicode_literals +import django -default_app_config = 'vinny.apps.VinceCommConfig' - +if django.VERSION < (3, 2): + default_app_config = "vinny.apps.VinceCommConfig" diff --git a/vinny/forms.py b/vinny/forms.py index 2fe7bc4..3bf6ca8 100644 --- a/vinny/forms.py +++ b/vinny/forms.py @@ -41,7 +41,7 @@ from django.core.exceptions import ValidationError import mimetypes import os -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str as smart_text from vinny.settings import DEFAULT_USER_SETTINGS from django.urls import reverse, reverse_lazy import base64 diff --git a/vinny/models.py b/vinny/models.py index eb14c79..7a7c1e5 100644 --- a/vinny/models.py +++ b/vinny/models.py @@ -27,8 +27,9 @@ # DM21-1126 ######################################################################## from django.db import models, transaction + # Create your models here. -from django.utils.translation import ugettext, gettext_lazy as _ +from django.utils.translation import gettext, gettext_lazy as _ from django.contrib.auth.models import User, Group from django.conf import settings from django.contrib.postgres.indexes import GinIndex @@ -40,12 +41,13 @@ from django.urls import reverse from django.contrib.auth.hashers import make_password import logging -#from .signals import message_sent + +# from .signals import message_sent from .utils import cached_attribute from django.utils.functional import cached_property from vinny.mailer import send_newmessage_mail from bigvince.storage_backends import PrivateMediaStorage, SharedMediaStorage -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str as smart_text from lib.vince.m2crypto_encrypt_decrypt import ED import base64 import os @@ -61,44 +63,43 @@ from lib.vince import utils as vinceutils from django.db.models import JSONField + class OldJSONField(JSONField): - """ This was due to legacy support in Django 2.2. from_db_value - should be explicitily sepcified when extending JSONField """ + """This was due to legacy support in Django 2.2. from_db_value + should be explicitily sepcified when extending JSONField""" def db_type(self, connection): - return 'json' + return "json" def from_db_value(self, value, expression, connection): return value + logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -message_sent = Signal(providing_args=["message", "thread", "reply"]) +message_sent = Signal() +# providing_args=["message", "thread", "reply"]) + def random_logo_color(): - return "#"+''.join([random.choice('0123456789ABCDEF') for j in range(6)]) + return "#" + "".join([random.choice("0123456789ABCDEF") for j in range(6)]) + def generate_uuid(): return uuid.uuid1() + class VinceAPIToken(models.Model): """ The default authorization token model """ - key = models.CharField( - _("Key"), - max_length=250, - primary_key=True) - - user = models.OneToOneField( - settings.AUTH_USER_MODEL, - related_name="auth_token", - on_delete=models.CASCADE) - created = models.DateTimeField( - _("Created"), - auto_now_add=True) + key = models.CharField(_("Key"), max_length=250, primary_key=True) + + user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="auth_token", on_delete=models.CASCADE) + + created = models.DateTimeField(_("Created"), auto_now_add=True) class Meta: verbose_name = _("Token") @@ -120,30 +121,31 @@ def __str__(self): class VinceProfile(models.Model): - user = models.OneToOneField(settings.AUTH_USER_MODEL, - related_name="vinceprofile", - on_delete=models.CASCADE) + user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="vinceprofile", on_delete=models.CASCADE) org = models.CharField(max_length=250, blank=True, null=True) preferred_username = models.CharField(max_length=250, blank=True, null=True) country = CountryField(blank=True, null=True, default="US") email_verified = models.BooleanField(default=False) title = models.CharField(max_length=200, blank=True, null=True) logocolor = models.CharField(max_length=10, default=random_logo_color) - #if we don't know this user, the user must go into pending mode before they can login + # if we don't know this user, the user must go into pending mode before they can login api_key = models.CharField(max_length=256, blank=True, null=True) pending = models.BooleanField(default=True) ignored = models.BooleanField(default=False) service = models.BooleanField(default=False) multifactor = models.BooleanField(default=False) - timezone = models.CharField(default='UTC', max_length=100) + timezone = models.CharField(default="UTC", max_length=100) settings_pickled = models.TextField( - _('Settings Dictionary'), - help_text=_('This is a base64-encoded representation of a ' - 'pickled Python dictionary.' - 'Do not change this field via the admin.'), + _("Settings Dictionary"), + help_text=_( + "This is a base64-encoded representation of a " + "pickled Python dictionary." + "Do not change this field via the admin." + ), blank=True, - null=True, + null=True, ) + def _get_username(self): if self.preferred_username: return self.preferred_username @@ -153,21 +155,21 @@ def _get_username(self): vince_username = property(_get_username) def _get_track_access(self): - return self.user.groups.filter(name='vincetrack').exists() + return self.user.groups.filter(name="vincetrack").exists() is_track = property(_get_track_access) - + def _get_url(self): ed = ED(base64.b64encode(settings.SECRET_KEY.encode())) euid = ed.encrypt(str(self.user.pk)) return reverse("vinny:usercard", args=[euid]) url = property(_get_url) - + def _get_logo(self): groups = self.user.groups.exclude(groupcontact__isnull=True) if len(groups) >= 1: - logo_groups = groups.exclude(Q(groupcontact__logo='')|Q(groupcontact__logo=None)) + logo_groups = groups.exclude(Q(groupcontact__logo="") | Q(groupcontact__logo=None)) if len(logo_groups) >= 1: return logo_groups[0].groupcontact.get_logo() return None @@ -175,7 +177,9 @@ def _get_logo(self): logo = property(_get_logo) def _get_vendor_status(self): - groups = self.user.groups.filter(groupcontact__contact__active=True).exclude(groupcontact__contact__isnull=True) + groups = self.user.groups.filter(groupcontact__contact__active=True).exclude( + groupcontact__contact__isnull=True + ) if len(groups) >= 1: vendor_groups = groups.exclude(groupcontact__contact__vendor_type__in=["Contact", "User"]) if len(vendor_groups) >= 1: @@ -185,13 +189,15 @@ def _get_vendor_status(self): is_vendor = property(_get_vendor_status) def _get_admin_status(self): - admin = VinceCommGroupAdmin.objects.filter(email__email=self.user.email, contact__active=True, contact__vendor_type__in=["Coordinator", "Vendor"]) + admin = VinceCommGroupAdmin.objects.filter( + email__email=self.user.email, contact__active=True, contact__vendor_type__in=["Coordinator", "Vendor"] + ) if admin: return True return False is_vendor_admin = property(_get_admin_status) - + def __str__(self): if self.preferred_username: return self.preferred_username @@ -199,7 +205,7 @@ def __str__(self): return self.user.get_full_name() name = property(__str__) - + def _first_initial(self): if self.preferred_username: return self.preferred_username[0] @@ -209,7 +215,7 @@ def _first_initial(self): return "?" initial = property(_first_initial) - + def _get_association(self): if self.real_org: return self.real_org @@ -228,10 +234,10 @@ def _get_modified(self): return timezone.now() modified = property(_get_modified) - + def _set_settings(self, data): # data should always be a Python dictionary. - if not isinstance(data,dict): + if not isinstance(data, dict): print("Non dictionary item sent to pickle %s" % str(data)) logger.warn("Non dictionary item sent to pickle %s" % str(data)) data = {} @@ -240,34 +246,37 @@ def _set_settings(self, data): except ImportError: import cPickle as pickle from base64 import encodebytes as b64encode + self.settings_pickled = b64encode(pickle.dumps(data)).decode() - + def _get_settings(self): # return a python dictionary representing the pickled data. try: import pickle except ImportError: import cPickle as pickle + class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module, name): - """ If find_class gets called then return error """ - raise pickle.UnpicklingError("global '%s.%s' is forbidden" % - (module, name)) + """If find_class gets called then return error""" + raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) + try: from base64 import decodebytes as b64decode + if self.settings_pickled: - s = b64decode(self.settings_pickled.encode('utf-8')) - #replacement for pickle.loads() + s = b64decode(self.settings_pickled.encode("utf-8")) + # replacement for pickle.loads() return RestrictedUnpickler(io.BytesIO(s)).load() else: return {} except (pickle.UnpicklingError, AttributeError) as e: - print("Error when trying to unpickle data %s " %(str(e))) - logger.warn("Error when trying to unpickle data %s " %(str(e))) + print("Error when trying to unpickle data %s " % (str(e))) + logger.warn("Error when trying to unpickle data %s " % (str(e))) return {} except Exception as e: - print("Generic error when trying to unpickle data %s " %(str(e))) - logger.warn("Generic error when trying to unpickle data %s " %(str(e))) + print("Generic error when trying to unpickle data %s " % (str(e))) + logger.warn("Generic error when trying to unpickle data %s " % (str(e))) return {} settings = property(_get_settings, _set_settings) @@ -275,14 +284,15 @@ def find_class(self, module, name): @property @cached_attribute def real_org(self): - groups = self.user.groups.filter(groupcontact__contact__vendor_type="Vendor").exclude(groupcontact__isnull=True) + groups = self.user.groups.filter(groupcontact__contact__vendor_type="Vendor").exclude( + groupcontact__isnull=True + ) my_groups = [] for ug in groups: my_groups.append(ug.groupcontact.contact.vendor_name) return ", ".join(my_groups) - """ def get_username(self): if self.vinceprofile.preferred_username: @@ -293,18 +303,16 @@ def get_username(self): User.add_to_class("__str__", get_username) """ + class VinceCommContact(models.Model): VENDOR_TYPE = ( - ('Contact', 'Contact'), - ('Vendor', 'Vendor'), - ('User', 'User'), - ('Coordinator', 'Coordinator'), + ("Contact", "Contact"), + ("Vendor", "Vendor"), + ("User", "User"), + ("Coordinator", "Coordinator"), ) - LOCATION_CHOICES=( - ('Domestic', 'Domestic'), - ('International', 'International') - ) + LOCATION_CHOICES = (("Domestic", "Domestic"), ("International", "International")) vendor_id = models.IntegerField() vendor_name = models.CharField(max_length=100) @@ -314,20 +322,25 @@ class VinceCommContact(models.Model): location = models.CharField(max_length=15, choices=LOCATION_CHOICES, default="domestic") version = models.IntegerField(default=0) uuid = models.UUIDField(blank=True, null=True, editable=False) - + def __str__(self): return self.vendor_name def get_absolute_url(self): from django.urls import reverse - return reverse('vinny:contact', args=(self.id,)) + + return reverse("vinny:contact", args=(self.id,)) def get_emails(self): - email_contact = VinceCommEmail.objects.filter(contact=self, status=True).values_list('email', flat=True) + email_contact = VinceCommEmail.objects.filter(contact=self, status=True).values_list("email", flat=True) return list(email_contact) def get_list_email(self): - email_list = VinceCommEmail.objects.filter(contact=self, email_list=True, status=True, public=True).exclude(name__icontains='service').first() + email_list = ( + VinceCommEmail.objects.filter(contact=self, email_list=True, status=True, public=True) + .exclude(name__icontains="service") + .first() + ) if email_list: return email_list.email else: @@ -340,17 +353,18 @@ def get_phone_number(self): else: return "" + class VinceCommPostal(models.Model): ADDRESS_TYPE = ( - ('Home', 'Home'), - ('Work', 'Work'), - ('Other', 'Other'), - ('School', 'School'), + ("Home", "Home"), + ("Work", "Work"), + ("Other", "Other"), + ("School", "School"), ) contact = models.ForeignKey(VinceCommContact, on_delete=models.CASCADE) country = CountryField(blank=True, null=True, default="US") primary = models.BooleanField(default=True) - address_type = models.CharField(max_length=20, choices=ADDRESS_TYPE, default='Work') + address_type = models.CharField(max_length=20, choices=ADDRESS_TYPE, default="Work") street = models.CharField(max_length=150) street2 = models.CharField(max_length=150, blank=True, null=True) city = models.CharField(max_length=50) @@ -365,16 +379,16 @@ def __str__(self): class VinceCommPhone(models.Model): PHONE_TYPE = ( - ('Fax', 'Fax'), - ('Home', 'Home'), - ('Hotline', 'Hotline'), - ('Office', 'Office'), - ('Mobile', 'Mobile'), + ("Fax", "Fax"), + ("Home", "Home"), + ("Hotline", "Hotline"), + ("Office", "Office"), + ("Mobile", "Mobile"), ) contact = models.ForeignKey(VinceCommContact, on_delete=models.CASCADE) country_code = models.CharField(max_length=5, default="+1") phone = models.CharField(max_length=50) - phone_type = models.CharField(max_length=20, choices=PHONE_TYPE, default='Work') + phone_type = models.CharField(max_length=20, choices=PHONE_TYPE, default="Work") comment = models.CharField(max_length=200, blank=True, null=True) version = models.IntegerField(default=0) public = models.BooleanField(default=False) @@ -382,30 +396,21 @@ class VinceCommPhone(models.Model): def __str__(self): return "%s %s" % (country_code, phone) + class VinceCommEmail(models.Model): - EMAIL_FUNCTION=( - ('TO', 'TO'), - ('CC', 'CC'), - ('EMAIL', 'EMAIL'), - ('REPLYTO', 'REPLYTO') - ) - EMAIL_TYPE = ( - ('Work', 'Work'), - ('Other', 'Other'), - ('Home', 'Home'), - ('School', 'School') - ) + EMAIL_FUNCTION = (("TO", "TO"), ("CC", "CC"), ("EMAIL", "EMAIL"), ("REPLYTO", "REPLYTO")) + EMAIL_TYPE = (("Work", "Work"), ("Other", "Other"), ("Home", "Home"), ("School", "School")) contact = models.ForeignKey(VinceCommContact, on_delete=models.CASCADE) email = models.EmailField(max_length=254) - email_type = models.CharField(max_length=20, choices=EMAIL_TYPE, default='Work') + email_type = models.CharField(max_length=20, choices=EMAIL_TYPE, default="Work") name = models.CharField(max_length=200, blank=True, null=True) - email_function = models.CharField(max_length=10, choices=EMAIL_FUNCTION, default='TO') + email_function = models.CharField(max_length=10, choices=EMAIL_FUNCTION, default="TO") status = models.BooleanField(default=True) version = models.IntegerField(default=0) invited = models.BooleanField(default=False) public = models.BooleanField(default=False) email_list = models.BooleanField(default=True) - + def __str__(self): return self.email @@ -420,6 +425,7 @@ class VinceCommWebsite(models.Model): def __str__(self): return self.url + class VinceCommPgP(models.Model): contact = models.ForeignKey(VinceCommContact, on_delete=models.CASCADE) pgp_key_id = models.CharField(max_length=200) @@ -432,46 +438,30 @@ class VinceCommPgP(models.Model): pgp_protocol = models.CharField(max_length=30, default="GPG1 ARMOR MIME") version = models.IntegerField(default=0) public = models.BooleanField(default=False) - pgp_email = models.EmailField(max_length=254, - help_text=_('The email address that belongs with this PGP key'), - blank=True, null=True) + pgp_email = models.EmailField( + max_length=254, help_text=_("The email address that belongs with this PGP key"), blank=True, null=True + ) def __str__(self): return self.pgp_fingerprint class ContactInfoChange(models.Model): - contact = models.ForeignKey( - VinceCommContact, - on_delete=models.CASCADE - ) - model = models.CharField( - _('Model'), max_length=100 - ) - field = models.CharField( - _('Field'), max_length=100 - ) - old_value = models.TextField( - _('Old Value'), - blank=True, - null=True - ) - new_value = models.TextField( - _('New Value'), - blank=True, - null=True - ) - + contact = models.ForeignKey(VinceCommContact, on_delete=models.CASCADE) + model = models.CharField(_("Model"), max_length=100) + field = models.CharField(_("Field"), max_length=100) + old_value = models.TextField(_("Old Value"), blank=True, null=True) + new_value = models.TextField(_("New Value"), blank=True, null=True) + action = models.ForeignKey( "VendorAction", on_delete=models.CASCADE, blank=True, null=True, - verbose_name=_('Vendor Action'), + verbose_name=_("Vendor Action"), ) - approved = models.BooleanField( - default=False) - + approved = models.BooleanField(default=False) + def _get_ts(self): if self.action: return self.action.created @@ -479,52 +469,39 @@ def _get_ts(self): return timezone.now() action_ts = property(_get_ts) - + def __str__(self): - out = '%s: %s: %s ' % (self.action, self.model, self.field) + out = "%s: %s: %s " % (self.action, self.model, self.field) if not self.new_value: -# out += ugettext('removed') - out += '%s' % self.old_value + # out += ugettext('removed') + out += "%s" % self.old_value elif not self.old_value: - out += _('added %s') % self.new_value + out += _("added %s") % self.new_value else: out += _('changed from "%(old_value)s" to "%(new_value)s"') % { - 'old_value': self.old_value, - 'new_value': self.new_value + "old_value": self.old_value, + "new_value": self.new_value, } return out + def __repr__(self): return self.__str__() - + class GroupContact(models.Model): - group = models.OneToOneField( - Group, - on_delete=models.CASCADE) + group = models.OneToOneField(Group, on_delete=models.CASCADE) - contact = models.ForeignKey( - VinceCommContact, - on_delete=models.CASCADE, - blank=True, - null=True) + contact = models.ForeignKey(VinceCommContact, on_delete=models.CASCADE, blank=True, null=True) default_access = models.BooleanField( - default=True, - help_text="If true, grant case access to users in group by default") - - logo = models.FileField( - storage=SharedMediaStorage(location="vince_logos"), - blank=True, - null=True) + default=True, help_text="If true, grant case access to users in group by default" + ) - logocolor = models.CharField( - max_length=10, - default=random_logo_color) + logo = models.FileField(storage=SharedMediaStorage(location="vince_logos"), blank=True, null=True) - vincetrack = models.BooleanField( - default=False, - help_text="Is this a vincetrack group?" - ) + logocolor = models.CharField(max_length=10, default=random_logo_color) + + vincetrack = models.BooleanField(default=False, help_text="Is this a vincetrack group?") def _get_url(self): ed = ED(base64.b64encode(settings.SECRET_KEY.encode())) @@ -532,7 +509,7 @@ def _get_url(self): return reverse("vinny:groupcard", args=[egid]) url = property(_get_url) - + def get_logo(self): if self.logo: return self.logo.url @@ -565,140 +542,121 @@ def get_vince_users(self): class CoordinatorSettings(models.Model): - group = models.OneToOneField( - Group, - on_delete=models.CASCADE) + group = models.OneToOneField(Group, on_delete=models.CASCADE) team_signature = models.TextField( blank=True, null=True, - help_text=_('Email signature for automatic case messages sent by VINCE to case participants'), + help_text=_("Email signature for automatic case messages sent by VINCE to case participants"), ) team_email = models.CharField( max_length=100, blank=True, null=True, - help_text=_('Email address to use for outgoing email. If not set, uses DEFAULT_REPLY_EMAIL in settings'), + help_text=_("Email address to use for outgoing email. If not set, uses DEFAULT_REPLY_EMAIL in settings"), ) disclosure_link = models.URLField( blank=True, null=True, - help_text=_("Link to disclosure guidance that will be presented to case members at first view of case") + help_text=_("Link to disclosure guidance that will be presented to case members at first view of case"), ) - - + + class VinceCommGroupAdmin(models.Model): - contact = models.ForeignKey(VinceCommContact, - on_delete=models.CASCADE) + contact = models.ForeignKey(VinceCommContact, on_delete=models.CASCADE) - email = models.ForeignKey(VinceCommEmail, - on_delete=models.CASCADE) + email = models.ForeignKey(VinceCommEmail, on_delete=models.CASCADE) comm_action = models.BooleanField( - default = False, - help_text = "If true, group admin was created by another group admin" + default=False, help_text="If true, group admin was created by another group admin" ) def __str__(self): return "%s" % self.email.email - def get_uuid_filename(self, filename): name = str(self.uuid) return name - + + class VinceAttachment(models.Model): file = models.FileField( - _('File'), + _("File"), storage=SharedMediaStorage(), upload_to=get_uuid_filename, max_length=1000, ) filename = models.CharField( - _('Filename'), + _("Filename"), max_length=1000, ) mime_type = models.CharField( - _('MIME Type'), + _("MIME Type"), max_length=255, ) size = models.IntegerField( - _('Size'), - help_text=_('Size of this file in bytes'), + _("Size"), + help_text=_("Size of this file in bytes"), ) - uuid = models.UUIDField( - default=uuid.uuid4, - editable=False, - unique=True) + uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) - uploaded_time = models.DateTimeField( - default=timezone.now) + uploaded_time = models.DateTimeField(default=timezone.now) def __str__(self): - return '%s' % self.filename + return "%s" % self.filename def _get_access_url(self): - filename = vinceutils.safe_filename(self.filename,str(self.uuid),self.mime_type) - url = self.file.storage.url(self.file.name, parameters={'ResponseContentDisposition': f'attachment; filename="{filename}"'}, expire=10) + filename = vinceutils.safe_filename(self.filename, str(self.uuid), self.mime_type) + url = self.file.storage.url( + self.file.name, parameters={"ResponseContentDisposition": f'attachment; filename="{filename}"'}, expire=10 + ) return url - + access_url = property(_get_access_url) class Meta: - ordering = ('filename',) - verbose_name = _('Vince Attachment') - verbose_name_plural = _('Vince Attachments') + ordering = ("filename",) + verbose_name = _("Vince Attachment") + verbose_name_plural = _("Vince Attachments") class VinceTrackAttachment(models.Model): - file = models.ForeignKey( - VinceAttachment, - on_delete=models.CASCADE) + file = models.ForeignKey(VinceAttachment, on_delete=models.CASCADE) - case = models.ForeignKey( - "Case", - blank=True, - null=True, - on_delete=models.SET_NULL) + case = models.ForeignKey("Case", blank=True, null=True, on_delete=models.SET_NULL) + + vulnote = models.BooleanField(default=False) + + shared = models.BooleanField(default=False) - vulnote = models.BooleanField( - default = False) - shared = models.BooleanField( - default = False) - - class VinceCommInvitedUsers(models.Model): - email = models.CharField( - max_length=200) - - case = models.ForeignKey( - "Case", - on_delete=models.CASCADE) + email = models.CharField(max_length=200) + + case = models.ForeignKey("Case", on_delete=models.CASCADE) user = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, - help_text=_('The user that invited this user.'), - on_delete=models.SET_NULL) + help_text=_("The user that invited this user."), + on_delete=models.SET_NULL, + ) - coordinator = models.BooleanField( - default = False) + coordinator = models.BooleanField(default=False) + + # the id of the CaseParticipant in VINCE + vince_id = models.IntegerField(default=0) - # the id of the CaseParticipant in VINCE - vince_id = models.IntegerField( - default=0) - def __str__(self): return "%s" % self.email @@ -708,20 +666,21 @@ def update_filename(instance, filename): if instance.vrf_id: new_filename = "vrf%s_%s" % (instance.vrf_id, filename) else: - new_filename= "novrf_%s" % filename + new_filename = "novrf_%s" % filename return new_filename - + class VTCaseRequest(models.Model): - """ + """ A Case Request is a request for VINCE vulnerability coordination that has either been manually created by a coordinator or has come from the Vulnerability Reporting Form (VRF). - + A Case Request will eventually (but not always) become a Vulnerability - Case if selected by the Vuln Coordination Team. + Case if selected by the Vuln Coordination Team. """ + PENDING_STATUS = 0 OPEN_STATUS = 1 REOPENED_STATUS = 2 @@ -730,18 +689,20 @@ class VTCaseRequest(models.Model): DUPLICATE_STATUS = 5 STATUS_CHOICES = ( - (PENDING_STATUS, _('Pending')), - (OPEN_STATUS, _('Open')), - (REOPENED_STATUS, _('Reopened')), - (RESOLVED_STATUS, _('Resolved')), - (CLOSED_STATUS, _('Closed')), - (DUPLICATE_STATUS, _('Duplicate')), + (PENDING_STATUS, _("Pending")), + (OPEN_STATUS, _("Open")), + (REOPENED_STATUS, _("Reopened")), + (RESOLVED_STATUS, _("Resolved")), + (CLOSED_STATUS, _("Closed")), + (DUPLICATE_STATUS, _("Duplicate")), ) - WHY_NOT_CHOICES = [('1', 'I have not attempted to contact any vendors'), - ('2', 'I have been unable to find contact information for a vendor'), - ('3', 'Other')] - + WHY_NOT_CHOICES = [ + ("1", "I have not attempted to contact any vendors"), + ("2", "I have been unable to find contact information for a vendor"), + ("3", "Other"), + ] + vrf_id = models.CharField(max_length=20) contact_name = models.CharField(max_length=100, blank=True, null=True) contact_org = models.CharField(max_length=100, blank=True, null=True) @@ -760,9 +721,7 @@ class VTCaseRequest(models.Model): product_name = models.CharField(max_length=500) product_version = models.CharField(max_length=100, blank=True, null=True) metadata = OldJSONField( - help_text=_('Extensible, currently used to specify relevance to AI/ML systems'), - blank=True, - null=True + help_text=_("Extensible, currently used to specify relevance to AI/ML systems"), blank=True, null=True ) ics_impact = models.BooleanField(default=False) vul_description = models.TextField(blank=True, null=True) @@ -775,56 +734,51 @@ class VTCaseRequest(models.Model): exploit_references = models.CharField(max_length=1000, blank=True, null=True) vul_disclose = models.BooleanField(default=False) disclosure_plans = models.CharField(max_length=1000, blank=True, null=True) - user_file = models.FileField(blank=True, null=True, - storage=PrivateMediaStorage(), - upload_to=update_filename) + user_file = models.FileField(blank=True, null=True, storage=PrivateMediaStorage(), upload_to=update_filename) date_submitted = models.DateTimeField(default=timezone.now) - tracking = models.CharField(max_length=100,blank=True, null=True) + tracking = models.CharField(max_length=100, blank=True, null=True) status = models.IntegerField( - _('Status'), + _("Status"), choices=STATUS_CHOICES, - default=PENDING_STATUS, + default=PENDING_STATUS, ) comments = models.TextField(blank=True, null=True) search_vector = SearchVectorField(null=True) - new_vuid = models.CharField( - max_length=20, - blank=True, - null=True) - + new_vuid = models.CharField(max_length=20, blank=True, null=True) + coordinator = models.ForeignKey( - Group, - blank=True, - null=True, - help_text=_('The group assigned to this report.'), - on_delete=models.SET_NULL) - + Group, blank=True, null=True, help_text=_("The group assigned to this report."), on_delete=models.SET_NULL + ) + user = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, - help_text=_('The user that submitted the report.'), - on_delete=models.SET_NULL) + help_text=_("The user that submitted the report."), + on_delete=models.SET_NULL, + ) class Meta: - indexes = [ GinIndex( - fields = ['search_vector'], - name = 'vc_caserequest_gin', + indexes = [ + GinIndex( + fields=["search_vector"], + name="vc_caserequest_gin", ) - ] + ] def get_absolute_url(self): from django.urls import reverse - return reverse('vinny:cr', args=(self.id,)) + + return reverse("vinny:cr", args=(self.id,)) def _get_modified(self): return self.date_submitted modified = property(_get_modified) - + def __str__(self): return self.vrf_id @@ -837,48 +791,43 @@ def _get_status(self): """ Displays the ticket status, with an "On Hold" message if needed. """ - return u'%s' % (self.get_status_display()) + return "%s" % (self.get_status_display()) get_status = property(_get_status) def _get_status_html(self): if self.status == self.OPEN_STATUS: - return f"{self.get_status_display()}" + return f'{self.get_status_display()}' elif self.status == self.PENDING_STATUS: - return f"{self.get_status_display()}" + return f'{self.get_status_display()}' elif self.status == self.CLOSED_STATUS: - return f"{self.get_status_display()}" + return f'{self.get_status_display()}' else: - return f"{self.get_status_display()}" + return f'{self.get_status_display()}' get_status_html = property(_get_status_html) - + + class CRFollowUp(models.Model): cr = models.ForeignKey( VTCaseRequest, - on_delete = models.CASCADE, - verbose_name=_('Case Request'), + on_delete=models.CASCADE, + verbose_name=_("Case Request"), ) - date = models.DateTimeField( - _('Date'), - default=timezone.now - ) - - last_edit = models.DateTimeField( - _('Last Modified Date'), - blank=True, - null=True) + date = models.DateTimeField(_("Date"), default=timezone.now) + + last_edit = models.DateTimeField(_("Last Modified Date"), blank=True, null=True) title = models.CharField( - _('Title'), + _("Title"), max_length=200, blank=True, null=True, ) - + comment = models.TextField( - _('Comment'), + _("Comment"), blank=True, null=True, ) @@ -888,131 +837,98 @@ class CRFollowUp(models.Model): on_delete=models.CASCADE, blank=True, null=True, - verbose_name=_('User'), + verbose_name=_("User"), ) new_status = models.IntegerField( - _('New Status'), + _("New Status"), choices=VTCaseRequest.STATUS_CHOICES, blank=True, null=True, - help_text=_('If the status was changed, what was it changed to?'), + help_text=_("If the status was changed, what was it changed to?"), ) def __str__(self): - return '%s' % self.title + return "%s" % self.title + class ReportAttachment(models.Model): - + action = models.ForeignKey( - CRFollowUp, - on_delete=models.CASCADE, - verbose_name=_('Vendor Action'), - blank=True, - null=True + CRFollowUp, on_delete=models.CASCADE, verbose_name=_("Vendor Action"), blank=True, null=True ) - file = models.ForeignKey( - VinceAttachment, - on_delete=models.CASCADE, - blank=True, - null=True) - - attachment = models.FileField( - storage=PrivateMediaStorage(), - blank=True, - null=True) + file = models.ForeignKey(VinceAttachment, on_delete=models.CASCADE, blank=True, null=True) + + attachment = models.FileField(storage=PrivateMediaStorage(), blank=True, null=True) + + vince_id = models.IntegerField(default=0) - vince_id = models.IntegerField( - default = 0 - ) - class Case(models.Model): ACTIVE_STATUS = 1 INACTIVE_STATUS = 2 STATUS_CHOICES = ( - (ACTIVE_STATUS, _('Active')), - (INACTIVE_STATUS, _('Inactive')), - ) - vuid = models.CharField( - max_length=20 - ) - created = models.DateTimeField( - auto_now_add=True - ) - modified = models.DateTimeField( - auto_now=True + (ACTIVE_STATUS, _("Active")), + (INACTIVE_STATUS, _("Inactive")), ) + vuid = models.CharField(max_length=20) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) status = models.IntegerField( - _('Status'), + _("Status"), choices=STATUS_CHOICES, default=ACTIVE_STATUS, ) summary = models.CharField( max_length=1000, - help_text=_('A summary of the case.'), - ) - title = models.CharField( - max_length=500, - help_text=_('A title for this case. Optional.') - ) - - due_date = models.DateTimeField( - blank=True, null=True + help_text=_("A summary of the case."), ) + title = models.CharField(max_length=500, help_text=_("A title for this case. Optional.")) - publicdate = models.DateTimeField( - blank=True, null=True - ) + due_date = models.DateTimeField(blank=True, null=True) - publicurl = models.CharField( - max_length=500, blank=True, null=True, - help_text=_('The URL for the public notice of a vulnerability.') - ) - - vince_id = models.IntegerField( - default=0 - ) - - cr = models.OneToOneField( - VTCaseRequest, - blank=True, null=True, - on_delete=models.SET_NULL + publicdate = models.DateTimeField(blank=True, null=True) + + publicurl = models.CharField( + max_length=500, blank=True, null=True, help_text=_("The URL for the public notice of a vulnerability.") ) + vince_id = models.IntegerField(default=0) + + cr = models.OneToOneField(VTCaseRequest, blank=True, null=True, on_delete=models.SET_NULL) + team_owner = models.ForeignKey( - Group, - blank=True, null=True, - help_text=_('The coordinator group that is leading this case'), - on_delete=models.SET_NULL) - - note = models.OneToOneField( - "VCVUReport", + Group, + blank=True, + null=True, + help_text=_("The coordinator group that is leading this case"), on_delete=models.SET_NULL, - blank=True, null=True) + ) - uid = models.CharField( - max_length=50, - default=generate_uuid) + note = models.OneToOneField("VCVUReport", on_delete=models.SET_NULL, blank=True, null=True) + + uid = models.CharField(max_length=50, default=generate_uuid) search_vector = SearchVectorField(null=True) def get_absolute_url(self): from django.urls import reverse - return reverse('vinny:case', args=(self.id,)) - + + return reverse("vinny:case", args=(self.id,)) + class Meta: - indexes = [ GinIndex( - fields = ['search_vector'], - name = 'vc_case_gin', + indexes = [ + GinIndex( + fields=["search_vector"], + name="vc_case_gin", ) ] def __str__(self): return self.vuid - + def get_title(self): return "%s%s: %s" % (settings.CASE_IDENTIFIER, self.vuid, self.title) @@ -1020,26 +936,27 @@ def get_vuid(self): return f"{settings.CASE_IDENTIFIER}%s" % self.vuid vu_vuid = property(get_vuid) - + def _get_status(self): """ Displays the ticket status, with an "On Hold" message if needed. """ - held_msg="" - return u'%s%s' % (self.get_status_display(), held_msg) + held_msg = "" + return "%s%s" % (self.get_status_display(), held_msg) get_status = property(_get_status) def _get_case_for_url(self): - """ A URL-friendly ticket ID, used in links. """ - return u"VU%s" % (self.vuid) + """A URL-friendly ticket ID, used in links.""" + return "VU%s" % (self.vuid) case_for_url = property(_get_case_for_url) def get_absolute_url(self): from django.urls import reverse - return reverse('vinny:case', args=(self.id,)) - + + return reverse("vinny:case", args=(self.id,)) + def _is_published(self): try: if self.note.datefirstpublished: @@ -1053,255 +970,201 @@ def _is_published(self): def _get_status_html(self): return_str = "" if self.status == self.ACTIVE_STATUS: - return_str = f"{self.get_status_display()}" + return_str = f'{self.get_status_display()}' else: - return_str = f"{self.get_status_display()}" + return_str = f'{self.get_status_display()}' if self.published: # published assumed public - return_str = return_str + f" Public" + return_str = return_str + f' Public' elif self.publicdate: - return_str = return_str + f" Public" + return_str = return_str + f' Public' return return_str get_status_html = property(_get_status_html) + class VinceCommCaseAttachment(models.Model): action = models.ForeignKey( - "VendorAction", - on_delete=models.CASCADE, - verbose_name=_('Vendor Action'), - blank=True, - null=True + "VendorAction", on_delete=models.CASCADE, verbose_name=_("Vendor Action"), blank=True, null=True ) - file = models.ForeignKey( - VinceAttachment, - on_delete=models.CASCADE, - blank=True, - null=True) - - attachment = models.FileField( - storage=PrivateMediaStorage(), - blank=True, - null=True) + file = models.ForeignKey(VinceAttachment, on_delete=models.CASCADE, blank=True, null=True) + + attachment = models.FileField(storage=PrivateMediaStorage(), blank=True, null=True) + + vince_id = models.IntegerField(default=0) - vince_id = models.IntegerField( - default = 0 - ) class CaseVulnerabilityManager(models.Manager): def get_queryset(self): return super(CaseVulnerabilityManager, self).get_queryset().filter(deleted=False) - + + class CaseVulnerability(models.Model): - cve = models.CharField( - _('CVE'), - max_length=50, - blank=True, - null=True) + cve = models.CharField(_("CVE"), max_length=50, blank=True, null=True) - description = models.TextField( - _('Description')) - - case = models.ForeignKey( - Case, - on_delete=models.CASCADE) + description = models.TextField(_("Description")) + + case = models.ForeignKey(Case, on_delete=models.CASCADE) - date_added = models.DateTimeField( - default=timezone.now) + date_added = models.DateTimeField(default=timezone.now) vince_id = models.IntegerField( - blank=True, null=True, - help_text=_('The vince pk'), + blank=True, + null=True, + help_text=_("The vince pk"), ) deleted = models.BooleanField( - default = False, - help_text=_('Only True if vulnerability is removed after publication') + default=False, help_text=_("Only True if vulnerability is removed after publication") ) - case_increment = models.IntegerField( - default = 0) - - ask_vendor_status = models.BooleanField( - default=False) + case_increment = models.IntegerField(default=0) + + ask_vendor_status = models.BooleanField(default=False) objects = CaseVulnerabilityManager() - + def __str__(self): return "%s" % self.description def _get_vul(self): - """ A user-friendly Vul ID, which is the cve if cve exists, - otherwise it's a combination of vul ID and case. """ - if (self.cve): - return u"CVE-%s" % self.cve + """A user-friendly Vul ID, which is the cve if cve exists, + otherwise it's a combination of vul ID and case.""" + if self.cve: + return "CVE-%s" % self.cve else: - return u"%s" % self.cert_id + return "%s" % self.cert_id vul = property(_get_vul) def _get_vul_for_url(self): - """ A URL-friendly vul ID, used in links. """ - return u"%s-%s" % (self.case.case_for_url, self.vince_id) + """A URL-friendly vul ID, used in links.""" + return "%s-%s" % (self.case.case_for_url, self.vince_id) vul_for_url = property(_get_vul_for_url) - def _get_cert_id(self): - return u"%s%s.%d" % (settings.CASE_IDENTIFIER, self.case.vuid, self.case_increment) + def _get_cert_id(self): + return "%s%s.%d" % (settings.CASE_IDENTIFIER, self.case.vuid, self.case_increment) cert_id = property(_get_cert_id) def as_dict(self): exploits = CaseVulExploit.objects.filter(vul=self).count() link = reverse("vinny:vuldetail", args=(self.id,)) - editstatus = reverse('vinny:status', args=(self.case.id,)) + editstatus = reverse("vinny:status", args=(self.case.id,)) return { - 'id': self.id, - 'cert_id': self.cert_id, - 'ask_vendor_status': self.ask_vendor_status, - 'description': self.description, - 'cve': self.vul, - 'exploits': exploits, - 'vuldetaillink': link, - 'editstatus': editstatus, - 'date_added': self.date_added.strftime('%Y-%m-%d'), - } + "id": self.id, + "cert_id": self.cert_id, + "ask_vendor_status": self.ask_vendor_status, + "description": self.description, + "cve": self.vul, + "exploits": exploits, + "vuldetaillink": link, + "editstatus": editstatus, + "date_added": self.date_added.strftime("%Y-%m-%d"), + } class CaseVulCVSS(models.Model): - vul = models.ForeignKey( - CaseVulnerability, - on_delete=models.CASCADE) + vul = models.ForeignKey(CaseVulnerability, on_delete=models.CASCADE) - last_modified = models.DateTimeField( - _('Last Modified Date'), - default=timezone.now) + last_modified = models.DateTimeField(_("Last Modified Date"), default=timezone.now) - vector = models.CharField( - _('CVSS Vector String'), - max_length=100, - blank=True, - null=True) + vector = models.CharField(_("CVSS Vector String"), max_length=100, blank=True, null=True) - score = models.DecimalField( - _('CVSS Base Score'), - max_digits=3, - decimal_places=1, - blank=True, - null=True) + score = models.DecimalField(_("CVSS Base Score"), max_digits=3, decimal_places=1, blank=True, null=True) - severity = models.CharField( - _('CVSS Severity'), - max_length=20, - blank=True, - null=True) + severity = models.CharField(_("CVSS Severity"), max_length=20, blank=True, null=True) def __str__(self): return self.vector - - + + class CaseVulExploit(models.Model): - EXPLOIT_CHOICES = (('code', 'code'), - ('report', 'report'), - ('other', 'other') - ) + EXPLOIT_CHOICES = (("code", "code"), ("report", "report"), ("other", "other")) - vul = models.ForeignKey( - CaseVulnerability, - on_delete=models.CASCADE) + vul = models.ForeignKey(CaseVulnerability, on_delete=models.CASCADE) - date_added = models.DateTimeField( - default=timezone.now) + date_added = models.DateTimeField(default=timezone.now) - reference_date = models.DateTimeField( - blank=True, - null=True) + reference_date = models.DateTimeField(blank=True, null=True) link = models.URLField() - reference_type = models.CharField( - max_length=30, - choices=EXPLOIT_CHOICES, - default='code') + reference_type = models.CharField(max_length=30, choices=EXPLOIT_CHOICES, default="code") - notes = models.TextField(blank=True, - null=True) + notes = models.TextField(blank=True, null=True) vince_id = models.IntegerField( - blank=True, null=True, - help_text=_('The vince exploit pk'), + blank=True, + null=True, + help_text=_("The vince exploit pk"), ) def __str__(self): return "%s" % self.link - + + class CaseMember(models.Model): - case = models.ForeignKey( - Case, - on_delete=models.CASCADE) + case = models.ForeignKey(Case, on_delete=models.CASCADE) - group = models.ForeignKey( - Group, - blank=True, null=True, - on_delete=models.CASCADE) + group = models.ForeignKey(Group, blank=True, null=True, on_delete=models.CASCADE) participant = models.ForeignKey( settings.AUTH_USER_MODEL, - blank=True, null=True, - help_text=_('A participant in the case'), - related_name='participant', - on_delete = models.CASCADE) + blank=True, + null=True, + help_text=_("A participant in the case"), + related_name="participant", + on_delete=models.CASCADE, + ) - added = models.DateTimeField( - default=timezone.now) + added = models.DateTimeField(default=timezone.now) user = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, - help_text=_('The user that added the vendor.'), - on_delete=models.SET_NULL) + help_text=_("The user that added the vendor."), + on_delete=models.SET_NULL, + ) - seen = models.BooleanField( - default=False) + seen = models.BooleanField(default=False) - coordinator = models.BooleanField( - default = False) + coordinator = models.BooleanField(default=False) + + reporter_group = models.BooleanField(default=False) - reporter_group = models.BooleanField( - default = False) - # the id of the CaseParticipant in VINCE - vince_id = models.IntegerField( - default=0) + vince_id = models.IntegerField(default=0) def get_member_name(self): name = "Case Member" if self.participant: return self.participant elif self.group: - name = vinceutils.deepGet(self,'group.groupcontact.contact.vendor_name') + name = vinceutils.deepGet(self, "group.groupcontact.contact.vendor_name") if name: return name - elif vinceutils.deepGet(self,'group.name'): + elif vinceutils.deepGet(self, "group.name"): return self.group.name return name - + def __str__(self): name = self.get_member_name() return f"{name} for case {self.case.vu_vuid}" - + def share_status(self): status = CaseStatement.objects.filter(member=self).first() if status: return status.share else: return False - + def get_general_status(self): status = CaseMemberStatus.objects.filter(member=self) stat = 3 @@ -1312,43 +1175,40 @@ def get_general_status(self): if x.status == 2: stat = 2 return stat - + def get_statement(self): stmt = CaseStatement.objects.filter(member=self, statement__isnull=False) if stmt: return stmt else: return CaseMemberStatus.objects.filter(member=self, statement__isnull=False) - + class Meta: - unique_together = (('group', 'case', 'participant'),) + unique_together = (("group", "case", "participant"),) class CaseMemberStatusManager(models.Manager): def get_queryset(self): return super(CaseMemberStatusManager, self).get_queryset().exclude(vulnerability__deleted=True) - + + class CaseMemberStatus(models.Model): AFFECTED = 1 UNAFFECTED = 2 UNKNOWN = 3 STATUS_CHOICES = ( - (UNAFFECTED, _('Not Affected')), - (AFFECTED, _('Affected')), - (UNKNOWN, _('Unknown')), + (UNAFFECTED, _("Not Affected")), + (AFFECTED, _("Affected")), + (UNKNOWN, _("Unknown")), ) - member = models.ForeignKey( - CaseMember, - on_delete=models.CASCADE) + member = models.ForeignKey(CaseMember, on_delete=models.CASCADE) - vulnerability = models.ForeignKey( - CaseVulnerability, - on_delete=models.CASCADE) + vulnerability = models.ForeignKey(CaseVulnerability, on_delete=models.CASCADE) status = models.IntegerField( - _('Status'), + _("Status"), choices=STATUS_CHOICES, ) @@ -1356,88 +1216,71 @@ class CaseMemberStatus(models.Model): settings.AUTH_USER_MODEL, blank=True, null=True, - help_text=_('The user that committed status.'), - on_delete=models.SET_NULL) + help_text=_("The user that committed status."), + on_delete=models.SET_NULL, + ) - references = models.TextField( - blank=True, - null=True) + references = models.TextField(blank=True, null=True) - statement = models.TextField( - blank=True, - null=True) + statement = models.TextField(blank=True, null=True) - date_added = models.DateTimeField( - default=timezone.now) + date_added = models.DateTimeField(default=timezone.now) - date_modified = models.DateTimeField( - auto_now=True - ) + date_modified = models.DateTimeField(auto_now=True) - approved = models.BooleanField( - default=False - ) + approved = models.BooleanField(default=False) objects = CaseMemberStatusManager() - + class Meta: - unique_together = (('member', 'vulnerability'),) + unique_together = (("member", "vulnerability"),) + class CaseStatement(models.Model): ### This is a general statement on a case vs a statement on a vul ### case = models.ForeignKey( Case, - help_text=('The case this post belongs to'), + help_text=("The case this post belongs to"), on_delete=models.CASCADE, ) - member = models.ForeignKey( - CaseMember, - on_delete=models.CASCADE) + member = models.ForeignKey(CaseMember, on_delete=models.CASCADE) user = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, - help_text=_('The user that provided statement.'), - on_delete=models.SET_NULL) + help_text=_("The user that provided statement."), + on_delete=models.SET_NULL, + ) - references = models.TextField( - blank=True, - null=True) + references = models.TextField(blank=True, null=True) - statement = models.TextField( - blank=True, - null=True) + statement = models.TextField(blank=True, null=True) - addendum = models.TextField( - blank=True, - null=True) + addendum = models.TextField(blank=True, null=True) - share = models.BooleanField( - default=False) - - date_added = models.DateTimeField( - default=timezone.now) + share = models.BooleanField(default=False) - date_modified = models.DateTimeField( - auto_now=True - ) + date_added = models.DateTimeField(default=timezone.now) - approved = models.BooleanField( - default=False) + date_modified = models.DateTimeField(auto_now=True) + + approved = models.BooleanField(default=False) def __str__(self): return self.statement - + class Meta: - unique_together = (('case', 'member'),) + unique_together = (("case", "member"),) + - """ Adapted from Misago https://github.com/rafalp/Misago """ + + class PostManager(models.Manager): def search(self, case=None, query=None, author_list=[]): qs = self.get_queryset() @@ -1452,54 +1295,49 @@ def search(self, case=None, query=None, author_list=[]): class Post(models.Model): current_revision = models.OneToOneField( - 'PostRevision', + "PostRevision", blank=True, null=True, on_delete=models.CASCADE, - related_name='current_set', - help_text=_('The revision displayed for this post. If you need to rollback, change value of this field.')) + related_name="current_set", + help_text=_("The revision displayed for this post. If you need to rollback, change value of this field."), + ) case = models.ForeignKey( Case, - help_text=('The case this post belongs to'), + help_text=("The case this post belongs to"), on_delete=models.CASCADE, ) - created = models.DateTimeField( - auto_now_add=True - ) + created = models.DateTimeField(auto_now_add=True) - modified = models.DateTimeField( - auto_now=True - ) + modified = models.DateTimeField(auto_now=True) author = models.ForeignKey( settings.AUTH_USER_MODEL, - blank=True, null=True, - help_text=_('The writer of this post.'), - on_delete=models.SET_NULL) + blank=True, + null=True, + help_text=_("The writer of this post."), + on_delete=models.SET_NULL, + ) group = models.ForeignKey( - Group, - blank=True, null=True, - help_text=_('The group of the user'), - on_delete=models.SET_NULL + Group, blank=True, null=True, help_text=_("The group of the user"), on_delete=models.SET_NULL ) vince_id = models.IntegerField( - blank=True, null=True, - help_text=_('The vince pk'), + blank=True, + null=True, + help_text=_("The vince pk"), ) pinned = models.BooleanField( default=False, - help_text=_('A pinned post is pinned to the top of the page.'), + help_text=_("A pinned post is pinned to the top of the page."), ) - deleted = models.BooleanField( - default=False - ) - + deleted = models.BooleanField(default=False) + objects = PostManager() def add_revision(self, new_revision, save=True): @@ -1508,9 +1346,10 @@ def add_revision(self, new_revision, save=True): revision. """ assert self.id or save, ( - 'Article.add_revision: Sorry, you cannot add a' - 'revision to an article that has not been saved ' - 'without using save=True') + "Article.add_revision: Sorry, you cannot add a" + "revision to an article that has not been saved " + "without using save=True" + ) if not self.id: self.save() revisions = self.postrevision_set.all() @@ -1526,11 +1365,11 @@ def add_revision(self, new_revision, save=True): self.current_revision = new_revision if save: self.save() - + def __str__(self): if self.current_revision: return str(self.current_revision.revision_number) - obj_name = _('Post without content (%d)') % (self.id) + obj_name = _("Post without content (%d)") % (self.id) return str(obj_name) def get_org_name(self): @@ -1576,7 +1415,7 @@ def get_org_logo(self): return self.author.vinceprofile.get_logo() else: return None - + def get_post_count(self): numposts = Post.objects.filter(author=self.author).count() if numposts == 1: @@ -1604,31 +1443,32 @@ def replies(self): def num_replies(self): return self.children.count() + class ThreadedPost(Post): parent = models.ForeignKey( Post, - null=True, blank=True, + null=True, + blank=True, on_delete=models.CASCADE, default=None, related_name="children", - verbose_name=_('Parent')) + verbose_name=_("Parent"), + ) newest_activity = models.DateTimeField(null=True) objects = PostManager() class Meta(object): - verbose_name = _('Threaded post') - verbose_name_plural = _('Threaded posts') - -class BaseRevisionMixin(models.Model): + verbose_name = _("Threaded post") + verbose_name_plural = _("Threaded posts") + +class BaseRevisionMixin(models.Model): """This is an abstract model used as a mixin: Do not override any of the core model methods but respect the inheritor's freedom to do so itself.""" - revision_number = models.IntegerField( - editable=False, - verbose_name=_('revision number')) + revision_number = models.IntegerField(editable=False, verbose_name=_("revision number")) user_message = models.TextField( blank=True, @@ -1640,30 +1480,22 @@ class BaseRevisionMixin(models.Model): ) user = models.ForeignKey( - settings.AUTH_USER_MODEL, - verbose_name=_('user'), - blank=True, null=True, - on_delete=models.SET_NULL) + settings.AUTH_USER_MODEL, verbose_name=_("user"), blank=True, null=True, on_delete=models.SET_NULL + ) - modified = models.DateTimeField( - auto_now=True) + modified = models.DateTimeField(auto_now=True) - created = models.DateTimeField( - auto_now_add=True) + created = models.DateTimeField(auto_now_add=True) - previous_revision = models.ForeignKey( - 'self', - blank=True, null=True, - on_delete=models.SET_NULL - ) + previous_revision = models.ForeignKey("self", blank=True, null=True, on_delete=models.SET_NULL) deleted = models.BooleanField( - verbose_name=_('deleted'), + verbose_name=_("deleted"), default=False, ) locked = models.BooleanField( - verbose_name=_('locked'), + verbose_name=_("locked"), default=False, ) @@ -1680,7 +1512,6 @@ def inherit_predecessor(self, predecessor): self.locked = predecessor.locked self.revision_number = predecessor.revision_number + 1 - def set_from_request(self, request): if request.user.is_authenticated: self.user = request.user @@ -1689,22 +1520,17 @@ class Meta: abstract = True -class PostRevision(BaseRevisionMixin, models.Model): +class PostRevision(BaseRevisionMixin, models.Model): """This is where main revision data is stored. To make it easier to copy, NEVER create m2m relationships.""" - post = models.ForeignKey( - Post, - on_delete=models.CASCADE, - verbose_name=_('Post')) + post = models.ForeignKey(Post, on_delete=models.CASCADE, verbose_name=_("Post")) # This is where the content goes, with whatever markup language is used - content = models.TextField( - blank=True, - verbose_name=_('vulnote contents')) + content = models.TextField(blank=True, verbose_name=_("vulnote contents")) search_vector = SearchVectorField(null=True) - + def __str__(self): if self.revision_number: return "(%d)" % self.revision_number @@ -1716,7 +1542,7 @@ def clean(self): # but when revisions are created programatically, they might # have UNIX line endings \n instead. logger.debug(self.content) - self.content = self.content.replace('\r', '').replace('\n', '\r\n') + self.content = self.content.replace("\r", "").replace("\n", "\r\n") def inherit_predecessor(self, post): """ @@ -1727,17 +1553,18 @@ def inherit_predecessor(self, post): predecessor = post.current_revision self.post = predecessor.post self.content = predecessor.content -# self.title = predecessor.title + # self.title = predecessor.title self.deleted = predecessor.deleted self.locked = predecessor.locked class Meta: - get_latest_by = 'revision_number' - ordering = ('created',) - unique_together = ('post', 'revision_number') - indexes = [ GinIndex( - fields = ['search_vector'], - name = 'post_gin', + get_latest_by = "revision_number" + ordering = ("created",) + unique_together = ("post", "revision_number") + indexes = [ + GinIndex( + fields=["search_vector"], + name="post_gin", ) ] @@ -1745,32 +1572,17 @@ class Meta: # Adapted from Pinax-messages Project class Thread(models.Model): - subject = models.CharField( - max_length=150) + subject = models.CharField(max_length=150) - users = models.ManyToManyField( - settings.AUTH_USER_MODEL, - through="UserThread") + users = models.ManyToManyField(settings.AUTH_USER_MODEL, through="UserThread") - case = models.ForeignKey( - Case, - blank=True, null=True, - on_delete=models.CASCADE) + case = models.ForeignKey(Case, blank=True, null=True, on_delete=models.CASCADE) - to_group = models.CharField( - max_length=250, - blank = True, - null = True - ) + to_group = models.CharField(max_length=250, blank=True, null=True) - from_group = models.CharField( - max_length=150, - blank = True, - null = True - ) + from_group = models.CharField(max_length=150, blank=True, null=True) - groupchat = models.BooleanField( - default=False) + groupchat = models.BooleanField(default=False) @classmethod def none(cls): @@ -1790,17 +1602,16 @@ def deleted(cls, user): @classmethod def read(cls, user): - return cls.objects.filter(userthread__user=user, userthread__deleted=False, userthread__unread=False).distinct() + return cls.objects.filter( + userthread__user=user, userthread__deleted=False, userthread__unread=False + ).distinct() @classmethod def unread(cls, user): return cls.objects.filter(userthread__user=user, userthread__deleted=False, userthread__unread=True).distinct() def __str__(self): - return "{}: {}".format( - self.subject, - ", ".join([str(user) for user in self.users.all()]) - ) + return "{}: {}".format(self.subject, ", ".join([str(user) for user in self.users.all()])) def get_absolute_url(self): return reverse("vinny:thread_detail", args=[self.pk]) @@ -1824,7 +1635,7 @@ def number_attachments(self): @cached_attribute def num_messages(self): return len(self.messages.all()) - + @classmethod def ordered(cls, objs): """ @@ -1840,23 +1651,21 @@ def ordered(cls, objs): def get_absolute_url(self): from django.urls import reverse - return reverse('vinny:thread_detail', args=(self.id,)) + + return reverse("vinny:thread_detail", args=(self.id,)) class UserThread(models.Model): - thread = models.ForeignKey( - Thread, - on_delete=models.CASCADE) + thread = models.ForeignKey(Thread, on_delete=models.CASCADE) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) unread = models.BooleanField() deleted = models.BooleanField() + class MessageManager(models.Manager): def search(self, case=None, query=None, author_list=None): qs = self.get_queryset() @@ -1866,28 +1675,22 @@ def search(self, case=None, query=None, author_list=None): qs = qs.filter(sender__in=author_list) if query is not None: qs = qs.filter(content__search=query) - + return qs - + + class Message(models.Model): - thread = models.ForeignKey( - Thread, - related_name="messages", - on_delete=models.CASCADE) + thread = models.ForeignKey(Thread, related_name="messages", on_delete=models.CASCADE) - sender = models.ForeignKey( - settings.AUTH_USER_MODEL, - related_name="sent_messages", - on_delete=models.CASCADE) + sender = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="sent_messages", on_delete=models.CASCADE) - created = models.DateTimeField( - default=timezone.now) + created = models.DateTimeField(default=timezone.now) content = models.TextField(blank=True, null=True) objects = MessageManager() - + @classmethod def new_reply(cls, thread, user, content): """ @@ -1899,7 +1702,7 @@ def new_reply(cls, thread, user, content): thread.userthread_set.exclude(user=user).update(deleted=False, unread=True) thread.userthread_set.filter(user=user).update(deleted=False, unread=False) message_sent.send(sender=cls, message=msg, thread=thread, reply=True) - #for recip in thread.userthread_set.exclude(user=user): + # for recip in thread.userthread_set.exclude(user=user): # send_newmessage_mail(msg, recip.user) return msg @@ -1915,34 +1718,50 @@ def new_message(cls, from_user, to_users, case, subject, content, signal=True): else: vc = None thread = Thread.objects.create(subject=subject, case=vc) - track_users=[] + track_users = [] direct_msg = False - #get coordinators on case + # get coordinators on case if vc: - vt_groups = CaseMember.objects.filter(case=case, coordinator=True).exclude(group__groupcontact__vincetrack=False).exclude(group__groupcontact__isnull=True).values_list('group', flat=True) + vt_groups = ( + CaseMember.objects.filter(case=case, coordinator=True) + .exclude(group__groupcontact__vincetrack=False) + .exclude(group__groupcontact__isnull=True) + .values_list("group", flat=True) + ) logger.debug(vt_groups) - vt_users = User.objects.using('vincecomm').filter(groups__in=vt_groups) + vt_users = User.objects.using("vincecomm").filter(groups__in=vt_groups) logger.debug(vt_users) - to_group = ", ".join(list(CaseMember.objects.filter(case=case, coordinator=True).exclude(group__groupcontact__vincetrack=False).exclude(group__groupcontact__isnull=True).values_list('group__groupcontact__contact__vendor_name', flat=True))) - + to_group = ", ".join( + list( + CaseMember.objects.filter(case=case, coordinator=True) + .exclude(group__groupcontact__vincetrack=False) + .exclude(group__groupcontact__isnull=True) + .values_list("group__groupcontact__contact__vendor_name", flat=True) + ) + ) + else: - #otherwise just send to admin group (settings.py) - #lookup group: - to_g = Group.objects.using('vincecomm').filter(groupcontact__contact__vendor_name=settings.COGNITO_ADMIN_GROUP).first() + # otherwise just send to admin group (settings.py) + # lookup group: + to_g = ( + Group.objects.using("vincecomm") + .filter(groupcontact__contact__vendor_name=settings.COGNITO_ADMIN_GROUP) + .first() + ) if to_g: - vt_users = User.objects.using('vincecomm').filter(groups__id=to_g.id, is_staff=True) + vt_users = User.objects.using("vincecomm").filter(groups__id=to_g.id, is_staff=True) else: logger.warning(f"ERROR: No group for {settings.COGNITO_ADMIN_GROUP}") to_group = settings.COGNITO_ADMIN_GROUP - + if to_users: for user in to_users: - puser = User.objects.using('vincecomm').filter(id=user).first() - if puser.groups.filter(name='vincetrack').exists(): + puser = User.objects.using("vincecomm").filter(id=user).first() + if puser.groups.filter(name="vincetrack").exists(): track_users.append(puser) thread.userthread_set.create(user=puser, deleted=False, unread=True) - #IF TO_USERS means this is from Coordinators + # IF TO_USERS means this is from Coordinators direct_msg = True else: thread.to_group = to_group @@ -1952,16 +1771,16 @@ def new_message(cls, from_user, to_users, case, subject, content, signal=True): thread.userthread_set.create(user=user, deleted=False, unread=True) if direct_msg: - #need to give access to all vt_users in the from_user group + # need to give access to all vt_users in the from_user group if not vc: team = from_user.groups.filter(groupcontact__vincetrack=True) - vt_users = User.objects.using('vincecomm').filter(groups__in=team) - #else if case - this goes to the coordinators on the case + vt_users = User.objects.using("vincecomm").filter(groups__in=team) + # else if case - this goes to the coordinators on the case for user in vt_users: thread.userthread_set.create(user=user, deleted=True, unread=False) else: thread.userthread_set.create(user=from_user, deleted=True, unread=False) - + msg = cls.objects.create(thread=thread, sender=from_user, content=content) if signal: message_sent.send(sender=cls, message=msg, thread=thread, reply=False) @@ -1972,10 +1791,10 @@ def new_message(cls, from_user, to_users, case, subject, content, signal=True): # but we need to notify them if it's a direct message from # one track member to another send_newmessage_mail(msg, user, notrack=False) - #if to_users: + # if to_users: # for user in emails: # send_newmessage_mail(msg, user) - + return msg def _get_user(self): @@ -1987,29 +1806,21 @@ def _get_vc(self): return True vc = property(_get_vc) - + class Meta: ordering = ("created",) def get_absolute_url(self): return self.thread.get_absolute_url() + class MessageAttachment(models.Model): - file = models.ForeignKey( - VinceAttachment, - on_delete=models.CASCADE, - blank=True, - null=True) - - attachment = models.FileField( - storage=PrivateMediaStorage(), - blank=True, - null=True) - - message = models.ForeignKey( - Message, - on_delete=models.CASCADE) + file = models.ForeignKey(VinceAttachment, on_delete=models.CASCADE, blank=True, null=True) + + attachment = models.FileField(storage=PrivateMediaStorage(), blank=True, null=True) + + message = models.ForeignKey(Message, on_delete=models.CASCADE) @classmethod def attach_file(cls, message, file): @@ -2024,8 +1835,8 @@ def attach_file(cls, message, file): mime_type = file.content_type except: mime_type = mimetypes.guess_type(filename, strict=False)[0] - if not(mime_type): - mime_type = 'application/octet-stream' + if not (mime_type): + mime_type = "application/octet-stream" att = VinceAttachment( file=file, @@ -2037,30 +1848,32 @@ def attach_file(cls, message, file): na = cls.objects.create(message=message, file=att) print(na.file.file.name) - s3 = boto3.client('s3', region_name=settings.AWS_REGION) + s3 = boto3.client("s3", region_name=settings.AWS_REGION) # check tag will be acceptable? - nopass = re.findall(r'[^-+= \.:/@A-Za-z0-9_]', filename) + nopass = re.findall(r"[^-+= \.:/@A-Za-z0-9_]", filename) if nopass: - #this tag contains unacceptable chars, so do not add tag - rd = s3.put_object_tagging(Bucket=settings.PRIVATE_BUCKET_NAME, - Key='vince_attachments/'+ na.file.file.name, - Tagging={'TagSet':[{'Key': 'Message', 'Value':str(message.id)}]}) + # this tag contains unacceptable chars, so do not add tag + rd = s3.put_object_tagging( + Bucket=settings.PRIVATE_BUCKET_NAME, + Key="vince_attachments/" + na.file.file.name, + Tagging={"TagSet": [{"Key": "Message", "Value": str(message.id)}]}, + ) else: - rd = s3.put_object_tagging(Bucket=settings.PRIVATE_BUCKET_NAME, - Key='vince_attachments/'+ na.file.file.name, - Tagging={'TagSet':[{'Key': 'Message', 'Value':str(message.id)}, - {'Key':'Filename', 'Value':filename}]}) + rd = s3.put_object_tagging( + Bucket=settings.PRIVATE_BUCKET_NAME, + Key="vince_attachments/" + na.file.file.name, + Tagging={ + "TagSet": [{"Key": "Message", "Value": str(message.id)}, {"Key": "Filename", "Value": filename}] + }, + ) + - class VendorAction(models.Model): - created = models.DateTimeField( - _('Date'), - default=timezone.now - ) + created = models.DateTimeField(_("Date"), default=timezone.now) title = models.CharField( - _('Title'), + _("Title"), max_length=200, blank=True, null=True, @@ -2069,38 +1882,25 @@ class VendorAction(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, - verbose_name=_('User'), + verbose_name=_("User"), ) - member = models.ForeignKey( - CaseMember, - on_delete=models.SET_NULL, - blank=True, - null=True) - - case = models.ForeignKey( - Case, - on_delete=models.SET_NULL, - blank=True, - null=True) + member = models.ForeignKey(CaseMember, on_delete=models.SET_NULL, blank=True, null=True) - post = models.ForeignKey( - Post, - on_delete=models.SET_NULL, - blank=True, - null=True) + case = models.ForeignKey(Case, on_delete=models.SET_NULL, blank=True, null=True) + post = models.ForeignKey(Post, on_delete=models.SET_NULL, blank=True, null=True) def _get_date(self): # this is just so we can rename 'created' for sorting purposes return self.created date = property(_get_date) - + def __str__(self): if self.case and self.member: - return f'{self.case.vuid}: {self.created} {self.title}' - return '%s' % self.title + return f"{self.case.vuid}: {self.created} {self.title}" + return "%s" % self.title def _get_vc(self): return True @@ -2113,48 +1913,44 @@ class VendorStatusChange(models.Model): action = models.ForeignKey( VendorAction, on_delete=models.CASCADE, - verbose_name=_('Vendor Action'), + verbose_name=_("Vendor Action"), ) - + field = models.CharField( - _('Field'), + _("Field"), max_length=100, ) old_value = models.TextField( - _('Old Value'), + _("Old Value"), blank=True, null=True, ) new_value = models.TextField( - _('New Value'), + _("New Value"), blank=True, null=True, ) - vul = models.ForeignKey( - CaseVulnerability, - on_delete=models.CASCADE, - blank=True, - null=True) + vul = models.ForeignKey(CaseVulnerability, on_delete=models.CASCADE, blank=True, null=True) def __str__(self): - out = '%s ' % self.field + out = "%s " % self.field if not self.new_value: - out += 'removed' + out += "removed" elif not self.old_value: - out += ('set to %s') % self.new_value + out += ("set to %s") % self.new_value else: out += ('changed from "%(old_value)s" to "%(new_value)s"') % { - 'old_value': self.old_value, - 'new_value': self.new_value + "old_value": self.old_value, + "new_value": self.new_value, } return out class Meta: - verbose_name = _('Vendor Status change') - verbose_name_plural = _('Vendor status changes') + verbose_name = _("Vendor Status change") + verbose_name_plural = _("Vendor status changes") class VCVUReport(models.Model): @@ -2162,48 +1958,38 @@ class VCVUReport(models.Model): idnumber = models.CharField(max_length=20) name = models.CharField(max_length=500) overview = models.TextField(blank=True, null=True) - datecreated = models.DateTimeField(default = timezone.now) + datecreated = models.DateTimeField(default=timezone.now) publicdate = models.DateTimeField(null=True, blank=True) datefirstpublished = models.DateTimeField(null=True, blank=True) dateupdated = models.DateTimeField(blank=True, null=True) keywords_str = models.TextField(blank=True, null=True) vulnote = models.ForeignKey( - 'VCVulnerabilityNote', - blank=True, null=True, - help_text=('This is used for VINCE published Vul Notes'), - on_delete=models.CASCADE) + "VCVulnerabilityNote", + blank=True, + null=True, + help_text=("This is used for VINCE published Vul Notes"), + on_delete=models.CASCADE, + ) + class VCVulnerabilityNote(models.Model): - content = models.TextField( - verbose_name=_('vulnote contents')) + content = models.TextField(verbose_name=_("vulnote contents")) - title = models.CharField( - max_length=512, - verbose_name=_('vul note title')) + title = models.CharField(max_length=512, verbose_name=_("vul note title")) - references = models.TextField( - blank=True, - null=True, - verbose_name=_('references')) + references = models.TextField(blank=True, null=True, verbose_name=_("references")) - dateupdated = models.DateTimeField( - default=timezone.now) + dateupdated = models.DateTimeField(default=timezone.now) - datefirstpublished = models.DateTimeField( - blank=True, null=True) + datefirstpublished = models.DateTimeField(blank=True, null=True) - revision_number = models.IntegerField( - default=1, - verbose_name=_('revision number')) + revision_number = models.IntegerField(default=1, verbose_name=_("revision number")) vuid = models.CharField(max_length=20) - publicdate = models.DateTimeField( - null=True, - blank=True) + publicdate = models.DateTimeField(null=True, blank=True) - published = models.BooleanField( - default=False) + published = models.BooleanField(default=False) def get_title(self): return "%s: %s" % (self.vu_vuid, self.title) @@ -2223,241 +2009,161 @@ def _get_idnumber(self): def __str__(self): return self.vuid + class VCNoteVulnerability(models.Model): - cve = models.CharField( - _('CVE'), - max_length=50, - blank=True, - null=True) + cve = models.CharField(_("CVE"), max_length=50, blank=True, null=True) - description = models.TextField( - _('Description')) + description = models.TextField(_("Description")) - note = models.ForeignKey( - VCVulnerabilityNote, - related_name="notevuls", - on_delete=models.CASCADE) + note = models.ForeignKey(VCVulnerabilityNote, related_name="notevuls", on_delete=models.CASCADE) - uid = models.CharField( - max_length=100) + uid = models.CharField(max_length=100) - case_increment = models.IntegerField( - default = 0) + case_increment = models.IntegerField(default=0) - date_added = models.DateTimeField( - default=timezone.now) + date_added = models.DateTimeField(default=timezone.now) - dateupdated= models.DateTimeField( - auto_now=True - ) + dateupdated = models.DateTimeField(auto_now=True) def __str__(self): return "%s" % self.vul def _get_vul(self): - """ A user-friendly Vul ID, which is the cve if cve exists, - otherwise it's a combination of vul ID and case. """ - if (self.cve): - return u"CVE-%s" % self.cve + """A user-friendly Vul ID, which is the cve if cve exists, + otherwise it's a combination of vul ID and case.""" + if self.cve: + return "CVE-%s" % self.cve else: - return u"%s" % self.cert_id + return "%s" % self.cert_id + vul = property(_get_vul) def _get_cert_id(self): - return u"%s%s.%d" % (settings.CASE_IDENTIFIER, self.note.vuid, self.case_increment) + return "%s%s.%d" % (settings.CASE_IDENTIFIER, self.note.vuid, self.case_increment) cert_id = property(_get_cert_id) + class VCVendor(models.Model): - note = models.ForeignKey( - VCVulnerabilityNote, - related_name="vendors", - on_delete=models.CASCADE) + note = models.ForeignKey(VCVulnerabilityNote, related_name="vendors", on_delete=models.CASCADE) contact_date = models.DateTimeField( - help_text=_('The date that this vendor was first contacted about this vulnerability.'), - blank=True, - null=True + help_text=_("The date that this vendor was first contacted about this vulnerability."), blank=True, null=True ) - vendor = models.CharField( - max_length=200, - help_text=_('The name of the vendor that may be affected.') - ) + vendor = models.CharField(max_length=200, help_text=_("The name of the vendor that may be affected.")) - uuid = models.UUIDField( - blank=True, - null=True, - help_text=_('The uuid of the contact.') - ) + uuid = models.UUIDField(blank=True, null=True, help_text=_("The uuid of the contact.")) - references = models.TextField( - help_text=_('Vendor references for this case'), - blank=True, - null=True) + references = models.TextField(help_text=_("Vendor references for this case"), blank=True, null=True) statement = models.TextField( - help_text=_('A general vendor statement for all vuls in the case'), - blank=True, - null=True) - - statement_date = models.DateTimeField( - blank=True, - null=True - ) - - addendum = models.TextField( - blank=True, - null=True) - - dateupdated = models.DateTimeField( - default=timezone.now + help_text=_("A general vendor statement for all vuls in the case"), blank=True, null=True ) + statement_date = models.DateTimeField(blank=True, null=True) + + addendum = models.TextField(blank=True, null=True) + + dateupdated = models.DateTimeField(default=timezone.now) + def get_status(self): - status = VCVendorVulStatus.objects.filter(vendor=self).order_by('status').first() + status = VCVendorVulStatus.objects.filter(vendor=self).order_by("status").first() if status: return status.get_status_display() else: return "Unknown" + class VCVendorVulStatus(models.Model): AFFECTED_STATUS = 1 UNAFFECTED_STATUS = 2 UNKNOWN_STATUS = 3 - STATUS_CHOICES = ( - (AFFECTED_STATUS, "Affected"), - (UNAFFECTED_STATUS, "Not Affected"), - (UNKNOWN_STATUS, "Unknown") - ) + STATUS_CHOICES = ((AFFECTED_STATUS, "Affected"), (UNAFFECTED_STATUS, "Not Affected"), (UNKNOWN_STATUS, "Unknown")) - vendor = models.ForeignKey( - VCVendor, - related_name="vendorvulstatus", - on_delete=models.CASCADE) + vendor = models.ForeignKey(VCVendor, related_name="vendorvulstatus", on_delete=models.CASCADE) - vul = models.ForeignKey( - VCNoteVulnerability, - related_name="notevulnerability", - on_delete=models.CASCADE) + vul = models.ForeignKey(VCNoteVulnerability, related_name="notevulnerability", on_delete=models.CASCADE) status = models.IntegerField( choices=STATUS_CHOICES, - default = UNKNOWN_STATUS, - help_text=_('The vendor status. Unknown until vendor says otherwise.') + default=UNKNOWN_STATUS, + help_text=_("The vendor status. Unknown until vendor says otherwise."), ) - date_added = models.DateTimeField( - default=timezone.now) + date_added = models.DateTimeField(default=timezone.now) - dateupdated = models.DateTimeField( - auto_now=True - ) + dateupdated = models.DateTimeField(auto_now=True) + + references = models.TextField(blank=True, null=True) + + statement = models.TextField(blank=True, null=True) - references = models.TextField( - blank=True, - null=True) - statement = models.TextField( - blank=True, - null=True) - class CaseTracking(models.Model): - case = models.ForeignKey( - Case, - on_delete=models.CASCADE) + case = models.ForeignKey(Case, on_delete=models.CASCADE) - group = models.ForeignKey( - Group, - on_delete=models.CASCADE) + group = models.ForeignKey(Group, on_delete=models.CASCADE) - tracking = models.CharField( - blank=True, null=True, - max_length=100) - - added_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - blank=True, null=True, - on_delete=models.SET_NULL) + tracking = models.CharField(blank=True, null=True, max_length=100) - dateupdated = models.DateTimeField( - auto_now=True - ) + added_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL) + + dateupdated = models.DateTimeField(auto_now=True) class Meta: - unique_together = (('group', 'case'),) - + unique_together = (("group", "case"),) + def __str__(self): return self.tracking + class CaseViewed(models.Model): - case = models.ForeignKey( - Case, - on_delete=models.CASCADE) - - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - blank=True, null=True, - on_delete=models.CASCADE) + case = models.ForeignKey(Case, on_delete=models.CASCADE) - date_viewed = models.DateTimeField( - default=timezone.now) + user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE) + + date_viewed = models.DateTimeField(default=timezone.now) class CaseCoordinator(models.Model): - case = models.ForeignKey( - Case, - on_delete=models.CASCADE) + case = models.ForeignKey(Case, on_delete=models.CASCADE) - assigned = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + assigned = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) def __str__(self): return f"%s is assigned to %s" % (self.assigned.get_username(), self.case.vu_vuid) - + class CaseMemberUserAccess(models.Model): - casemember = models.ForeignKey( - CaseMember, - on_delete=models.CASCADE) + casemember = models.ForeignKey(CaseMember, on_delete=models.CASCADE) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) admin = models.ForeignKey( settings.AUTH_USER_MODEL, - related_name='admin', + related_name="admin", help_text="User that made change", on_delete=models.SET_NULL, - blank=True, null=True) + blank=True, + null=True, + ) - date_modified = models.DateTimeField( - default=timezone.now) + date_modified = models.DateTimeField(default=timezone.now) class VCDailyNotification(models.Model): - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - case = models.ForeignKey( - Case, - on_delete=models.CASCADE) + case = models.ForeignKey(Case, on_delete=models.CASCADE) - posts = models.IntegerField( - default = 1) + posts = models.IntegerField(default=1) + + tracking = models.CharField(max_length=100, blank=True, null=True) - tracking = models.CharField( - max_length=100, - blank=True, - null=True - ) - def __str__(self): if self.tracking: if self.posts > 1: @@ -2468,66 +2174,45 @@ def __str__(self): return f"{self.posts} new posts in case {self.case.vu_vuid}." else: return f"{self.posts} new post in case {self.case.vu_vuid}." - + class VINCEEmailNotification(models.Model): - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) - case = models.ForeignKey( - Case, - blank=True, - null=True, - on_delete=models.SET_NULL) + case = models.ForeignKey(Case, blank=True, null=True, on_delete=models.SET_NULL) - summary = models.BooleanField( - default=False) + summary = models.BooleanField(default=False) - date_sent = models.DateTimeField( - default=timezone.now) + date_sent = models.DateTimeField(default=timezone.now) class UserApproveRequest(models.Model): - """ User making a request to join a Vendor Group """ - + """User making a request to join a Vendor Group""" + class Status(models.IntegerChoices): ACCEPTED = 1 DENIED = 0 UNKNOWN = -1 EXPIRED = -2 - status = models.IntegerField(choices=Status.choices,default=Status.UNKNOWN) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + status = models.IntegerField(choices=Status.choices, default=Status.UNKNOWN) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) created_at = models.DateTimeField(default=timezone.now) - + completed_at = models.DateTimeField(blank=True, null=True) - #Admin user or whoever rejected/approved this request. - completed_by = models.CharField( - max_length=255, - blank=True, - null=True - ) + # Admin user or whoever rejected/approved this request. + completed_by = models.CharField(max_length=255, blank=True, null=True) expires_at = models.DateTimeField(default=timezone.now() + timedelta(days=30)) - contact = models.ForeignKey( - VinceCommContact, - on_delete=models.CASCADE) + contact = models.ForeignKey(VinceCommContact, on_delete=models.CASCADE) - thread = models.ForeignKey( - Thread, - on_delete=models.DO_NOTHING, - blank=True, - null=True) + thread = models.ForeignKey(Thread, on_delete=models.DO_NOTHING, blank=True, null=True) class Meta: - unique_together = (('user', 'contact'),) + unique_together = (("user", "contact"),) def __str__(self): return f"User approve request for {self.user} to join {self.contact}" - diff --git a/vinny/serializers.py b/vinny/serializers.py index b524211..681c2ff 100644 --- a/vinny/serializers.py +++ b/vinny/serializers.py @@ -28,7 +28,17 @@ ######################################################################## from django.contrib.auth.models import User from rest_framework import serializers -from vinny.models import Case, Post, VTCaseRequest, CaseMemberStatus, CaseVulnerability, CaseMember, CaseStatement, VCVulnerabilityNote, VinceCommContact +from vinny.models import ( + Case, + Post, + VTCaseRequest, + CaseMemberStatus, + CaseVulnerability, + CaseMember, + CaseStatement, + VCVulnerabilityNote, + VinceCommContact, +) import uuid import os import json @@ -41,6 +51,7 @@ logger.setLevel(logging.DEBUG) + class VendorInfoSerializer(serializers.ModelSerializer): emails = serializers.SerializerMethodField() users = serializers.SerializerMethodField() @@ -50,54 +61,75 @@ def get_emails(self, obj): def get_users(self, obj): emails = obj.get_emails() - users = User.objects.using('vincecomm').filter(email__in=emails).values_list('vinceprofile__preferred_username', flat=True) + users = ( + User.objects.using("vincecomm") + .filter(email__in=emails) + .values_list("vinceprofile__preferred_username", flat=True) + ) return list(users) - + class Meta: model = VinceCommContact - fields = ['id', 'vendor_name', 'emails', 'users'] - + fields = ["id", "vendor_name", "emails", "users"] + class CaseSerializer(serializers.ModelSerializer): status = serializers.SerializerMethodField() - - + class Meta: model = Case - fields = ("vuid", "created", "status", "summary", "title", "due_date") + fields = ("vuid", "created", "status", "summary", "title", "due_date") def get_status(self, obj): return obj.get_status_display() class PostSerializer(serializers.ModelSerializer): - content = serializers.CharField(source='current_revision.content') + content = serializers.CharField(source="current_revision.content") author = serializers.SerializerMethodField() - + class Meta: model = Post fields = ("created", "author", "pinned", "content") def get_author(self, obj): if obj.author: - return {"org": obj.get_org_name(), - "active": obj.author.is_active, - "name": obj.author.vinceprofile.preferred_username, - "email": obj.author.email} + return { + "org": obj.get_org_name(), + "active": obj.author.is_active, + "name": obj.author.vinceprofile.preferred_username, + "email": obj.author.email, + } else: - return {"org": obj.get_org_name(), - "active": False, - "name": "User removed", - "email": "unknown@example.com"} - + return {"org": obj.get_org_name(), "active": False, "name": "User removed", "email": "unknown@example.com"} class OrigReportSerializer(serializers.ModelSerializer): class Meta: model = VTCaseRequest - fields = ('vendor_name', 'product_name', 'product_version', 'vul_description', 'vul_exploit', 'vul_impact', 'vul_discovery', 'vul_public', 'public_references', 'vul_exploited', 'exploit_references', 'vul_disclose', 'disclosure_plans', 'date_submitted', 'share_release', 'contact_name', 'contact_phone', 'contact_email', 'contact_org') - + fields = ( + "vendor_name", + "product_name", + "product_version", + "vul_description", + "vul_exploit", + "vul_impact", + "vul_discovery", + "vul_public", + "public_references", + "vul_exploited", + "exploit_references", + "vul_disclose", + "disclosure_plans", + "date_submitted", + "share_release", + "contact_name", + "contact_phone", + "contact_email", + "contact_org", + ) + def remove_fields_from_representation(self, representation, remove_fields): for remove_field in remove_fields: try: @@ -108,18 +140,19 @@ def remove_fields_from_representation(self, representation, remove_fields): def to_representation(self, obj): ret = super(OrigReportSerializer, self).to_representation(obj) if obj.share_release == False: - remove_fields = ('contact_name', 'contact_email', 'contact_phone', 'contact_org', 'share_release') + remove_fields = ("contact_name", "contact_email", "contact_phone", "contact_org", "share_release") self.remove_fields_from_representation(ret, remove_fields) return ret + class VendorStatusSerializer(serializers.ModelSerializer): status = serializers.SerializerMethodField() vulnerability = serializers.SerializerMethodField() vendor = serializers.SerializerMethodField() - statement_date = serializers.DateTimeField(source='date_added') + statement_date = serializers.DateTimeField(source="date_added") statement = serializers.SerializerMethodField() references = serializers.SerializerMethodField() - + class Meta: model = CaseMemberStatus fields = ["vulnerability", "vendor", "status", "statement", "references", "statement_date"] @@ -135,6 +168,7 @@ def get_statement(self, obj): return obj.statement else: return "" + def get_references(self, obj): if obj.member.share_status(): return obj.references @@ -150,18 +184,22 @@ def get_vendor(self, obj): except: return obj.member.group.name + class VendorStatusUpdateSerializer(serializers.ModelSerializer): vendor = serializers.IntegerField(required=False) vulnerability = serializers.CharField(max_length=50) - references = serializers.ListField(child=serializers.URLField(max_length=250, min_length=None, allow_blank=False), allow_empty=True) + references = serializers.ListField( + child=serializers.URLField(max_length=250, min_length=None, allow_blank=False), allow_empty=True + ) statement = serializers.CharField(max_length=2000, allow_blank=True) - status = serializers.ChoiceField(choices=['Affected', 'Not Affected', 'Unknown']) + status = serializers.ChoiceField(choices=["Affected", "Not Affected", "Unknown"]) share = serializers.BooleanField(default=False, required=False) - + class Meta: model = CaseMemberStatus fields = ["vendor", "status", "statement", "references", "vulnerability", "share"] - + + class VendorSerializer(serializers.ModelSerializer): vendor = serializers.SerializerMethodField() status = serializers.SerializerMethodField() @@ -170,14 +208,14 @@ class VendorSerializer(serializers.ModelSerializer): cert_addendum = serializers.SerializerMethodField() date_added = serializers.SerializerMethodField() statement_date = serializers.SerializerMethodField() - + class Meta: model = CaseMember fields = ["vendor", "status", "statement", "references", "date_added", "cert_addendum", "statement_date"] def get_date_added(self, obj): return obj.added - + def get_statement(self, obj): stmt = obj.get_statement() if stmt: @@ -189,7 +227,7 @@ def get_statement_date(self, obj): return stmt[0].date_modified else: return None - + def get_references(self, obj): stmt = obj.get_statement() if stmt: @@ -201,7 +239,7 @@ def get_cert_addendum(self, obj): return stmt.addendum else: return None - + def get_status(self, obj): if obj.share_status(): status = obj.get_general_status() @@ -212,37 +250,47 @@ def get_status(self, obj): else: return "Unknown" return "Unknown" - def get_vendor(self, obj): try: return obj.group.groupcontact.contact.vendor_name except: return obj.group.name - + + class VulSerializer(serializers.ModelSerializer): name = serializers.SerializerMethodField() - + class Meta: model = CaseVulnerability - fields = ['name', 'cve', 'description', 'date_added'] + fields = ["name", "cve", "description", "date_added"] def get_name(self, obj): return obj.vul - class VulNoteSerializer(serializers.ModelSerializer): revision = serializers.IntegerField(source="revision_number") references = serializers.SerializerMethodField() - + class Meta: model = VCVulnerabilityNote - fields = ["vuid", "title", "content", "references", "datefirstpublished", "vuid", "dateupdated", "published", "revision"] + fields = [ + "vuid", + "title", + "content", + "references", + "datefirstpublished", + "vuid", + "dateupdated", + "published", + "revision", + ] def get_references(self, obj): return obj.references.splitlines() + class CSAFSerializer(serializers.ModelSerializer): """ This serializer starts with a Case and then builds out using both @@ -254,35 +302,42 @@ class CSAFSerializer(serializers.ModelSerializer): status = cs[i].status (1 == 'AFFECTED', 2 == 'UNAFFECTED') vendor = cs[i].member.group.groupcontact.contact.vendor_name """ - document = serializers.SerializerMethodField('get_csafdocument') - vulnerabilities = serializers.SerializerMethodField('get_csafvuls') - product_tree = serializers.SerializerMethodField('get_csafprods') + + document = serializers.SerializerMethodField("get_csafdocument") + vulnerabilities = serializers.SerializerMethodField("get_csafvuls") + product_tree = serializers.SerializerMethodField("get_csafprods") mproduct_tree = {"branches": []} - template_json_dir = os.path.join(os.path.dirname(__file__), - 'templatesjson', 'csaf') - + template_json_dir = os.path.join(os.path.dirname(__file__), "templatesjson", "csaf") + class Meta: model = Case - fields = ["document","vulnerabilities","product_tree"] - + fields = ["document", "vulnerabilities", "product_tree"] def to_representation(self, case): ret = super().to_representation(case) - if not ret['vulnerabilities']: - del ret['product_tree'] - if hasattr(settings,'CSAF_VUL_EMPTY'): - ret['vulnerabilities'] = settings.CSAF_VUL_EMPTY + if not ret["vulnerabilities"]: + del ret["product_tree"] + if hasattr(settings, "CSAF_VUL_EMPTY"): + ret["vulnerabilities"] = settings.CSAF_VUL_EMPTY else: - ret['vulnerabilities'] = [{"notes": [{"category": "general","text": "No vulnerabilities have been defined at this time for this report"}]}] + ret["vulnerabilities"] = [ + { + "notes": [ + { + "category": "general", + "text": "No vulnerabilities have been defined at this time for this report", + } + ] + } + ] return ret - - def get_csafdocument(self,case): - tfile = os.path.join(self.template_json_dir,"document.json") + def get_csafdocument(self, case): + tfile = os.path.join(self.template_json_dir, "document.json") add_document = {} if not os.path.exists(tfile): return {"error": "Template file for csaf missing"} - csafdocument_template = open(tfile,"r").read() + csafdocument_template = open(tfile, "r").read() vulnote = reverse("vincepub:vudetail", args=[case.vuid]) # Either one of this is the way to know l.publicdate or l.published if case.publicdate or case.published: @@ -291,18 +346,18 @@ def get_csafdocument(self,case): else: publicurl = f"{settings.KB_SERVER_NAME}{vulnote}#PendingRelease" case_status = "interim" - if hasattr(settings,"CSAF_TLP_MAP") and settings.CSAF_TLP_MAP.get("PRIVATE"): + if hasattr(settings, "CSAF_TLP_MAP") and settings.CSAF_TLP_MAP.get("PRIVATE"): tlp_type = settings.CSAF_TLP_MAP.get("PRIVATE") - if hasattr(settings,"CSAF_DISTRIBUTION_OPTIONS") and settings.CSAF_DISTRIBUTION_OPTIONS.get(tlp_type): + if hasattr(settings, "CSAF_DISTRIBUTION_OPTIONS") and settings.CSAF_DISTRIBUTION_OPTIONS.get(tlp_type): add_document.update(settings.CSAF_DISTRIBUTION_OPTIONS.get(tlp_type)) ackurl = f"{settings.KB_SERVER_NAME}{vulnote}#acknowledgments" if case.modified: - revision_date = case.modified.isoformat(timespec='seconds') + revision_date = case.modified.isoformat(timespec="seconds") revision_number = case.modified.strftime("1.%Y%m%d%H%M%S.0") case_version = revision_number else: - revision_date = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec='seconds') + revision_date = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds") revision_number = revision_date.strftime("1.%Y%m%d%H%M%S.0") case_version = revision_number csafdocument = csafdocument_template % { @@ -311,7 +366,7 @@ def get_csafdocument(self,case): "summary": json.dumps(case.summary), "LEGAL_DISCLAIMER": settings.LEGAL_DISCLAIMER, "title": json.dumps(case.title), - "due_date": case.due_date, + "due_date": case.due_date.isoformat(timespec="seconds"), "VINCE_VERSION": settings.VERSION, "ORG_NAME": settings.ORG_NAME, "ORG_POLICY_URL": settings.ORG_POLICY_URL, @@ -323,9 +378,9 @@ def get_csafdocument(self,case): "revision_date": revision_date, "revision_number": revision_number, "case_status": case_status, - "case_version": case_version + "case_version": case_version, } - csafd = json.loads(csafdocument,strict=False) + csafd = json.loads(csafdocument, strict=False) csafd.update(add_document) return csafd @@ -335,14 +390,14 @@ def get_csafvuls(self, case): if not len(casevuls): return None csafvuls = [] - tfile = os.path.join(self.template_json_dir,"vulnerability.json") - csafvul_template = open(tfile,"r").read() - tfile = os.path.join(self.template_json_dir,"product_tree.json") + tfile = os.path.join(self.template_json_dir, "vulnerability.json") + csafvul_template = open(tfile, "r").read() + tfile = os.path.join(self.template_json_dir, "product_tree.json") if not os.path.exists(tfile): return [{"error": "Template file for csaf missing"}] - csafproduct_template = open(tfile,"r").read() + csafproduct_template = open(tfile, "r").read() for casevul in casevuls: - casems = list(CaseMemberStatus.objects.filter(vulnerability = casevul)) + casems = list(CaseMemberStatus.objects.filter(vulnerability=casevul)) known_affected = [] known_not_affected = [] if casevul.cve: @@ -352,14 +407,15 @@ def get_csafvuls(self, case): else: cve = None csafvul = csafvul_template % { - "vuid": casevul.vul, - "cve": cve, + "vuid": casevul.vul, + "cve": cve, "ORG_NAME": settings.ORG_NAME, - "title": json.dumps(casevul.description.split(".")[0]+"."), - "description": json.dumps(casevul.description) } - csafvulj = json.loads(csafvul,strict=False) + "title": json.dumps(casevul.description.split(".")[0] + "."), + "description": json.dumps(casevul.description), + } + csafvulj = json.loads(csafvul, strict=False) if cve is None: - del csafvulj['cve'] + del csafvulj["cve"] for casem in casems: try: vendor = casem.member.group.groupcontact.contact.vendor_name @@ -367,7 +423,7 @@ def get_csafvuls(self, case): logger.info(f"Strange vendor without a vendor name {casem} for case # {case}") vendor = "Unspecified" if case.published or casem.member.share_status(): - csaf_productid = "CSAFPID-"+str(uuid.uuid1()) + csaf_productid = "CSAFPID-" + str(uuid.uuid1()) else: logger.debug(f"Vendor {vendor} for case {case} is not sharing their status") continue @@ -375,23 +431,22 @@ def get_csafvuls(self, case): known_affected.append(csaf_productid) elif casem.status == 2: known_not_affected.append(csaf_productid) - #(1 == 'AFFECTED', 2 == 'UNAFFECTED') + # (1 == 'AFFECTED', 2 == 'UNAFFECTED') # we include products that are Unknown # so it is clear that we have anounced to this vendor # who has not responded. - csafproduct = csafproduct_template % { - "vendor_name": vendor, - "csaf_productid": csaf_productid } + csafproduct = csafproduct_template % {"vendor_name": vendor, "csaf_productid": csaf_productid} self.mproduct_tree["branches"].append(json.loads(csafproduct)) if len(known_affected) > 0: - if not 'product_status' in csafvulj: - csafvulj['product_status'] = {} - csafvulj['product_status']['known_affected'] = known_affected + if not "product_status" in csafvulj: + csafvulj["product_status"] = {} + csafvulj["product_status"]["known_affected"] = known_affected if len(known_not_affected) > 0: - if not 'product_status' in csafvulj: - csafvulj['product_status'] = {} - csafvulj['product_status']['known_not_affected'] = known_not_affected + if not "product_status" in csafvulj: + csafvulj["product_status"] = {} + csafvulj["product_status"]["known_not_affected"] = known_not_affected csafvuls.append(csafvulj) return csafvuls - def get_csafprods(self,case): + + def get_csafprods(self, case): return self.mproduct_tree diff --git a/vinny/templates/vinny/base.html b/vinny/templates/vinny/base.html index ff0c738..6c110e9 100644 --- a/vinny/templates/vinny/base.html +++ b/vinny/templates/vinny/base.html @@ -92,7 +92,7 @@

-
ATTENTION: VINCE web interface and API interfaces will be down for maintenance from 1200 EDT on Tuesday, March 19, 2024, until no later than 0900 EDT Wednesday, March 20, 2024.
+
diff --git a/vinny/templates/vinny/base_public.html b/vinny/templates/vinny/base_public.html index b5edc69..488ca37 100644 --- a/vinny/templates/vinny/base_public.html +++ b/vinny/templates/vinny/base_public.html @@ -91,7 +91,7 @@

-
ATTENTION: VINCE web interface and API interfaces will be down for maintenance from 1200 EDT on Tuesday, March 19, 2024, until no later than 0900 EDT Wednesday, March 20, 2024.
+
diff --git a/vinny/templates/vinny/cr_table.html b/vinny/templates/vinny/cr_table.html index 93169c8..9514e55 100644 --- a/vinny/templates/vinny/cr_table.html +++ b/vinny/templates/vinny/cr_table.html @@ -280,8 +280,8 @@

Vulnerability Report

{% if ticket.resolution %} - {% trans "Resolution" %}{% ifequal ticket.get_status_display "Resolved" %} - {% endifequal %}
+ {% trans "Resolution" %}{% if ticket.get_status_display == "Resolved" %} + {% endif %}
{{ ticket.resolution|force_escape|urlizetrunc:50|linebreaksbr }} {% endif %}
diff --git a/vinny/views.py b/vinny/views.py index 9806dbc..61a9dec 100644 --- a/vinny/views.py +++ b/vinny/views.py @@ -50,7 +50,7 @@ from django.core.validators import validate_email from django.core.exceptions import ValidationError, PermissionDenied from django.core import serializers -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.utils import timezone from django.db.models import Case as DBCase import pytz