From d988ff1e36ca0780e5970e1efe3a503331dfebdf Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Wed, 25 Oct 2023 12:33:52 -0400 Subject: [PATCH 1/2] VINCE upgrade to 2.1.6 --- CHANGELOG.md | 12 + bigvince/settings_.py | 2 +- requirements.txt | 4 +- vince/lib.py | 5 +- vince/mailer.py | 2 +- vince/models.py | 29 +- vince/permissions.py | 22 +- vince/static/vince/js/case.js | 258 ++++++++++-------- vince/static/vince/js/case_search.js | 56 ++-- vince/static/vince/js/vince.js | 225 +++++++-------- vince/templates/vince/case.html | 8 +- vince/templates/vince/cr.html | 4 +- vince/templates/vince/cr_table.html | 11 + .../tabs/case_original_report_tab.html | 11 + vince/templates/vince/printreport.html | 2 +- vince/templates/vince/printweeklyreport.html | 2 +- vince/templates/vince/reports.html | 2 +- vince/templates/vince/ticket.html | 8 +- vince/views.py | 49 ++-- vincepub/forms.py | 8 + vincepub/templates/vincepub/reportcoord.html | 5 + vincepub/templates/vincepub/success.html | 4 +- vincepub/views.py | 12 +- vinceworker/views.py | 3 +- vinny/forms.py | 11 + vinny/models.py | 16 ++ vinny/static/vinny/js/addfile.js | 8 +- vinny/static/vinny/js/vincecomm.js | 137 +++++----- vinny/templates/vinny/cr_table.html | 8 + vinny/views.py | 87 +++++- 30 files changed, 606 insertions(+), 405 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a4cca8..3f1fcf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # VINCE Changelog +Version 2.1.6 2023-10-25 + +* Fixed bug that interfered in certain circumstances with the operation of the vendor filter button on the VINCEComm case page +* Dependabot update recommendations: `urllib3` 1.26.12 to 1.26.18 +* Fixed bug that obstrcuted case assignment process for VINCETrack users with identical preferred usernames +* Adjusted code for asynchronous loading on ticket page to ensure it works on all ticket pages, including case request tickets +* Set up periodic autorefresh feature for VINCE Track ticket page +* Reformulated misleading UI labels for case transfer request process +* Resolved Issue by simpifying/correcting search code & disambiguating labels in report views +* Added AI/ML systems checkbox to public & VINCE Comm vul report form, routing of AI/ML-related tickets + + Version 2.1.5 2023-09-21 * Enhanced operation of VINCEComm case discussion section, moving focus to editable div when the user chooses to edit a post diff --git a/bigvince/settings_.py b/bigvince/settings_.py index d09348e..5d5478c 100644 --- a/bigvince/settings_.py +++ b/bigvince/settings_.py @@ -56,7 +56,7 @@ ROOT_DIR = environ.Path(__file__) - 3 # any change that requires database migrations is a minor release -VERSION = "2.1.5" +VERSION = "2.1.6" # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ diff --git a/requirements.txt b/requirements.txt index 7df43b3..c66be40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ chardet==5.0.0 charset-normalizer==2.1.1 click==8.1.3 colorama==0.4.4 -cryptography==41.0.4 +cryptography==41.0.3 cvelib==1.1.0 Deprecated==1.2.13 dictdiffer==0.9.0 @@ -75,7 +75,7 @@ six==1.16.0 soupsieve==2.3.2.post1 sqlparse==0.4.4 typing_extensions==4.4.0 -urllib3==1.26.12 +urllib3==1.26.18 vine==5.0.0 watchtower==3.0.0 webencodings==0.5.1 diff --git a/vince/lib.py b/vince/lib.py index e5c5676..2379685 100644 --- a/vince/lib.py +++ b/vince/lib.py @@ -397,6 +397,7 @@ def update_vinny_cr(instance): vtcr.vendor_communication = cr.vendor_communication vtcr.product_name = cr.product_name vtcr.ics_impact = cr.ics_impact + vtcr.metadata = cr.metadata vtcr.product_version = cr.product_version vtcr.vul_description = cr.vul_description vtcr.vul_exploit = cr.vul_exploit @@ -1678,7 +1679,7 @@ def parse_attachment(message_part): def create_ticket_for_error_email(filename, bucket, queue=None, from_email=None, body=None, cert_id=None, case=None): if queue == None: - queue = TicketQueue.objects.filter(queue_type=1, from_email=bucket).first() + queue = TicketQueue.objects.filter(queue_type=TicketQueue.GENERAL_TICKET_QUEUE, from_email=bucket).first() if len(body) > 5000: #truncate long bodies @@ -2176,7 +2177,7 @@ def create_ticket_from_email(filename, body, bucket): logger.debug(rq) #this is the default queue - the general queue for this bucket - queue = TicketQueue.objects.filter(from_email=bucket, queue_type=1).first() + queue = TicketQueue.objects.filter(from_email=bucket, queue_type=TicketQueue.GENERAL_TICKET_QUEUE).first() if queue == None: # this is misconfigured! send_error_sns("ticket queues", "misconfiguration", diff --git a/vince/mailer.py b/vince/mailer.py index d12fb47..a2913ac 100644 --- a/vince/mailer.py +++ b/vince/mailer.py @@ -516,7 +516,7 @@ def send_updatecase_mail(action, new_user=None): # cut a new ticket to alert if case.team_owner: tq = get_team_queues(case.team_owner) - queue = tq.filter(queue_type=2).first() + queue = tq.filter(queue_type=TicketQueue.CASE_REQUEST_QUEUE).first() else: queue = get_case_case_queue(case) diff --git a/vince/models.py b/vince/models.py index aa7fab8..b13aa43 100644 --- a/vince/models.py +++ b/vince/models.py @@ -55,17 +55,6 @@ logger.setLevel(logging.DEBUG) -GENERAL_TICKET_QUEUE = 1 -CASE_REQUEST_QUEUE = 2 -CASE_TASK_QUEUE = 3 -OTHER_QUEUE = 4 -QUEUE_TYPE = ( - (GENERAL_TICKET_QUEUE, _('General Ticket')), - (CASE_REQUEST_QUEUE, _('Case Request Queue')), - (CASE_TASK_QUEUE, _('Case Task Queue')), - (OTHER_QUEUE, _('Other Queue')) -) - class OldJSONField(JSONField): """ This was due to legacy support in Django 2.2. from_db_value should be explicitily sepcified when extending JSONField """ @@ -161,7 +150,7 @@ class GroupSettings(models.Model): def _get_triage(self): #get cr wueue - queue = TicketQueue.objects.filter(queue_type=2, team=self.group).first() + queue = TicketQueue.objects.filter(queue_type=TicketQueue.CASE_REQUEST_QUEUE, team=self.group).first() return queue triage = property(_get_triage) @@ -539,6 +528,17 @@ class TicketQueue(models.Model): a queue for each of Accounts, Pre-Sales, and Support. """ + GENERAL_TICKET_QUEUE = 1 + CASE_REQUEST_QUEUE = 2 + CASE_TASK_QUEUE = 3 + OTHER_QUEUE = 4 + QUEUE_TYPE = ( + (GENERAL_TICKET_QUEUE, _('General Ticket')), + (CASE_REQUEST_QUEUE, _('Case Request Queue')), + (CASE_TASK_QUEUE, _('Case Task Queue')), + (OTHER_QUEUE, _('Other Queue')) + ) + title = models.CharField(_('Title'), max_length=100) @@ -1924,6 +1924,11 @@ class CaseRequest(Ticket): vendor_communication = models.TextField(blank=True, null=True) 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 + ) ics_impact = models.BooleanField(default=False) vul_description = models.TextField(blank=True, null=True) vul_exploit = models.TextField(blank=True, null=True) diff --git a/vince/permissions.py b/vince/permissions.py index 77c89d1..0c58d1a 100644 --- a/vince/permissions.py +++ b/vince/permissions.py @@ -79,23 +79,27 @@ def get_r_queues(user): queues = QueuePermissions.objects.filter(group__in=user_groups, group_read=True).values_list('queue', flat=True) return queues -def get_case_case_queue(case): - groups = CasePermissions.objects.filter(case=case, group_write=True).exclude(group__groupsettings__contact__isnull=True).values_list('group', flat=True) - qperm = QueuePermissions.objects.filter(group__in=groups, group_write=True, queue__queue_type=3).first() - if qperm: - return qperm.queue +def get_case_case_queue(case, user=None): + if user: + groups = CasePermissions.objects.filter(case=case, group_write=True,group__in=user.groups.all()).exclude(group__groupsettings__contact__isnull=True).values_list('group', flat=True) + else: + groups = CasePermissions.objects.filter(case=case, group_write=True).exclude(group__groupsettings__contact__isnull=True).values_list('group', flat=True) + if groups: + qperm = QueuePermissions.objects.filter(group__in=groups, group_write=True, queue__queue_type=TicketQueue.CASE_TASK_QUEUE).first() + if qperm: + return qperm.queue return TicketQueue.objects.get(slug='case') def get_user_case_queue(user): user_groups = user.groups.exclude(groupsettings__contact__isnull=True) - perms = QueuePermissions.objects.filter(group__in=user_groups, group_read=True, group_write=True, queue__queue_type=3).first() + perms = QueuePermissions.objects.filter(group__in=user_groups, group_read=True, group_write=True, queue__queue_type=TicketQueue.CASE_TASK_QUEUE).first() if perms: return perms.queue return TicketQueue.objects.get(slug='case') def get_user_gen_queue(user): user_groups = user.groups.exclude(groupsettings__contact__isnull=True) - perms = QueuePermissions.objects.filter(group__in=user_groups, group_read=True, group_write=True, queue__queue_type=1).first() + perms = QueuePermissions.objects.filter(group__in=user_groups, group_read=True, group_write=True, queue__queue_type=TicketQueue.GENERAL_TICKET_QUEUE).first() if perms: return perms.queue return TicketQueue.objects.get(slug='gen') @@ -112,14 +116,14 @@ def get_vendor_queue(user): def get_user_cr_queue(user): user_groups = user.groups.exclude(groupsettings__contact__isnull=True) - perms = QueuePermissions.objects.filter(group__in=user_groups, group_read=True, group_write=True, queue__queue_type=2).first() + perms = QueuePermissions.objects.filter(group__in=user_groups, group_read=True, group_write=True, queue__queue_type=TicketQueue.CASE_REQUEST_QUEUE).first() if perms: return perms.queue return TicketQueue.objects.get(slug='cr') def get_all_cr_queue(user): user_groups = user.groups.exclude(groupsettings__contact__isnull=True) - perms = QueuePermissions.objects.filter(group__in=user_groups, group_read=True, group_write=True, queue__queue_type=2).values_list('queue', flat=True) + perms = QueuePermissions.objects.filter(group__in=user_groups, group_read=True, group_write=True, queue__queue_type=TicketQueue.CASE_REQUEST_QUEUE).values_list('queue', flat=True) if perms: return TicketQueue.objects.filter(id__in=perms) return None diff --git a/vince/static/vince/js/case.js b/vince/static/vince/js/case.js index 30d0692..db04588 100644 --- a/vince/static/vince/js/case.js +++ b/vince/static/vince/js/case.js @@ -128,10 +128,11 @@ function reloadArtifacts(case_id) { function reloadParticipants(case_id, tablet) { $.ajax({ - url: "/vince/ajax_calls/case/participants/"+case_id+"/", + url: "/vince/ajax_calls/case/participants/"+case_id+"/", success: function(data) { - tablet.replaceData(data); - }}); + tablet.replaceData(data); + } + }); } function reload_case_activity() { @@ -140,7 +141,7 @@ function reload_case_activity() { url: url, success: function(data) { $("#timeline").html(data); - /* reload plugins */ + /* reload plugins */ $(document).foundation(); } }); @@ -226,28 +227,28 @@ function getDate( element ) { } function auto(data, taggle, tag_url, modal) { + // This sets up the dropdown where you can select options as you type. var container = taggle.getContainer(); var input = taggle.getInput(); $(input).autocomplete({ - source: data, - appendTo:container, - position: { at: "left bottom", of: container }, - select: function(event, data) { - event.preventDefault(); - if (event.which === 1) { - taggle.add(data.item.value); - var csrftoken = getCookie('csrftoken'); - $.post(tag_url, - {'state': 1, 'add_tag': 1, 'csrfmiddlewaretoken': csrftoken, 'tag':data.item.value }, function(d) { - reload_case_activity(); - }) - .fail(function(d) { - alert("An error occurred while trying to add this tag."); - taggle.remove(data.item.value); - }); + source: data, + appendTo: container, + position: { at: "left bottom", of: container }, + select: function(event, data) { + event.preventDefault(); + if (event.which === 1) { + taggle.add(data.item.value); + var csrftoken = getCookie('csrftoken'); + $.post(tag_url, {'state': 1, 'add_tag': 1, 'csrfmiddlewaretoken': csrftoken, 'tag':data.item.value }, function(d) { + reload_case_activity(); + }) + .fail(function(d) { + alert("An error occurred while trying to add this tag."); + taggle.remove(data.item.value); + }); - } - } + } + } }); } @@ -281,53 +282,88 @@ $(document).ready(function() { } if (document.getElementById("user_taggs")) { - var tag_url = $("#user_taggs").attr("href"); - var case_id = $('.case-container').attr('caseid'); - var assigned_users = JSON.parse(document.getElementById('assigned_users').textContent); - var assignable = JSON.parse(document.getElementById('assignable').textContent); - var tags = []; - var taggle2 = new Taggle('user_taggs', { - tags: assigned_users, + var tag_url = $("#user_taggs").attr("href"); + var case_id = $('.case-container').attr('caseid'); + var assigned_ordered_pairs = JSON.parse(document.getElementById('assigned_ordered_pairs').textContent); + var assigned_users_emails = [] + for (let i = 0; i < assigned_ordered_pairs.length; i++){ + assigned_users_emails.push(assigned_ordered_pairs[i]['value']) + } + var assignable_ordered_pairs = JSON.parse(document.getElementById('assignable_ordered_pairs').textContent); + var assignable_emails = [] + for (let i = 0; i < assignable_ordered_pairs.length; i++){ + assignable_emails.push(assignable_ordered_pairs[i]['value']) + } + var taggle2 = new Taggle('user_taggs', { + tags: assigned_users_emails, duplicateTagClass: 'bounce', - preserveCase: true, - allowedTags: assignable, - placeholder: ["Tag a user..."], - onTagAdd: function(event, tag) { - if (event) { - var csrftoken = getCookie('csrftoken'); - var tag_url = $("#user_taggs").attr("href"); - $.post(tag_url, - {'state': 1, 'csrfmiddlewaretoken': csrftoken, 'tag':tag }, function(data) { - reload_case_activity(); - /*reload participants, because this will add a participant */ - reloadParticipants(case_id, participants_table); - }) - .fail(function(data) { - permissionDenied(addmodal); - taggle2.remove(tag); - }); - } - }, - onBeforeTagRemove: function(event, tag) { - if (event) { - var tag_url = $("#user_taggs").attr("href"); - var csrftoken = getCookie('csrftoken'); - var jqxhr = $.post(tag_url, - {'state': 0, 'csrfmiddlewaretoken': csrftoken, 'tag':tag}, function(data) { - reload_case_activity(); - /*reload participants, because this will rm a participant */ - reloadParticipants(case_id, participants_table); - }) - .fail(function(data) { - permissionDenied(addmodal); - taggle2.add(tag); - }); - } - return true; - }, - }); + preserveCase: true, + allowedTags: assignable_emails, + placeholder: ["Tag a user..."], + tagFormatter: function(li) { + spanElement = li.querySelector('.taggle_text') + inputElement = li.querySelector('input') + inputElementValue = li.querySelector('input').value + // find the element whose value = the current input, then change the spanElement innerHTML to the label in that element. + let objectWithThisEmail = assignable_ordered_pairs.filter(function checkit(object){ + if (object['value'] == inputElement.value){ + return object + } + })[0] + spanElement.setAttribute('title', spanElement.innerHTML) + spanElement.innerHTML = objectWithThisEmail['label'] + return li; + }, + onTagAdd: function(event, tag) { + // to be clear, tag is the value of the input element, not the text in the span element. + if (event) { + var csrftoken = getCookie('csrftoken'); + var tag_url = $("#user_taggs").attr("href"); + $.post(tag_url, {'state': 1, 'csrfmiddlewaretoken': csrftoken, 'tag':tag }, function(data) { + reload_case_activity(); + // reload participants, because this will add a participant. now that the participants tab laods async, + // we only want to do this if the participants table has loaded + if (participants_table){ + reloadParticipants(case_id, participants_table); + } + }) + .fail(function(data) { + permissionDenied(addmodal); + taggle2.remove(tag); + }); + } + }, + onBeforeTagRemove: function(event, tag) { + // to be clear, tag is the value of the input element, not the text in the span element. + if (event) { + var tag_url = $("#user_taggs").attr("href"); + var csrftoken = getCookie('csrftoken'); + var jqxhr = $.post(tag_url, {'state': 0, 'csrfmiddlewaretoken': csrftoken, 'tag':tag}, function(data) { + reload_case_activity(); + // reload participants, because this will add a participant. now that the participants tab laods async, + // we only want to do this if the participants table has loaded + if (participants_table){ + reloadParticipants(case_id, participants_table); + } + }) + .fail(function(data) { + permissionDenied(addmodal); + taggle2.add(tag); + }); + } + return true; + }, + }); + + let assignable_ordered_pairs_with_emails = assignable_ordered_pairs.map(function(originalOrderedPair){ + newOrderedPair = {} + newOrderedPair['label'] = originalOrderedPair['label'] + ' [' + originalOrderedPair['value'] + ']' + newOrderedPair['value'] = originalOrderedPair['value'] + return newOrderedPair + }) - auto(assignable, taggle2, tag_url, addmodal); + + auto(assignable_ordered_pairs_with_emails, taggle2, tag_url, addmodal); } @@ -337,7 +373,7 @@ $(document).ready(function() { var case_tags = JSON.parse(document.getElementById('case_tags').textContent); var case_avail_tags = JSON.parse(document.getElementById('case_available_tags').textContent); var tags = []; - var case_tag_url = $("#case_taggs").attr("href"); + var case_tag_url = $("#case_taggs").attr("href"); var casetaggle = new Taggle('case_taggs', { tags: case_tags, tagFormatter: function(li) { @@ -352,7 +388,7 @@ $(document).ready(function() { placeholder: ["Tag this case..."], onTagAdd: function(event, tag) { if (event) { - var case_tag_url = $("#case_taggs").attr("href"); + var case_tag_url = $("#case_taggs").attr("href"); var csrftoken = getCookie('csrftoken'); $.post(case_tag_url, {'add_tag': 1, 'csrfmiddlewaretoken': csrftoken, 'tag':tag }, function(data) { @@ -933,6 +969,7 @@ $(document).ready(function() { $.post(url, {'csrfmiddlewaretoken': csrftoken, 'coordinator': val}, function(data) { + console.log('#type_submit has been clicked') reloadParticipants($(".case-container").attr('caseid'), participants_table); }) .done(function() { @@ -1997,6 +2034,7 @@ $(document).ready(function() { $.post(url, {'csrfmiddlewaretoken': csrftoken, 'users': vendors, 'case_id': case_id}, function(data) { + console.log('#adduserform has been submitted') reloadParticipants(case_id, participants_table); }) .done(function() { @@ -2011,40 +2049,36 @@ $(document).ready(function() { participants_table = new Tabulator("#participant-table", { data:participants_data, //set initial table data layout:"fitColumns", - tooltipsHeader:true, + tooltipsHeader:true, selectable:true, - dataEdited:function(data) { - - var csrftoken=getCookie('csrftoken'); - for (i=0; i < data.length; i++) { - var url = data[i].changetype; - var type = data[i].role; - if (type == "Coordinator") { - type = "True"; - } else { - type = "False"; - } - $.post(url, {'csrfmiddlewaretoken': csrftoken, 'coordinator': type}, - function(data) { - }) - .done(function() { - reloadParticipants($(".case-container").attr('caseid'), participants_table); - - }) - .fail(function(d) { - permissionDenied(addmodal); - }); - } - }, - placeholder: "There are no participants in this case", + dataEdited:function(data) { + var csrftoken=getCookie('csrftoken'); + for (i=0; i < data.length; i++) { + var url = data[i].changetype; + var type = data[i].role; + if (type == "Coordinator") { + type = "True"; + } else { + type = "False"; + } + $.post(url, {'csrfmiddlewaretoken': csrftoken, 'coordinator': type}, function(data) {}) + .done(function() { + console.log('the participants table has just been loaded') + reloadParticipants($(".case-container").attr('caseid'), participants_table); + }) + .fail(function(d) { + permissionDenied(addmodal); + }); + } + }, + placeholder: "There are no participants in this case", columns:[ {title:"Name", field:"name", formatter:participantClickFunction}, {title:"Date Notified", field:"notified"}, {titleFormatter: roleFormatterFunction, field:"role", editor:"select", editorParams: {values: {"Coordinator":"Coordinator", "Reporter":"Reporter"}}}, - {title:"Seen", field:"seen", formatter:participantSeenFormatter}, - {title:"Status", field:"status", formatter: statusFormatter}, + {title:"Seen", field:"seen", formatter:participantSeenFormatter}, + {title:"Status", field:"status", formatter: statusFormatter}, ], - }); } @@ -2067,29 +2101,33 @@ $(document).ready(function() { window.setTimeout(checkFlag, 100); } if (selectedRows[i].getData().rm_confirm) { - flag = false; - $.ajax({ + flag = false; + $.ajax({ url: selectedRows[i].getData().remove_link, success: function(data) { - approvemodal.html(data).foundation('open'); + approvemodal.html(data).foundation('open'); }, - error: function(xhr, status) { - permissionDenied(approvemodal); - }}); + error: function(xhr, status) { + permissionDenied(approvemodal); + } + }); } else { - $.ajax({ + $.ajax({ url: selectedRows[i].getData().remove_link, success: function(data) { - sleep(2000); - reloadParticipants($(".case-container").attr('caseid'), participants_table); + sleep(2000); + console.log('remove-participant has just been clicked') + reloadParticipants($(".case-container").attr('caseid'), participants_table); }, - error: function(xhr, status) { - permissionDenied(approvemodal); - }}); - flag = true; + error: function(xhr, status) { + permissionDenied(approvemodal); + } + }); + flag = true; } } + console.log('remove-participant has just been clicked and is almost finished processing') reloadParticipants($(".case-container").attr('caseid'), participants_table); }); diff --git a/vince/static/vince/js/case_search.js b/vince/static/vince/js/case_search.js index 0f1f8a0..13beab7 100644 --- a/vince/static/vince/js/case_search.js +++ b/vince/static/vince/js/case_search.js @@ -60,7 +60,7 @@ var txhr = null; function searchTickets(e) { if (e) { - e.preventDefault(); + e.preventDefault(); } $("#id_page").val("1"); var url = "/vince/case/results/"; @@ -69,44 +69,48 @@ function searchTickets(e) { } lockunlock(true,'div.mainbody,div.vtmainbody','#searchresults'); txhr = $.ajax({ - url: url, - type: "POST", - data: $('#searchform').serialize(), - success: function(data) { - lockunlock(false,'div.mainbody,div.vtmainbody','#searchresults'); - $("#searchresults").html(data); - }, - error: function() { - lockunlock(false,'div.mainbody,div.vtmainbody','#searchresults'); - console.log(arguments); - alert("Search failed or canceled! See console log for details."); - }, - complete: function() { - /* Just safety net */ - lockunlock(false,'div.mainbody,div.vtmainbody','#searchresults'); - window.txhr = null; - } - }); + url: url, + type: "POST", + data: $('#searchform').serialize(), + success: function(data) { + lockunlock(false,'div.mainbody,div.vtmainbody','#searchresults'); + $("#searchresults").html(data); + }, + error: function() { + lockunlock(false,'div.mainbody,div.vtmainbody','#searchresults'); + console.log(arguments); + alert("Search failed or canceled! See console log for details."); + }, + complete: function() { + /* Just safety net */ + lockunlock(false,'div.mainbody,div.vtmainbody','#searchresults'); + window.txhr = null; + } + }); } $(document).ready(function() { + // dead code: $(document).on("click", '.search_page', function(event) { - var page = $(this).attr('next'); - nextPage(page); + var page = $(this).attr('next'); + nextPage(page); }); + // dead code: $(document).on("click", '.search_notes', function(event) { - var page = $(this).attr('next'); - nextTickets(page); + var page = $(this).attr('next'); + nextTickets(page); }); + // this is it: var input = document.getElementById("id_wordSearch"); - input.addEventListener("keydown", function(event) { - if (event.keyCode == 13) { + input.addEventListener("keydown", function(event) { + if (event.keyCode == 13) { searchTickets(event); - } + } }); + // so when you press enter, you trigger searchTickets (above) with the data in #searchform var form = document.getElementById('searchform'); if (form.attachEvent) { diff --git a/vince/static/vince/js/vince.js b/vince/static/vince/js/vince.js index 3f1f81b..6e5cb72 100644 --- a/vince/static/vince/js/vince.js +++ b/vince/static/vince/js/vince.js @@ -125,12 +125,6 @@ function asyncLoad(fdiv,furl,fmethod,pdiv,formpost,silent,transform) { fdata = $(formpost).serialize(); } lockunlock(true,pdiv,fdiv); - console.log('furl is ') - console.log(furl) - console.log('fmethod is ') - console.log(fmethod) - console.log('fdata is ') - console.log(fdata) window.txhr = $.ajax({ url : furl, type: fmethod, @@ -141,7 +135,6 @@ function asyncLoad(fdiv,furl,fmethod,pdiv,formpost,silent,transform) { $(fdiv).html(transform(data)).foundation(); else $(fdiv).html(data).foundation(); - console.log($(fdiv)) }, error: function() { lockunlock(false,pdiv,fdiv); @@ -288,36 +281,56 @@ $(function () { }); */ - // The following 70 lines or so is a rough draft that needs substantial work for getting parts of the case & ticket pages to refresh automatically. - // let periodBetweenInactivityChecks = 10000; - // let autorefreshInterval = 60000; + let autorefreshInterval = 60000; // let time = new Date().getTime(); // $(document.body).bind("mousemove keypress", function(e) { // time = new Date().getTime(); // }); - // function autorefresh() { - // console.log('autorefresh is starting') - // let request = new XMLHttpRequest(); - // request.open('GET', location.href.replace(location.hash,"")); - // request.onload = function () { - // if (request.response){ - // let retrievedHTML = request.response; - // const parser = new DOMParser(); - // const parsedHTML = parser.parseFromString(retrievedHTML, "text/html"); - // $('.shouldautorefresh').each(function() { - // current_id = $(this).attr("id"); - // html_to_inject = parsedHTML.querySelectorAll("[id='"+current_id+"']")[0].innerHTML - // if ($(this).html() != html_to_inject) { - // console.log('we are replacing the html') - // $(this).html(html_to_inject) - // } - // }); - // } - // }; - // request.send(); + function autorefresh() { + + let autorefreshers = document.querySelectorAll('.shouldautorefresh'); + + for (let i = 0; i < autorefreshers.length; i++) { + if (autorefreshers[i].classList.contains('asyncload')) { + asyncLoad(autorefreshers[i]) + } + } + + + // this code is a rough draft, requiring serious revision, of what might work if we want to reload page sections that do not have '.asyncload' + + // console.log('autorefresh is starting') + // let request = new XMLHttpRequest(); + // console.log('we are GETting from ' + location.href.replace(location.hash,"")) + // request.open('GET', location.href.replace(location.hash,"")); + // request.onload = function () { + // if (request.response){ + // let retrievedHTML = request.response; + // console.log('retrievedHTML is ') + // console.log(retrievedHTML) + // const parser = new DOMParser(); + // const parsedHTML = parser.parseFromString(retrievedHTML, "text/html"); + // $('.shouldautorefresh').each(function() { + // current_id = $(this).attr("id"); + // console.log('current_id is ') + // console.log(current_id) + // html_to_inject = parsedHTML.querySelectorAll("[id='"+current_id+"']")[0].innerHTML + // console.log('html_to_inject is') + // console.log(html_to_inject) + // console.log('$(this).html() is') + // console.log($(this).html()) + // console.log('it is ' + ($(this).html() != html_to_inject) + ' that $(this).html() is not equal to html_to_inject') + // if ($(this).html() != html_to_inject) { + // console.log('we are replacing the html') + // $(this).html(html_to_inject) + // } + // }); + // } + // }; + // request.send(); @@ -333,32 +346,11 @@ $(function () { // } // } - // } + } - // if ($('.shouldautorefresh')[0]){ - // setTimeout(autorefresh, autorefreshInterval); - // } - - // This works like a charm, but the above might be better: - - // let autorefreshInterval = 60000 - - // function autorefresh(){ - // inputText = document.getElementById("commentBox").value - // if (inputText == ''){ - // $.get(location.href,function(retrievedHTML) { - // const parser = new DOMParser(); - // const parsedHTML = parser.parseFromString(retrievedHTML, "text/html"); - // if (!parsedHTML.isEqualNode(document)) { - // location.reload() - // } - // }) - // } - // }; - - // if ($('.shouldautorefresh')[0]){ - // setInterval(autorefresh, autorefreshInterval); - // } + if ($('.shouldautorefresh')[0]){ + setInterval(autorefresh, autorefreshInterval); + } var tabIDsoughtviaurl = $(location).prop('hash').substr(1); @@ -409,83 +401,74 @@ $(function () { return false; }); function post_content_refresh(ctx) { - /* This function runs on all divs that can use SHOW_MORE - and SHOW_LESS*/ - if(!ctx) - ctx = document.body; - var lines = 12; - var buttonspacing = 0; - var buttonside = 'left'; - var animationspeed = 1000; - var lineheight = 0; - if ($('.text_content',ctx).css("line-height")) { - lineheight = $('.text_content',ctx).css("line-height"). - replace("px", ""); - } - var startheight = lineheight * lines; - var shortheight = $('.textheightshort',ctx).css('max-height'); - var buttonheight = $('.showfull',ctx).height(); - $('div.long_text_container',ctx).css( - {'padding-bottom' : (buttonheight + buttonspacing ) }); - - if(buttonside == 'right'){ - $('.showfull',ctx).css( - {'bottom' : (buttonspacing / 2), 'right' : buttonspacing }); - } else{ - $('.showfull',ctx).css( - {'bottom' : (buttonspacing / 2), 'left' : buttonspacing }); - } + /* This function runs on all divs that can use SHOW_MORE + and SHOW_LESS*/ + if(!ctx) { + ctx = document.body; + } + var lines = 12; + var buttonspacing = 0; + var buttonside = 'left'; + var animationspeed = 1000; + var lineheight = 0; + if ($('.text_content',ctx).css("line-height")) { + lineheight = $('.text_content',ctx).css("line-height"). + replace("px", ""); + } + var startheight = lineheight * lines; + var shortheight = $('.textheightshort',ctx).css('max-height'); + var buttonheight = $('.showfull',ctx).height(); + $('div.long_text_container',ctx).css({'padding-bottom' : (buttonheight + buttonspacing ) }); + + if(buttonside == 'right'){ + $('.showfull',ctx).css({'bottom' : (buttonspacing / 2), 'right' : buttonspacing }); + } else{ + $('.showfull',ctx).css({'bottom' : (buttonspacing / 2), 'left' : buttonspacing }); + } - $('.moretext',ctx).on('click', function(){ - var newheight = $(this).parent('div.long_text_container'). - find('div.text_content').height(); - $(this).parent('div.long_text_container'). - find('div.text_container'). - animate({'max-height' : newheight }, animationspeed ); - $(this).hide().siblings('.lesstext').show(); - $(this).next().next('.scrollnext').fadeIn(); + $('.moretext',ctx).on('click', function(){ + var newheight = $(this).parent('div.long_text_container').find('div.text_content').height(); + $(this).parent('div.long_text_container').find('div.text_container').animate({'max-height' : newheight }, animationspeed ); + $(this).hide().siblings('.lesstext').show(); + $(this).next().next('.scrollnext').fadeIn(); - }); + }); - $('.lesstext',ctx).on('click', function(){ - var shortelem = $(this).parent('div.long_text_container'). - find('div.text_container').hasClass('textheightshort'); - var newheight = startheight; - if (shortelem) { - newheight = shortheight; - } - var h = $(this).parent('div.long_text_container'). - find('div.text_content').height(); - $(this).parent('div.long_text_container'). - find('div.text_container'). - animate({'max-height' : newheight }, animationspeed ); - $(this).hide().siblings('.moretext').show(); - $(this).next('.scrollnext').fadeOut(); - }); + $('.lesstext',ctx).on('click', function(){ + var shortelem = $(this).parent('div.long_text_container').find('div.text_container').hasClass('textheightshort'); + var newheight = startheight; + if (shortelem) { + newheight = shortheight; + } + var h = $(this).parent('div.long_text_container').find('div.text_content').height(); + $(this).parent('div.long_text_container').find('div.text_container').animate({'max-height' : newheight }, animationspeed ); + $(this).hide().siblings('.moretext').show(); + $(this).next('.scrollnext').fadeOut(); + }); - $('div.long_text_container',ctx).each(function(){ - var elm = $(this).find('div.text_content'); - if( elm.height() > $(this).find('div.text_container').height()){ - $(this).find('.showfull.moretext').show(); + $('div.long_text_container',ctx).each(function(){ + var elm = $(this).find('div.text_content'); + if( elm.height() > $(this).find('div.text_container').height()){ + $(this).find('.showfull.moretext').show(); - } - }); + } + }); } /* Run post_content_refresh once and then on any divs that are dynamically generated */ post_content_refresh(); function mutation_refresh(mu,ob) { - console.log(arguments); - if(mu.length && mu[0].target) { - if($(mu[0].target).attr("onmutate")) { - post_content_refresh(mu[0].target); - } - } + console.log(arguments); + if(mu.length && mu[0].target) { + if($(mu[0].target).attr("onmutate")) { + post_content_refresh(mu[0].target); + } + } } $('.asyncrefresh').each(function() { - console.log(arguments); - let ob = new MutationObserver(mutation_refresh); - ob.observe(this,{childList:true}); + console.log(arguments); + let ob = new MutationObserver(mutation_refresh); + ob.observe(this,{childList:true}); }); $(document).keyup(function(e) { if (e.key === "Escape") { @@ -499,7 +482,7 @@ $(function () { /* All asyncload class div with autoload should be populated async on document.ready() */ $('div.asyncload.autoload').each(function(_,fdiv) { - asyncLoad(fdiv); + asyncLoad(fdiv); }); /* Create async onclick loaders via buttons if any */ $('.asyncclick').on("click", clickAsyncLoad); diff --git a/vince/templates/vince/case.html b/vince/templates/vince/case.html index f16e209..fe86fb0 100644 --- a/vince/templates/vince/case.html +++ b/vince/templates/vince/case.html @@ -37,8 +37,8 @@

