diff --git a/vulnerabilities/migrations/0140_alter_advisorytodo_issue_type_and_more.py b/vulnerabilities/migrations/0140_alter_advisorytodo_issue_type_and_more.py new file mode 100644 index 000000000..274e1c5b0 --- /dev/null +++ b/vulnerabilities/migrations/0140_alter_advisorytodo_issue_type_and_more.py @@ -0,0 +1,77 @@ +# Generated by Django 5.2.11 on 2026-07-16 10:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0139_cleanup_none_string_in_severity"), + ] + + operations = [ + migrations.AlterField( + model_name="advisorytodo", + name="issue_type", + field=models.CharField( + choices=[ + ("MISSING_AFFECTED_PACKAGE", "Advisory is missing affected package"), + ("MISSING_FIXED_BY_PACKAGE", "Advisory is missing fixed-by package"), + ( + "MISSING_AFFECTED_AND_FIXED_BY_PACKAGES", + "Advisory is missing both affected and fixed-by packages", + ), + ("MISSING_SUMMARY", "Advisory is missing summary"), + ( + "CONFLICTING_FIXED_BY_PACKAGES", + "Advisories have conflicting fixed-by packages", + ), + ( + "CONFLICTING_AFFECTED_PACKAGES", + "Advisories have conflicting affected packages", + ), + ( + "CONFLICTING_AFFECTED_AND_FIXED_BY_PACKAGES", + "Advisories have conflicting affected and fixed-by packages", + ), + ("CONFLICTING_SEVERITY_SCORES", "Advisories have conflicting severity scores"), + ("CONFLICTING_WEAKNESSES", "Advisories have conflicting weaknesses"), + ], + db_index=True, + help_text="Select the issue that needs to be addressed from the available options.", + max_length=50, + ), + ), + migrations.AlterField( + model_name="advisorytodov2", + name="issue_type", + field=models.CharField( + choices=[ + ("MISSING_AFFECTED_PACKAGE", "Advisory is missing affected package"), + ("MISSING_FIXED_BY_PACKAGE", "Advisory is missing fixed-by package"), + ( + "MISSING_AFFECTED_AND_FIXED_BY_PACKAGES", + "Advisory is missing both affected and fixed-by packages", + ), + ("MISSING_SUMMARY", "Advisory is missing summary"), + ( + "CONFLICTING_FIXED_BY_PACKAGES", + "Advisories have conflicting fixed-by packages", + ), + ( + "CONFLICTING_AFFECTED_PACKAGES", + "Advisories have conflicting affected packages", + ), + ( + "CONFLICTING_AFFECTED_AND_FIXED_BY_PACKAGES", + "Advisories have conflicting affected and fixed-by packages", + ), + ("CONFLICTING_SEVERITY_SCORES", "Advisories have conflicting severity scores"), + ("CONFLICTING_WEAKNESSES", "Advisories have conflicting weaknesses"), + ], + db_index=True, + help_text="Select the issue that needs to be addressed from the available options.", + max_length=50, + ), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 46670dadb..0787cdd45 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -34,7 +34,6 @@ from cwe2.mappings import xml_database_path from cwe2.weakness import Weakness as DBWeakness from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group from django.contrib.auth.models import UserManager from django.core import exceptions from django.core.exceptions import ValidationError @@ -2463,6 +2462,7 @@ def create_new_job(self, execute_now=False): "Advisories have conflicting affected and fixed-by packages", ), ("CONFLICTING_SEVERITY_SCORES", "Advisories have conflicting severity scores"), + ("CONFLICTING_WEAKNESSES", "Advisories have conflicting weaknesses"), ] diff --git a/vulnerabilities/pipelines/v2_improvers/compute_advisory_todo.py b/vulnerabilities/pipelines/v2_improvers/compute_advisory_todo.py index fca2b81cf..7a807cf1d 100644 --- a/vulnerabilities/pipelines/v2_improvers/compute_advisory_todo.py +++ b/vulnerabilities/pipelines/v2_improvers/compute_advisory_todo.py @@ -45,6 +45,7 @@ def steps(cls): cls.compute_individual_advisory_todo, cls.detect_conflicting_package_versions, cls.detect_conflicting_cvss_scores, + cls.detect_conflicting_weakness, ) def compute_individual_advisory_todo(self): @@ -436,6 +437,223 @@ def detect_conflicting_cvss_scores(self): f"conflicting CVSS scores related to {total_count_conflicting_advisory} advisories." ) + def detect_conflicting_weakness(self): + """ + Create ToDos for advisories with conflicting opinions on weaknesses for a vulnerability. + """ + advisory_relation_to_create = {} + todo_to_create = [] + new_todos_count = 0 + batch_size = 1 + total_count_conflicting_advisory = 0 + total_weakness_conflict_count = 0 + total_successfully_compared_advisory_count = 0 + existing_todo_ids = set( + AdvisoryToDoV2.objects.values_list("related_advisories_id", flat=True) + ) + + advisory_qs = ( + AdvisoryV2.objects.exclude( + advisory_todos__issue_type="MISSING_AFFECTED_AND_FIXED_BY_PACKAGES" + ) + .filter(weaknesses__isnull=False) + .todo_excluded() + .latest_per_avid() + .distinct() + .prefetch_related("weaknesses") + ) + + cve_aliases = AdvisoryAlias.objects.filter(alias__istartswith="cve").prefetch_related( + Prefetch("advisories", queryset=advisory_qs, to_attr="filtered_advisories") + ) + non_cve_aliases = AdvisoryAlias.objects.exclude(alias__istartswith="cve").prefetch_related( + Prefetch("advisories", queryset=advisory_qs, to_attr="filtered_advisories") + ) + advisory_count = advisory_qs.count() + aliases_count = cve_aliases.count() + non_cve_aliases.count() + progress = LoopProgress( + total_iterations=aliases_count, + logger=self.log, + progress_step=5, + ) + self.log(f"Detect conflicting weaknesses in {advisory_count} advisory.") + aliases = chain( + cve_aliases.iterator(chunk_size=50), + non_cve_aliases.iterator(chunk_size=50), + ) + for alias in progress.iter(aliases): + + advisory_avid_map = {} + cwe_avid_map = defaultdict( + lambda: { + "cwes": (), + "avid_precedence": [], + "primary": "", + "secondaries": [], + } + ) + + advisories_with_common_alias = alias.filtered_advisories or [] + known_advisory_ids = [a.id for a in advisories_with_common_alias] + adv_with_alias_in_adv_id = advisory_qs.filter(advisory_id=alias.alias).exclude( + id__in=known_advisory_ids + ) + if not advisories_with_common_alias and not adv_with_alias_in_adv_id.exists(): + continue + + advisories_with_common_alias.extend(adv_with_alias_in_adv_id) + initial_advisory_group_size = len(advisories_with_common_alias) + + if initial_advisory_group_size < 2: + continue + + cwe_details = {} + for advisory in advisories_with_common_alias: + cwes = set() + for w in advisory.weaknesses.all(): + if not w.weakness.weakness_abstraction: + continue + + cwe_details[w.cwe_id] = w.to_dict() + cwes.add(w.cwe_id) + + canonical_cwes = canonical_value(cwes) + cwe_checksum = sha256_digest(canonical_cwes) + cwe_avid_map[cwe_checksum]["cwes"] = canonical_cwes + cwe_avid_map[cwe_checksum]["avid_precedence"].append( + (advisory.avid, advisory.precedence) + ) + advisory_avid_map[advisory.avid] = advisory + + if len(cwe_avid_map) < 2: + continue + + for map in cwe_avid_map.values(): + avid_precedence = map["avid_precedence"] + sorted_avids = [ + x[0] for x in sorted(avid_precedence, key=lambda x: x[1], reverse=True) + ] + map["primary"] = {"advisory_uid": sorted_avids[0]} + map["secondaries"] = [{"advisory_uid": a} for a in sorted_avids[1:]] + del map["avid_precedence"] + + weakness_conflict_count, count_conflicting_advisory = ( + check_conflicting_weaknesses_for_alias( + alias=alias, + comparable_cwe_map=cwe_avid_map, + advisories=advisory_avid_map, + cwe_details=cwe_details, + todo_to_create=todo_to_create, + advisory_relation_to_create=advisory_relation_to_create, + existing_todo_ids=existing_todo_ids, + ) + ) + + total_weakness_conflict_count += weakness_conflict_count + total_count_conflicting_advisory += count_conflicting_advisory + total_successfully_compared_advisory_count += initial_advisory_group_size + + if len(todo_to_create) > batch_size: + new_todos_count += bulk_create_with_m2m( + todos=todo_to_create, + advisories=advisory_relation_to_create, + logger=self.log, + ) + advisory_relation_to_create.clear() + todo_to_create.clear() + + new_todos_count += bulk_create_with_m2m( + todos=todo_to_create, + advisories=advisory_relation_to_create, + logger=self.log, + ) + + self.log( + f"Successfully compared {total_successfully_compared_advisory_count} advisories, created {new_todos_count} new ToDos for {total_weakness_conflict_count} " + f"conflicting weaknesses related to {total_count_conflicting_advisory} advisories." + ) + + +def compute_cwe_disagreement(cwe_groups): + """Compute differences in cwe across given cwe groups.""" + + cwe_union = set().union(*cwe_groups) + cwe_intersection = set.intersection(*cwe_groups) + + return { + "cwe_union": list(sorted(cwe_union)), + "cwe_intersection": list(cwe_intersection), + "cwe_disagreement": list(cwe_union - cwe_intersection), + } + + +def check_conflicting_weaknesses_for_alias( + alias, + advisories, + comparable_cwe_map, + cwe_details, + todo_to_create, + advisory_relation_to_create, + existing_todo_ids, +): + """ + Add appropriate AdvisoryToDo for conflicting weaknesses for given advisories.. + """ + + curation_item = {} + cwe_groups = [set(value["cwes"]) for value in comparable_cwe_map.values()] + disagreement = compute_cwe_disagreement(cwe_groups) + + cwe_disagreement_count = len(disagreement["cwe_disagreement"]) + if cwe_disagreement_count < 1: + return 0, 0 + + noun = "weaknesses" if cwe_disagreement_count > 1 else "weakness" + curation_item["all_cwes"] = disagreement["cwe_union"] + curation_item["cwe_details"] = cwe_details + curation_item["partial_curation"] = {"cwes": disagreement["cwe_intersection"]} + curation_item["conflict_reason"] = f"Advisories report different {noun} for {alias}" + curation_item["advisories"] = list(comparable_cwe_map.values()) + + issue_type = "CONFLICTING_WEAKNESSES" + conflicting_advisories = list(advisories.values()) + + conflict_checksum = sha256_digest(canonical_value([curation_item])) + issue_detail = { + "alias": alias.alias, + "conflict_checksum": conflict_checksum, + "curation_items": [curation_item], + } + + todo_id = advisories_checksum(conflicting_advisories) + + if todo_id in existing_todo_ids: + return 0, 0 + + existing_todo_ids.add(todo_id) + conflicting_advisories_count = len(conflicting_advisories) + + date_published = min( + (a.date_published for a in conflicting_advisories if a.date_published), + default=None, + ) + date_collected = min( + (a.date_collected for a in conflicting_advisories if a.date_collected), + default=None, + ) + todo = AdvisoryToDoV2( + related_advisories_id=todo_id, + issue_type=issue_type, + issue_detail=issue_detail, + alias=alias, + advisories_count=conflicting_advisories_count, + oldest_advisory_date=date_published or date_collected, + ) + todo_to_create.append(todo) + advisory_relation_to_create[todo_id] = conflicting_advisories + + return cwe_disagreement_count, conflicting_advisories_count + def check_conflicting_cvss_for_alias( alias, @@ -495,7 +713,7 @@ def check_conflicting_cvss_for_alias( "cvss": cvss_version, "conflict_reason": conflict_message, "partial_cvss_curation": consensus_metrics, - "advisories": get_grouped_advisory_curation( + "advisories": get_grouped_cvss_advisory_curation( advisory_curation_item_map, cvss_type, advisories, item.keys() ), } @@ -539,7 +757,7 @@ def check_conflicting_cvss_for_alias( return len(curation_items), conflicting_advisories_count -def get_grouped_advisory_curation(advisory_curation_item_map, cvss_type, advisories, avids): +def get_grouped_cvss_advisory_curation(advisory_curation_item_map, cvss_type, advisories, avids): """Group curation advisory based on CVSS vector similarity.""" curation_items = [] vector_group = defaultdict(list) diff --git a/vulnerabilities/templates/advisory_todos.html b/vulnerabilities/templates/advisory_todos.html index b47d51055..f3eca864a 100644 --- a/vulnerabilities/templates/advisory_todos.html +++ b/vulnerabilities/templates/advisory_todos.html @@ -127,6 +127,8 @@

