Skip to content

feat(scorecard): add the ability to drill down into aggregated scorecard data - #2350

Merged
Eswaraiahsapram merged 23 commits into
redhat-developer:mainfrom
PatAKnight:scorecard-aggregated-drill-down
Mar 6, 2026
Merged

feat(scorecard): add the ability to drill down into aggregated scorecard data#2350
Eswaraiahsapram merged 23 commits into
redhat-developer:mainfrom
PatAKnight:scorecard-aggregated-drill-down

Conversation

@PatAKnight

@PatAKnight PatAKnight commented Feb 18, 2026

Copy link
Copy Markdown
Member

User description

Hey, I just made a Pull Request!

This PR adds the ability to drill down from aggregated scorecard KPIs to view the individual entities that contribute to the overall score. This enables managers and platform engineers to identify specific services impacting metrics and troubleshoot issues at the entity level.

Included is a new endpoint GET /metrics/:metricId/catalog/aggregations/entities that includes the ability to filter by status, owner, kind, and entity name. The owner parameter accepts multiple values (e.g., ?owner=a&owner=b, up to 50) so that frontends can implement "owned by me" scoping by passing the user's ownership refs directly. The endpoint also includes the ability to sort by entity name, owner, kind, timestamp, and metric value. Finally, offset-based pagination has been included.

I also added the entity_kind and entity_owner columns to the metric_values table. This allows all filtering and sorting to be performed at the database level, keeping queries efficient even at large entity counts. All filters, ORDER BY, and LIMIT/OFFSET are pushed to the database so only the requested page of rows is ever fetched.

The entityName filter performs a case-insensitive substring search against the full entity reference (kind:namespace/name), rather than just the entity name, which keeps it consistent with how references are stored and avoids an additional column.

I also batch-fetch from the catalog using getEntitiesByRefs to ensure metadata is accurate and up-to-date. This call also serves as the per-row authorization gate — entities the user doesn't have catalog.entity.read access to are returned as null by the catalog and are excluded from the response.

Which issue(s) does this PR fix

✔️ Checklist

  • A changeset describing the change and affected packages. (more info)
  • Added or Updated documentation
  • Tests for new functionality and regression tests for bug fixes
  • Screenshots attached (for UI changes)

Testing the changes

# Set environment variables for convenience
export BACKSTAGE_URL="http://localhost:7007"
export TOKEN="your-auth-token-here"

1. Get All Entities for a Metric (Default)

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" | jq

2. Get Second Page (if you have multiple pages)

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?page=2&pageSize=5" \
  -H "Authorization: Bearer ${TOKEN}" | jq

3. Get More Results Per Page

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?pageSize=10" \
  -H "Authorization: Bearer ${TOKEN}" | jq

4. Get Entities in Error State

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?status=error" \
  -H "Authorization: Bearer ${TOKEN}" | jq

5. Get Only Components

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?kind=Component" \
  -H "Authorization: Bearer ${TOKEN}" | jq

6. Get Only Systems (lowercase — filter is case-insensitive)

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?kind=system" \
  -H "Authorization: Bearer ${TOKEN}" | jq

7. Get Entities Owned by Development Guests Team

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?owner=group:development/guests" \
  -H "Authorization: Bearer ${TOKEN}" | jq

8. Get Entities Owned by Guest User

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?owner=user:development/guest" \
  -H "Authorization: Bearer ${TOKEN}" | jq

9. Get Entities Owned by Multiple Teams (repeat the owner param)

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?owner=group:development/guests&owner=user:development/guest" \
  -H "Authorization: Bearer ${TOKEN}" | jq

10. Search for "scorecard" Services (substring match against full entity ref)

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?entityName=scorecard" \
  -H "Authorization: Bearer ${TOKEN}" | jq

11. Search Using the Full Ref Format

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?entityName=component:default/my-service" \
  -H "Authorization: Bearer ${TOKEN}" | jq

12. Sort by Entity Name (Alphabetically)

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?sortBy=entityName&sortOrder=asc" \
  -H "Authorization: Bearer ${TOKEN}" | jq

13. Sort by Metric Value (Highest First)

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?sortBy=metricValue&sortOrder=desc" \
  -H "Authorization: Bearer ${TOKEN}" | jq

14. Combine Filters — Team Errors Sorted by Metric Value

curl -X GET "${BACKSTAGE_URL}/api/scorecard/metrics/github.open_prs/catalog/aggregations/entities?owner=group:development/guests&status=error&sortBy=metricValue&sortOrder=desc&page=1&pageSize=10" \
  -H "Authorization: Bearer ${TOKEN}" | jq

PR Type

Enhancement


Description

  • Add drill-down endpoint to view individual entities contributing to aggregated scorecard metrics

  • Implement database-level filtering by status, owner, and kind for optimal performance

  • Add entity metadata columns (entity_kind, entity_owner) to metric_values table with indexes

  • Support application-level filtering by entity name, sorting, and pagination with comprehensive tests

  • Include batch catalog enrichment and graceful fallback when catalog metadata unavailable


Diagram Walkthrough

flowchart LR
  A["GET /metrics/:metricId/catalog/aggregations/entities"] -->|Query params| B["Router Handler"]
  B -->|Fetch metrics| C["CatalogMetricService.getEntityMetricDetails"]
  C -->|Database query| D["DatabaseMetricValues.readEntityMetricsByStatus"]
  D -->|Filter & paginate| E["metric_values table"]
  C -->|Batch fetch| F["Catalog Service"]
  F -->|Enrich with metadata| G["EntityMetricDetail[]"]
  G -->|Apply app-level filters| H["Sort & paginate"]
  H -->|Return response| I["EntityMetricDetailResponse"]
Loading

File Walkthrough

Relevant files
Enhancement
7 files
DatabaseMetricValues.ts
Add readEntityMetricsByStatus method for filtered entity metrics
+61/-0   
types.ts
Add entity_kind and entity_owner fields to metric types   
+4/-0     
CatalogMetricService.ts
Implement getEntityMetricDetails with filtering, sorting, pagination
+213/-1 
router.ts
Add GET endpoint for entity drill-down with permission checks
+85/-0   
PullMetricsByProviderTask.ts
Populate entity_kind and entity_owner during metric collection
+10/-0   
Metric.ts
Add EntityMetricDetail and EntityMetricDetailResponse types
+35/-0   
plugin.ts
Pass logger to CatalogMetricService constructor                   
+1/-0     
Configuration changes
1 files
20260217152637_add_entity_metadata_columns.js
Create migration to add entity_kind and entity_owner columns
+41/-0   
Tests
6 files
DatabaseMetricValues.test.ts
Add comprehensive tests for readEntityMetricsByStatus functionality
+603/-0 
CatalogMetricService.test.ts
Add tests for getEntityMetricDetails with filters and sorting
+451/-1 
router.test.ts
Add endpoint tests for entity drill-down with permissions
+358/-0 
PullMetricsByProviderTask.test.ts
Update tests to verify entity metadata population               
+4/-0     
mockDatabaseMetricValues.ts
Add mock for readEntityMetricsByStatus database method     
+8/-0     
mockMetricProvidersRegistry.ts
Add getMetric mock implementation to registry                       
+6/-0     
Documentation
2 files
drill-down.md
Add comprehensive documentation for drill-down endpoint   
+483/-0 
sour-coins-check.md
Add changeset for drill-down feature release                         
+6/-0     

@rhdh-gh-app

rhdh-gh-app Bot commented Feb 18, 2026

Copy link
Copy Markdown

Important

This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior.

Changed Packages