{% if case.team_owner %}{{ case.team_owner.name }} {% endif %}Case -{{ assigned_users|json_script:"assigned_users" }} -{{ assignable|json_script:"assignable" }} +{{ assigned_ordered_pairs|json_script:"assigned_ordered_pairs" }} +{{ assignable_ordered_pairs|json_script:"assignable_ordered_pairs" }} {{ artifactsjs|json_script:"artifacts" }}
{{ case_assigned_to }}
@@ -69,7 +69,7 @@

{% if case.team_owner %}{{ case.team_owner.name }} {% endif %}Case
-
+
{% include 'vince/case_summary.html' %}
@@ -85,7 +85,7 @@

Artifacts

-
+
{% include 'vince/include/artifacts.html' with show_ticket_info=True form=artifact_form %}
diff --git a/vince/templates/vince/cr.html b/vince/templates/vince/cr.html index c47cb7f..c5832ed 100644 --- a/vince/templates/vince/cr.html +++ b/vince/templates/vince/cr.html @@ -67,8 +67,8 @@

Artifacts


-
-{% include 'vince/ticket_activity.html' %} +
+

diff --git a/vince/templates/vince/cr_table.html b/vince/templates/vince/cr_table.html index d22a470..4fbe1b0 100644 --- a/vince/templates/vince/cr_table.html +++ b/vince/templates/vince/cr_table.html @@ -157,6 +157,17 @@

