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
2 changes: 1 addition & 1 deletion openspec/specs/report-page/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ The report page SHALL display an embedded repository reports table that conforms
### Requirement: Report Page Layout
The report page SHALL use PatternFly layout components and follow the standard page structure. The report page SHALL display a breadcrumb navigation, page title, and **product analysis status** at the top of the page. The product analysis status SHALL appear on its own row **directly below** the page title (not inline beside the title).

The product analysis status SHALL be exactly one of two labels: **In progress** or **Completed**. It SHALL NOT display vulnerability findings (Vulnerable, Uncertain, Not vulnerable, Excluded, Failed)—those remain in the Finding column on the reports table and in per-repository rows.
The product analysis status SHALL be exactly one of two labels: **In progress** or **Completed**. It SHALL NOT display vulnerability findings (Vulnerable, Uncertain, Not vulnerable, No components analyzed, Failed)—those remain in the Finding column on the reports table and in per-repository rows.

Status SHALL be derived from backend-computed `summary.productState` on `ProductSummary` (same rules as `ReportRepositoryService` product summary aggregation):

Expand Down
26 changes: 18 additions & 8 deletions openspec/specs/reports-table/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,23 +125,33 @@ When a product has exactly one submitted report (`submittedCount === 1`), clicki
- **AND** only the clicked row shows the loading indicator (other rows remain unchanged)

### Requirement: Reports Table Finding Column
The reports table SHALL display a "Finding" column with one finding per product. If any component report is still in pending, queued, or sent state, the Finding SHALL be "In progress" until every component has a terminal outcome (completed, failed, expired, or excluded). After that, six outcomes apply in priority order: Vulnerable > Uncertain > Failed > Excluded > Not vulnerable. Only the single applicable finding is shown per row. Counts for Vulnerable, Uncertain, and Excluded only; no count for In progress, Not vulnerable, or Failed.
The reports table SHALL display a "Finding" column with one finding per product. If any component report is still in pending, queued, or sent state, the Finding SHALL be "In progress" until every component has a terminal outcome (completed, failed, expired, or excluded). After that, outcomes apply in priority order: Vulnerable > Uncertain > Failed > Not vulnerable > No components analyzed. Only the single applicable finding is shown per row. Counts for Vulnerable and Uncertain only; no count for In progress, Not vulnerable, Failed, or No components analyzed.

The legacy Finding label **Excluded** (including count forms such as "2 Excluded") SHALL NOT be shown in the Finding column.

#### Scenario: Finding column header
- **WHEN** a user views the reports table
- **THEN** the column header is "Finding" with a help icon and popover explaining priority and states
- **AND** the popover states that while any repository is pending, queued, or sent the finding is In progress; after that the highest-priority analyzed outcome is shown; Not vulnerable applies when analyzed components are not vulnerable even if some components were excluded; No components analyzed applies when every submitted component was excluded

#### Scenario: Finding by priority
- **WHEN** a product has any report in pending, queued, or sent state → display "In progress", no count, outlined grey label and InProgressIcon (even if some other repositories already have a vulnerable or uncertain result)
- **WHEN** no reports are pending, queued, or sent, and the product has one or more vulnerable repositories → display "Vulnerable" + count, red (danger) label
- **WHEN** no reports are pending, queued, or sent, zero vulnerable, and one or more uncertain → display "Uncertain" + count, orange (warning) label
- **WHEN** no reports are pending, queued, or sent, zero vulnerable, zero uncertain, no failed/expired reports, and one or more excluded components (`statusCounts["excluded"]` > 0) → display "Excluded" + count, with label styling as defined (e.g. grey or info)
- **WHEN** no reports are pending, queued, or sent, 100% of reports are completed and 100% are not vulnerable and no excluded components → display "Not vulnerable", no count, green (success) label

