Skip to content

Commit 992c171

Browse files
AradTraubclaude
andcommitted
CM-71730: Key SCA detail rows by policy instead of chaining conditions
The printers picked their rows from an if/elif chain over has_alert, which is only a guess at the policy and has to grow a branch for every new one. It had already gone wrong once: unmaintained packages have no alert, so they fell to the license branch and rendered an empty License row. Each policy now contributes its own labelled fields from one map that both printers render, and a policy with no entry contributes nothing rather than borrowing another's fields. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 9d85b21 commit 992c171

4 files changed

Lines changed: 137 additions & 36 deletions

File tree

cycode/cli/printers/rich_printer.py

Lines changed: 3 additions & 13 deletions
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_maintained_score, get_ossf_report_url, get_ossf_score
19+
from cycode.cli.printers.utils.sca_policy_details import get_sca_policy_details
2020

2121
if TYPE_CHECKING:
2222
from cycode.cli.models import CliError, Detection, Document, LocalScanResult
@@ -91,21 +91,11 @@ def __add_sca_scan_related_rows(details_table: Table, detection: 'Detection') ->
9191
details_table.add_row('Package', detection_details.get('package_name'))
9292
details_table.add_row('Version', detection_details.get('package_version'))
9393

94-
if detection.has_alert:
95-
patched_version = detection_details['alert'].get('first_patched_version')
96-
details_table.add_row('First patched version', patched_version or 'Not fixed')
97-
9894
dependency_path = detection_details.get('dependency_paths')
9995
details_table.add_row('Dependency path', dependency_path or 'N/A')
10096

101-
if detection.detection_type_id == consts.UNMAINTAINED_PACKAGE_POLICY_ID:
102-
maintained_score = get_maintained_score(detection_details)
103-
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))
105-
details_table.add_row('OSSF Scorecard score', 'N/A' if ossf_score is None else str(ossf_score))
106-
details_table.add_row('Scorecard report', get_ossf_report_url(detection_details) or 'N/A')
107-
elif not detection.has_alert:
108-
details_table.add_row('License', detection_details.get('license'))
97+
for label, value in get_sca_policy_details(detection):
98+
details_table.add_row(label, value)
10999

110100
@staticmethod
111101
def __add_iac_scan_related_rows(details_table: Table, detection: 'Detection') -> None:

cycode/cli/printers/text_printer.py

Lines changed: 2 additions & 23 deletions
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_maintained_score, get_ossf_report_url, get_ossf_score
10+
from cycode.cli.printers.utils.sca_policy_details import get_sca_policy_details
1111

1212
if TYPE_CHECKING:
1313
from cycode.cli.models import Detection, LocalScanResult
@@ -83,28 +83,7 @@ def __get_intermediate_summary_lines(self, detection: 'Detection') -> list[str]:
8383

8484
@staticmethod
8585
def __get_sca_related_summary_lines(detection: 'Detection') -> list[str]:
86-
summary_lines = []
87-
88-
if detection.detection_type_id == consts.UNMAINTAINED_PACKAGE_POLICY_ID:
89-
maintained_score = get_maintained_score(detection.detection_details)
90-
ossf_score = get_ossf_score(detection.detection_details)
91-
maintained = 'N/A' if maintained_score is None else maintained_score
92-
score = 'N/A' if ossf_score is None else ossf_score
93-
report_url = get_ossf_report_url(detection.detection_details) or 'N/A'
94-
95-
summary_lines.append(f'Maintained score: [cyan]{maintained}[/]\n')
96-
summary_lines.append(f'OSSF Scorecard score: [cyan]{score}[/]\n')
97-
summary_lines.append(f'Scorecard report: [cyan]{report_url}[/]\n')
98-
elif detection.has_alert:
99-
patched_version = detection.detection_details['alert'].get('first_patched_version')
100-
patched_version = patched_version or 'Not fixed'
101-
102-
summary_lines.append(f'First patched version: [cyan]{patched_version}[/]\n')
103-
else:
104-
package_license = detection.detection_details.get('license', 'N/A')
105-
summary_lines.append(f'License: [cyan]{package_license}[/]\n')
106-
107-
return summary_lines
86+
return [f'{label}: [cyan]{value}[/]\n' for label, value in get_sca_policy_details(detection)]
10887

