Skip to content

Commit 1e259ba

Browse files
AradTraubclaude
andcommitted
CM-71730: Report the OSSF Maintained check score, not the aggregate
The unmaintained policy decides on the scorecard's Maintained check, so showing the aggregate as the only number was misleading: a package flagged as unmaintained could display a healthy-looking 4.1. The Maintained score now leads and the aggregate stays as context. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent b5e57bc commit 1e259ba

7 files changed

Lines changed: 65 additions & 19 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,7 @@ In the previous example, if you wanted to only scan a branch named `dev`, you co
872872
> [!NOTE]
873873
> This option is only available to SCA scans.
874874

875-
To scan only for unmaintained packages (packages whose [OpenSSF Scorecard](https://scorecard.dev) score is low), add the argument `--sca-scan unmaintained-packages` following the `-t sca` or `--scan-type sca` option.
875+
To scan only for unmaintained packages (packages whose [OpenSSF Scorecard](https://scorecard.dev) `Maintained` check is low, meaning little or no recent commit and issue activity), add the argument `--sca-scan unmaintained-packages` following the `-t sca` or `--scan-type sca` option.
876876

877877
> [!NOTE]
878878
> Whether unmaintained packages are reported at all is controlled by your organization's policy. This option narrows what a scan reports; it cannot enable a policy that is turned off for your tenant.

cycode/cli/printers/rich_printer.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
from cycode.cli.printers.utils.detection_ordering.common_ordering import sort_and_group_detections_from_scan_result
1818
from cycode.cli.printers.utils.rich_helpers import get_columns_in_1_to_3_ratio, get_markdown_panel, get_panel
19-
from cycode.cli.printers.utils.sca_ossf import get_ossf_report_url, get_ossf_score
19+
from cycode.cli.printers.utils.sca_ossf import get_maintained_score, get_ossf_report_url, get_ossf_score
2020

2121
if TYPE_CHECKING:
2222
from cycode.cli.models import CliError, Detection, Document, LocalScanResult
@@ -99,7 +99,9 @@ def __add_sca_scan_related_rows(details_table: Table, detection: 'Detection') ->
9999
details_table.add_row('Dependency path', dependency_path or 'N/A')
100100

101101
if detection.detection_type_id == consts.UNMAINTAINED_PACKAGE_POLICY_ID:
102+
maintained_score = get_maintained_score(detection_details)
102103
ossf_score = get_ossf_score(detection_details)
104+
details_table.add_row('Maintained score', 'N/A' if maintained_score is None else str(maintained_score))
103105
details_table.add_row('OSSF Scorecard score', 'N/A' if ossf_score is None else str(ossf_score))
104106
details_table.add_row('Scorecard report', get_ossf_report_url(detection_details) or 'N/A')
105107
elif not detection.has_alert:

cycode/cli/printers/tables/sca_table_printer.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from cycode.cli.printers.tables.table_printer_base import TablePrinterBase
1414
from cycode.cli.printers.utils import is_git_diff_based_scan
1515
from cycode.cli.printers.utils.detection_ordering.sca_ordering import sort_and_group_detections
16-
from cycode.cli.printers.utils.sca_ossf import get_ossf_score
16+
from cycode.cli.printers.utils.sca_ossf import get_maintained_score
1717
from cycode.cli.utils.string_utils import shortcut_dependency_paths
1818

1919
if TYPE_CHECKING:
@@ -28,7 +28,7 @@
2828
ECOSYSTEM_COLUMN = column_builder.build(name='Ecosystem', highlight=False)
2929
PACKAGE_COLUMN = column_builder.build(name='Package', highlight=False)
3030
CVE_COLUMNS = column_builder.build(name='CVE', highlight=False)
31-
OSSF_SCORE_COLUMN = column_builder.build(name='OSSF Score', highlight=False)
31+
MAINTAINED_SCORE_COLUMN = column_builder.build(name='Maintained Score', highlight=False)
3232
DEPENDENCY_PATHS_COLUMN = column_builder.build(name='Dependency Paths')
3333
UPGRADE_COLUMN = column_builder.build(name='Upgrade')
3434
LICENSE_COLUMN = column_builder.build(name='License', highlight=False)
@@ -71,7 +71,7 @@ def _get_table(self, policy_id: str) -> Table:
7171
elif policy_id == LICENSE_COMPLIANCE_POLICY_ID:
7272
table.add_column(LICENSE_COLUMN)
7373
elif policy_id == UNMAINTAINED_PACKAGE_POLICY_ID:
74-
table.add_column(OSSF_SCORE_COLUMN)
74+
table.add_column(MAINTAINED_SCORE_COLUMN)
7575

7676
if is_git_diff_based_scan(self.command_scan_type):
7777
table.add_column(REPOSITORY_COLUMN)
@@ -130,8 +130,8 @@ def _enrich_table_with_values(table: Table, detection: Detection) -> None:
130130
table.add_cell(CVE_COLUMNS, detection_details.get('vulnerability_id'))
131131
table.add_cell(LICENSE_COLUMN, detection_details.get('license'))
132132

133-
ossf_score = get_ossf_score(detection_details)
134-
table.add_cell(OSSF_SCORE_COLUMN, 'N/A' if ossf_score is None else str(ossf_score))
133+
maintained_score = get_maintained_score(detection_details)
134+
table.add_cell(MAINTAINED_SCORE_COLUMN, 'N/A' if maintained_score is None else str(maintained_score))
135135

136136
def _print_summary_issues(self, detections_count: int, title: str) -> None:
137137
self.console.print(f'[bold]Cycode found {detections_count} violations of type: [cyan]{title}[/]')

cycode/cli/printers/text_printer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from cycode.cli.printers.utils.code_snippet_syntax import get_code_snippet_syntax, get_detection_line
88
from cycode.cli.printers.utils.detection_data import get_detection_title
99
from cycode.cli.printers.utils.detection_ordering.common_ordering import sort_and_group_detections_from_scan_result
10-
from cycode.cli.printers.utils.sca_ossf import get_ossf_report_url, get_ossf_score
10+
from cycode.cli.printers.utils.sca_ossf import get_maintained_score, get_ossf_report_url, get_ossf_score
1111

1212
if TYPE_CHECKING:
1313
from cycode.cli.models import Detection, LocalScanResult
@@ -86,10 +86,13 @@ def __get_sca_related_summary_lines(detection: 'Detection') -> list[str]:
8686
summary_lines = []
8787

8888
if detection.detection_type_id == consts.UNMAINTAINED_PACKAGE_POLICY_ID:
89+
maintained_score = get_maintained_score(detection.detection_details)
8990
ossf_score = get_ossf_score(detection.detection_details)
91+
maintained = 'N/A' if maintained_score is None else maintained_score
9092
score = 'N/A' if ossf_score is None else ossf_score
9193
report_url = get_ossf_report_url(detection.detection_details) or 'N/A'
9294

95+
summary_lines.append(f'Maintained score: [cyan]{maintained}[/]\n')
9396
summary_lines.append(f'OSSF Scorecard score: [cyan]{score}[/]\n')
9497
summary_lines.append(f'Scorecard report: [cyan]{report_url}[/]\n')
9598
elif detection.has_alert:

cycode/cli/printers/utils/sca_ossf.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from typing import Any, Optional
22

3+
_MAINTAINED_CHECK_NAME = 'maintained'
4+
35

46
def _get_ossf_details(detection_details: dict) -> dict:
57
return detection_details.get('ossf') or {}
@@ -11,3 +13,15 @@ def get_ossf_score(detection_details: dict) -> Optional[Any]:
1113

1214
def get_ossf_report_url(detection_details: dict) -> Optional[str]:
1315
return _get_ossf_details(detection_details).get('scorecard_report_url')
16+
17+
18+
def get_maintained_score(detection_details: dict) -> Optional[Any]:
19+
"""Score of the scorecard's Maintained check, which is what the unmaintained policy decides on.
20+
21+
The aggregate score blends unrelated checks, so it is not the number the violation was raised for.
22+
"""
23+
for check in _get_ossf_details(detection_details).get('checks') or []:
24+
if str(check.get('name', '')).lower() == _MAINTAINED_CHECK_NAME:
25+
return check.get('score')
26+
27+
return None

tests/cli/printers/test_sca_table_printer.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from cycode.cli.printers.tables.sca_table_printer import (
1212
CVE_COLUMNS,
1313
LICENSE_COLUMN,
14-
OSSF_SCORE_COLUMN,
14+
MAINTAINED_SCORE_COLUMN,
1515
UPGRADE_COLUMN,
1616
ScaTablePrinter,
1717
)
@@ -53,7 +53,7 @@ def test_get_title_unknown_policy() -> None:
5353
def test_get_table_unmaintained_packages_columns(printer: ScaTablePrinter) -> None:
5454
columns = printer._get_table(UNMAINTAINED_PACKAGE_POLICY_ID).get_columns_info()
5555

56-
assert OSSF_SCORE_COLUMN in columns
56+
assert MAINTAINED_SCORE_COLUMN in columns
5757
assert CVE_COLUMNS not in columns
5858
assert UPGRADE_COLUMN not in columns
5959
assert LICENSE_COLUMN not in columns
@@ -67,16 +67,16 @@ def test_get_table_unmaintained_packages_column_order(printer: ScaTablePrinter)
6767
'Code Project',
6868
'Ecosystem',
6969
'Package',
70-
'OSSF Score',
70+
'Maintained Score',
7171
'Dependency Paths',
7272
'Direct Dependency',
7373
'Development Dependency',
7474
]
7575

7676

7777
def test_get_table_other_policies_do_not_get_the_score_column(printer: ScaTablePrinter) -> None:
78-
assert OSSF_SCORE_COLUMN not in printer._get_table(PACKAGE_VULNERABILITY_POLICY_ID).get_columns_info()
79-
assert OSSF_SCORE_COLUMN not in printer._get_table(LICENSE_COMPLIANCE_POLICY_ID).get_columns_info()
78+
assert MAINTAINED_SCORE_COLUMN not in printer._get_table(PACKAGE_VULNERABILITY_POLICY_ID).get_columns_info()
79+
assert MAINTAINED_SCORE_COLUMN not in printer._get_table(LICENSE_COMPLIANCE_POLICY_ID).get_columns_info()
8080

8181

8282
def test_enrich_table_with_values_populates_the_score(printer: ScaTablePrinter) -> None:
@@ -87,13 +87,17 @@ def test_enrich_table_with_values_populates_the_score(printer: ScaTablePrinter)
8787
ecosystem='npm',
8888
package_name='left-pad',
8989
package_version='1.0.0',
90-
ossf={'score': 1.5, 'scorecard_report_url': 'https://scorecard.dev/viewer/?uri=github.com/example/left-pad'},
90+
ossf={
91+
'score': 4.1,
92+
'scorecard_report_url': 'https://scorecard.dev/viewer/?uri=github.com/example/left-pad',
93+
'checks': [{'name': 'Maintained', 'score': 1.5, 'reason': 'no recent activity'}],
94+
},
9195
)
9296

9397
ScaTablePrinter._enrich_table_with_values(table, detection)
9498

9599
row = table.get_rows()[0]
96-
score_index = table.get_columns_info().index(OSSF_SCORE_COLUMN)
100+
score_index = table.get_columns_info().index(MAINTAINED_SCORE_COLUMN)
97101
assert row[score_index] == '1.5'
98102

99103

@@ -104,5 +108,5 @@ def test_enrich_table_with_values_missing_score(printer: ScaTablePrinter) -> Non
104108
ScaTablePrinter._enrich_table_with_values(table, detection)
105109

106110
row = table.get_rows()[0]
107-
score_index = table.get_columns_info().index(OSSF_SCORE_COLUMN)
111+
score_index = table.get_columns_info().index(MAINTAINED_SCORE_COLUMN)
108112
assert row[score_index] == 'N/A'

tests/cli/printers/test_text_printer.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,18 @@ def _render(printer: TextPrinter, output: io.StringIO, detection: Detection) ->
5858
def test_unmaintained_package_prints_the_score_and_report(printer: TextPrinter, output: io.StringIO) -> None:
5959
detection = _make_detection(
6060
UNMAINTAINED_PACKAGE_POLICY_ID,
61-
ossf={'score': 2.1, 'scorecard_report_url': 'https://scorecard.dev/viewer/?uri=github.com/a/b'},
61+
ossf={
62+
'score': 4.1,
63+
'scorecard_report_url': 'https://scorecard.dev/viewer/?uri=github.com/a/b',
64+
'checks': [{'name': 'Maintained', 'score': 0, 'reason': '0 commit(s) in the last 90 days'}],
65+
},
6266
)
6367

6468
result = _render(printer, output, detection)
6569

66-
assert 'OSSF Scorecard score: 2.1' in result
70+
# The Maintained check is what the policy fired on; the aggregate stays as context.
71+
assert 'Maintained score: 0' in result
72+
assert 'OSSF Scorecard score: 4.1' in result
6773
assert 'Scorecard report: https://scorecard.dev/viewer/?uri=github.com/a/b' in result
6874
assert 'License' not in result
6975

@@ -73,19 +79,36 @@ def test_unmaintained_package_without_ossf_details(printer: TextPrinter, output:
7379

7480
result = _render(printer, output, detection)
7581

82+
assert 'Maintained score: N/A' in result
7683
assert 'OSSF Scorecard score: N/A' in result
7784
assert 'Scorecard report: N/A' in result
7885

7986

8087
def test_unmaintained_package_with_zero_score(printer: TextPrinter, output: io.StringIO) -> None:
81-
detection = _make_detection(UNMAINTAINED_PACKAGE_POLICY_ID, ossf={'score': 0, 'scorecard_report_url': ''})
88+
detection = _make_detection(
89+
UNMAINTAINED_PACKAGE_POLICY_ID,
90+
ossf={'score': 0, 'scorecard_report_url': '', 'checks': [{'name': 'Maintained', 'score': 0}]},
91+
)
8292

8393
result = _render(printer, output, detection)
8494

95+
assert 'Maintained score: 0' in result
8596
assert 'OSSF Scorecard score: 0' in result
8697
assert 'Scorecard report: N/A' in result
8798

8899

100+
def test_unmaintained_package_without_maintained_check(printer: TextPrinter, output: io.StringIO) -> None:
101+
detection = _make_detection(
102+
UNMAINTAINED_PACKAGE_POLICY_ID,
103+
ossf={'score': 1.5, 'checks': [{'name': 'License', 'score': 10}]},
104+
)
105+
106+
result = _render(printer, output, detection)
107+
108+
assert 'Maintained score: N/A' in result
109+
assert 'OSSF Scorecard score: 1.5' in result
110+
111+
89112
def test_license_compliance_still_prints_the_license(printer: TextPrinter, output: io.StringIO) -> None:
90113
detection = _make_detection(LICENSE_COMPLIANCE_POLICY_ID, license='GPL-3.0')
91114

0 commit comments

Comments
 (0)