#### Scenario: Excluded state uses submission failure count
- **WHEN** a product has excluded components (submissionFailures length > 0) and no higher-priority finding
- **THEN** the Finding column displays "Excluded" with the count equal to `statusCounts["excluded"]` (length of product submissionFailures)
- **AND** the count is shown in the same format as Vulnerable and Uncertain (e.g. "3 Excluded")
- **WHEN** no reports are pending, queued, or sent, zero vulnerable, zero uncertain, and one or more failed/expired reports → display "Failed", no count, grey filled label with ExclamationCircleIcon
- **WHEN** no reports are pending, queued, or sent, zero vulnerable, zero uncertain, no failed/expired reports, and one or more analyzed components are not vulnerable (even if `statusCounts["excluded"]` > 0 but not 100% of submitted components) → display "Not vulnerable", no count, green (success) label
- **WHEN** no reports are pending, queued, or sent, zero vulnerable, zero uncertain, no failed/expired reports, and 100% of submitted components are excluded (`statusCounts["excluded"]` equals `submittedCount`) → display "No components analyzed", no count, purple filled PatternFly Label (default size/state; no icon)

#### Scenario: Mixed excluded and analyzed components use analyzed finding
- **WHEN** a product has one or more excluded components and one or more successfully analyzed components with an active finding status (Vulnerable, Uncertain, or Not vulnerable)
- **THEN** the Finding column displays the analyzed outcome according to priority Vulnerable > Uncertain > Not vulnerable
- **AND** the Finding column does not display "Excluded" or any excluded count

#### Scenario: All components excluded shows No components analyzed
- **WHEN** a product has `submittedCount` equal to the excluded count (`statusCounts["excluded"]`, from `submissionFailures.length`) and no higher-priority finding applies
- **THEN** the Finding column displays "No components analyzed"
- **AND** the label uses PatternFly `color="purple"` and `variant="filled"` with no icon and no numeric count
- **AND** the Finding column does not display "Excluded" or an excluded count

### Requirement: Reports Page Tabs and Single Repositories

Expand Down
12 changes: 8 additions & 4 deletions src/main/webui/src/components/Finding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export const FailedStatus: React.FC = () => (
</Label>
);

export const NoComponentsAnalyzedStatus: React.FC = () => (
<Label color="purple" variant="filled">
No components analyzed
</Label>
);

