Skip to content

Comments

feat(scorecard): add dependabot backend module#2351

Open
alizard0 wants to merge 1 commit intomainfrom
RHIDP-12269
Open

feat(scorecard): add dependabot backend module#2351
alizard0 wants to merge 1 commit intomainfrom
RHIDP-12269

Conversation

@alizard0
Copy link
Member

@alizard0 alizard0 commented Feb 18, 2026

User description

Hey, I just made a Pull Request!

✔️ 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)

PR Type

Enhancement


Description

  • Add new Dependabot backend module for scorecard plugin

  • Implement DependabotClient to fetch alerts via GitHub GraphQL API

  • Create DependabotMetricProvider to calculate severity-based scores

  • Register module in backend and add comprehensive tests


Diagram Walkthrough

flowchart LR
  A["Backend Index"] -->|registers| B["Dependabot Module"]
  B -->|initializes| C["DependabotMetricProvider"]
  C -->|uses| D["DependabotClient"]
  D -->|queries| E["GitHub GraphQL API"]
  E -->|returns| F["Vulnerability Alerts"]
  F -->|scored| G["Metric 0-9"]
Loading

File Walkthrough

Relevant files
Configuration changes
3 files
index.ts
Register Dependabot backend module                                             
+5/-0     
DependabotConfig.ts
Define Dependabot metric configuration and thresholds       
+32/-0   
.eslintrc.js
Configure ESLint for module                                                           
+1/-0     
Enhancement
4 files
module.ts
Create Dependabot backend module definition                           
+38/-0   
index.ts
Export module entry point                                                               
+23/-0   
DependabotClient.ts
Implement GitHub GraphQL client for Dependabot                     
+104/-0 
DependabotMetricProvider.ts
Implement metric provider with severity scoring                   
+123/-0 
Tests
1 files
DependabotClient.test.ts
Add comprehensive tests for DependabotClient                         
+103/-0 
Dependencies
2 files
package.json
Add package configuration for Dependabot module                   
+44/-0   
package.json
Add Dependabot module dependency                                                 
+1/-0     
Documentation
2 files
README.md
Document Dependabot module installation and usage               
+39/-0   
dependabot-scorecard-only.yaml
Add example entity with Dependabot annotation                       
+13/-0   

@rhdh-gh-app
Copy link

rhdh-gh-app bot commented Feb 18, 2026

Changed Packages

Package Name Package Path Changeset Bump Current Version
backend workspaces/scorecard/packages/backend none v0.0.0
@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-dependabot workspaces/scorecard/plugins/scorecard-backend-module-dependabot none v0.1.0

@rhdh-qodo-merge
Copy link

PR Compliance Guide 🔍

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
🔴
beforeEach Component

Description:
This beforeEach block (clear mocks,
jest.unstable_mockModule('@octokit/graphql'...), build ConfigReader with a
github.com token, then construct the client) is structurally identical to the existing
GithubClient tests, only differing by instantiating DependabotClient instead of
GithubClient. Move this repeated setup into a shared test utility and parameterize the
constructed client type.

PR Code:
DependabotClient.test.ts [34-55]