Package Name Package Path Changeset Bump Current Version
@red-hat-developer-hub/backstage-plugin-scorecard-backend workspaces/scorecard/plugins/scorecard-backend minor v2.4.0
@red-hat-developer-hub/backstage-plugin-scorecard-common workspaces/scorecard/plugins/scorecard-common minor v2.4.0

@rhdh-qodo-merge

rhdh-qodo-merge Bot commented Feb 18, 2026

Copy link
Copy Markdown

PR Compliance Guide 🔍

(Compliance updated until commit bc10a2a)

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- describe: readEntityMetricsByStatus
- readEntityMetricsByStatus
- normalizeOwner
- describe: getEntityMetricDetails
- it: should fetch entity metrics with default options
Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
No access audit: The new drill-down read endpoint returns per-entity metric details but does not emit an
audit-style log event identifying the user, action, and outcome, which may be required if
this data is considered sensitive.

Referred Code
router.get(
  '/metrics/:metricId/catalog/aggregations/entities',
  async (req, res) => {
    const { metricId } = req.params;

    const {
      page,
      pageSize,
      status,
      owner,
      kind,
      namespace,
      entityName,
      sortBy,
      sortOrder,
    } = validateDrillDownMetricsSchema(req.query, logger);

    const credentials = await httpAuth.credentials(req, { allow: ['user'] });

    const { conditions } = await authorizeConditional(
      credentials,


 ... (clipped 36 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

Previous compliance checks

Compliance check up to commit 64777b9
Security Compliance
Potential DoS vector

Description: The new drill-down path can trigger expensive work by fetching up to 10,000 DB rows and
then making potentially many sequential catalog.getEntitiesByRefs calls (in 100-item
batches) per request before returning results, which could be abused by an authenticated
user to cause denial-of-service via repeated requests (even though unauthorized entities
are filtered out).
CatalogMetricService.ts [201-308]

Referred Code
async getEntityMetricDetails(
  metricId: string,
  credentials: BackstageCredentials,
  options: {
    status?: 'success' | 'warning' | 'error';
    owner?: string[];
    kind?: string;
    entityName?: string;
    sortBy?:
      | 'entityName'
      | 'owner'
      | 'entityKind'
      | 'timestamp'
      | 'metricValue';
    sortOrder?: 'asc' | 'desc';
    page: number;
    limit: number;
  },
): Promise<EntityMetricDetailResponse> {
  // Get metric metadata
  const metric = this.registry.getMetric(metricId);


 ... (clipped 87 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- describe: readEntityMetricsByStatus
- readEntityMetricsByStatus
- normalizeOwner
- describe: getEntityMetricDetails
- it: should fetch entity metrics with default options
Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit context: The new drill-down read endpoint does not emit an audit log including the calling
user/entity reference and outcome, so it is unclear whether access to potentially
sensitive per-entity metric data is fully traceable.

Referred Code
router.get(
  '/metrics/:metricId/catalog/aggregations/entities',
  async (req, res) => {
    const { metricId } = req.params;

    const {
      page,
      pageSize,
      status,
      owner,
      kind,
      entityName,
      sortBy,
      sortOrder,
    } = validateDrillDownMetricsSchema(req.query, logger);

    const credentials = await httpAuth.credentials(req, { allow: ['user'] });

    const { conditions } = await authorizeConditional(
      credentials,
      scorecardMetricReadPermission,


 ... (clipped 35 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance check up to commit 338421c
Security Compliance
Denial of service

Description: The drill-down path always queries up to MAX_FETCHABLE_ROWS (10,000) and then calls
catalog.getEntitiesByRefs in multiple batches per request, which can be abused with
repeated requests to cause resource exhaustion/DoS (DB load + many catalog round-trips),
even when requesting small pages.
CatalogMetricService.ts [223-318]

Referred Code
// High-page early-exit guard
if (
  (options.page - 1) * options.limit >=
  CatalogMetricService.MAX_FETCHABLE_ROWS
) {
  return {
    metricId: metric.id,
    metricMetadata: {
      title: metric.title,
      description: metric.description,
      type: metric.type,
    },
    entities: [],
    pagination: {
      page: options.page,
      pageSize: options.limit,
      total: 0,
      totalPages: 0,
      isCapped: false,
    },
  };


 ... (clipped 75 lines)
Sensitive data persistence

Description: normalizeOwner JSON-stringifies non-string entity.spec.owner values and persists the
result into entity_owner, which could unintentionally store unexpected structured data
(potentially sensitive or large) in the database and later expose it via filtering/sorting
paths.
PullMetricsByProviderTask.ts [161-208]

Referred Code
        value,
        timestamp: new Date(),
        status,
        entity_kind: entity.kind,
        entity_owner: normalizeOwner(entity?.spec?.owner),
      } as DbMetricValueCreate;
    } catch (error) {
      return {
        catalog_entity_ref: stringifyEntityRef(entity),
        metric_id: this.providerId,
        value,
        timestamp: new Date(),
        error_message:
          error instanceof Error ? error.message : String(error),
        entity_kind: entity.kind,
        entity_owner: normalizeOwner(entity?.spec?.owner),
      } as DbMetricValueCreate;
    }
  }),
).then(promises =>
  promises.reduce((acc, curr) => {


 ... (clipped 27 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- describe: readEntityMetricsByStatus
- readEntityMetricsByStatus
- describe: getEntityMetricDetails
- it: should fetch entity metrics with default options
- it: should enrich entities with catalog metadata
Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing access audit: The new drill-down read endpoint does not emit an explicit audit log entry (e.g., user
identity, metricId, filters, outcome), so it is unclear whether access to detailed
metric/entity data is captured for audit purposes.

Referred Code
router.get(
  '/metrics/:metricId/catalog/aggregations/entities',
  async (req, res) => {
    const { metricId } = req.params;

    const {
      page,
      pageSize,
      status,
      owner,
      kind,
      entityName,
      sortBy,
      sortOrder,
    } = validateDrillDownMetricsSchema(req.query, logger);

    const credentials = await httpAuth.credentials(req, { allow: ['user'] });

    const { conditions } = await authorizeConditional(
      credentials,
      scorecardMetricReadPermission,


 ... (clipped 35 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance check up to commit a14730f
Security Compliance
🔴
Authorization count leakage

Description: The endpoint returns pagination.total and totalPages computed from the database total
(dbTotal) before per-entity authorization filtering via catalog.getEntitiesByRefs, which
can leak the existence/count of unauthorized entities matching the query (e.g., a user
sees total: 500 but only receives 2 authorized rows).
CatalogMetricService.ts [201-305]

Referred Code
async getEntityMetricDetails(
  metricId: string,
  credentials: BackstageCredentials,
  options: {
    status?: 'success' | 'warning' | 'error';
    owner?: string[];
    kind?: string;
    entityName?: string;
    sortBy?:
      | 'entityName'
      | 'owner'
      | 'entityKind'
      | 'timestamp'
      | 'metricValue';
    sortOrder?: 'asc' | 'desc';
    page: number;
    limit: number;
  },
): Promise<EntityMetricDetailResponse> {
  const dbPagination = {
    limit: options.limit,


 ... (clipped 84 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- describe: readEntityMetricsByStatus
- readEntityMetricsByStatus
- describe: getEntityMetricDetails
- it: should fetch entity metrics with default options
- it: should enrich entities with catalog metadata
Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unauthorized count leak: The response pagination.total/totalPages are computed from the database dbTotal even
though unauthorized entities are removed via catalog.getEntitiesByRefs, which can leak the
existence/count of entities the user is not permitted to read.

Referred Code
// Only include entities the catalog confirmed the user can access.
// Unauthorized entities are returned as null by getEntitiesByRefs and are never added
// to entityMap, so they are silently excluded here.
const enrichedEntities: EntityMetricDetail[] = rows
  .filter(row => entityMap.has(row.catalog_entity_ref))
  .map(row => {
    const entity = entityMap.get(row.catalog_entity_ref);
    return {
      entityRef: row.catalog_entity_ref,
      entityName: entity?.metadata?.name ?? 'Unknown',
      entityKind: entity?.kind ?? row.entity_kind ?? 'Unknown',
      owner:
        (entity?.spec?.owner as string) ?? row.entity_owner ?? 'Unknown',
      metricValue: row.value,
      timestamp: new Date(row.timestamp).toISOString(),
      status: row.status ?? 'error', // default to error if status is null
    };
  });

// Format and return response
return {


 ... (clipped 14 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logging: The new drill-down read endpoint does not appear to emit any audit log entry (who, what
metric, filters, outcome), so whether this read of potentially sensitive metric/entity
data is appropriately auditable cannot be verified from the diff.

Referred Code
router.get(
  '/metrics/:metricId/catalog/aggregations/entities',
  async (req, res) => {
    const { metricId } = req.params;

    const {
      page,
      pageSize,
      status,
      owner,
      kind,
      entityName,
      sortBy,
      sortOrder,
    } = validateDrillDownMetricsSchema(req.query, logger);

    const { conditions } = await authorizeConditional(
      req,
      scorecardMetricReadPermission,
    );



 ... (clipped 37 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Pagination edge mismatch: When catalog authorization filters out rows (or catalog fails), the returned
pagination.total/totalPages are still based on dbTotal, so clients may see inconsistent
pagination vs returned entities and may not be able to page reliably.

Referred Code
// Only include entities the catalog confirmed the user can access.
// Unauthorized entities are returned as null by getEntitiesByRefs and are never added
// to entityMap, so they are silently excluded here.
const enrichedEntities: EntityMetricDetail[] = rows
  .filter(row => entityMap.has(row.catalog_entity_ref))
  .map(row => {
    const entity = entityMap.get(row.catalog_entity_ref);
    return {
      entityRef: row.catalog_entity_ref,
      entityName: entity?.metadata?.name ?? 'Unknown',
      entityKind: entity?.kind ?? row.entity_kind ?? 'Unknown',
      owner:
        (entity?.spec?.owner as string) ?? row.entity_owner ?? 'Unknown',
      metricValue: row.value,
      timestamp: new Date(row.timestamp).toISOString(),
      status: row.status ?? 'error', // default to error if status is null
    };
  });

// Format and return response
return {


 ... (clipped 14 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance check up to commit 8dec549
Security Compliance
🔴
Authorization count leak

Description: The API returns pagination total/totalPages derived from the database row count (dbTotal)
even though unauthorized entities are filtered out after catalog.getEntitiesByRefs, which
can leak the existence/volume of entities the caller is not allowed to read (e.g.,
entities length is 2 but total may be 100).
CatalogMetricService.ts [226-305]

Referred Code
const { rows, total: dbTotal } =
  await this.database.readEntityMetricsByStatus(metricId, {
    status: options.status,
    entityName: options.entityName,
    entityKind: options.kind,
    entityOwner: options.owner,
    sortBy: options.sortBy,
    sortOrder: options.sortOrder,
    pagination: dbPagination,
  });

// Get metric metadata
const metric = this.registry.getMetric(metricId);

// Batch-fetch entities from catalog using user credentials.
// The catalog enforces catalog.entity.read permissions — entities the user
// cannot access are returned as null in response.items.
const entityRefsToFetch = rows.map(row => row.catalog_entity_ref);
const entityMap = new Map<string, Entity>();

if (entityRefsToFetch.length > 0) {


 ... (clipped 59 lines)
SQL injection risk

Description: The query uses orderByRaw with an interpolated ${direction} string, so if
readEntityMetricsByStatus is ever called with unvalidated sortOrder (outside the router’s
Zod validation), it could enable SQL injection via crafted sort direction.
DatabaseMetricValues.ts [170-181]

Referred Code
const column =
  (options.sortBy && sortColumnMap[options.sortBy]) ?? 'timestamp';
const direction = options.sortOrder ?? 'desc';

// Nulls last for metricValue (value can be null)
if (options.sortBy === 'metricValue') {
  query.orderByRaw(
    `value IS NULL, CAST(CAST(value AS TEXT) AS REAL) ${direction}`,
  );
} else {
  query.orderBy(column, direction);
}
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- describe: readEntityMetricsByStatus
- readEntityMetricsByStatus
- describe: getEntityMetricDetails
- it: should fetch entity metrics with default options
- it: should enrich entities with catalog metadata
Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Incorrect pagination totals: The response pagination.total/totalPages are calculated from the database total before
catalog authorization filtering, so counts/pages can be incorrect and not reflect the
returned entities.

Referred Code
// Only include entities the catalog confirmed the user can access.
// Unauthorized entities are returned as null by getEntitiesByRefs and are never added
// to entityMap, so they are silently excluded here.
const enrichedEntities: EntityMetricDetail[] = rows
  .filter(row => entityMap.has(row.catalog_entity_ref))
  .map(row => {
    const entity = entityMap.get(row.catalog_entity_ref);
    return {
      entityRef: row.catalog_entity_ref,
      entityName: entity?.metadata?.name ?? 'Unknown',
      entityKind: entity?.kind ?? row.entity_kind ?? 'Unknown',
      owner:
        (entity?.spec?.owner as string) ?? row.entity_owner ?? 'Unknown',
      metricValue: row.value,
      timestamp: new Date(row.timestamp).toISOString(),
      status: row.status ?? 'error', // default to error if status is null
    };
  });

// Format and return response
return {


 ... (clipped 14 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Detailed validation errors: The thrown InputError includes ${parsed.error.message} which may expose internal
validation/schema details to end users instead of a generic message.

Referred Code
const parsed = drillDownSchema.safeParse(query);

if (!parsed.success) {
  throw new InputError(`Invalid query parameters: ${parsed.error.message}`);
}

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unauthorized count leakage: The endpoint returns pagination.total derived from the DB (dbTotal) even when unauthorized
entities are filtered out (or when catalog lookup fails), which can leak information about
entities the user cannot read.

Referred Code
// Only include entities the catalog confirmed the user can access.
// Unauthorized entities are returned as null by getEntitiesByRefs and are never added
// to entityMap, so they are silently excluded here.
const enrichedEntities: EntityMetricDetail[] = rows
  .filter(row => entityMap.has(row.catalog_entity_ref))
  .map(row => {
    const entity = entityMap.get(row.catalog_entity_ref);
    return {
      entityRef: row.catalog_entity_ref,
      entityName: entity?.metadata?.name ?? 'Unknown',
      entityKind: entity?.kind ?? row.entity_kind ?? 'Unknown',
      owner:
        (entity?.spec?.owner as string) ?? row.entity_owner ?? 'Unknown',
      metricValue: row.value,
      timestamp: new Date(row.timestamp).toISOString(),
      status: row.status ?? 'error', // default to error if status is null
    };
  });

// Format and return response
return {


 ... (clipped 14 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing access logging: The new drill-down read endpoint does not emit any explicit audit log for access, and
whether this constitutes a “critical action” cannot be verified from the diff alone.

Referred Code
router.get(
  '/metrics/:metricId/catalog/aggregations/entities',
  async (req, res) => {
    const { metricId } = req.params;

    const {
      page,
      pageSize,
      status,
      owner,
      kind,
      entityName,
      sortBy,
      sortOrder,
    } = validateDrillDownMetricsSchema(req.query);

    const { conditions } = await authorizeConditional(
      req,
      scorecardMetricReadPermission,
    );



 ... (clipped 37 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance check up to commit 9d3523a
Security Compliance
🔴
Authorization bypass

Description: When catalog.getEntitiesByRefs fails, the code sets catalogAvailable = false and then
returns all DB rows without filtering unauthorized entities, which can leak entity metric
details to users who lack catalog.entity.read access (an authorization bypass triggered by
catalog outage or induced failure).
CatalogMetricService.ts [241-289]

Referred Code
// Batch-fetch entities from catalog using user credentials.
// The catalog enforces catalog.entity.read permissions — entities the user
// cannot access are returned as null in response.items.
const entityRefsToFetch = rows.map(row => row.catalog_entity_ref);
const entityMap = new Map<string, Entity>();
let catalogAvailable = true;

if (entityRefsToFetch.length > 0) {
  try {
    const response = await this.catalog.getEntitiesByRefs(
      {
        entityRefs: entityRefsToFetch,
        fields: ['kind', 'metadata', 'spec'],
      },
      { credentials },
    );

    // Build map of ref -> entity (null entries = unauthorized, not added to map)
    entityRefsToFetch.forEach((ref, index) => {
      const entity = response.items[index];
      if (entity) {


 ... (clipped 28 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- describe: readEntityMetricsByStatus
- readEntityMetricsByStatus
- describe: getEntityMetricDetails
- it: should fetch entity metrics with default options
- it: should enrich entities with catalog metadata
Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Auth bypass on fallback: When catalog.getEntitiesByRefs fails the code returns all DB rows with fallback metadata,
which bypasses the catalog permission gate and can expose entity metric data to callers
who would otherwise be filtered as unauthorized.

Referred Code
// Batch-fetch entities from catalog using user credentials.
// The catalog enforces catalog.entity.read permissions — entities the user
// cannot access are returned as null in response.items.
const entityRefsToFetch = rows.map(row => row.catalog_entity_ref);
const entityMap = new Map<string, Entity>();
let catalogAvailable = true;

if (entityRefsToFetch.length > 0) {
  try {
    const response = await this.catalog.getEntitiesByRefs(
      {
        entityRefs: entityRefsToFetch,
        fields: ['kind', 'metadata', 'spec'],
      },
      { credentials },
    );

    // Build map of ref -> entity (null entries = unauthorized, not added to map)
    entityRefsToFetch.forEach((ref, index) => {
      const entity = response.items[index];
      if (entity) {


 ... (clipped 28 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing access audit: The new drill-down read endpoint does not emit an audit-style log entry capturing who
accessed which metric and the outcome, so it is unclear if critical read access is
reconstructable from logs.

Referred Code
router.get(
  '/metrics/:metricId/catalog/aggregations/entities',
  async (req, res) => {
    const { metricId } = req.params;

    const {
      page,
      pageSize,
      status,
      owner,
      kind,
      entityName,
      sortBy,
      sortOrder,
    } = validateDrillDownMetricsSchema(req.query);

    const { conditions } = await authorizeConditional(
      req,
      scorecardMetricReadPermission,
    );



 ... (clipped 36 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance check up to commit 1ec8035
Security Compliance
Denial of service

Description: The new drill-down endpoint can be abused for resource exhaustion because it allows very
large page values (leading to huge DB OFFSETs) and may build extremely large
authorized-entity scopes (via catalog enumeration) that are then passed downstream,
potentially triggering expensive catalog pagination and DB queries per request.
router.ts [201-277]

Referred Code
const page = Number(req.query.page) || 1;
const pageSize = Math.min(Number(req.query.pageSize) || 5, 100);
const status = req.query.status as
  | 'success'
  | 'warning'
  | 'error'
  | undefined;
const ownedByMe = req.query.ownedByMe === 'true';
const owner = req.query.owner as string | undefined;
const kind = req.query.kind as string | undefined;
const entityName = req.query.entityName as string | undefined;
const sortBy = req.query.sortBy as
  | 'entityName'
  | 'owner'
  | 'entityKind'
  | 'timestamp'
  | 'metricValue'
  | undefined;
const sortOrder = (req.query.sortOrder as 'asc' | 'desc') || 'desc';

const { conditions } = await authorizeConditional(


 ... (clipped 56 lines)
Catalog enumeration DoS

Description: getAuthorizedEntityRefs iterates through all catalog entities readable by the caller using
cursor pagination and collects every ref into memory, which can be exploited by repeated
requests (or large catalogs) to cause high CPU/memory usage and downstream query
amplification.
permissionUtils.ts [56-77]

Referred Code
export async function getAuthorizedEntityRefs(options: {
  catalog: CatalogService;
  credentials: BackstageCredentials;
}): Promise<string[]> {
  const entityRefs: string[] = [];
  let cursor: string | undefined = undefined;

  do {
    const result = await options.catalog.queryEntities(
      {
        fields: ['kind', 'metadata.name', 'metadata.namespace'],
        limit: QUERY_ENTITIES_BATCH_SIZE,
        ...(cursor ? { cursor } : {}),
      },
      { credentials: options.credentials }, // user credentials — enforces conditional policies
    );

    cursor = result.pageInfo.nextCursor;
    entityRefs.push(...result.items.map(e => stringifyEntityRef(e)));
  } while (cursor !== undefined);



 ... (clipped 1 lines)
Expensive DB query

Description: readEntityMetricsByStatus performs whereIn('catalog_entity_ref', catalog_entity_refs) with
a caller-supplied array that could become very large, creating oversized SQL IN clauses
and heavy window-count queries that are susceptible to performance-based denial-of-service
when invoked with large authorized-entity scopes.
DatabaseMetricValues.ts [137-186]

Referred Code
async readEntityMetricsByStatus(
  catalog_entity_refs: string[],
  metric_id: string,
  status?: 'success' | 'warning' | 'error',
  entityKind?: string,
  entityOwner?: string,
  pagination?: { limit: number; offset: number },
): Promise<{ rows: DbMetricValue[]; total: number }> {
  if (catalog_entity_refs.length === 0) {
    return { rows: [], total: 0 };
  }

  const latestIdsSubquery = this.dbClient(this.tableName)
    .max('id')
    .where('metric_id', metric_id)
    .whereIn('catalog_entity_ref', catalog_entity_refs)
    .groupBy('metric_id', 'catalog_entity_ref');

  const query = this.dbClient(this.tableName)
    .select('*')
    .select(this.dbClient.raw('COUNT(*) OVER() as total_count'))


 ... (clipped 29 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- describe: readEntityMetricsByStatus
- readEntityMetricsByStatus
- describe: getAuthorizedEntityRefs
- it: should return entity refs for entities the user is authorized to see
- it: should call queryEntities with user credentials
Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: 🏷️
Missing query validation: The new endpoint casts multiple query parameters (page, sortOrder, status, sortBy, etc.)
without validation/enforcement of allowed values or bounds, enabling invalid/negative
pagination inputs and unexpected parameter values to reach service/database logic.

Referred Code
const page = Number(req.query.page) || 1;
const pageSize = Math.min(Number(req.query.pageSize) || 5, 100);
const status = req.query.status as
  | 'success'
  | 'warning'
  | 'error'
  | undefined;
const ownedByMe = req.query.ownedByMe === 'true';
const owner = req.query.owner as string | undefined;
const kind = req.query.kind as string | undefined;
const entityName = req.query.entityName as string | undefined;
const sortBy = req.query.sortBy as
  | 'entityName'
  | 'owner'
  | 'entityKind'
  | 'timestamp'
  | 'metricValue'
  | undefined;
const sortOrder = (req.query.sortOrder as 'asc' | 'desc') || 'desc';

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: 🏷️
Missing access audit: The new drill-down read endpoint returns entity-level metric values but does not add any
audit logging of who accessed which metric/entity scope and the outcome.

Referred Code
router.get(
  '/metrics/:metricId/catalog/aggregations/entities',
  async (req, res) => {
    const { metricId } = req.params;

    const page = Number(req.query.page) || 1;
    const pageSize = Math.min(Number(req.query.pageSize) || 5, 100);
    const status = req.query.status as
      | 'success'
      | 'warning'
      | 'error'
      | undefined;
    const ownedByMe = req.query.ownedByMe === 'true';
    const owner = req.query.owner as string | undefined;
    const kind = req.query.kind as string | undefined;
    const entityName = req.query.entityName as string | undefined;
    const sortBy = req.query.sortBy as
      | 'entityName'
      | 'owner'
      | 'entityKind'
      | 'timestamp'


 ... (clipped 65 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: 🏷️
Unhandled catalog errors: getAuthorizedEntityRefs performs a paginated catalog query loop without adding contextual
error handling/logging, so catalog failures may surface as generic internal errors without
actionable context.

Referred Code
export async function getAuthorizedEntityRefs(options: {
  catalog: CatalogService;
  credentials: BackstageCredentials;
}): Promise<string[]> {
  const entityRefs: string[] = [];
  let cursor: string | undefined = undefined;

  do {
    const result = await options.catalog.queryEntities(
      {
        fields: ['kind', 'metadata.name', 'metadata.namespace'],
        limit: QUERY_ENTITIES_BATCH_SIZE,
        ...(cursor ? { cursor } : {}),
      },
      { credentials: options.credentials }, // user credentials — enforces conditional policies
    );

    cursor = result.pageInfo.nextCursor;
    entityRefs.push(...result.items.map(e => stringifyEntityRef(e)));
  } while (cursor !== undefined);



 ... (clipped 2 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance check up to commit 8398560
Security Compliance
🔴
Authorization bypass

Description: The new drill-down endpoint returns entity-level metric data for all entities when
ownedByMe is not true (by setting entityRefsToQuery = []) without performing per-entity
authorization checks, enabling users with metric read permission to access metric details
for entities they may not be allowed to read (IDOR / authorization bypass).
router.ts [195-277]

Referred Code
router.get(
  '/metrics/:metricId/catalog/aggregations/entities',
  async (req, res) => {
    const { metricId } = req.params;

    const page = Number(req.query.page) || 1;
    const pageSize = Math.min(Number(req.query.pageSize) || 5, 100);
    const status = req.query.status as
      | 'success'
      | 'warning'
      | 'error'
      | undefined;
    const ownedByMe = req.query.ownedByMe === 'true';
    const owner = req.query.owner as string | undefined;
    const kind = req.query.kind as string | undefined;
    const entityName = req.query.entityName as string | undefined;
    const sortBy = req.query.sortBy as
      | 'entityN...

@rhdh-qodo-merge

rhdh-qodo-merge Bot commented Feb 18, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to bc10a2a

CategorySuggestion                                                                                                                                    Impact
Possible issue
Add indexes for drill-down queries

Add database indexes to the metric_values table to improve the performance of
new drill-down queries that filter and sort on newly added columns.

workspaces/scorecard/plugins/scorecard-backend/migrations/20260217152637_add_entity_metadata_columns.js [17-28]

 exports.up = async function up(knex) {
   await knex.schema.alterTable('metric_values', table => {
     // Add entity_kind column (e.g., "Component", "API", "System")
     table.string('entity_kind', 255).nullable();
 
     // Add entity_owner column (stores the owner entity ref)
     table.string('entity_owner', 255).nullable();
 
     // Add entity_namespace column
     table.string('entity_namespace', 255).nullable();
+
+    // Indexes to support drill-down filters/sorting (best-effort, cross-db)
+    table.index(['metric_id', 'catalog_entity_ref'], 'mv_metric_entity_idx');
+    table.index(['metric_id', 'status'], 'mv_metric_status_idx');
+    table.index(['metric_id', 'entity_owner'], 'mv_metric_owner_idx');
+    table.index(['metric_id', 'entity_kind'], 'mv_metric_kind_idx');
+    table.index(['metric_id', 'entity_namespace'], 'mv_metric_namespace_idx');
+    table.index(['metric_id', 'timestamp'], 'mv_metric_timestamp_idx');
   });
 };
 
+exports.down = async function down(knex) {
+  await knex.schema.alterTable('metric_values', table => {
+    table.dropIndex(['metric_id', 'catalog_entity_ref'], 'mv_metric_entity_idx');
+    table.dropIndex(['metric_id', 'status'], 'mv_metric_status_idx');
+    table.dropIndex(['metric_id', 'entity_owner'], 'mv_metric_owner_idx');
+    table.dropIndex(['metric_id', 'entity_kind'], 'mv_metric_kind_idx');
+    table.dropIndex(['metric_id', 'entity_namespace'], 'mv_metric_namespace_idx');
+    table.dropIndex(['metric_id', 'timestamp'], 'mv_metric_timestamp_idx');
+
+    // Drop columns
+    table.dropColumn('entity_kind');
+    table.dropColumn('entity_owner');
+    table.dropColumn('entity_namespace');
+  });
+};
+
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical performance issue where the new drill-down queries would cause full table scans, and proposes adding appropriate database indexes to prevent this.

High
Prevent silent owner truncation

Modify the normalizeOwner function to avoid silently truncating long owner refs;
instead, log a warning and return undefined to prevent data integrity issues.

workspaces/scorecard/plugins/scorecard-backend/src/scheduler/tasks/PullMetricsByProviderTask.ts [206-213]

-function normalizeOwner(owner: unknown): string | undefined {
+function normalizeOwner(
+  owner: unknown,
+  logger?: LoggerService,
+): string | undefined {
   if (typeof owner !== 'string') return undefined;
+
   const normalized = owner.trim().toLowerCase();
   if (!normalized) return undefined;
 
-  // Prevent DB insertion failures (metric_values.entity_owner is VARCHAR(255))
-  return normalized.length <= 255 ? normalized : normalized.slice(0, 255);
+  if (normalized.length > 255) {
+    logger?.warn(
+      'Entity owner ref exceeds 255 characters; omitting entity_owner',
+      { ownerLength: normalized.length },
+    );
+    return undefined;
+  }
+
+  return normalized;
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that silently truncating data can lead to data integrity issues and proposes a much safer approach of logging a warning and omitting the value.

Medium
  • Update

Previous suggestions

Suggestions up to commit 64777b9
CategorySuggestion                                                                                                                                    Impact
Possible issue
Select latest rows by timestamp

To ensure the latest metric value is always selected, modify the database query
to order by timestamp and then id, instead of relying solely on MAX(id). This
prevents returning stale data in cases of non-chronological inserts.

workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts [155-162]

-const latestIdsSubquery = this.dbClient(this.tableName)
-  .max('id')
-  .where('metric_id', metric_id)
-  .groupBy('catalog_entity_ref');
+const clientName: string =
+  (this.dbClient as any).client?.config?.client ?? '';
+const isPostgres = clientName === 'pg' || clientName.includes('postgres');
 
-const query = this.dbClient(this.tableName)
-  .select('*')
-  .whereIn('id', latestIdsSubquery);
+const query = isPostgres
+  ? this.dbClient
+      .select('*')
+      .from(
+        this.dbClient(this.tableName)
+          .select(
+            '*',
+            this.dbClient.raw(
+              `ROW_NUMBER() OVER (
+                 PARTITION BY catalog_entity_ref
+                 ORDER BY timestamp DESC, id DESC
+               ) AS rn`,
+            ),
+          )
+          .where('metric_id', metric_id)
+          .as('mv'),
+      )
+      .where('rn', 1)
+  : this.dbClient(this.tableName)
+      .select('*')
+      .where('metric_id', metric_id)
+      .whereNotExists(function notExistsNewer() {
+        this.select(1)
+          .from({ mv2: 'metric_values' })
+          .whereRaw('mv2.metric_id = metric_values.metric_id')
+          .whereRaw('mv2.catalog_entity_ref = metric_values.catalog_entity_ref')
+          .andWhere(qb =>
+            qb
+              .whereRaw('mv2.timestamp > metric_values.timestamp')
+              .orWhere(qb2 =>
+                qb2
+                  .whereRaw('mv2.timestamp = metric_values.timestamp')
+                  .andWhereRaw('mv2.id > metric_values.id'),
+              ),
+          );
+      });
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical flaw where using MAX(id) to find the latest metric can return stale data if records are inserted non-chronologically. The proposed fix using a window function for Postgres and a correlated subquery for SQLite is robust and ensures the truly latest record by timestamp is selected, which is a major correctness improvement.

High
Prevent SQL cast sort failures

To prevent SQL errors when sorting by metricValue, update the orderByRaw clause
to use a CASE statement. This will safely cast only numeric values for sorting
and treat non-numeric values as NULL, avoiding runtime exceptions for boolean or
other non-numeric metric types.

workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts [177-192]

 if (options.sortBy === 'metricValue') {
   if (isPostgres) {
-    // value is JSON; cast to text then to float for numeric sort; NULLS LAST is native syntax
     query.orderByRaw(
-      `CAST(value::text AS DOUBLE PRECISION) ${direction} NULLS LAST, id ASC`,
+      `
+        CASE
+          WHEN (value::text ~ '^-?\\d+(\\.\\d+)?$') THEN CAST(value::text AS DOUBLE PRECISION)
+          ELSE NULL
+        END ${direction} NULLS LAST,
+        id ASC
+      `,
     );
   } else {
-    // SQLite: "value IS NULL" puts nulls last; double-cast handles JSON-stored values
     query.orderByRaw(
-      `value IS NULL, CAST(CAST(value AS TEXT) AS REAL) ${direction}, id ASC`,
+      `value IS NULL,
+       CASE
+         WHEN CAST(value AS TEXT) GLOB '-[0-9]*' OR CAST(value AS TEXT) GLOB '[0-9]*'
+           OR CAST(value AS TEXT) GLOB '-[0-9]*.[0-9]*' OR CAST(value AS TEXT) GLOB '[0-9]*.[0-9]*'
+         THEN CAST(CAST(value AS TEXT) AS REAL)
+         ELSE NULL
+       END ${direction},
+       id ASC`,
     );
   }
 } else {
   query.orderBy(column, direction);
-  query.orderBy('id', 'asc'); // Ensure a stable sort in the event that two metrics share a similar primary sort value
+  query.orderBy('id', 'asc');
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out that casting the value column for sorting can cause SQL errors if the metric is boolean or non-numeric. The proposed solution using a CASE statement to safely cast only numeric-like values is a robust fix that prevents potential runtime errors and improves the reliability of the drill-down endpoint.

Medium
Return correct metric by id

Improve the getMetric mock implementation to correctly find and return a metric
by its metricId from the metricsList or provider. This will make tests more
accurate by ensuring the correct metric metadata is used, preventing potential
false positives.

workspaces/scorecard/plugins/scorecard-backend/fixtures/mockMetricProvidersRegistry.ts [52-56]

-const getMetric = provider
-  ? jest.fn().mockImplementation((_metricId: string) => {
-      return provider.getMetric(); // Returns the metric from the provider
-    })
-  : jest.fn();
+const getMetric =
+  provider || metricsList
+    ? jest.fn().mockImplementation((metricId: string) => {
+        if (metricsList) {
+          const found = metricsList.find(m => m.id === metricId);
+          if (found) return found;
+        }
 
+        const pMetric = provider?.getMetric();
+        if (pMetric && pMetric.id === metricId) return pMetric;
+
+        throw new Error(`Metric not found: ${metricId}`);
+      })
+    : jest.fn();
+
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the mock getMetric implementation is too simplistic and could lead to incorrect test behavior. The proposed improvement makes the mock more realistic by searching for the metric by metricId in the provided lists, which enhances test accuracy and reliability.

Medium
Suggestions up to commit 338421c
CategorySuggestion                                                                                                                                    Impact
Possible issue
Add safe response fallbacks

Add safe fallbacks for entityName, entityKind, and owner in the drill-down
response to prevent returning undefined values and ensure consistency with the
API contract.

workspaces/scorecard/plugins/scorecard-backend/src/service/CatalogMetricService.ts [346-354]

+const [kindPart, rest] = row.catalog_entity_ref.split(':', 2);
+const nameFromRef = rest?.includes('/') ? rest.split('/')[1] : row.catalog_entity_ref;
+
 enrichedEntities.push({
   entityRef: row.catalog_entity_ref,
-  entityName: entity.metadata?.name,
-  entityKind: entity.kind,
-  owner: entity.spec?.owner as string,
+  entityName: entity.metadata?.name ?? nameFromRef,
+  entityKind: entity.kind ?? row.entity_kind ?? kindPart ?? 'Unknown',
+  owner:
+    (typeof entity.spec?.owner === 'string' && entity.spec.owner) ||
+    row.entity_owner ||
+    'unknown',
   metricValue: row.value,
   timestamp: new Date(row.timestamp).toISOString(),
   status: row.status ?? 'error',
 });
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out a potential API contract violation where entityName, entityKind, and owner could be undefined, while the API documentation in report.api.md defines them as required. The proposed change adds robust fallbacks, using data from the entity ref or the database cache, ensuring the API response is always consistent and preventing potential frontend errors.

Medium
Prevent invalid owner persistence

Improve the normalizeOwner function to only process string values, and add
trimming and truncation to prevent database insertion errors from oversized or
invalid data.

workspaces/scorecard/plugins/scorecard-backend/src/scheduler/tasks/PullMetricsByProviderTask.ts [204-208]

 function normalizeOwner(owner: unknown): string | undefined {
-  if (!owner) return undefined;
-  if (typeof owner === 'string') return owner.toLowerCase();
-  return JSON.stringify(owner).toLowerCase();
+  if (typeof owner !== 'string') return undefined;
+
+  const normalized = owner.trim().toLowerCase();
+  if (!normalized) return undefined;
+
+  // Prevent DB insertion failures (metric_values.entity_owner is VARCHAR(255))
+  return normalized.length <= 255 ? normalized : normalized.slice(0, 255);
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that JSON.stringify on an arbitrary owner object can cause database insertion failures if the resulting string exceeds the 255-character limit of the entity_owner column. The proposed change makes the data normalization more robust by only processing strings and truncating them to fit the schema, preventing potential runtime errors.

Medium
Suggestions up to commit 8dec549
CategorySuggestion                                                                                                                                    Impact
Possible issue
Make metric sorting type-safe

Improve the sorting logic for metricValue to be database-agnostic and handle
boolean values correctly by using a CASE statement instead of casting to REAL.

workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts [174-181]

-// Nulls last for metricValue (value can be null)
+// Nulls last for metricValue (value can be null and can be boolean)
 if (options.sortBy === 'metricValue') {
   query.orderByRaw(
-    `value IS NULL, CAST(CAST(value AS TEXT) AS REAL) ${direction}`,
+    `
+    CASE WHEN value IS NULL THEN 1 ELSE 0 END ASC,
+    CASE
+      WHEN value = 'true' THEN 1
+      WHEN value = 'false' THEN 0
+      ELSE value
+    END ${direction}
+    `.trim(),
   );
 } else {
   query.orderBy(column, direction);
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that casting the value column to REAL is not robust, as the column can contain booleans, which could lead to sorting errors or incorrect behavior across different databases. The proposed CASE statement is a more portable and type-safe solution for sorting mixed-type data.

Medium
Select latest rows by timestamp

To ensure the latest metric data is always selected, modify the query to select
rows based on the maximum timestamp for each entity, rather than relying on the
maximum id.

workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts [151-160]

-const latestIdsSubquery = this.dbClient(this.tableName)
-  .max('id')
+const latestPerEntity = this.dbClient(this.tableName)
+  .select('catalog_entity_ref')
+  .max({ max_timestamp: 'timestamp' })
   .where('metric_id', metric_id)
-  .groupBy('metric_id', 'catalog_entity_ref');
+  .groupBy('catalog_entity_ref')
+  .as('latest_per_entity');
 
 const query = this.dbClient(this.tableName)
-  .select('*')
+  .select(`${this.tableName}.*`)
   .select(this.dbClient.raw('COUNT(*) OVER() as total_count'))
-  .whereIn('id', latestIdsSubquery)
-  .where('metric_id', metric_id);
+  .join(latestPerEntity, join => {
+    join
+      .on(`${this.tableName}.catalog_entity_ref`, '=', 'latest_per_entity.catalog_entity_ref')
+      .andOn(`${this.tableName}.timestamp`, '=', 'latest_per_entity.max_timestamp');
+  })
+  .where(`${this.tableName}.metric_id`, metric_id);
Suggestion importance[1-10]: 4

__

Why: The suggestion correctly identifies that using MAX(id) is not a reliable way to get the latest metric value if data can be inserted out of order. However, the proposed improved_code is flawed as it can return duplicate rows if multiple entries share the same latest timestamp for an entity, which would be a new bug.

Low
✅ Suggestions up to commit 8398560
CategorySuggestion                                                                                                                                    Impact
High-level
Denormalization introduces data synchronization challenges

The denormalization of entity_kind and entity_owner in the metric_values table
will cause stale data issues when entity metadata changes. Implement a
synchronization mechanism, like a catalog processor, to update historical metric
records and maintain data consistency.

Examples:

workspaces/scorecard/plugins/scorecard-backend/migrations/20260217152637_add_entity_metadata_columns.js [18-28]
  await knex.schema.alterTable('metric_values', table => {
    // Add entity_kind column (e.g., "Component", "API", "System")
    table.string('entity_kind', 255).nullable();

    // Add entity_owner column (stores the owner entity ref)
    table.string('entity_owner', 255).nullable();

    // Optional: Add index for better filtering performance
    table.index(['entity_kind'], 'idx_metric_values_entity_kind');
    table.index(['entity_owner'], 'idx_metric_values_entity_owner');

 ... (clipped 1 lines)
workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts [175-182]
    if (entityKind) {
      query.whereRaw('LOWER(entity_kind) = LOWER(?)', [entityKind]);
    }

    // Filter by entity_owner
    if (entityOwner) {
      query.whereRaw('LOWER(entity_owner) = LOWER(?)', [entityOwner]);
    }

Solution Walkthrough:

Before:

// In PullMetricsByProviderTask.ts
// When a new metric is pulled, current metadata is stored.
const metricValue = {
  // ... other fields
  entity_kind: entity.kind,
  entity_owner: entity.spec.owner,
};
database.createMetricValues([metricValue]);

// In DatabaseMetricValues.ts
// When filtering, the query uses the stored (potentially stale) data.
function readEntityMetricsByStatus(..., entityOwner, entityKind) {
  query.where('entity_owner', entityOwner); // Uses stale data
  query.where('entity_kind', entityKind);   // Uses stale data
  // ...
}
// No mechanism exists to update old metric_values records
// if entity.kind or entity.owner changes in the catalog.

After:

// Suggestion: Add a new mechanism, e.g., a Catalog Processor.

class MetricValueUpdaterProcessor implements CatalogProcessor {
  // ... constructor with database connection

  async postProcessEntity(entity, location) {
    const owner = entity.spec?.owner;
    const kind = entity.kind;
    const entityRef = stringifyEntityRef(entity);

    // Check if the stored metadata for this entity is outdated.
    const isOutdated = await this.database.isMetadataOutdated(entityRef, kind, owner);

    // If metadata has changed, update all historical records for this entity.
    if (isOutdated) {
       await this.database.updateHistoricalEntityMetadata(entityRef, kind, owner);
    }
    return entity;
  }
}
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical data consistency issue where stale entity_kind and entity_owner data will lead to incorrect filtering results, undermining the reliability of the new drill-down feature.

High
Possible issue
Fix dropIndex column args
Suggestion Impact:Instead of adding the missing column args to dropIndex, the commit removed the index creation in the up migration and removed the dropIndex calls entirely from the down migration, avoiding the failing rollback behavior.

code diff:

-    // Optional: Add index for better filtering performance
-    table.index(['entity_kind'], 'idx_metric_values_entity_kind');
-    table.index(['entity_owner'], 'idx_metric_values_entity_owner');
   });
 };
 
 exports.down = async function down(knex) {
   await knex.schema.alterTable('metric_values', table => {
-    // Drop indexes first
-    table.dropIndex([], 'idx_metric_values_entity_kind');
-    table.dropIndex([], 'idx_metric_values_entity_owner');
-

Fix the down migration script by providing the column names (['entity_kind'] and
['entity_owner']) to the table.dropIndex calls, which is required for them to
function correctly.

workspaces/scorecard/plugins/scorecard-backend/migrations/20260217152637_add_entity_metadata_columns.js [31-41]

 exports.down = async function down(knex) {
   await knex.schema.alterTable('metric_values', table => {
     // Drop indexes first
-    table.dropIndex([], 'idx_metric_values_entity_kind');
-    table.dropIndex([], 'idx_metric_values_entity_owner');
+    table.dropIndex(['entity_kind'], 'idx_metric_values_entity_kind');
+    table.dropIndex(['entity_owner'], 'idx_metric_values_entity_owner');
 
     // Drop columns
     table.dropColumn('entity_kind');
     table.dropColumn('entity_owner');
   });
 };

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: This suggestion fixes a critical bug in the database migration's down function. The dropIndex calls were missing the required column names, which would cause the rollback to fail. This is an important correctness fix.

High
Fix undefined value in catch

Fix a ReferenceError in the catch block by explicitly setting value: null in the
returned error object, as the value variable is out of scope.

workspaces/scorecard/plugins/scorecard-backend/src/scheduler/tasks/PullMetricsByProviderTask.ts [167-178]

 } catch (error) {
   return {
     catalog_entity_ref: stringifyEntityRef(entity),
     metric_id: this.providerId,
-    value,
+    value: null,
     timestamp: new Date(),
     error_message:
       error instanceof Error ? error.message : String(error),
     entity_kind: entity.kind,
     entity_owner: normalizeOwner(entity?.spec?.owner),
   } as DbMetricValueCreate;
 }
Suggestion importance[1-10]: 9

__

Why: This suggestion identifies a critical ReferenceError bug where the value variable is used in a catch block where it is not defined. The fix is correct and essential for preventing the metric-pulling task from crashing.

High
Prevent storing invalid owner data

Update the normalizeOwner function to only process string values for owners and
return undefined for other types, preventing the storage of non-filterable
JSON-stringified owner data.

workspaces/scorecard/plugins/scorecard-backend/src/scheduler/tasks/PullMetricsByProviderTask.ts [204-208]

 function normalizeOwner(owner: unknown): string | undefined {
-  if (!owner) return undefined;
-  if (typeof owner === 'string') return owner;
-  return JSON.stringify(owner);
+  if (typeof owner === 'string') {
+    return owner;
+  }
+  return undefined;
 }
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies that storing stringified JSON for an entity owner will break the filtering logic. The proposed fix ensures only valid string owners are stored, which is critical for the correctness of the new drill-down feature.

Medium
General
Safe‐guard null status mapping

Replace the non-null assertion (!) on row.status with a nullish coalescing
operator (?? 'error') to safely handle cases where the status might be null and
prevent potential runtime errors.

workspaces/scorecard/plugins/scorecard-backend/src/service/CatalogMetricService.ts [272-284]

 const enrichedEntities: EntityMetricDetail[] = rows.map(row => {
   const entity = entityMap.get(row.catalog_entity_ref);
 
   return {
     entityRef: row.catalog_entity_ref,
     entityName: entity?.metadata?.name ?? 'Unknown',
     entityKind: entity?.kind ?? row.entity_kind ?? 'Unknown',
     owner: (entity?.spec?.owner as string) ?? row.entity_owner ?? 'Unknown',
     metricValue: row.value,
     timestamp: new Date(row.timestamp).toISOString(),
-    status: row.status!,
+    status: row.status ?? 'error',
   };
 });
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies the unsafe use of a non-null assertion on row.status. Replacing it with a nullish coalescing operator (??) provides a safe fallback and prevents potential runtime errors, improving code robustness.

Medium
Sort nulls always last

Modify the sorting logic for metricValue to use Infinity for ascending and
-Infinity for descending order when the value is null, ensuring null values are
always sorted to the end.

workspaces/scorecard/plugins/scorecard-backend/src/service/CatalogMetricService.ts [318-322]

 case 'metricValue':
   // Handle null values - sort them to the end
-  aValue = a.metricValue ?? -Infinity;
-  bValue = b.metricValue ?? -Infinity;
+  aValue = a.metricValue ?? (options.sortOrder === 'asc' ? Infinity : -Infinity);
+  bValue = b.metricValue ?? (options.sortOrder === 'asc' ? Infinity : -Infinity);
   break;
Suggestion importance[1-10]: 7

__

Why: This suggestion provides an elegant and correct fix for sorting null metric values. The proposed change ensures nulls are consistently placed at the end of the sorted list for both ascending and descending orders, which fixes a bug in the original implementation.

Medium
Validate pagination query parameters

Add validation to ensure page and pageSize query parameters are positive
integers to prevent invalid pagination values.

workspaces/scorecard/plugins/scorecard-backend/src/service/router.ts [200-201]

-const page = Number(req.query.page) || 1;
-const pageSize = Math.min(Number(req.query.pageSize) || 5, 100);
+const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
+const pageSize = Math.min(
+  Math.max(1, Math.floor(Number(req.query.pageSize) || 5)),
+  100,
+);
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a lack of input validation for pagination parameters and proposes a robust fix using Math.floor and Math.max to prevent invalid values, improving the endpoint's reliability.

Low
Improve handling of missing entities

Improve the handling of missing catalog entities by changing the fallback
entityName from Unknown to [Deleted Entity] to more clearly indicate that the
entity no longer exists in the catalog.

workspaces/scorecard/plugins/scorecard-backend/src/service/CatalogMetricService.ts [278-279]

+entityName: entity?.metadata?.name ?? '[Deleted Entity]',
 entityKind: entity?.kind ?? row.entity_kind ?? 'Unknown',
 owner: (entity?.spec?.owner as string) ?? row.entity_owner ?? 'Unknown',
Suggestion importance[1-10]: 5

__

Why: The suggestion improves the user experience by making it clearer when an entity has been deleted from the catalog, changing the fallback name from Unknown to [Deleted Entity]. This is a useful but minor enhancement.

Low

@Eswaraiahsapram Eswaraiahsapram left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @PatAKnight . I tested the endpoints, and they look good to me. The response also matches the Figma design.

@dzemanov / @imykhno , Could you please review this and share your +1 so we can proceed with the merge? Once this is merged, I can move forward with the frontend work and skip the dummy response implementation on the FE side.

/lgtm

endpoint - http://localhost:7007/api/scorecard/metrics/jira.open_issues/catalog/aggregations/entities?page=2&pageSize=1

Response
{
  "metricId": "jira.open_issues",
  "metricMetadata": {
    "title": "Jira open blocking tickets",
    "description": "Highlights the number of issues that are currently open in Jira.",
    "type": "number"
  },
  "entities": [
    {
      "entityRef": "component:default/all-scorecards-service-different-owner",
      "entityName": "all-scorecards-service-different-owner",
      "entityKind": "Component",
      "owner": "group:default/red-hat",
      "metricValue": 0,
      "timestamp": "2026-02-24T13:02:43.957Z",
      "status": "success"
    }
  ],
  "pagination": {
    "page": 2,
    "pageSize": 1,
    "total": 3,
    "totalPages": 3
  }
}

@dzemanov dzemanov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks great @PatAKnight, only have a couple of comments.

@PatAKnight
PatAKnight force-pushed the scorecard-aggregated-drill-down branch from a14730f to 338421c Compare March 2, 2026 05:51
@openshift-ci openshift-ci Bot removed the lgtm label Mar 2, 2026
@PatAKnight
PatAKnight force-pushed the scorecard-aggregated-drill-down branch from 338421c to 64777b9 Compare March 2, 2026 06:03
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
…g fetch

Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
…ser credentials during catalog fetch

Signed-off-by: Patrick Knight <pknight@redhat.com>
…a param

Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
Signed-off-by: Patrick Knight <pknight@redhat.com>
@PatAKnight
PatAKnight force-pushed the scorecard-aggregated-drill-down branch from 5a598cc to 539e3f1 Compare March 5, 2026 13:19

@dzemanov dzemanov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you so much!
/lgtm
/approve

@imykhno imykhno left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you!

@openshift-ci openshift-ci Bot removed the lgtm label Mar 6, 2026
@sonarqubecloud

sonarqubecloud Bot commented Mar 6, 2026

Copy link
Copy Markdown

@Eswaraiahsapram Eswaraiahsapram left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @PatAKnight , 🎉

/lgtm

@openshift-ci openshift-ci Bot added the lgtm label Mar 6, 2026
@Eswaraiahsapram
Eswaraiahsapram merged commit c83b206 into redhat-developer:main Mar 6, 2026
9 checks passed
rohitratannagar pushed a commit to rohitratannagar/rhdh-plugins that referenced this pull request Mar 12, 2026
…ard data (redhat-developer#2350)

* feat(scorecard): add the ability to drill down into aggregated scorecard data

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): add tests for drill down functionality

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): add docs for drill down functionality

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): normalize owners returned from catalog

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): ensure kind and owner are case insensitive

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): add changeset

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): remove indexes from migration

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): update report.api.md

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): ensure we do not bypass catalog conditional permissions

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): ensure we utilize users credentials for batch catalog fetch

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): add input validation to the new endpoint

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): remove getAuthorizedEntityRefs as we are utilizing user credentials during catalog fetch

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): remove ownedByMe in favor of passing owner array as a param

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): move entityName filtering to the database

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): move sorting down to the database layer

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): address sonarqube findings

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): logger full error but throw generic error

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): ensure we have accurate totals when filtering entities

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): handle some review suggestions

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): add ability to filter and sort by namespace

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): handle review comments

Signed-off-by: Patrick Knight <pknight@redhat.com>

* feat(scorecard): handle more review comments

Signed-off-by: Patrick Knight <pknight@redhat.com>

---------

Signed-off-by: Patrick Knight <pknight@redhat.com>
Co-authored-by: Eswaraiah Sapram <esapram@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants