Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 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
2f23269
add exclude metric for openssf
alizard0 Feb 17, 2026
d8eaa6f
Merge branch 'main' into RHIDP-12183
alizard0 Feb 25, 2026
0152c57
renamed baseUrl to scorecard-location
alizard0 Feb 25, 2026
aea0d64
rename annotation excludeChecks to exclude-checks
alizard0 Feb 25, 2026
7a7647a
Merge branch 'main' into RHIDP-12183
alizard0 Feb 25, 2026
b3b73b4
rollback component name to openssf-scorecard-only
alizard0 Feb 25, 2026
80a7a99
fix unit tests
alizard0 Feb 25, 2026
84b2172
fix the issue where the exclude-checks is compsoed by a string instea…
alizard0 Feb 25, 2026
dcf4d50
Merge branch 'main' into RHIDP-12183
alizard0 Feb 25, 2026
8da97ef
Merge branch 'main' into RHIDP-12183
alizard0 Feb 27, 2026
b2b7cb4
Merge branch 'main' into RHIDP-12183
alizard0 Mar 2, 2026
56c6abc
exclude metrics in PullMetricsByProviderTask instead
alizard0 Mar 2, 2026
de32169
Merge branch 'main' into RHIDP-12183
alizard0 Mar 2, 2026
5552d49
Merge branch 'main' into RHIDP-12183
alizard0 Mar 2, 2026
9ad2312
review work cicd
alizard0 Mar 2, 2026
69b6f05
Merge branch 'main' into RHIDP-12183
alizard0 Mar 3, 2026
9c2996a
Merge branch 'main' into RHIDP-12183
alizard0 Mar 4, 2026
c9afc8d
implemented exclude/include metrics using app-config and exclude usin…
alizard0 Mar 4, 2026
4dfeaa3
Merge branch 'main' into RHIDP-12183
alizard0 Mar 5, 2026
ad03413
Merge branch 'main' into RHIDP-12183
alizard0 Mar 6, 2026
2251333
Merge branch 'main' into RHIDP-12183
alizard0 Mar 6, 2026
05e836f
review work
alizard0 Mar 6, 2026
e72289a
review work
alizard0 Mar 6, 2026
c26593f
review work
alizard0 Mar 6, 2026
44897a8
Merge branch 'main' into RHIDP-12183
alizard0 Mar 9, 2026
9a6cd78
review work; moved isMetricIdExcluded to utils file instead
alizard0 Mar 9, 2026
05d221a
Merge branch 'main' into RHIDP-12183
alizard0 Mar 9, 2026
bcf8f38
Merge branch 'main' into RHIDP-12183
alizard0 Mar 10, 2026
f4f1407
review work
alizard0 Mar 12, 2026
edb19b6
Merge branch 'main' into RHIDP-12183
alizard0 Mar 12, 2026
2d077fb
add docs
alizard0 Mar 13, 2026
656ac13
Merge branch 'main' into RHIDP-12183
alizard0 Mar 13, 2026
0258d83
rename entityOverrides to entityAnnotations
alizard0 Mar 13, 2026
1031941
reviewed the isMetricIdDisabled rules
alizard0 Mar 13, 2026
5398602
review work
alizard0 Mar 13, 2026
973f9e2
Update workspaces/scorecard/plugins/scorecard-backend/config.d.ts
alizard0 Mar 13, 2026
a9a4759
review readme.md
alizard0 Mar 13, 2026
31fc075
added changesets
alizard0 Mar 13, 2026
779e2ca
moved docs/disabled-metrics-logic.md to scorecard-backend/docs
alizard0 Mar 13, 2026
3cab394
Merge branch 'main' into RHIDP-12183
alizard0 Mar 13, 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
5 changes: 5 additions & 0 deletions workspaces/scorecard/.changeset/full-streets-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf': patch
---

rework openssf client
5 changes: 5 additions & 0 deletions workspaces/scorecard/.changeset/polite-sheep-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-scorecard-backend': minor
---

added disable metric feature to the pull metric task
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ metadata:
name: openssf-scorecard-only
annotations:
openssf/scorecard-location: https://api.securityscorecards.dev/projects/github.com/alizard0/rhdh-plugins
scorecard.io/disabled-metrics: openssf.maintained
spec:
type: service
owner: group:development/guests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ const mockOpenSSFResponse: OpenSSFResponse = {
details: null,
documentation: { short: '', url: '' },
},
{
name: 'Code-Review',
score: 9,
reason: null,
details: null,
documentation: { short: '', url: '' },
},
],
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ export class OpenSSFClient {
);
}

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

return data;
return await response.json();
}
}
4 changes: 4 additions & 0 deletions workspaces/scorecard/plugins/scorecard-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ To use these providers, install the corresponding backend modules:
- Jira: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-jira`](../scorecard-backend-module-jira/README.md)
- OpenSSF: [`@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-openssf`](../scorecard-backend-module-openssf/README.md)

### Disabling Metrics

Administrators can disable metric checks globally via app-config, and users can disable them for concrete entity via `scorecard.io/disabled-metrics` annotation. For more details, see [disabled-metrics-logic.md](./docs/disabled-metrics-logic.md).

## Thresholds

Thresholds define conditions to assign metric values to specific visual categories (`success`, `warning`, `error` or any custom category). The Scorecard plugin provides multiple ways to configure thresholds:
Expand Down
12 changes: 12 additions & 0 deletions workspaces/scorecard/plugins/scorecard-backend/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ export interface Config {
scorecard?: {
/** Number of days to retain metric data in the database. Older data will be automatically cleaned up. Default: 365 days */
dataRetentionDays?: number;
/** List of metric IDs (e.g. openssf.packaging) that are disabled globally. Entity annotations cannot override this. */
disabledMetrics?: string[];
/** Control whether users can override behavior via entity annotations. */
entityAnnotations?: {
/** Whether entity scorecard.io/disabled-metrics annotation can override. Only affects annotations; global disabledMetrics is unchanged. */
disabledMetrics?: {
/** If true (default), entities can disable metrics that are not mentioned in `except` list via `scorecard.io/disabled-metrics` annotation; if false, the annotation has no effect */
enabled?: boolean;
/** When enabled is true: entity annotations cannot disable metric IDs listed here (these checks always run). */
except?: string[];
};
};
/** Configuration for scorecard metric providers */
plugins?: {
/** Configuration for datasource */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Disabled metrics: when a metric check is skipped (not executed)

Administrators can disable metric checks globally via app-config, and users can disable them for concrete entity via `scorecard.io/disabled-metrics` annotation. Administrators control whether this annotation is honored in app-config via `scorecard.entityAnnotations.disabledMetrics.enabled` (and can force specific checks to always run via `scorecard.entityAnnotations.disabledMetrics.except`). When a metric check is skipped, no data is fetched and the metric is not calculated.
**Evaluation order:** `scorecard.disabledMetrics` is checked first. If the metric ID is in that list, the metric check is always skipped and the rest is ignored. Otherwise, `entityAnnotations.disabledMetrics` in app-config and the entity annotations are applied.
The following table describes the result for each combination of app-config and entity annotation.
| `scorecard.disabledMetrics` includes `metricId` | `entityAnnotations.disabledMetrics.enabled` | `entityAnnotations.disabledMetrics.except` | `scorecard.io/disabled-metrics` entity annotation has `metricId`? | Metric check skipped for `metricId` (not run) |
| ----------------------------------------------- | ------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------- |
| Yes | — | — | — | **Yes** |
| No | `false` | ignored | No | **No** |
| No | `false` | ignored | Yes | **No** (annotation ignored, forced to run) |
| No | `true` (or unset) | unset / empty / does not include `metricId` | No | **No** |
| No | `true` (or unset) | unset / empty / does not include `metricId` | Yes | **Yes** |
| No | `true` (or unset) | includes `metricId` | No | **No** |
| No | `true` (or unset) | includes `metricId` | Yes | **No** (annotation ignored, forced to run) |

## Summary

- **`scorecard.disabledMetrics`**
If the metric ID is in this list, the metric check is always skipped (not executed). Entity annotations cannot override.
- **`entityAnnotations.disabledMetrics.enabled = false`**
Users cannot disable metrics by `scorecard.io/disabled-metrics` annotation.
The `except` list is not used.
- **`entityAnnotations.disabledMetrics.enabled = true`**
Users can disable metrics by `scorecard.io/disabled-metrics` annotation. When the key is absent, behavior is the same as `true`.
The `except` list applies: metric IDs in `except` cannot have their checks skipped by annotation (they always run). Metrics not in `except` can have their checks skipped by the entity annotation.
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,36 @@ describe('PullMetricsByProviderTask', () => {
);
});

it('should skip entities when scorecard.io/disabled-metrics annotation contains the provider id', async () => {
const entityExcluded = {
apiVersion: '1.0.0',
kind: 'Component',
metadata: {
name: 'excluded-entity',
annotations: {
'scorecard.io/disabled-metrics': 'github.test_metric',
},
},
};

mockCatalog.queryEntities.mockReset().mockResolvedValueOnce({
items: [entityExcluded],
pageInfo: { nextCursor: undefined },
totalItems: 2,
});

const calculateMetricSpy = jest.spyOn(mockProvider, 'calculateMetric');
const createMetricValuesSpy = jest.spyOn(
mockDatabaseMetricValues,
'createMetricValues',
);
await (task as any).pullProviderMetrics(mockProvider, mockLogger);

expect(calculateMetricSpy).not.toHaveBeenCalled();
expect(createMetricValuesSpy).toHaveBeenCalledTimes(1);
expect(createMetricValuesSpy).toHaveBeenCalledWith([]);
});

it('should throw error if pullProviderMetrics fails', async () => {
(task as any).pullProviderMetrics = jest
.fn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { Config } from '@backstage/config';
import { CatalogService } from '@backstage/plugin-catalog-node';
import { MetricProvider } from '@red-hat-developer-hub/backstage-plugin-scorecard-node';
import { mergeEntityAndProviderThresholds } from '../../utils/mergeEntityAndProviderThresholds';
import { isMetricIdDisabled } from '../../utils/metricUtils';
import { normalizeOwnerRef } from '../../utils/normalizeOwnerRef';
import { v4 as uuid } from 'uuid';
import { stringifyEntityRef } from '@backstage/catalog-model';
Expand Down Expand Up @@ -143,6 +144,17 @@ export class PullMetricsByProviderTask implements SchedulerTask {
let value: MetricValue | undefined;

try {
if (
isMetricIdDisabled(
this.config,
provider.getProviderId(),
entity,
logger,
)
) {
return undefined;
}

value = await provider.calculateMetric(entity);

const thresholds = mergeEntityAndProviderThresholds(
Expand Down Expand Up @@ -184,7 +196,7 @@ export class PullMetricsByProviderTask implements SchedulerTask {
}),
).then(promises =>
promises.reduce((acc, curr) => {
if (curr.status === 'fulfilled') {
if (curr.status === 'fulfilled' && curr.value !== undefined) {
return [...acc, curr.value];
}
return acc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const thresholdRulesAnnotationPrefix = (providerId: string) =>
/**
* Extract threshold override rules from entity annotations for a given provider, doesn't validate rules.
*/
function parseEntityOverrideThresholds(
function parseEntityAnnotationThresholds(
entity: Entity,
providerId: string,
): ThresholdRule[] {
Expand All @@ -57,13 +57,13 @@ export function mergeEntityAndProviderThresholds(
const providerId = provider.getProviderId();
const providerThresholds = provider.getMetricThresholds();
const providerMetricType = provider.getMetricType();
const entityOverrideThresholds = parseEntityOverrideThresholds(
const entityAnnotationThresholds = parseEntityAnnotationThresholds(
entity,
providerId,
);

const mergedRules = [...providerThresholds.rules];
for (const override of entityOverrideThresholds) {
for (const override of entityAnnotationThresholds) {
const foundKey = mergedRules.findIndex(rule => rule.key === override.key);
if (foundKey === -1) {
throw new ThresholdConfigFormatError(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/*
* 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 { mockServices } from '@backstage/backend-test-utils';
import { MockEntityBuilder } from '../../__fixtures__/mockEntityBuilder';
import { isMetricIdDisabled } from './metricUtils';

describe('isMetricIdDisabled', () => {
const metricId = 'openssf.maintained';
let mockLogger: ReturnType<typeof mockServices.logger.mock>;

function createConfig(
scorecardOverrides: {
disabledMetrics?: string[];
entityAnnotations?: {
disabledMetrics?: { enabled?: boolean; except?: string[] };
};
} = {},
) {
return mockServices.rootConfig({
data: {
scorecard: {
...scorecardOverrides,
},
},
});
}

function createEntity(annotationValue?: string) {

Check warning on line 42 in workspaces/scorecard/plugins/scorecard-backend/src/utils/metricUtils.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move function 'createEntity' to the outer scope.

See more on https://sonarcloud.io/project/issues?id=redhat-developer_rhdh-plugins&issues=AZzSuJjsd1orA-MjABIH&open=AZzSuJjsd1orA-MjABIH&pullRequest=2393
return new MockEntityBuilder()
.withMetadata({
name: 'test-entity',
namespace: 'default',
...(annotationValue !== undefined && {
annotations: {
'scorecard.io/disabled-metrics': annotationValue,
},
}),
})
.build();
}

beforeEach(() => {
mockLogger = mockServices.logger.mock();
});

afterEach(() => {
jest.clearAllMocks();
});

it('returns true when metric is in app-config disabledMetrics', () => {
const config = createConfig({ disabledMetrics: [metricId] });
const entity = createEntity();

const result = isMetricIdDisabled(config, metricId, entity, mockLogger);

expect(result).toBe(true);
});

it('returns true when disabled by annotation (no except list)', () => {
const config = createConfig();
const entity = createEntity(metricId);

const result = isMetricIdDisabled(config, metricId, entity, mockLogger);

expect(result).toBe(true);
});

it('returns false when not disabled by app-config or annotation', () => {
const config = createConfig();
const entity = createEntity();

const result = isMetricIdDisabled(config, metricId, entity, mockLogger);

expect(result).toBe(false);
});

it('returns false when no config and no annotation', () => {
const config = createConfig();
const entity = createEntity();

const result = isMetricIdDisabled(config, metricId, entity, mockLogger);

expect(result).toBe(false);
});

it('When entityOverride.disabledMetrics.enabled=false, users can NO override by annotations.', () => {
const config = createConfig({
entityAnnotations: {
disabledMetrics: {
enabled: false,
except: [metricId],
},
},
});

const entity = createEntity(metricId);
const result = isMetricIdDisabled(config, metricId, entity, mockLogger);

expect(result).toBe(false);
});

it('returns true, when entityOverride.disabledMetrics.enabled=true, users can override by annotations, and metric is not listed in exception list', () => {
const config = createConfig({
entityAnnotations: {
disabledMetrics: {
enabled: false,
except: ['other-metric-id'],
},
},
});

const entity = createEntity(metricId);
const result = isMetricIdDisabled(config, metricId, entity, mockLogger);

expect(result).toBe(false);
});

it('returns false when disabled by annotation but metric is in entityAnnotations.disabledMetrics.except but entityAnnotations.disabledMetrics.enabled is true', () => {
const config = createConfig({
entityAnnotations: {
disabledMetrics: {
enabled: true,
except: [metricId],
},
},
});

const entity = createEntity(metricId);
const result = isMetricIdDisabled(config, metricId, entity, mockLogger);

expect(result).toBe(false);
});

it('returns true when disabled by annotation but metric is NOT in entityAnnotations.disabledMetrics.except but entityAnnotations.disabledMetrics.enabled is true', () => {
const config = createConfig({
entityAnnotations: {
disabledMetrics: {
enabled: true,
except: [],
},
},
});

const entity = createEntity(metricId);
const result = isMetricIdDisabled(config, metricId, entity, mockLogger);

expect(result).toBe(true);
});
});
Loading
Loading