beforeEach(() => {
  jest.clearAllMocks();

  // @ts-ignore
  jest.unstable_mockModule('@octokit/graphql', async () => ({
    graphql: {
      defaults: () => mockedGraphqlClient,
    },
  }));

  const mockConfig = new ConfigReader({
    integrations: {
      github: [
        {
          host: 'github.com',
          token: 'dummy-token',
        },
      ],
    },
  });
  dependabotClient = new DependabotClient(mockConfig);


 ... (clipped 1 lines)

Codebase Context Code:
redhat-developer/rhdh-plugins/workspaces/scorecard/plugins/scorecard-backend-module-github/src/github/GitHubClient.test.ts [22-96]

describe('GithubClient', () => {
 let githubClient: GithubClient;
 const mockedGraphqlClient = jest.fn();
 const repository: GithubRepository = {
   owner: 'owner',
   repo: 'repo',
 };

 const getCredentialsSpy = jest
   .spyOn(DefaultGithubCredentialsProvider.prototype, 'getCredentials')
   .mockResolvedValue({
     type: 'token',
     headers: { Authorization: 'Bearer dummy-token' },
     token: 'dummy-token',
   });

 beforeEach(() => {
   jest.clearAllMocks();

   // @ts-ignore


... (clipped 55 lines)
it: should throw error when GitHub integration for URL is missing Component

Description:
The error test asserting rejection when the GitHub integration for the provided unknownUrl
is missing is the same structure and assertion message as in existing GithubClient tests.
Reuse a shared test case/helper (e.g., itThrowsMissingIntegration(clientCall)) to avoid
duplicating this identical behavior check across clients.

PR Code:
DependabotClient.test.ts [96-101]

it('should throw error when GitHub integration for URL is missing', async () => {
  const unknownUrl = 'https://unknown-host/owner/repo';
  await expect(
    dependabotClient.getDependabotAlerts(unknownUrl, repository),
  ).rejects.toThrow(`Missing GitHub integration for '${unknownUrl}'`);
});

Codebase Context Code:
redhat-developer/rhdh-plugins/workspaces/scorecard/plugins/scorecard-backend-module-github/src/github/GitHubClient.test.ts [22-96]

describe('GithubClient', () => {
 let githubClient: GithubClient;
 const mockedGraphqlClient = jest.fn();
 const repository: GithubRepository = {
   owner: 'owner',
   repo: 'repo',
 };

 const getCredentialsSpy = jest
   .spyOn(DefaultGithubCredentialsProvider.prototype, 'getCredentials')
   .mockResolvedValue({
     type: 'token',
     headers: { Authorization: 'Bearer dummy-token' },
     token: 'dummy-token',
   });

 beforeEach(() => {
   jest.clearAllMocks();

   // @ts-ignore


... (clipped 55 lines)
describe: DependabotClient Component

Description:
The Jest test harness setup (creating mockedGraphqlClient, spying on
DefaultGithubCredentialsProvider.prototype.getCredentials, and starting a beforeEach that
clears mocks and stubs @octokit/graphql defaults) mirrors the existing GithubClient test
suite structure. Consider extracting a shared test helper (e.g.,
setupMockGraphqlClientWithGithubCredentials()) used by both DependabotClient and
GithubClient tests to avoid repeating the same mocking boilerplate.

PR Code:
DependabotClient.test.ts [21-103]

describe('DependabotClient', () => {
  let dependabotClient: DependabotClient;
  const mockedGraphqlClient = jest.fn();
  const repository = { owner: 'owner', repo: 'repo' };

  const getCredentialsSpy = jest
    .spyOn(DefaultGithubCredentialsProvider.prototype, 'getCredentials')
    .mockResolvedValue({
      type: 'token',
      headers: { Authorization: 'Bearer dummy-token' },
      token: 'dummy-token',
    });

  beforeEach(() => {
    jest.clearAllMocks();

    // @ts-ignore
    jest.unstable_mockModule('@octokit/graphql', async () => ({
      graphql: {
        defaults: () => mockedGraphqlClient,
      },


 ... (clipped 62 lines)

Codebase Context Code:
redhat-developer/rhdh-plugins/workspaces/scorecard/plugins/scorecard-backend-module-github/src/github/GitHubClient.test.ts [22-96]

describe('GithubClient', () => {
 let githubClient: GithubClient;
 const mockedGraphqlClient = jest.fn();
 const repository: GithubRepository = {
   owner: 'owner',
   repo: 'repo',
 };

 const getCredentialsSpy = jest
   .spyOn(DefaultGithubCredentialsProvider.prototype, 'getCredentials')
   .mockResolvedValue({
     type: 'token',
     headers: { Authorization: 'Bearer dummy-token' },
     token: 'dummy-token',
   });

 beforeEach(() => {
   jest.clearAllMocks();

   // @ts-ignore


... (clipped 55 lines)
describe: getDependabotAlerts Component

Description:
The test pattern “prepare url, build a GraphQL-shaped response,
mockedGraphqlClient.mockResolvedValue(response), call client method with url + repository
matches the existing GithubClient#getOpenPullRequestsCount test flow. You could factor out
a helper like mockGraphqlResponseAndCall(clientCall) (or a reusable “givenGraphqlResponse”
fixture) to reduce repeated scaffolding across GraphQL client tests.

PR Code:
DependabotClient.test.ts [57-102]

describe('getDependabotAlerts', () => {
  it('should return the list of alerts', async () => {
    const url = 'https://github.com/owner/repo';
    const response = {
      repository: {
        vulnerabilityAlerts: {
          nodes: [
            {
              number: 1,
              state: 'OPEN',
              createdAt: '2021-01-01',
              securityAdvisory: { severity: 'HIGH' },
            },
          ],
        },
      },
    };
    mockedGraphqlClient.mockResolvedValue(response);

    const result = await dependabotClient.getDependabotAlerts(
      url,


 ... (clipped 25 lines)

Codebase Context Code:
redhat-developer/rhdh-plugins/workspaces/scorecard/plugins/scorecard-backend-module-github/src/github/GitHubClient.test.ts [22-96]

describe('GithubClient', () => {
 let githubClient: GithubClient;
 const mockedGraphqlClient = jest.fn();
 const repository: GithubRepository = {
   owner: 'owner',
   repo: 'repo',
 };

 const getCredentialsSpy = jest
   .spyOn(DefaultGithubCredentialsProvider.prototype, 'getCredentials')
   .mockResolvedValue({
     type: 'token',
     headers: { Authorization: 'Bearer dummy-token' },
     token: 'dummy-token',
   });

 beforeEach(() => {
   jest.clearAllMocks();

   // @ts-ignore


... (clipped 55 lines)
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:
Constructor misuse: The provider has clear runtime/compile-time breakages (missing Config import and
createDependabotMetricProvider calling new DependabotMetricProvider(DEPENDABOT_THRESHOLDS)
with the wrong argument type), preventing graceful handling of any edge cases because the
module may not initialize at all.

Referred Code
constructor(config: Config, thresholds?: ThresholdConfig) {
  this.dependabotClient = new DependabotClient(config);
  this.thresholds = thresholds ?? DEPENDABOT_THRESHOLDS;
}

getProviderDatasourceId(): string {
  return 'dependabot';
}

getProviderId(): string {
  return 'dependabot.alerts';
}

getMetricType(): 'number' {
  return 'number';
}

getMetric(): Metric<'number'> {
  return {
    id: this.getProviderId(),
    title: 'Dependabot Alerts',


 ... (clipped 64 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:
No audit logging: The new module performs external GitHub API reads for vulnerability alerts but the diff
shows no audit/event logging that would enable reconstructing who/what triggered these
reads and their outcome.

Referred Code
async getDependabotAlerts(
  url: string,
  repository: { owner: string; repo: string },
): Promise<DependabotAlert[]> {
  const octokit = await this.getOctokitClient(url);

  const query = `
    query getDependabotAlerts($owner: String!, $repo: String!) {
      repository(owner: $owner, name: $repo) {
        vulnerabilityAlerts(first: 300, states: [OPEN]) {
          nodes {
            number
            state
            createdAt
            securityAdvisory {
              severity
            }
          }
        }
      }
    }


 ... (clipped 27 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:
Potential info leakage: Errors thrown for missing/invalid annotations include full entity references and raw
annotation values which may be surfaced to callers depending on upstream handling,
potentially revealing internal catalog details in user-facing responses.

Referred Code
getRepository(entity: Entity): { owner: string; repo: string } {
  const projectSlug =
    entity.metadata.annotations?.[GITHUB_PROJECT_ANNOTATION];
  if (!projectSlug) {
    throw new Error(
      `Missing annotation '${GITHUB_PROJECT_ANNOTATION}' for entity ${stringifyEntityRef(
        entity,
      )}`,
    );
  }

  const [owner, repo] = projectSlug.split('/');
  if (!owner || !repo) {
    throw new Error(
      `Invalid format of '${GITHUB_PROJECT_ANNOTATION}' ${projectSlug} for entity ${stringifyEntityRef(
        entity,
      )}`,
    );
  }

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:
Logging not shown: The diff introduces GitHub credential usage and external calls but contains no visible
structured logging, so it is unclear whether failures/success are logged safely without
leaking headers/tokens elsewhere in the module lifecycle.

Referred Code
private async getOctokitClient(url: string): Promise<typeof graphql> {
  const githubIntegration = this.integrations.github.byUrl(url);
  if (!githubIntegration) {
    throw new Error(`Missing GitHub integration for '${url}'`);
  }

  const credentialsProvider =
    DefaultGithubCredentialsProvider.fromIntegrations(this.integrations);

  const { headers } = await credentialsProvider.getCredentials({
    url,
  });

  const { graphql } = await import('@octokit/graphql');
  return graphql.defaults({
    headers,
    baseUrl: githubIntegration.config.apiBaseUrl,
  });
}

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:
External input limits: The GitHub GraphQL fetch hardcodes first: 300 with no pagination and relies on url and
repository inputs with minimal validation, which may yield incomplete results or
unexpected behavior for large repos or non-GitHub source URLs.

Referred Code
async getDependabotAlerts(
  url: string,
  repository: { owner: string; repo: string },
): Promise<DependabotAlert[]> {
  const octokit = await this.getOctokitClient(url);

  const query = `
    query getDependabotAlerts($owner: String!, $repo: String!) {
      repository(owner: $owner, name: $repo) {
        vulnerabilityAlerts(first: 300, states: [OPEN]) {
          nodes {
            number
            state
            createdAt
            securityAdvisory {
              severity
            }
          }
        }
      }
    }


 ... (clipped 27 lines)

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

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

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
5.1% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@rhdh-qodo-merge
Copy link

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Implement pagination for fetching alerts

Implement pagination in getDependabotAlerts to fetch all alerts from the GitHub
GraphQL API, as the current implementation is limited to the first 300, which
can cause inaccurate scoring.

workspaces/scorecard/plugins/scorecard-backend-module-dependabot/src/clients/DependabotClient.ts [63-102]

 const query = `
-  query getDependabotAlerts($owner: String!, $repo: String!) {
+  query getDependabotAlerts($owner: String!, $repo: String!, $cursor: String) {
     repository(owner: $owner, name: $repo) {
-      vulnerabilityAlerts(first: 300, states: [OPEN]) {
+      vulnerabilityAlerts(first: 100, states: [OPEN], after: $cursor) {
+        pageInfo {
+          hasNextPage
+          endCursor
+        }
         nodes {
           number
           state
           createdAt
           securityAdvisory {
             severity
           }
         }
       }
     }
   }
 `;
 
-const response = await octokit<{
+type VulnerabilityAlertsResponse = {
   repository: {
     vulnerabilityAlerts: {
+      pageInfo: {
+        hasNextPage: boolean;
+        endCursor?: string;
+      };
       nodes: Array<{
         number: number;
         state: string;
         createdAt: string;
         securityAdvisory: { severity: string } | null;
       }>;
     };
   };
-}>(query, {
-  owner: repository.owner,
-  repo: repository.repo,
-});
+};
 
-const nodes = response.repository.vulnerabilityAlerts.nodes ?? [];
-return nodes.map(node => ({
+let allNodes: VulnerabilityAlertsResponse['repository']['vulnerabilityAlerts']['nodes'] =
+  [];
+let hasNextPage = true;
+let cursor: string | undefined;
+
+while (hasNextPage) {
+  const response = await octokit<VulnerabilityAlertsResponse>(query, {
+    owner: repository.owner,
+    repo: repository.repo,
+    cursor,
+  });
+
+  const vulnerabilityAlerts = response.repository.vulnerabilityAlerts;
+  allNodes = allNodes.concat(vulnerabilityAlerts.nodes ?? []);
+  hasNextPage = vulnerabilityAlerts.pageInfo.hasNextPage;
+  cursor = vulnerabilityAlerts.pageInfo.endCursor;
+}
+
+return allNodes.map(node => ({
   number: node.number,
   state: node.state,
   createdAt: node.createdAt,
   severity: node.securityAdvisory?.severity ?? null,
 }));
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a potential bug where not all Dependabot alerts are fetched due to a hardcoded limit, which could lead to inaccurate metric calculations. Implementing pagination is crucial for correctness.

High
Remove buggy and unused function

Remove the unused and buggy createDependabotMetricProvider function, as it
incorrectly calls the DependabotMetricProvider constructor with a
ThresholdConfig object instead of the required Config object.

workspaces/scorecard/plugins/scorecard-backend-module-dependabot/src/metricProviders/DependabotMetricProvider.ts [118-123]

 /**
  * @returns a Dependabot metric provider.
  */
+// This function is not used and has a bug in how it instantiates the provider.
+// It is recommended to remove it. If it is to be kept, it should be fixed to accept a Config object.
+/*
 export function createDependabotMetricProvider(): MetricProvider<'number'> {
   return new DependabotMetricProvider(DEPENDABOT_THRESHOLDS);
 }
+*/
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a bug in the createDependabotMetricProvider function where it passes incorrect arguments to the constructor, which would cause a runtime error. Removing this unused and buggy function improves code quality.

Medium
General
Add safe chaining for response

Add optional chaining to response.repository.vulnerabilityAlerts.nodes to
prevent a runtime error if repository or vulnerabilityAlerts are missing in the
GraphQL response.

workspaces/scorecard/plugins/scorecard-backend-module-dependabot/src/clients/DependabotClient.ts [96]

-const nodes = response.repository.vulnerabilityAlerts.nodes ?? [];
+const nodes = response.repository?.vulnerabilityAlerts?.nodes ?? [];
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: This suggestion improves the robustness of data handling by using optional chaining to prevent potential runtime errors if the GraphQL response structure is unexpected, which is good practice for handling external API data.

Low
Optimize metric calculation logic

Optimize the calculateMetric function by iterating over the alerts array only
once to find the highest severity, instead of using filter multiple times, to
improve performance.

workspaces/scorecard/plugins/scorecard-backend-module-dependabot/src/metricProviders/DependabotMetricProvider.ts [99-115]

 async calculateMetric(entity: Entity): Promise<number> {
   const alerts = await this.dependabotClient.getDependabotAlerts(
     getEntitySourceLocation(entity).target,
     this.getRepository(entity),
   );
 
-  if (alerts.filter(alert => alert.severity === 'CRITICAL').length > 0) {
+  const severities = new Set(alerts.map(alert => alert.severity));
+
+  if (severities.has('CRITICAL')) {
     return 9;
-  } else if (alerts.filter(alert => alert.severity === 'HIGH').length > 0) {
+  }
+  if (severities.has('HIGH')) {
     return 6;
-  } else if (alerts.filter(alert => alert.severity === 'MEDIUM').length > 0) {
+  }
+  if (severities.has('MEDIUM')) {
     return 3;
-  } else if (alerts.filter(alert => alert.severity === 'LOW').length > 0) {
-    return 0;
   }
   return 0;
 }
  • Apply / Chat
Suggestion importance[1-10]: 4

__

Why: The suggestion offers a valid performance optimization by reducing multiple array iterations to a single pass, which improves code efficiency and readability, although the performance impact is minor.

Low
  • More

@rhdh-qodo-merge
Copy link

rhdh-qodo-merge bot commented Feb 18, 2026

CI Feedback 🧐

(Feedback updated until commit cb5db1f)

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: check all required jobs

Failed stage: Run exit 1 [❌]

Failed test name: ""

Failure summary:

The action failed because the workflow explicitly ran exit 1, which forces the step to terminate
with a non-zero status.
- The failing step is Run exit 1 (log lines 41-45), and it ended with exit
code 1, causing the job check all required jobs to fail.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

30:  Packages: write
31:  Pages: write
32:  PullRequests: write
33:  RepositoryProjects: write
34:  SecurityEvents: write
35:  Statuses: write
36:  ##[endgroup]
37:  Secret source: Actions
38:  Prepare workflow directory
39:  Prepare all required actions
40:  Complete job name: check all required jobs
41:  ##[group]Run exit 1
42:  �[36;1mexit 1�[0m
43:  shell: /usr/bin/bash -e {0}
44:  ##[endgroup]
45:  ##[error]Process completed with exit code 1.
46:  Cleaning up orphan processes

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.

1 participant