10988
def __print_detection_code_segment(self, detection: 'Detection', document: Document) -> None:
11089
self.console.print(
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from typing import TYPE_CHECKING, Callable
2+
3+
from cycode.cli.consts import (
4+
LICENSE_COMPLIANCE_POLICY_ID,
5+
PACKAGE_VULNERABILITY_POLICY_ID,
6+
UNMAINTAINED_PACKAGE_POLICY_ID,
7+
)
8+
from cycode.cli.printers.utils.sca_ossf import get_maintained_score, get_ossf_report_url, get_ossf_score
9+
10+
if TYPE_CHECKING:
11+
from cycode.cyclient.models import Detection
12+
13+
_NOT_AVAILABLE = 'N/A'
14+
15+
16+
def _package_vulnerability_details(detection: 'Detection') -> list[tuple[str, str]]:
17+
alert = detection.detection_details.get('alert') or {}
18+
return [('First patched version', alert.get('first_patched_version') or 'Not fixed')]
19+
20+
21+
def _license_compliance_details(detection: 'Detection') -> list[tuple[str, str]]:
22+
return [('License', detection.detection_details.get('license') or _NOT_AVAILABLE)]
23+
24+
25+
def _unmaintained_package_details(detection: 'Detection') -> list[tuple[str, str]]:
26+
detection_details = detection.detection_details
27+
maintained_score = get_maintained_score(detection_details)
28+
ossf_score = get_ossf_score(detection_details)
29+
30+
return [
31+
('Maintained score', _NOT_AVAILABLE if maintained_score is None else str(maintained_score)),
32+
('OSSF Scorecard score', _NOT_AVAILABLE if ossf_score is None else str(ossf_score)),
33+
('Scorecard report', get_ossf_report_url(detection_details) or _NOT_AVAILABLE),
34+
]
35+
36+
37+
_DETAILS_BY_POLICY: dict[str, Callable[['Detection'], list[tuple[str, str]]]] = {
38+
PACKAGE_VULNERABILITY_POLICY_ID: _package_vulnerability_details,
39+
LICENSE_COMPLIANCE_POLICY_ID: _license_compliance_details,
40+
UNMAINTAINED_PACKAGE_POLICY_ID: _unmaintained_package_details,
41+
}
42+
43+
44+
def get_sca_policy_details(detection: 'Detection') -> list[tuple[str, str]]:
45+
"""Labelled fields specific to the SCA policy that raised the detection, in display order.
46+
47+
A policy with no entry contributes nothing rather than borrowing another policy's fields, so a new one shows
48+
no details until it is added here.
49+
"""
50+
build_details = _DETAILS_BY_POLICY.get(detection.detection_type_id)
51+
52+
return build_details(detection) if build_details else []
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from cycode.cli.consts import (
2+
LICENSE_COMPLIANCE_POLICY_ID,
3+
PACKAGE_VULNERABILITY_POLICY_ID,
4+
UNMAINTAINED_PACKAGE_POLICY_ID,
5+
)
6+
from cycode.cli.printers.utils.sca_policy_details import get_sca_policy_details
7+
from cycode.cyclient.models import Detection
8+
9+
10+
def _make_detection(policy_id: str, **details: object) -> Detection:
11+
return Detection(
12+
detection_type_id=policy_id,
13+
type='sca',
14+
message='message',
15+
detection_details=dict(details),
16+
detection_rule_id='rule-id',
17+
severity='Medium',
18+
)
19+
20+
21+
def test_package_vulnerability_reports_the_patched_version() -> None:
22+
detection = _make_detection(PACKAGE_VULNERABILITY_POLICY_ID, alert={'first_patched_version': '4.17.21'})
23+
24+
assert get_sca_policy_details(detection) == [('First patched version', '4.17.21')]
25+
26+
27+
def test_package_vulnerability_without_a_patch() -> None:
28+
detection = _make_detection(PACKAGE_VULNERABILITY_POLICY_ID, alert={'first_patched_version': None})
29+
30+
assert get_sca_policy_details(detection) == [('First patched version', 'Not fixed')]
31+
32+
33+
def test_license_compliance_reports_the_license() -> None:
34+
detection = _make_detection(LICENSE_COMPLIANCE_POLICY_ID, license='GPL-3.0')
35+
36+
assert get_sca_policy_details(detection) == [('License', 'GPL-3.0')]
37+
38+
39+
def test_license_compliance_without_a_license() -> None:
40+
detection = _make_detection(LICENSE_COMPLIANCE_POLICY_ID)
41+
42+
assert get_sca_policy_details(detection) == [('License', 'N/A')]
43+
44+
45+
def test_unmaintained_package_reports_the_maintained_check_first() -> None:
46+
detection = _make_detection(
47+
UNMAINTAINED_PACKAGE_POLICY_ID,
48+
ossf={
49+
'score': 4.1,
50+
'scorecard_report_url': 'https://scorecard.dev/viewer/?uri=github.com/a/b',
51+
'checks': [{'name': 'Maintained', 'score': 0}],
52+
},
53+
)
54+
55+
assert get_sca_policy_details(detection) == [
56+
('Maintained score', '0'),
57+
('OSSF Scorecard score', '4.1'),
58+
('Scorecard report', 'https://scorecard.dev/viewer/?uri=github.com/a/b'),
59+
]
60+
61+
62+
def test_unmaintained_package_without_a_scorecard() -> None:
63+
detection = _make_detection(UNMAINTAINED_PACKAGE_POLICY_ID)
64+
65+
assert get_sca_policy_details(detection) == [
66+
('Maintained score', 'N/A'),
67+
('OSSF Scorecard score', 'N/A'),
68+
('Scorecard report', 'N/A'),
69+
]
70+
71+
72+
def test_an_unregistered_policy_contributes_nothing() -> None:
73+
"""A policy with no entry must stay silent rather than borrow another policy's fields.
74+
75+
Before this was keyed by policy, an unmaintained detection fell through to the license branch and rendered
76+
an empty License row. A fourth policy would do the same.
77+
"""
78+
detection = _make_detection('00000000-0000-0000-0000-000000000000', license='GPL-3.0', alert={})
79+
80+
assert get_sca_policy_details(detection) == []

0 commit comments

Comments
 (0)