Skip to content

Commit bfe9974

Browse files
authored
Merge pull request #1343 from okorach:sonar-migration-exports-hotspots-counts
Export hotspots counts in sonar-migration
2 parents cad684c + 5725da7 commit bfe9974

File tree

6 files changed

+124
-91
lines changed

6 files changed

+124
-91
lines changed

sonar/branches.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,8 @@ def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr:
220220
:return: The branch new code period definition
221221
:rtype: str
222222
"""
223-
from sonar import issues
223+
from sonar.issues import count as issue_count
224+
from sonar.hotspots import count as hotspot_count
224225

225226
log.debug("Exporting %s", str(self))
226227
data = {settings.NEW_CODE_PERIOD: self.new_code()}
@@ -240,20 +241,19 @@ def export(self, export_settings: types.ConfigSettings) -> types.ObjectJsonRepr:
240241
loc_distrib = {m.split("=")[0]: int(m.split("=")[1]) for m in lang_distrib.split(";")}
241242
loc_distrib["total"] = self.loc()
242243
data["ncloc"] = loc_distrib
243-
if export_settings.get("MODE", "") == "MIGRATION":
244244
tpissues = self.count_third_party_issues()
245-
issue_data = {"thirdParty": tpissues if len(tpissues) > 0 else 0}
246-
if self.endpoint.version() >= (10, 0, 0):
247-
issue_data["falsePositives"] = issues.count(
248-
self.endpoint, components=self.concerned_object.key, branch=self.name, issueStatuses="FALSE_POSITIVE"
249-
)
250-
issue_data["accepted"] = issues.count(self.endpoint, components=self.concerned_object.key, branch=self.name, issueStatuses="ACCEPTED")
251-
else:
252-
issue_data["falsePositives"] = issues.count(
253-
self.endpoint, componentKeys=self.concerned_object.key, branch=self.name, resolutions="FALSE-POSITIVE"
254-
)
255-
issue_data["wontFix"] = issues.count(self.endpoint, componentKeys=self.concerned_object.key, branch=self.name, resolutions="WONTFIX")
256-
data["issues"] = issue_data
245+
params = self.search_params()
246+
data["issues"] = {
247+
"thirdParty": tpissues if len(tpissues) > 0 else 0,
248+
"falsePositives": issue_count(self.endpoint, issueStatuses=["FALSE_POSITIVE"], **params),
249+
}
250+
status = "accepted" if self.endpoint.version() >= (10, 2, 0) else "wontFix"
251+
data["issues"][status] = issue_count(self.endpoint, issueStatuses=[status.upper()], **params)
252+
data["hotspots"] = {
253+
"acknowledged": hotspot_count(self.endpoint, resolution=["ACKNOWLEDGED"], **params),
254+
"safe": hotspot_count(self.endpoint, resolution=["SAFE"], **params),
255+
"fixed": hotspot_count(self.endpoint, resolution=["FIXED"], **params),
256+
}
257257
log.debug("%s has these notable issues %s", str(self), str(data["issues"]))
258258
data = util.remove_nones(data)
259259
return None if len(data) == 0 else data

sonar/hotspots.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,10 @@ def search(endpoint: pf.Platform, filters: types.ApiParams = None) -> dict[str,
387387
new_params = get_search_filters(endpoint=endpoint, params=filters)
388388
new_params = util.dict_remap(original_dict=new_params, remapping=_FILTERS_HOTSPOTS_REMAPPING)
389389
filters_iterations = split_search_filters(new_params)
390+
ps = 500 if "ps" not in new_params else new_params["ps"]
390391
for inline_filters in filters_iterations:
391392
p = 1
392-
inline_filters["ps"] = 500
393+
inline_filters["ps"] = ps
393394
log.info("Searching hotspots with sanitized filters %s", str(inline_filters))
394395
while True:
395396
inline_filters["p"] = p
@@ -403,7 +404,7 @@ def search(endpoint: pf.Platform, filters: types.ApiParams = None) -> dict[str,
403404
nbr_hotspots = 0
404405
return {}
405406
raise e
406-
nbr_pages = (nbr_hotspots + 499) // 500
407+
nbr_pages = (nbr_hotspots + ps - 1) // ps
407408
log.debug("Number of hotspots: %d - Page: %d/%d", nbr_hotspots, inline_filters["p"], nbr_pages)
408409
if nbr_hotspots > 10000:
409410
raise TooManyHotspotsError(
@@ -433,6 +434,7 @@ def get_object(endpoint: pf.Platform, key: str, data: dict[str] = None, from_exp
433434

434435
def get_search_filters(endpoint: pf.Platform, params: types.ApiParams) -> types.ApiParams:
435436
"""Returns the filtered list of params that are allowed for api/hotspots/search"""
437+
log.debug("Sanitizing hotspot search criteria %s", str(params))
436438
if params is None:
437439
return {}
438440
criterias = util.remove_nones(params.copy())
@@ -441,11 +443,12 @@ def get_search_filters(endpoint: pf.Platform, params: types.ApiParams) -> types.
441443
criterias["status"] = util.allowed_values_string(criterias["status"], STATUSES)
442444
if "resolution" in criterias:
443445
criterias["resolution"] = util.allowed_values_string(criterias["resolution"], RESOLUTIONS)
444-
log.warning("hotspot 'status' criteria incompatible with 'resolution' criteria, ignoring 'status'")
445446
criterias["status"] = "REVIEWED"
446-
if endpoint.version() >= (10, 2, 0):
447-
criterias = util.dict_remap(original_dict=criterias, remapping={PROJECT_FILTER_OLD: PROJECT_FILTER})
448-
return util.dict_subset(criterias, SEARCH_CRITERIAS)
447+
if endpoint.version() <= (10, 2, 0):
448+
criterias = util.dict_remap(original_dict=criterias, remapping={PROJECT_FILTER: PROJECT_FILTER_OLD})
449+
criterias = util.dict_subset(criterias, SEARCH_CRITERIAS)
450+
log.debug("Sanitized hotspot search criteria %s", str(criterias))
451+
return criterias
449452

450453

451454
def split_filter(params: types.ApiParams, criteria: str) -> list[types.ApiParams]:
@@ -475,7 +478,7 @@ def split_search_filters(params: types.ApiParams) -> list[types.ApiParams]:
475478
def post_search_filter(hotspots_dict: dict[str, Hotspot], filters: types.ApiParams) -> dict[str, Hotspot]:
476479
"""Filters a dict of hotspots with provided filters"""
477480
filtered_findings = hotspots_dict.copy()
478-
log.info("Post filtering findings with %s", str(filters))
481+
log.debug("Post filtering findings with %s", str(filters))
479482
if "createdAfter" in filters:
480483
min_date = util.string_to_date(filters["createdAfter"])
481484
if "createdBefore" in filters:
@@ -491,3 +494,12 @@ def post_search_filter(hotspots_dict: dict[str, Hotspot], filters: types.ApiPara
491494
filtered_findings.pop(key, None)
492495

493496
return filtered_findings
497+
498+
499+
def count(endpoint: pf.Platform, **kwargs) -> int:
500+
"""Returns number of hotspots of a search"""
501+
params = {} if not kwargs else kwargs.copy()
502+
params["ps"] = 1
503+
nbr_hotspots = len(search(endpoint=endpoint, filters=params))
504+
log.debug("Hotspot counts with filters %s returned %d hotspots", str(kwargs), nbr_hotspots)
505+
return nbr_hotspots

sonar/issues.py

Lines changed: 46 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@
4343
COMPONENT_FILTER_OLD = "componentKeys"
4444
COMPONENT_FILTER = "components"
4545

46+
OLD_STATUS = "resolutions"
47+
NEW_STATUS = "issueStatuses"
48+
49+
OLD_FP = "FALSE-POSITIVE"
50+
NEW_FP = "FALSE_POSITIVE"
51+
4652
_SEARCH_CRITERIAS = (
4753
COMPONENT_FILTER_OLD,
4854
COMPONENT_FILTER,
@@ -77,18 +83,17 @@
7783
"author",
7884
"issues",
7985
"languages",
80-
"resolutions",
86+
OLD_STATUS,
8187
"resolved",
8288
"rules",
8389
"scopes",
8490
# 10.2 new filter
8591
"impactSeverities",
8692
# 10.4 new filter
87-
"issueStatuses",
93+
NEW_STATUS,
8894
)
8995

9096
_FILTERS_10_2_REMAPPING = {"severities": "impactSeverities"}
91-
_FILTERS_10_4_REMAPPING = {"statuses": "issueStatuses"}
9297

9398
TYPES = ("BUG", "VULNERABILITY", "CODE_SMELL")
9499
SEVERITIES = ("BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO")
@@ -102,7 +107,7 @@
102107
"impactSoftwareQualities": IMPACT_SOFTWARE_QUALITIES,
103108
"impactSeverities": IMPACT_SEVERITIES,
104109
"statuses": STATUSES,
105-
"resolutions": RESOLUTIONS,
110+
OLD_STATUS: RESOLUTIONS,
106111
}
107112

108113
_TOO_MANY_ISSUES_MSG = "Too many issues, recursing..."
@@ -336,7 +341,7 @@ def is_wont_fix(self) -> bool:
336341
:return: Whether the issue is won't fix
337342
:rtype: bool
338343
"""
339-
return self.resolution == "WONT-FIX"
344+
return self.resolution == "WONTFIX"
340345

