Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3cbb66b
Remove abstract metric providers, left only openssf metric provider a…
alizard0 Feb 10, 2026
4c4db46
fixed calculateMetric signature to receive entity as expected by the …
alizard0 Feb 10, 2026
b621e10
Merge branch 'main' into RHIDP-12106
alizard0 Feb 10, 2026
8d0afbd
add component for testing the new baseUrl annotation for openssf scor…
alizard0 Feb 11, 2026
faa1564
Merge branch 'main' into RHIDP-12106
alizard0 Feb 11, 2026
9741aa6
Merge branch 'main' into RHIDP-12106
alizard0 Feb 12, 2026
e563c26
Merge branch 'main' into RHIDP-12106
alizard0 Feb 16, 2026
58cfe83
Merge branch 'main' into RHIDP-12106
alizard0 Feb 17, 2026
33a1f8a
rename baseUrl to scorecardUrl
alizard0 Feb 17, 2026
a0535e8
Merge branch 'main' into RHIDP-12106
alizard0 Feb 17, 2026
7a175e1
Merge branch 'main' into RHIDP-12106
alizard0 Feb 18, 2026
0956b65
run yarn prettier fix
alizard0 Feb 18, 2026
333358e
Merge branch 'main' into RHIDP-12106
alizard0 Feb 19, 2026
f629382
Merge branch 'main' into RHIDP-12106
alizard0 Feb 19, 2026
0b78b07
Merge branch 'main' into RHIDP-12106
alizard0 Feb 19, 2026
1927e76
review work
alizard0 Feb 19, 2026
1b423d8
Merge branch 'main' into RHIDP-12106
alizard0 Feb 19, 2026
e841609
rename variables to match scorecard-location changes
alizard0 Feb 19, 2026
6ec1a34
Merge branch 'main' into RHIDP-12106
alizard0 Feb 23, 2026
1515a8e
Merge branch 'main' into RHIDP-12106
alizard0 Feb 23, 2026
7743382
review work
alizard0 Feb 23, 2026
dc2cbd5
Merge branch 'main' into RHIDP-12106
alizard0 Feb 23, 2026
25b6944
Merge branch 'main' into RHIDP-12106
alizard0 Feb 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions workspaces/scorecard/examples/openssf-scorecard-only.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
---
# Component with OpenSSF Scorecard
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: openssf-scorecard-only
annotations:
github.com/project-slug: backstage/backstage
openssf/project: backstage/backstage
backstage.io/source-location: url:https://github.com/backstage/backstage
openssf/scorecard-location: https://api.securityscorecards.dev/projects/github.com/alizard0/rhdh-plugins
spec:
type: service
owner: guests
owner: group:development/guests
lifecycle: experimental
Original file line number Diff line number Diff line change
@@ -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/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).

## 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
```

Expand All @@ -24,104 +15,62 @@ 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/scorecard-location` | 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/scorecard-location: 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. |
All OpenSSF metrics use fixed thresholds: **Error** &lt;2, **Warning** 2–7, **Success** &gt;7. Not configurable. See [threshold docs](../scorecard-backend/docs/thresholds.md).

## Metrics

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

### Metric shows "not found"

This can occur if:

- 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.

### 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:

```
https://api.securityscorecards.dev/projects/github.com/{owner}/{repo}
```
- **Metric "not found"**: Scorecard URL unreachable, repo not yet analyzed, or score outside 0–10.
Original file line number Diff line number Diff line change
@@ -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',
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,65 +14,92 @@
* 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 mockScorecardLocation =
'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(scorecardLocation: string): Entity {
return {
apiVersion: 'backstage.io/v1beta1',
kind: 'Component',
metadata: {
name: 'my-service',
annotations: {
'openssf/scorecard-location': scorecardLocation,
},
},
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(mockScorecardLocation);

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 scorecard URL', 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();
const result = await client.getScorecard(entity);

expect(fetch).toHaveBeenCalledWith(mockScorecardLocation, {
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();

await expect(client.getScorecard(entity)).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();

await expect(client.getScorecard(entity)).rejects.toThrow(
'Network error',
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,23 @@
* 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<OpenSSFResponse> {
const apiUrl = `${this.baseUrl}/${this.gitServiceHost}/${owner}/${repo}`;
async getScorecard(entity: Entity): Promise<OpenSSFResponse> {
const scorecardLocation =
entity.metadata.annotations?.['openssf/scorecard-location'] ?? '';
if (
!scorecardLocation ||
scorecardLocation.trim() === '' ||
!scorecardLocation.startsWith('https://')
) {
throw new Error(`Invalid annotation 'openssf/scorecard-location' value`);
}

const response = await fetch(apiUrl, {
const response = await fetch(scorecardLocation, {
method: 'GET',
headers: {
Accept: 'application/json',
Expand Down
Loading