diff --git a/openspec/specs/report-page/spec.md b/openspec/specs/report-page/spec.md
index 2ce285cd..a9ed1f77 100644
--- a/openspec/specs/report-page/spec.md
+++ b/openspec/specs/report-page/spec.md
@@ -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):
diff --git a/openspec/specs/reports-table/spec.md b/openspec/specs/reports-table/spec.md
index ea1e173d..38b0409d 100644
--- a/openspec/specs/reports-table/spec.md
+++ b/openspec/specs/reports-table/spec.md
@@ -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
diff --git a/src/main/webui/src/components/Finding.tsx b/src/main/webui/src/components/Finding.tsx
index 2243dc49..56d5e5ca 100644
--- a/src/main/webui/src/components/Finding.tsx
+++ b/src/main/webui/src/components/Finding.tsx
@@ -37,6 +37,12 @@ export const FailedStatus: React.FC = () => (
);
+export const NoComponentsAnalyzedStatus: React.FC = () => (
+
+);
+
const Finding: React.FC = ({ finding }) => {
if (!finding) return null;
@@ -56,10 +62,8 @@ const Finding: React.FC = ({ finding }) => {
label = "Not vulnerable";
color = apiToColor(JUSTIFICATION_API.NOT_VULNERABLE);
break;
- case "excluded":
- label = "Excluded";
- color = "grey";
- break;
+ case "no-components-analyzed":
+ return ;
case "uncertain":
label = "Uncertain";
color = apiToColor(JUSTIFICATION_API.UNCERTAIN);
diff --git a/src/main/webui/src/components/SbomsTable.tsx b/src/main/webui/src/components/SbomsTable.tsx
index d6c8c921..11ebcc9b 100644
--- a/src/main/webui/src/components/SbomsTable.tsx
+++ b/src/main/webui/src/components/SbomsTable.tsx
@@ -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.
}
>
diff --git a/src/main/webui/src/hooks/useReportsTableData.ts b/src/main/webui/src/hooks/useReportsTableData.ts
index 6bf732a0..b85791cf 100644
--- a/src/main/webui/src/hooks/useReportsTableData.ts
+++ b/src/main/webui/src/hooks/useReportsTableData.ts
@@ -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);
@@ -119,7 +118,6 @@ export function transformProductSummaryToRow(productSummary: ProductSummary): Re
const finding = getProductFinding(
productStatus,
- analysisState,
statusCounts,
product.submittedCount
);
diff --git a/src/main/webui/src/utils/findingDisplay.ts b/src/main/webui/src/utils/findingDisplay.ts
index c7cf511c..96233235 100644
--- a/src/main/webui/src/utils/findingDisplay.ts
+++ b/src/main/webui/src/utils/findingDisplay.ts
@@ -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;
@@ -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,
submittedCount?: number,
): Finding | null {
@@ -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;
}
diff --git a/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/GetProductsRestTest.java b/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/GetProductsRestTest.java
index a3b5afb7..a88e49c2 100644
--- a/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/GetProductsRestTest.java
+++ b/src/test/java/com/redhat/ecosystemappeng/exploitiq/rest/GetProductsRestTest.java
@@ -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()
diff --git a/src/test/resources/devservices/products/product-10.json b/src/test/resources/devservices/products/product-10.json
index 08de8c84..8fce0ac2 100644
--- a/src/test/resources/devservices/products/product-10.json
+++ b/src/test/resources/devservices/products/product-10.json
@@ -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"
+ }
+ ]
}
-
diff --git a/src/test/resources/devservices/reports/test-sbom-report-10-report-1.json b/src/test/resources/devservices/reports/test-sbom-report-10-report-1.json
deleted file mode 100644
index 8ef6ac50..00000000
--- a/src/test/resources/devservices/reports/test-sbom-report-10-report-1.json
+++ /dev/null
@@ -1,45 +0,0 @@
-{
- "_id": {
- "$oid": "507f1f77bcf86cd799439021"
- },
- "input": {
- "scan": {
- "id": "x1y2z3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a",
- "type": null,
- "started_at": "2025-02-10T10:00:00.000000",
- "vulns": [
- {
- "vuln_id": "CVE-2024-99999",
- "description": null,
- "score": null,
- "severity": null,
- "published_date": null,
- "last_modified_date": null,
- "url": null,
- "feed_group": null,
- "package": null,
- "package_version": null,
- "package_name": null,
- "package_type": null
- }
- ]
- },
- "image": {
- "name": "test-image-10",
- "tag": "v1.0.0",
- "source_info": [
- {
- "type": "code",
- "git_repo": "https://github.com/example/test-product-10",
- "ref": "main"
- }
- ]
- }
- },
- "metadata": {
- "product_id": "product-10",
- "submitted_at": "2025-02-10T10:00:00.000000Z",
- "user": "test@example.com"
- },
- "info": {}
-}
\ No newline at end of file
diff --git a/src/test/resources/devservices/reports/test-sbom-report-10-report-2.json b/src/test/resources/devservices/reports/test-sbom-report-10-report-2.json
deleted file mode 100644
index db394208..00000000
--- a/src/test/resources/devservices/reports/test-sbom-report-10-report-2.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "_id": {
- "$oid": "507f1f77bcf86cd799439022"
- },
- "input": {
- "scan": {
- "id": "y2z3a4b5-c6d7-4e8f-9a0b-1c2d3e4f5a6b",
- "type": null,
- "started_at": "2025-02-10T10:05:00.000000",
- "vulns": [
- {
- "vuln_id": "CVE-2024-99999",
- "description": null,
- "score": null,
- "severity": null,
- "published_date": null,
- "last_modified_date": null,
- "url": null,
- "feed_group": null,
- "package": null,
- "package_version": null,
- "package_name": null,
- "package_type": null
- }
- ]
- },
- "image": {
- "name": "test-image-10",
- "tag": "v1.0.1",
- "source_info": [
- {
- "type": "code",
- "git_repo": "https://github.com/example/test-product-10",
- "ref": "main"
- }
- ]
- }
- },
- "metadata": {
- "product_id": "product-10",
- "submitted_at": "2025-02-10T10:00:00.000000Z",
- "sent_at": "2025-02-10T10:05:00.000000Z",
- "user": "test@example.com"
- },
- "info": {}
-}
\ No newline at end of file