341346
def is_accepted(self) -> bool:
342347
"""
@@ -350,7 +355,7 @@ def is_false_positive(self) -> bool:
350355
:return: Whether the issue is a false positive
351356
:rtype: bool
352357
"""
353-
return self.resolution == "FALSE-POSITIVE"
358+
return self.resolution in ("FALSE-POSITIVE", "FALSE_POSITIVE")
354359

355360
def strictly_identical_to(self, another_finding: Issue, ignore_component: bool = False) -> bool:
356361
"""
@@ -447,7 +452,7 @@ def __apply_event(self, event: str, settings: ConfigSettings) -> bool:
447452
else:
448453
self.reopen()
449454
# self.add_comment(f"Issue re-open {origin}", settings[SYNC_ADD_COMMENTS])
450-
elif event_type == "FALSE-POSITIVE":
455+
elif event_type in ("FALSE-POSITIVE", "FALSE_POSITIVE"):
451456
self.mark_as_false_positive()
452457
# self.add_comment(f"False positive {origin}", settings[SYNC_ADD_COMMENTS])
453458
elif event_type == "WONT-FIX":
@@ -747,8 +752,6 @@ def search(endpoint: pf.Platform, params: ApiParams = None, raise_error: bool =
747752
filters = pre_search_filters(endpoint=endpoint, params=params)
748753
# if endpoint.version() >= (10, 2, 0):
749754
# new_params = util.dict_remap_and_stringify(new_params, _FILTERS_10_2_REMAPPING)
750-
if endpoint.version() >= (10, 4, 0):
751-
filters = _change_filters_for_10_4(filters)
752755

753756
log.debug("Search filters = %s", str(filters))
754757
if not filters:
@@ -822,11 +825,10 @@ def count(endpoint: pf.Platform, **kwargs) -> int:
822825
params = {} if not kwargs else kwargs.copy()
823826
params["ps"] = 1
824827
try:
825-
log.debug("Count params = %s", str(params))
826828
nbr_issues = len(search(endpoint=endpoint, params=params))
827829
except TooManyIssuesError as e:
828830
nbr_issues = e.nbr_issues
829-
log.debug("Issue search %s would return %d issues", str(kwargs), nbr_issues)
831+
log.debug("Count issues with filters %s returned %d issues", str(kwargs), nbr_issues)
830832
return nbr_issues
831833

832834

@@ -852,7 +854,7 @@ def count_by_rule(endpoint: pf.Platform, **kwargs) -> dict[str, int]:
852854
if d["val"] not in rulecount:
853855
rulecount[d["val"]] = 0
854856
rulecount[d["val"]] += d["count"]
855-
log.debug("Rule counts = %s", util.json_dump(rulecount))
857+
# log.debug("Rule counts = %s", util.json_dump(rulecount))
856858
return rulecount
857859

858860

@@ -868,42 +870,36 @@ def pre_search_filters(endpoint: pf.Platform, params: ApiParams) -> ApiParams:
868870
"""Returns the filtered list of params that are allowed for api/issue/search"""
869871
if not params:
870872
return {}
871-
filters = util.dict_subset(util.remove_nones(params.copy()), _SEARCH_CRITERIAS)
872-
if endpoint.version() >= (10, 2, 0):
873-
if COMPONENT_FILTER_OLD in filters:
874-
filters[COMPONENT_FILTER] = filters.pop(COMPONENT_FILTER_OLD)
875-
if "types" in filters:
876-
__MAP = {"BUG": "RELIABILITY", "CODE_SMELL": "MAINTAINABILITY", "VULNERABILITY": "SECURITY", "SECURITY_HOTSPOT": "SECURITY"}
877-
filters["impactSoftwareQualities"] = [__MAP[t] for t in filters.pop("types")]
878-
if len(filters["impactSoftwareQualities"]) == 0:
879-
filters.pop("impactSoftwareQualities")
880-
if "severities" in filters:
881-
__MAP = {"BLOCKER": "HIGH", "CRITICAL": "HIGH", "MAJOR": "MEDIUM", "MINOR": "LOW", "INFO": "LOW"}
882-
filters["impactSeverities"] = [__MAP[t] for t in filters.pop("severities")]
883-
if len(filters["impactSeverities"]) == 0:
884-
filters.pop("impactSeverities")
885-
for k, v in FILTERS_MAP.items():
886-
if k in filters:
887-
filters[k] = util.allowed_values_string(filters[k], v)
888-
if filters.get("languages", None) is not None:
889-
filters["languages"] = util.list_to_csv(filters["languages"])
890-
873+
log.debug("Sanitizing issue search filters %s", str(params))
874+
version = endpoint.version()
875+
filters = util.dict_remap(original_dict=params.copy(), remapping={"project": COMPONENT_FILTER})
876+
filters = util.dict_subset(util.remove_nones(filters), _SEARCH_CRITERIAS)
877+
if version < (10, 2, 0):
878+
# Starting from 10.2 - "componentKeys" was renamed "components"
879+
filters = util.dict_remap(original_dict=filters, remapping={COMPONENT_FILTER: COMPONENT_FILTER_OLD})
880+
else:
881+
# Starting from 10.2 - Issue types were replaced by software qualities, and severities replaced by impacts
882+
__MAP = {"BUG": "RELIABILITY", "CODE_SMELL": "MAINTAINABILITY", "VULNERABILITY": "SECURITY", "SECURITY_HOTSPOT": "SECURITY"}
883+
filters["impactSoftwareQualities"] = util.list_re_value(filters.pop("types", None), __MAP)
884+
if len(filters["impactSoftwareQualities"]) == 0:
885+
filters.pop("impactSoftwareQualities")
886+
__MAP = {"BLOCKER": "HIGH", "CRITICAL": "HIGH", "MAJOR": "MEDIUM", "MINOR": "LOW", "INFO": "LOW"}
887+
filters["impactSeverities"] = util.list_re_value(filters.pop("severities", None), __MAP)
888+
if len(filters["impactSeverities"]) == 0:
889+
filters.pop("impactSeverities")
890+
891+
if version < (10, 4, 0):
892+
log.debug("Sanitizing issue search filters - fixing resolutions")
893+
filters = util.dict_remap(original_dict=filters, remapping={NEW_STATUS: OLD_STATUS})
894+
if OLD_STATUS in filters:
895+
filters[OLD_STATUS] = util.list_re_value(filters[OLD_STATUS], mapping={NEW_FP: OLD_FP})
896+
else:
897+
# Starting from 10.4 - "resolutions" was renamed "issuesStatuses", "FALSE-POSITIVE" was renamed "FALSE_POSITIVE"
898+
filters = util.dict_remap(original_dict=filters, remapping={OLD_STATUS: NEW_STATUS})
899+
if NEW_STATUS in filters:
900+
filters[NEW_STATUS] = util.list_re_value(filters[NEW_STATUS], mapping={OLD_FP: NEW_FP})
901+
902+
filters = {k: util.allowed_values_string(v, FILTERS_MAP[k]) if k in FILTERS_MAP else v for k, v in filters.items()}
903+
filters = {k: util.list_to_csv(v) for k, v in filters.items() if v}
904+
log.debug("Sanitized issue search filters %s", str(filters))
891905
return filters
892-
893-
894-
def _change_filters_for_10_4(filters: ApiParams) -> ApiParams:
895-
"""Adjust filters for new 10.4 issues/search API parameters"""
896-
if not filters:
897-
return None
898-
new_filters = util.dict_remap(filters.copy(), _FILTERS_10_4_REMAPPING)
899-
statuses = []
900-
for f in "resolutions", "issueStatuses":
901-
if f in new_filters:
902-
statuses += util.csv_to_list(new_filters[f])
903-
new_filters.pop("resolutions", None)
904-
if len(statuses) > 0:
905-
if "FALSE-POSITIVE" in statuses:
906-
statuses.remove("FALSE-POSITIVE")
907-
statuses.append("FALSE_POSITIVE")
908-
new_filters["issueStatuses"] = util.list_to_csv(statuses)
909-
return new_filters

sonar/projects.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,8 @@ def export(self, export_settings: types.ConfigSettings, settings_list: dict[str,
953953
:return: All project configuration settings
954954
:rtype: dict
955955
"""
956-
from sonar import issues
956+
from sonar.issues import count as issue_count
957+
from sonar.hotspots import count as hotspot_count
957958

