Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@
</label>
}
}

@if (activeCategory === 'language') {
@if (languageOptions.length === 0) {
<span class="empty-hint">No languages available yet</span>
}
@for (language of languageOptions; track language.id) {
<label class="option-row">
<input
type="checkbox"
[checked]="filters.languages.includes(language.id)"
(change)="toggleLanguage(language.id)"
/>
{{ language.label }}
</label>
}
}
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface CveFilters {
impact: string[];
branches: string[];
statuses: string[];
languages: string[];
showUnmaintained: boolean;
}

Expand All @@ -15,15 +16,17 @@ export const DEFAULT_CVE_FILTERS: CveFilters = {
impact: [],
branches: [],
statuses: [],
languages: [],
showUnmaintained: false,
};

export type FilterCategory = 'cvss' | 'impact' | 'branches';
export type FilterCategory = 'cvss' | 'impact' | 'branches' | 'language';

export const CVE_FILTER_CATEGORIES: { key: FilterCategory; label: string }[] = [
{ key: 'cvss', label: 'CVSS Score' },
{ key: 'impact', label: 'FF Impact' },
{ key: 'branches', label: 'Branches' },
{ key: 'language', label: 'Language' },
];

const SEVERITY_OPTIONS = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'];
Expand All @@ -36,6 +39,11 @@ const STATUS_OPTIONS: { value: string; label: string }[] = [
{ value: 'not-affected', label: 'Not affected' },
];

export interface LanguageOption {
id: string;
label: string;
}

