From 3cbb66b9785061fe8354043ec273648447233d41 Mon Sep 17 00:00:00 2001 From: Andre Lizardo Date: Tue, 10 Feb 2026 11:24:50 +0100 Subject: [PATCH 1/8] Remove abstract metric providers, left only openssf metric provider and simplied the usage of the openssf client by fetching scorecards only using a baseUrl --- .../README.md | 104 ++------ .../src/clients/OpenSSFClient.test.ts | 81 ++++-- .../src/clients/OpenSSFClient.ts | 19 +- .../src/clients/utils.ts | 41 --- .../AbstractMetricProvider.test.ts | 246 ------------------ .../DefaultOpenSSFMetricProvider.test.ts | 54 ---- .../DefaultOpenSSFMetricProvider.ts | 58 ----- .../OpenSSFMetricProvider.test.ts | 227 ++++++++++++++++ ...icProvider.ts => OpenSSFMetricProvider.ts} | 76 +++--- .../src/module.ts | 8 +- 10 files changed, 348 insertions(+), 566 deletions(-) delete mode 100644 workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/utils.ts delete mode 100644 workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/AbstractMetricProvider.test.ts delete mode 100644 workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/DefaultOpenSSFMetricProvider.test.ts delete mode 100644 workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/DefaultOpenSSFMetricProvider.ts create mode 100644 workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts rename workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/{AbstractMetricProvider.ts => OpenSSFMetricProvider.ts} (58%) diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/README.md b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/README.md index 1bbaf6fdae..79f3da5cd9 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/README.md +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/README.md @@ -1,21 +1,12 @@ # Scorecard Backend Module for OpenSSF -This is an extension module to the `backstage-plugin-scorecard-backend` plugin. It provides [OpenSSF Security Scorecard](https://securityscorecards.dev/) metrics for software components registered in the Backstage catalog. +Adds [OpenSSF Security Scorecard](https://securityscorecards.dev/) metrics to the Scorecard backend. Fetches scorecard data from the URL configured per component (`openssf/baseUrl`), so it can use the public API, a self-hosted endpoint, or any other scorecard source. Exposes 18 checks as Backstage metrics (scores 0–10). -## Overview - -The OpenSSF Security Scorecards project provides automated security assessments for open source projects hosted on GitHub. This module fetches scorecard data from the public OpenSSF API and exposes individual security check scores as metrics in Backstage. - -## Prerequisites - -Before installing this module, ensure that the Scorecard backend plugin is integrated into your Backstage instance. Follow the [Scorecard backend plugin README](../scorecard-backend/README.md) for setup instructions. +Requires the [Scorecard backend plugin](../scorecard-backend/README.md) to be installed. ## Installation -To install this backend module: - ```bash -# From your root directory yarn workspace backend add @red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf ``` @@ -24,104 +15,41 @@ yarn workspace backend add @red-hat-developer-hub/backstage-plugin-scorecard-bac import { createBackend } from '@backstage/backend-defaults'; const backend = createBackend(); - -// Scorecard backend plugin backend.add( import('@red-hat-developer-hub/backstage-plugin-scorecard-backend'), ); - -// Install the OpenSSF module -/* highlight-add-next-line */ backend.add( import( '@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf' ), ); - backend.start(); ``` -## Entity Annotations +## Configuration + +### Catalog (catalog-info.yaml) -For the OpenSSF metrics to work, your catalog entities must have the required annotation: +| Annotation | Required | Description | +| ----------------- | -------- | --------------------------------------------------------------------------- | +| `openssf/baseUrl` | Yes | Full scorecard API URL for this component (e.g. public API or self-hosted). | + +Example: ```yaml -# catalog-info.yaml -apiVersion: backstage.io/v1alpha1 -kind: Component metadata: - name: my-service annotations: - # Required: GitHub repository in owner/repo format - openssf/project: owner/repo -spec: - type: service - lifecycle: production - owner: my-team + openssf/baseUrl: https://api.securityscorecards.dev/projects/github.com/owner/repo ``` -The `openssf/project` annotation should contain the GitHub repository path in `owner/repo` format (e.g., `kubernetes/kubernetes`). - -## Configuration - -This module uses the public OpenSSF Security Scorecards API (`api.securityscorecards.dev`) and does not require any additional configuration in `app-config.yaml`. - ### Thresholds -Thresholds define conditions that determine which category a metric value belongs to (`error`, `warning`, or `success`). Check out detailed explanation of [threshold configuration](../scorecard-backend/docs/thresholds.md). - -All OpenSSF metrics use the following **fixed** thresholds: - -| Category | Expression | Description | -| -------- | ---------- | --------------------------------- | -| Error | `<2` | Score less than 2 | -| Warning | `2-7` | Score between 2 and 7 (inclusive) | -| Success | `>7` | Score greater than 7 | - -> **Note:** These thresholds are not configurable via `app-config.yaml`. They are defined in the module source code. - -## Available Metrics - -This module provides 18 metrics corresponding to the [OpenSSF Security Scorecard checks](https://github.com/ossf/scorecard/blob/main/docs/checks.md). Each metric returns a score from 0 to 10. - -| Metric ID | Risk | Description | -| -------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | -| `openssf.binary_artifacts` | High | Determines if the project has generated executable (binary) artifacts in the source repository. | -| `openssf.branch_protection` | High | Determines if the default and release branches are protected with GitHub's branch protection or repository rules settings. | -| `openssf.cii_best_practices` | Low | Determines if the project has an OpenSSF (formerly CII) Best Practices Badge. | -| `openssf.ci_tests` | Low | Determines if the project runs tests before pull requests are merged. | -| `openssf.code_review` | High | Determines if the project requires human code review before pull requests are merged. | -| `openssf.contributors` | Low | Determines if the project has contributors from multiple organizations. | -| `openssf.dangerous_workflow` | Critical | Determines if the project's GitHub Action workflows avoid dangerous patterns. | -| `openssf.dependency_update_tool` | High | Determines if the project uses a dependency update tool. | -| `openssf.fuzzing` | Medium | Determines if the project uses fuzzing. | -| `openssf.license` | Low | Determines if the project has defined a license. | -| `openssf.maintained` | High | Determines if the project is "actively maintained". | -| `openssf.packaging` | Medium | Determines if the project is published as a package that others can easily download, install, update, and uninstall. | -| `openssf.pinned_dependencies` | Medium | Determines if the project has declared and pinned the dependencies of its build process. | -| `openssf.sast` | Medium | Determines if the project uses static code analysis. | -| `openssf.security_policy` | Medium | Determines if the project has published a security policy. | -| `openssf.signed_releases` | High | Determines if the project cryptographically signs release artifacts. | -| `openssf.token_permissions` | High | Determines if the project's automated workflow tokens follow the principle of least privilege. | -| `openssf.vulnerabilities` | High | Determines if the project has open, unfixed vulnerabilities in its codebase or dependencies using OSV. | - -## Troubleshooting - -### Metric shows "not found" +All OpenSSF metrics use fixed thresholds: **Error** <2, **Warning** 2–7, **Success** >7. Not configurable. See [threshold docs](../scorecard-backend/docs/thresholds.md). -This can occur if: +## Metrics -- The repository has not been analyzed by OpenSSF Scorecards yet -- The repository is private (OpenSSF only analyzes public repositories) -- The repository path in the annotation is incorrect -- The metric score is lower than -1 or higher than 10. +18 metrics from [OpenSSF checks](https://github.com/ossf/scorecard/blob/main/docs/checks.md): `openssf.binary_artifacts`, `openssf.branch_protection`, `openssf.cii_best_practices`, `openssf.ci_tests`, `openssf.code_review`, `openssf.contributors`, `openssf.dangerous_workflow`, `openssf.dependency_update_tool`, `openssf.fuzzing`, `openssf.license`, `openssf.maintained`, `openssf.packaging`, `openssf.pinned_dependencies`, `openssf.sast`, `openssf.security_policy`, `openssf.signed_releases`, `openssf.token_permissions`, `openssf.vulnerabilities`. -### No data for my repository - -OpenSSF Security Scorecards only analyzes **public GitHub repositories**. Private repositories and repositories on other Git hosting services are not supported. - -To verify your repository has scorecard data, visit: +## Troubleshooting -``` -https://api.securityscorecards.dev/projects/github.com/{owner}/{repo} -``` +- **Metric "not found"**: Scorecard URL unreachable, repo not yet analyzed, or score outside 0–10. diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts index ca5f406255..997c3b6cc3 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts @@ -14,66 +14,91 @@ * limitations under the License. */ +import type { Entity } from '@backstage/catalog-model'; + import { OpenSSFClient } from './OpenSSFClient'; -import { OpenSSFResponse } from './types'; +import type { OpenSSFResponse } from './types'; -describe('OpenSSFClient', () => { - let client: OpenSSFClient; +const mockScorecardUrl = + 'https://api.securityscorecards.dev/projects/github.com/owner/repo'; - const mockOpenSSFResponse: OpenSSFResponse = { - date: '2024-01-15', - repo: { - name: 'github.com/owner/test', - commit: 'abc123', +function createEntity(baseUrl: string): Entity { + return { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'my-service', + annotations: { + 'openssf/baseUrl': baseUrl, + }, }, - scorecard: { - version: '4.0.0', - commit: 'def456', + spec: {}, + } as Entity; +} + +const mockOpenSSFResponse: OpenSSFResponse = { + date: '2024-01-15', + repo: { name: 'github.com/owner/repo', commit: 'abc123' }, + scorecard: { version: '4.0.0', commit: 'def456' }, + score: 7.5, + checks: [ + { + name: 'Maintained', + score: 8, + reason: null, + details: null, + documentation: { short: '', url: '' }, }, - score: 7.5, - checks: [], - }; + ], +}; + +describe('OpenSSFClient', () => { + const entity = createEntity(mockScorecardUrl); beforeEach(() => { jest.clearAllMocks(); - client = new OpenSSFClient(); globalThis.fetch = jest.fn(); }); describe('getScorecard', () => { - it('should return the scorecard', async () => { - // mocked fetch behaviour for the test + it('fetches the scorecard from the entity baseUrl', async () => { (globalThis.fetch as jest.Mock).mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue(mockOpenSSFResponse), }); - const scorecard = await client.getScorecard('owner', 'test'); - expect(scorecard).toEqual(mockOpenSSFResponse); + const client = new OpenSSFClient(entity); + const result = await client.getScorecard(); + + expect(fetch).toHaveBeenCalledWith(mockScorecardUrl, { + method: 'GET', + headers: { Accept: 'application/json' }, + }); + expect(result).toEqual(mockOpenSSFResponse); }); - it('should throw an error if the API returns a non-ok response', async () => { - // mock response from the API + it('throws when the response is not ok', async () => { (globalThis.fetch as jest.Mock).mockResolvedValue({ ok: false, status: 404, statusText: 'Not Found', }); - await expect(client.getScorecard('wrong', 'test')).rejects.toThrow( + const client = new OpenSSFClient(entity); + + await expect(client.getScorecard()).rejects.toThrow( 'OpenSSF API request failed with status 404: Not Found', ); }); - it('should throw an error if API request fails', async () => { - // mocked fetch behaviour for the test + it('throws when fetch rejects', async () => { (globalThis.fetch as jest.Mock).mockRejectedValue( - new Error('API request failed'), + new Error('Network error'), ); - await expect(client.getScorecard('owner', 'test')).rejects.toThrow( - 'API request failed', - ); + const client = new OpenSSFClient(entity); + + await expect(client.getScorecard()).rejects.toThrow('Network error'); }); }); }); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts index efda22c59f..b58f37fa3a 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts @@ -14,24 +14,19 @@ * limitations under the License. */ +import type { Entity } from '@backstage/catalog-model'; + import { OpenSSFResponse } from './types'; export class OpenSSFClient { private readonly baseUrl: string; - private readonly gitServiceHost: string; - - constructor( - baseUrl: string = 'https://api.securityscorecards.dev/projects', - gitServiceHost: string = 'github.com', - ) { - this.baseUrl = baseUrl; - this.gitServiceHost = gitServiceHost; - } - async getScorecard(owner: string, repo: string): Promise { - const apiUrl = `${this.baseUrl}/${this.gitServiceHost}/${owner}/${repo}`; + constructor(entity: Entity) { + this.baseUrl = entity.metadata.annotations?.['openssf/baseUrl'] ?? ''; + } - const response = await fetch(apiUrl, { + async getScorecard(): Promise { + const response = await fetch(this.baseUrl, { method: 'GET', headers: { Accept: 'application/json', diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/utils.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/utils.ts deleted file mode 100644 index 03ec0cf43d..0000000000 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { type Entity, stringifyEntityRef } from '@backstage/catalog-model'; - -export const getRepositoryInformationFromEntity = ( - entity: Entity, -): { owner: string; repo: string } => { - const projectSlug = entity.metadata.annotations?.['openssf/project']; - if (!projectSlug) { - throw new Error( - `Missing annotation 'openssf/project' for entity ${stringifyEntityRef( - entity, - )}`, - ); - } - - const [owner, repo] = projectSlug.split('/'); - if (!owner || !repo) { - throw new Error( - `Invalid format of 'openssf/project' ${projectSlug} for entity ${stringifyEntityRef( - entity, - )}`, - ); - } - - return { owner, repo }; -}; diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/AbstractMetricProvider.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/AbstractMetricProvider.test.ts deleted file mode 100644 index 5d51a618c3..0000000000 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/AbstractMetricProvider.test.ts +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; -import { type Entity } from '@backstage/catalog-model'; -import { - DEFAULT_NUMBER_THRESHOLDS, - ThresholdConfig, -} from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; - -import { OpenSSFClient } from '../clients/OpenSSFClient'; -import { OpenSSFResponse } from '../clients/types'; -import { AbstractMetricProvider } from './AbstractMetricProvider'; - -// Mock the OpenSSFClient module -jest.mock('../clients/OpenSSFClient'); - -// Concrete implementation for testing the abstract class -class TestMetricProvider extends AbstractMetricProvider { - getMetricName(): string { - return 'Test-Metric'; - } - - getMetricDisplayTitle(): string { - return 'Test Metric Title'; - } - - getMetricDescription(): string { - return 'Test metric description'; - } -} - -describe('AbstractMetricProvider', () => { - let provider: TestMetricProvider; - let mockGetScorecard: jest.Mock; - - const mockOpenSSFResponse: OpenSSFResponse = { - date: '2024-01-15', - repo: { - name: 'github.com/owner/test', - commit: 'abc123', - }, - scorecard: { - version: '4.0.0', - commit: 'def456', - }, - score: 7.5, - checks: [ - { - name: 'Test-Metric', - score: 8, - reason: 'Test reason', - details: null, - documentation: { - short: 'Short doc', - url: 'https://example.com', - }, - }, - { - name: 'Other-Metric', - score: 6, - reason: 'Other reason', - details: null, - documentation: { - short: 'Other doc', - url: 'https://example.com/other', - }, - }, - ], - }; - - const createMockEntity = (projectSlug?: string): Entity => ({ - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'test-component', - annotations: projectSlug ? { 'openssf/project': projectSlug } : undefined, - }, - }); - - beforeEach(() => { - jest.clearAllMocks(); - - // Setup mock for OpenSSFClient - mockGetScorecard = jest.fn().mockResolvedValue(mockOpenSSFResponse); - (OpenSSFClient as jest.Mock).mockImplementation(() => ({ - getScorecard: mockGetScorecard, - })); - - provider = new TestMetricProvider(); - }); - - describe('getProviderDatasourceId', () => { - it('should return "openssf"', () => { - expect(provider.getProviderDatasourceId()).toBe('openssf'); - }); - }); - - describe('getProviderId', () => { - it('should return normalized provider ID with openssf prefix', () => { - expect(provider.getProviderId()).toBe('openssf.test_metric'); - }); - - it('should convert hyphens to underscores and lowercase', () => { - // The metric name is "Test-Metric", should become "test_metric" - expect(provider.getProviderId()).toBe('openssf.test_metric'); - }); - }); - - describe('getMetricType', () => { - it('should return "number"', () => { - expect(provider.getMetricType()).toBe('number'); - }); - }); - - describe('getMetric', () => { - it('should return metric object with correct properties', () => { - const metric = provider.getMetric(); - - expect(metric).toEqual({ - id: 'openssf.test_metric', - title: 'Test Metric Title', - description: 'Test metric description', - type: 'number', - history: true, - }); - }); - }); - - describe('getMetricThresholds', () => { - it('should return default thresholds when none provided', () => { - expect(provider.getMetricThresholds()).toEqual(DEFAULT_NUMBER_THRESHOLDS); - }); - - it('should return custom thresholds when provided', () => { - const customThresholds: ThresholdConfig = { - rules: [ - { key: 'success', expression: '>9' }, - { key: 'warning', expression: '7-9' }, - { key: 'error', expression: '<7' }, - ], - }; - const customProvider = new TestMetricProvider(customThresholds); - - expect(customProvider.getMetricThresholds()).toEqual(customThresholds); - }); - }); - - describe('getCatalogFilter', () => { - it('should return filter for openssf/project-slug annotation', () => { - expect(provider.getCatalogFilter()).toEqual({ - 'metadata.annotations.openssf/project': CATALOG_FILTER_EXISTS, - }); - }); - }); - - describe('calculateMetric', () => { - it('should call OpenSSFClient with owner and repo from entity', async () => { - const entity = createMockEntity('owner/test'); - - await provider.calculateMetric(entity); - - expect(mockGetScorecard).toHaveBeenCalledWith('owner', 'test'); - }); - - it('should return the score for the matching metric', async () => { - const entity = createMockEntity('owner/test'); - - const score = await provider.calculateMetric(entity); - - // provider has getMetricName() returning 'Test-Metric', so score should be 8 - expect(score).toBe(8); - }); - - it('should throw error when metric is not found in scorecard', async () => { - const responseWithoutMetric: OpenSSFResponse = { - ...mockOpenSSFResponse, - checks: [ - { - name: 'Different-Metric', - score: 5, - reason: 'Different reason', - details: null, - documentation: { - short: 'Different doc', - url: 'https://example.com/different', - }, - }, - ], - }; - mockGetScorecard.mockResolvedValue(responseWithoutMetric); - - const entity = createMockEntity('owner/test'); - - await expect(provider.calculateMetric(entity)).rejects.toThrow( - "OpenSSF check 'Test-Metric' not found in scorecard for owner/test", - ); - }); - - it('should throw error when entity is missing openssf/project annotation', async () => { - const entity = createMockEntity(); - - await expect(provider.calculateMetric(entity)).rejects.toThrow( - "Missing annotation 'openssf/project'", - ); - }); - - it('should throw error when project slug has invalid format', async () => { - const entity = createMockEntity('invalid-slug-without-slash'); - - await expect(provider.calculateMetric(entity)).rejects.toThrow( - "Invalid format of 'openssf/project'", - ); - }); - - it('should throw error when metric score is less than 0', async () => { - const entity = createMockEntity('owner/test'); - mockOpenSSFResponse.checks[0].score = -1; - mockOpenSSFResponse.checks[0].reason = 'Repository not found.'; - await expect(provider.calculateMetric(entity)).rejects.toThrow( - "OpenSSF check 'Test-Metric' has invalid score -1 for owner/test. Reason: Repository not found.", - ); - }); - - it('should throw error when metric score is greater than 10', async () => { - const entity = createMockEntity('owner/test'); - mockOpenSSFResponse.checks[0].score = 11; - await expect(provider.calculateMetric(entity)).rejects.toThrow( - "OpenSSF check 'Test-Metric' has invalid score 11 for owner/test", - ); - }); - }); -}); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/DefaultOpenSSFMetricProvider.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/DefaultOpenSSFMetricProvider.test.ts deleted file mode 100644 index 1a710addbd..0000000000 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/DefaultOpenSSFMetricProvider.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - createDefaultOpenSSFMetricProviders, - DefaultOpenSSFMetricProvider, -} from './DefaultOpenSSFMetricProvider'; -import { OPENSSF_METRICS, OPENSSF_THRESHOLDS } from './OpenSSFConfig'; - -describe('DefaultOpenSSFMetricProviderTests', () => { - it('should create a default OpenSSF metric provider', () => { - const provider = new DefaultOpenSSFMetricProvider( - OPENSSF_METRICS[0], - OPENSSF_THRESHOLDS, - ); - expect(provider.getMetricDisplayTitle()).toBe( - OPENSSF_METRICS[0].displayTitle, - ); - expect(provider.getMetricDescription()).toBe( - OPENSSF_METRICS[0].description, - ); - expect(provider.getMetricThresholds()).toBe(OPENSSF_THRESHOLDS); - }); - - it('should create a default OpenSSF metric provider with custom thresholds', () => { - const provider = new DefaultOpenSSFMetricProvider( - OPENSSF_METRICS[0], - OPENSSF_THRESHOLDS, - ); - expect(provider).toBeDefined(); - }); - - it('should create all default OpenSSF metric providers', () => { - const providers = createDefaultOpenSSFMetricProviders(OPENSSF_THRESHOLDS); - expect(providers.length).toBe(OPENSSF_METRICS.length); - for (const provider of providers) { - expect(provider).toBeInstanceOf(DefaultOpenSSFMetricProvider); - expect(provider.getMetricThresholds()).toBe(OPENSSF_THRESHOLDS); - } - }); -}); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/DefaultOpenSSFMetricProvider.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/DefaultOpenSSFMetricProvider.ts deleted file mode 100644 index 968488bdbe..0000000000 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/DefaultOpenSSFMetricProvider.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ThresholdConfig } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; -import { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node'; -import { AbstractMetricProvider } from './AbstractMetricProvider'; -import { OPENSSF_METRICS, OpenSSFMetricConfig } from './OpenSSFConfig'; - -/** - * Default metric provider for OpenSSF Security Scorecards. - * Extracts a specific check from the OpenSSF scorecard response based on the provided configuration. - */ -export class DefaultOpenSSFMetricProvider extends AbstractMetricProvider { - constructor( - private readonly config: OpenSSFMetricConfig, - thresholds?: ThresholdConfig, - ) { - super(thresholds); - } - - getMetricName(): string { - return this.config.name; - } - - getMetricDisplayTitle(): string { - return this.config.displayTitle; - } - - getMetricDescription(): string { - return this.config.description; - } -} - -/** - * Creates all default OpenSSF metric providers. - * @param thresholds Optional threshold configuration to apply to all providers - * @returns Array of OpenSSF metric providers - */ -export function createDefaultOpenSSFMetricProviders( - thresholds?: ThresholdConfig, -): MetricProvider<'number'>[] { - return OPENSSF_METRICS.map( - config => new DefaultOpenSSFMetricProvider(config, thresholds), - ); -} diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts new file mode 100644 index 0000000000..95de2b81cf --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts @@ -0,0 +1,227 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; +import type { Entity } from '@backstage/catalog-model'; + +import { OpenSSFMetricProvider } from './OpenSSFMetricProvider'; +import { OPENSSF_THRESHOLDS } from './OpenSSFConfig'; + +const scorecardUrl = + 'https://api.securityscorecards.dev/projects/github.com/owner/repo'; + +function createEntity(): Entity { + return { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'my-service', + annotations: { 'openssf/baseUrl': scorecardUrl }, + }, + spec: {}, + } as Entity; +} + +const maintainedConfig = { + name: 'Maintained', + displayTitle: 'OpenSSF Maintained', + description: 'Determines if the project is actively maintained.', +}; + +describe('OpenSSFMetricProvider', () => { + const entity = createEntity(); + + beforeEach(() => { + jest.clearAllMocks(); + globalThis.fetch = jest.fn(); + }); + + describe('metadata', () => { + it('returns metric name from config', () => { + const provider = new OpenSSFMetricProvider( + maintainedConfig, + OPENSSF_THRESHOLDS, + entity, + ); + expect(provider.getMetricName()).toBe('Maintained'); + }); + + it('returns display title and description from config', () => { + const provider = new OpenSSFMetricProvider( + maintainedConfig, + OPENSSF_THRESHOLDS, + entity, + ); + expect(provider.getMetricDisplayTitle()).toBe('OpenSSF Maintained'); + expect(provider.getMetricDescription()).toContain('actively maintained'); + }); + + it('returns provider id as openssf.', () => { + const provider = new OpenSSFMetricProvider( + maintainedConfig, + OPENSSF_THRESHOLDS, + entity, + ); + expect(provider.getProviderId()).toBe('openssf.maintained'); + }); + + it('returns openssf as provider datasource id', () => { + const provider = new OpenSSFMetricProvider( + maintainedConfig, + OPENSSF_THRESHOLDS, + entity, + ); + expect(provider.getProviderDatasourceId()).toBe('openssf'); + }); + + it('returns number as metric type', () => { + const provider = new OpenSSFMetricProvider( + maintainedConfig, + OPENSSF_THRESHOLDS, + entity, + ); + expect(provider.getMetricType()).toBe('number'); + }); + + it('returns metric descriptor with history enabled', () => { + const provider = new OpenSSFMetricProvider( + maintainedConfig, + OPENSSF_THRESHOLDS, + entity, + ); + const metric = provider.getMetric(); + expect(metric.id).toBe('openssf.maintained'); + expect(metric.title).toBe('OpenSSF Maintained'); + expect(metric.type).toBe('number'); + expect(metric.history).toBe(true); + }); + + it('returns configured thresholds', () => { + const provider = new OpenSSFMetricProvider( + maintainedConfig, + OPENSSF_THRESHOLDS, + entity, + ); + expect(provider.getMetricThresholds()).toEqual(OPENSSF_THRESHOLDS); + }); + + it('requires openssf/baseUrl annotation in catalog filter', () => { + const provider = new OpenSSFMetricProvider( + maintainedConfig, + OPENSSF_THRESHOLDS, + entity, + ); + expect(provider.getCatalogFilter()).toEqual({ + 'metadata.annotations.openssf/baseUrl': CATALOG_FILTER_EXISTS, + }); + }); + }); + + describe('calculateMetric', () => { + it('returns the score for the configured check', async () => { + (globalThis.fetch as jest.Mock).mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({ + date: '2024-01-15', + repo: { name: 'github.com/owner/repo', commit: 'x' }, + scorecard: { version: '4.0.0', commit: 'y' }, + score: 7, + checks: [ + { + name: 'Maintained', + score: 8, + reason: null, + details: null, + documentation: { short: '', url: '' }, + }, + ], + }), + }); + + const provider = new OpenSSFMetricProvider( + maintainedConfig, + OPENSSF_THRESHOLDS, + entity, + ); + const result = await provider.calculateMetric(); + + expect(result).toBe(8); + expect(fetch).toHaveBeenCalledWith(scorecardUrl, expect.any(Object)); + }); + + it('throws when the check is not in the scorecard', async () => { + (globalThis.fetch as jest.Mock).mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({ + date: '2024-01-15', + repo: { name: 'x', commit: 'x' }, + scorecard: { version: '4.0.0', commit: 'y' }, + score: 7, + checks: [ + { + name: 'Other-Check', + score: 5, + reason: null, + details: null, + documentation: { short: '', url: '' }, + }, + ], + }), + }); + + const provider = new OpenSSFMetricProvider( + maintainedConfig, + OPENSSF_THRESHOLDS, + entity, + ); + + await expect(provider.calculateMetric()).rejects.toThrow( + "OpenSSF check 'Maintained' not found in scorecard", + ); + }); + + it('throws when the check score is out of range (< 0 or > 10)', async () => { + (globalThis.fetch as jest.Mock).mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({ + date: '2024-01-15', + repo: { name: 'x', commit: 'x' }, + scorecard: { version: '4.0.0', commit: 'y' }, + score: 7, + checks: [ + { + name: 'Maintained', + score: 11, + reason: null, + details: null, + documentation: { short: '', url: '' }, + }, + ], + }), + }); + + const provider = new OpenSSFMetricProvider( + maintainedConfig, + OPENSSF_THRESHOLDS, + entity, + ); + + await expect(provider.calculateMetric()).rejects.toThrow( + "OpenSSF check 'Maintained' has invalid score 11", + ); + }); + }); +}); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/AbstractMetricProvider.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts similarity index 58% rename from workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/AbstractMetricProvider.ts rename to workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts index cddf5c1506..a049fc45c7 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/AbstractMetricProvider.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts @@ -16,42 +16,45 @@ import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { type Entity } from '@backstage/catalog-model'; + import { - DEFAULT_NUMBER_THRESHOLDS, Metric, ThresholdConfig, } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; import { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node'; import { OpenSSFClient } from '../clients/OpenSSFClient'; -import { getRepositoryInformationFromEntity } from '../clients/utils'; +import { + OPENSSF_METRICS, + OPENSSF_THRESHOLDS, + OpenSSFMetricConfig, +} from './OpenSSFConfig'; -/** - * Abstract base class for OpenSSF metric providers. - * Extracts a specific check from the OpenSSF scorecard response. - * - * Subclasses must implement: - * - getCheckName(): The name of the check to extract (e.g., "Maintained", "Code-Review") - * - getMetricName(): The metric name for the provider ID (e.g., "maintained", "code_review") - * - getMetricTitle(): Display title for the metric - * - getMetricDescription(): Description of what the metric measures - */ -export abstract class AbstractMetricProvider - implements MetricProvider<'number'> -{ +export class OpenSSFMetricProvider implements MetricProvider<'number'> { protected readonly openSSFClient: OpenSSFClient; protected readonly thresholds: ThresholdConfig; - constructor(thresholds?: ThresholdConfig) { - this.openSSFClient = new OpenSSFClient(); - this.thresholds = thresholds ?? DEFAULT_NUMBER_THRESHOLDS; + constructor( + readonly config: OpenSSFMetricConfig, + thresholds: ThresholdConfig, + entity: Entity, + ) { + this.thresholds = thresholds; + this.config = config; + this.openSSFClient = new OpenSSFClient(entity); } - abstract getMetricName(): string; + getMetricName(): string { + return this.config.name; + } - abstract getMetricDisplayTitle(): string; + getMetricDisplayTitle(): string { + return this.config.displayTitle; + } - abstract getMetricDescription(): string; + getMetricDescription(): string { + return this.config.description; + } getProviderDatasourceId(): string { return 'openssf'; @@ -84,30 +87,37 @@ export abstract class AbstractMetricProvider getCatalogFilter(): Record { return { - 'metadata.annotations.openssf/project': CATALOG_FILTER_EXISTS, + 'metadata.annotations.openssf/baseUrl': CATALOG_FILTER_EXISTS, }; } - async calculateMetric(entity: Entity): Promise { - const { owner, repo } = getRepositoryInformationFromEntity(entity); - const scorecard = await this.openSSFClient.getScorecard(owner, repo); + async calculateMetric(): Promise { + const scorecard = await this.openSSFClient.getScorecard(); const metricName = this.getMetricName(); const metric = scorecard.checks.find(c => c.name === metricName); if (!metric) { - throw new Error( - `OpenSSF check '${metricName}' not found in scorecard for ${owner}/${repo}`, - ); + throw new Error(`OpenSSF check '${metricName}' not found in scorecard`); } else if (metric.score < 0 || metric.score > 10) { throw new Error( - `OpenSSF check '${metricName}' has invalid score ${ - metric.score - } for ${owner}/${repo}. Reason: ${ - metric.reason ?? 'No reason provided' - }`, + `OpenSSF check '${metricName}' has invalid score ${metric.score}`, ); } return metric.score; } } + +/** + * Creates all default OpenSSF metric providers. + * @param clientOptions Optional base URL and git service host (from app-config) + * @returns Array of OpenSSF metric providers + */ +export function createOpenSSFMetricProvider( + clientOptions?: OpenSSFClientOptions, +): MetricProvider<'number'>[] { + return OPENSSF_METRICS.map( + config => + new OpenSSFMetricProvider(config, OPENSSF_THRESHOLDS, clientOptions), + ); +} diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/module.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/module.ts index 9ca937c14c..0e661f04dd 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/module.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/module.ts @@ -15,8 +15,7 @@ */ import { createBackendModule } from '@backstage/backend-plugin-api'; import { scorecardMetricsExtensionPoint } from '@red-hat-developer-hub/backstage-plugin-scorecard-node'; -import { createDefaultOpenSSFMetricProviders } from './metricProviders/DefaultOpenSSFMetricProvider'; -import { OPENSSF_THRESHOLDS } from './metricProviders/OpenSSFConfig'; +import { createOpenSSFMetricProvider } from './metricProviders/OpenSSFMetricProvider'; export const scorecardOpenSFFModule = createBackendModule({ pluginId: 'scorecard', @@ -27,10 +26,7 @@ export const scorecardOpenSFFModule = createBackendModule({ metrics: scorecardMetricsExtensionPoint, }, async init({ metrics }) { - // Register all default OpenSSF metric providers - metrics.addMetricProvider( - ...createDefaultOpenSSFMetricProviders(OPENSSF_THRESHOLDS), - ); + metrics.addMetricProvider(...createOpenSSFMetricProvider()); }, }); }, From 4c4db4654c087ea8e6300b2f8ff452cee37c155a Mon Sep 17 00:00:00 2001 From: Andre Lizardo Date: Tue, 10 Feb 2026 13:11:53 +0100 Subject: [PATCH 2/8] fixed calculateMetric signature to receive entity as expected by the interface, refactored tests --- .../src/clients/OpenSSFClient.test.ts | 14 ++++++++------ .../src/clients/OpenSSFClient.ts | 13 ++++++------- .../OpenSSFMetricProvider.test.ts | 17 +++-------------- .../metricProviders/OpenSSFMetricProvider.ts | 14 +++++--------- 4 files changed, 22 insertions(+), 36 deletions(-) diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts index 997c3b6cc3..c5c4ebf857 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts @@ -67,8 +67,8 @@ describe('OpenSSFClient', () => { json: jest.fn().mockResolvedValue(mockOpenSSFResponse), }); - const client = new OpenSSFClient(entity); - const result = await client.getScorecard(); + const client = new OpenSSFClient(); + const result = await client.getScorecard(entity); expect(fetch).toHaveBeenCalledWith(mockScorecardUrl, { method: 'GET', @@ -84,9 +84,9 @@ describe('OpenSSFClient', () => { statusText: 'Not Found', }); - const client = new OpenSSFClient(entity); + const client = new OpenSSFClient(); - await expect(client.getScorecard()).rejects.toThrow( + await expect(client.getScorecard(entity)).rejects.toThrow( 'OpenSSF API request failed with status 404: Not Found', ); }); @@ -96,9 +96,11 @@ describe('OpenSSFClient', () => { new Error('Network error'), ); - const client = new OpenSSFClient(entity); + const client = new OpenSSFClient(); - await expect(client.getScorecard()).rejects.toThrow('Network error'); + await expect(client.getScorecard(entity)).rejects.toThrow( + 'Network error', + ); }); }); }); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts index b58f37fa3a..9606e7138a 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts @@ -19,14 +19,13 @@ import type { Entity } from '@backstage/catalog-model'; import { OpenSSFResponse } from './types'; export class OpenSSFClient { - private readonly baseUrl: string; - - constructor(entity: Entity) { - this.baseUrl = entity.metadata.annotations?.['openssf/baseUrl'] ?? ''; - } + async getScorecard(entity: Entity): Promise { + const baseUrl = entity.metadata.annotations?.['openssf/baseUrl'] ?? ''; + if (!baseUrl || baseUrl.trim() === '' || !baseUrl.startsWith('https://')) { + throw new Error(`Invalid annotation 'openssf/baseUrl' value`); + } - async getScorecard(): Promise { - const response = await fetch(this.baseUrl, { + const response = await fetch(baseUrl, { method: 'GET', headers: { Accept: 'application/json', diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts index 95de2b81cf..7143f747fd 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts @@ -54,7 +54,6 @@ describe('OpenSSFMetricProvider', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, - entity, ); expect(provider.getMetricName()).toBe('Maintained'); }); @@ -63,7 +62,6 @@ describe('OpenSSFMetricProvider', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, - entity, ); expect(provider.getMetricDisplayTitle()).toBe('OpenSSF Maintained'); expect(provider.getMetricDescription()).toContain('actively maintained'); @@ -73,7 +71,6 @@ describe('OpenSSFMetricProvider', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, - entity, ); expect(provider.getProviderId()).toBe('openssf.maintained'); }); @@ -82,7 +79,6 @@ describe('OpenSSFMetricProvider', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, - entity, ); expect(provider.getProviderDatasourceId()).toBe('openssf'); }); @@ -91,7 +87,6 @@ describe('OpenSSFMetricProvider', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, - entity, ); expect(provider.getMetricType()).toBe('number'); }); @@ -100,7 +95,6 @@ describe('OpenSSFMetricProvider', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, - entity, ); const metric = provider.getMetric(); expect(metric.id).toBe('openssf.maintained'); @@ -113,7 +107,6 @@ describe('OpenSSFMetricProvider', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, - entity, ); expect(provider.getMetricThresholds()).toEqual(OPENSSF_THRESHOLDS); }); @@ -122,7 +115,6 @@ describe('OpenSSFMetricProvider', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, - entity, ); expect(provider.getCatalogFilter()).toEqual({ 'metadata.annotations.openssf/baseUrl': CATALOG_FILTER_EXISTS, @@ -154,9 +146,8 @@ describe('OpenSSFMetricProvider', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, - entity, ); - const result = await provider.calculateMetric(); + const result = await provider.calculateMetric(entity); expect(result).toBe(8); expect(fetch).toHaveBeenCalledWith(scorecardUrl, expect.any(Object)); @@ -185,10 +176,9 @@ describe('OpenSSFMetricProvider', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, - entity, ); - await expect(provider.calculateMetric()).rejects.toThrow( + await expect(provider.calculateMetric(entity)).rejects.toThrow( "OpenSSF check 'Maintained' not found in scorecard", ); }); @@ -216,10 +206,9 @@ describe('OpenSSFMetricProvider', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, - entity, ); - await expect(provider.calculateMetric()).rejects.toThrow( + await expect(provider.calculateMetric(entity)).rejects.toThrow( "OpenSSF check 'Maintained' has invalid score 11", ); }); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts index a049fc45c7..1c39f12a8a 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts @@ -37,11 +37,10 @@ export class OpenSSFMetricProvider implements MetricProvider<'number'> { constructor( readonly config: OpenSSFMetricConfig, thresholds: ThresholdConfig, - entity: Entity, ) { this.thresholds = thresholds; this.config = config; - this.openSSFClient = new OpenSSFClient(entity); + this.openSSFClient = new OpenSSFClient(); } getMetricName(): string { @@ -91,8 +90,8 @@ export class OpenSSFMetricProvider implements MetricProvider<'number'> { }; } - async calculateMetric(): Promise { - const scorecard = await this.openSSFClient.getScorecard(); + async calculateMetric(entity: Entity): Promise { + const scorecard = await this.openSSFClient.getScorecard(entity); const metricName = this.getMetricName(); const metric = scorecard.checks.find(c => c.name === metricName); @@ -113,11 +112,8 @@ export class OpenSSFMetricProvider implements MetricProvider<'number'> { * @param clientOptions Optional base URL and git service host (from app-config) * @returns Array of OpenSSF metric providers */ -export function createOpenSSFMetricProvider( - clientOptions?: OpenSSFClientOptions, -): MetricProvider<'number'>[] { +export function createOpenSSFMetricProvider(): MetricProvider<'number'>[] { return OPENSSF_METRICS.map( - config => - new OpenSSFMetricProvider(config, OPENSSF_THRESHOLDS, clientOptions), + config => new OpenSSFMetricProvider(config, OPENSSF_THRESHOLDS), ); } From 8d0afbdff2a8f7c5ded7bef7ed83ead1e8d28e97 Mon Sep 17 00:00:00 2001 From: Andre Lizardo Date: Wed, 11 Feb 2026 15:23:37 +0100 Subject: [PATCH 3/8] add component for testing the new baseUrl annotation for openssf scorecard --- .../scorecard/examples/openssf-scorecard-only.yaml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/workspaces/scorecard/examples/openssf-scorecard-only.yaml b/workspaces/scorecard/examples/openssf-scorecard-only.yaml index 00adb4328d..5a4bb82142 100644 --- a/workspaces/scorecard/examples/openssf-scorecard-only.yaml +++ b/workspaces/scorecard/examples/openssf-scorecard-only.yaml @@ -1,14 +1,11 @@ --- -# Component with OpenSSF Scorecard apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: openssf-scorecard-only + name: openssf-scorecard annotations: - github.com/project-slug: backstage/backstage - openssf/project: backstage/backstage - backstage.io/source-location: url:https://github.com/backstage/backstage + openssf/baseUrl: https://api.securityscorecards.dev/projects/github.com/alizard0/rhdh-plugins spec: type: service - owner: guests - lifecycle: experimental + owner: group:development/guests + lifecycle: development \ No newline at end of file From 33a1f8ac3f79d98a6e5ec8b9b1c35a6c5b190236 Mon Sep 17 00:00:00 2001 From: Andre Lizardo Date: Tue, 17 Feb 2026 11:48:07 +0100 Subject: [PATCH 4/8] rename baseUrl to scorecardUrl --- .../scorecard/examples/openssf-scorecard-only.yaml | 2 +- .../src/clients/OpenSSFClient.test.ts | 6 +++--- .../src/clients/OpenSSFClient.ts | 13 +++++++++---- .../metricProviders/OpenSSFMetricProvider.test.ts | 6 +++--- .../src/metricProviders/OpenSSFMetricProvider.ts | 3 +-- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/workspaces/scorecard/examples/openssf-scorecard-only.yaml b/workspaces/scorecard/examples/openssf-scorecard-only.yaml index 5a4bb82142..e4640afd24 100644 --- a/workspaces/scorecard/examples/openssf-scorecard-only.yaml +++ b/workspaces/scorecard/examples/openssf-scorecard-only.yaml @@ -4,7 +4,7 @@ kind: Component metadata: name: openssf-scorecard annotations: - openssf/baseUrl: https://api.securityscorecards.dev/projects/github.com/alizard0/rhdh-plugins + openssf/scorecardUrl: https://api.securityscorecards.dev/projects/github.com/alizard0/rhdh-plugins spec: type: service owner: group:development/guests diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts index c5c4ebf857..7805d77e9e 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts @@ -22,14 +22,14 @@ import type { OpenSSFResponse } from './types'; const mockScorecardUrl = 'https://api.securityscorecards.dev/projects/github.com/owner/repo'; -function createEntity(baseUrl: string): Entity { +function createEntity(scorecardUrl: string): Entity { return { apiVersion: 'backstage.io/v1beta1', kind: 'Component', metadata: { name: 'my-service', annotations: { - 'openssf/baseUrl': baseUrl, + 'openssf/scorecardUrl': scorecardUrl, }, }, spec: {}, @@ -61,7 +61,7 @@ describe('OpenSSFClient', () => { }); describe('getScorecard', () => { - it('fetches the scorecard from the entity baseUrl', async () => { + it('fetches the scorecard from the entity scorecard URL', async () => { (globalThis.fetch as jest.Mock).mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue(mockOpenSSFResponse), diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts index 9606e7138a..2612758c0b 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts @@ -20,12 +20,17 @@ import { OpenSSFResponse } from './types'; export class OpenSSFClient { async getScorecard(entity: Entity): Promise { - const baseUrl = entity.metadata.annotations?.['openssf/baseUrl'] ?? ''; - if (!baseUrl || baseUrl.trim() === '' || !baseUrl.startsWith('https://')) { - throw new Error(`Invalid annotation 'openssf/baseUrl' value`); + const scorecardUrl = + entity.metadata.annotations?.['openssf/scorecardUrl'] ?? ''; + if ( + !scorecardUrl || + scorecardUrl.trim() === '' || + !scorecardUrl.startsWith('https://') + ) { + throw new Error(`Invalid annotation 'openssf/scorecardUrl' value`); } - const response = await fetch(baseUrl, { + const response = await fetch(scorecardUrl, { method: 'GET', headers: { Accept: 'application/json', diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts index 7143f747fd..766a116cfa 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts @@ -29,7 +29,7 @@ function createEntity(): Entity { kind: 'Component', metadata: { name: 'my-service', - annotations: { 'openssf/baseUrl': scorecardUrl }, + annotations: { 'openssf/scorecardUrl': scorecardUrl }, }, spec: {}, } as Entity; @@ -111,13 +111,13 @@ describe('OpenSSFMetricProvider', () => { expect(provider.getMetricThresholds()).toEqual(OPENSSF_THRESHOLDS); }); - it('requires openssf/baseUrl annotation in catalog filter', () => { + it('requires openssf/scorecardUrl annotation in catalog filter', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, ); expect(provider.getCatalogFilter()).toEqual({ - 'metadata.annotations.openssf/baseUrl': CATALOG_FILTER_EXISTS, + 'metadata.annotations.openssf/scorecardUrl': CATALOG_FILTER_EXISTS, }); }); }); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts index 1c39f12a8a..90024ad123 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts @@ -86,7 +86,7 @@ export class OpenSSFMetricProvider implements MetricProvider<'number'> { getCatalogFilter(): Record { return { - 'metadata.annotations.openssf/baseUrl': CATALOG_FILTER_EXISTS, + 'metadata.annotations.openssf/scorecardUrl': CATALOG_FILTER_EXISTS, }; } @@ -109,7 +109,6 @@ export class OpenSSFMetricProvider implements MetricProvider<'number'> { /** * Creates all default OpenSSF metric providers. - * @param clientOptions Optional base URL and git service host (from app-config) * @returns Array of OpenSSF metric providers */ export function createOpenSSFMetricProvider(): MetricProvider<'number'>[] { From 0956b65f864a0bc7d681dd992a9cc28b2efa2a3b Mon Sep 17 00:00:00 2001 From: Andre Lizardo Date: Wed, 18 Feb 2026 11:30:29 +0100 Subject: [PATCH 5/8] run yarn prettier fix --- workspaces/scorecard/examples/openssf-scorecard-only.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/scorecard/examples/openssf-scorecard-only.yaml b/workspaces/scorecard/examples/openssf-scorecard-only.yaml index e4640afd24..58078e212c 100644 --- a/workspaces/scorecard/examples/openssf-scorecard-only.yaml +++ b/workspaces/scorecard/examples/openssf-scorecard-only.yaml @@ -8,4 +8,4 @@ metadata: spec: type: service owner: group:development/guests - lifecycle: development \ No newline at end of file + lifecycle: development From 1927e76af8b88be5cba72f07cd25a29b3644654d Mon Sep 17 00:00:00 2001 From: Andre Lizardo Date: Thu, 19 Feb 2026 15:33:40 +0100 Subject: [PATCH 6/8] review work --- .../examples/openssf-scorecard-only.yaml | 4 +-- .../README.md | 33 +++++++++++++++---- .../src/annotations.ts | 19 +++++++++++ .../src/clients/OpenSSFClient.test.ts | 2 +- .../src/clients/OpenSSFClient.ts | 4 +-- .../OpenSSFMetricProvider.test.ts | 7 ++-- .../metricProviders/OpenSSFMetricProvider.ts | 2 +- 7 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/annotations.ts diff --git a/workspaces/scorecard/examples/openssf-scorecard-only.yaml b/workspaces/scorecard/examples/openssf-scorecard-only.yaml index 58078e212c..159911a579 100644 --- a/workspaces/scorecard/examples/openssf-scorecard-only.yaml +++ b/workspaces/scorecard/examples/openssf-scorecard-only.yaml @@ -2,9 +2,9 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: openssf-scorecard + name: openssf-scorecard-only annotations: - openssf/scorecardUrl: https://api.securityscorecards.dev/projects/github.com/alizard0/rhdh-plugins + openssf/scorecard-location: https://api.securityscorecards.dev/projects/github.com/alizard0/rhdh-plugins spec: type: service owner: group:development/guests diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/README.md b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/README.md index 79f3da5cd9..5a5947f313 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/README.md +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/README.md @@ -1,6 +1,6 @@ # Scorecard Backend Module for OpenSSF -Adds [OpenSSF Security Scorecard](https://securityscorecards.dev/) metrics to the Scorecard backend. Fetches scorecard data from the URL configured per component (`openssf/baseUrl`), so it can use the public API, a self-hosted endpoint, or any other scorecard source. Exposes 18 checks as Backstage metrics (scores 0–10). +Adds [OpenSSF Security Scorecard](https://securityscorecards.dev/) metrics to the Scorecard backend. Fetches scorecard data from the URL configured per component (`openssf/scorecard-location`), so it can use the public API, a self-hosted endpoint, or any other scorecard source. Exposes 18 checks as Backstage metrics (scores 0–10). Requires the [Scorecard backend plugin](../scorecard-backend/README.md) to be installed. @@ -30,16 +30,16 @@ backend.start(); ### Catalog (catalog-info.yaml) -| Annotation | Required | Description | -| ----------------- | -------- | --------------------------------------------------------------------------- | -| `openssf/baseUrl` | Yes | Full scorecard API URL for this component (e.g. public API or self-hosted). | +| Annotation | Required | Description | +| ---------------------------- | -------- | --------------------------------------------------------------------------- | +| `openssf/scorecard-location` | Yes | Full scorecard API URL for this component (e.g. public API or self-hosted). | Example: ```yaml metadata: annotations: - openssf/baseUrl: https://api.securityscorecards.dev/projects/github.com/owner/repo + openssf/scorecard-location: https://api.securityscorecards.dev/projects/github.com/owner/repo ``` ### Thresholds @@ -48,7 +48,28 @@ All OpenSSF metrics use fixed thresholds: **Error** <2, **Warning** 2–7, ** ## Metrics -18 metrics from [OpenSSF checks](https://github.com/ossf/scorecard/blob/main/docs/checks.md): `openssf.binary_artifacts`, `openssf.branch_protection`, `openssf.cii_best_practices`, `openssf.ci_tests`, `openssf.code_review`, `openssf.contributors`, `openssf.dangerous_workflow`, `openssf.dependency_update_tool`, `openssf.fuzzing`, `openssf.license`, `openssf.maintained`, `openssf.packaging`, `openssf.pinned_dependencies`, `openssf.sast`, `openssf.security_policy`, `openssf.signed_releases`, `openssf.token_permissions`, `openssf.vulnerabilities`. +18 metrics from [OpenSSF checks](https://github.com/ossf/scorecard/blob/main/docs/checks.md): + +| Metric | Description | +| -------------------------------- | ------------------------------------------------------------------------------------------- | +| `openssf.binary_artifacts` | No executable (binary) artifacts in the source repository. | +| `openssf.branch_protection` | Default and release branches protected (e.g. require review, status checks, no force push). | +| `openssf.cii_best_practices` | Project has an OpenSSF Best Practices badge (passing, silver, or gold). | +| `openssf.ci_tests` | Tests run before pull requests are merged. | +| `openssf.code_review` | Human code review required before PRs are merged. | +| `openssf.contributors` | Recent contributors from multiple organizations. | +| `openssf.dangerous_workflow` | GitHub Actions workflows avoid dangerous patterns (untrusted checkout, script injection). | +| `openssf.dependency_update_tool` | Dependency update tool in use (e.g. Dependabot, Renovate). | +| `openssf.fuzzing` | Fuzzing in use (e.g. OSS-Fuzz, ClusterFuzzLite, or language fuzz tests). | +| `openssf.license` | Project has a published license. | +| `openssf.maintained` | Project is actively maintained (not archived, recent activity). | +| `openssf.packaging` | Project is published as a package. | +| `openssf.pinned_dependencies` | Dependencies pinned (hash or fixed version) in build/release. | +| `openssf.sast` | Static application security testing (SAST) in use. | +| `openssf.security_policy` | Security policy present (e.g. SECURITY.md). | +| `openssf.signed_releases` | Releases are cryptographically signed. | +| `openssf.token_permissions` | GitHub Actions use minimal token permissions. | +| `openssf.vulnerabilities` | Known vulnerabilities in dependencies (lower score = more issues). | ## Troubleshooting diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/annotations.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/annotations.ts new file mode 100644 index 0000000000..d3a8c43c64 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/annotations.ts @@ -0,0 +1,19 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum OpenSSFAnnotations { + SCORECARD_LOCATION = 'openssf/scorecard-location', +} diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts index 7805d77e9e..e187146200 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts @@ -29,7 +29,7 @@ function createEntity(scorecardUrl: string): Entity { metadata: { name: 'my-service', annotations: { - 'openssf/scorecardUrl': scorecardUrl, + 'openssf/scorecard-location': scorecardUrl, }, }, spec: {}, diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts index 2612758c0b..ce7fecbfbb 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts @@ -21,13 +21,13 @@ import { OpenSSFResponse } from './types'; export class OpenSSFClient { async getScorecard(entity: Entity): Promise { const scorecardUrl = - entity.metadata.annotations?.['openssf/scorecardUrl'] ?? ''; + entity.metadata.annotations?.['openssf/scorecard-location'] ?? ''; if ( !scorecardUrl || scorecardUrl.trim() === '' || !scorecardUrl.startsWith('https://') ) { - throw new Error(`Invalid annotation 'openssf/scorecardUrl' value`); + throw new Error(`Invalid annotation 'openssf/scorecard-location' value`); } const response = await fetch(scorecardUrl, { diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts index 766a116cfa..4a0a76e845 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts @@ -29,7 +29,7 @@ function createEntity(): Entity { kind: 'Component', metadata: { name: 'my-service', - annotations: { 'openssf/scorecardUrl': scorecardUrl }, + annotations: { 'openssf/scorecard-location': scorecardUrl }, }, spec: {}, } as Entity; @@ -111,13 +111,14 @@ describe('OpenSSFMetricProvider', () => { expect(provider.getMetricThresholds()).toEqual(OPENSSF_THRESHOLDS); }); - it('requires openssf/scorecardUrl annotation in catalog filter', () => { + it('requires openssf/scorecard-location annotation in catalog filter', () => { const provider = new OpenSSFMetricProvider( maintainedConfig, OPENSSF_THRESHOLDS, ); expect(provider.getCatalogFilter()).toEqual({ - 'metadata.annotations.openssf/scorecardUrl': CATALOG_FILTER_EXISTS, + 'metadata.annotations.openssf/scorecard-location': + CATALOG_FILTER_EXISTS, }); }); }); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts index 90024ad123..6bed221dbb 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.ts @@ -86,7 +86,7 @@ export class OpenSSFMetricProvider implements MetricProvider<'number'> { getCatalogFilter(): Record { return { - 'metadata.annotations.openssf/scorecardUrl': CATALOG_FILTER_EXISTS, + 'metadata.annotations.openssf/scorecard-location': CATALOG_FILTER_EXISTS, }; } From e8416091093d748a164109a150b4f26a79390874 Mon Sep 17 00:00:00 2001 From: Andre Lizardo Date: Thu, 19 Feb 2026 16:29:53 +0100 Subject: [PATCH 7/8] rename variables to match scorecard-location changes --- .../src/clients/OpenSSFClient.test.ts | 10 +++++----- .../src/clients/OpenSSFClient.ts | 10 +++++----- .../src/metricProviders/OpenSSFMetricProvider.test.ts | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts index e187146200..7602b1ec0d 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.test.ts @@ -19,17 +19,17 @@ import type { Entity } from '@backstage/catalog-model'; import { OpenSSFClient } from './OpenSSFClient'; import type { OpenSSFResponse } from './types'; -const mockScorecardUrl = +const mockScorecardLocation = 'https://api.securityscorecards.dev/projects/github.com/owner/repo'; -function createEntity(scorecardUrl: string): Entity { +function createEntity(scorecardLocation: string): Entity { return { apiVersion: 'backstage.io/v1beta1', kind: 'Component', metadata: { name: 'my-service', annotations: { - 'openssf/scorecard-location': scorecardUrl, + 'openssf/scorecard-location': scorecardLocation, }, }, spec: {}, @@ -53,7 +53,7 @@ const mockOpenSSFResponse: OpenSSFResponse = { }; describe('OpenSSFClient', () => { - const entity = createEntity(mockScorecardUrl); + const entity = createEntity(mockScorecardLocation); beforeEach(() => { jest.clearAllMocks(); @@ -70,7 +70,7 @@ describe('OpenSSFClient', () => { const client = new OpenSSFClient(); const result = await client.getScorecard(entity); - expect(fetch).toHaveBeenCalledWith(mockScorecardUrl, { + expect(fetch).toHaveBeenCalledWith(mockScorecardLocation, { method: 'GET', headers: { Accept: 'application/json' }, }); diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts index ce7fecbfbb..bc8d1e0a2d 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/clients/OpenSSFClient.ts @@ -20,17 +20,17 @@ import { OpenSSFResponse } from './types'; export class OpenSSFClient { async getScorecard(entity: Entity): Promise { - const scorecardUrl = + const scorecardLocation = entity.metadata.annotations?.['openssf/scorecard-location'] ?? ''; if ( - !scorecardUrl || - scorecardUrl.trim() === '' || - !scorecardUrl.startsWith('https://') + !scorecardLocation || + scorecardLocation.trim() === '' || + !scorecardLocation.startsWith('https://') ) { throw new Error(`Invalid annotation 'openssf/scorecard-location' value`); } - const response = await fetch(scorecardUrl, { + const response = await fetch(scorecardLocation, { method: 'GET', headers: { Accept: 'application/json', diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts index 4a0a76e845..c81b18c364 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-openssf/src/metricProviders/OpenSSFMetricProvider.test.ts @@ -20,7 +20,7 @@ import type { Entity } from '@backstage/catalog-model'; import { OpenSSFMetricProvider } from './OpenSSFMetricProvider'; import { OPENSSF_THRESHOLDS } from './OpenSSFConfig'; -const scorecardUrl = +const scorecardLocation = 'https://api.securityscorecards.dev/projects/github.com/owner/repo'; function createEntity(): Entity { @@ -29,7 +29,7 @@ function createEntity(): Entity { kind: 'Component', metadata: { name: 'my-service', - annotations: { 'openssf/scorecard-location': scorecardUrl }, + annotations: { 'openssf/scorecard-location': scorecardLocation }, }, spec: {}, } as Entity; @@ -151,7 +151,7 @@ describe('OpenSSFMetricProvider', () => { const result = await provider.calculateMetric(entity); expect(result).toBe(8); - expect(fetch).toHaveBeenCalledWith(scorecardUrl, expect.any(Object)); + expect(fetch).toHaveBeenCalledWith(scorecardLocation, expect.any(Object)); }); it('throws when the check is not in the scorecard', async () => { From 7743382c1310519db2a3ca8d6191d0338bd83078 Mon Sep 17 00:00:00 2001 From: Andre Lizardo Date: Mon, 23 Feb 2026 11:13:46 +0100 Subject: [PATCH 8/8] review work --- workspaces/scorecard/examples/openssf-scorecard-only.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/scorecard/examples/openssf-scorecard-only.yaml b/workspaces/scorecard/examples/openssf-scorecard-only.yaml index 159911a579..b16a5cd821 100644 --- a/workspaces/scorecard/examples/openssf-scorecard-only.yaml +++ b/workspaces/scorecard/examples/openssf-scorecard-only.yaml @@ -8,4 +8,4 @@ metadata: spec: type: service owner: group:development/guests - lifecycle: development + lifecycle: experimental