diff --git a/static/js/src/cve/cve.js b/static/js/src/cve/cve.js index a7b685f0e92..e93c08b4233 100644 --- a/static/js/src/cve/cve.js +++ b/static/js/src/cve/cve.js @@ -9,10 +9,12 @@ import { const searchInput = document.querySelector("#q"); const searchForm = document.querySelector("#searchForm"); -const cveList = document.querySelector("#cve-list"); -const recentCves = document.querySelector("#recent-cves"); const url = new URL(window.location.href); const urlParams = new URLSearchParams(url.search); +const limitSelect = document.querySelector(".js-limit-select"); +const orderSelect = document.querySelector(".js-order-select"); +const exportLink = document.querySelector("#js-export-link"); +const apiBase = "https://ubuntu.com/security/cves.json"; function handleCveIdInput(value) { const packageInput = document.querySelector("#package"); @@ -153,11 +155,39 @@ tooltipIconList.forEach(function (tooltipIcon) { ); }); -function showCVEList() { - if (urlParams.has("status")) { - recentCves.classList.add("u-hide"); - cveList.classList.remove("u-hide"); +function handleLimitSelect() { + if (urlParams.has("limit")) { + limitSelect.value = urlParams.get("limit"); } + + limitSelect.onchange = function (event) { + limitSelect.value = event.target.value; + urlParams.set("limit", limitSelect.value); + url.search = urlParams.toString(); + window.location.href = url.href; + }; } +handleLimitSelect(); + +function handleOrderSelect() { + if (urlParams.has("order")) { + orderSelect.value = urlParams.get("order"); + } -showCVEList(); + orderSelect.onchange = function (event) { + orderSelect.value = event.target.value; + urlParams.set("order", orderSelect.value); + url.search = urlParams.toString(); + window.location.href = url.href; + }; +} +handleOrderSelect(); + +function exportToJSON() { + exportLink.onclick = function (event) { + event.preventDefault(); + let apiURL = new URL(url.search, apiBase); + window.location.href = apiURL.href; + }; +} +exportToJSON(); diff --git a/static/sass/_pattern_cve.scss b/static/sass/_pattern_cve.scss index 4b1d7d9609e..f9751c9d151 100644 --- a/static/sass/_pattern_cve.scss +++ b/static/sass/_pattern_cve.scss @@ -224,4 +224,82 @@ border-bottom-color: #66a61e; } + + .cve-landing { + .p-inline-list { + margin-top: 0.3rem; + } + } + + .cve-results { + display: flex; + justify-content: space-between; + + .p-text--small-caps { + display: inline-flex; + } + + .p-form { + display: flex; + gap: 2.5rem; + } + + .p-form__group { + margin-left: auto; + margin-right: 0; + } + + .p-form__control { + max-width: fit-content; + min-width: 0; + padding-right: 1rem; + } + } + + .cve-card-header { + margin-bottom: 1.1rem; + padding-top: 0.4rem; + + h2 { + display: inline; + padding-top: 0; + } + + #priority-icon { + float: inline-end; + } + } + + .cve-pagination-footer { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + } + + .cve-pagination-footer-links { + display: flex; + gap: inherit; + + a { + margin-top: 0.4rem; + } + + @media screen and (max-width: $breakpoint-small - 1) { + flex-direction: column; + gap: 1rem; + } + + .p-form__control { + max-width: fit-content; + min-width: 0; + padding-right: 1rem; + } + } + + #cve-search-button { + @media screen and (min-width: $breakpoint-small) { + margin-top: 2.6rem; + width: 100%; + } + } } diff --git a/templates/security/cve/_cve-card.html b/templates/security/cve/_cve-card.html new file mode 100644 index 00000000000..23e93a96dc0 --- /dev/null +++ b/templates/security/cve/_cve-card.html @@ -0,0 +1,80 @@ +{% for cve in cves %} +
+
+
+

+ {{ cve.id }} +

+ + {% if cve.priority == 'unknown' %} + + {% elif cve.priority == 'negligible' %} + + {% elif cve.priority == 'low' %} + + {% elif cve.priority == 'medium' %} + + {% elif cve.priority == 'high' %} + + {% elif cve.priority == 'critical' %} + + {% else %} + + {% endif %} + {{ cve.priority.split() | first | capitalize }} priority + +
+
+
+ {% set status = cve.summarized_status %} + {% if status.name == "Some fixed" %} +

Some fixes available {{ status.fixed_count }} of {{ status.total_count }}

+ {% elif status.name == "Needs evaluation" %} +

+ Needs evaluation +

+ {% elif status.name == "Fixed" %} +

+ Fixed +

+ {% elif status.name == "Vulnerable" %} +

+ Vulnerable +

+ {% elif status.name == "DNE" %} +

+ Not in release +

+ {% else %} + {{status}} + {% endif %} +
+
+
+
+

{{ cve.description|truncate(235) }}

+
+
+

{{ cve.packages | length}} affected packages

+

+ {% for package in cve.packages %} + {% if cve.packages | length > 6 %} + {% if loop.index < 5 %} + {{ package.name }}, + {% elif loop.index == 5 %} + {{ package.name }}... + {% endif %} + {% else %} + {% if loop.last %} + {{ package.name }} + {% else %} + {{ package.name }}, + {% endif %} + {% endif %} + {% endfor %} +

+
+ {% if loop.index < cves | length %} +
+ {% endif %} +{% endfor %} \ No newline at end of file diff --git a/templates/security/cve/_cve-filters.html b/templates/security/cve/_cve-filters.html new file mode 100644 index 00000000000..8b4fa951352 --- /dev/null +++ b/templates/security/cve/_cve-filters.html @@ -0,0 +1,104 @@ +
+

Filters

+
+ +
+ + +
+ +
+ Ubuntu releases +
+ + + + +

Still maintained with
Ubuntu Pro

+ + + +
+ Show unmaintained releases +
+ +
+ Ubuntu priority +
+ + + + + +
+ What is Ubuntu priority? +
+ +
+ Ubuntu priority +
+ + + + + + +
+ What do statuses mean? +
+ + \ No newline at end of file diff --git a/templates/security/cve/_cve-footer.html b/templates/security/cve/_cve-footer.html new file mode 100644 index 00000000000..8aa636b05e9 --- /dev/null +++ b/templates/security/cve/_cve-footer.html @@ -0,0 +1,46 @@ +
+
+
+
+

Resources

+
+ +
+
\ No newline at end of file diff --git a/templates/security/cve/_cve-landing.html b/templates/security/cve/_cve-landing.html new file mode 100644 index 00000000000..e9039c62314 --- /dev/null +++ b/templates/security/cve/_cve-landing.html @@ -0,0 +1,51 @@ +
+
+
+
+

Search CVEs

+
+
+ {% include "security/cve/_cve-search.html" %} +
+
+
+ +
+
+
+
+
+
+

By Ubuntu release

+
+ +
+
+
+
+ +
+
+
+
+

Recent CVEs

+
+
+ {% with cves = high_priority_cves %} + {% include "security/cve/_cve-card.html" %} + {% endwith %} +
+
+
+ +{% include "security/cve/_cve-footer.html" %} \ No newline at end of file diff --git a/templates/security/cve/_cve-search-results.html b/templates/security/cve/_cve-search-results.html new file mode 100644 index 00000000000..80d94923456 --- /dev/null +++ b/templates/security/cve/_cve-search-results.html @@ -0,0 +1,49 @@ +
+
+
+
+ {% include "security/cve/_cve-filters.html" %} +
+ {% if total_results > 0 %} +
+
+

+ {% if total_results > 1 %} + {{ offset + 1 }} + – + {% if offset + limit > total_results %} + {{ total_results }} + {% else %} + {{ offset + limit }} + {% endif %} + of + {% endif %} + {{ total_results }} result{% if total_results != 1 %}s{% endif %} +

+
+ +
+ + +
+
+
+
+
+ {% with cves = cves %} + {% include "security/cve/_cve-card.html" %} + {% endwith %} +
+ {% include "security/cve/_pagination.html" %} +
+ {% endif %} +
+
\ No newline at end of file diff --git a/templates/security/cve/_cve-search.html b/templates/security/cve/_cve-search.html new file mode 100644 index 00000000000..8f1e9aed283 --- /dev/null +++ b/templates/security/cve/_cve-search.html @@ -0,0 +1,14 @@ +
+
+
+ + +
+
+ +
+
+
\ No newline at end of file diff --git a/templates/security/cve/_pagination.html b/templates/security/cve/_pagination.html index 9cf48e8dedf..c8f1b70c1d5 100644 --- a/templates/security/cve/_pagination.html +++ b/templates/security/cve/_pagination.html @@ -5,98 +5,113 @@ {% endif %} {% if total_results > limit %} -
-
    -
  1. - {% if current_page > 1 %} - - Previous page - - {% else %} - - Previous page - - {% endif %} -
  2. - - {# always show 5 pages in pagination #} - {% if current_page > 4 and current_page == total_pages %} -
  3. - {{ current_page - 4 }} -
  4. - {% endif %} - - {% if current_page > 3 and current_page == total_pages - 1 %} -
  5. - {{ current_page - 3 }} -
  6. - {% endif %} - - {% if current_page > 3 and current_page == total_pages %} -
  7. - {{ current_page - 3 }} -
  8. - {% endif %} - - {% if current_page > 2 %} -
  9. - {{ current_page - 2 }} -
  10. - {% endif %} - - {% if current_page > 1 %} -
  11. - {{ current_page - 1 }} -
  12. - {% endif %} - - {# current current_page #} -
  13. - {{ current_page }} -
  14. - - {% if current_page < total_pages %} -
  15. - {{ current_page + 1 }} -
  16. - {% endif %} - - {% if current_page < total_pages - 1 %} -
  17. - {{ current_page + 2 }} -
  18. - {% endif %} - - {% if current_page < total_pages - 2 and current_page == 1 %} +
+
+ {% endif %} diff --git a/templates/security/cve/index.html b/templates/security/cve/index.html index 2b16003d73e..56cbe7bc3fe 100644 --- a/templates/security/cve/index.html +++ b/templates/security/cve/index.html @@ -15,7 +15,10 @@
{% if query or package or component or priority or versions or statuses %} -

CVE reports

+
+

Search CVE reports

+
+ {% include "security/cve/_cve-search.html" %} {% else %}

CVE reports

The Common Vulnerabilities and Exposures (CVE) system is used to identify, define, and catalog publicly disclosed cybersecurity vulnerabilities. Canonical keeps track of all CVEs affecting Ubuntu, and releases a security notice when an issue is fixed.

@@ -29,224 +32,11 @@

CVE reports

-
-
-
-
- {% if query or package or component or priority or versions or statuses %} - - {% else %} -

Search CVEs

- {% endif %} -
-
-
-
-
- - -
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-

By Ubuntu release

-
- -
-
-
-
- -
-
-
-
-

Recent CVEs

-
-
- {% for cve in high_priority_cves %} -
-
-
-

- {{ cve.id }} - - {% if cve.priority == 'unknown' %} - - {% elif cve.priority == 'negligible' %} - - {% elif cve.priority == 'low' %} - - {% elif cve.priority == 'medium' %} - - {% elif cve.priority == 'high' %} - - {% elif cve.priority == 'critical' %} - - {% else %} - - {% endif %} - {{ cve.priority.split() | first | capitalize }} priority - -

-
-
-
- {% set status = cve.summarized_status %} - {% if status.name == "Some fixed" %} -

Some fixes available {{ status.fixed_count }} of {{ status.total_count }}

- {% elif status.name == "Needs evaluation" %} -

- Needs evaluation -

- {% endif %} -

-
-
-
-
-

{{ cve.description }}

-
-
-

{{ cve.packages | length}} affected packages

-

- {% for package in cve.packages %} - {% if cve.packages | length > 6 %} - {% if loop.index < 6 %} - {{ package.name }}, - {% elif loop.index == 6 %} - and {{ cve.packages | length - 5 }} more - {% endif %} - {% else %} - {% if loop.last %} - {{ package.name }} - {% else %} - {{ package.name }}, - {% endif %} - {% endif %} - {% endfor %} -

-
- {% if loop.index < 5 %} -
- {% endif %} - {% endfor %} -
-
-
- -
-
- {% if query or package or priority %} -

- {% if total_results > 1 %} - {{ offset + 1 }} - – - {% if offset + limit > total_results %} - {{ total_results }} - {% else %} - {{ offset + limit }} - {% endif %} - of - {% endif %} - {{ total_results }} result{% if total_results != 1 %}s{% endif %} -

- {% else %} -

Recent CVEs affecting Ubuntu

- {% endif %} -
- - {% if total_results > 0 %} -
- {% with cves=cves, releases=releases %} - {% include "security/cve/_cve-table.html" %} - {% endwith %} - - {% with %} - {% include "security/cve/_pagination.html" %} - {% endwith %} -
- {% endif %} -
- - - -
- -
+{% if query or package or component or priority or versions or statuses %} + {% include "security/cve/_cve-search-results.html"%} +{% else %} + {% include "security/cve/_cve-landing.html" %} +{% endif %} diff --git a/webapp/security/api.py b/webapp/security/api.py index 6a99ae0768e..e809b43a753 100644 --- a/webapp/security/api.py +++ b/webapp/security/api.py @@ -123,6 +123,7 @@ def get_cves( component: str, versions: list, statuses: list, + order: str, ): parameters = { "q": query, @@ -133,6 +134,7 @@ def get_cves( "component": component, "version": versions, "status": statuses, + "order": order, } # Remove falsey items from dictionary diff --git a/webapp/security/helpers.py b/webapp/security/helpers.py index fe0e59a7115..55d7f771572 100644 --- a/webapp/security/helpers.py +++ b/webapp/security/helpers.py @@ -15,94 +15,91 @@ def get_summarized_status(cve, ignored_low_indicators, vulnerable_indicators): Check if all statuses for all packages are the same, excluding DNE and empty data """ - if ( - len( - { - d["status"] - for d in package["statuses"] - if d["status"] not in {"DNE", ""} - } - ) - == 1 - ): - cve["summarized_status"] = { - "name": cve["packages"][0]["statuses"][0]["status"] + # TODO: Fix this logic, causing false flags in summarized status + # if ( + # len( + # { + # d["status"] + # for d in package["statuses"] + # if d["status"] not in {"DNE", ""} + # } + # ) + # == 1 + # ): + # cve["summarized_status"] = { + # "name": cve["packages"][0]["statuses"][0]["status"] + # } + # else: + """ + Considered ignored_low if status is ignored and + status description contains any of the indicators + """ + for status in package["statuses"]: + if "ignored" in status["status"]: + if any( + indicator in status["description"].lower() + for indicator in ignored_low_indicators + ): + status["status"] = "ignored-low" + if len( + { + d["status"] + for d in package["statuses"] + if d["status"] not in {"DNE", "ignored-low"} } - else: - """ - Considered ignored_low if status is ignored and - status description contains any of the indicators - """ - for status in package["statuses"]: - if "ignored" in status["status"]: - if any( - indicator in status["description"].lower() - for indicator in ignored_low_indicators - ): - status["status"] = "ignored-low" - if len( - { + ) == len("not-affected"): + cve["summarized_status"] = {"name": "Not affected"} + # Ignoring "DNE", "not-affected", and "ignored-low" statuses + # check if all other statuses are "released" + elif len( + { + d["status"] == "released" + for d in package["statuses"] + if d["status"] not in {"DNE", "not-affected", "ignored-low"} + } + ) == len("released"): + cve["summarized_status"] = {"name": "Fixed"} + + # If any package status is released, + # count how many packages have that status + elif any(d["status"] == "released" for d in package["statuses"]): + if more_packages: + total_fixed = 0 + total_fixable = 0 + for package in cve["packages"]: + for status in package["statuses"]: + if status["status"] == "released": + total_fixed += 1 + elif status["status"] not in { + "DNE", + "not-affected", + }: + total_fixable += 1 + else: + fixed_count = Counter( d["status"] for d in package["statuses"] - if d["status"] not in {"DNE", "ignored-low"} - } - ) == len("not-affected"): - cve["summarized_status"] = {"name": "Not affected"} - # Ignoring "DNE", "not-affected", and "ignored-low" statuses - # check if all other statuses are "released" - elif len( - { - d["status"] == "released" - for d in package["statuses"] - if d["status"] - not in {"DNE", "not-affected", "ignored-low"} - } - ) == len("released"): - cve["summarized_status"] = {"name": "Fixed"} - - # If any package status is released, - # count how many packages have that status - elif any(d["status"] == "released" for d in package["statuses"]): - if more_packages: - total_fixed = 0 - total_fixable = 0 - for package in cve["packages"]: - for status in package["statuses"]: - if status["status"] == "released": - total_fixed += 1 - elif status["status"] not in { - "DNE", - "not-affected", - }: - total_fixable += 1 - else: - fixed_count = Counter( - d["status"] - for d in package["statuses"] - if d["status"] == "released" - ) - total_fixed = fixed_count["released"] - total_fixable = len(package["statuses"]) + if d["status"] == "released" + ) + total_fixed = fixed_count["released"] + total_fixable = len(package["statuses"]) - cve["summarized_status"] = { - "name": "Some fixed", - "fixed_count": total_fixed, - "total_count": total_fixable, - } + cve["summarized_status"] = { + "name": "Some fixed", + "fixed_count": total_fixed, + "total_count": total_fixable, + } - elif any( - d["status"] in vulnerable_indicators - for d in package["statuses"] - ): - cve["summarized_status"] = { - "name": "Vulnerable", - } + elif any( + d["status"] in vulnerable_indicators for d in package["statuses"] + ): + cve["summarized_status"] = { + "name": "Vulnerable", + } - elif any( - d["status"] == "needs-triage" for d in package["statuses"] - ): - cve["summarized_status"] = { - "name": "Needs evaluation", - } + elif any(d["status"] == "needs-triage" for d in package["statuses"]): + cve["summarized_status"] = { + "name": "Needs evaluation", + } return cve["summarized_status"] diff --git a/webapp/security/views.py b/webapp/security/views.py index b45547fe238..b34e699c0fc 100644 --- a/webapp/security/views.py +++ b/webapp/security/views.py @@ -293,7 +293,7 @@ def cve_index(): """ Display the list of CVEs, with pagination. Also accepts the following filtering query parameters: - - order-by - "oldest" or "newest" + - order-by - "descending" (default) or "ascending" - query - search query for the description field - priority - limit - default 20 @@ -303,11 +303,12 @@ def cve_index(): query = flask.request.args.get("q", "").strip() priority = flask.request.args.get("priority", default="", type=str) package = flask.request.args.get("package", default="", type=str) - limit = flask.request.args.get("limit", default=20, type=int) + limit = flask.request.args.get("limit", default=10, type=int) offset = flask.request.args.get("offset", default=0, type=int) component = flask.request.args.get("component") versions = flask.request.args.getlist("version") statuses = flask.request.args.getlist("status") + order = flask.request.args.get("order", default="", type=str) # All CVEs cves_response = security_api.get_cves( @@ -319,6 +320,7 @@ def cve_index(): component=component, versions=versions, statuses=statuses, + order=order, ) cves = cves_response.get("cves") @@ -334,6 +336,7 @@ def cve_index(): component=component, versions=versions, statuses=statuses, + order=order, ) high_priority_cves = high_priority_response.get("cves") @@ -398,27 +401,39 @@ def cve_index(): selected_releases = sorted(selected_releases, key=lambda d: d["version"]) + """ + TODO: Lines 407-417 and 422-430 are commented out because they will + be needed for the detailed view of the cve card + BUT currently cause errors as that has not been implemented in + this branch yet. + """ + # Format summarized statuses - friendly_names = { - "DNE": "Not in release", - "needs-triage": "Needs evaluation", - "not-affected": "Not vulnerable", - "needed": "Vulnerable", - "deferred": "Vulnerable", - "ignored": "Ignored", - "pending": "Vulnerable", - "released": "Fixed", - } + # friendly_names = { + # "DNE": "Not in release", + # "needs-triage": "Needs evaluation", + # "not-affected": "Not vulnerable", + # "needed": "Vulnerable", + # "deferred": "Vulnerable", + # "ignored": "Ignored", + # "pending": "Vulnerable", + # "released": "Fixed", + # } for cve in cves: - for cve_package in cve["packages"]: - cve_package["release_statuses"] = {} - for status in cve_package["statuses"]: - cve_package["release_statuses"][status["release_codename"]] = { - "slug": status["status"], - "name": friendly_names[status["status"]], - "pocket": status["pocket"], - } + cve["summarized_status"] = {} + get_summarized_status( + cve, ignored_low_indicators, vulnerable_indicators + ) + # for cve_package in cve["packages"]: + # cve_package["release_statuses"] = {} + # for status in cve_package["statuses"]: + # cve_package["release_statuses"][status["release_codename"]] = + # { + # "slug": status["status"], + # "name": friendly_names[status["status"]], + # "pocket": status["pocket"], + # } return flask.render_template( "security/cve/index.html", @@ -438,6 +453,7 @@ def cve_index(): lts_releases=lts_releases, maintained_releases=maintained_releases, high_priority_cves=high_priority_cves, + order=order, )