{{ ticket.title }}

+
+
+ {% if ai_ml_system %} + AI/ML: This vulnerability is related to AI/ML systems. + {% else %} + AI/ML: No indication of a relationship to AI/ML systems. + {% endif %} +
+
+ +
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 981b0b2..0d8e923 100644 --- a/vince/templates/vince/include/tabs/case_original_report_tab.html +++ b/vince/templates/vince/include/tabs/case_original_report_tab.html @@ -118,6 +118,17 @@

{{ cr.title }}

+
+
+ {% if ticket.ai_ml_system %} + AI/ML: This vulnerability is related to AI/ML systems. + {% else %} + AI/ML: No indication of a relationship to AI/ML systems. + {% endif %} +
+
+ +
diff --git a/vince/templates/vince/printreport.html b/vince/templates/vince/printreport.html index f171c4c..aa4b766 100644 --- a/vince/templates/vince/printreport.html +++ b/vince/templates/vince/printreport.html @@ -122,7 +122,7 @@

Cases

- Active Cases + Pre-existing Active Cases {{ case_stats.active_cases | length }} diff --git a/vince/templates/vince/printweeklyreport.html b/vince/templates/vince/printweeklyreport.html index 71be5b3..7cd2afe 100644 --- a/vince/templates/vince/printweeklyreport.html +++ b/vince/templates/vince/printweeklyreport.html @@ -123,7 +123,7 @@

