Skip to content

Commit b0527d1

Browse files
authored
Merge pull request #26 from Andrei-Constantin-Programmer/IBM-35-SonarQube-Improvements
IBM-35: SonarQube integration: update token handling in main, add Sonar…
2 parents c95b451 + 4a5b6a4 commit b0527d1

File tree

6 files changed

+239
-8
lines changed

6 files changed

+239
-8
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ docker-compose.override.yml
5757
# Intermediate files/output
5858
repos.txt
5959
clones/
60+
jacoco_results/
6061

6162
# Binary files
6263
*.bin

remediate_repos.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@ def _resolve_path(base_dir: str, path: str) -> str:
1414
def main():
1515
parser = argparse.ArgumentParser(description="Run SonarQube on a list of repositories.")
1616
parser.add_argument(
17-
"token",
18-
help="SonarQube token. To get this: run Docker on your PC, then: `docker run -d --name sonarqube -p 9000:9000 sonarqube:community`. Log into 'http://localhost:9000', then My Account > Security, and generate a global analysis token to use."
17+
"--token",
18+
help="""
19+
SonarQube token. If not provided, will read from SONARQUBE_TOKEN environment variable.
20+
To get this: run Docker on your PC, then: `docker run -d --name sonarqube -p 9000:9000 sonarqube:community`.
21+
Use "admin" as the username and the password you set during the setup.
22+
Log into 'http://localhost:9000', then My Account > Security, and generate a "User Token" type token to use.
23+
It will have the necessary permissions for analysis.
24+
"""
1925
)
2026
parser.add_argument(
2127
"--repos",
@@ -35,13 +41,19 @@ def main():
3541
)
3642
args = parser.parse_args()
3743

44+
# Get token from argument or environment variable
45+
token = args.token or os.getenv('SONARQUBE_TOKEN')
46+
if not token:
47+
print("Error: SonarQube token is required. Provide it via --token argument or set SONARQUBE_TOKEN environment variable.")
48+
return
49+
3850
base_dir = os.path.dirname(os.path.abspath(__file__))
3951

4052
repos_file = _resolve_path(base_dir, args.repos)
4153
clone_dir = _resolve_path(base_dir, args.clone_dir)
4254

4355
clone_repos_from_file(repos_file, clone_dir, post_pull_hook=delete_sonarqube_output_if_updated)
44-
scan_repos(args.token, clone_dir, args.force_scan)
56+
scan_repos(token, clone_dir, args.force_scan)
4557

4658

4759
if __name__ == "__main__":

sonarqube_tool/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .sonarqube_api import SonarQubeAPI
2+
3+
__all__ = [
4+
'SonarQubeAPI'
5+
]

sonarqube_tool/scan_repos.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import platform
33
import shutil
44
import subprocess
5+
from sonarqube_tool.sonarqube_api import SonarQubeAPI
56

