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
4 changes: 2 additions & 2 deletions doc/source/ci_conditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ A condition is always expressed as either:
Tokens can be:
- `true`, `false`: mapped to the corresponding boolean values
- `0`, `5`, `2.3`, `-1`, `.42`, `25%`: recognized as numbers. Percentages are divided by 100 internally.
- any unrecognised token is treated as a string identifier. String tokens can start with `[a-zA-Z_]` and contain `[a-zA-Z0-9_-:]`.
- any unrecognised token appearing on the **right-hand side** of `==` or `!=` is treated as a literal string (e.g. `id == CVE-2021-44228`). String tokens can start with `[a-zA-Z_]` and contain `[a-zA-Z0-9_-:]`. An unrecognised token anywhere else (left-hand side, ordering comparisons, boolean position) is an error, so typos in token names are caught instead of silently evaluating.

**Example:** `cvss >= 5 or ignored`

Expand All @@ -82,7 +82,7 @@ The following tokens are evaluated per vulnerability:
| `pending` | boolean | Whether the vulnerability has not yet been reviewed (status is pending). |
| `new` | boolean | Whether the vulnerability was not present in the previous scan. |

Any other token will be treated as a string. Strings are not quoted, can start with `[a-zA-Z]` or an underscore `_`, and can contain `[a-zA-Z0-9_-:]`.
Any other token used on the right-hand side of `==` or `!=` will be treated as a string. Strings are not quoted, can start with `[a-zA-Z]` or an underscore `_`, and can contain `[a-zA-Z0-9_-:]`.

---

Expand Down
8 changes: 8 additions & 0 deletions doc/source/interactive-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ The **Columns** dropdown lets you toggle which columns are displayed. The defaul

- **Source**: restrict the table to packages originating from a specific SBOM source (e.g. a particular SPDX or CycloneDX document).

### Match Condition

The **Match condition** field in the toolbar filters packages using the same condition language as CI fail conditions (e.g. `cvss >= 7 and pending`). Press **Enter** or click **Apply** to evaluate the expression against every vulnerability in the current scope; the table is then reduced to packages that have at least one matching vulnerability. Clearing the field and applying again removes the filter. Whenever vulnerability data is refreshed the filter is cleared so it never shows stale results — apply the condition again to evaluate the fresh data.

While a match condition is active, the **Show Vulnerabilities** button carries the matching set along: the Vulnerability Table opens pre-filtered to that package *and* to the vulnerabilities that satisfied the condition.

See the [Match Conditions](ci_conditions.md) page for the full syntax and token reference.

### Severity Toggle

The **Severity** toggle switch in the toolbar adds a colour-coded severity tag next to each package's vulnerability count, showing the highest severity among the package's associated vulnerabilities. This gives an at-a-glance view of which packages carry the most critical exposure.
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/TableGeneric.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ function TableGeneric<DataType> ({
<div className="rounded-b-md flex justify-between items-center py-4 px-4 text-white bg-slate-800 border-t border-slate-600 text-sm">
<div className="flex items-center gap-2">
<span>
{pageIndex * itemsPerPage + 1}-
{filteredData.length === 0 ? 0 : pageIndex * itemsPerPage + 1}-
{Math.min((pageIndex + 1) * itemsPerPage, filteredData.length)} / {filteredData.length}
</span>
<span>- Results per page:</span>
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/handlers/vulnerabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,48 @@ const asVulnerability = (data: any): Vulnerability | [] => {
}

class Vulnerabilities {
static async matchCondition(condition: string, vulnerabilities: Vulnerability[]): Promise<string[]> {
// Facts must stay aligned with the CI fail-condition evaluation in
// src/bin/cmd_process.py (evaluate_condition) so a given condition
// matches identically in the UI and in CI pipelines.
const items = vulnerabilities.map(vulnerability => {
const lastAssessment = vulnerability.assessments.reduce<Assessment | undefined>((latest, assessment) => {
if (!latest || new Date(assessment.timestamp).getTime() > new Date(latest.timestamp).getTime()) {
return assessment;
}
return latest;
}, undefined);

return {
id: vulnerability.id,
data: {
id: vulnerability.id,
cvss: vulnerability.severity.max_score || vulnerability.severity.min_score || false,
cvss_min: vulnerability.severity.min_score || vulnerability.severity.max_score || false,
epss: vulnerability.epss.score || false,
effort: vulnerability.effort.likely.total_seconds || false,
effort_min: vulnerability.effort.optimistic.total_seconds || false,
effort_max: vulnerability.effort.pessimistic.total_seconds || false,
fixed: lastAssessment ? ['fixed', 'resolved', 'resolved_with_pedigree'].includes(lastAssessment.status) : false,
ignored: lastAssessment ? ['not_affected', 'false_positive'].includes(lastAssessment.status) : false,
affected: lastAssessment ? ['affected', 'exploitable'].includes(lastAssessment.status) : false,
pending: lastAssessment ? ['under_investigation', 'in_triage'].includes(lastAssessment.status) : true,
new: !lastAssessment,
},
};
});
const response = await fetch(import.meta.env.VITE_API_URL + '/api/vulnerabilities/match-condition', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ condition, items }),
});
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || `Match condition failed (${response.status})`);
return Array.isArray(data.matching_ids)
? data.matching_ids.filter((id: unknown): id is string => typeof id === 'string')
: [];
}