Cases

- Active Cases + Pre-existing Active Cases {{ case_stats.active_cases | length }} diff --git a/vince/templates/vince/reports.html b/vince/templates/vince/reports.html index 4632c5f..8dfe32a 100644 --- a/vince/templates/vince/reports.html +++ b/vince/templates/vince/reports.html @@ -118,7 +118,7 @@

Cases

{{ case_stats.new_cases | length }} New Cases
  • - {{ case_stats.active_cases | length }} Active Cases + {{ case_stats.active_cases | length }} Pre-existing Active Cases
  • {{ case_stats.deactive_cases | length }} Deactivated Cases diff --git a/vince/templates/vince/ticket.html b/vince/templates/vince/ticket.html index c4ce6b9..2521801 100644 --- a/vince/templates/vince/ticket.html +++ b/vince/templates/vince/ticket.html @@ -57,7 +57,7 @@

    Ticket [{{ ticket.queue }}-{{ ticket.id }}] {% if ticket.case %} -
    -
    +
    {% include 'vince/include/artifacts.html' with empty_message="No artifacts have been created for this ticket" %}

    -
    +

    diff --git a/vince/views.py b/vince/views.py index 2e341df..85d50b0 100644 --- a/vince/views.py +++ b/vince/views.py @@ -2289,9 +2289,7 @@ def post(self, request, *args, **kwargs): if 'team' in self.request.POST: teamlist = self.request.POST.getlist('team') - groups = Group.objects.filter(id__in=teamlist) - cases = list(CaseAssignment.objects.filter(assigned__groups__in=groups).values_list('case', flat=True)) - res = res.filter(id__in=cases) + res = res.filter(team_owner__in=teamlist) if 'changes_to_publish' in self.request.POST: res = res.filter(vulnote__date_published__isnull=False,changes_to_publish=True) @@ -2473,7 +2471,7 @@ def post(self, request, *args, **kwargs): if CasePermissions.objects.filter(case=ticket.case, group=new_group, group_read=True, group_write=True).exists(): logger.debug(f"new_group {new_group} has access to case {ticket.case}") # this case can be read/write by the group - so change it to appropriate case queue - new_queue = QueuePermissions.objects.filter(queue__queue_type=2, group__in=[new_group], group_read=True, group_write=True).first() + new_queue = QueuePermissions.objects.filter(queue__queue_type=TicketQueue.CASE_REQUEST_QUEUE, group__in=[new_group], group_read=True, group_write=True).first() if new_queue: ticket.queue = new_queue.queue tqa = True @@ -2494,7 +2492,7 @@ def post(self, request, *args, **kwargs): #this group doesn't have access to the case or this doesn't belong to a case # put this in General queue if not tqa: - new_queue = QueuePermissions.objects.filter(queue__queue_type=1, group__in=[new_group], group_read=True, group_write=True).first() + new_queue = QueuePermissions.objects.filter(queue__queue_type=TicketQueue.GENERAL_TICKET_QUEUE, group__in=[new_group], group_read=True, group_write=True).first() if new_queue: ticket.queue = new_queue.queue ticket.assigned_to = None @@ -2818,7 +2816,7 @@ def get_context_data(self, **kwargs): form = TicketForm(initial=initial_data) if case: - queue = get_case_case_queue(case) + queue = get_case_case_queue(case, self.request.user) form.fields['queue'].choices = [(queue.id, queue.title)] else: form.fields['queue'].choices = [('', '--------')] + [ @@ -2881,7 +2879,7 @@ def get_context_data(self, **kwargs): context['other_teams'] = user_groups.exclude(id=user_groups[0].id) user_groups=[user_groups[0]] - queue = TicketQueue.objects.filter(queuepermissions__group__in=user_groups, queuepermissions__group_write=True, queue_type=2).first() + queue = TicketQueue.objects.filter(queuepermissions__group__in=user_groups, queuepermissions__group_write=True, queue_type=TicketQueue.CASE_REQUEST_QUEUE).first() if queue == None: context['misconfiguration'] = 1 @@ -3153,7 +3151,7 @@ def form_valid(self, form): if ticket.queue.team: if self.request.user.groups.filter(id=ticket.queue.team.id).exists(): #get case queue for this team - ticket.queue = TicketQueue.objects.filter(queue_type=3, team=ticket.queue.team).first() + ticket.queue = TicketQueue.objects.filter(queue_type=TicketQueue.CASE_TASK_QUEUE, team=ticket.queue.team).first() # if it returns none, then try the default if ticket.queue == None: ticket.queue = get_user_case_queue(self.request.user) @@ -4354,6 +4352,10 @@ def post(self, request, *args, **kwargs): def get_context_data(self, **kwargs): context = super(CaseRequestView, self).get_context_data(**kwargs) context['ticket'] = get_object_or_404(CaseRequest, id=self.kwargs['pk']) + if deepGet(context["ticket"].metadata,"ai_ml_system") == True: + context['ai_ml_system'] = True + else: + context['ai_ml_system'] = False user_groups = context['ticket'].queue.queuepermissions_set.filter(group_read=True, group_write=True).values_list('group', flat=True) if context['ticket'].assigned_to: context['assignable_users'] = User.objects.filter(is_active=True, groups__in=user_groups).order_by(User.USERNAME_FIELD).exclude(id=context['ticket'].assigned_to.id) @@ -5647,7 +5649,7 @@ def _transfer_case(case, new_group): #get CR queue permissions of new_group and assign same case permssions to case - qp = QueuePermissions.objects.filter(queue__queue_type=2, queue__team=new_group) + qp = QueuePermissions.objects.filter(queue__queue_type=TicketQueue.CASE_REQUEST_QUEUE, queue__team=new_group) for x in qp: cp = CasePermissions.objects.update_or_create(group=x.group, case=case, defaults = {'group_read':x.group_read, @@ -5751,7 +5753,7 @@ def post(self, request, *args, **kwargs): case = case, description = f"Case Request Transfer Reason: {form.cleaned_data['reason']}\r\n\r\n"\ f"Link to case: {settings.SERVER_NAME}{case_url}\r\n\r\n"\ - f"Accept Transfer: {settings.SERVER_NAME}{case_accept_url}") + f"Accept or Reject Transfer Request: {settings.SERVER_NAME}{case_accept_url}") if ca: ticket.assigned_to = ca.assigned ticket.save() @@ -5803,7 +5805,7 @@ def post(self, request, *args, **kwargs): case = case, description = f"Case Request Transfer Reason: {form.cleaned_data['reason']}\r\n\r\n"\ f"Link to case: {settings.SERVER_NAME}{case_url}\r\n"\ - f"Accept Transfer: {settings.SERVER_NAME}{case_accept_url}") + f"Accept or Reject Transfer Request: {settings.SERVER_NAME}{case_accept_url}") if ca: ticket.assigned_to = ca.assigned ticket.save() @@ -5828,9 +5830,9 @@ def post(self, request, *args, **kwargs): else: #this is most likely the person assigned to the case #trying to hand off to another team - new_queue = QueuePermissions.objects.filter(queue__queue_type=1, group__in=[new_group], group_read=True, group_write=True).first() + new_queue = QueuePermissions.objects.filter(queue__queue_type=TicketQueue.GENERAL_TICKET_QUEUE, group__in=[new_group], group_read=True, group_write=True).first() if new_queue == None: - new_queue = QueuePermissions.objects.filter(queue__queue_type=2, group__in=[new_group], group_read=True, group_write=True).first() + new_queue = QueuePermissions.objects.filter(queue__queue_type=TicketQueue.CASE_REQUEST_QUEUE, group__in=[new_group], group_read=True, group_write=True).first() if new_queue: ticket = Ticket(title=f"Case Transfer Request for {case.vu_vuid}", submitter_email=self.request.user.email, @@ -5838,7 +5840,7 @@ def post(self, request, *args, **kwargs): queue = new_queue.queue, description = f"Case Request Transfer Reason: {form.cleaned_data['reason']}\r\n\r\n"\ f"Link to case: {settings.SERVER_NAME}{case_url}\r\n\r\n"\ - f"Accept Transfer: {settings.SERVER_NAME}{case_accept_url}") + f"Accept or Reject Transfer Request: {settings.SERVER_NAME}{case_accept_url}") ticket.save() fup = FollowUp(ticket=ticket, title=f"Case Transfer Requested. Request to transfer to {new_group.name}", @@ -5906,8 +5908,18 @@ def get_context_data(self, **kwargs): context['ticket_list'] = Ticket.objects.filter(case=context['case']) #context['ticketsjs'] = [ obj.as_dict() for obj in context['ticket_list'] ] users = CaseAssignment.objects.filter(case=context['case']) - context['assigned_users'] = [ u.assigned.usersettings.preferred_username for u in users] - context['assignable'] = [ u.usersettings.preferred_username for u in User.objects.filter(is_active=True, groups__name='vince')] + # context['assigned_users'] = [ u.assigned.usersettings.preferred_username for u in users] + assigned_ordered_pairs = [] + context['assigned_ordered_pairs'] = [] + for u in users: + assigned_ordered_pairs.append({'label': u.assigned.usersettings.preferred_username, 'value': u.assigned.email}) + context['assigned_ordered_pairs'] = assigned_ordered_pairs + # context['assignable'] = [ u.usersettings.preferred_username for u in User.objects.filter(is_active=True, groups__name='vince')] + assignable_ordered_pairs = [] + context['assignable_ordered_pairs'] = [] + for u in User.objects.filter(is_active=True, groups__name='vince'): + assignable_ordered_pairs.append({'label': u.usersettings.preferred_username, 'value': u.email}) + context['assignable_ordered_pairs'] = assignable_ordered_pairs user_groups= self.request.user.groups.exclude(groupsettings__contact__isnull=True) context['case_tags'] = [tag.tag for tag in context['case'].casetag_set.all()] context['case_available_tags'] = [tag.tag for tag in TagManager.objects.filter(tag_type=3).filter(Q(team__in=user_groups)|Q(team__isnull=True)).exclude(tag__in=context['case_tags']).order_by('tag').distinct('tag')] @@ -6362,7 +6374,7 @@ def post(self, request, *args, **kwargs): logger.debug(f"{self.__class__.__name__} post: {request.POST}") if (int(request.POST['state']) == 1): #get case - user = User.objects.filter(usersettings__preferred_username=request.POST['tag']).first() + user = User.objects.filter(email=request.POST['tag']).first() if user: ca = CaseAction(case=case, title=f"User {user.usersettings.preferred_username} assigned to case", @@ -6373,6 +6385,7 @@ def post(self, request, *args, **kwargs): case=case) assignment.save() else: + logger.debug(f"there was an error when trying to add {request.POST['tag']} to the case") return JsonResponse({'error': 'User does not exist'}, status=401) #is this user a part of one of the caseparticipants - otherwise add them @@ -6394,7 +6407,7 @@ def post(self, request, *args, **kwargs): else: # delete user from case - user = User.objects.filter(usersettings__preferred_username=request.POST['tag']).first() + user = User.objects.filter(email=request.POST['tag']).first() if user: ca = CaseAction(case=case, title=f"User {user.usersettings.preferred_username} removed from case assignment", diff --git a/vincepub/forms.py b/vincepub/forms.py index 16b8be4..c4693f3 100644 --- a/vincepub/forms.py +++ b/vincepub/forms.py @@ -301,7 +301,15 @@ class VulCoordForm(forms.ModelForm): ics_impact = forms.BooleanField( label='Significant ICS/OT impact?', required=False) + + ai_ml_system = forms.BooleanField( + label='Related to AI/ML systems?', + required=False) + metadata = forms.CharField( + required=False + ) + vul_description = forms.CharField( max_length=20000, label='What is the vulnerability?', diff --git a/vincepub/templates/vincepub/reportcoord.html b/vincepub/templates/vincepub/reportcoord.html index 88c8486..6aa58a9 100644 --- a/vincepub/templates/vincepub/reportcoord.html +++ b/vincepub/templates/vincepub/reportcoord.html @@ -139,6 +139,11 @@

    Vulnerability Information

    {% render_field form.ics_impact class="check-sameline" %}
    + +
    + + {% render_field form.ai_ml_system class="check-sameline" %} +
    diff --git a/vincepub/templates/vincepub/success.html b/vincepub/templates/vincepub/success.html index a8253e5..fdee536 100644 --- a/vincepub/templates/vincepub/success.html +++ b/vincepub/templates/vincepub/success.html @@ -58,6 +58,7 @@

    Vulnerability Information

    Product Affected{{ product_name }} Product Version{{ product_version }} Significant ICS/OT impact?{{ ics_impact }} + Related to AI/ML systems?{{ ai_ml_system }} Description of vulnerability{{ vul_description }} How does an attacker exploit this vulnerability?{{ vul_exploit }} What is the impact of this vulnerability?{{ vul_impact }} @@ -146,7 +147,8 @@

    Your Contact Information

    -
    +
    + {% include "vincepub/reportsidebar.html" %}
    {% endblock %} diff --git a/vincepub/views.py b/vincepub/views.py index 7b5abc2..b1e03cf 100644 --- a/vincepub/views.py +++ b/vincepub/views.py @@ -963,7 +963,7 @@ def form_valid(self, form): logger.debug("invalid recaptcha validation") form._errors[forms.forms.NON_FIELD_ERRORS] = ErrorList([ u'Invalid reCAPTCHA. Please try again' - ]) + ]) return super().form_invalid(form) @@ -1111,10 +1111,14 @@ def form_valid(self, form): # process the data in form.cleaned_data as required vrf_id = get_vrf_id() + context = form.cleaned_data + if context['ai_ml_system'] == True: + context['metadata'] = {"ai_ml_system": True} + else: + context['metadata'] = {"ai_ml_system": False} form.instance.vrf_id = vrf_id newrequest = form.save(commit=False) newrequest.save() - context = form.cleaned_data coord_choice=[] context['vrf_id']=vrf_id for selection in context['coord_status']: @@ -1122,7 +1126,7 @@ def form_valid(self, form): if context['why_no_attempt']: context['coord_choice']= form.fields['why_no_attempt'].choices[int(context['why_no_attempt'])-1][1] -# context['coord_choice'] = coord_choice + # context['coord_choice'] = coord_choice context['vrf_date_submitted'] = datetime.now(EST()).isoformat() # get some meta info about who submitted this context['remote_addr'] = self.request.META['REMOTE_ADDR'] if 'REMOTE_ADDR' in self.request.META else "unknown" @@ -1223,6 +1227,8 @@ def form_valid(self, form): else: logger.debug("Email Sent! Message ID: "), logger.debug(response['MessageId']) + # log the report + logger.debug(f'Without signing in first, a user has submitted a report with the following info: {context}') # redirect to a new URL return render(self.request, 'vincepub/success.html', context) diff --git a/vinceworker/views.py b/vinceworker/views.py index 641fd62..b861a60 100644 --- a/vinceworker/views.py +++ b/vinceworker/views.py @@ -281,7 +281,6 @@ def ingest_vulreport(request): try: data = json.loads(body_data['Message']) - logger.debug(data) if "notificationType" in data: #this is a bounce or a complaint if data['notificationType'] in ["Bounce", "Complaint"]: @@ -308,7 +307,7 @@ def ingest_vulreport(request): data['request_type'] = CaseRequest.GOV_FORM data['queue'] = cisaqueue.id data['product_name'] = data['affected_website'] - elif data.get('ics_impact') and (data.get('ics_impact') == True): + elif data.get('ics_impact') and (data.get('ics_impact') == True) and not (data.get("metadata") and data["metadata"].get("ai_ml_system") and data["metadata"].get("ai_ml_system") == True): #is there an ICS queue? icsqueue = TicketQueue.objects.filter(title='INL-CR').first() if icsqueue: diff --git a/vinny/forms.py b/vinny/forms.py index 5b4154c..9d3c4eb 100644 --- a/vinny/forms.py +++ b/vinny/forms.py @@ -677,6 +677,12 @@ class VulStatementForm(forms.Form): class StatementForm(forms.Form): + mute_case = forms.BooleanField( + label=_('Mute Case'), + help_text=_('Turn off post notifications for this case only. You will still receive important notifications from the coordination team or if you are tagged in the case discussion.'), + required=False) + + share = forms.BooleanField( label=_('Share status and statement pre-publication'), help_text=_('Checking this box will share your status and statement privately with all of the other participants in this case until the vulnerability note becomes publicly available.'), @@ -705,6 +711,7 @@ class StatementForm(forms.Form): required=False ) + class CaseRoleForm(forms.Form): owner = forms.MultipleChoiceField( choices=(), @@ -1107,6 +1114,10 @@ class CaseRequestForm(forms.ModelForm): label='Significant ICS/OT impact?', required=False) + ai_ml_system = forms.BooleanField( + label='Related to AI/ML systems?', + required=False) + vul_description = forms.CharField( max_length=20000, label='What is the vulnerability?', diff --git a/vinny/models.py b/vinny/models.py index dd2aa0c..eb14c79 100644 --- a/vinny/models.py +++ b/vinny/models.py @@ -59,6 +59,17 @@ from django.dispatch import Signal import io 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 """ + + def db_type(self, connection): + return 'json' + + def from_db_value(self, value, expression, connection): + return value logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -748,6 +759,11 @@ class VTCaseRequest(models.Model): vendor_communication = models.TextField(blank=True, null=True) 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 + ) ics_impact = models.BooleanField(default=False) vul_description = models.TextField(blank=True, null=True) vul_exploit = models.TextField(blank=True, null=True) diff --git a/vinny/static/vinny/js/addfile.js b/vinny/static/vinny/js/addfile.js index e63b21f..a82dca4 100644 --- a/vinny/static/vinny/js/addfile.js +++ b/vinny/static/vinny/js/addfile.js @@ -59,9 +59,9 @@ $(document).ready(function() { }); $(document).on("submit", '#uploadfile', function(event) { - if( document.getElementById("id_attachment").files.length == 0) { - event.preventDefault(); - } - }); + if( document.getElementById("id_attachment").files.length == 0) { + event.preventDefault(); + } + }) }); diff --git a/vinny/static/vinny/js/vincecomm.js b/vinny/static/vinny/js/vincecomm.js index 13b8cb3..82891a0 100644 --- a/vinny/static/vinny/js/vincecomm.js +++ b/vinny/static/vinny/js/vincecomm.js @@ -283,84 +283,82 @@ $(function () { loadiftarget($(e.target).closest('form'),e); },1000)); $('.asyncform input').not('.asyncdelaysearch').on("change",function(e) { - loadiftarget($(e.target).closest('form'),e); + loadiftarget($(e.target).closest('form'),e); }); $('.select_all_checkbox').on('click',function(e) { - $(e.target).closest('.select_all_group') - .find('input[type="checkbox"]') - .prop('checked',$(e.target).prop('checked')); + $(e.target).closest('.select_all_group') + .find('input[type="checkbox"]') + .prop('checked',$(e.target).prop('checked')); }); function filter_navli(e) { - let li = $(e.currentTarget || e.target || e.srcElement); - li.parent().find('.fa-check').css('opacity',0); - li.find('.fa-check').css('opacity',1); - let rowdiv = li.closest('div.row'); - let statusd = "[" + li.html() + "]"; - if(rowdiv.find('.statusd_view').length) { - rowdiv.find('.statusd_view').html(statusd); - } else { - rowdiv.append($('
    ').addClass('statusd_view').html(statusd)); - } - rowdiv.find('.statusd_view i').addClass('fa-filter'); - let partdiv = li.closest('.participant_type'); - let all = partdiv.find('.participant').not('.pheader'); - let moreless = partdiv.find(".moreless"); - let data = moreless.data(); - if('asyncdivid' in data) - $('#' + data.asyncdivid).show() - if(li.hasClass('all')) { - all.show(); - moreless.show(); - let all_count = all.length - li.find('.count').html(all_count); - } else { - all.hide(); - moreless.hide(); - let aclass = $(li).data('class'); - all.find('.'+aclass).closest('.participant').show() - } + let li = $(e.currentTarget || e.target || e.srcElement); + li.parent().find('.fa-check').css('opacity',0); + li.find('.fa-check').css('opacity',1); + let rowdiv = li.closest('div.row'); + let statusd = "[" + li.html() + "]"; + if(rowdiv.find('.statusd_view').length) { + rowdiv.find('.statusd_view').html(statusd); + } else { + rowdiv.append($('
    ').addClass('statusd_view').html(statusd)); + } + rowdiv.find('.statusd_view i').addClass('fa-filter'); + let partdiv = li.closest('.participant_type'); + let all = partdiv.find('.participant').not('.pheader'); + let moreless = partdiv.find(".moreless"); + let data = moreless.data(); + if('asyncdivid' in data){ + $('#' + data.asyncdivid).show() + } + if(li.hasClass('all')) { + all.show(); + moreless.show(); + let all_count = all.length; + li.find('.count').html(all_count); + } else { + all.hide(); + moreless.hide(); + let aclass = $(li).data('class'); + all.find('.'+aclass).closest('.participant').show(); + } } function activate_navli(nav) { - let partdiv = $(nav).closest('.participant_type'); - let all = partdiv.find('.participant').not('.pheader'); - let all_count = all.length; - - $(nav).find('li li').each(function(_,li) { - $(li).off('click'); - $(li).on('click',filter_navli); - if($(li).hasClass('all')) { - $(li).find('.count').html(all_count); - $(li).find('.fa-check').css('opacity',1); - } else { - let aclass = $(li).data('class'); - if(aclass) { - let count = partdiv.find('.'+aclass).length; - $(li).find('.count').html(count); - } - $(li).find('.fa-check').css('opacity',0); - } - }) + let partdiv = $(nav).closest('.participant_type'); + let all = partdiv.find('.participant').not('.pheader'); + let all_count = all.length; + $(nav).find('li li').each(function(_,li) { + $(li).off('click'); + $(li).on('click',filter_navli); + if($(li).hasClass('all')) { + $(li).find('.count').html(all_count); + $(li).find('.fa-check').css('opacity',1); + } else { + let aclass = $(li).data('class'); + if(aclass) { + let count = partdiv.find('.'+aclass).length; + $(li).find('.count').html(count); + } + $(li).find('.fa-check').css('opacity',0); + } + }) } $('nav.cdown').each(function(_,nav) { - activate_navli(nav); + activate_navli(nav); }); - async function asyncshowall(adiv,clicked) { + function asyncshowall(adiv,clicked) { let dad = $(adiv).parent(); if(clicked) { - $(adiv).hide(); + $(adiv).hide(); dad.find('.asyncshowless').show(); } - let data = dad.data(); + let data = dad.data(); if('asyncdivid' in data) { let asyncdiv = $('#' + data.asyncdivid); if(clicked){ asyncdiv.show(); } let href = data.href; - let rowdiv = data.rowdivclass; let pdiv = data.parentdivclass; - let total = parseInt(dad.find('.showallcount').html()); let batch = parseInt(data.batchcount); if(isNaN(batch)){ batch = 20; @@ -369,29 +367,24 @@ $(function () { let loop = 0; let nav = $('.'+pdiv).find("nav.cdown"); /* Hide the filter till all the rows are loaded */ - if($('.' + data.parentdivclass + ' .' + data.rowdivclass).length < total){ - $(nav).hide(); - } + $(nav).hide(); + + + async function getNextBatch() { + count = $('.'+data.parentdivclass + ' .' + data.rowdivclass).length - function getNextBatch() { if(loop > maxloop) { console.log("Breaking due to too many loops"); return; }; loop++; - count = $('.'+data.parentdivclass + ' .' + data.rowdivclass).length - if (count == total){ - return; - }; - - $.ajax({ + await $.ajax({ url: href + "?start=" + String(count) + "&end=" + String(count + batch), method: 'GET', async: true, success: function(data) { if (!$.trim(data)){ - console.log('no further data received') return } asyncdiv.append(data); @@ -402,17 +395,13 @@ $(function () { console.error('Error', error); } }); - } - getNextBatch(); - - if($('.' + data.parentdivclass + ' .' + data.rowdivclass).length >= total) { - console.log("Activate the filter button"); if(nav){ activate_navli(nav[0]); + $(nav).show(); } - $(nav).show(); } + getNextBatch(); } } diff --git a/vinny/templates/vinny/cr_table.html b/vinny/templates/vinny/cr_table.html index 932c86b..93169c8 100644 --- a/vinny/templates/vinny/cr_table.html +++ b/vinny/templates/vinny/cr_table.html @@ -98,6 +98,14 @@

    Vulnerability Report

    {{ ticket.ics_impact }}
    +
    +
    + +
    +
    + {{ ai_ml_system }} +
    +
    {% if ticket.contact_name %}
    diff --git a/vinny/views.py b/vinny/views.py index 712a117..f5f7c07 100644 --- a/vinny/views.py +++ b/vinny/views.py @@ -2542,6 +2542,8 @@ def get_context_data(self, **kwargs): except: logger.debug("Vendor name ordering failed error: {e}") context['vendors'] = CaseMember.objects.filter(case=case, coordinator=False, reporter_group=False).order_by("group__name") + for vendor in context['vendors']: + logger.debug(vendor) context['num_vendors'] = context['vendors'].count() # context['participants'] = User.objects.filter(groups__name=case.vuid) @@ -2663,9 +2665,16 @@ def get_context_data(self, **kwargs): p = Paginator(posts, posts_after) context['posts'] = p.page(1) context['paginate_by'] = posts_after - - cviewed, created = CaseViewed.objects.update_or_create(case=case, user=self.request.user, + try: + cviewed, created = CaseViewed.objects.update_or_create(case=case, user=self.request.user, defaults={'date_viewed':timezone.now}) + except: + c = CaseViewed.objects.filter(case=case, user=self.request.user) + if c.count > 1: + # here we will put code for deleting all of the entries but one. + pass + pass + if created: # if just created, this is the first time user is viewing case # show vul disclosure policy @@ -2836,7 +2845,7 @@ def test_func(self): case = get_object_or_404(Case, id=self.kwargs['pk']) # add the extra _my_group_for_case because reporters/participants shouldn't # provide a status - return _is_my_case(self.request.user, self.kwargs['pk']) and _my_group_for_case(self.request.user, case) and PendingTestMixin.test_func(self) + return self.request.user.is_superuser or (_is_my_case(self.request.user, self.kwargs['pk']) and _my_group_for_case(self.request.user, case) and PendingTestMixin.test_func(self)) def dispatch(self, request, *args, **kwargs): if self.kwargs.get('vendor_id'): @@ -2865,7 +2874,45 @@ def form_valid(self, form): affected = [] unaffected=[] unknown=[] + user = self.request.user + settings = user.vinceprofile.settings try: + if 'mute_case' in self.request.POST: + # the user wants to mute the case + if user.vinceprofile.settings.get('muted_cases'): + # the user already has muted cases + muted_cases = user.vinceprofile.settings['muted_cases'] + if case.id in muted_cases: + # the user already has this case muted, so there's nothing to do: + pass + else: + # the user has muted cases but this isn't one of them yet, so we add it to muted_cases: + muted_cases.append(case.id) + settings.update({'muted_cases': muted_cases}) + user.vinceprofile.settings = settings + user.vinceprofile.save() + else: + # the user has no muted cases yet, so we create muted_cases and include the current case: + settings.update({'muted_cases': [case.id]}) + user.vinceprofile.settings = settings + user.vinceprofile.save() + else: + # the user wants the case not to be muted. + if user.vinceprofile.settings.get('muted_cases'): + # the user already has muted cases + muted_cases = user.vinceprofile.settings['muted_cases'] + if case.id in muted_cases: + # the user has this case muted, so we have to remove it from muted_cases: + muted_cases.remove(case.id) + settings.update({'muted_cases': muted_cases}) + user.vinceprofile.settings = settings + user.vinceprofile.save() + else: + #this case is not muted, so there's nothing to do: + pass + else: + # the user has no muted cases yet, and doesn't want to add any at this time, so there's nothing to do: + pass for k,v in self.request.POST.items(): if k.startswith('status'): vul_id = int(k.split('_')[1]) @@ -2875,6 +2922,7 @@ def form_valid(self, form): unaffected.append(vul_id) else: unknown.append(vul_id) + except Exception as e: logger.debug(f"Error in {self.__class__.__name__} for form processing error: {e}") pass @@ -2937,9 +2985,19 @@ def get_context_data(self, **kwargs): context['status'] = CaseMemberStatus.objects.filter(vulnerability__in=context['vuls'], member=member) + user = self.request.user + muted = '' + if user.vinceprofile.settings.get('muted_cases'): + if case.id in user.vinceprofile.settings['muted_cases']: + muted = 'on' + + logger.debug('currently muted is') + logger.debug(muted) + stmt = CaseStatement.objects.filter(case = case, member=member).first() + if stmt: - initial={'statement':stmt.statement, 'references':stmt.references, 'share': stmt.share, 'addendum':stmt.addendum} + initial={'mute_case':muted, 'statement':stmt.statement, 'references':stmt.references, 'share': stmt.share, 'addendum':stmt.addendum} else: initial={} @@ -2950,7 +3008,7 @@ def get_context_data(self, **kwargs): #actions = VendorAction.objects.filter(case=case, member=member).values_list('id', flat=True) context['activity'] = VendorStatusChange.objects.filter(action__in=actions).order_by("-action__created")[:15] context['form'] = StatementForm(initial=initial) - + return context class AddStatement(LoginRequiredMixin, TokenMixin, UserPassesTestMixin, FormView): @@ -3499,6 +3557,13 @@ def post(self, request, *args, **kwargs): old.delete() continue else: + #Does this email already exist? + oe = VinceCommEmail.objects.filter(contact=self.contact, email=cd['email']).first() + if oe: + messages.error( + self.request, + _("The email you are attempting to add is already associated with this contact. Personal emails should be modified by a group admin on the User Management page.")) + return redirect("vinny:contact") changes.extend(contact_update.change_email_contact(self.contact, old, cd, self.request.user)) else: #Does this email already exist? @@ -3739,6 +3804,9 @@ def get_context_data(self, **kwargs): context['unread_msg_count'] = 0 context['crpage'] = 1 context['report'] = VTCaseRequest.objects.filter(id=self.kwargs['pk']).first() + logger.debug(f"context['report'] is {context['report']}") + logger.debug(f"vinceutils.deepGet(context['report'].metadata,'ai_ml_system') is {vinceutils.deepGet(context['report'].metadata,'ai_ml_system')}") + context['ai_ml_system'] = vinceutils.deepGet(context['report'].metadata,'ai_ml_system') context['attachments'] = ReportAttachment.objects.filter(action__cr=context['report']) if hasattr(context['report'], "case"): context['case_permission'] = _is_my_case(self.request.user, context['report'].case.id) @@ -3855,11 +3923,15 @@ def form_valid(self, form): # process the data in form.cleaned_data as required vrf_id = get_vrf_id() + context = form.cleaned_data + if context['ai_ml_system'] == True: + context['metadata'] = {"ai_ml_system": True} + else: + context['metadata'] = {"ai_ml_system": False} form.instance.vrf_id = vrf_id newrequest = form.save(commit=False) newrequest.user = self.request.user newrequest.save() - context = form.cleaned_data coord_choice=[] context['vrf_id']=vrf_id context['vc_id'] = newrequest.id @@ -3958,6 +4030,9 @@ def form_valid(self, form): send_sns(vrf_id, "Sending ack email for vul reporting form", "something") else: logger.debug(f"Email Sent! Message ID: {response['MessageId']}") + # log the report + logger.debug(f'{self.request.user} has submitted a report with the following info: {context}') + # redirect to a new URL return render(self.request, 'vincepub/success.html', context) def form_invalid(self, form): From d28726fb96401ae07f68d0a0c32a4af1b2b6c43a Mon Sep 17 00:00:00 2001 From: Vijay Sarvepalli Date: Wed, 25 Oct 2023 12:39:29 -0400 Subject: [PATCH 2/2] version 2.1.6 update to vincepub/models.py --- vincepub/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vincepub/models.py b/vincepub/models.py index 3c8b457..8ea5fac 100644 --- a/vincepub/models.py +++ b/vincepub/models.py @@ -223,6 +223,11 @@ class VulCoordRequest(models.Model): product_name = models.CharField(max_length=100) product_version = models.CharField(max_length=100) ics_impact = models.BooleanField(default=False) + metadata = OldJSONField( + help_text=_('Extensible, currently used to specify relevance to AI/ML systems'), + blank=True, + null=True + ) vul_description = models.TextField() vul_exploit = models.TextField() vul_impact = models.TextField()