Skip to content

Commit

Permalink
When using SonarQube security hotspots as source for the security war…
Browse files Browse the repository at this point in the history
…nings metric, allow for filtering hotspots by hotspot status ('to review', 'acknowledged', 'fixed', 'safe'). Closes #5956.
  • Loading branch information
fniessink committed May 10, 2023
1 parent 473189f commit ec26026
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,12 @@ async def _get_source_responses(self, *urls: URL, **kwargs) -> SourceResponses:
)
)
if "security_hotspot" in security_types:
# Note: SonarQube is a bit inconsistent. For issue search, the SonarQube status parameter is called
# "statuses", but for hotspots it's called "status".
api_urls.append(
URL(f"{base_url}/api/hotspots/search?projectKey={component}&branch={branch}&status=TO_REVIEW&ps=500")
)
api_urls.append(URL(f"{base_url}/api/hotspots/search?projectKey={component}&branch={branch}&ps=500"))
return await super()._get_source_responses(*api_urls, **kwargs)

async def _parse_source_responses(self, responses: SourceResponses) -> SourceMeasurement:
"""Override to parse the selected security types."""
security_types = self._parameter(self.types_parameter)
review_priorities = [priority.upper() for priority in self._parameter("review_priorities")]
vulnerabilities = (
await super()._parse_source_responses(SourceResponses(responses=[responses[0]]))
if "vulnerability" in security_types
Expand All @@ -64,9 +59,9 @@ async def _parse_source_responses(self, responses: SourceResponses) -> SourceMea
if "security_hotspot" in security_types:
json = await responses[-1].json()
hotspots = [
await self.__entity(hotspot)
await self.__hotspot_entity(hotspot)
for hotspot in json.get("hotspots", [])
if hotspot["vulnerabilityProbability"] in review_priorities
if self.__include_hotspot(hotspot)
]
else:
hotspots = []
Expand All @@ -75,8 +70,16 @@ async def _parse_source_responses(self, responses: SourceResponses) -> SourceMea
entities=vulnerabilities.entities + hotspots,
)

async def __entity(self, hotspot) -> Entity:
"""Create the security warning entity."""
def __include_hotspot(self, hotspot) -> bool:
"""Return whether to include the hotspot."""
review_priorities = self._parameter("review_priorities")
review_priority = hotspot["vulnerabilityProbability"].lower()
statuses = self._parameter("hotspot_statuses")
status = self.__hotspot_status(hotspot)
return review_priority in review_priorities and status in statuses

async def __hotspot_entity(self, hotspot) -> Entity:
"""Create the security warning entity for the hotspot."""
return Entity(
key=hotspot["key"],
component=hotspot["component"],
Expand All @@ -86,8 +89,19 @@ async def __entity(self, hotspot) -> Entity:
review_priority=hotspot["vulnerabilityProbability"].lower(),
creation_date=hotspot["creationDate"],
update_date=hotspot["updateDate"],
hotspot_status=self.__hotspot_status(hotspot),
)

@staticmethod
def __hotspot_status(hotspot: dict[str, str]) -> str:
"""Return the hotspot status."""
# The SonarQube documentation describes the hotspot lifecycle as having four statuses (see
# https://docs.sonarqube.org/latest/user-guide/security-hotspots/#lifecycle): 'to review', 'acknowledged',
# 'fixed', and 'safe'. However, in the API the status is either 'to review' or 'reviewed' and the other
# statuses ('acknowledged', 'fixed', and 'safe') are called resolutions. So to determine the status as defined
# by the docs, check both the hotspot status and the hotspot resolution:
return "to review" if hotspot["status"] == "TO_REVIEW" else hotspot["resolution"].lower()

async def __hotspot_landing_url(self, hotspot_key: str) -> URL:
"""Generate a landing url for the hotspot."""
url = await SonarQubeCollector._landing_url(self, SourceResponses()) # pylint: disable=protected-access
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def entity( # pylint: disable=too-many-arguments
review_priority: str = None,
creation_date: str = None,
update_date: str = None,
hotspot_status: str = None,
) -> Entity:
"""Create an entity."""
url = (
Expand All @@ -53,4 +54,6 @@ def entity( # pylint: disable=too-many-arguments
entity["rationale"] = rationale
if review_priority is not None:
entity["review_priority"] = review_priority
if hotspot_status is not None:
entity["hotspot_status"] = hotspot_status
return entity
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def setUp(self):
vulnerabilityProbability="MEDIUM",
creationDate="2010-12-13T10:37:07+0000",
updateDate="2019-08-26T09:02:49+0000",
status="TO_REVIEW",
),
dict(
key="hotspot2",
Expand All @@ -52,6 +53,8 @@ def setUp(self):
vulnerabilityProbability="LOW",
creationDate="2011-10-26T13:34:12+0000",
updateDate="2020-08-31T08:19:00+0000",
status="REVIEWED",
resolution="FIXED",
),
],
)
Expand All @@ -64,6 +67,7 @@ def setUp(self):
review_priority="medium",
creation_date="2010-12-13T10:37:07+0000",
update_date="2019-08-26T09:02:49+0000",
hotspot_status="to review",
),
self.entity(
key="hotspot2",
Expand All @@ -73,6 +77,7 @@ def setUp(self):
review_priority="low",
creation_date="2011-10-26T13:34:12+0000",
update_date="2020-08-31T08:19:00+0000",
hotspot_status="fixed",
),
]
self.vulnerability_entities = [
Expand Down Expand Up @@ -133,3 +138,16 @@ async def test_security_warnings_vulnerabilities_only(self):
entities=self.vulnerability_entities,
landing_url="https://sonarqube/project/issues?id=id&branch=master&resolved=false&types=VULNERABILITY",
)

