reflectsonar 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- reflectsonar/__init__.py +19 -0
- reflectsonar/__main__.py +9 -0
- reflectsonar/api/__init__.py +1 -0
- reflectsonar/api/get_data.py +256 -0
- reflectsonar/data/__init__.py +1 -0
- reflectsonar/data/models.py +154 -0
- reflectsonar/main.py +101 -0
- reflectsonar/report/__init__.py +1 -0
- reflectsonar/report/cover_page.py +142 -0
- reflectsonar/report/hotspots.py +395 -0
- reflectsonar/report/issues.py +336 -0
- reflectsonar/report/pdfgen.py +136 -0
- reflectsonar/report/reflect-sonar.png +0 -0
- reflectsonar/report/rules.py +207 -0
- reflectsonar/report/utils.py +272 -0
- reflectsonar-1.0.0.dist-info/METADATA +209 -0
- reflectsonar-1.0.0.dist-info/RECORD +21 -0
- reflectsonar-1.0.0.dist-info/WHEEL +5 -0
- reflectsonar-1.0.0.dist-info/entry_points.txt +2 -0
- reflectsonar-1.0.0.dist-info/licenses/LICENSE +674 -0
- reflectsonar-1.0.0.dist-info/top_level.txt +1 -0
reflectsonar/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ReflectSonar - PDF Report Generator for SonarQube Analysis
|
|
3
|
+
|
|
4
|
+
ReflectSonar is a Python tool for generating PDF reports from SonarQube analysis data.
|
|
5
|
+
It reads data via the SonarQube API and generates comprehensive PDF reports for
|
|
6
|
+
general metrics, issues, and security hotspots.
|
|
7
|
+
|
|
8
|
+
Author: Ata Seren
|
|
9
|
+
License: MIT
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "1.0.0"
|
|
13
|
+
__author__ = "Ata Seren"
|
|
14
|
+
__email__ = "ata.seren@hotmail.com"
|
|
15
|
+
__license__ = "GPL-3.0"
|
|
16
|
+
|
|
17
|
+
from .main import main
|
|
18
|
+
|
|
19
|
+
__all__ = ["main"]
|
reflectsonar/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API module for ReflectSonar - handles SonarQube API interactions"""
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module is used to fetch data from SonarQube API
|
|
3
|
+
"""
|
|
4
|
+
import traceback
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from ..data.models import (SonarQubeProject, SonarQubeIssue, SonarQubeMeasure, # pylint: disable=import-error
|
|
9
|
+
SonarQubeHotspot, ReportData, SonarQubeRule) # pylint: disable=import-error
|
|
10
|
+
|
|
11
|
+
from ..report.utils import log
|
|
12
|
+
|
|
13
|
+
# Helpers
|
|
14
|
+
def get_json(url: str, token: str) -> Dict:
|
|
15
|
+
"""Helper function to perform GET request and return JSON response"""
|
|
16
|
+
response = requests.get(url, auth=(token, ""), timeout=30)
|
|
17
|
+
response.raise_for_status()
|
|
18
|
+
return response.json()
|
|
19
|
+
|
|
20
|
+
def fetch(name: str, url: str, token: str, verbose: bool) -> Dict:
|
|
21
|
+
"""Fetches data from SonarQube API by using get_json and utils.log"""
|
|
22
|
+
log(verbose, f"Fetching {name}...")
|
|
23
|
+
return get_json(url, token)
|
|
24
|
+
|
|
25
|
+
# Getters
|
|
26
|
+
def get_rules(base_url: str, token: str, rule_keys: List[str],
|
|
27
|
+
verbose: bool = False) -> Dict[str, 'SonarQubeRule']:
|
|
28
|
+
"""Fetches rule descriptions from SonarQube API for given rule keys"""
|
|
29
|
+
if not rule_keys:
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
# SonarQube API: /api/rules/show?key=rule_key
|
|
34
|
+
rules = {}
|
|
35
|
+
|
|
36
|
+
for rule_key in rule_keys:
|
|
37
|
+
|
|
38
|
+
log(verbose, f" 📋 Fetching rule description: {rule_key}")
|
|
39
|
+
|
|
40
|
+
rule_url = f"{base_url}/api/rules/show?key={rule_key}"
|
|
41
|
+
response = fetch("rule description...", rule_url, token, verbose)
|
|
42
|
+
|
|
43
|
+
if 'rule' in response:
|
|
44
|
+
rules[rule_key] = SonarQubeRule.from_dict(response['rule'])
|
|
45
|
+
|
|
46
|
+
log(verbose, f" ✅ Fetched {len(rules)} rule descriptions")
|
|
47
|
+
return rules
|
|
48
|
+
|
|
49
|
+
except requests.RequestException as e:
|
|
50
|
+
print(f"ERROR: Error fetching rules: {e}")
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
# Function to fetch code snippet for a specific issue or hotspot
|
|
54
|
+
def get_code_snippet(base_url: str, token: str, component: str, line: int) -> str:
|
|
55
|
+
"""Fetches code snippet for a specific issue or hotspotfrom SonarQube API"""
|
|
56
|
+
try:
|
|
57
|
+
# Calculate line range (line ± context_lines)
|
|
58
|
+
from_line = max(1, line - 3)
|
|
59
|
+
to_line = line + 3
|
|
60
|
+
|
|
61
|
+
sources_url = f"{base_url}/api/sources/show?key={component}&from={from_line}&to={to_line}"
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
response = fetch("code snippets...", sources_url, token, False)
|
|
65
|
+
except requests.RequestException as api_error:
|
|
66
|
+
print(f"ERROR: API call failed: {api_error}")
|
|
67
|
+
return ""
|
|
68
|
+
|
|
69
|
+
sources = response.get("sources", [])
|
|
70
|
+
|
|
71
|
+
if not sources:
|
|
72
|
+
print(f"ERROR: No sources found for {component}:{line}")
|
|
73
|
+
print(f"ERROR: Full API response: {response}")
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
# Format the code snippet with line numbers
|
|
77
|
+
snippet_lines = []
|
|
78
|
+
for source in sources:
|
|
79
|
+
line_num = source[0]
|
|
80
|
+
code = source[1]
|
|
81
|
+
# Mark the problematic line with >>>
|
|
82
|
+
marker = ">>> " if line_num == line else " "
|
|
83
|
+
snippet_lines.append(f"{marker}{line_num:3d}: {code}")
|
|
84
|
+
|
|
85
|
+
result = "\n".join(snippet_lines)
|
|
86
|
+
return result
|
|
87
|
+
except requests.RequestException as e:
|
|
88
|
+
print(f"ERROR: Error fetching code snippet for {component}:{line}: {e}") # Debug log
|
|
89
|
+
traceback.print_exc()
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
def fetch_all_hotspots(base_url: str, token: str, project_key: str) -> Dict:
|
|
93
|
+
"""Fetches all security hotspots from a project"""
|
|
94
|
+
all_hotspots = []
|
|
95
|
+
page = 1
|
|
96
|
+
page_size = 500 # Maximum page size
|
|
97
|
+
|
|
98
|
+
while True:
|
|
99
|
+
hotspots_url = f"{base_url}/api/hotspots/search?projectKey={project_key}&ps={page_size}&p={page}" # pylint: disable=line-too-long
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
page_data = fetch("hotspots...", hotspots_url, token, False)
|
|
103
|
+
page_hotspots = page_data.get("hotspots", [])
|
|
104
|
+
|
|
105
|
+
all_hotspots.extend(page_hotspots)
|
|
106
|
+
|
|
107
|
+
# Check if we have more pages
|
|
108
|
+
paging = page_data.get("paging", {})
|
|
109
|
+
total = paging.get("total", 0)
|
|
110
|
+
current_total = len(all_hotspots)
|
|
111
|
+
|
|
112
|
+
if current_total >= total:
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
page += 1
|
|
116
|
+
|
|
117
|
+
except requests.RequestException as e:
|
|
118
|
+
print(f"ERROR: Failed to fetch hotspots page {page}: {e}")
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
# Return in the same format as the original API response
|
|
122
|
+
return {
|
|
123
|
+
"hotspots": all_hotspots,
|
|
124
|
+
"paging": {
|
|
125
|
+
"pageIndex": 1,
|
|
126
|
+
"pageSize": len(all_hotspots),
|
|
127
|
+
"total": len(all_hotspots)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Main function to get all report data
|
|
132
|
+
def get_report_data(base_url: str, token: str,
|
|
133
|
+
project_key: str, verbose: bool = False) -> ReportData:
|
|
134
|
+
"""Main function that fetches all necessary data from SonarQube API"""
|
|
135
|
+
metric_keys = [
|
|
136
|
+
"software_quality_security_rating",
|
|
137
|
+
"software_quality_reliability_rating",
|
|
138
|
+
"software_quality_maintainability_rating",
|
|
139
|
+
"lines_to_cover",
|
|
140
|
+
"software_quality_maintainability_issues",
|
|
141
|
+
"software_quality_security_issues",
|
|
142
|
+
"software_quality_reliability_issues",
|
|
143
|
+
"accepted_issues",
|
|
144
|
+
"coverage",
|
|
145
|
+
"duplicated_lines_density",
|
|
146
|
+
"lines",
|
|
147
|
+
"security_hotspots",
|
|
148
|
+
]
|
|
149
|
+
metrics_param = ",".join(metric_keys)
|
|
150
|
+
|
|
151
|
+
component_url = f"{base_url}/api/components/show?component={project_key}"
|
|
152
|
+
issues_url = f"{base_url}/api/issues/search?componentKeys={project_key}&ps=500"
|
|
153
|
+
measures_url = f"{base_url}/api/measures/component?component={project_key}&metricKeys={metrics_param}" # pylint: disable=line-too-long
|
|
154
|
+
settings_url = f"{base_url}/api/settings/values?keys=sonar.multi-quality-mode.enabled"
|
|
155
|
+
|
|
156
|
+
component_data = fetch("project component data...", component_url, token, verbose)
|
|
157
|
+
issues_data = fetch("issues data...", issues_url, token, verbose)
|
|
158
|
+
measures_data = fetch("measures data...", measures_url, token, verbose)
|
|
159
|
+
settings_data = fetch("SonarQube settings...", settings_url, token, verbose)
|
|
160
|
+
hotspots_data = fetch_all_hotspots(base_url, token, project_key)
|
|
161
|
+
|
|
162
|
+
project = SonarQubeProject.from_dict(component_data)
|
|
163
|
+
|
|
164
|
+
# Process issues and fetch code snippets
|
|
165
|
+
issues: List[SonarQubeIssue] = []
|
|
166
|
+
total_issues = len(issues_data.get("issues", []))
|
|
167
|
+
|
|
168
|
+
log(verbose, f"Processing {total_issues} issues and fetching code snippets...")
|
|
169
|
+
|
|
170
|
+
for i, issue_data in enumerate(issues_data.get("issues", [])):
|
|
171
|
+
issue = SonarQubeIssue.from_dict(issue_data)
|
|
172
|
+
|
|
173
|
+
if (i + 1) % 10 == 0: # Progress every 10 issues
|
|
174
|
+
log(verbose, f" Processed {i + 1}/{total_issues} issues...")
|
|
175
|
+
|
|
176
|
+
# Fetch code snippet if the issue has a line number
|
|
177
|
+
if issue.line:
|
|
178
|
+
|
|
179
|
+
code_snippet = get_code_snippet(base_url, token, issue.component, issue.line)
|
|
180
|
+
|
|
181
|
+
if not code_snippet.strip():
|
|
182
|
+
code_snippet = "No code snippet available for this issue."
|
|
183
|
+
|
|
184
|
+
issue.code_snippet = code_snippet
|
|
185
|
+
else:
|
|
186
|
+
print(f"ERROR: Issue {issue.key} has no line number, skipping code snippet")
|
|
187
|
+
|
|
188
|
+
issues.append(issue)
|
|
189
|
+
|
|
190
|
+
measures: Dict[str, SonarQubeMeasure] = {
|
|
191
|
+
m["metric"]: SonarQubeMeasure.from_dict(m)
|
|
192
|
+
for m in measures_data.get("component", {}).get("measures", [])
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Process hotspots and fetch code snippets
|
|
196
|
+
hotspots: List[SonarQubeHotspot] = []
|
|
197
|
+
total_hotspots = len(hotspots_data.get("hotspots", []))
|
|
198
|
+
|
|
199
|
+
log(verbose, f"Processing {total_hotspots} security hotspots and fetching code snippets...")
|
|
200
|
+
|
|
201
|
+
for i, hotspot_data in enumerate(hotspots_data.get("hotspots", [])):
|
|
202
|
+
hotspot = SonarQubeHotspot.from_dict(hotspot_data)
|
|
203
|
+
|
|
204
|
+
if (i + 1) % 10 == 0: # Progress every 10 hotspots
|
|
205
|
+
log(verbose, f" Processed {i + 1}/{total_hotspots} hotspots...")
|
|
206
|
+
|
|
207
|
+
# Fetch code snippet if the hotspot has a line number
|
|
208
|
+
if hotspot.line:
|
|
209
|
+
code_snippet = get_code_snippet(base_url, token, hotspot.component, hotspot.line)
|
|
210
|
+
|
|
211
|
+
if not code_snippet.strip():
|
|
212
|
+
code_snippet = "No code snippet available for this hotspot."
|
|
213
|
+
|
|
214
|
+
hotspot.code_snippet = code_snippet
|
|
215
|
+
else:
|
|
216
|
+
log(verbose, f"Hotspot {hotspot.key} has no line number, skipping code snippet")
|
|
217
|
+
|
|
218
|
+
hotspots.append(hotspot)
|
|
219
|
+
|
|
220
|
+
# Collect unique rule keys from issues and hotspots
|
|
221
|
+
rule_keys = set()
|
|
222
|
+
for issue in issues:
|
|
223
|
+
if issue.rule:
|
|
224
|
+
rule_keys.add(issue.rule)
|
|
225
|
+
for hotspot in hotspots:
|
|
226
|
+
if hotspot.rule_key:
|
|
227
|
+
rule_keys.add(hotspot.rule_key)
|
|
228
|
+
|
|
229
|
+
log(verbose, f"📋 Fetching descriptions for {len(rule_keys)} unique rules...")
|
|
230
|
+
|
|
231
|
+
# Fetch rule descriptions
|
|
232
|
+
rules = get_rules(base_url, token, list(rule_keys), verbose)
|
|
233
|
+
|
|
234
|
+
settings: bool = settings_data.get("sonar.multi-quality-mode.enabled",
|
|
235
|
+
{}).get("value", "true").lower() == "true"
|
|
236
|
+
|
|
237
|
+
log(verbose, "Data collection summary:")
|
|
238
|
+
log(verbose, f" • Project: {project.name}")
|
|
239
|
+
log(verbose, f" • Issues collected: {len(issues)}")
|
|
240
|
+
log(verbose, f" • Hotspots collected: {len(hotspots)}")
|
|
241
|
+
log(verbose, f" • Measures collected: {len(measures)}")
|
|
242
|
+
log(verbose, f" • MQR mode enabled: {settings}")
|
|
243
|
+
log(verbose, f" • Issues with code snippets: {sum(1 for i in issues if i.code_snippet and i.code_snippet.strip())}") # pylint: disable=line-too-long
|
|
244
|
+
log(verbose, f" • Hotspots with code snippets: {sum(1 for h in hotspots if h.code_snippet and h.code_snippet.strip())}") # pylint: disable=line-too-long
|
|
245
|
+
log(verbose, f" • Rules descriptions fetched: {len(rules)}")
|
|
246
|
+
|
|
247
|
+
return ReportData(
|
|
248
|
+
project=project,
|
|
249
|
+
issues=issues,
|
|
250
|
+
measures=measures,
|
|
251
|
+
hotspots=hotspots,
|
|
252
|
+
quality_gate={},
|
|
253
|
+
quality_profiles=[],
|
|
254
|
+
mode_setting=settings,
|
|
255
|
+
rules=rules
|
|
256
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Data models for ReflectSonar - SonarQube entity definitions"""
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Defines data models for SonarQube entities such as issues, rules, hotspots, etc.
|
|
3
|
+
"""
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import List, Dict, Any, Optional
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class SonarQubeIssue:
|
|
10
|
+
"""Data model for a SonarQube issue"""
|
|
11
|
+
key: str
|
|
12
|
+
component: str
|
|
13
|
+
project: str
|
|
14
|
+
rule: str
|
|
15
|
+
severity: str
|
|
16
|
+
status: str
|
|
17
|
+
message: str
|
|
18
|
+
type: str
|
|
19
|
+
line: Optional[int] = None
|
|
20
|
+
effort: Optional[str] = None
|
|
21
|
+
author: Optional[str] = None
|
|
22
|
+
tags: List[str] = field(default_factory=list)
|
|
23
|
+
creation_date: Optional[datetime] = None
|
|
24
|
+
update_date: Optional[datetime] = None
|
|
25
|
+
impacts: List[Dict[str, Any]] = field(default_factory=list)
|
|
26
|
+
code_snippet: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'SonarQubeIssue':
|
|
30
|
+
"""Creates a SonarQubeIssue instance from a dictionary"""
|
|
31
|
+
return cls(
|
|
32
|
+
key=data.get('key', ''),
|
|
33
|
+
component=data.get('component', ''),
|
|
34
|
+
project=data.get('project', ''),
|
|
35
|
+
rule=data.get('rule', ''),
|
|
36
|
+
severity=data.get('severity', ''),
|
|
37
|
+
status=data.get('status', ''),
|
|
38
|
+
message=data.get('message', ''),
|
|
39
|
+
type=data.get('type', ''),
|
|
40
|
+
line=data.get('line'),
|
|
41
|
+
effort=data.get('effort'),
|
|
42
|
+
author=data.get('author'),
|
|
43
|
+
tags=data.get('tags', []),
|
|
44
|
+
creation_date=data.get('creationDate')
|
|
45
|
+
if data.get('creationDate') else None,
|
|
46
|
+
update_date=data.get('updateDate')
|
|
47
|
+
if data.get('updateDate') else None,
|
|
48
|
+
impacts=data.get('impacts', [])
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class SonarQubeRule:
|
|
53
|
+
"""Data model for a SonarQube rule"""
|
|
54
|
+
key: str
|
|
55
|
+
name: str
|
|
56
|
+
description_sections: List[Dict[str, Any]] = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'SonarQubeRule':
|
|
60
|
+
"""Creates a SonarQubeRule instance from a dictionary"""
|
|
61
|
+
return cls(
|
|
62
|
+
key=data.get('key', ''),
|
|
63
|
+
name=data.get('name', ''),
|
|
64
|
+
description_sections=data.get('descriptionSections', [])
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class SonarQubeMeasure:
|
|
69
|
+
"""Data model for a SonarQube measure"""
|
|
70
|
+
metric: str
|
|
71
|
+
value: Any
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'SonarQubeMeasure':
|
|
75
|
+
"""Creates a SonarQubeMeasure instance from a dictionary"""
|
|
76
|
+
return cls(
|
|
77
|
+
metric=data.get('metric', ''),
|
|
78
|
+
value=data.get('value') or data.get('period', {}).get('value')
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class SonarQubeProject:
|
|
83
|
+
"""Data model for a SonarQube project"""
|
|
84
|
+
key: str
|
|
85
|
+
name: str
|
|
86
|
+
qualifier: str
|
|
87
|
+
visibility: str
|
|
88
|
+
last_analysis_date: Optional[datetime] = None
|
|
89
|
+
revision: Optional[str] = None
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'SonarQubeProject':
|
|
93
|
+
"""Creates a SonarQubeProject instance from a dictionary"""
|
|
94
|
+
component = data.get('component', {})
|
|
95
|
+
return cls(
|
|
96
|
+
key=component.get('key', ''),
|
|
97
|
+
name=component.get('name', ''),
|
|
98
|
+
qualifier=component.get('qualifier', ''),
|
|
99
|
+
visibility=component.get('visibility', ''),
|
|
100
|
+
last_analysis_date=component.get('analysisDate')
|
|
101
|
+
if component.get('analysisDate') else None,
|
|
102
|
+
revision=component.get('revision')
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class SonarQubeHotspot:
|
|
107
|
+
"""Data model for a SonarQube security hotspot"""
|
|
108
|
+
key: str
|
|
109
|
+
component: str
|
|
110
|
+
project: str
|
|
111
|
+
rule: str
|
|
112
|
+
status: str
|
|
113
|
+
message: str
|
|
114
|
+
line: Optional[int] = None
|
|
115
|
+
author: Optional[str] = None
|
|
116
|
+
creation_date: Optional[datetime] = None
|
|
117
|
+
update_date: Optional[datetime] = None
|
|
118
|
+
vulnerability_probability: str = "MEDIUM"
|
|
119
|
+
code_snippet: Optional[str] = None
|
|
120
|
+
security_category: Optional[str] = None
|
|
121
|
+
rule_key: Optional[str] = None
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'SonarQubeHotspot':
|
|
125
|
+
"""Creates a SonarQubeHotspot instance from a dictionary"""
|
|
126
|
+
return cls(
|
|
127
|
+
key=data.get('key', ''),
|
|
128
|
+
component=data.get('component', ''),
|
|
129
|
+
project=data.get('project', ''),
|
|
130
|
+
rule=data.get('rule', ''),
|
|
131
|
+
status=data.get('status', ''),
|
|
132
|
+
message=data.get('message', ''),
|
|
133
|
+
line=data.get('line'),
|
|
134
|
+
author=data.get('author'),
|
|
135
|
+
creation_date=data.get('creationDate')
|
|
136
|
+
if data.get('creationDate') else None,
|
|
137
|
+
update_date=data.get('updateDate')
|
|
138
|
+
if data.get('updateDate') else None,
|
|
139
|
+
vulnerability_probability=data.get('vulnerabilityProbability', 'MEDIUM'),
|
|
140
|
+
security_category=data.get('securityCategory'),
|
|
141
|
+
rule_key=data.get('ruleKey')
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class ReportData:
|
|
146
|
+
"""Data model for report data"""
|
|
147
|
+
project: SonarQubeProject
|
|
148
|
+
issues: List[SonarQubeIssue]
|
|
149
|
+
measures: Dict[str, SonarQubeMeasure]
|
|
150
|
+
hotspots: List[SonarQubeHotspot]
|
|
151
|
+
quality_gate: Dict[str, Any]
|
|
152
|
+
quality_profiles: List[Dict[str, Any]]
|
|
153
|
+
mode_setting: bool
|
|
154
|
+
rules: Dict[str, SonarQubeRule] = field(default_factory=dict)
|
reflectsonar/main.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main entry point for the ReflectSonar PDF report generator.
|
|
3
|
+
Handles command-line arguments, configuration loading,
|
|
4
|
+
and orchestrates the report generation process.
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
import signal
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from .report.pdfgen import generate_pdf
|
|
12
|
+
from .report.utils import log, handle_exception
|
|
13
|
+
from .api.get_data import get_report_data
|
|
14
|
+
|
|
15
|
+
def load_config(config_path):
|
|
16
|
+
"""Loads configuration from a YAML file"""
|
|
17
|
+
if not config_path:
|
|
18
|
+
return {}
|
|
19
|
+
try:
|
|
20
|
+
with open(config_path, 'r', encoding="utf-8") as f:
|
|
21
|
+
return yaml.safe_load(f) or {}
|
|
22
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
23
|
+
print(f"Warning: Could not load config file {config_path}: {e}")
|
|
24
|
+
return {}
|
|
25
|
+
|
|
26
|
+
def parse_arguments():
|
|
27
|
+
"""Parses command-line arguments and loads configuration from a YAML file if provided"""
|
|
28
|
+
parser = argparse.ArgumentParser(description='Generate PDF reports from SonarQube data')
|
|
29
|
+
parser.add_argument('-c', '--config', default=None,
|
|
30
|
+
help='Path to YAML configuration file, overrides arguments if provided in the file')
|
|
31
|
+
parser.add_argument('-o', '--output', help='Output PDF file path')
|
|
32
|
+
parser.add_argument('-p', '--project', help='SonarQube project key')
|
|
33
|
+
parser.add_argument('-u', '--url', help='SonarQube server URL', default="http://localhost:9000")
|
|
34
|
+
parser.add_argument('-t', '--token', help='SonarQube authentication token')
|
|
35
|
+
parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose logging')
|
|
36
|
+
|
|
37
|
+
args = parser.parse_args()
|
|
38
|
+
|
|
39
|
+
# Load config file and override args if config values exist
|
|
40
|
+
if args.config:
|
|
41
|
+
config = load_config(args.config)
|
|
42
|
+
if 'project' in config:
|
|
43
|
+
args.project = config['project']
|
|
44
|
+
if 'token' in config:
|
|
45
|
+
args.token = config['token']
|
|
46
|
+
if 'url' in config:
|
|
47
|
+
args.url = config['url']
|
|
48
|
+
if 'output' in config:
|
|
49
|
+
args.output = config['output']
|
|
50
|
+
if 'verbose' in config:
|
|
51
|
+
args.verbose = config['verbose']
|
|
52
|
+
|
|
53
|
+
# Validate required fields
|
|
54
|
+
if not args.project:
|
|
55
|
+
parser.error('Project key is required (use -p or set in config file)')
|
|
56
|
+
if not args.token:
|
|
57
|
+
parser.error('SonarQube token is required (use -t or set in config file)')
|
|
58
|
+
|
|
59
|
+
return args
|
|
60
|
+
|
|
61
|
+
def handle_interrupt(signum, frame): # pylint: disable=unused-argument
|
|
62
|
+
"""Handles keyboard interrupt (Ctrl+C)"""
|
|
63
|
+
print("\n")
|
|
64
|
+
print("🛑 Report generation interrupted by user")
|
|
65
|
+
print("✨ Thanks for using ReflectSonar!")
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
# Main function to generate the PDF report
|
|
69
|
+
def main():
|
|
70
|
+
"""Main function to generate the PDF report"""
|
|
71
|
+
signal.signal(signal.SIGINT, handle_interrupt)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
args = parse_arguments()
|
|
75
|
+
|
|
76
|
+
log(args.verbose, "🚀 Starting ReflectSonar PDF Report Generation")
|
|
77
|
+
log(args.verbose, f"📊 Project: {args.project}")
|
|
78
|
+
log(args.verbose, f"🌐 SonarQube URL: {args.url}")
|
|
79
|
+
log(args.verbose, f"📄 Output: {args.output or f'reflect_sonar_report_{args.project}_[timestamp].pdf'}") # pylint: disable=line-too-long
|
|
80
|
+
|
|
81
|
+
# Fetch data from SonarQube
|
|
82
|
+
log(args.verbose, "📡 Connecting to SonarQube and fetching data...")
|
|
83
|
+
print("📡 Fetching data from SonarQube... (Press Ctrl+C to cancel)")
|
|
84
|
+
|
|
85
|
+
report_data = get_report_data(args.url, args.token, args.project, verbose=args.verbose)
|
|
86
|
+
|
|
87
|
+
print("📄 Generating PDF report... (Press Ctrl+C to cancel)")
|
|
88
|
+
|
|
89
|
+
output_file = generate_pdf(report_data, args.output, args.project, verbose=args.verbose)
|
|
90
|
+
|
|
91
|
+
# Success message
|
|
92
|
+
print("✅ PDF report generated successfully!")
|
|
93
|
+
print(f"📁 Saved to: {output_file}")
|
|
94
|
+
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
98
|
+
return handle_exception(e, args.verbose)
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Report generation module for ReflectSonar - PDF creation and formatting"""
|