Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions workspaces/scorecard/examples/openssf-scorecard-only.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
# 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
spec:
type: service
owner: guests
lifecycle: experimental
1 change: 1 addition & 0 deletions workspaces/scorecard/packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@red-hat-developer-hub/backstage-plugin-scorecard-backend": "workspace:^",
"@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-github": "workspace:^",
"@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira": "workspace:^",
"@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf": "workspace:^",
"app": "link:../app",
"better-sqlite3": "^9.0.0",
"node-gyp": "^10.0.0",
Expand Down
5 changes: 5 additions & 0 deletions workspaces/scorecard/packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,9 @@ backend.add(
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira'
),
);
backend.add(
import(
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf'
),
);
backend.start();
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# 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.

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

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

```ts
// packages/backend/src/index.ts
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

For the OpenSSF metrics to work, your catalog entities must have the required annotation:

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

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"

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}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf",
"version": "0.1.0",
"license": "Apache-2.0",
"private": true,
"description": "The openssf backend module for the scorecard plugin.",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/redhat-developer/rhdh-plugins",
"directory": "workspaces/scorecard/plugins/scorecard-backend-module-openssf"
},
"backstage": {
"role": "backend-plugin-module",
"pluginId": "scorecard",
"pluginPackage": "@red-hat-developer-hub/backstage-plugin-scorecard-backend"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-plugin-api": "^1.5.0",
"@backstage/catalog-client": "^1.12.1",
"@backstage/catalog-model": "^1.7.6",
"@red-hat-developer-hub/backstage-plugin-scorecard-common": "workspace:^",
"@red-hat-developer-hub/backstage-plugin-scorecard-node": "workspace:^"
},
"devDependencies": {
"@backstage/backend-test-utils": "^1.10.0",
"@backstage/cli": "^0.34.5"
},
"files": [
"dist"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## API Report File for "@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf"

> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).

```ts
import { BackendFeature } from '@backstage/backend-plugin-api';

// @public (undocumented)
const scorecardOpenSFFModule: BackendFeature;
export default scorecardOpenSFFModule;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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 { OpenSSFClient } from './OpenSSFClient';
import { OpenSSFResponse } from './types';

describe('OpenSSFClient', () => {
let client: OpenSSFClient;

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: [],
};

beforeEach(() => {
jest.clearAllMocks();
client = new OpenSSFClient();
globalThis.fetch = jest.fn();
});

describe('getScorecard', () => {
it('should return the scorecard', async () => {
// mocked fetch behaviour for the test
(globalThis.fetch as jest.Mock).mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue(mockOpenSSFResponse),
});

const scorecard = await client.getScorecard('owner', 'test');
expect(scorecard).toEqual(mockOpenSSFResponse);
});

it('should throw an error if the API returns a non-ok response', async () => {
// mock response from the API
(globalThis.fetch as jest.Mock).mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found',
});

await expect(client.getScorecard('wrong', 'test')).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
(globalThis.fetch as jest.Mock).mockRejectedValue(
new Error('API request failed'),
);

await expect(client.getScorecard('owner', 'test')).rejects.toThrow(
'API request failed',
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 { 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}`;

const response = await fetch(apiUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
},
});

if (!response.ok) {
throw new Error(
`OpenSSF API request failed with status ${response.status}: ${response.statusText}`,
);
}

const data: OpenSSFResponse = await response.json();

return data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 interface OpenSSFCheckDocumentation {
short: string;
url: string;
}

export interface OpenSSFCheck {
name: string;
score: number;
reason: string | null;
details: string[] | null;
documentation: OpenSSFCheckDocumentation;
}

export interface OpenSSFResponse {
date: string;
repo: {
name: string;
commit: string;
};
scorecard: {
version: string;
commit: string;
};
score: number;
checks: OpenSSFCheck[];
}
Loading