Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions gcp/website/frontend3/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,118 @@ dl.vulnerability-details,
pointer-events: none;
}

&.force-collapse {
h2.package-header {
width: 100%;
background: $osv-grey-800;
color: #fff;
padding: 12px 16px;
margin-bottom: 2px;
border-radius: 0;
font-weight: normal;
transition: background-color 0.2s ease-in-out;
cursor: pointer;

&::before {
content: '';
width: 12px;
height: 12px;
margin-right: 8px;
background-image: url(/static/img/filled-triangle.svg);
background-position: center;
background-repeat: no-repeat;
transition: transform 0.2s ease-in-out;
transform: rotate(0deg);
filter: invert(100%);
}

&[expanded]::before {
transform: rotate(90deg);
}

&:hover {
background: #4F4F4F;
}

&[expanded] {
background: #fff;
color: $osv-accent-color;
font-weight: bold;

&::before {
filter: invert(24%) sepia(89%) saturate(2293%) hue-rotate(345deg) brightness(81%) contrast(107%);
}

&:hover {
background: #f0f0f0;
}
}
}

.ecosystem-content-panel {
padding: 8px 0;
border-bottom: 2px solid $osv-grey-800;
margin-bottom: 8px;
}

.package-accordion {
margin-bottom: 8px;
}

.package-accordion h3.package-name-title {
font-family: $osv-heading-font-family;
font-size: 1.1rem;
color: #f1f1f1;
padding: 12px 16px;
cursor: pointer;
background: #333333;
border: 1px solid #444;
border-radius: 0;

&::before {
content: '';
width: 12px;
height: 12px;
margin-right: 8px;
background-image: url(/static/img/filled-triangle.svg);
background-position: center;
background-repeat: no-repeat;
filter: invert(100%);
transition: transform 0.2s ease-in-out;
display: inline-block;
vertical-align: middle;
transform: rotate(0deg);
}

&[expanded] {
border-bottom: 1px dashed #fff;

&::before {
transform: rotate(90deg);
}
}
}

.package-accordion .package-details-card {
background: #333333;
border: 1px solid #444;
border-top: none;
border-radius: 0;

.mdc-layout-grid {
padding: 0 16px 16px 16px;
}
.vulnerability-package-subsection {
padding: 14px 0;
border-bottom: 1px dashed #555;

&:last-child {
border-bottom: none;
}
}
}
}