async def test_filter_security_warnings_hotspots_by_status(self):
"""Test that the security hotspots can be filtered by status."""
self.set_source_parameter("security_types", ["security_hotspot"])
self.set_source_parameter("hotspot_statuses", ["fixed"])
response = await self.collect(get_request_json_return_value=self.hotspots_json)
self.assert_measurement(
response,
value="1",
total="100",
entities=self.hotspot_entities[1:],
landing_url="https://sonarqube/project/security_hotspots?id=id&branch=master",
)
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,19 @@ class Lines(str, Enum):
CODE = "lines with code"


def violation_entity_attributes(include_review_priority=False, include_resolution=False, include_rationale=False):
def violation_entity_attributes(
include_review_priority=False, include_resolution=False, include_rationale=False, include_status=False
):
"""Return the violation entity attributes."""
attributes = [
dict(name="Message"),
dict(name="Severity", color=dict(blocker=Color.NEGATIVE, critical=Color.NEGATIVE, major=Color.WARNING)),
]
if include_review_priority:
attributes.append(dict(name="Review priority", color=dict(high=Color.NEGATIVE, medium=Color.WARNING)))
attributes.append(dict(name="Type"))
attributes.append(dict(name="Warning type", key="type"))
if include_status:
attributes.append(dict(name="Hotspot status"))
if include_resolution:
attributes.append(dict(name="Resolution"))
if include_rationale:
Expand Down Expand Up @@ -245,6 +249,14 @@ def violation_entity_attributes(include_review_priority=False, include_resolutio
values=["info", "minor", "major", "critical", "blocker"],
metrics=["security_warnings", "suppressed_violations", "violations"],
),
hotspot_statuses=MultipleChoiceParameter(
name="Security hotspot statuses",
short_name="hotspot statuses",
help_url="https://docs.sonarqube.org/latest/user-guide/security-hotspots/",
placeholder="all hotspot statuses",
values=["to review", "acknowledged", "safe", "fixed"],
metrics=["security_warnings"],
),
review_priorities=MultipleChoiceParameter(
name="Security hotspot review priorities",
short_name="review priorities",
Expand Down Expand Up @@ -280,10 +292,10 @@ def violation_entity_attributes(include_review_priority=False, include_resolutio
security_types=MultipleChoiceParameter(
name="Security issue types (measuring security hotspots requires SonarQube 8.2 or newer)",
short_name="types",
placeholder="all security issue types",
placeholder="vulnerability",
help_url="https://docs.sonarqube.org/latest/user-guide/rules/",
default_value=["vulnerability"],
values=["vulnerability", "security_hotspot"],
values=["security_hotspot", "vulnerability"],
metrics=["security_warnings"],
),
),
Expand All @@ -309,7 +321,8 @@ def violation_entity_attributes(include_review_priority=False, include_resolutio
],
),
security_warnings=dict(
name="security warning", attributes=violation_entity_attributes(include_review_priority=True)
name="security warning",
attributes=violation_entity_attributes(include_review_priority=True, include_status=True),
),
suppressed_violations=dict(
name="violation",
Expand Down
10 changes: 10 additions & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

<!-- The line "## <square-bracket>Unreleased</square-bracket>" is replaced by the release/release.py script with the new release version and release date. -->

## [Unreleased]

### Deployment notes

If your currently installed *Quality-time* version is v4.0.0 or older, please read the v4.0.0 deployment notes.

### Added

- When using SonarQube security hotspots as source for the security warnings metric, allow for filtering hotspots by hotspot status ('to review', 'acknowledged', 'fixed', 'safe'). Closes [#5956](https://github.com/ICTU/quality-time/issues/5956).

## v4.10.0 - 2023-04-26

### Deployment notes
Expand Down
2 changes: 1 addition & 1 deletion docs/styles/Vocab/Base/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ donut
errored
favicon
hostnames?
hotspots
hotspots?
misconfigured
mypy
namespace
Expand Down

0 comments on commit ec26026

Please sign in to comment.