sonar-ansible
sonar-ansible copied to clipboard
To whoever aiming to use this module in 2024
I hope this will save you time.
This plugin has not been updated for a very long time, and then it is not working anymore with newer ansible-lint versions.
You can try another approach with sarif exports, and generic issues import.
Note that at the date I write this message, the direct sarif import is not working for ansible-lint generated reports.
First say to ansible-lint to generate a sarif export:
ansible-lint --sarif-file ansible-lint-sarif.json
To convert the sarif to generic-issue-import-format, you may decide to use a script that I paste right below.
Once you have the json file, you simply have to add the following to you sonar-project.properties file:
sonar.externalIssuesReportPaths=converted-file-sq.json
I hope this helps.
"""Convert SARIF to Generic SonarQube issues import format.
Initially based on work made by David Fischer <[email protected]>
* https://community.sonarsource.com/t/import-sarif-results-as-security-hotspots/83223
* docs.sonarqube.org/9.8/analyzing-source-code/importing-external-issues/generic-issue-import-format
* https://gist.github.com/davidfischer-ch/cdfede27ac053a8332b2127becc07608
Authors: David Fischer <[email protected]>, Florck
"""
from __future__ import annotations
from pathlib import Path
from typing import Final
import collections
import json
import os
import sys
# https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html#_Toc34317648
# SonarQube severity can be one of BLOCKER, CRITICAL, MAJOR, MINOR, INFO
LEVEL_TO_SERVERITY: Final[dict[str, str]] = {
"warning": "MEDIUM",
"error": "HIGH",
"note": "LOW",
"none": "LOW",
}
DEFAULT_LEVEL: Final[str] = "warning"
DEFAULT_CLEAN_CODE_ATTRIBUTE: Final[str] = "CONVENTIONAL"
DEFAULT_SOFTWARE_QUALITY: Final[str] = "RELIABILITY"
Position = collections.namedtuple("Position", ["line", "column"])
def fix_start_end(start, end, file_path):
"""Ensure the end position makes sense or fix it"""
lines = Path(file_path).read_text(encoding="utf-8").split(os.linesep)
if start == end or (end.column and end.column > len(lines[end.line])):
if end.line + 1 < len(lines):
# Move end position to next line at column 0
end = Position(end.line + 1, 0)
else:
# Move start to previous line at same column
# Move end position to same line at column 0
start = Position(start.line - 1, start.column)
end = Position(end.line, 0)
return start, end
def get_rules(sarif_rules, engine_id):
"""Get rules as formatted by sonar."""
rules: list[dict] = []
for rule_id in sarif_rules:
sarif_rule = sarif_rules[rule_id]
description = sarif_rule.get("shortDescription", {}).get("text")
description += "<br />"
description += sarif_rule.get("help", {}).get("text")
description += "<br />"
description += f'<a href="{sarif_rule.get("helpUri")}">Documentation</a>'
description += "<br /> <h3>Tags</h3><br /> "
description += ",".join(sarif_rule.get("properties", {}).get("tags", []))
rule = {
"id": rule_id,
"name": sarif_rule["name"],
"description": description,
"engineId": engine_id,
"cleanCodeAttribute": DEFAULT_CLEAN_CODE_ATTRIBUTE,
"impacts": [
{
"softwareQuality": DEFAULT_SOFTWARE_QUALITY,
"severity": LEVEL_TO_SERVERITY[
sarif_rule.get("defaultConfiguration", {}).get(
"level", DEFAULT_LEVEL
)
],
}
],
}
rules.append(rule)
return rules
def get_issues(
run_data, source, sarif_rules, engine_key, run_index
): # pylint:disable=too-many-locals
"""Get the issues formatted as expected by sonar."""
issues: list[dict] = []
for result_index, result_data in enumerate(run_data["results"], 1):
# Code is not programmed to handle multiple locations, because ... Its a WIP
if (num_locations := len(result_data["locations"])) != 1:
raise NotImplementedError(
f"File {source} : run[{run_index}].results[{result_index}].locations[] "
f"size expected 1, actual {num_locations}"
)
rule_id = result_data["ruleId"]
rule_data = (
sarif_rules[rule_id] if sarif_rules else {}
) # Only if rules is not empty
location_data = result_data["locations"][0]["physicalLocation"]
file_path = location_data["artifactLocation"]["uri"]
issue = {
"primaryLocation": {
"filePath": file_path,
"message": rule_data.get("help", {}).get("text"),
},
"ruleId": rule_id,
}
# Converting location data
start = Position(
location_data["region"]["startLine"] - 1,
location_data["region"].get("startColumn", 1) - 1,
)
end = Position(
location_data["region"].get("endLine", start.line + 1) - 1,
location_data["region"].get("endColumn", start.column + 1) - 1,
)
# Fix location data for some tools (data is wrong or missing)
if engine_key in {"ansible-lint", "robocop"}:
start, end = fix_start_end(start, end, file_path)
# Lines are 1-indexed both in SARIF and Sonar Generic
# Columns are 1-indexed in SARIF 0-indexed in Sonar Generic
issue["primaryLocation"]["textRange"] = {
"startLine": start.line + 1,
"startColumn": start.column,
"endLine": end.line + 1,
"endColumn": end.column,
}
issues.append(issue)
return issues
def main(
source: Path | str, target: Path | str
) -> None: # pylint:disable=too-many-locals
"""Implement main logic."""
source = Path(source).resolve()
target = Path(target).resolve()
if target.exists():
raise IOError(f'Target file "{target}" already exist.')
sarif_data: dict = json.loads(source.read_text(encoding="utf-8"))
if "sarif" not in sarif_data["$schema"]:
raise ValueError("Source is (probably) not a valid sarif file.")
issues: list[dict] = []
rules: list[dict] = []
for run_index, run_data in enumerate(sarif_data["runs"], 1):
driver_data = run_data["tool"]["driver"]
engine_id = driver_data["name"]
engine_key = engine_id.lower()
sarif_rules: dict[str, dict] = {
rule["id"]: rule for rule in driver_data.get("rules", {})
}
rules.extend(get_rules(sarif_rules, engine_id))
issues.extend(get_issues(run_data, source, sarif_rules, engine_key, run_index))
target.write_text(
json.dumps({"rules": rules, "issues": issues}, indent=2), encoding="utf-8"
)
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])