958959
log.info("Exporting %s", str(self))
959960
try:
@@ -997,14 +998,18 @@ def export(self, export_settings: types.ConfigSettings, settings_list: dict[str,
997998
"taskHistory": [t._json for t in self.task_history()],
998999
}
9991000
tpissues = self.count_third_party_issues()
1000-
issue_data = {"thirdParty": tpissues if len(tpissues) > 0 else 0}
1001-
if self.endpoint.version() >= (10, 0, 0):
1002-
issue_data["falsePositives"] = issues.count(self.endpoint, components=self.key, issueStatuses="FALSE_POSITIVE")
1003-
issue_data["accepted"] = issues.count(self.endpoint, components=self.key, issueStatuses="ACCEPTED")
1004-
else:
1005-
issue_data["falsePositives"] = issues.count(self.endpoint, componentKeys=self.key, resolutions="FALSE-POSITIVE")
1006-
issue_data["wontFix"] = issues.count(self.endpoint, componentKeys=self.key, resolutions="WONTFIX")
1007-
json_data["issues"] = issue_data
1001+
params = self.search_params()
1002+
json_data["issues"] = {
1003+
"thirdParty": tpissues if len(tpissues) > 0 else 0,
1004+
"falsePositives": issue_count(self.endpoint, issueStatuses=["FALSE_POSITIVE"], **params),
1005+
}
1006+
status = "accepted" if self.endpoint.version() >= (10, 2, 0) else "wontFix"
1007+
json_data["issues"][status] = issue_count(self.endpoint, issueStatuses=[status.upper()], **params)
1008+
json_data["hotspots"] = {
1009+
"acknowledged": hotspot_count(self.endpoint, resolution=["ACKNOWLEDGED"], **params),
1010+
"safe": hotspot_count(self.endpoint, resolution=["SAFE"], **params),
1011+
"fixed": hotspot_count(self.endpoint, resolution=["FIXED"], **params),
1012+
}
10081013
log.debug("%s has these notable issues %s", str(self), str(json_data["issues"]))
10091014

10101015
settings_dict = settings.get_bulk(endpoint=self.endpoint, component=self, settings_list=settings_list, include_not_set=False)

sonar/utilities.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -209,19 +209,21 @@ def csv_to_list(string: str, separator: str = ",") -> list[str]:
209209
return [s.strip() for s in string.split(separator)]
210210

211211

212-
def list_to_csv(array: Union[None, str, list[str]], separator: str = ",", check_for_separator: bool = False) -> Optional[str]:
212+
def list_to_csv(array: Union[None, str, int, float, list[str]], separator: str = ",", check_for_separator: bool = False) -> Optional[str]:
213213
"""Converts a list of strings to CSV"""
214214
if isinstance(array, str):
215215
return csv_normalize(array, separator) if " " in array else array
216216
if array is None:
217217
return None
218-
if check_for_separator:
219-
# Don't convert to string if one array item contains the string separator
220-
s = separator.strip()
221-
for item in array:
222-
if s in item:
223-
return array
224-
return separator.join([v.strip() for v in array])
218+
if isinstance(array, (list, set, tuple)):
219+
if check_for_separator:
220+
# Don't convert to string if one array item contains the string separator
221+
s = separator.strip()
222+
for item in array:
223+
if s in item:
224+
return array
225+
return separator.join([v.strip() for v in array])
226+
return str(array)
225227

226228

227229
def csv_normalize(string: str, separator: str = ",") -> str:
@@ -587,6 +589,15 @@ def dict_remap(original_dict: dict[str, str], remapping: dict[str, str]) -> dict
587589
return remapped_filters
588590

589591

592+
def list_re_value(a_list: list[str], mapping: dict[str, str]) -> list[str]:
593+
"""Adjust findings search filters based on Sonar version"""
594+
if not a_list or len(a_list) == 0:
595+
return []
596+
for old, new in mapping.items():
597+
a_list = [new if v == old else v for v in a_list]
598+
return a_list
599+
600+
590601
def dict_stringify(original_dict: dict[str, str]) -> dict[str, str]:
591602
"""Covert dict list values into CSV string"""
592603
if not original_dict:

0 commit comments

Comments
 (0)