const Finding: React.FC<FindingProps> = ({ finding }) => {
if (!finding) return null;

Expand All @@ -56,10 +62,8 @@ const Finding: React.FC<FindingProps> = ({ finding }) => {
label = "Not vulnerable";
color = apiToColor(JUSTIFICATION_API.NOT_VULNERABLE);
break;
case "excluded":
label = "Excluded";
color = "grey";
break;
case "no-components-analyzed":
return <NoComponentsAnalyzedStatus />;
case "uncertain":
label = "Uncertain";
color = apiToColor(JUSTIFICATION_API.UNCERTAIN);
Expand Down
10 changes: 7 additions & 3 deletions src/main/webui/src/components/SbomsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,13 @@ const SbomsTable: React.FC = () => {
While any repository is still pending, queued, or
sent, the finding is In progress. After every
repository has finished, this shows the highest risk
level detected across all of them. Not Vulnerable
appears only when every repository is analyzed and
found to be not vulnerable.
level detected across analyzed components (Vulnerable,
Uncertain, Failed, then Not vulnerable). Not
Vulnerable appears only when every analyzed
repository is found to be not vulnerable. Excluded
components do not override an analyzed finding. When
every submitted component was excluded, the finding is
No components analyzed.
</div>
}
>
Expand Down
2 changes: 0 additions & 2 deletions src/main/webui/src/hooks/useReportsTableData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ export function transformProductSummaryToRow(productSummary: ProductSummary): Re
const completedAt = product.completedAt || "";

const statusCounts = summary.statusCounts || {};
const analysisState = summary.productState;

// Repositories analyzed from shared utility (completed + data.submittedCount, "analyzed" suffix)
const { getDisplay } = getRepositoriesAnalyzedFromProduct(productSummary);
Expand All @@ -119,7 +118,6 @@ export function transformProductSummaryToRow(productSummary: ProductSummary): Re

const finding = getProductFinding(
productStatus,
analysisState,
statusCounts,
product.submittedCount
);
Expand Down
23 changes: 12 additions & 11 deletions src/main/webui/src/utils/findingDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type Finding =
| { type: "uncertain"; count?: number }
| { type: "in-progress" }
| { type: "failed" }
| { type: "excluded"; count?: number };
| { type: "no-components-analyzed" };

export type ProductStatus = {
vulnerableCount: number;
Expand Down Expand Up @@ -75,14 +75,14 @@ export function getProductAnalysisStatus(
/**
* Returns the single prioritized finding for a product row (reports table).
* While any repository is still pending, queued, or sent, the row shows In progress only;
* after all have a terminal outcome, priority is: Vulnerable > Uncertain > Failed > Excluded > Not vulnerable.
* after all have a terminal outcome, priority is:
* Vulnerable > Uncertain > Failed > Not vulnerable > No components analyzed.
* Only returns type + optional count; color/variant are applied by Finding component.
* totalCount uses submittedCount (e.g. productSummary.data.submittedCount) when provided.
* Excluded uses statusCounts["excluded"] (submission failure count from API).
* Not vulnerable applies when any analyzed component is not vulnerable (exclusions do not block it).
* No components analyzed applies when statusCounts["excluded"] equals submittedCount (100% excluded).
*/
export function getProductFinding(
productStatus: ProductStatus,
analysisState: string,
statusCounts: Record<string, number>,
submittedCount?: number,
): Finding | null {
Expand All @@ -104,15 +104,16 @@ export function getProductFinding(
if (hasFailedInCounts(statusCounts)) {
return { type: "failed" };
}
const excludedCount = getExcludedCount(statusCounts);
if (excludedCount > 0) {
return { type: "excluded", count: excludedCount };
if (productStatus.notVulnerableCount > 0) {
return { type: "not-vulnerable" };
}
const excludedCount = getExcludedCount(statusCounts);
if (
analysisState === "completed" &&
productStatus.notVulnerableCount === submittedCount
submittedCount !== undefined &&
submittedCount > 0 &&
excludedCount === submittedCount
) {
return { type: "not-vulnerable" };
return { type: "no-components-analyzed" };
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,24 @@ void testGetSbomReportById_ReturnsExpectedStructure() {
.body("summary.justificationStatusCounts", equalTo(java.util.Map.of("FALSE", 1, "TRUE", 1)));
}

@Test
void testGetSbomReportById_AllExcludedProduct_ReturnsExcludedStatusCount() {
String productId = "product-10";

RestAssured.given()
.when()
.get("/api/v1/reports/product/{id}", productId)
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("data.id", equalTo(productId))
.body("data.name", equalTo("test-sbom-product-10"))
.body("data.submittedCount", equalTo(5))
.body("data.submissionFailures", hasSize(5))
.body("summary.statusCounts.excluded", equalTo(5))
.body("summary.productState", equalTo("completed"));
}

@Test
void testGetSbomReportById_NotFound() {
RestAssured.given()
Expand Down
37 changes: 34 additions & 3 deletions src/test/resources/devservices/products/product-10.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,39 @@
"version": "1.0.0",
"cveId": "CVE-2024-99999",
"submittedAt": "2025-02-10T10:00:00.000000Z",
"submittedCount": 2,
"submittedCount": 5,
"completedAt": "2025-02-10T10:30:00.000000Z",
"metadata": {},
"submissionFailures": []
"submissionFailures": [
{
"name": "excluded-component-1",
"version": "1.0.0",
"image": "pkg:oci/excluded-component-1@sha256:aaa111",
"error": "Component excluded from analysis due to unsupported package type"
},
{
"name": "excluded-component-2",
"version": "1.0.0",
"image": "pkg:oci/excluded-component-2@sha256:aaa222",
"error": "Component excluded from analysis due to unsupported package type"
},
{
"name": "excluded-component-3",
"version": "1.0.0",
"image": "pkg:oci/excluded-component-3@sha256:aaa333",
"error": "Component excluded from analysis due to unsupported package type"
},
{
"name": "excluded-component-4",
"version": "1.0.0",
"image": "pkg:oci/excluded-component-4@sha256:aaa444",
"error": "Component excluded from analysis due to unsupported package type"
},
{
"name": "excluded-component-5",
"version": "1.0.0",
"image": "pkg:oci/excluded-component-5@sha256:aaa555",
"error": "Component excluded from analysis due to unsupported package type"
}
]
}

This file was deleted.

This file was deleted.