67
SONARQUBE_URL = "http://localhost:9000"
78
SONARQUBE_FILE_NAME = "sonarqube_output.txt"
@@ -28,14 +29,20 @@ def _compile_java_sources(repo_dir):
2829
Compile Java sources using Maven, supporting both Windows and Linux.
2930
Skips Apache RAT license checking with -Drat.skip=true.
3031
"""
31-
print(f"Compiling Java sources for {repo_dir.name}...")
32-
3332
# Check if it's a Maven project
3433
pom_file = repo_dir / "pom.xml"
3534
if not pom_file.exists():
3635
print(f"No pom.xml found in {repo_dir.name}, skipping compilation.")
3736
return
3837

38+
# Check if already compiled (target directory exists)
39+
target_dir = repo_dir / "target"
40+
if target_dir.exists():
41+
print(f"Target directory already exists for {repo_dir.name}, skipping compilation.")
42+
return
43+
44+
print(f"Compiling Java sources for {repo_dir.name}...")
45+
3946
try:
4047
# Determine the Maven command based on platform
4148
if platform.system() == "Windows":
@@ -91,6 +98,11 @@ def _scan_repo(repo_dir, token, force_scan):
9198
)
9299
output_file.write_text(result.stdout)
93100
print(f"Scan complete. Output saved to {output_file}")
101+
api = SonarQubeAPI(token=token)
102+
if api.is_scan_successful(project_key):
103+
issues_path = repo_dir / "issues.json"
104+
api.save_all_issues(project_key, issues_path)
105+
print(f"All issues saved for {repo_dir.name}.")
94106
except subprocess.CalledProcessError as e:
95107
print(f"SonarQube scan failed for {repo_dir.name}: {e}")
96108
output_file.write_text(e.stdout or "No output captured.")

sonarqube_tool/sonarqube_api.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import os
2+
import json
3+
import requests
4+
from typing import Optional, Dict
5+
6+
SONARQUBE_URL = "http://localhost:9000"
7+
PAGE_SIZE = 500
8+
9+
class SonarQubeAPI:
10+
def __init__(self, base_url: str = SONARQUBE_URL, token: Optional[str] = None):
11+
self.base_url = base_url.rstrip('/')
12+
self.token = token or os.getenv('SONARQUBE_TOKEN')
13+
14+
if not self.token:
15+
raise ValueError("SonarQube token is required. Provide it as parameter or set SONARQUBE_TOKEN environment variable.")
16+
17+
self.headers = {
18+
'Authorization': f'Bearer {self.token}',
19+
'Content-Type': 'application/json'
20+
}
21+
22+
def _get_issues(self, component_key: str) -> Dict:
23+
url = f"{self.base_url}/api/issues/search"
24+
all_issues = []
25+
page = 1
26+
27+
while True:
28+
params = {
29+
'componentKeys': component_key,
30+
'ps': PAGE_SIZE,
31+
'p': page
32+
}
33+
try:
34+
response = requests.get(url, headers=self.headers, params=params)
35+
response.raise_for_status()
36+
data = response.json()
37+
38+
page_issues = data.get('issues', [])
39+
all_issues.extend(page_issues)
40+
41+
if len(page_issues) == 0:
42+
break
43+
page += 1
44+
45+
except requests.exceptions.RequestException as e:
46+
print(f"Error calling SonarQube API: {e}")
47+
if hasattr(e, 'response') and e.response is not None:
48+
print(f"Response status: {e.response.status_code}")
49+
print(f"Response text: {e.response.text}")
50+
raise
51+
return {
52+
'total': len(all_issues),
53+
'issues': all_issues
54+
}
55+
56+
def _get_rule_details(self, rule_key: str) -> Dict:
57+
url = f"{self.base_url}/api/rules/show"
58+
params = {
59+
'key': rule_key
60+
}
61+
try:
62+
response = requests.get(url, headers=self.headers, params=params)
63+
response.raise_for_status()
64+
data = response.json()
65+
except requests.exceptions.RequestException as e:
66+
print(f"Error calling SonarQube API: {e}")
67+
if hasattr(e, 'response') and e.response is not None:
68+
print(f"Response status: {e.response.status_code}")
69+
print(f"Response text: {e.response.text}")
70+
raise
71+
return data
72+
73+
def is_scan_successful(self, project_key: str) -> bool:
74+
url = f"{self.base_url}/api/ce/component"
75+
params = {
76+
'component': project_key
77+
}
78+
try:
79+
response = requests.get(url, headers=self.headers, params=params)
80+
response.raise_for_status()
81+
data = response.json()
82+
info = data.get('current', {})
83+
result = info.get('status', '') == 'SUCCESS'
84+
return result
85+
except Exception as e:
86+
print(f"Error: {e}")
87+
return False
88+
89+
def get_issues_for_file(self, project_key: str, file_path: str) -> Dict:
90+
component_key = f"{project_key}:{file_path}"
91+
return self._get_issues(component_key)
92+
93+
def get_all_issues(self, project_key: str) -> Dict:
94+
return self._get_issues(project_key)
95+
96+
def get_rules_and_fix_method(self, rule_key: str) -> Dict:
97+
data = self._get_rule_details(rule_key)
98+
result = {}
99+
rule = data.get('rule', {})
100+
if rule:
101+
result['rule_key'] = rule.get('key', '')
102+
result['rule_name'] = rule.get('name', '')
103+
result['severity'] = rule.get('severity', '')
104+
result['type'] = rule.get('type', '')
105+
106+
description_sections = rule.get('descriptionSections', [])
107+
for section in description_sections:
108+
section_key = section.get('key', '')
109+
section_content = section.get('content', '')
110+
result[section_key] = section_content
111+
return result
112+
113+
def print_all_issues(self, project_key: str) -> None:
114+
try:
115+
issues_data = self.get_all_issues(project_key)
116+
issues = issues_data.get('issues', [])
117+
total = issues_data.get('total', 0)
118+
119+
severity_count = {}
120+
for issue in issues:
121+
impacts = issue.get('impacts', [])
122+
if impacts and len(impacts) > 0:
123+
severity = impacts[0].get('severity', 'UNKNOWN')
124+
severity_count[severity] = severity_count.get(severity, 0) + 1
125+
print(f"Total issues: {total}")
126+
for severity, count in sorted(severity_count.items()):
127+
print(f"{severity}: {count}")
128+
except Exception as e:
129+
print(f"Error: {e}")
130+
131+
def print_file_issues(self, project_key: str, file_path: str) -> None:
132+
try:
133+
issues_data = self.get_issues_for_file(project_key, file_path)
134+
issues = issues_data.get('issues', [])
135+
136+
if issues:
137+
# Show only first 3 issues
138+
for i, issue in enumerate(issues[:3], 1):
139+
print(f"{i}. {issue.get('message', 'No message')}")
140+
print(f" Rule: {issue.get('rule', 'Unknown')}")
141+
142+
impacts = issue.get('impacts', [])
143+
severity = 'Unknown'
144+
if impacts and len(impacts) > 0:
145+
severity = impacts[0].get('severity', 'Unknown')
146+
147+
print(f" Severity: {severity}")
148+
print(f" Type: {issue.get('type', 'Unknown')}")
149+
150+
if 'textRange' in issue:
151+
line = issue['textRange'].get('startLine', 'Unknown')
152+
print(f" Line: {line}")
153+
print()
154+
# Show remaining count if there are more than 3 issues
155+
if len(issues) > 3:
156+
remaining = len(issues) - 3
157+
print(f"... and {remaining} more issues not shown")
158+
else:
159+
print("No issues found")
160+
except Exception as e:
161+
print(f"Error: {e}")
162+
163+
def save_all_issues(self, project_key: str, file_path: str) -> None:
164+
try:
165+
issues_data = self.get_all_issues(project_key)
166+
with open(file_path, 'w') as f:
167+
json.dump(issues_data, f, indent=4)
168+
print(f"All issues saved to {file_path}")
169+
except Exception as e:
170+
print(f"Error: {e}")
171+
172+
if __name__ == "__main__":
173+
api = SonarQubeAPI()
174+
project_key = "commons-collections"
175+
file_path = "src/main/java/org/apache/commons/collections4/map/AbstractHashedMap.java"
176+
rule_key_1 = "java:S2160"
177+
rule_key_2 = "java:S1117"
178+
rule_key_3 = "java:S5993"
179+
print(f"Checking scan success for project {project_key}: {api.is_scan_successful(project_key)}")
180+
181+
print("Project issues summary:")
182+
api.print_all_issues(project_key)
183+
184+
print(f"\nFile issues details:")
185+
api.print_file_issues(project_key, file_path)
186+
187+
print(f"\nRule details:")
188+
print(api.get_rules_and_fix_method(rule_key_3))

sonarqube_tool/tests/test_scan_repos.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ def mock_which():
2525
with patch("sonarqube_tool.scan_repos.shutil.which", return_value="/usr/bin/sonar-scanner"):
2626
yield
2727

28+
# Mock SonarQubeAPI to avoid requiring real tokens in tests
29+
@pytest.fixture
30+
def mock_sonarqube_api():
31+
with patch("sonarqube_tool.scan_repos.SonarQubeAPI") as mock_api:
32+
mock_instance = MagicMock()
33+
mock_instance.is_scan_successful.return_value = True
34+
mock_instance.save_all_issues.return_value = None
35+
mock_api.return_value = mock_instance
36+
yield mock_api
37+
2838

2939
def test_scan_repos_skips_if_sonar_scanner_missing(tmp_path):
3040
# Arrange
@@ -75,10 +85,11 @@ def test_scan_repos_skips_repo_if_output_exists(tmp_path, mock_which, mock_subpr
7585
mock_subprocess_success.assert_not_called()
7686

7787

78-
def test_scan_repos_runs_repo_and_writes_output(tmp_path, mock_which, mock_subprocess_success):
88+
def test_scan_repos_runs_repo_and_writes_output(tmp_path, mock_which, mock_subprocess_success, mock_sonarqube_api):
7989
# Arrange
8090
_ = mock_which
8191
_ = mock_subprocess_success
92+
_ = mock_sonarqube_api
8293
repo = tmp_path / "repo1"
8394
repo.mkdir()
8495

@@ -109,10 +120,11 @@ def test_scan_repos_handles_scan_failure(tmp_path, mock_which, mock_subprocess_f
109120
assert not (repo / PROPERTIES_FILE_NAME).exists()
110121

111122

112-
def test_scan_repos_runs_multiple_repos(tmp_path, mock_which, mock_subprocess_success):
123+
def test_scan_repos_runs_multiple_repos(tmp_path, mock_which, mock_subprocess_success, mock_sonarqube_api):
113124
# Arrange
114125
_ = mock_which
115126
_ = mock_subprocess_success
127+
_ = mock_sonarqube_api
116128
repo1 = tmp_path / "repo1"
117129
repo2 = tmp_path / "repo2"
118130
repo1.mkdir()
@@ -127,9 +139,10 @@ def test_scan_repos_runs_multiple_repos(tmp_path, mock_which, mock_subprocess_su
127139
assert "scan succeeded" in (repo / SONARQUBE_FILE_NAME).read_text()
128140

129141

130-
def test_scan_repos_runs_mixed_state_repos(tmp_path, mock_which):
142+
def test_scan_repos_runs_mixed_state_repos(tmp_path, mock_which, mock_sonarqube_api):
131143
# Arrange
132144
_ = mock_which
145+
_ = mock_sonarqube_api
133146
scanned = tmp_path / "scanned"
134147
unscanned = tmp_path / "success"
135148
failed = tmp_path / "failure"

0 commit comments

Comments
 (0)