@Component({
selector: 'app-cve-filter-panel',
standalone: true,
Expand All @@ -46,6 +54,7 @@ const STATUS_OPTIONS: { value: string; label: string }[] = [
export class CveFilterPanelComponent {
@Input() filters: CveFilters = { ...DEFAULT_CVE_FILTERS };
@Input() availableBranches: string[] = [];
@Input() languageOptions: LanguageOption[] = [];
@Input() visibleCategories: FilterCategory[] = ['cvss', 'impact', 'branches'];
@Input() showStatusSection = true;
@Output() filtersChange = new EventEmitter<CveFilters>();
Expand All @@ -70,14 +79,16 @@ export class CveFilterPanelComponent {
this.filters.severities.length +
this.filters.impact.length +
this.filters.branches.length +
this.filters.statuses.length
this.filters.statuses.length +
this.filters.languages.length
);
}

protected countForCategory(category: FilterCategory): number {
if (category === 'cvss') return this.filters.severities.length;
if (category === 'impact') return this.filters.impact.length;
if (category === 'branches') return this.filters.branches.length;
if (category === 'language') return this.filters.languages.length;

return 0;
}
Expand All @@ -98,6 +109,10 @@ export class CveFilterPanelComponent {
this.emit({ ...this.filters, branches: this.toggle(this.filters.branches, branch) });
}

protected toggleLanguage(language: string): void {
this.emit({ ...this.filters, languages: this.toggle(this.filters.languages, language) });
}

protected toggleStatus(status: string): void {
this.emit({ ...this.filters, statuses: this.toggle(this.filters.statuses, status) });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@ const makeDetail = (overrides: Partial<VulnerabilityDetail> = {}): Vulnerability
publishedAt: null,
},
affectedReleasesByBranch: {
'9.0': [{ id: 'r1', tagName: 'v9.0.1', name: '9.0.1', publishedAt: '2024-01-01', branch: { id: 'b1', name: 'release/9.0' } }],
'9.0': [
{
release: { id: 'r1', tagName: 'v9.0.1', name: '9.0.1', publishedAt: '2024-01-01', branch: { id: 'b1', name: 'release/9.0' } },
target: null,
pkgName: null,
pkgPath: null,
purl: null,
installedVersion: null,
fixedVersion: null,
},
],
},
shortTermFixByBranch: {},
longTermFixByBranch: {},
Expand Down Expand Up @@ -82,7 +92,19 @@ describe('CveOverviewItemComponent', () => {
component.branchMaintainedMap = new Map([['7.0', false]]);
component.showUnmaintained = false;
component.detail = makeDetail({
affectedReleasesByBranch: { '7.0': [{ id: 'r1', tagName: 'v7.0.1', name: '7.0.1', publishedAt: '2020-01-01', branch: { id: 'b1', name: 'release/7.0' } }] },
affectedReleasesByBranch: {
'7.0': [
{
release: { id: 'r1', tagName: 'v7.0.1', name: '7.0.1', publishedAt: '2020-01-01', branch: { id: 'b1', name: 'release/7.0' } },
target: null,
pkgName: null,
pkgPath: null,
purl: null,
installedVersion: null,
fixedVersion: null,
},
],
},
shortTermFixByBranch: {},
longTermFixByBranch: {},
});
Expand All @@ -93,7 +115,19 @@ describe('CveOverviewItemComponent', () => {
it('returns no-fix when affected branch is unmaintained but showUnmaintained=true', () => {
component.showUnmaintained = true;
component.detail = makeDetail({
affectedReleasesByBranch: { '7.0': [{ id: 'r1', tagName: 'v7.0.1', name: '7.0.1', publishedAt: '2020-01-01', branch: { id: 'b1', name: 'release/7.0' } }] },
affectedReleasesByBranch: {
'7.0': [
{
release: { id: 'r1', tagName: 'v7.0.1', name: '7.0.1', publishedAt: '2020-01-01', branch: { id: 'b1', name: 'release/7.0' } },
target: null,
pkgName: null,
pkgPath: null,
purl: null,
installedVersion: null,
fixedVersion: null,
},
],
},
shortTermFixByBranch: {},
longTermFixByBranch: {},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@
}

<app-vulnerability-stats [vulnerability]="vulnerabilityDetail.vulnerability" />

@if (hasPackageInfos()) {
<div class="package-row">
@for (pkg of packageInfos(); track trackPackageInfo(pkg)) {
<span class="tag tag-package">
<code>{{ pkg.pkgName }}</code>
@if (pkg.target) {
<span class="package-meta">({{ pkg.target }})</span>
}
@if (pkg.pkgPath) {
<span class="package-meta">{{ pkg.pkgPath }}</span>
}
</span>
}
</div>
}
</div>

<div class="details-body">
Expand Down Expand Up @@ -50,6 +66,22 @@ <h4 class="section-title">CWEs</h4>
</div>
}

@if (hasRecommendedUpgrades()) {
<div class="detail-section">
<h4 class="section-title">Recommended Upgrade</h4>
<p class="fix-hint">Upgrade to the patched release of your branch, or move to the latest stable release</p>
<div class="branch-list">
@for (upgrade of recommendedUpgrades(); track upgrade.branch) {
<div class="branch-card branch-patched">
<span class="branch-name">{{ upgrade.branch }}</span>
<span class="release-label">{{ upgrade.isLatestStable ? 'Latest stable' : 'Patched in' }}</span>
<span class="tag tag-patched">{{ upgrade.release.tagName }}</span>
</div>
}
</div>
</div>
}

@if (hasAffectedBranches()) {
<div class="detail-section">
<h4 class="section-title">Affected Releases</h4>
Expand All @@ -63,7 +95,7 @@ <h4 class="section-title">Affected Releases</h4>
<div class="branch-card" [class.branch-patched]="fix" [class.branch-affected]="!fix">
<span class="branch-name">{{ branch }}</span>
<span class="release-label">started in</span>
<span class="tag tag-affected">{{ sortedAffectedReleases(branch)[0]?.tagName }}</span>
<span class="tag tag-affected">{{ sortedAffectedOccurrences(branch)[0]?.release?.tagName }}</span>
@if (fix) {
<span class="timeline-arrow">→</span>
<span class="release-label">fixed in</span>
Expand All @@ -73,21 +105,21 @@ <h4 class="section-title">Affected Releases</h4>
<span class="tag tag-unmaintained">unmaintained</span>
}
</div>
}
</div>
</div>
}

@if (hasRecommendedUpgrades()) {
<div class="detail-section">
<h4 class="section-title">Recommended Upgrade</h4>
<p class="fix-hint">Upgrade to the patched release of your branch, or move to the latest stable release</p>
<div class="branch-list">
@for (upgrade of recommendedUpgrades(); track upgrade.branch) {
<div class="branch-card branch-patched">
<span class="branch-name">{{ upgrade.branch }}</span>
<span class="release-label">{{ upgrade.isLatestStable ? 'Latest stable' : 'Patched in' }}</span>
<span class="tag tag-patched">{{ upgrade.release.tagName }}</span>
<div class="occurrence-list">
@for (occurrence of sortedAffectedOccurrences(branch); track trackOccurrence(occurrence)) {
@if (occurrence.pkgName) {
<div class="occurrence-row">
<span class="occurrence-release">{{ occurrence.release.tagName }}</span>
@if (occurrence.installedVersion) {
<span class="occurrence-version">{{ occurrence.installedVersion }}</span>
}
@if (occurrence.fixedVersion) {
<span class="timeline-arrow">→</span>
<span class="occurrence-version occurrence-fixed">{{ occurrence.fixedVersion }}</span>
}
</div>
}
}
</div>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,33 @@
margin: 0.5rem 0 0.75rem 0;
}

.package-row {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
margin-top: 0.75rem;
}

.tag-package {
display: inline-flex;
align-items: baseline;
gap: 0.375rem;
background: #f5f3ff;
border-color: #ddd6fe;
color: #5b21b6;

code {
font-size: 0.75rem;
color: inherit;
}
}

.package-meta {
font-size: 0.7rem;
color: #7c3aed;
opacity: 0.75;
}

.badge {
flex-shrink: 0;
padding: 0.25rem 0.5rem;
Expand Down Expand Up @@ -269,6 +296,37 @@
color: #9ca3af;
}

.occurrence-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin: 0 0 0.5rem 0.85rem;
padding-left: 0.6rem;
border-left: 2px solid #f3f4f6;
}

.occurrence-row {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0.4rem;
font-size: 0.7rem;
color: #d1d5db;
}

.occurrence-release {
color: #d1d5db;
min-width: 4rem;
}

.occurrence-version {
color: #9ca3af;
}

.occurrence-fixed {
color: #15803d;
}

.empty-state {
display: flex;
flex-direction: column;
Expand Down
Loading
Loading