&[affordance="collapse"] h2.package-header {
.vuln-ecosystem {
display: inline;
Expand Down
219 changes: 200 additions & 19 deletions gcp/website/frontend3/src/templates/vulnerability.html
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,143 @@ <h1 class="title">
</div>
</div>
<div class="vulnerability-packages-container">
<h2 class="title">Affected packages</h2>
<h2 class="title">Affected packages</h2>

{% if vulnerability.affected|should_collapse %}
{% set ecosystems = vulnerability.affected | group_by_ecosystem %}
<spicy-sections class="vulnerability-packages force-collapse">
{% for ecosystem_name, packages in ecosystems.items() -%}
<h2 class="package-header">
<span class="vuln-ecosystem spicy-sections-workaround">{{ ecosystem_name }}</span>
</h2>
<div class="ecosystem-content-panel">
{% for affected in packages -%}
<spicy-sections class="package-accordion">
<h3 class="package-name-title">
{% if 'package' in affected %}{{ affected.package.name }}{% else %}{{ vulnerability.repo | strip_scheme }}{% endif %}
</h3>
<div class="package-details-card">
<div class="mdc-layout-grid">
{%- if 'package' in affected -%}
<div class="vulnerability-package-subsection mdc-layout-grid__inner">
<h3 class="mdc-layout-grid__cell--span-3">Package</h3>
<div class="mdc-layout-grid__cell--span-9">
<dl>
<dt>Name</dt>
<dd>{{ affected.package.name }}</dd>
{%- if ecosystem_name | has_link_to_deps_dev -%}
<dd><a href="{{ affected.package.name | link_to_deps_dev(ecosystem_name) }}" target="_blank" rel="noopener noreferrer">View open source insights on deps.dev</a></dd>
{%- endif -%}
{%- if 'purl' in affected.package -%}
<dt>Purl</dt>
<dd class="purl">{{ affected.package.purl }}</dd>
{%- endif -%}
</dl>
</div>
</div>
{%- endif -%}
{%- if 'severity' in affected -%}
<div class="vulnerability-package-subsection mdc-layout-grid__inner">
<h3 class="mdc-layout-grid__cell--span-3">Severity</h3>
<div class="mdc-layout-grid__cell--span-9">
<ul class="severity">
{% for item in affected.severity -%}
<li>
{% if item | is_cvss %}
<span class="severity-level severity-{{ item | severity_level }}">{{ item | display_severity_rating }}</span>
{{ item.type }} - {{ item.score }}
<a href="{{ item | cvss_calculator_url }}" target="_blank" rel="noopener noreferrer">
CVSS Calculator</a>
{% else %}
<span class="severity-level severity-invalid">{{ item.type }} - {{ item.score }}</span>
{% endif %}
</li>
{% endfor -%}
</ul>
</div>
</div>
{%- endif -%}
<div class="vulnerability-package-subsection mdc-layout-grid__inner">
<h3 class="mdc-layout-grid__cell--span-3">Affected ranges <a href="https://ossf.github.io/osv-schema/#examples" target="_blank" rel="noopener noreferrer"></a></h3>
<div class="mdc-layout-grid__cell--span-9">
{% for range in affected.ranges -%}
<dl>
<dt>Type</dt>
<dd>{{ range.type -}}</dd>
{%- if range.repo -%}
<dt>Repo</dt>
<dd>{{ range.repo }}</dd>
{%- endif -%}
<dt>Events</dt>
<dd>
<div class="mdc-layout-grid__inner events">
{% for event in range.events -%}
<div class="mdc-layout-grid__cell--span-3">{{ event | event_type -}}</div>
<div class="mdc-layout-grid__cell--span-9 version-value">
{% set link = event | event_link -%}
{% if link -%}
<a href="{{ link }}">{{ event | event_value -}}</a>
{% elif event | event_type == 'Introduced' and event | event_value == '0' -%}
<div class="tooltip">{{ event | event_value -}}
{% if range.type == 'GIT' %}
<span class="tooltiptext">Unknown introduced commit / All previous commits are affected</span>
{% else -%}
<span class="tooltiptext">Unknown introduced version / All previous versions are affected</span>
{% endif -%}
</div>
{% else -%}
{{ event | event_value -}}
{% endif -%}
</div>
{% endfor -%}
</div>
</dd>
{%- if range.database_specific -%}
<dt>Database specific <a href="https://ossf.github.io/osv-schema/#affectedrangesdatabase_specific-field" target="_blank" rel="noopener noreferrer"></a></dt>
<dd><pre class="specific">{{ range.database_specific | display_json }}</pre></dd>
{%- endif -%}
</dl>
{% endfor -%}
</div>
</div>
{% if affected.versions -%}
<div class="vulnerability-package-subsection mdc-layout-grid__inner">
<h3 class="mdc-layout-grid__cell--span-3">Affected versions <a href="https://ossf.github.io/osv-schema/#affectedversions-field" target="_blank" rel="noopener noreferrer"></a></h3>
<div class="mdc-layout-grid__cell--span-9 version-value">
{% for group, versions in (affected.versions|group_versions(affected.package.ecosystem)).items() -%}
<spicy-sections class="versions-section">
<h2 class="version-header">{{ group }}</h2>
<div class="versions {% if not loop.last %}versions-separator{% endif %}">
{% for version in versions -%}
<div class="version">{{ version }}</div>
{% endfor -%}
</div>
</spicy-sections>
{% endfor -%}
</div>
</div>
{% endif -%}
{% if affected.ecosystem_specific -%}
<div class="vulnerability-package-subsection mdc-layout-grid__inner">
<h3 class="mdc-layout-grid__cell--span-3">Ecosystem specific <a href="https://ossf.github.io/osv-schema/#affectedecosystem_specific-field" target="_blank" rel="noopener noreferrer"></a></h3>
<div class="mdc-layout-grid__cell--span-9"><pre class="specific">{{ affected.ecosystem_specific | display_json }}</pre></div>
</div>
{% endif -%}
{% if affected.database_specific -%}
<div class="vulnerability-package-subsection mdc-layout-grid__inner">
<h3 class="mdc-layout-grid__cell--span-3">Database specific <a href="https://ossf.github.io/osv-schema/#affecteddatabase_specific-field" target="_blank" rel="noopener noreferrer"></a></h3>
<div class="mdc-layout-grid__cell--span-9"><pre class="specific">{{ affected.database_specific | display_json }}</pre></div>
</div>
{% endif -%}
</div>
</div>
</spicy-sections>
{% endfor -%}
</div>
{% endfor -%}
</spicy-sections>

{% else %}
<spicy-sections
class="vulnerability-packages{% if vulnerability.affected|should_collapse %} force-collapse{% endif %}">
{% for affected in vulnerability.affected -%}
Expand Down Expand Up @@ -347,7 +483,8 @@ <h3 class="mdc-layout-grid__cell--span-3">
</div>
{% endfor -%}
</spicy-sections>
</div>
{% endif %}
</div>
</div>
<turbo-stream action="update" target="title">
<template>
Expand All @@ -357,18 +494,73 @@ <h3 class="mdc-layout-grid__cell--span-3">

<script>
document.addEventListener('turbo:load', function() {
const ECOSYSTEM_EXPAND_THRESHOLD = 10;
const PACKAGE_EXPAND_THRESHOLD = 7;

/**
* Sets up the vertical, grouped layout for vulnerability packages.
* - All ecosystem headers are expanded if their total count is below `ECOSYSTEM_EXPAND_THRESHOLD`.
* - Within each ecosystem, all package headers are expanded if their count is below `PACKAGE_EXPAND_THRESHOLD`.
*/
function setupVerticalLayout() {
const ecosystemHeaders = document.querySelectorAll('.vulnerability-packages.force-collapse .package-header');
if (!ecosystemHeaders.length) return;

if (ecosystemHeaders.length < ECOSYSTEM_EXPAND_THRESHOLD) {
ecosystemHeaders.forEach(header => {
if (header.getAttribute('aria-expanded') === 'false') {
header.click();
}
});
}

const ecosystemPanels = document.querySelectorAll('.ecosystem-content-panel');
ecosystemPanels.forEach(panel => {
const packageHeaders = panel.querySelectorAll('.package-name-title');
if (packageHeaders.length <= PACKAGE_EXPAND_THRESHOLD) {
packageHeaders.forEach(header => {
if (header.getAttribute('aria-expanded') === 'false') {
header.click();
}
});
}
});
}

/**
* Sets up the default layout,(tabs on desktop, accordion on mobile).
* This function ensures that on the mobile accordion view,
* all package headers are expanded by default if their count is
* below a defined threshold. This has no effect on the desktop tab view.
*/
function setupDefaultLayout() {
const packageHeaders = document.querySelectorAll('.vulnerability-packages:not(.force-collapse) .package-header');
if (!packageHeaders.length) return;

/**
* Expands collapsed package headers.
* We use `spicy-section` to make the packages content collapsible for mobile view,
* but it collapses the content by default. We want it expanded after the page
* is loaded, so we programmatically click on the header of collapsed packages
* to make the content visible.
*/
function expandPackageHeaders() {
const packageHeaders = document.querySelectorAll('.package-header[affordance="collapse"][aria-expanded="false"]');
packageHeaders.forEach((header) => {
header.click();
});
function expandPackageHeaders() {
packageHeaders.forEach((header) => {
if (header.getAttribute('aria-expanded') === 'false') {
header.click();
}
});
}

if (packageHeaders.length < ECOSYSTEM_EXPAND_THRESHOLD) {
expandPackageHeaders();
}
}

if (document.querySelector('.vulnerability-packages.force-collapse')) {
setupVerticalLayout();
} else {
setupDefaultLayout();
}

/**
Expand All @@ -381,13 +573,9 @@ <h3 class="mdc-layout-grid__cell--span-3">
function setupExpandibleList(listSelector, itemSelector) {
const EXPANDIBLE_LIST_DEFAULT_LENGTH = 6;
const lists = document.querySelectorAll(`${listSelector}:not(.expanded):not([data-has-toggle-btn])`);

lists.forEach((list) => {
const items = list.getElementsByTagName(itemSelector);

if (items.length <= EXPANDIBLE_LIST_DEFAULT_LENGTH) {
return;
}
if (items.length <= EXPANDIBLE_LIST_DEFAULT_LENGTH) return;

const expandibleItems = [...items].slice(EXPANDIBLE_LIST_DEFAULT_LENGTH);
expandibleItems.forEach((item) => {
Expand All @@ -411,13 +599,6 @@ <h3 class="mdc-layout-grid__cell--span-3">
});
}

const packageHeaders = document.querySelectorAll('.package-header');
const EXPAND_PACKAGES_THRESHOLD = 10;

if (packageHeaders.length < EXPAND_PACKAGES_THRESHOLD) {
expandPackageHeaders();
}

setupExpandibleList('.expandible-hierarchy-list', 'ul');
setupExpandibleList('.expandible-list', 'li');
});
Expand Down
Loading
Loading