static async list(variantId?: string, projectId?: string, compareVariantId?: string, operation?: string, variantIds?: string[], multiOperation?: string): Promise<Vulnerability[]> {
const url = new URL(import.meta.env.VITE_API_URL + "/api/vulnerabilities", window.location.href);
url.searchParams.set('format', 'list');
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/pages/Explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function Explorer({ darkMode, setDarkMode }: Readonly<Props>) {
const vulnsRef = useRef<Vulnerability[]>([]);
const [filterLabel, setFilterLabel] = useState<"Source" | "Severity" | "Status" | "Package" | undefined>(undefined);
const [filterValue, setFilterValue] = useState<string | undefined>(undefined);
const [filterVulnerabilityIds, setFilterVulnerabilityIds] = useState<string[] | undefined>(undefined);
const [bannerMessage, setBannerMessage] = useState<string>('');
const [bannerType, setBannerType] = useState<'error' | 'success'>('success');
const [bannerVisible, setBannerVisible] = useState<boolean>(false);
Expand Down Expand Up @@ -230,7 +231,8 @@ function Explorer({ darkMode, setDarkMode }: Readonly<Props>) {
setTab('vulnerabilities');
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The matching-ID snapshot remains stored after drilling down, while TablePackages is unmounted. A subsequent vulnerability refresh therefore leaves TableVulnerabilities filtering fresh data with stale IDs. Clear filterVulnerabilityIds when the vulnerability snapshot or scope changes.


function showVulnsForPackage(packageId: string) {
function showVulnsForPackage(packageId: string, matchingVulnerabilityIds?: string[]) {
setFilterVulnerabilityIds(matchingVulnerabilityIds);
goToVulnsTabWithFilter("Package", packageId);
}

Expand All @@ -241,6 +243,7 @@ function Explorer({ darkMode, setDarkMode }: Readonly<Props>) {
if (newTab === 'vulnerabilities' && tab !== 'vulnerabilities') {
setFilterLabel(undefined);
setFilterValue(undefined);
setFilterVulnerabilityIds(undefined);
}
setTab(newTab);
}
Expand Down Expand Up @@ -297,7 +300,7 @@ function Explorer({ darkMode, setDarkMode }: Readonly<Props>) {
appendCVSS={appendCVSS}
projectId={currentProjectId}
/>}
{tab === 'packages' && <TablePackages packages={pkgs} onShowVulns={showVulnsForPackage} />}
{tab === 'packages' && <TablePackages packages={pkgs} vulnerabilities={vulns} onShowVulns={showVulnsForPackage} />}
{tab === 'vulnerabilities' &&
<TableVulnerabilities
appendAssessment={appendAssessment}
Expand All @@ -306,6 +309,7 @@ function Explorer({ darkMode, setDarkMode }: Readonly<Props>) {
vulnerabilities={vulns}
filterLabel={filterLabel}
filterValue={filterValue}
filterVulnerabilityIds={filterVulnerabilityIds}
variantId={currentVariantId}
projectId={currentProjectId}
baseVariantId={currentBaseVariantId}
Expand Down
Loading
Loading