Advisory To-Dos

{% elif todo.issue_type == "CONFLICTING_SEVERITY_SCORES" %} + {% elif todo.issue_type == "CONFLICTING_WEAKNESSES" %} + {% endif %}
diff --git a/vulnerabilities/templates/weakness_curation.html b/vulnerabilities/templates/weakness_curation.html new file mode 100644 index 000000000..b027861dd --- /dev/null +++ b/vulnerabilities/templates/weakness_curation.html @@ -0,0 +1,261 @@ +{% extends "base.html" %} +{% load static %} +{% load utils %} + +{% block extrahead %} + +{% endblock %} + +{% block content %} +
+ +
+{% endblock %} + + +{% block scripts %} + + +{% endblock %} diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 0a152951c..53c522875 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -1198,3 +1198,24 @@ def get_context_data(self, **kwargs): context["vulnerability_id"] = todo.alias context["curation_items"] = json.dumps(todo.issue_detail["curation_items"]) return context + + +class AdvisoryWeaknessCurationView(DetailView): + model = AdvisoryToDoV2 + template_name = "weakness_curation.html" + slug_url_kwarg = "todo_id" + slug_field = "todo_id" + + def get_queryset(self): + return super().get_queryset().filter(issue_type="CONFLICTING_WEAKNESSES") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + todo = self.object + + context["advisory_summaries"] = { + adv.avid: adv.summary for adv in todo.advisories.all() if adv.summary.strip() + } + context["vulnerability_id"] = todo.alias + context["curation_items"] = json.dumps(todo.issue_detail["curation_items"]) + return context diff --git a/vulnerablecode/static/js/package_curation.js b/vulnerablecode/static/js/package_curation.js index e0083614b..0961b5f3f 100644 --- a/vulnerablecode/static/js/package_curation.js +++ b/vulnerablecode/static/js/package_curation.js @@ -16,7 +16,7 @@ const app = { renderPackageCuration() { const item = curationItems[this.currentIndex]; - const versions = item.all_versions || item.all_version; + const versions = item.all_versions; const total = curationItems.length; const progPercentage = ((this.currentIndex + 1) / total) * 100; @@ -222,7 +222,7 @@ const app = { resetCurrentCuration() { const item = curationItems[this.currentIndex]; - const versions = item.all_versions || item.all_version; + const versions = item.all_versions; versions.forEach(v => { if (item.partial_curation.affected.includes(v)) { this.userStates[this.currentIndex][v] = 'affected'; @@ -257,13 +257,12 @@ const app = { if (isExpanded && advGroup.secondaries) { advGroup.secondaries.forEach(sec => { - const secAffected = sec.affected || advGroup.affected; - const secFixing = sec.fixing || advGroup.fixing; + const secAffected = advGroup.affected; + const secFixing = advGroup.fixing; const secState = secAffected.includes(v) ? 'affected' : (secFixing.includes(v) ? 'fixed' : 'unaffected'); const secTd = document.createElement('td'); secTd.className = `state-${secState} has-text-centered advisory-cell`; - secTd.style.borderLeft = "1px dashed #dbdbdb"; secTd.innerText = secState.toUpperCase(); tr.appendChild(secTd); }); @@ -314,7 +313,7 @@ const app = { } } const item = curationItems[this.currentIndex]; - const versions = item.all_versions || item.all_version; + const versions = item.all_versions; this.renderBody(item, versions); }, @@ -326,7 +325,7 @@ const app = { this.expandedFolds.add(colKey); } const item = curationItems[this.currentIndex]; - const versions = item.all_versions || item.all_version; + const versions = item.all_versions; this.renderHeader(item); this.renderBody(item, versions); @@ -335,7 +334,7 @@ const app = { toggleRanges() { this.showRanges = !this.showRanges; const item = curationItems[this.currentIndex]; - const versions = item.all_versions || item.all_version; + const versions = item.all_versions; this.renderBody(item, versions); }, @@ -344,14 +343,14 @@ const app = { const current = this.userStates[this.currentIndex][v]; this.userStates[this.currentIndex][v] = seq[(seq.indexOf(current) + 1) % 3]; const item = curationItems[this.currentIndex]; - const versions = item.all_versions || item.all_version; + const versions = item.all_versions; this.renderBody(item, versions); }, pickAdvisory(advIdx, type, secondaryIdx) { const item = curationItems[this.currentIndex]; const advGroup = item.advisories[advIdx]; - const versions = item.all_versions || item.all_version; + const versions = item.all_versions; versions.forEach(v => { if (advGroup.affected.includes(v)) this.userStates[this.currentIndex][v] = 'affected'; diff --git a/vulnerablecode/static/js/weakness_curation.js b/vulnerablecode/static/js/weakness_curation.js new file mode 100644 index 000000000..f696a4650 --- /dev/null +++ b/vulnerablecode/static/js/weakness_curation.js @@ -0,0 +1,329 @@ +const app = { + currentIndex: 0, + userStates: {}, + expandedFolds: new Set(), + showRanges: false, + foldAgreementBlocks: true, + init() { + this.renderPackageCuration(); + + document.querySelector('.summary-text')?.addEventListener('click', (e) => { + if (e.target.closest('a')) { + e.target.target = '_blank'; + } + }); + }, + + renderPackageCuration() { + const item = curationItems[this.currentIndex]; + const weaknesses = item.all_cwes; + + const total = curationItems.length; + const progPercentage = ((this.currentIndex + 1) / total) * 100; + document.getElementById('progress').value = progPercentage; + document.getElementById('progress-text').innerText = `${this.currentIndex + 1} / ${total}`; + document.getElementById('current-purl').innerText = item.conflict_reason; + + if (!this.userStates[this.currentIndex]) { + this.userStates[this.currentIndex] = {}; + weaknesses.forEach(v => { + if (item.partial_curation.cwes.includes(v)) this.userStates[this.currentIndex][v] = 'applicable'; + else this.userStates[this.currentIndex][v] = 'empty'; + }); + } + + this.renderHeader(item); + this.renderBody(item, weaknesses); + this.updateNavButtons(); + }, + + renderHeader(item) { + const header = document.getElementById('table-header'); + header.innerHTML = ` + Weaknesses + +
+
+
Curation
+
+ +
+ `; + + item.advisories.forEach((advGroup, groupIdx) => { + const secondaries = advGroup.secondaries || []; + const hasSecondaries = secondaries.length > 0; + + const colKey = `${this.currentIndex}-col-${groupIdx}`; + const isExpanded = this.expandedFolds.has(colKey); + + const primaryTh = document.createElement('th'); + primaryTh.className = "has-text-centered"; + + const primaryUrl = baseAdvisoryUrl.replace('0', advGroup.primary.advisory_uid); + let primaryUidHtml = ` +
+ `; + + let toggleHtml = ""; + if (hasSecondaries) { + toggleHtml = ` + + `; + } + + primaryTh.innerHTML = ` +
+
+ ${toggleHtml} + ${primaryUidHtml} +
+ +
+ `; + header.appendChild(primaryTh); + + if (hasSecondaries && isExpanded) { + secondaries.forEach((sec, secIdx) => { + const secTh = document.createElement('th'); + secTh.className = "has-text-centered"; + secTh.style.backgroundColor = "#fafafa"; + + const secUrl = baseAdvisoryUrl.replace('0', sec.advisory_uid); + secTh.innerHTML = ` +
+
+
+ Similar +
+ + ${sec.advisory_uid} + + +
+ +
+ `; + header.appendChild(secTh); + }); + } + }); + }, + + renderBody(item, weaknesses) { + const body = document.getElementById('curation-body'); + body.innerHTML = ''; + + let totalColumns = 2; + item.advisories.forEach((advGroup, groupIdx) => { + totalColumns += 1; + const colKey = `${this.currentIndex}-col-${groupIdx}`; + if (this.expandedFolds.has(colKey)) { + totalColumns += (advGroup.secondaries || []).length; + } + }); + + const foldable = this.getFoldableRanges(item, weaknesses); + for (let i = 0; i < weaknesses.length; i++) { + const range = foldable.find(r => i >= r.start && i <= r.end); + if (range) { + const foldKey = `${this.currentIndex}-${range.start}`; + let isExpanded = this.expandedFolds.has(foldKey); + if (!this.foldAgreementBlocks) { + isExpanded = !this.expandedFolds.has(`${this.currentIndex}-${range.start}-collapsed`); + } + if (i === range.start) { + const marker = document.createElement('tr'); + marker.innerHTML = ` + + ${isExpanded ? 'Hide' : 'Show'} Consensus Range (${range.end - range.start + 1} weaknesses) + `; + body.appendChild(marker); + } + if (!isExpanded) { + if (i === range.end) continue; + i = range.end; + continue; + } + } + body.appendChild(this.createRow(weaknesses[i], item)); + } + }, + + resetCurrentCuration() { + const item = curationItems[this.currentIndex]; + const weaknesses = item.all_cwes; + weaknesses.forEach(v => { + if (item.partial_curation.cwes.includes(v)) { + this.userStates[this.currentIndex][v] = 'applicable'; + } + else { + this.userStates[this.currentIndex][v] = 'empty'; + } + }); + this.renderBody(item, weaknesses); + }, + + createRow(v, item) { + const tr = document.createElement('tr'); + const cwe_info = item.cwe_details[v] + const isLastItem = item.all_cwes[item.all_cwes.length - 1] === v; + const tooltipPosition = isLastItem ? "has-tooltip-top" : "has-tooltip-right"; + const state = this.userStates[this.currentIndex][v]; + tr.innerHTML = ` + + CWE-${v}
+ ${cwe_info.name} + + + + `; + const userTd = document.createElement('td'); + userTd.className = `curation-cell state-${state}`; + userTd.innerText = state === "empty"? "Select value": state.replace('-', ' ').toUpperCase(); + userTd.onclick = () => this.cycleState(v); + tr.appendChild(userTd); + + item.advisories.forEach((advGroup, groupIdx) => { + const colKey = `${this.currentIndex}-col-${groupIdx}`; + const isExpanded = this.expandedFolds.has(colKey); + + const primaryState = advGroup.cwes.includes(v) ? 'applicable' : 'not-applicable'; + const td = document.createElement('td'); + td.className = `state-${primaryState} has-text-centered advisory-cell`; + td.innerText = primaryState.replace('-', ' ').toUpperCase(); + tr.appendChild(td); + + if (isExpanded && advGroup.secondaries) { + advGroup.secondaries.forEach(sec => { + const secAffected = advGroup.cwes; + + const secState = secAffected.includes(v) ? 'applicable' : 'not-applicable'; + const secTd = document.createElement('td'); + secTd.className = `state-${secState} has-text-centered advisory-cell`; + secTd.style.borderLeft = "1px dashed #dbdbdb"; + secTd.innerText = secState.replace('-', ' ').toUpperCase(); + tr.appendChild(secTd); + }); + } + }); + return tr; + }, + + getFoldableRanges(item, weaknesses) { + const ranges = []; + const foldThreshHold = 3; + let start = -1; + for (let i = 0; i < weaknesses.length; i++) { + const v = weaknesses[i]; + const states = item.advisories.map(a => a.cwes.includes(v) ? 'applicable' : 'not-applicable'); + const allMatch = states.every(s => s === states[0]); + if (allMatch) { + if (start === -1) start = i; + } else { + if (start !== -1 && (i - start) >= foldThreshHold) ranges.push({ + start, + end: i - 1 + }); + start = -1; + } + } + if (start !== -1 && (weaknesses.length - start) >= foldThreshHold) ranges.push({ + start, + end: weaknesses.length - 1 + }); + return ranges; + }, + + toggleFold(startIdx) { + const foldKey = `${this.currentIndex}-${startIdx}`; + const collapseKey = `${this.currentIndex}-${startIdx}-collapsed`; + + if (this.foldAgreementBlocks) { + if (this.expandedFolds.has(foldKey)) { + this.expandedFolds.delete(foldKey); + } else { + this.expandedFolds.add(foldKey); + } + } else { + if (this.expandedFolds.has(collapseKey)) { + this.expandedFolds.delete(collapseKey); + } else { + this.expandedFolds.add(collapseKey); + } + } + const item = curationItems[this.currentIndex]; + const weaknesses = item.all_cwes; + this.renderBody(item, weaknesses); + }, + + toggleColumnFold(groupIdx) { + const colKey = `${this.currentIndex}-col-${groupIdx}`; + if (this.expandedFolds.has(colKey)) { + this.expandedFolds.delete(colKey); + } else { + this.expandedFolds.add(colKey); + } + const item = curationItems[this.currentIndex]; + const weaknesses = item.all_cwes; + + this.renderHeader(item); + this.renderBody(item, weaknesses); + }, + + toggleRanges() { + this.showRanges = !this.showRanges; + const item = curationItems[this.currentIndex]; + const weaknesses = item.all_cwes; + this.renderBody(item, weaknesses); + }, + + cycleState(v) { + const seq = ['applicable', 'not-applicable']; + const current = this.userStates[this.currentIndex][v]; + this.userStates[this.currentIndex][v] = seq[(seq.indexOf(current) + 1) % 2]; + const item = curationItems[this.currentIndex]; + const weaknesses = item.all_cwes; + this.renderBody(item, weaknesses); + }, + + pickAdvisory(advIdx, type, secondaryIdx) { + const item = curationItems[this.currentIndex]; + const advGroup = item.advisories[advIdx]; + const weaknesses = item.all_cwes; + + weaknesses.forEach(v => { + if (advGroup.cwes.includes(v)) this.userStates[this.currentIndex][v] = 'applicable'; + else this.userStates[this.currentIndex][v] = 'not-applicable'; + }); + this.renderBody(item, weaknesses); + }, + + navigate(dir) { + this.currentIndex += dir; + this.renderPackageCuration(); + }, + + updateNavButtons() { + document.getElementById('prev-btn').disabled = this.currentIndex === 0; + const isLast = this.currentIndex === curationItems.length - 1; + document.getElementById('next-btn').classList.toggle('is-hidden', isLast); + document.getElementById('finish-btn').classList.toggle('is-hidden', !isLast); + }, + +}; +document.addEventListener('DOMContentLoaded', () => app.init()); \ No newline at end of file diff --git a/vulnerablecode/urls.py b/vulnerablecode/urls.py index cc63632bd..0f43f919e 100644 --- a/vulnerablecode/urls.py +++ b/vulnerablecode/urls.py @@ -28,6 +28,7 @@ from vulnerabilities.views import AdvisoryPackagesDetails from vulnerabilities.views import AdvisorySeverityCurationView from vulnerabilities.views import AdvisoryToDoListView +from vulnerabilities.views import AdvisoryWeaknessCurationView from vulnerabilities.views import AffectedByAdvisoriesListView from vulnerabilities.views import AltchaView from vulnerabilities.views import ApiUserCreateView @@ -91,6 +92,11 @@ def __init__(self, *args, **kwargs): AdvisorySeverityCurationView.as_view(), name="todo-severity-detail", ), + path( + "advisories/todos//weakness/curate/", + AdvisoryWeaknessCurationView.as_view(), + name="todo-weakness-detail", + ), path( "pipelines//runs/", PipelineRunListView.as_view(),